目录
1 什么是全局异常处理器
2 为什么需要全局异常
3 原理和目标
4 @ControllerAdvice注解
4.1 Advice(通知)
4.2 @ControllerAdvice结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到全局不同类型的异常区别处理的目的。
4.3 结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的
4.4 结合方法型注解@ModelAttribute,表示其注解的方法将会在目标Controller方法执行之前执行
4.5 @ControllerAdvice注解作用原理
4.6 @RestControllerAdvice
5 编码实现全局异常处理器
软件开发springboot项目过程中,不可避免的需要处理各种异常,spring mvc架构中各层会出现大量的try{...} catch{...} finally{...}代码块,不仅有大量的冗余代码,而且还影响代码的可读性。这样就需要定义个全局统一异常处理器,以便业务层再也不必处理异常。
Spring在3.2版本增加了一个注解@ControllerAdvice,可以与@ExceptionHandler、@InitBinder、@ModelAtribute等注解配套使用。不过跟异常处理相关的只有注解@ExceptionHandler,从字面上看,就是异常处理器的意思。
简单的说,@ControllerAdvice注解可以把异常处理器应用到所有控制器,而不是单个控制器。借助该注解,我们可以实现:在独立的某个地方,比如单独的一个类,定义一套对各章异常的处理机制,然后在类的签名加上注解@ControllerAdvice,统一对不同阶段的,不同异常进行处理。这就是统一异常处理的原理。
对异常按阶段进行分类,大体可以分成:进入Controller前的异常和Service层异常
目标就是消灭95%以上的try catch代码块,并以优雅的Assert(断言)方式来校验业务的异常情况,只关注业务逻辑,而不用花费大量精力写冗余的try catch代码块。
@ControllerAdvice注解是Spring3.2中新增的注解,学名是Controller增强器,作用是给Controller控制器添加统一的操作或处理,对于@ControllerAdvice,我们比较熟悉的用法是结合@ExceptionHandler用于全局异常的处理,但其作用不止于此。ControllerAdvice拆开来就是Controller Advice,关于Advice,在Spring的AOP中,是用来封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。这里ControllerAdvice也可以这么理解。
Spring AOP通过PointCut来指定在哪些类的哪写方法上织入横切逻辑,通过Advice来指定在切点上具体做什么事情。如方法前做什么,方法后做什么,抛出异常做什么。再来看一下图
主要可分为5类增强:
前置增强:主要匹配到的切点运行之前执行,在XML配置中使用
在调用相应的切点方法之前,前置增强都会生效。
后置增强:弄明白了前置增强,后置增强也是同一个道理,不过后置增强是在切点运行后执行。接口为AfterReturningAdvice,在切点方法运行之后,后置增强会生效。
异常抛出增强:当切点方法抛出异常时,异常抛出增强才会被执行。其接口为ThrowAdvice,接口没有指定方法,实现这个接口的对象是通过反射来调用其增强方法的。
增强案例:根据前面的前置,后置,异常抛出增强,看一个完整的案例:
// TimeLoggingAop实现了前置增强,后置增强,异常环绕增强
public class TimeLoggingAop implements MethodBeforeAdvice,AfterReturningAdvice,ThrowsAdvice {
private long startTime = 0;
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
startTime = System.nanoTime();
}
@Override
public void afterReturning(Object returnValue, Method method, Object[] objects, Object target) throws Throwable {
long spentTime = System.nanoTime() - startTime;
String clazzName = target.getClass().getCanonicalName();
String methodName = method.getName();
System.out.println("执行" + clazzName + "#" + methodName + "消耗" + new BigDecimal(spentTime).divide(new BigDecimal(1000000)) + "毫秒");
}
public void afterThrowing(Method method, Object[] args, Object target, Exception ex){
System.out.println("执行" + method.getName() + "出现异常," + "异常消息为:" + ex.getMessage());
}
}
// 普通的HelloWorld对象
public class HelloWorld {
public void sayHello(){
System.out.println("hello");
}
public void sayHelloWithException(){
System.out.println("hello");
throw new RuntimeException("Hello World运行时出了一点问题");
}
}
public static void main(String[] args) {
//普通对象
HelloWorld helloWorld = new HelloWorld();
//增强对象
BeforeAdvice beforeAdvice = new TimeLoggingAop();
//组装普通对象和增强对象
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(helloWorld);
proxyFactory.addAdvice(beforeAdvice);
//获取组装后的代理对象
HelloWorld proxyHelloWorld = (HelloWorld) proxyFactory.getProxy();
//运行
proxyHelloWorld.sayHello();
proxyHelloWorld.sayHelloWithException();
}
//运行结果
hello
执行me.aihe.exam.controller.HelloWorld#sayHello消耗29.392268毫秒
hello
执行sayHelloWithException出现异常,异常消息为:Hello World运行时出了一点问题
在原本方法之上增加一些额外的东西,原本的功能增强了,所以叫增强,这是中文翻译过来的增强。英文名为Advice,建议,在方法周围建议方法做什么事情,然后真的做了。。。
@ExceptionHandler这个注解表示Controller中任何一个方法发生异常,则会被注解了@ExceptionHandler的方法拦截到。对应的异常类执行对应的方法,如果都没有匹配到异常类,则采用近亲匹配的方法
package com.liurq.config.shiro.handle;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.NativeWebRequest;
@ControllerAdvice
public class UnauthorizedExceptionHandle {
/**
* 可以获取请求的request对象,按需进行操作
* 可以获取抛出的异常对象,按需进行操作
* 可对请求进行重定向,将请求重定向到某个页面或控制器
* 需要返回json串儿的话,在方法上添加@ResponseBody注解即可
* @param request
* @param e
* @return
*/
// @ResponseBody
@ExceptionHandler({UnauthorizedException.class})
public String unauthorizedException(NativeWebRequest request, UnauthorizedException e){
System.out.println(request.getParameter("param1"));
e.printStackTrace();
return "redirect:/403";
}
}
用于请求中注册自定义参数的解析,从而达到自定义请求参数格式的目的。
@ControllerAdvice
public class MyControllerAdvice {
/**
* 转换前端传入的日期变量参数为指定格式。
*
* @param binder 数据绑定参数。
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class,
new CustomDateEditor(new SimpleDateFormat(MyDateUtil.COMMON_SHORT_DATETIME_FORMAT), false));
}
}
搭配@ModelAttribute注解可以做全局数据绑定,在到达控制器前,将属性放入modelMap中,可以通过name属性指定数据在数据在map中的key,如果不指定的话,有相同返回类型会产生覆盖,建议每个都通过name参数指定key。
我们看看Spring是怎么实现的,首先前端控制器DispatcherServlet对象在创建时会初始化一系列的对象:
public class DispatcherServlet extends FrameworkServlet{
//......
protected void initStrategies(ApplicationContext context){
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
//......
}
对于@ControllerAdvice注解,我们重点关注initHandlerAdapters(context)和initHandlerExceptionResolvers(context)这两个方法。
点进去看到@RestControllerAdvice是一个组合注解,是@ResponseBody和@ControllerAdvice的组合
而函数体中用到了@AliasFor注解,因此可以通过@RestControllerAdvice的属性传递将属性值传给@ControllerAdvice。
因此,在使用时可直接将包名传给@RestControllerAdvice来指定Controller范围
@ResponseBody:
这里做了这样的解释:
@responsebody这个注解表示你的返回值将存在responsebody中返回到前端,也就是将return返回值作为请求返回值,return的数据不会解析成返回跳转路径,将java对象转为json格式的数据,前端接收后会显示将数据到页面,如果不加的话 返回值将会作为url的一部分,页面会跳转到这个url,也就是跳转到你返回的这个路径。
@ResponseBody这个注解通常使用在控制层(controller)的方法上,其作用是将方法的返回值以特定的格式写入到response的body区域,进而将数据返回给客户端。当方法上面没有写ResponseBody,底层会将方法的返回值封装为ModelAndView对象。
@ResponseBody这个注解使用情景:当返回的数据不是html标签的页面,而是其他某种格式的数据时(如json、xml等)使用,常用在ajax异步请求中,可以通过 ajax 的“success”:fucntion(data){} data直接获取到。
@ResponseBody这个注解一般是作用在方法上的,加上该注解表示该方法的返回结果直接写到Http response Body中,在RequestMapping中 return返回值默认解析为跳转路径,如果你此时想让Controller返回一个字符串或者对象到前台。
/**
* 返回应答中的错误信息
* **/
public enum ErrorCodeEnum {
NO_ERROR("没有错误"), DATA_VALIDATED_FAILED("数据验证事白,请核对!"),
UNHANDLED_EXCEPTION("未处理的异常!");
ErrorCodeEnum(String errorMessage) { this.errorMessage = errorMessage; }
private final String errorMessage;
public String getErrorMessage(){
return errorMessage;
}
}
@Data
public class CallResult{
private static final CallResult OK = new CallResult();
private boolean success = true;
private String errorMessage = null;
private JSONObject data;
public static CallResult create(String errorMessage){
return errorMessage == null ? ok() : error(errorMessage);
}
public static CallResult ok() { return OK; }
public static CallResult ok(JSONObject data){
CallResult result = new CallResult();
result.data = data;
return result;
}
public static CallResult error(String errorMessage) {
CallResult result = new CallResult();
result.success = false;
result.errorMessage = errorMessage;
return result;
}
public static CallResult error(String errorMessage, T data){
CallResult result = new CallResult();
result.success = false;
result.errorMessge = errorMessage;
JSONObject jsonObject new JSONObject();
jsonObject.put("errorData", data);
result.data = jsonObject;
return result;
}
}
package com.****.demo.common;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
@RestControllerAdvice("com.****.demo")
public class MyExceptionHandler {
//@NotBlank抛出异常处理
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public CallResult bindExceptionHandle(MethodArgumentNotValidException ex){
String message = ex.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return CallResult.error(message);
}
}
public class User implements Serializable {
@ApiModelProperty(value = "名称")
@NotBlank(message = "name不能为空(哈哈,自定义哒)")
private String demoName;
@ApiModelProperty(value = "年龄")
@NotNull(message = "age 不能为空")
private Integer age;
@ApiModelProperty(value = "id")
@NotNull(message = "id 不能为空")
private Integer id;
}
@RestController
public class UserController{
@PostMapping("/setUser")
@ApiOperation(value = "获取用户信息")
public CallResult updateUser(@RequestBody @Valid User user) {
userService.setUser(user);
return CallResult.ok();
}
}
结果显示:
没有try-catch块的优雅代码……完美!
本文引用了很多大神的博客,基本都加了链接,如果有漏加链接侵权的,请联系作者删除,谢谢!