SpringBoot中AOP实现落地——Filter(过滤器)、Intercepter(拦截器)、Aspect(Spring AOP)

此文转载自:https://blog.csdn.net/shang_0122/article/details/112061671#commentBox

一、一切要从Servlet说起

1.1什么是Servlet

Servlet(Server Applet),全称是Java Servlet,是提供基于协议请求/响应服务的Java类。
在JavaEE中是Servlet规范,即是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的Java类,一般人们理解是后者

1.2为什么需要Servlet

最重要的就是,提供动态的Web内容
当向一个Web服务器(如Nginx、IIS、Apache)请求一个资源时,一般提供都是一个静态页面,Web服务器不能做的两件事

不能提供动态即时网页
不能往服务库中保存数据

为了提升用户的体验度,有了Servlet实现动态内容的展示,进而有了JSP动态网页。

1.3Servlet如何响应用户请求

正如前面所说,Servlet是一个Java程序,一个Servlet应用有一个或多个Servlet程序,JSP页面会被转换和编译成Servlet程序。
Servlet应用无法独立运行,必须运行在Servlet容器中。Servlet容器将用户的请求传递给Servlet应用,并将结果返回给用户。
这个Servlet容器就是Tomcat,当然其他的,比如Jetty。
但是值得一提的是Tomcat只是实现了JavaEE13个规范中的Servlet/JSP规范,其他规范没有实现,所以不是一个JavaEE容器

1.4Servlet与Tomcat处理请求的流程

在这里插入图片描述
不得不说,这位小哥很有才啊,简要的说下主要的步骤:

  • 1.用户发送一个HTTP请求到Tomcat
  • 2.根据URL找到对应的Servlet类
  • 3.Tomcat从磁盘加载Servlet类到内存,将HTTP请求解析封装成一个ServletRequest实例,且封装一个ServletResponse实例
  • 4.此时Servlet容器调用Servlet的Service方法,并将ServletRequest实例及ServletResponse实例传入方法中
  • 5.方法执行完后将ServletResonse响应给浏览器

1.5Servlet与Controller之间的关系

聪明的你可能已经发现在上述第二步,根据URL找到对应的Servlet类,现在都是通过URL锁定Controller中的方法进行执行,那么Controller是一个Servlet吗?
答案是不是的
,这个要分为两个阶段,一个是没有引入SpringMVC框架时,一个是引入SpringMVC框架后

没有引入SpringMVC时,咱们通过在web.xml中配置URL和Serlvet类映射关系
如下

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <servlet>
        <!--servlet名称,与servlet-mapping中的servlet-name必须一致-->
        <servlet-name>LoginServlet</servlet-name>
        <!--Servlet类的位置-->
        <servlet-class>Jsp_Servlet_login.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <!--servlet名称,与上面中servlet-name必须一致-->
        <servlet-name>LoginServlet</servlet-name>
        <!--servlet名称,与上面中servlet-name必须一致-->
        <url-pattern>/LoginServlet.action</url-pattern>
    </servlet-mapping>
</web-app>

这时通过/LoginServlet.action就可以找到Jsp_Servlet_login.LoginServlet这个类

引入SpringMVC框架后,就有了著名的SpringMVC处理流程图

在这里插入图片描述
看图中标红的两处,DispatcherServlet,也叫前端控制器,是SpringMVC中最后一个Servlet类,Servlet容器将用户请求发送给DispatcherServlet,由DispatcherServlet根据用户的url找到Controller中的方法并执行,这个过程完全可以再写一篇博客的,后续完成,现在大家知道Controller不是Serlvet即可。

1.6敲黑板,重点来了!!

总结上述就是,Servlet容器将用户请求封装了ServletRequest实例及ServletResponse实例,而今天的主题,Filter、Intercepter、Aspect就是可以在用户请求到目标方法前拿到这两个实例,也就是拿到了用户的请求(我在网上查阅资料时,大家说Aspect不能拿到ServleRequest实例及ServletResonse实例,其实是可以拿到的)进行校验、增强,而Aspect更多的是对Controller中方法的增强。

二、过滤器、拦截器、Aspect概览

为什么需要上面三者

如果要回答这个问题,需要从它们三者的共同点入手,那么它们三个有什么共同点呢?没错,它们都是AOP编程思想的落地实现

在spring官方文档中是这样描述AOP的

Aspect-oriented Programming (AOP) complements Object-oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization of concerns (such as transaction management) that cut across multiple types and objects. (Such concerns are often termed “crosscutting” concerns in AOP literature.)
文档地址
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop

大致的意思如下:

面向切面编程(AOP)是面向对象编程(OOP)的一个补充,面向对象编程的基石是类,面向切面编程的基石是切面(Aspect)。切面可以将多个类或者对象都要执行的代码进行模块化(比如事务管理)

再通俗一点的话:
可以用下面的图进行解释
在这里插入图片描述

由上图可以看出,权限认证是每个方法都要执行的,并且不是业务代码,因此可以将权限认证的代码抽离出来成为一个切面,今天咱们讨论这三个都可以实现切面,这是它们三的共同点,下面也会围绕AOP展开分享

开始实践环节

三、搭建一个简单springboot项目

1.项目目录结构如下

在这里插入图片描述
结构比较简单,新建一个maven工程即可

2.pom及application文件

pom依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.3.3.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
        <version>2.3.3.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
    </dependency>
</dependencies>

application.yml

server:
  port: 8082

3.主启动类

@SpringBootApplication
public class SpringbootFilter {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootFilter.class);
    }
}

好了,一个简单的springboot项目就搭建成功了

四、Springboot中自定义过滤器

1.过滤器基本知识

是什么

过滤器Filter,是在Servlet规范中定义的,是Servlet容器支持的,该接口定义在javax.servlet包下,主要是对客户端请求(HttpServletRequest)进行预处理,以及对服务器响应(HttpServletResponse)进行后处理

Filter接口

package javax.servlet;
import java.io.IOException;
public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {}
}

该接口包含了Filter的3个生命周期:init、doFilter、destroy

init方法

Servlet容器在初始化Filter时,会触发Filter的init方法,一般来说是当服务程序启动时,而且这个方法只调用一次,用于初始化Filter

void init(FilterConfig filterConfig)

其中参数FilterConfig是由Servlet容器传入到init方法中,该参数封装了初始化Filter的参数值,类似于构造函数给对象初始值一样

doFilter方法

当init方法初始化Filter后,Filter拦截到用户请求时,Filter就开始工作了

 void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3)

正如前面所说Servlet容器会将用户请求封装成ServletRequest,而doFilter方法参数中就有ServletRequest,这也就意味着允许给ServletRequest增加属性或者增加header,也可以修饰ServletReqest或者ServletResponse来改变其行为(装饰者模式的应用)

请注意最后一个参数FilterChain var3,该接口定义如下

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

该参数存在意味着,到达用户请求的真正方法之前,可能被多个过滤器进行过滤,这时Filter.doFilter()方法将触发Filter链条中下一个Filter。

值得注意的是:只有在Filter链条中最后一个Filter里调用FilterChain.doFilter(),才会触发处理资源的方法(值得验证),如果结尾处没有调用该方法,后面的处理就会中断

destroy方法

void destroy() {}

这个方法就比较简单了,顾名思义,该方法就是在Servlet容器要销毁Filter时触发,一般在应用停止的时候调用

好了,下面开始实践部分

2.springboot中自定义Filter

在springboot中自定义filter主要是两种方式
一个是使用配置类,一个是使用@WebFilter注解, 推荐使用配置类,和spring项目其他组件保持一致,其实配置类也就是@WebFilter注解的变形

2.1使用@WebFilter注解

该注解属于Servlet3.0中的注解,不属于Spring,因此需要在主启动类加上@ServletComponentScan。但是如果定义多个filter,filter的执行顺序需要配置在web.xml或者使用spring的注解order()定义filter执行顺序,所以建议大家还是用配置类

好现在用自定义filter实现一个登陆的小功能

新建一个LoginFilter类

package com.thinkcoer.filter;

import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Slf4j
@WebFilter(urlPatterns = "/*",filterName = "LoginFilter",initParams = {
        @WebInitParam(name="includeUrls",value = "/login")
})
public class LoginFilter implements Filter {

    //不需要登录就可以访问的路径(比如:注册登录等)
    private String includeUrls;
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        //获取初始化filter的参数
        this.includeUrls=filterConfig.getInitParameter("includeUrls");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpSession session = request.getSession();
        String uri = request.getRequestURI();

        System.out.println("filter url:"+uri);

		//不需要过滤直接传给下一个过滤器
        if (uri.equals(includeUrls)) { 
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            //需要过滤器
            // session中包含user对象,则是登录状态
            if(session!=null&&session.getAttribute("user") != null){
                System.out.println("user:"+session.getAttribute("user"));
                filterChain.doFilter(request, response);
            }else{
                response.setContentType("Application/json;charset=UTF-8");
                response.getWriter().write("您还未登录");
                //重定向到登录页(需要在static文件夹下建立此html文件)
                //response.sendRedirect(request.getContextPath()+"/user/login.html");
                return;
            }
        }
    }

    @Override
    public void destroy() {
        log.info("loginfilter销毁方法执行了");
    }
}

该类主要功能是除登陆外url进行拦截,如果登陆成功会产生一个session,并在客户端产生一个cookie,用户请求别的资源会携带cookie进行验证,如果验证通过则可以拿到该资源

新建一个LoginController

@RestController
public class LoginController {

    @PostMapping("/login")
    public String login(@RequestBody User user, HttpServletRequest request){
        HttpSession session = request.getSession();

        if(!user.getName().equals("root")&&!user.getPwd().equals("root")){
            return "用户名或者密码错误!";
        }
        session.setAttribute("user",user);
        return "登录成功";
    }

    @GetMapping("/test")
    public String loginTest(){
        return "登录校验成功";
    }
}

该类中的自定义User类可以自己建一个实体类,这里就不再赘述了

主启动类
加上@ServletComponentScan注解

@ServletComponentScan
@SpringBootApplication
public class SpringbootFilter {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootFilter.class);
    }
}

开始验证
postman发送请求
在这里插入图片描述
进行登录校验
在这里插入图片描述

2.2使用spring中的配置类方式

该方式使用FilterRegistrationBean类注册自定义的Filter类,并为自定义Filter设置初始化参数,下面自定义两个Filter类,一个是用户认证Filter,一个是打印日志Filter,设置优先级顺序用户认证在前,打印日志在后

用户认证Filter(AuthFilter)

@Slf4j
public class AuthFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("用户认证filter init方法执行");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("用户认证doFilter方法执行");
        log.info("处理业务逻辑,改变请求体对象和回复体对象");
        //调用filter链中的下一个filter
        filterChain.doFilter(servletRequest,servletResponse);
    }


    @Override
    public void destroy() {
        log.info("用户认证destroy方法执行");
    }
}

打印日志Filter(LogFilter)

@Slf4j
public class LogFilter implements Filter {

    @Override
    public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
        log.info("过滤器初始化时配置"+filterConfig);
        log.info("日志filter init方法执行");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("日志doFilter方法执行");
        log.info("处理业务逻辑,改变请求体对象和回复体对象");
        //调用filter链中的下一个filter
        filterChain.doFilter(servletRequest,servletResponse);
    }


    @Override
    public void destroy() {
        log.info("日志filter destroy方法执行");
    }
}

配置类FilterConfig
注册两个Filter

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean authFilterRegistation(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        //注册bean
        registrationBean.setFilter(new AuthFilter());
        //设置bean name
        registrationBean.setName("AuthFilter");
        //拦截所有请求
        registrationBean.addUrlPatterns("/*");
        //执行顺序,数字越小优先级越高
        registrationBean.setOrder(1);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean logFilterRegistation(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new LogFilter());
        registrationBean.setName("LogFilter");
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(2);
        return registrationBean;
    }
}

新建一个LogController用于测试两个Filter类

@Slf4j
@RestController
public class LogController {

    @GetMapping("/log")
    public void testLog(){
        log.info("日志controller方法执行了");
    }
}

开始验证
启动项目

用户认证filter init方法执行
日志filter init方法执行

请求方法

用户认证doFilter方法执行
处理业务逻辑,改变请求体对象和回复体对象
日志doFilter方法执行
处理业务逻辑,改变请求体对象和回复体对象

关闭程序

用户认证destroy方法执行
日志filter destroy方法执行

小总结:

Filter是拦截Request请求的对象,在用户的请求访问资源前处理ServletRequest以及ServletResponse,可以用于日志记录、Session检查等,多个Filter协同工作时可以设置Filter的先后顺序,值得一说的是现在微服务的组件中,底层也是用到了Filter,比如gateway网关、zuul、spring
security等等

好了,关于自定义Filter暂搞一段落,现在用户的请求已经到达了DispatcherServlet(假设用的是SpringMVC),在真正到达Controller类中的方法前,还要经过拦截器

五、Springboot中自定义拦截器

1.拦截器基本知识

是什么

简单一点理解拦截器就是,能够在进行某个操作之前拦截请求,如果请求符合条件就允许向下执行

HandlerInterceptor接口
该接口提供了拦截器的功能,如果自定义拦截器要实现该接口

public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return true;
	}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}


	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}

}

该接口的作用,我把这个接口一段注释搬下来,理解一下

A HandlerInterceptor gets called before the appropriate HandlerAdapter
triggers the execution of the handler itself. This mechanism can be used
for a large field of preprocessing aspects, e.g. for authorization checks,
or common handler behavior like locale or theme changes. Its main purpose
is to allow for factoring out repetitive handler code.

大致的意思就是在handler(controller中的方法)执行之前拦截器,这个机制不会产生大量的重复性代码,比如授权检查啊等等,这个第2节写过,就不再赘述了。

下面说下三个方法的功能及执行顺序
(1).preHandle()方法

该方法会在控制器方法前执行,其返回值表示是否中断后续操作。当返回值为true时,表示继续向下执行;当返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)。

(2).postHandle()方法

该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。

(3).afterCompletion()方法

该方法会在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作。

大体执行顺序是preHandle→handler(controller中的方法)→postHandle→afterCompletion

具体可以看接口中方法的注释,写的比较清晰

2.springboot中自定义拦截器

(1)实现HandlerInterceptor接口

@Slf4j
public class AuthIntercepter implements HandlerInterceptor{

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        log.info("用户认证拦截器preHandle方法执行");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("用户认证拦截器postHandle方法执行");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("用户认证拦截器afterCompletion方法执行");
    }
}

(2)向spring注册拦截器

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //需要拦截的路径,/**表示拦截所有请求
        String[] addPathPatterns={"/**"};
        //不需要拦截的路径
        String[] excludePathPatterns={"/boot/login","/boot/exit"};

        registry.addInterceptor(new AuthIntercepter())
                .addPathPatterns(addPathPatterns)
                .excludePathPatterns(excludePathPatterns);
    }
}

(3).测试

public class LoginController {

    @ResponseBody
    @GetMapping("/test")
    public void loginTest(){
        log.info("handler方法执行");
    }
}

(4).测试结果

2021-01-03 14:20:01.597  INFO 22664 --- [nio-8082-exec-2] c.thinkcoer.interceptor.AuthIntercepter  : 用户认证拦截器preHandle方法执行
2021-01-03 14:20:01.605  INFO 22664 --- [nio-8082-exec-2] c.thinkcoer.controller.LoginController   : handler方法执行
2021-01-03 14:20:01.616  INFO 22664 --- [nio-8082-exec-2] c.thinkcoer.interceptor.AuthIntercepter  : 用户认证拦截器postHandle方法执行
2021-01-03 14:20:01.617  INFO 22664 --- [nio-8082-exec-2] c.thinkcoer.interceptor.AuthIntercepter  : 用户认证拦截器afterCompletion方法执行

可以验证下面的执行顺序

preHandle→handler(controller中的方法)→postHandle→afterCompletion

其实在DispatcherServlet的doDispatch方法中也可以看出来

//如果preHandler方法返回false,则直接return结束请求
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
	return;
}

// 执行controller中的方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
	return;
}

applyDefaultViewName(processedRequest, mv);
//执行postHandler方法
mappedHandler.applyPostHandle(processedRequest, response, mv);

3.过滤器与拦截器比较

相同点

  1. 都是AOP编程思想体现
  2. 都能实现权限检查、日志记录等

不同点:

  • 1.Filter(过滤器)属于Servlet规范,拦截器属于spring容器

从这里可以延伸出,拦截器可以拿到spring容器各种bean,而过滤器是拿不到的,除非将Filter本身交给spring管理,但是经过测试doFilter方法会执行两遍

  • 2.Filter(过滤器)和拦截器执行顺序不同,Filter要先于拦截器执行

4.多个过滤器与多个拦截器协同工作

(1)在上面代码基础上新建LogInterceptor类

@Slf4j
public class LogInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("日志拦截器preHandle方法执行");
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        log.info("日志拦截器postHandle方法执行");
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        log.info("日志拦截器afterCompletion方法执行");
    }
}

(2)在InterceptorConfig类中注册

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //需要拦截的路径,/**表示拦截所有请求
        String[] addPathPatterns={"/**"};
        //不需要拦截的路径
        String[] excludePathPatterns={"/boot/login","/boot/exit"};

        registry.addInterceptor(new AuthIntercepter())
                .addPathPatterns(addPathPatterns)
                .excludePathPatterns(excludePathPatterns);
        //新注册的过滤器
        registry.addInterceptor(new LogInterceptor())
                .addPathPatterns(addPathPatterns)
                .excludePathPatterns(excludePathPatterns);
    }
}

(3).总体项目结构
在这里插入图片描述
(4).测试
打印日志如下

用户认证doFilter方法执行
处理业务逻辑,改变请求体对象和回复体对象
日志doFilter方法执行
处理业务逻辑,改变请求体对象和回复体对象
用户认证拦截器preHandle方法执行
日志拦截器preHandle方法执行
handler方法执行
日志拦截器postHandle方法执行
用户认证拦截器postHandle方法执行
日志拦截器afterCompletion方法执行
用户认证拦截器afterCompletion方法执行
用户认证filter destroy方法执行
日志filter destroy方法执行

用下面的图表示
在这里插入图片描述
注意:

  • filter的init方法和destroy方法在应用程序整个生命周期(从启动到关闭)中,只执行一次
  • afterCompletion方法一个用户请求最后执行的方法

六、SpringBoot中使用Aspect

1.基本知识

AOP、Spring AOP、Aspect的关系
首先AOP是编程思想,SpringAOP是AOP的实现,实现AOP不止SpringAOP一种,而Aspect是SpringAOP的一种实现方式,还有一种是xml配置

2.AOP相关术语

AOP并不是Spring中特有的概念,所以AOP有相关的术语去描述AOP
在这里插入图片描述
对于导图左边部分了解即可,重点是右边部分,要理解切面、通知、连接点、切点之间的关系,所以对于Spring AOP切面的使用,可以总结如下
在这里插入图片描述

3.SpringAOP如何定位切点

通过切点表达式,SpringAOP支持的表达式类型还是比较多的,主要说下execution表达式
在这里插入图片描述
下面说下Spring官网上比较难理解的两个例子
在这里插入图片描述
当然还有其他表达式,详见spring官网

4.开始实践

终于到了实践部分,下面会使用上面的步骤,用AOP实现一个用户认证的小例子
(1)引入maven坐标

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
     <version>2.1.1.RELEASE</version>
 </dependency>

(2)定义切面
新建一个AuthAspect切面类,用于用户认证功能

@Slf4j
@Aspect
@Component
@Order(1) //指定切面类执行顺序,数字越小越先执行
public class AuthAspect {

    @Pointcut(value = "execution(* com.*.controller.*.*(..))")
    public void authPointCut(){ }

    @Before(value = "authPointCut()")
    public void doBefore(JoinPoint point){
        log.info("【用户认证切面:Before方法执行了】");
    }

    @Around(value = "authPointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("【用户认证切面:执行目标方法前Around方法执行】");
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String serverName = request.getServerName();
        String queryString = request.getQueryString();
        //拿到HttpServletRequest对象就可以对权限进行校验
        //如果校验不通过,直接 return null即可,就不会请求到控制器方法
        Object proceed = joinPoint.proceed();
        log.info("【用户认证切面:执行目标方法后Around方法执行】");
        return proceed;
    }

    @After(value = "authPointCut()")
    public void doAfter(){
        log.info("【用户认证切面:After方法执行】");
    }

    @AfterReturning(returning = "ret",value = "authPointCut()")
    public void doAfterReturn(JoinPoint joinPoint,Object ret){
        log.info("【用户认证切面:AfterReturning方法执行】");
    }

    @AfterThrowing(value = "authPointCut()",throwing ="throwable")
    public void doAfterThrowing(Throwable throwable){
        log.info("【用户认证切面:AfterThrowing方法执行】");
    }
}

在上述代码Around方法中可以看出,是可以拿到用户请求的HttpServletRequest对象的

定义切面类的注意点

  • Around环绕通知中参数类型只能是ProceedingJoinPoint,不能是JoinPoint,因为JoinPoint中没有proceed方法,也就是说执行不了控制器中的方法
  • 注意在AfterThrowing及After注解中不能有JoinPoint参数

(3)测试类

@Slf4j
@RestController
public class LogController {

    @GetMapping("/log")
    public void testLog(String name,String age){
        log.info("日志controller方法执行了");
    }
}

(4)请求结果

【用户认证切面:执行目标方法前Around方法执行】
【用户认证切面:Before方法执行了】
日志controller方法执行了
【用户认证切面:执行目标方法后Around方法执行】
【用户认证切面:After方法执行】
【用户认证切面:AfterReturning方法执行】

值得注意的是,在切面中首先执行的不是Before前置通知,而是Around环绕通知proceed方法之前的代码

(5)用图表示
在这里插入图片描述
那么定义多个切面执行顺序又是怎样呢?

(6)多个切面协同工作
新建一个LogAspect,用于打印日志

@Aspect
@Slf4j
@Component
@Order(2)
public class LogAspect {

    @Pointcut(value = "execution(* com..controller..*(..)) ")
    public void logPointCut(){ }

    /***
     *方法前执行
     * @param joinPoint
     * @return
     */
    @Before("logPointCut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        log.info("【日志切面:Before方法执行了】");
        StringBuilder str = this.getMethodInfo(joinPoint);
        if (CollectionUtils.arrayToList(joinPoint.getArgs()).isEmpty()) {
            str.append("该方法无参数");
        } else {
            StringBuilder strArgs = new StringBuilder("【请求参数】:");
            for (Object o : joinPoint.getArgs()) {
                strArgs.append(o + ",");
            }
            str.append(strArgs);
        }
        log.info(str.toString());
    }


    /***
     * 于Before增强处理和AfterReturing增强,
     * Around增强处理可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标方法的执行
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("logPointCut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        log.info("【日志切面:执行目标方法前Around方法执行】");
        StringBuilder sb = this.getMethodInfo(point);

        long startTime = System.currentTimeMillis();
        //执行方法
        Object returnVal = point.proceed();
        //计算耗时
        long elapsedTime = System.currentTimeMillis() - startTime;
        log.info("【日志切面:执行目标方法后Around方法执行】");
        sb.append("【请求消耗时长" + elapsedTime + "ms】");

        log.info(sb.toString());
        return returnVal;
    }

    //注意在AfterThrowing及After注解中不能有JoinPoint参数
    @After(value = "logPointCut()")
    public void doAfter(){
        log.info("【日志切面:After方法执行了】");
    }

    /***
     * 方法执行完后执行
     * @param point
     * @param ret
     */
    @AfterReturning(returning = "ret", pointcut = "logPointCut()")
    public void doAfterReturning(JoinPoint point,Object ret) {
        log.info("【日志切面:AfterReturning方法执行了】");
        StringBuilder sb = this.getMethodInfo(point);
        if(ObjectUtils.isEmpty(ret)){
            sb.append("【请求返回结果没有返回值】");
        }else{
            sb.append("【请求返回结果】:"+ret.toString());
        }

        log.info(sb.toString());
    }

    /***
     * 请求方法信息
     * @param point
     */
    private StringBuilder getMethodInfo(JoinPoint point){
        StringBuilder sb = new StringBuilder();
        sb.append("【方法名】"+point.getSignature().getDeclaringTypeName()+"."+point.getSignature().getName());
        return sb;
    }

    @AfterThrowing(value = "logPointCut()", throwing = "throwable")
    public void doAfterThrowing(Throwable throwable) {
        log.info("【日志切面:AfterThrowing方法执行了】");
        // 保存异常日志记录
        log.error("发生异常时间:{}" +new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()));
        log.error("抛出异常:{}" + throwable.getMessage());
    }
}

测试结果

【用户认证切面:执行目标方法前Around方法执行】
【用户认证切面:Before方法执行了】
【日志切面:执行目标方法前Around方法执行】
【日志切面:Before方法执行了】
日志controller方法执行了
【日志切面:执行目标方法后Around方法执行】
【日志切面:After方法执行了】
【日志切面:AfterReturning方法执行了】
【用户认证切面:执行目标方法后Around方法执行】
【用户认证切面:After方法执行】
【用户认证切面:AfterReturning方法执行】

咱们也来画一个图更加直观的看下效果
在这里插入图片描述

七、Filter、Intercepter、Spring AOP大总结

1.三者共同点与区别

共同点

  • 三者都是AOP思想体现
  • 都可以对HttpServletRequest对象进行处理,日志、权限控制等

区别

  • Filter属于Servlet规范,Intercepter、Spring AOP属于Spring框架
  • 实现AOP的方式不同,Filter用回调函数实现,一般情况下拿不到Spring bean对象,Intercepter用责任链实现,Spring AOP基于动态代理

2.三者应用场景

先大致说下下,用户的请求的顺序,下面有更详细的,先到Servlet容器,然后过滤器→servlet(DispatcherServlet)→拦截器→SpringAOP→Controller
在这里插入图片描述
再写下在Spring AOP如何拿到http请求和响应对象

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();

3.三者执行顺序

将上面的程序一起运行,得到下面的日志

用户认证doFilter方法执行
日志doFilter方法执行
用户认证拦截器preHandle方法执行
日志拦截器preHandle方法执行
【用户认证切面:执行目标方法前Around方法执行】
【用户认证切面:Before方法执行了】
【日志切面:执行目标方法前Around方法执行】
【日志切面:Before方法执行了】
日志controller方法执行了
【日志切面:执行目标方法后Around方法执行】
【日志切面:After方法执行了】
【日志切面:AfterReturning方法执行了】
【用户认证切面:执行目标方法后Around方法执行】
【用户认证切面:After方法执行】
【用户认证切面:AfterReturning方法执行】
日志拦截器postHandle方法执行
用户认证拦截器postHandle方法执行
日志拦截器afterCompletion方法执行
用户认证拦截器afterCompletion方法执行

用下面一幅图表示
在这里插入图片描述
本文代码git地址 :
https://gitee.com/shang_jun_shu/springboot-aop

参考文献

【1】.扬俊的小屋
【2】.Servlet、JSP和Spring MVC初学指南 【加】Buid Kurniawan 【美】Paul Deck 著 林仪明 俞黎敏 译 中国工信出版社
【3】springboot 过滤器Filter vs 拦截器Interceptor vs 切片Aspect 详解
【4】Spring Aop实例@Aspect、@Before、@AfterReturning@Around 注解方式配置


创作不易,觉得有帮助的,来个三连吧
在这里插入图片描述
什么?不来,不来就不来吧,哈哈

   

更多内容详见微信公众号:Python测试和开发

Python测试和开发

原文地址:https://www.cnblogs.com/phyger/p/14272358.html