做项目的时候,经常需要统一处理返回结果,特别是对异常的返回,前端希望返回的结果都是一致的格式,下面我们看看如何处理全局异常
参考https://blog.csdn.net/hao_kkkkk/article/details/80538955
基于@ControllerAdvice
基于@ControllerAdvice注解的Controller层的全局异常统一处理
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* controller 增强器
*/
@ControllerAdvice
public class MyControllerAdvice {
/**
* 全局异常捕捉处理
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Map errorHandler(Exception ex) {
Map map = new HashMap();
map.put("code", 100);
map.put("msg", ex.getMessage());
return map;
}
/**
* 拦截捕捉自定义异常 MyException.class
*/
@ResponseBody
@ExceptionHandler(value = MyException.class)
public Map myErrorHandler(MyException ex) {
Map map = new HashMap();
map.put("code", ex.getCode());
map.put("msg", ex.getMsg());
return map;
}
}
代码很简洁,亲测可以实现控制层和服务层的异常处理。
基于Springboot自身的全局异常统一处理
主要是实现ErrorController接口或者继承AbstractErrorController抽象类或者继承BasicErrorController类
/**
* @Author: gmy
* @Description: Springboot全局异常统一处理
*/
@RestController
@EnableConfigurationProperties({ServerProperties.class})
public class ExceptionController implements ErrorController {
private ErrorAttributes errorAttributes;
@Autowired
private ServerProperties serverProperties;
/**
* 初始化ExceptionController
*/
@Autowired
public ExceptionController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
@RequestMapping(value = "/error")
@ResponseBody
public APIResponse error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new APIResponse(APIResponse.FAIL,null,body.get("message").toString());
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request,
MediaType produces) {
ErrorProperties.IncludeStacktrace include = this.serverProperties.getError().getIncludeStacktrace();
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
return true;
}
if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}
/**
* 获取错误的信息
* @param request
* @param includeStackTrace
* @return
*/
private Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
/**
* 是否包含trace
* @param request
* @return
*/
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
/**
* 获取错误编码
* @param request
* @return
*/
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
/**
* 实现错误路径,暂时无用
* @return
*/
@Override
public String getErrorPath() {
return "";
}
}
经过测试,可以捕获到所有层面上的异常,当前前提仍然是没有对异常进行catch处理,否则这里也是捕获不到
以上为网络上常用的两种全局异常统一处理方案,经过实际测试发现都可以实现满足要求。
基于AOP
其实基于AOP也可以实现异常的全局处理,自己相应的做了测试发现也满足要求,相应的代码如下:
/**
* @Author: gmy
* @Description: 基于AOP的全局异常统一处理
* @Date: 2018/6/1
*/
@Component
@Aspect
public class ExceptionAspectController {
public static final Logger logger = LoggerFactory.getLogger(ExceptionAspectController.class);
@Pointcut("execution(* com.test.test.*.*(..))")//此处基于自身项目的路径做具体的设置
public void pointCut(){}
@Around("pointCut()")
public Object handleControllerMethod(ProceedingJoinPoint pjp) {
Stopwatch stopwatch = Stopwatch.createStarted();
APIResponse<?> apiResponse;
try {
logger.info("执行Controller开始: " + pjp.getSignature() + " 参数:" + Lists.newArrayList(pjp.getArgs()).toString());
apiResponse = (APIResponse<?>) pjp.proceed(pjp.getArgs());
logger.info("执行Controller结束: " + pjp.getSignature() + ", 返回值:" + apiResponse.toString());
logger.info("耗时:" + stopwatch.stop().elapsed(TimeUnit.MILLISECONDS) + "(毫秒).");
} catch (Throwable throwable) {
apiResponse = handlerException(pjp, throwable);
}
return apiResponse;
}
private APIResponse<?> handlerException(ProceedingJoinPoint pjp, Throwable e) {
APIResponse<?> apiResponse = null;
if(e.getClass().isAssignableFrom(MessageCenterException.class) ){
MessageCenterException messageCenterException = (MessageCenterException)e;
logger.error("RuntimeException{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + messageCenterException.getException().getMessage() + "}", e);
apiResponse = messageCenterException.getApiResponse();
} else if (e instanceof RuntimeException) {
logger.error("RuntimeException{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e);
apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());
} else {
logger.error("异常{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e);
apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());
}
return apiResponse;
}
}
经过测试,在执行切点中配置的路径中的方法有异常时,可以被这里捕获到。
现在项目里已经统一使用AOP方式来做全局异常统一处理了,选用AOP方式主要是因为AOP不只可以做全局异常统一处理还可以统一打印接口请求入参和返回结果日志,打印接口访问性能日志,处理sql注入攻击以及处理入参特殊字符等问题
下面贴出代码,供大家参考,也仅供参考
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @Author: gmy
* @Description: 调用接口打印性能日志以及接口报错之后记录错误日志
* @Date: 2018/9/20
*/
@Component
@Aspect
public class InterfaceRequestErrrorAndPerformanceLog {
public static final Logger logger = LoggerFactory.getLogger(InterfaceRequestErrrorAndPerformanceLog.class);
@Value("${dc.log.bad.value:3000}")
private int performanceBadValue;
@Resource
private RabbitMQService rabbitMQService;
@Resource
private InterfaceErrorService interfaceErrorService;
@Pointcut("execution(* test.test.test.test.test.controller.*.*.*(..))")
public void pointCut(){}
@Around("pointCut()")
public APIResponse handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable{
Stopwatch stopwatch = Stopwatch.createStarted();
APIResponse apiResponse;
try {
logger.info("执行Controller开始: " + pjp.getSignature() + " 参数:" + Lists.newArrayList(pjp.getArgs()).toString());
//处理入参特殊字符和sql注入攻击
checkRequestParam(pjp);
//执行访问接口操作
apiResponse = (APIResponse) pjp.proceed(pjp.getArgs());
try{
logger.info("执行Controller结束: " + pjp.getSignature() + ", 返回值:" + JSONObject.toJSONString(apiResponse));
//此处将日志打印放入try-catch是因为项目中有些对象实体bean过于复杂,导致序列化为json的时候报错,但是此处报错并不影响主要功能使用,只是返回结果日志没有打印,所以catch中也不做抛出异常处理
}catch (Exception ex){
logger.error(pjp.getSignature()+" 接口记录返回结果失败!,原因为:{}",ex.getMessage());
}
Long consumeTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS);
logger.info("耗时:" + consumeTime + "(毫秒).");
//当接口请求时间大于3秒时,标记为异常调用时间,并记录入库
if(consumeTime > performanceBadValue){
DcPerformanceEntity dcPerformanceEntity = new DcPerformanceEntity();
dcPerformanceEntity.setInterfaceName(pjp.getSignature().toString());
dcPerformanceEntity.setRequestParam(Lists.newArrayList(pjp.getArgs()).toString());
dcPerformanceEntity.setConsumeTime(consumeTime + "毫秒");
RabbitMQMessageTarget mqTarget = RabbitMQMessageTarget.createFanoutTarget(ProjectConstants.DC_KEY_EXCHANGE_PERFORMANCE, new String[] { ProjectConstants.DC_KEY_QUEUE_PERFORMANCE});
rabbitMQService.send(mqTarget, JSON.toJSONString(dcPerformanceEntity));
}
} catch (Exception throwable) {
apiResponse = handlerException(pjp, throwable);
}
return apiResponse;
}
/**
* @Author: gmy
* @Description: 处理接口调用异常
* @Date: 15:13 2018/10/25
*/
private APIResponse handlerException(ProceedingJoinPoint pjp, Throwable e) {
APIResponse apiResponse;
if(e.getClass().isAssignableFrom(ProjectException.class) ){
//ProjectException为自定义异常类,项目中Controller层会把所有的异常都catch掉,并手工封装成ProjectException抛出来,这样做的目的是ProjectException会记录抛出异常接口的路径,名称以及请求参数等等,有助于错误排查
ProjectException projectException = (ProjectException)e;
logger.error("捕获到ProjectException异常:",JSONObject.toJSONString(projectException.getDcErrorEntity()));
RabbitMQMessageTarget mqTarget = RabbitMQMessageTarget.createFanoutTarget(ProjectConstants.DC_KEY_EXCHANGE_INTERFACE_ERROR, new String[] { ProjectConstants.DC_KEY_QUEUE_INTERFACE_ERROR});
rabbitMQService.send(mqTarget, JSON.toJSONString(dataCenterException.getDcErrorEntity()));
apiResponse = new APIResponse(APIResponse.FAIL,null,projectException.getDcErrorEntity().getErrorMessage());
} else if (e instanceof RuntimeException) {
logger.error("RuntimeException{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e);
apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());
} else {
logger.error("异常{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e);
apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());
}
return apiResponse;
}
/**
* @Author: gmy
* @Description: 处理入参特殊字符和sql注入攻击
* @Date: 15:37 2018/10/25
*/
private void checkRequestParam(ProceedingJoinPoint pjp){
String str = String.valueOf(pjp.getArgs());
if (!IllegalStrFilterUtil.sqlStrFilter(str)) {
logger.info("访问接口:" + pjp.getSignature() + ",输入参数存在SQL注入风险!参数为:" + Lists.newArrayList(pjp.getArgs()).toString());
DcErrorEntity dcErrorEntity = interfaceErrorService.processDcErrorEntity(pjp.getSignature() + "",Lists.newArrayList(pjp.getArgs()).toString(),"输入参数存在SQL注入风险!");
throw new DataCenterException(dcErrorEntity);
}
if (!IllegalStrFilterUtil.isIllegalStr(str)) {
logger.info("访问接口:" + pjp.getSignature() + ",输入参数含有非法字符!,参数为:" + Lists.newArrayList(pjp.getArgs()).toString());
DcErrorEntity dcErrorEntity = interfaceErrorService.processDcErrorEntity(pjp.getSignature() + "",Lists.newArrayList(pjp.getArgs()).toString(),"输入参数含有非法字符!");
throw new DataCenterException(dcErrorEntity);
}
}
}
代码中使用了一些其他的工具类,比如IllegalStrFilterUtil等
import org.slf4j.LoggerFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author: gmy
* @Description: 特殊字符检测工具(防止传入非法字符和sql注入攻击)
* @Date: 2018/10/25
*/
public class IllegalStrFilterUtil {
private static final org.slf4j.Logger Logger = LoggerFactory.getLogger(IllegalStrFilterUtil.class);
private static final String REGX = "!|!|@|◎|#|#|(\\$)|¥|%|%|(\\^)|……|(\\&)|※|(\\*)|×|(\\()|(|(\\))|)|_|——|(\\+)|+|(\\|)|§ ";
/**
* 对常见的sql注入攻击进行拦截
*
* @param sInput
* @return
* true 表示参数不存在SQL注入风险
* false 表示参数存在SQL注入风险
*/
public static Boolean sqlStrFilter(String sInput) {
if (sInput == null || sInput.trim().length() == 0) {
return false;
}
sInput = sInput.toUpperCase();
if (sInput.indexOf("DELETE") >= 0 || sInput.indexOf("ASCII") >= 0 || sInput.indexOf("UPDATE") >= 0 || sInput.indexOf("SELECT") >= 0
|| sInput.indexOf("'") >= 0 || sInput.indexOf("SUBSTR(") >= 0 || sInput.indexOf("COUNT(") >= 0 || sInput.indexOf(" OR ") >= 0
|| sInput.indexOf(" AND ") >= 0 || sInput.indexOf("DROP") >= 0 || sInput.indexOf("EXECUTE") >= 0 || sInput.indexOf("EXEC") >= 0
|| sInput.indexOf("TRUNCATE") >= 0 || sInput.indexOf("INTO") >= 0 || sInput.indexOf("DECLARE") >= 0 || sInput.indexOf("MASTER") >= 0) {
Logger.error("该参数怎么SQL注入风险:sInput=" + sInput);
return false;
}
Logger.info("通过sql检测");
return true;
}
/**
* 对非法字符进行检测
*
* @param sInput
* @return
* true 表示参数不包含非法字符
* false 表示参数包含非法字符
*/
public static Boolean isIllegalStr(String sInput) {
if (sInput == null || sInput.trim().length() == 0) {
return false;
}
sInput = sInput.trim();
Pattern compile = Pattern.compile(REGX, Pattern.CASE_INSENSITIVE);
Matcher matcher = compile.matcher(sInput);
Logger.info("通过字符串检测");
return matcher.find();
}
}
以上代码中涉及到真实项目信息的内容我都做了相应修改,代码仅供技术交流使用。
网友评论