Spring?AOP簡(jiǎn)介及統(tǒng)一處理
一.Spring AOP
1.什么是Spring AOP
AOP(Aspect Oriented Programming):面向切面編程,它是一種思想,它是對(duì)某一類事情的集中處理。
2.AOP的作用
想象一個(gè)場(chǎng)景,我們?cè)谧龊笈_(tái)系統(tǒng)時(shí),除了登錄和注冊(cè)等幾個(gè)功能不需要做用戶登錄驗(yàn)證之外,其他幾乎所有頁(yè)面調(diào)用的前端控制器( Controller)都需要先驗(yàn)證用戶登錄的狀態(tài),那這個(gè)時(shí)候我們要怎么處 理呢?我們之前的處理方式是每個(gè) Controller 都要寫一遍用戶登錄驗(yàn)證,然而當(dāng)你的功能越來(lái)越多,那么你要 寫的登錄驗(yàn)證也越來(lái)越多,而這些方法又是相同的,這么多的方法就會(huì)代碼修改和維護(hù)的成本。那有沒 有簡(jiǎn)單的處理方案呢?答案是有的,對(duì)于這種功能統(tǒng)一,且使用的地方較多的功能,就可以考慮 AOP 來(lái)統(tǒng)一處理了。
- 除了統(tǒng)一的用戶登錄判斷之外,AOP 還可以實(shí)現(xiàn):
- 統(tǒng)一日志記錄
- 統(tǒng)一方法執(zhí)行時(shí)間統(tǒng)計(jì)
- 統(tǒng)一的返回格式設(shè)置
- 統(tǒng)一的異常處理
- 事務(wù)的開啟和提交等
- AOP是OOP的補(bǔ)充
3.AOP的相關(guān)概念
1.切面(Aspect)
面(Aspect)由切點(diǎn)(Pointcut)和通知(Advice)組成,它既包含了橫切邏輯的定義,也包
括了連接點(diǎn)的定義。
2.連接點(diǎn) (Join Point)
應(yīng)用執(zhí)行過(guò)程中能夠插入面的一個(gè)點(diǎn),這個(gè)點(diǎn)可以是方法調(diào)用時(shí),拋出異常時(shí),甚至修改字段 時(shí)。切面代碼可以利用這些點(diǎn)插入到應(yīng)用的正常流程之中,并添加新的行為
3.切點(diǎn)(Pointcut)
Pointcut 是匹配 Join Point 的謂詞。 Pointcut 的作用就是提供一組規(guī)則(使用 AspectJ pointcut expression language 來(lái)描述)來(lái) 匹配 Join Point,給滿足規(guī)則的 Join Point 添加 Advice
4.通知(Advice)
切面也是有目標(biāo)的 ——它必須完成的工作。在 AOP 術(shù)語(yǔ)中,切面的工作被稱之為通知。
通知:定義了切面是什么,何時(shí)使用,其描述了面要完成的工作,還解決何時(shí)執(zhí)行這個(gè)工作的
問(wèn)題。
Spring 切面類中,可以在方法上使用以下注解,會(huì)設(shè)置方法為通知方法,在滿足條件后會(huì)通知本
方法進(jìn)行調(diào)用:
- 前置通知使用 @Before:通知方法會(huì)在目標(biāo)方法調(diào)用之前執(zhí)行。
- 后置通知使用 @After:通知方法會(huì)在目標(biāo)方法返回或者拋出異常后調(diào)用。
- 返回之后通知使用 @AfterReturning:通知方法會(huì)在目標(biāo)方法返回后調(diào)用。
- 拋異常后通知使用 @AfterThrowing:通知方法會(huì)在目標(biāo)方法拋出異常后調(diào)用。
- 環(huán)繞通知使用 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和調(diào)用之后執(zhí)
- 行自定義的行為。
4.Spring AOP的實(shí)現(xiàn)
1.添加 AOP 框架支持
在 pom.xml 中添加如下配置:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Springboot test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring AOP 框架--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
2.定義切面和切點(diǎn)
@Component @Slf4j @Aspect public class LoginAspect { @Pointcut("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))") public void pointcut() { } }
@Aspect類注解表示LoginAspect是一個(gè)切面類,@Pointcut表示定義一個(gè)切點(diǎn),其中的內(nèi)容表示連接點(diǎn)的規(guī)則,也就是包括哪些類或者方法屬于這個(gè)切點(diǎn),連接點(diǎn).
其中 pointcut 方法為空方法,它不需要有方法體,此方法名就是起到?個(gè)“標(biāo)識(shí)”的作用,標(biāo)識(shí)下面的
通知方法具體指的是哪個(gè)切點(diǎn)(因?yàn)榍悬c(diǎn)可能有很多個(gè))
AspectJ 支持三種通配符
* :匹配任意字符,只匹配?個(gè)元素(包,類,或方法,方法參數(shù))
.. :匹配任意字符,可以匹配多個(gè)元素 ,在表示類時(shí),必須和 * 聯(lián)合使用。
+ :表示按照類型匹配指定類的所有類,必須跟在類名后面,如 com.cad.Car+ ,表示繼承該類的
所有子類包括本身
切點(diǎn)表達(dá)式由切點(diǎn)函數(shù)組成,其中 execution() 是最常用的切點(diǎn)函數(shù),用來(lái)匹配方法,語(yǔ)法為:
execution(<修飾符><返回類型><包.類.方法(參數(shù))><異常>)
修飾符和異??梢允÷?,具體含義如下:
上面我們定義的含義為,匹配com.javastudy.springaopdemo5.controller.LoginController類下所有的方法.
3.定義相關(guān)通知
先來(lái)定義controller層的內(nèi)容
@RestController @Slf4j @RequestMapping("/user") public class LoginController { @RequestMapping("/login") public String login() { log.info("login..."); return "login..."; } @RequestMapping("/register") public String register() { log.info("register..."); return "register..."; } @RequestMapping("/get") public String get() { log.info("get..."); return "get..."; } }
1.前置通知 @Before
通知方法會(huì)在目標(biāo)方法調(diào)用之前執(zhí)行。
注意:@Before里面的內(nèi)容表示切點(diǎn),即在哪些接口中執(zhí)行這些通知.
@Component @Slf4j @Aspect public class LoginAspect { @Pointcut("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))") public void pointcut() { } //前置通知 @Before("pointcut()") public void doBefore() { log.info("do before...."); } }
當(dāng)我們?cè)L問(wèn)任意一個(gè)頁(yè)面的時(shí)候,控制臺(tái)打印如下日志
可以看到都會(huì)先執(zhí)行doBefore通知.
2.后置通知 @After
通知方法會(huì)在目標(biāo)方法返回或者拋出異常后調(diào)用。
//后置通知 @After("pointcut()") public void doAfter() { log.info("do after..."); }
模擬異常情況,可以在LoginController中某一個(gè)方法加10/0,觀察
@RequestMapping("/login") public String login() { log.info("login..."); int i=10/0; return "login..."; }
可以觀察到在方法異常之后, @After通知仍然會(huì)執(zhí)行.
3.返回之后通知 @AfterReturning
通知方法會(huì)在目標(biāo)方法返回后調(diào)用。
// return 之前通知 @AfterReturning("pointcut()") public void doAfterReturning() { log.info("do after returning..."); }
當(dāng)我們?cè)L問(wèn)login接口的時(shí)候(10/0,有異常),觀察是否有輸出
此時(shí)是沒有輸出的.
訪問(wèn)其它接口,沒有異常的接口
此時(shí)@AfterReturning通知正常執(zhí)行
4.拋異常后通知 @AfterThrowing
通知方法會(huì)在目標(biāo)方法拋出異常后調(diào)用。
//拋出異常之前通知 @AfterThrowing("pointcut()") public void doAfterThrowing() { log.info("do after throwing"); }
執(zhí)行有異常的接口,有日志的打印
執(zhí)行沒有異常的接口,沒有日志的打印
5.環(huán)繞通知 @Around
通知包裹了被通知的方法,在被通知的方法通知之前和調(diào)用之后執(zhí)行自定義的行為。
@Around("pointcut()") public Object doAround(ProceedingJoinPoint joinPoint) { Object oj = null; log.info("環(huán)繞通知執(zhí)行之前..."); log.info(joinPoint.getSignature().toLongString()); try { oj = joinPoint.proceed();//調(diào)用目標(biāo)方法 } catch (Throwable e) { throw new RuntimeException(e); } log.info("環(huán)繞通知執(zhí)行之后...."); return oj; }
執(zhí)行沒有異常的接口:
執(zhí)行有異常的接口:
接下來(lái)我們看看ProceedingJoinPoint 常用方法
toString | 連接點(diǎn)所在位置的相關(guān)信息 |
toShortString | 連接點(diǎn)所在位置的簡(jiǎn)短相關(guān)信息 |
toLongString | 連接點(diǎn)所在位置的全部相關(guān)信息 |
getThis | 返回AOP代理對(duì)象,也就是com.sun.proxy.$Proxy18 |
getTarget | 返回目標(biāo)對(duì)象(定義方法的接口或類) |
getArgs() | 返回被通知方法參數(shù)列表 |
getSignature | 返回當(dāng)前連接點(diǎn)簽名,其getName()方法返回方法的FQN |
執(zhí)行所有的通知,觀察環(huán)繞通知和前置通知和后置通知的先后順序
可以觀察到,環(huán)繞通知先于before,后于after.
如果一個(gè)切點(diǎn)只含有一個(gè)通知,那么我們可以將切點(diǎn)的規(guī)則放在通知上
@Component @Slf4j @Aspect public class LoginAspect { //前置通知 @Before("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))") public void doBefore() { log.info("do before...."); } }
4.Spring AOP 實(shí)現(xiàn)原理
Spring AOP 是構(gòu)建在動(dòng)態(tài)代理基礎(chǔ)上,因此 Spring 對(duì) AOP 的支持局限于方法級(jí)別的攔截。
Spring AOP 支持 JDK Proxy 和 CGLIB 方式實(shí)現(xiàn)動(dòng)態(tài)代理。默認(rèn)情況下,實(shí)現(xiàn)了接口的類,使
用 AOP 會(huì)基于 JDK 生成代理類,沒有實(shí)現(xiàn)接口的類,會(huì)基于 CGLIB 生成代理類。
代理模式:
1.靜態(tài)代理
1.定義接口
public interface PayService { void pay(); }
2.實(shí)現(xiàn)接口
public class AliPayService implements PayService{ @Override public void pay() { System.out.println("ali pay..."); } }
3.創(chuàng)建代理類, 并同樣實(shí)現(xiàn)支付接口
public class StaticProxy implements PayService { private final PayService payService; public StaticProxy(PayService payService) { this.payService = payService; } @Override public void pay() { System.out.println("before..."); payService.pay(); System.out.println("after..."); } }
4.實(shí)際使用
public static void main(String[] args) { PayService service = new AliPayService(); PayService proxy = new StaticProxy(service); proxy.pay(); }
靜態(tài)代理有個(gè)很大的缺點(diǎn),就是當(dāng)有很多不同的接口的時(shí)候,我們需要定義很多個(gè)代理類實(shí)現(xiàn)不同的接口,當(dāng)我們代理實(shí)現(xiàn)的功能相同的時(shí)候,但是有多個(gè)接口,此時(shí)完成這么多代理類很麻煩,此時(shí)需要我們的動(dòng)態(tài)代理.
2.動(dòng)態(tài)代理
1.JDK動(dòng)態(tài)代理
從 JVM 角度來(lái)說(shuō),動(dòng)態(tài)代理是在運(yùn)行時(shí)動(dòng)態(tài)生成類字節(jié)碼,并加載到 JVM 中 的。
就 Java 來(lái)說(shuō),動(dòng)態(tài)代理的實(shí)現(xiàn)方式有很多種,比如 JDK 動(dòng)態(tài)代理、CGLIB 動(dòng)態(tài) 代理等等。
定義JDK動(dòng)態(tài)代理類
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * @author Chooker * @create 2023-07-27 22:36 */ public class JDKInvocationHandler implements InvocationHandler { //?標(biāo)對(duì)象即就是被代理對(duì)象 private Object target; public JDKInvocationHandler(Object target) { this.target = target; } //proxy代理對(duì)象 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //1.安全檢查 System.out.println("安全檢查"); //2.記錄?志 System.out.println("記錄?志"); //3.時(shí)間統(tǒng)計(jì)開始 System.out.println("記錄開始時(shí)間"); //通過(guò)反射調(diào)?被代理類的?法 Object retVal = method.invoke(target, args); //4.時(shí)間統(tǒng)計(jì)結(jié)束 System.out.println("記錄結(jié)束時(shí)間"); return retVal; } }
創(chuàng)建?個(gè)代理對(duì)象并使用
public static void main(String[] args) { PayService target = new AliPayService(); //創(chuàng)建?個(gè)代理類:通過(guò)被代理類、被代理實(shí)現(xiàn)的接?、?法調(diào)?處理器來(lái)創(chuàng)建 PayService proxy = (PayService) Proxy.newProxyInstance( target.getClass().getClassLoader(), new Class[]{PayService.class}, new JDKInvocationHandler(target) ); proxy.pay(); }
缺點(diǎn):JDK的動(dòng)態(tài)代理必須有接口
2.CGLIB動(dòng)態(tài)代理
CGLIB 動(dòng)態(tài)代理類使用步驟
1. 定義一個(gè)類;
2. 自定義 MethodInterceptor 并重寫 intercept 方法,intercept 用于攔截增強(qiáng)
被代理類的方法,和 JDK 動(dòng)態(tài)代理中的 invoke 方法類似;
3. 通過(guò) Enhancer 類的 create()創(chuàng)建代理類
添加依賴(如果創(chuàng)建的是一個(gè)Spring項(xiàng)目,不需要引入,因?yàn)镾pring底層已經(jīng)引入了cglib框架)
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
自定義 MethodInterceptor(方法攔截器)
import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * @author Chooker * @create 2023-07-27 22:48 */ public class CGLIBInterceptor implements MethodInterceptor { //被代理對(duì)象 private Object target; public CGLIBInterceptor(Object target) { this.target = target; } @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { //1.安全檢查 System.out.println("安全檢查"); //2.記錄?志 System.out.println("記錄?志"); //3.時(shí)間統(tǒng)計(jì)開始 System.out.println("記錄開始時(shí)間"); //通過(guò)cglib的代理?法調(diào)? Object retVal = methodProxy.invoke(target, args); //4.時(shí)間統(tǒng)計(jì)結(jié)束 System.out.println("記錄結(jié)束時(shí)間"); return retVal; } }
1. obj : 被代理的對(duì)象(需要增強(qiáng)的對(duì)象)
2. method : 被攔截的方法(需要增強(qiáng)的方法)
3. args : 方法入?yún)?/p>
4. proxy : 用于調(diào)用原始方法
創(chuàng)建代理類, 并使用
public static void main(String[] args) { PayService target = new AliPayService(); PayService proxy = (PayService) Enhancer.create(target.getClass(), new CGLIBInterceptor(target)); proxy.pay(); }
JDK 動(dòng)態(tài)代理和 CGLIB 動(dòng)態(tài)代理對(duì)比
1. JDK 動(dòng)態(tài)代理只能代理實(shí)現(xiàn)了接口的類或者直接代理接口,而 CGLIB 可以代 理未實(shí)現(xiàn)任何接口的類。
2. CGLIB 動(dòng)態(tài)代理是通過(guò)生成?個(gè)被代理類的子類來(lái)攔截被代理類的方法調(diào)用,因此不能代理聲明為 final
性能: 大部分情況都是 JDK 動(dòng)態(tài)代理更優(yōu)秀,隨著 JDK 版本的升級(jí),這個(gè)優(yōu)勢(shì)更 加明顯。
Spring代理選擇
1. proxyTargetClass 為false, 目標(biāo)實(shí)現(xiàn)了接口, 用jdk代理
2. proxyTargetClass 為false, 目標(biāo)未實(shí)現(xiàn)接口, 用cglib代理
3. proxyTargetClass 為true, 用cglib代理
織入(Weaving):代理的生成時(shí)機(jī)織入是把切面應(yīng)用到目標(biāo)對(duì)象并創(chuàng)建新的代理對(duì)象的過(guò)程,切面在指定的連接點(diǎn)被織入到目標(biāo)對(duì)象中。在目標(biāo)對(duì)象的生命周期里有多個(gè)點(diǎn)可以進(jìn)行織入:
- 編譯期:切面在目標(biāo)類編譯時(shí)被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
- 類加載期:切面在目標(biāo)類加載到JVM時(shí)被織入。這種方式需要特殊的類加載器(ClassLoader),它可以在目標(biāo)類被引入應(yīng)用之前增強(qiáng)該目標(biāo)類的字節(jié)碼。AspectJ5的加載時(shí)織入(load-time weaving. LTW)就支持以這種方式織入切面。
- 運(yùn)行期:切面在應(yīng)用運(yùn)行的某?時(shí)刻被織入。?般情況下,在織入切面時(shí),AOP容器會(huì)為目標(biāo)對(duì)象動(dòng)態(tài)創(chuàng)建?個(gè)代理對(duì)象。SpringAOP就是以這種方式織入切面的。
上面我們學(xué)習(xí)的是Spring AOP的原理,是接下來(lái)我們學(xué)習(xí)內(nèi)容的底層,SpringBoot一些常見的功能進(jìn)行了封裝,底層使用AOP實(shí)現(xiàn)的
二.SpringBoot 統(tǒng)一功能處理
需要實(shí)現(xiàn)用戶的登錄權(quán)限的校驗(yàn)功能,在Servlet階段,我們可以通過(guò)在Session中保存用戶的信息,之后每個(gè)頁(yè)面先通過(guò)session中判斷是否存在用戶的信息,如果存在說(shuō)明用戶已經(jīng)登錄過(guò)了,沒有就跳轉(zhuǎn)到登錄的頁(yè)面.
1.Spring AOP 用戶統(tǒng)一登錄驗(yàn)證的問(wèn)題
我們第一時(shí)間想到的就是通過(guò)環(huán)繞通知來(lái)解決這個(gè)問(wèn)題,可以對(duì)除了登錄和注冊(cè)的頁(yè)面采用環(huán)繞通知,用來(lái)判斷用戶是否登錄過(guò)了.但是會(huì)出現(xiàn)以下兩個(gè)問(wèn)題
1.. 沒辦法獲取到 HttpSession 對(duì)象。
2. 我們要對(duì)一部分方法進(jìn)行攔截,而另一部分方法不攔截,如注冊(cè)方法和登錄方法是不攔截的,這樣 的話排除方法的規(guī)則很難定義,甚至沒辦法定義。
那么該如何解決呢?
2.Spring 攔截器
對(duì)于以上問(wèn)題 Spring 中提供了具體的實(shí)現(xiàn)攔截器:HandlerInterceptor,攔截器的實(shí)現(xiàn)分為以下兩個(gè)步驟:
1. 創(chuàng)建自定義攔截器,實(shí)現(xiàn) HandlerInterceptor 接口的 preHandle(執(zhí)行具體方法之前的預(yù)處理)方法。
2. 將自定義攔截器加入 WebMvcConfigurer 的 addInterceptors 方法中
1.自定義攔截器
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session != null && session.getAttribute("username") != null) { //通過(guò) return true; } //沒有權(quán)限訪問(wèn) response.setStatus(401); return false; } }
2.將自定義攔截器加入到系統(tǒng)配置
@Configuration public class AppConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor). //表示攔截所有的路徑 addPathPatterns("/**"). //不攔截login接口 excludePathPatterns("/login"). //不攔截register接口 excludePathPatterns("/register"); } }
其中:
addPathPatterns:表示需要攔截的 URL,“**”表示攔截任意方法(也就是所有方法)。
excludePathPatterns:表示需要排除的 URL。
說(shuō)明:以上攔截規(guī)則可以攔截此項(xiàng)目中的使用 URL,包括靜態(tài)文件(圖片文件、JS 和 CSS 等文件)。
排除所有的靜態(tài)資源
// 攔截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") // 攔截所有接? .excludePathPatterns("/**/*.js") .excludePathPatterns("/**/*.css") .excludePathPatterns("/**/*.jpg") .excludePathPatterns("/login.html") .excludePathPatterns("/**/login"); // 排除接? }
拓展以下,可以在里面添加統(tǒng)一前綴的添加
@Configuration public class AppConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**")// 攔截所有url .excludePathPatterns("/api/user/login") .excludePathPatterns("/api/user/reg"); } @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.addPathPrefix("api", c -> true); } }
3.controller接口模仿登錄
@RestController @Slf4j @RequestMapping("/user") public class LoginController { @RequestMapping("/login") public boolean login(HttpServletRequest request, String username, String password) { log.info("login..."); if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) { return false; } //此時(shí)表示賬號(hào)密碼正確 if ("admin".equals(username) && "123456".equals(password)) { HttpSession session = request.getSession(true); session.setAttribute("username", username); return true; } return false; } @RequestMapping("/register") public String register() { log.info("register..."); return "register..."; } @RequestMapping("/get") public String get() { log.info("get..."); return "get..."; } }
當(dāng)我們直接訪問(wèn)get接口的時(shí)候:顯示的是401,表示沒有權(quán)限
正常訪問(wèn)login和register接口都是可以實(shí)現(xiàn)的
此時(shí)我們使用正確的賬號(hào)密碼登錄:可以看到此時(shí)已經(jīng)正確登錄了
此時(shí)我們?cè)俅卧L問(wèn)get接口:可以看到此時(shí)正確訪問(wèn)
4.攔截器的實(shí)現(xiàn)原理
正常情況下的調(diào)用順序:
然而有了攔截器之后,會(huì)在調(diào)用 Controller 之前進(jìn)行相應(yīng)的業(yè)務(wù)處理,執(zhí)行的流程如下圖所示:
攔截器是基于AOP的,Spring是基于Servlet的
三.統(tǒng)一異常處理
統(tǒng)一異常處理使用的是 @ControllerAdvice + @ExceptionHandler 來(lái)實(shí)現(xiàn)的,@ControllerAdvice 表示控制器通知類,@ExceptionHandler 是異常處理器,兩個(gè)結(jié)合表示當(dāng)出現(xiàn)異常的時(shí)候執(zhí)行某個(gè)通知, 也就是執(zhí)行某個(gè)方法事件,具體實(shí)現(xiàn)代碼如下
@ControllerAdvice @ResponseBody public class ErrorHandler { @ExceptionHandler(Exception.class) public Object error(Exception e) { HashMap<String, Object> map = new HashMap<>(); map.put("success", 0); map.put("status", -1); map.put("msg", e.getMessage()); return map; } @ExceptionHandler(NullPointerException.class) public Object error2(NullPointerException e) { HashMap<String, Object> result = new HashMap<>(); result.put("success", 0); result.put("status", -2); result.put("message", "空指針異常:" + e.getMessage()); return result; } @ExceptionHandler(ArithmeticException.class) public Object error2(ArithmeticException e) { HashMap<String, Object> result = new HashMap<>(); result.put("success", 0); result.put("status", -3); result.put("message", "算數(shù)異常:" + e.getMessage()); return result; } }
controller
@RestController @Slf4j @RequestMapping("/error") public class ErrorController { @RequestMapping("/test1") public boolean test1() { int i = 10 / 0; return true; } @RequestMapping("/test2") public boolean test2() { String a = null; a.length(); return true; } @RequestMapping("/test3") public String test3() { throw new RuntimeException("test3手動(dòng)創(chuàng)建異常"); } }
當(dāng)有多個(gè)異常通知時(shí),匹配順序?yàn)楫?dāng)前類及其子類向上依次匹配
訪問(wèn)test1
訪問(wèn)test2
訪問(wèn)test3
可以觀察到當(dāng)錯(cuò)誤異常為子類的時(shí)候,匹配順序?yàn)楫?dāng)前類及其子類向上依次匹配
四.統(tǒng)一數(shù)據(jù)返回格式
1.為什么需要統(tǒng)一返回格式
統(tǒng)一數(shù)據(jù)返回格式的優(yōu)點(diǎn)有很多,比如以下幾個(gè):
- 方便前端程序員更好的接收和解析后端數(shù)據(jù)接口返回的數(shù)據(jù)。
- 降低前端程序員和后端程序員的溝通成本,按照某個(gè)格式實(shí)現(xiàn)就行了,因?yàn)樗薪涌诙际沁@樣返回的。
- 有利于項(xiàng)目統(tǒng)一數(shù)據(jù)的維護(hù)和修改。
- 有利于后端技術(shù)部門的統(tǒng)一規(guī)范的標(biāo)準(zhǔn)制定,不會(huì)出現(xiàn)稀奇古怪的返回內(nèi)容
2.統(tǒng)一數(shù)據(jù)返回格式的實(shí)現(xiàn)
統(tǒng)一的數(shù)據(jù)返回格式可以使? @ControllerAdvice + ResponseBodyAdvice 的方式實(shí)現(xiàn),具體實(shí)現(xiàn)代碼如下:
@ControllerAdvice public class ResponseHandler implements ResponseBodyAdvice { /** * 內(nèi)容是否需要重寫(通過(guò)此?法可以選擇性部分控制器和?法進(jìn)?重寫) * 返回 true 表示重寫 */ @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } /** * ?法返回之前調(diào)?此?法 */ @SneakyThrows @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 構(gòu)造統(tǒng)?返回對(duì)象 HashMap<String, Object> result = new HashMap<>(); result.put("state", 1); result.put("msg", ""); result.put("data", body); if(body instanceof String){ ObjectMapper objectMapper = new ObjectMapper(); return objectMapper.writeValueAsString(result); } return result; } }
假如沒有if((body instanceof String)這一段代碼,會(huì)發(fā)生如下的錯(cuò)誤
controller:
@RestController @Slf4j @RequestMapping("/user") public class LoginController { @RequestMapping("/login") public boolean login(HttpServletRequest request, String username, String password) { log.info("login..."); if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) { return false; } //此時(shí)表示賬號(hào)密碼正確 if ("admin".equals(username) && "123456".equals(password)) { HttpSession session = request.getSession(true); session.setAttribute("username", username); return true; } return false; } @RequestMapping("/register") public String register() { log.info("register..."); return "register..."; } @RequestMapping("/get") public String get() { log.info("get..."); return "get..."; } }
到此這篇關(guān)于Spring AOP簡(jiǎn)介及統(tǒng)一處理的文章就介紹到這了,更多相關(guān)Spring AOP內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot集成mybatis實(shí)現(xiàn)多數(shù)據(jù)源配置詳解流程
在日常開發(fā)中,若遇到多個(gè)數(shù)據(jù)源的需求,怎么辦呢?通過(guò)springboot集成mybatis實(shí)現(xiàn)多數(shù)據(jù)源配置,簡(jiǎn)單嘗試一下,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06SpringBoot 請(qǐng)求參數(shù)忽略大小寫的實(shí)例
這篇文章主要介紹了SpringBoot 請(qǐng)求參數(shù)忽略大小寫的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01springboot實(shí)現(xiàn)異步調(diào)用@Async的示例
這篇文章主要介紹了springboot實(shí)現(xiàn)異步調(diào)用@Async的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12java使用篩選法求n以內(nèi)的素?cái)?shù)示例(java求素?cái)?shù))
這篇文章主要介紹了java使用篩選法求n以內(nèi)的素?cái)?shù)示例(java求素?cái)?shù)),需要的朋友可以參考下2014-04-04Java使用線程池批量處理數(shù)據(jù)操作具體流程
這篇文章主要給大家介紹了關(guān)于Java使用線程池批量處理數(shù)據(jù)操作的相關(guān)資料,Java多線程編程中線程池是一個(gè)非常重要的概念,線程池可以提高線程的復(fù)用率和任務(wù)調(diào)度的效率,尤其是當(dāng)需要查詢大批量數(shù)據(jù)時(shí),需要的朋友可以參考下2023-06-06