分享

猿学-springboot 实现拦截的五种姿势

 AnonymousV脸 2019-04-06

简介AOP(面向切面编程)常用于解决系统中的一些耦合问题,是一种编程的模式

通过将一些通用逻辑抽取为公共模块,由容器来进行调用,以达到模块间隔离的效果。

其还有一个别名,叫面向关注点编程,把系统中的核心业务逻辑称为核心关注点,而一些通用的非核心逻辑划分为横切关注点OP常用于...日志记录你需要为你的Web应用程序实现访问日志记录,却又不想在所有接口中一个个进行打点。



安全控制为URL 实现访问权限控制,自动拦截一些非法访问。

事务某些业务流程需要在一个事务中串行

异常处理系统发生处理异常,根据不同的异常返回定制的消息体。

在笔者刚开始接触编程之时,AOP还是个新事物,当时曾认为AOP会大行其道。

果不其然,目前流行的Spring 框架中,AOP已经成为其关键的核心能力。

先看看下面的一个Controller方法:

示例@RestController

@RequestMapping('/intercept')

public class InterceptController {

@PostMapping(value = '/body', consumes = { MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE })

public String body(@RequestBody MsgBody msg) {

return msg == null ? '<EMPTY>' : msg.getContent();

}

public static class MsgBody {

private String content;

public String getContent() {

return content;

}

public void setContent(String content) {

this.content = content;

}

}

在上述代码的 body 方法中,会接受一个MsgBody请求消息体,最终简单的输出content字段。

下面,我们将介绍如何为这个方法实现拦截动作。算起来,共有五种姿势。

姿势一、使用 Filter 接口Filter 接口由 J2EE 定义,在Servlet执行之前由容器进行调用。

而SpringBoot中声明 Filter 又有两种方式:

1. 注册 FilterRegistrationBean声明一个FilterRegistrationBean 实例,对Filter 做一系列定义,如下:

@Bean

public FilterRegistrationBean customerFilter() {

FilterRegistrationBean registration = new FilterRegistrationBean();

// 设置过滤器

registration.setFilter(new CustomerFilter());

// 拦截路由规则

registration.addUrlPatterns('/intercept/*');

// 设置初始化参数

registration.addInitParameter('name', 'customFilter');

registration.setName('CustomerFilter');

registration.setOrder(1);

return registration;

}

其中 CustomerFilter 实现了Filter接口,如下:

public class CustomerFilter implements Filter {

private static final Logger logger = LoggerFactory.getLogger(CustomerFilter.class);

private String name;

@Override

public void init(FilterConfig filterConfig) throws ServletException {

name = filterConfig.getInitParameter('name');

}

@Override

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

throws IOException, ServletException {

logger.info('Filter {} handle before', name);

chain.doFilter(request, response);

logger.info('Filter {} handle after', name);

}

}

2. @WebFilter 注解为Filter的实现类添加 @WebFilter注解,由SpringBoot 框架扫描后注入

@WebFilter的启用需要配合@ServletComponentScan才能生效@Component

@ServletComponentScan

@WebFilter(urlPatterns = '/intercept/*', filterName = 'annotateFilter')

public class AnnotateFilter implements Filter {

private static final Logger logger = LoggerFactory.getLogger(AnnotateFilter.class);

private final String name = 'annotateFilter';

@Override

public void init(FilterConfig filterConfig) throws ServletException {

}

@Override

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

throws IOException, ServletException {

logger.info('Filter {} handle before', name);

chain.doFilter(request, response);

logger.info('Filter {} handle after', name);

}

}

使用注解是最简单的,但其缺点是仍然无法支持 order属性(用于控制Filter的排序)。

而通常的@Order注解只能用于定义Bean的加载顺序,却真正无法控制Filter排序。

这是一个已知问题,参考这里

推荐指数3 颗星,Filter 定义属于J2EE规范,由Servlet容器调度执行。

由于独立于框架之外,无法使用 Spring 框架的便捷特性,

目前一些第三方组件集成时会使用该方式。

姿势二、HanlderInterceptorHandlerInterceptor 用于拦截 Controller 方法的执行,其声明了几个方法:

|方法 | 说明|

|-----|-----|

|preHandle | Controller方法执行前调用 |

|preHandle | Controller方法后,视图渲染前调用 |

|afterCompletion| 整个方法执行后(包括异常抛出捕获) |

基于 HandlerInterceptor接口 实现的样例:

public class CustomHandlerInterceptor implements HandlerInterceptor {

private static final Logger logger = LoggerFactory.getLogger(CustomHandlerInterceptor.class);

/*

* Controller方法调用前,返回true表示继续处理

*/

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

throws Exception {

HandlerMethod method = (HandlerMethod) handler;

logger.info('CustomerHandlerInterceptor preHandle, {}', method.getMethod().getName());

return true;

}

/*

* Controller方法调用后,视图渲染前

*/

@Override

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,

ModelAndView modelAndView) throws Exception {

HandlerMethod method = (HandlerMethod) handler;

logger.info('CustomerHandlerInterceptor postHandle, {}', method.getMethod().getName());

response.getOutputStream().write('append content'.getBytes());

}

/*

* 整个请求处理完,视图已渲染。如果存在异常则Exception不为空

*/

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)

throws Exception {

HandlerMethod method = (HandlerMethod) handler;

logger.info('CustomerHandlerInterceptor afterCompletion, {}', method.getMethod().getName());

}

}

除了上面的代码实现,还不要忘了将 Interceptor 实现进行注册:

@Configuration

public class InterceptConfig extends WebMvcConfigurerAdapter {

// 注册拦截器

@Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(new CustomHandlerInterceptor()).addPathPatterns('/intercept/**');

super.addInterceptors(registry);

}

推荐指数4颗星,HandlerInterceptor 来自SpringMVC框架,基本可代替 Filter 接口使用;

除了可以方便的进行异常处理之外,通过接口参数能获得Controller方法实例,还可以实现更灵活的定制。

姿势三、@ExceptionHandler 注解@ExceptionHandler 的用途是捕获方法执行时抛出的异常,

通常可用于捕获全局异常,并输出自定义的结果。

如下面的实例:

@ControllerAdvice(assignableTypes = InterceptController.class)

public class CustomInterceptAdvice {

private static final Logger logger = LoggerFactory.getLogger(CustomInterceptAdvice.class);

/**

* 拦截异常

*

* @param e

* @param m

* @return

*/

@ExceptionHandler(value = { Exception.class })

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

@ResponseBody

public String handle(Exception e, HandlerMethod m) {

logger.info('CustomInterceptAdvice handle exception {}, method: {}', e.getMessage(), m.getMethod().getName());

return e.getMessage();

}

}

需要注意的是,@ExceptionHandler 需要与 @ControllerAdvice配合使用

其中 @ControllerAdvice的 assignableTypes 属性指定了所拦截类的名称。

除此之外,该注解还支持指定包扫描范围、注解范围等等。

推荐指数5颗星,@ExceptionHandler 使用非常方便,在异常处理的机制上是首选;

目前也是SpringBoot 框架最为推荐使用的方法。

姿势四、RequestBodyAdvice/ResponseBodyAdviceRequestBodyAdvice、ResponseBodyAdvice 相对于读者可能比较陌生,

而这俩接口也是 Spring 4.x 才开始出现的。

RequestBodyAdvice 的用法我们都知道,SpringBoot 中可以利用@RequestBody这样的注解完成请求内容体与对象的转换。

RequestBodyAdvice 则可用于在请求内容对象转换的前后时刻进行拦截处理,其定义了几个方法:

方法说明supports判断是否支持handleEmptyBody当请求体为空时调用beforeBodyRead在请求体未读取(转换)时调用afterBodyRead在请求体完成读取后调用

实现代码如下:

@ControllerAdvice(assignableTypes = InterceptController.class)

public class CustomRequestAdvice extends RequestBodyAdviceAdapter {

private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);

@Override

public boolean supports(MethodParameter methodParameter, Type targetType,

Class<? extends HttpMessageConverter<?>> converterType) {

// 返回true,表示启动拦截

return MsgBody.class.getTypeName().equals(targetType.getTypeName());

}

@Override

public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,

Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {

logger.info('CustomRequestAdvice handleEmptyBody');

// 对于空请求体,返回对象

return body;

}

@Override

public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,

Class<? extends HttpMessageConverter<?>> converterType) throws IOException {

logger.info('CustomRequestAdvice beforeBodyRead');

// 可定制消息序列化

return new BodyInputMessage(inputMessage);

}

@Override

public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,

Class<? extends HttpMessageConverter<?>> converterType) {

logger.info('CustomRequestAdvice afterBodyRead');

// 可针对读取后的对象做转换,此处不做处理

return body;

}

上述代码实现中,针对前面提到的 MsgBody对象类型进行了拦截处理。

在beforeBodyRead 中,返回一个BodyInputMessage对象,而这个对象便负责源数据流解析转换 public static class BodyInputMessage implements HttpInputMessage {

private HttpHeaders headers;

private InputStream body;

public BodyInputMessage(HttpInputMessage inputMessage) throws IOException {

this.headers = inputMessage.getHeaders();

// 读取原字符串

String content = IOUtils.toString(inputMessage.getBody(), 'UTF-8');

MsgBody msg = new MsgBody();

msg.setContent(content);

this.body = new ByteArrayInputStream(JsonUtil.toJson(msg).getBytes());

}

@Override

public InputStream getBody() throws IOException {

return body;

}

@Override

public HttpHeaders getHeaders() {

return headers;

}

}

代码说明完成数据流的转换,包括以下步骤:

ResponseBodyAdvice 用法ResponseBodyAdvice 的用途在于对返回内容做拦截处理,如下面的示例:

@ControllerAdvice(assignableTypes = InterceptController.class)

public static class CustomResponseAdvice implements ResponseBodyAdvice<String> {

private static final Logger logger = LoggerFactory.getLogger(CustomRequestAdvice.class);

@Override

public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {

// 返回true,表示启动拦截

return true;

}

@Override

public String beforeBodyWrite(String body, MethodParameter returnType, MediaType selectedContentType,

Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,

ServerHttpResponse response) {

logger.info('CustomResponseAdvice beforeBodyWrite');

// 添加前缀

String raw = String.valueOf(body);

return 'PREFIX:' + raw;

}

}

看,还是容易理解的,我们在返回的字符串中添加了一个前缀!

推荐指数2 颗星,这是两个非常冷门的接口,目前的使用场景也相对有限;

一般在需要对输入输出流进行特殊处理(比如加解密)的场景下使用。

姿势五、@Aspect 注解这是目前最灵活的做法,直接利用注解可实现任意对象、方法的拦截。

在某个Bean的类上面** @Aspect** 注解便可以将一个Bean 声明为具有AOP能力的对象。

@Aspect

@Component

public class InterceptControllerAspect {

private static final Logger logger = LoggerFactory.getLogger(InterceptControllerAspect.class);

@Pointcut('target(org.zales.dmo.boot.controllers.InterceptController)')

public void interceptController() {

}

@Around('interceptController()')

public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {

logger.info('aspect before.');

try {

return joinPoint.proceed();

} finally {

logger.info('aspect after.');

}

}

}

简单说明@Pointcut 用于定义切面点,而使用target关键字可以定位到具体的类。

@Around 定义了一个切面处理方法,通过注入ProceedingJoinPoint对象达到控制的目的。

一些常用的切面注解:

注解说明@Before方法执行之前@After方法执行之后@Around方法执行前后@AfterThrowing抛出异常后@AfterReturing正常返回后

深入一点aop的能力来自于spring-boot-starter-aop,进一步依赖于aspectjweaver组件。

有兴趣可以进一步了解。

推荐指数5颗星,aspectj 与 SpringBoot 可以无缝集成,这是一个经典的AOP框架,

可以实现任何你想要的功能,笔者之前曾在多个项目中使用,效果是十分不错的。

注解的支持及自动包扫描大大简化了开发,然而,你仍然需要先对 Pointcut 的定义有充分的了解。

思考到这里,读者可能想知道,这些实现拦截器的接口之间有什么关系呢?

答案是,没有什么关系! 每一种接口都会在不同的时机被调用,我们基于上面的代码示例做了日志输出:

- Filter customFilter handle before

- Filter annotateFilter handle before

- CustomerHandlerInterceptor preHandle, body

- CustomRequestAdvice beforeBodyRead

- CustomRequestAdvice afterBodyRead

- aspect before.

- aspect after.

- CustomResponseAdvice beforeBodyWrite

- CustomerHandlerInterceptor postHandle, body

- CustomerHandlerInterceptor afterCompletion, body

- Filter annotateFilter handle after

- Filter customFilter handle after

可以看到,各种拦截器接口的执行顺序如下图:

小结AOP 是实现拦截器的基本思路,本文介绍了SpringBoot 项目中实现拦截功能的五种常用姿势

对于每一种方法都给出了真实的代码样例,读者可以根据需要选择自己适用的方案。

声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多