- 前言
接触 Spring 也有一段时间了, 之所以一直没有写相关的博客, 一是因为框架的东西相对比较杂, 不好下手, 其次是因为懒(^_^), 接下来的几篇我会挑一些框架中比较常见的特性, 来谈谈其用法, 总结一下近期的所学。
- 环境配置
JDK: 1.8
spring boot: v2.1.6.RELEASE
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springboot</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 先提一个需求
为了方便介绍这三种特性, 先来提一个贯穿全文的需求, 我会使用这三种不同的特性来实现这个需求
统计一下每个请求所消耗的时间
- 准备一个DEMO
项目的包结构如下图所示
Mycontroller
package com.springboot.demo.web.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/my")
public class MyController {
@GetMapping("/test")
public Map test(String name) {
Map res = new HashMap<>();
res.put("name", name);
return res;
}
}
这样一个简单的测试Demo就写好了, 访问 http://localhost:8080/my/test?name=bob
效果如图
- Filter 实现
相信熟悉 javaEE 的同学应该对这个名词不会太陌生, 它的主要作用就是, 能对用户的请求进行预先处理,然后再将请求转发给其他web组件, 实现这个需求的大体思路就是, 在请求进入控制器方法之前记录一个时间, 在请求结束之后记录一个时间, 两个时间的差值就是所消耗的时间。
新建类 TimeFilter 继承了 Filter 类并重写相应的方法
TimeFilter
package com.springboot.demo.web.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* 耗时计算过滤器
*/
@WebFilter(urlPatterns = {"/my/*"})
public class TimeFilter implements Filter {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("过滤器: 初始化");
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
logger.info("过滤器: 开始\n");
long start = System.currentTimeMillis();
chain.doFilter(req, resp); // 调用控制器方法
long end = System.currentTimeMillis();
logger.info("过滤器拦截耗时: " + (end - start) + "ms\n");
}
@Override
public void destroy() {
logger.info("过滤器: 销毁");
}
}
由于这里使用了 @WebFilter 注解所以在Spring Boot 的启动项里需要加上@ServletComponentScan注解, 这样Spring Boot才会扫描到该类如下:
DemoApplication
package com.springboot.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
当然在 Spring Boot 中配置 Filter 的方式并不只有这一种, 下面我再列出一种配置方法, 二选一即可
新建类 WebConfig 作为配置类
WebConfig
package com.springboot.demo.web.config;
import com.springboot.demo.web.filter.TimeFilter;
import com.springboot.demo.web.intercepter.TimeIntercepter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
// 这是一个配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 添加自定义 filter
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new TimeFilter()); // 加入自定义 filter
List urls = new ArrayList<>(); // 指定拦截url
urls.add("/my/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
}
这样也可以将 Filter 注入到 Spring 的容器当中, 看个人喜好, 二选一即可。到此, 实现该功能的代码已经完成, 让我们来看看运行效果, 运行项目, 并访问测试地址。
可以看到我们的过滤器发挥了作用, 该次请求耗时 65ms
2. Interceptor 实现
先来看一下Interceptor的定义
SpringMVC拦截器(Interceptor)实现对每一个请求处理前后进行相关的业务处理,类似与servlet中的Filter。
SpringMVC 中的Interceptor 拦截请求是通过HandlerInterceptor来实现的。
实现需求的逻辑和 Filter 实现的方式大致相同, 我们直接来看代码。
新建 TimeIntercepter 类 并且继承 Spring 的 HandlerInterceptor, 重写其方法
TimeIntercepter
package com.springboot.demo.web.intercepter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 耗时计算拦截器
*/
@Component
public class TimeIntercepter implements HandlerInterceptor {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 控制器方法执行之前
* @param req 请求
* @param resp 响应
* @param handler 对应控制器的方法
* @return boolean 返回 true 进入控制器
* @throws Exception 异常
*/
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
logger.info("拦截器: preHandle");
req.setAttribute("startTime", System.currentTimeMillis());
HandlerMethod method = (HandlerMethod) handler;
logger.info("请求控制器类名: " + method.getBean().getClass().getSimpleName());
logger.info("请求控制器方法名: " + method.getMethod().getName() + "\n");
return true;
}
/**
* 控制器方法执行时 (当控制器发生异常后就不会执行了)
* @param req 请求
* @param resp 响应
* @param handler 对应控制器的方法
* @param mav 模型和视图
* @throws Exception 异常
*/
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView mav) throws Exception {
logger.info("拦截器: postHandle");
long startTime = (long) req.getAttribute("startTime");
long endTime = System.currentTimeMillis();
logger.info("耗时: " + (endTime - startTime) + "ms");
}
/**
* 控制器方法完成后 (总是会执行)
* @param req 请求
* @param resp 响应
* @param handler 对应控制器的方法
* @param ex 控制器执行时发生的异常
* @throws Exception 异常
*/
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception {
logger.info("拦截器: afterCompletion");
if (ex != null) {
logger.info("发生了异常: " + ex.getMessage());
}
long startTime = (long) req.getAttribute("startTime");
long endTime = System.currentTimeMillis();
logger.info("拦截器拦截耗时: " + (endTime - startTime) + "ms\n");
}
}
最后在 WebConfig 中注册我们自定义的 Interceptor
WebConfig
package com.springboot.demo.web.config;
import com.springboot.demo.web.intercepter.TimeIntercepter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 这是一个配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TimeIntercepter timeIntercepter = null;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器, 拦截指定url
registry.addInterceptor(timeIntercepter)
.addPathPatterns("/my/*");
}
}
代码已全部写完, 来看看执行效果, 访问测试 url, 运行效果如下图
可以看到在 Filter 的拦截基础上, 又多了几条Interceptor的日志输出, 它是晚于 Filter 拦截的。
2. Aspcet 实现
Apsect 即 Spring Aop 中的切面, 概念有些抽象我们直接来看代码。
新建类 TimeAspect
TimeAspect
package com.springboot.demo.web.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 切片拦截请求
*/
@Aspect
@Component
public class TimeAspect {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 切点 (环绕)
* @param pjp 被拦截的方法
* @return 被拦截的方法的返回值
*/
@Around("execution(* com.springboot.demo.web.controller.MyController.*(..))") // 拦截 UserController 里的所有方法
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
logger.info("切片拦截: 开始");
long start = System.currentTimeMillis();
Object[] args = pjp.getArgs(); // 获取请求方法的参数
for (Object arg : args) {
logger.info("拦截方法:" + pjp.getSignature().getName() + " 参数: " + arg);
}
Object res = pjp.proceed(); // 调用被拦截的方法, 返回拦截方法的返回值
long end = System.currentTimeMillis();
logger.info("切片拦截耗时: " + (end - start) + "ms");
logger.info("切片拦截结束\n");
return res;
}
}
切面的实现很简单, 只需要这一个类即可, 原理是使用环绕通知将被调用的方法包裹起来, 注释有详细的解释这里不再赘述。在此启动程序, 来看看运行效果。
可以看到在Filter, Interceptor的基础上又多了一层切片的的拦截, 它是晚于Filter 与 Interceptor的。
- 总结
上面我们通过spring的三种特性, 对我们的需求做了三个版本的实现, 虽然是同一个功能, 但是这三种实现还是有所区别的, 相信细心的同学已经从我们打印出的日志中发现了不通点, 下面我们来做一个总结:
- 拦截顺序: Filter -> Interceptor -> Aspect -> Interceptor -> Filter
- 时间快慢: Filter > Interceptor > Aspect
- Filter 只能拦截请求, 不能获取到请求对应的方法
- Interceptor 可以获取到请求的方法, 但不能获取到方法的参数
- Aspect 既可以获取到请求的方法, 也可以获取到方法的参数, 并且可以织入到整个方法的流程中, 改变方法的原有逻辑。
end~