五. 轻松自定义类型转换
spring目前支持3中类型转换器:
- Converter<S,T>:将 S 类型对象转为 T 类型对象
- ConverterFactory<S, R>:将 S 类型对象转为 R 类型及子类对象
- GenericConverter:它支持多个source和目标类型的转化,同时还提供了source和目标类型的上下文,这个上下文能让你实现基于属性上的注解或信息来进行类型转换。
这3种类型转换器使用的场景不一样,我们以Converter<S,T>为例。假如:接口中接收参数的实体对象中,有个字段的类型是Date,但是实际传参的是字符串类型:2021-01-03 10:20:15,要如何处理呢?
第一步,定义一个实体User:
@Data public class User { private Long id; private String name; private Date registerDate; }
第二步,实现Converter接口:
public class DateConverter implements Converter<String, Date> { private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override public Date convert(String source) { if (source != null && !"".equals(source)) { try { simpleDateFormat.parse(source); } catch (ParseException e) { e.printStackTrace(); } } return null; } }
第三步,将新定义的类型转换器注入到spring容器中:
@Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new DateConverter()); } }
第四步,调用接口
@RequestMapping("/user") @RestController public class UserController { @RequestMapping("/save") public String save(@RequestBody User user) { return "success"; } }
请求接口时User对象中registerDate字段会被自动转换成Date类型。
六 .spring mvc拦截器,用过的都说好
spring mvc拦截器根spring拦截器相比,它里面能够获取HttpServletRequest和HttpServletResponse 等web对象实例。
spring mvc拦截器的顶层接口是:HandlerInterceptor,包含三个方法:
preHandle 目标方法执行前执行
postHandle 目标方法执行后执行
afterCompletion 请求完成时执行
为了方便我们一般情况会用HandlerInterceptor
接口的实现类HandlerInterceptorAdapter
类。
假如有权限认证、日志、统计的场景,可以使用该拦截器。
第一步,继承HandlerInterceptorAdapter
类定义拦截器:
public class AuthInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestUrl = request.getRequestURI(); if (checkAuth(requestUrl)) { return true; } return false; } private boolean checkAuth(String requestUrl) { System.out.println("===权限校验==="); return true; } }
第二步,将该拦截器注册到spring容器:
@Configuration public class WebAuthConfig extends WebMvcConfigurerAdapter { @Bean public AuthInterceptor getAuthInterceptor() { return new AuthInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()); } }
第三步,在请求接口时spring mvc通过该拦截器,能够自动拦截该接口,并且校验权限。
该拦截器其实相对来说,比较简单,可以在DispatcherServlet
类的doDispatch
方法中看到调用过程:
顺便说一句,这里只讲了spring mvc的拦截器,并没有讲spring的拦截器,是因为我有点小私心,后面就会知道。
七. Enable开关真香
不知道你有没有用过Enable开头的注解,比如:EnableAsync、EnableCaching、EnableAspectJAutoProxy
等,这类注解就像开关一样,只要在@Configuration
定义的配置类上加上这类注解,就能开启相关的功能。
让我们一起实现一个自己的开关:
第一步,定义一个LogFilter:
public class LogFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("记录请求日志"); chain.doFilter(request, response); System.out.println("记录响应日志"); } @Override public void destroy() { } }
第二步,注册LogFilter:
@ConditionalOnWebApplication public class LogFilterWebConfig { @Bean public LogFilter timeFilter() { return new LogFilter(); } }
注意,这里用了@ConditionalOnWebApplication
注解,没有直接使用@Configuration
注解。
第三步,定义开关@EnableLog注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(LogFilterWebConfig.class) public @interface EnableLog { }
第四步,只需在springboot启动类加上@EnableLog
注解即可开启LogFilter
记录请求和响应日志的功能。
八. RestTemplate拦截器的春天
我们使用RestTemplate
调用远程接口时,有时需要在header
中传递信息,比如:traceId,source等,便于在查询日志时能够串联一次完整的请求链路,快速定位问题。
这种业务场景就能通过ClientHttpRequestInterceptor
接口实现,具体做法如下:
第一步,实现ClientHttpRequestInterceptor
接口:
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().set("traceId", MdcUtil.get()); return execution.execute(request, body); } }
第二步,定义配置类:
@Configuration public class RestTemplateConfiguration { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList(restTemplateInterceptor())); return restTemplate; } @Bean public RestTemplateInterceptor restTemplateInterceptor() { return new RestTemplateInterceptor(); } }
其中MdcUtil其实是利用MDC工具在ThreadLocal中存储和获取traceId
public class MdcUtil { private static final String TRACE_ID = "TRACE_ID"; public static String get() { return MDC.get(TRACE_ID); } public static void add(String value) { MDC.put(TRACE_ID, value); } }
当然,这个例子中没有演示MdcUtil类的add方法具体调的地方,我们可以在filter中执行接口方法之前,生成traceId,调用MdcUtil类的add方法添加到MDC中,然后在同一个请求的其他地方就能通过MdcUtil类的get方法获取到该traceId。
九. 统一异常处理
以前我们在开发接口时,如果出现异常,为了给用户一个更友好的提示,例如:
@RequestMapping("/test") @RestController public class TestController { @GetMapping("/add") public String add() { int a = 10 / 0; return "成功"; } }
如果不做任何处理请求add接口结果直接报错:
what?用户能直接看到错误信息?
这种交互方式给用户的体验非常差,为了解决这个问题,我们通常会在接口中捕获异常:
@GetMapping("/add") public String add() { String result = "成功"; try { int a = 10 / 0; } catch (Exception e) { result = "数据异常"; } return result; }
接口改造后,出现异常时会提示:“数据异常”,对用户来说更友好。
看起来挺不错的,但是有问题。。。
如果只是一个接口还好,但是如果项目中有成百上千个接口,都要加上异常捕获代码吗?
答案是否定的,这时全局异常处理就派上用场了:RestControllerAdvice
。
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public String handleException(Exception e) { if (e instanceof ArithmeticException) { return "数据异常"; } if (e instanceof Exception) { return "服务器内部异常"; } retur nnull; } }
只需在handleException方法中处理异常情况,业务接口中可以放心使用,不再需要捕获异常(有人统一处理了)。真是爽歪歪。
@RestControllerAdvice是什么?
@RestControllerAdvice
是一个组合注解,由@ControllerAdvice、@ResponseBody
组成,而@ControllerAdvice
继承了@Component
,因此@RestControllerAdvice
本质上是个Component,用于定义@ExceptionHandler
,@InitBinder和@ModelAttribute
方法,适用于所有使用@RequestMapping
方法。
@RestControllerAdvice的特点:
- 通过
@ControllerAdvice
注解可以将对于控制器的全局配置放在同一个位置。 - 注解了
@RestControllerAdvice
的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute
注解到方法上。 @RestControllerAdvice
注解将作用在所有注解了@RequestMapping
的控制器的方法上。@ExceptionHandler
:用于指定异常处理方法。当与@RestControllerAdvice
配合使用时,用于全局处理控制器里的异常。@InitBinder
:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。@ModelAttribute
:本来作用是绑定键值对到Model中,当与@ControllerAdvice
配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对
@ControllerAdvice public class GlobalController{ //(1)全局数据绑定 //应用到所有@RequestMapping注解方法 //此处将键值对添加到全局,注解了@RequestMapping的方法都可以获得此键值对 @ModelAttribute public void addUser(Model model) { model.addAttribute("msg", "此处将键值对添加到全局,注解了@RequestMapping的方法都可以获得此键值对"); } //(2)全局数据预处理 //应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器 //用来设置WebDataBinder @InitBinder("user") public void initBinder(WebDataBinder binder) { } // (3)全局异常处理 //应用到所有@RequestMapping注解的方法,在其抛出Exception异常时执行 //定义全局异常处理,value属性可以过滤拦截指定异常,此处拦截所有的Exception @ExceptionHandler(Exception.class) public String handleException(Exception e) { return "error"; }
@ControllerAdvice可以指定 Controller 范围
- basePackages: 指定一个或多个包,这些包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理
@RestControllerAdvice(basePackages={"top.onething"}) @Slf4j public class ExceptionHandlerAdvice { @ExceptionHandler(Exception.class) public String handleException(Exception e) { return "error"; } }
- basePackageClasses: 是 basePackages 的一种变形,指定一个或多个 Controller 类,这些类所属的包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理
@RestControllerAdvice(basePackageClasses={TestController.class}) @Slf4j public class ExceptionHandlerAdvice { @ExceptionHandler(Exception.class) public String handleException(Exception e) { return "error"; } }
- assignableTypes: 指定一个或多个 Controller 类,这些类被该 @ControllerAdvice 管理
@RestControllerAdvice(assignableTypes={TestController.class}) @Slf4j public class ExceptionHandlerAdvice { @ExceptionHandler(Exception.class) public String handleException(Exception e) { return "error"; } }
@ControllerAdvice 指定 Controller 范围
根据 API,我们可以看到注解 @ControllerAdvice 有如下几种配置:
basePackages
//@ControllerAdvice("cn.myz.demo.controller") //@ControllerAdvice(value = "cn.myz.demo.controller") @ControllerAdvice(basePackages = {"cn.myz.demo.controller"}) public class GlobalExceptionHandler {}
basePackages:指定一个或多个包,这些包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理。其中上面两种等价于 basePackages。
basePackageClasses
@ControllerAdvice(basePackageClasses = {MyController1.class}) public class GlobalExceptionHandler {}
basePackageClasses:是 basePackages 的一种变形,指定一个或多个 Controller 类,这些类所属的包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理。
assignableTypes
@ControllerAdvice(assignableTypes = {MyController1.class}) public class GlobalExceptionHandler {}
assignableTypes:指定一个或多个 Controller 类,这些类被该 @ControllerAdvice 管理。
annotations
@ControllerAdvice(annotations = {RestController.class}) public class GlobalExceptionHandler {}
annotations:指定一个或多个注解,被这些注解所标记的 Controller 会被该 @ControllerAdvice 管理。
Demo
用 assignableTypes 配置指定的 Controller 进行测试。
创建三个 Controller
@Controller public class MyController1 { @RequestMapping(value = "/test1") public void test1() { throw new BusinessException("1", "test1 错误"); } }
@Controller public class MyController2 { @RequestMapping(value = "/test2") public void test1() { throw new BusinessException("2", "test2 错误"); } }
@Controller public class MyController3 { @RequestMapping(value = "/test3") public void test1() { throw new BusinessException("3", "test3 错误"); } }
其中,BusinessException 是我自定义的异常类。
创建两个全局异常处理类
@ControllerAdvice(assignableTypes = {MyController1.class}) @Slf4j public class GlobalExceptionHandler1 { /** * 处理 Exception 异常 * * @param httpServletRequest httpServletRequest * @param e 异常 * @return */ @ResponseBody @ExceptionHandler(value = Exception.class) public String exceptionHandler(HttpServletRequest httpServletRequest, Exception e) { log.error("GlobalExceptionHandler1 服务错误"); return "GlobalExceptionHandler1 服务错误"; } }
@ControllerAdvice(assignableTypes = {MyController2.class}) @Slf4j public class GlobalExceptionHandler2 { /** * 处理 Exception 异常 * * @param httpServletRequest httpServletRequest * @param e 异常 * @return */ @ResponseBody @ExceptionHandler(value = Exception.class) public String exceptionHandler(HttpServletRequest httpServletRequest, Exception e) { log.error("GlobalExceptionHandler2 服务错误"); return "GlobalExceptionHandler2 服务错误"; } }
分别调用接口,查看错误日志
1.调用 localhost:8080/test1
返回:GlobalExceptionHandler1 服务错误
即 MyController1 异常被 GlobalExceptionHandler1 全局异常类捕获。
2.调用 localhost:8080/test2
返回:GlobalExceptionHandler2 服务错误
即 MyController2 异常被 GlobalExceptionHandler2 全局异常类捕获。
3.调用 localhost:8080/test3
返回:
{ "timestamp": "2019-03-15T06:40:06.224+0000", "status": 500, "error": "Internal Server Error", "message": "No message available", "path": "/test3" }
即 MyController3 异常没有被全局异常捕获。
以上就是全局异常的分类处理。