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

文章目录

  • 1、一切要从Servlet提及html


    • 1.1什么是Servlet前端

    • 1.2为何须要Servletjava

    • 1.3Servlet如何响应用户请求git

    • 1.4Servlet与Tomcat处理请求的流程web

    • 1.5Servlet与Controller之间的关系spring

    • 1.6敲黑板,重点来了!!编程

  • 2、过滤器、拦截器、Aspect概览json

  • 3、搭建一个简单springboot项目浏览器


    • 1.项目目录结构以下springboot

    • 2.pom及application文件

    • 3.主启动类

  • 4、Springboot中自定义过滤器


    • 2.1使用@WebFilter注解

    • 2.2使用spring中的配置类方式

    • 1.过滤器基本知识

    • 2.springboot中自定义Filter


  • 5、Springboot中自定义拦截器


    • 1.拦截器基本知识

    • 2.springboot中自定义拦截器

    • 3.过滤器与拦截器比较

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

  • 6、SpringBoot中使用Aspect


    • 1.基本知识

    • 2.AOP相关术语

    • 3.SpringAOP如何定位切点

    • 4.开始实践

  • 7、Filter、Intercepter、Spring AOP大总结


    • 1.三者共同点与区别

    • 2.三者应用场景

    • 3.三者执行顺序

  • 参考文献


1、一切要从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类映射关系
以下

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

<?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中方法的加强。

2、过滤器、拦截器、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展开分享

开始实践环节

3、搭建一个简单springboot项目

1.项目目录结构以下


结构比较简单,新建一个maven工程便可

2.pom及application文件

pom依赖

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

<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

  
  
   
   
            
   
   
  • 1

  • 2

server:
port: 8082

3.主启动类

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

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

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

4、Springboot中自定义过滤器

1.过滤器基本知识

是什么

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

Filter接口

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

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

  
  
   
   
            
   
   
  • 1

void init(FilterConfig filterConfig)

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

doFilter方法

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

  
  
   
   
            
   
   
  • 1

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

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

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

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

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

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

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

destroy方法

  
  
   
   
            
   
   
  • 1

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类

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

  • 35

  • 36

  • 37

  • 38

  • 39

  • 40

  • 41

  • 42

  • 43

  • 44

  • 45

  • 46

  • 47

  • 48

  • 49

  • 50

  • 51

  • 52

  • 53

  • 54

  • 55

  • 56

  • 57

  • 58

  • 59

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

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

@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注解

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

@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)

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

@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)

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

@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

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

@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类

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

@Slf4j
@RestController
public class LogController {

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

开始验证
启动项目

  
  
   
   
            
   
   
  • 1

  • 2

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

请求方法

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

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

关闭程序

  
  
   
   
            
   
   
  • 1

  • 2

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

小总结:

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

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

5、Springboot中自定义拦截器

1.拦截器基本知识

是什么

简单一点理解拦截器就是,可以在进行某个操做以前拦截请求,若是请求符合条件就容许向下执行

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

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

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接口

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

@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注册拦截器

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

@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).测试

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

public class LoginController {

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

(4).测试结果

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 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方法中也能够看出来

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

//若是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类

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

@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类中注册

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

@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).测试
打印日志以下

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

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

用下面的图表示

注意:

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

  • afterCompletion方法一个用户请求最后执行的方法

6、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坐标

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

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

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

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

  • 35

  • 36

  • 37

  • 38

  • 39

  • 40

  • 41

  • 42

@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)测试类

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

@Slf4j
@RestController
public class LogController {

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

(4)请求结果

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

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

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

(5)用图表示

那么定义多个切面执行顺序又是怎样呢?

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

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

  • 35

  • 36

  • 37

  • 38

  • 39

  • 40

  • 41

  • 42

  • 43

  • 44

  • 45

  • 46

  • 47

  • 48

  • 49

  • 50

  • 51

  • 52

  • 53

  • 54

  • 55

  • 56

  • 57

  • 58

  • 59

  • 60

  • 61

  • 62

  • 63

  • 64

  • 65

  • 66

  • 67

  • 68

  • 69

  • 70

  • 71

  • 72

  • 73

  • 74

  • 75

  • 76

  • 77

  • 78

  • 79

  • 80

  • 81

  • 82

  • 83

  • 84

  • 85

  • 86

  • 87

  • 88

  • 89

  • 90

  • 91

  • 92

  • 93

  • 94

  • 95

  • 96

  • 97

@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());
}
}

测试结果

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

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

我们也来画一个图更加直观的看下效果

7、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请求和响应对象

  
  
   
   
            
   
   
  • 1

  • 2

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

3.三者执行顺序

将上面的程序一块儿运行,获得下面的日志

  
  
   
   
            
   
   
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

用户认证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 注解方式配置


创做不易,以为有帮助的,来个三连吧

什么?不来,不来就不来吧,哈哈


本文分享自微信公众号 - 愿天堂没有BUG(ma214617)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索