过滤器、拦截器、切面区别
(1)三种可获得的信息
可获得的信息 | 原始的Request和Response | 拦截的类名/方法名 | 控制器的方法参数信息 |
---|---|---|---|
过滤器 | √ | × | × |
拦截器 | √ | √ | × |
切面 | × | √ | √ |
(2)拦截器、过滤器区别
拦截器是基于java的反射机制的,而过滤器是基于函数回调。
过滤器需要在servlet容器中实现,拦截器可以适用于javaEE,javaSE等各种环境
拦截器可以调用IOC容器中的各种依赖,而过滤器不能
过滤器只能在请求的前后使用,而拦截器可以详细到每个方法
Filter可以拦截所有请求。包括静态资源css,js;而拦截器只能拦截action请求。不包括静态资源
(3)AOP与过滤器、拦截器区别
- 过滤器,拦截器拦截的是URL。AOP拦截的是类的元数据(包、类、方法名、参数等)。
- 过滤器并没有定义业务用于执行逻辑前、后等,仅仅是请求到达就执行。
- 拦截器有三个方法,相对于过滤器更加细致,有被拦截逻辑执行前、后等。 AOP针对具体的代码,能够实现更加复杂的业务逻辑。
- 三者功能类似,但各有优势,从过滤器–》拦截器–》切面,拦截规则越来越细致。 执行顺序依次是过滤器、拦截器、切面。
(4)如何选择(使用场景)
过滤器
- 统一设置编码
- 过滤敏感字符
- 登录校验
- URL级别的访问权限控制
- 数据压缩
拦截器
- 日志记录
- 权限校验
- 登录校验
- 性能检测[检测方法的执行时间]
其实拦截器和过滤器很像,有些使用场景。无论选用谁都能实现。需要注意的使他们彼此的使用范围,触发机制。
三者使用场景区别
在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。
比如:
权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;
日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。
一、过滤器3种配置方式
0、可捕获的参数
- request : 请求的参数,可以通过HttpServletRequestWrapper来修改传入的URI、头信息等;
- response : 返回体
- filterChain : 过滤链(待补充)
Filter的这个特性在生产环境中有很广泛的应用,如:修改请求和响应、防止xss攻击、包装二进制流使其可以多次读,等等。
以下三种方式配置过滤器对比
使用方式 | 排序 | 指定URL |
---|---|---|
@Component @Order | √ | × |
@WebFilter @ServletComponentScan | × | √ |
JavaConfig | √ | √ |
实际使用过程中,可以按照业务需求选择合适的使用方式,比如:如果编写的过滤器要拦截所有请求,不需要指定URL,那选择最简单的 @Component+@Order 就非常合适。
1、配置类(推荐)
通过 Java 代码显式配置 Filter ,功能强大,配置灵活。只需要把每个自定义的 Filter 声明成 Bean 交给 Spring 管理即可,还可以设置匹配的 URL 、指定 Filter 的先后顺序。
1、定义过滤器
@Slf4j
public class TestFilter1 implements Filter {
// 项目启动时执行
@Override
public void init(FilterConfig filterConfig) {
log.info("过滤器1初始化!!!!");
}
// 请求被拦截时执行
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 请求执行前
log.info("过滤器1开始!!!!");
long start = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
// 请求执行后
log.info("过滤器1结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
}
//当Filter被移除或服务器正常关闭时
@Override
public void destroy() {
log.info("过滤器1销毁!!!!");
}
}
2、配置过滤器
@Configuration
public class FilterConfig {
// 配置 filter1
@Bean
public FilterRegistrationBean registFilter1() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new TestFilter1());//设置过滤器
registration.addUrlPatterns("/*");//设置过滤URL
registration.setName("TestFilter1");//设置过滤器名称
registration.setOrder(1);//设置过滤器执行顺序
return registration;
}
// 配置 filter2
@Bean
public FilterRegistrationBean registFilter2() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new TestFilter2());
registration.addUrlPatterns("/*");
registration.setName("TestFilter2");
registration.setOrder(2);
return registration;
}
}
执行顺序
假设有order
分别为 1 和 2 的2个过滤器:
过滤器1开始 - 过滤器2开始 - controller - 过滤器2结束 - 过滤器1结束
2、@WebFilter
在 MyFilter
上添加@WebFilter
注解,并在启动类上增加@ServletComponentScan("com.zhengxl.filterdemo.filter")
注解,参数就是Filter所在的包路径,相当于告诉 SpringBoot,去哪里扫描 Filter。
@WebFilter+@ServletComponentScan 注解方式支持对 Filter 匹配指定URL,但是不支持指定 Filter 的执行顺序。
@Slf4j
/**
* 如果用此注解,一定要在配置类中加另外一个注解:@ServletComponetScan,指定扫描的包。
* 过滤器执行顺序会按照filterName的字母顺序进行
* @WebFilter指定的过滤器优先级都高于FilterRegistrationBean配置的过滤器
*/
@WebFilter(urlPatterns = "/*", filterName = "aaaa")
public class TestFilter1 implements Filter {
@Override
public void init(FilterConfig filterConfig) {
log.info("过滤器1初始化!!!!");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("过滤器1开始!!!!");
long start = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
log.info("过滤器1结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
}
@Override
public void destroy() {
//当Filter被移除或服务器正常关闭时
log.info("过滤器1销毁!!!!");
}
}
@SpringBootApplication
// 指定扫描的包中的 servlet 组件
@ServletComponentScan("com.qjc.filter")
public class FilterInteceptorApplication {
public static void main(String[] args) {
SpringApplication.run(FilterInteceptorApplication.class, args);
}
}
3、简易(仅全局)
@Component
@Order(1)
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("MyFilter");
// 要继续处理请求,必须添加 filterChain.doFilter()
filterChain.doFilter(servletRequest,servletResponse);
}
}
没错就这么简单,这样 MyFilter 就生效了,写个Controller
调用一下就可以看到效果。
当有多个Filter时,这里的@Order(1)
注解会指定执行顺序,数字越小,越优先执行,如果只写@Order
,默认顺序值是Integer.MAX_VALUE
。
@Component + @Order 注解方式配置简单,支持自定义 Filter 顺序。缺点是只能拦截所有URL,不能通过配置去拦截指定的 URL。
二、拦截器
1、可捕获的参数
- request : 请求的参数,可以通过HttpServletRequestWrapper来修改传入的URI、头信息等;
- response : 返回体
- handler :
2、代码实现
拦截器
@Slf4j
public class TestInterceptor1 implements HandlerInterceptor {
// 请求执行前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("请求执行前拦截器11111开始!!!!");
long start = System.currentTimeMillis();
// 获取拦截的类和方法信息
HandlerMethod handlerMethod = (HandlerMethod) handler;
System.out.println(handlerMethod.getBean().getClass().getName()); //获取类名+信息
System.out.println(handlerMethod.getMethod().getName()); //获取方法名
// 向request设置值
request.setAttribute("MyKey","MyValue");
log.info("请求执行前拦截器11111结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
// 可自定义返回数据
//response.setContentType("application/json;charset=UTF-8");
//response.getWriter().println(json);
//当返回true的时候,才可以执行postHandle/afterCompletion,即表示是否进入controller层,返回false表示丢弃请求
return true;
}
//请求完成后,视图渲染前执行
//preHandler返回false/Controller的方法发生异常
//这两种状况都不会调用 postHandler ;
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 从request 获取值
System.out.println((String) request.getAttribute("MyKey"));
log.info("请求执行完成后,拦截器11111开始!!!!");
long start = System.currentTimeMillis();
log.info("请求执行完成后,拦截器11111结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
}
// 视图渲染完成后
//无论Controller的方法正常完成或发生异常, 都会调用, 而且可以拿到 Exception 对象!
//若 handler成功, 则 ex==null; 若 handler抛出异常, ex有值(前提是: @ExceptionHandler没有处理抛出的异常, 如果处理了, 就到不了ex这里了);
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("视图渲染完成后,拦截器11111开始!!!!");
long start = System.currentTimeMillis();
log.info("视图渲染完成后,拦截器11111结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
配置拦截器
@Configuration
//继承WebMvcConfigurer
public class mvcConfig implements WebMvcConfigurer {
//添加自定义拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 链式编程,第一个参数为 自定义的拦截器
registry.addInterceptor(new TestInterceptor1())
//配置拦截路径
.addPathPatterns("/**")
//配置不拦截的路径
.excludePathPatterns("/","/login","/css/**","/js/**","/fonts/**","images/**")
//设置拦截器执行顺序
.order(1);
// 配置多个个拦截器
registry.addInterceptor(new TestInterceptor2())
.addPathPatterns("/**")
.excludePathPatterns("/","/login","/css/**","/js/**","/fonts/**","images/**")
.order(2);
}
}
三、切面
1、可捕获的参数
- 对应方法的参数
- 方法的返回值
2、基本实现
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Aspect
@Component
public class OrderAspect {
@Before("execution(public * com.rewind.controller.OrderController.*(..))")
public void before(){
System.out.println("前置增强,比如创建连接对象");
}
@After("execution(public * com.rewind.controller.OrderController.*(..))")
public void after(){
System.out.println("后置增强,比如关闭连接对象");
}
@AfterReturning("execution(public * com.rewind.controller.OrderController.*(..))")
public void afterReturning(){
System.out.println("方法执行成功通知,比如提交事务");
}
@AfterThrowing("execution(public * com.rewind.controller.OrderController.*(..))")
public void afterThrowing(){
System.out.println("方法执行异常通知,比如回滚事务");
}
// 获取到请求的参数/响应值,并且对参数和响应值做出一定修改并生效
@Around("execution(public * com.rewind.controller.OrderController.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法参数值数组
Object[] args = joinPoint.getArgs();
//得到其方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取方法参数类型数组
Class[] paramTypeArray = methodSignature.getParameterTypes();
if (EntityManager.class.isAssignableFrom(paramTypeArray[paramTypeArray.length - 1])) {
//如果方法的参数列表最后一个参数是entityManager类型,则给其赋值
args[args.length - 1] = entityManager;
}
log.info("请求参数为{}",args);
//动态修改其参数
//执行目标方法
//注意,如果调用joinPoint.proceed()方法,则修改的参数值不会生效,必须调用joinPoint.proceed(Object[] args)
Result result = (Result)joinPoint.proceed(args);
if (result.getData() instanceof User) {
User user = (User) result.getData();
user.setUsername(user.getUsername() + " &"); // 设置值
}
log.info("响应结果为{}",result);
//如果这里不返回result,则目标对象实际返回值会被置为null
return result;
}
}
切入点表达式
execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
execution(public * com.rewind.controller.OrderController.*(..))
execution(<返回类型模式><方法名模式>(<参数模式>))
execution(* com.rewind.controller.TestController.*(..))
其中:返回类型模式、方法名模式、参数模式是必选项
修饰符模式和异常模式可以省略,?表示可省略
3、看看就好
@Aspect
@Slf4j
@Component
public class TestAspect {
@Pointcut("execution(* com.rewind.controller.TestController.*(..))")
// @Pointcut("execution(* com.rewind.controller..*.*(..))") //切所有controller
public void pointcut() {
}
@Before("pointcut()")
public void beforeControllerMethod(JoinPoint joinPoint) {
log.info("前置通知!!!!方法执行前执行");
}
@Around("pointcut()")
public Object handleControllerMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String uuIdTrace = UUID.randomUUID().toString().replace("-", "");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//IP地址
String ipAddr = getRemoteHost(request);
// String url = request.getRequestURL().toString();
String requestMethod = request.getMethod();
String requestURI = request.getRequestURI();
String reqParam = preHandle(proceedingJoinPoint, request);//获取请求参数
log.info(uuIdTrace + " 环绕通知,执行方法前,请求源IP:【{}】,请求方法:【{}】,请求URL:【{}】,请求参数:【{}】", ipAddr, requestMethod, requestURI, reqParam);
long start = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();//执行方法,获取响应信息
String respParam = postHandle(result);
log.info(uuIdTrace + " 环绕通知,执行方法后,请求源IP:【{}】,请求URL:【{}】,返回参数:【{}】,执行耗时 : {}", ipAddr, requestURI, respParam, (System.currentTimeMillis() - start) + "ms");
return result;
}
@After("pointcut()")
public void afterControllerMethod(JoinPoint joinPoint) {
log.info("后置通知:方法执行完后执行");
}
@AfterReturning(returning = "result", pointcut = "pointcut()")
public void doAfterReturnint(Object result) {
log.info("后置通知,方法执行完后执行,响应信息为:{}", JSONObject.toJSONString(result));
}
/* ----------------------------------以下为自定义方法---------------------------------------------- */
/**
* 入参数据
*
* @param joinPoint
* @param request
* @return
*/
private String preHandle(ProceedingJoinPoint joinPoint, HttpServletRequest request) {
try {
Map<String, Object> fieldsName = getFieldsName(joinPoint);
return JSON.toJSONString(fieldsName);
} catch (Exception e) {
log.warn("切面获取请求参数出现异常", e);
return null;
}
}
private Map<String, Object> getFieldsName(JoinPoint joinPoint) throws Exception {
String classType = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
// 参数值
Object[] args = joinPoint.getArgs();
Class<?>[] classes = new Class[args.length];
for (int k = 0; k < args.length; k++) {
// 对于接受参数中含有MultipartFile,ServletRequest,ServletResponse类型的特殊处理,我这里是直接返回了null。(如果不对这三种类型判断,会报异常)
if (args[k] instanceof MultipartFile || args[k] instanceof ServletRequest || args[k] instanceof ServletResponse) {
return null;
}
if (!args[k].getClass().isPrimitive()) {
// 当方法参数是基础类型,但是获取到的是封装类型的就需要转化成基础类型
// String result = args[k].getClass().getName();
// Class s = map.get(result);
// 当方法参数是封装类型
Class s = args[k].getClass();
classes[k] = s == null ? args[k].getClass() : s;
}
}
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
// 获取指定的方法,第二个参数可以不传,但是为了防止有重载的现象,还是需要传入参数的类型
Method method = Class.forName(classType).getMethod(methodName, classes);
// 参数名
String[] parameterNames = pnd.getParameterNames(method);
// 通过map封装参数和参数值
HashMap<String, Object> paramMap = new HashMap();
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
return paramMap;
}
/**
* 返回数据
*
* @param retVal
* @return
*/
private String postHandle(Object retVal) {
if (null == retVal) {
return "";
}
return JSON.toJSONString(retVal);
}
/**
* 获取目标主机的ip
*
* @param request
* @return
*/
private String getRemoteHost(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}
执行顺序
过滤器 -> 拦截器 -> 切面
: 过滤器1开始!!!!
: 过滤器2开始!!!!
: 请求执行前拦截器11111开始!!!!
: 请求执行前拦截器11111结束!!!!耗时:0ms
: 请求执行前拦截器22222开始!!!!
: 请求执行前拦截器22222结束!!!!耗时:0ms
: a715fda106894f5c83c68f21bc1d35d2 环绕通知,执行方法前,请求源IP:【127.0.0.1】,请求方法:【POST】,请求URL:【/test/find/dataList】,请求参数:【{"userName":"rewind"}】
: 前置通知!!!!方法执行前执行
: 查询数据成功!!!!
: a715fda106894f5c83c68f21bc1d35d2 环绕通知,执行方法后,请求源IP:【127.0.0.1】,请求URL:【/test/find/dataList】,返回参数:【{"data":"rewind","message":"查询成功","status":200}】,执行耗时 : 1ms
: 后置通知:方法执行完后执行
: 后置通知,方法执行完后执行,响应信息为:{"data":"rewind","message":"查询成功","status":200}
: 请求执行完成后,拦截器22222开始!!!!
: 请求执行完成后,拦截器22222结束!!!!耗时:0ms
: 请求执行完成后,拦截器11111开始!!!!
: 请求执行完成后,拦截器11111结束!!!!耗时:0ms
: 视图渲染完成后,拦截器22222开始!!!!
: 视图渲染完成后,拦截器22222结束!!!!耗时:0ms
: 视图渲染完成后,拦截器11111开始!!!!
: 视图渲染完成后,拦截器11111结束!!!!耗时:0ms
: 过滤器2结束!!!!耗时:15ms
: 过滤器1结束!!!!耗时:15ms
过滤器 - 拦截器 - 环绕通知 - 前置通知 - controller - 环绕通知 - 后置通知 - 拦截器 - 过滤器