文章目錄
一、一切要從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 注解方式配置
創作不易,覺得有幫助的,來個三連吧
什么?不來,不來就不來吧,哈哈