文章目錄
- 一、一切要從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.過濾器與攔截器比較
相同點
- 都是AOP編程思想體現
- 都能實作權限檢查、日志記錄等
不同點:
- 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
