SpringBoot过滤器、拦截器、Aop切面
一、过滤器(Filter)
启动类添加如下注解:@ServletComponentScan。
Filter过滤器是Servlet容器层面的,在实现上基于函数回调,可以对几乎所有请求进行过滤。Filter完整的流程:Filter对用户请求进行预处理,接着将请求交给Servlet进行预处理并生成响应,最后Filter再对服务器响应进行后处理。过滤器实现示例如下:
package com.example.demo01.filter;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
@WebFilter(filterName = "SessionFilter",
displayName = "SessionFilter",
urlPatterns = {"/*"},
initParams = @WebInitParam(
name = "SessionFilterInitParam",
value = "SessionFilter")
)
public class SessionFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper((HttpServletRequest) servletRequest) {
@Override
public String getHeader(String name) {
String superHeader = super.getHeader(name);
if ("test".equals(name) && StringUtils.isEmpty(superHeader)) {
String session = servletRequest.getParameter("session");
if (!StringUtils.isEmpty(session)) {
return session;
}
}
return superHeader;
}
};
filterChain.doFilter(requestWrapper, servletResponse);
}
}
二、拦截器(Interceptor)
Interceptor拦截器和Filter有本质上的不同,Filter依赖于Servlet容器,而Interceptor依赖于Spring框架,是aop的一种表现,基于Java的动态代理实现的。拦截器在AOP中用于在某个方法或字段被访问之前,进行拦截,然后在之前或之后加入某些操作。拦截是AOP的一种实现策略。
拦截器作用:
日志记录:记录请求信息的日志;
权限检查:如登录检测,进入处理器检测检测是否登录;
性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。
拦截器实现
通过实现HandlerInterceptor接口,并重写该接口的方法来实现自定义拦截。在SpringBoot中实现拦截器的方式:
声明拦截器的类:
通过实现HandlerInterceptor接口,实现preHandle、postHandle和afterCompletion方法。
配置类配置拦截器:
通过实现WebMvcConfigurer接口,实现addInterceptors方法。如下示例代码:
@Slf4j
public class SignAuthInterceptor implements HandlerInterceptor {
/**
* 5分钟有效期
*/
private final static long MAX_EXPIRE = 5 * 60;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("request URI = " + request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String timesTamp = request.getHeader(CommonConstant.X_TIMESTAMP);
//1.校验时间有消息
try {
DateUtils.parseDate(timesTamp, "yyyyMMddHHmmss");
} catch (Exception e) {
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP格式必须为:yyyyMMddHHmmss");
}
Long clientTimestamp = Long.parseLong(timesTamp);
//判断时间戳 timestamp=201808091113
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
return true;
} else {
log.error("request URI = " + request.getRequestURI());
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
//校验失败返回前端
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
Result<?> result = Result.error("Sign签名校验失败!");
out.print(JSON.toJSON(result));
return false;
}
}
/**
* 签名 拦截器配置
*/
@Configuration
public class SignAuthConfiguration implements WebMvcConfigurer {
public static String[] urlList = new String[] {"/sys/dict/getDictItems/*", "/sys/dict/loadDict/*","/sys/api/translateDictFromTable", "/sys/api/translateDictFromTableByKeys"};
@Bean
public SignAuthInterceptor signAuthInterceptor() {
return new SignAuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signAuthInterceptor()).addPathPatterns(urlList);
}
}
三、Spring aop切面(Aspect)
相比较于拦截器,Spring 的aop则功能更强大,封装的更细。
Aop涉及到的注解如下:
@Aspect:将一个 java 类定义为切面类。
@Pointcut:定义一个切入点,可以是一个规则表达式,比如下例中某个 package 下的所有函数,也可以是一个注解等。
@Before:在切入点开始处切入内容。
@After:在切入点结尾处切入内容。
@AfterReturning:在切入点 return 内容之后处理逻辑。
@Around:在切入点前后切入内容,并自己控制何时执行切入点自身的内容。原则上可以替代
@Before和@After。
@AfterThrowing:用来处理当切入内容部分抛出异常之后的处理逻辑。
@Order(100):AOP 切面执行顺序, @Before 数值越小越先执行,@After 和 @AfterReturning
数值越大越先执行。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
示例代码如下:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
@Aspect
@Slf4j
@Component
public class MethodMonitorAspect {
private long startTime;
//声明切面类路径,类型必须为final String类型的,注解里要使用的变量只能是静态常量类型的
//public static final String POINT = "execution(* com.product.service.*.*(..))";
//也可以使用注解声明切入点,如下
@Pointcut("execution(* com.liuc.server.api.sechdule.invoice.*.*(..))")
public void point() {
}
@Before("point()")
public void doBefore(JoinPoint pj) {
this.startTime = System.currentTimeMillis();
Object target = pj.getTarget();
String className = target.getClass().getName(); //当前执行的方法所属的类、包
MethodSignature signature = (MethodSignature) pj.getSignature();
String methodName = signature.getName(); //当前执行的方法名称
SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("类{}.{}方法开始执行" + (dateformat.format(this.startTime)),className,methodName);
}
@After("point()")
public void doAfter(JoinPoint pj) {
long endTime = System.currentTimeMillis();
Object target = pj.getTarget();
String className = target.getClass().getName(); //当前执行的方法所属的类、包
MethodSignature signature = (MethodSignature) pj.getSignature();
String methodName = signature.getName(); //当前执行的方法名称
log.info("类{}.{}方法执行了" + (endTime - this.startTime) + "ms",className,methodName);
}
@Around("point()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
saveSysLog(......);
return result;
}
}
总结
过滤器、拦截器以及切片的调用顺序
调用顺序Filter->Interceptor->Aspect->Controller。
系统中请求被过滤的时机越早对服务的性能影响越小,因此在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;比如日志记录,一般日志只针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,非常适合日志功能。
1.过滤器
过滤器可以拿到原始的http请求,但是拿不到请求的控制器和请求控制器中的方法的信息。过滤器可以拦截到方法的请求和响应(ServletRequest request, ServletResponse response),并对请求响应做出像响应的过滤操作,比如设置字符编码,鉴权操作等。过滤器依赖于Servlet容器,属于Servlet规范的一部分。Filter的执行由Servlet容器回调完成。Filter的生命周期由Servlet容器管理。
2.拦截器
拦截器可以拿到你请求的控制器和方法,却拿不到请求方法的参数。拦截器是独立存在的,可以在任何情况下使用。拦截器通常通过动态代理(反射)的方式来执行,通过IOC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,使用会更方便。
3.Aop切片
AOP可以拿到方法的参数,但是却拿不到http请求和响应的对象。AOP操作可以对操作进行横向的拦截,最大的优势在于他可以获取执行方法的参数,对方法进行统一的处理.常见使用日志,事务,请求参数安全验证等 。