主頁 > 後端開發 > SpringBoot中AOP實作落地——Filter(過濾器)、Intercepter(攔截器)、Aspect(Spring AOP)

SpringBoot中AOP實作落地——Filter(過濾器)、Intercepter(攔截器)、Aspect(Spring AOP)

2021-01-12 11:51:23 後端開發

文章目錄

  • 一、一切要從Servlet說起
    • 1.1什么是Servlet
    • 1.2為什么需要Servlet
    • 1.3Servlet如何回應用戶請求
    • 1.4Servlet與Tomcat處理請求的流程
    • 1.5Servlet與Controller之間的關系
    • 1.6敲黑板,重點來了!!
  • 二、過濾器、攔截器、Aspect概覽
  • 三、搭建一個簡單springboot專案
    • 1.專案目錄結構如下
    • 2.pom及application檔案
    • 3.主啟動類
  • 四、Springboot中自定義過濾器
    • 1.過濾器基本知識
    • 2.springboot中自定義Filter
      • 2.1使用@WebFilter注解
      • 2.2使用spring中的配置類方式
  • 五、Springboot中自定義攔截器
    • 1.攔截器基本知識
    • 2.springboot中自定義攔截器
    • 3.過濾器與攔截器比較
    • 4.多個過濾器與多個攔截器協同作業
  • 六、SpringBoot中使用Aspect
    • 1.基本知識
    • 2.AOP相關術語
    • 3.SpringAOP如何定位切點
    • 4.開始實踐
  • 七、Filter、Intercepter、Spring AOP大總結
    • 1.三者共同點與區別
    • 2.三者應用場景
    • 3.三者執行順序
  • 參考文獻

一、一切要從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 注解方式配置


創作不易,覺得有幫助的,來個三連吧
在這里插入圖片描述
什么?不來,不來就不來吧,哈哈

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/247652.html

標籤:java

上一篇:JVM垃圾回收的時候如何確定垃圾 ? 是否知道什么是GC Roots

下一篇:倪文迪陪你學藍橋杯2021寒假每日一題:1.11日(2017省賽A第9題)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more