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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索