一、Spring AOP簡單介紹
1、AOP簡單介紹
- AOP(Aspect Oriented Programming),即面向切面程式設計,可以說是OOP(Object Oriented Programming,物件導向程式設計)的補充和完善。OOP引入封裝、繼承、多型等概念來建立一種物件層次結構,用於模擬公共行為的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌程式碼往往橫向地散佈在所有物件層次中,而與它對應的物件的核心功能毫無關係對於其他型別的程式碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的程式碼被稱為橫切(cross cutting),在OOP設計中,它導致了大量程式碼的重複,而不利於各個模組的重用。
- AOP技術恰恰相反,它利用一種稱為"橫切"的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未來的可操作性和可維護性。
- 使用"橫切"技術,AOP把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在核心關注點的多處,而各處基本相似,比如許可權認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。
- Spring中的AOP代理還是離不開Spring的IOC容器,代理的生成,管理及其依賴關係都是由IOC容器負責,Spring預設使用JDK動態代理,在需要代理類而不是代理介面的時候,Spring會自動切換為使用CGLIB代理,不過現在的專案都是面向介面程式設計,所以JDK動態代理相對來說用的還是多一些。
2、AOP的概念
橫切關注點:
對哪些方法進行攔截,攔截後怎麼處理,這些關注點稱之為橫切關注點(概念)- 切面(aspect):類是對物體特徵的抽象,切面就是對橫切關注點的抽象
- 連線點(joinpoint):被攔截到的點,因為Spring只支援方法型別的連線點,所以在Spring中連線點指的就是被攔截到的方法,實際上連線點還可以是欄位或者構造器
- 切入點(pointcut):就是帶有通知的連線點,在程式中主要體現為書寫切入點表示式
- 通知(advice):所謂通知指的就是指攔截到連線點之後要執行的程式碼,通知分為前置(before)、後置(afterReturning)、異常(afterThrowing)、最終(after)、環繞通知(around)五類
- 目標物件:代理的目標物件
- 織入(weave):將切面應用到目標物件並導致代理物件建立的過程
- 引入(introduction):在不修改程式碼的前提下,引入可以在執行期為類動態地新增一些方法或欄位
3、AOP開發步驟
- 定義普通業務元件
- 定義切入點,一個切入點可能橫切多個業務元件
- 定義增強處理,增強處理就是在AOP框架為普通業務元件織入的處理動作
所以進行AOP程式設計的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理物件的方法=增強處理+被代理物件的方法。
二、Spring整合AOP開發
- pom檔案新增依賴
<!--aop--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>複製程式碼
- 切面類、切入點、通知、連線點程式碼
package com.wxx.demo.aop; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Maps; import com.wxx.demo.model.HelloModel; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.PrintWriter; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; @Component @Aspect//切面類 public class HelloAspect { private static final Logger logger = LoggerFactory.getLogger(HelloAspect.class); //凡是註解了RequestMapping的方法都被攔截 @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)") private void webPointcut() { } //指定切入點 @Pointcut("execution(* com.wxx.demo.controller.HelloController.*(..)))") private void validate() { } //通知advice @Before("validate()") public void doBefore(JoinPoint joinPoint) {//通過joinpoint獲取通知的簽名資訊如目標名,引數資訊 System.out.println("========================前置通知========================"); Object[] args = joinPoint.getArgs(); joinPoint.getThis();//aop代理資訊 System.out.println("========================aop代理資訊:" + joinPoint.getThis() + "========================"); joinPoint.getTarget();//代理物件 System.out.println("========================aop代理物件:" + joinPoint.getTarget() + "========================"); Signature signature = joinPoint.getSignature(); System.out.println("========================aop通知簽名:" + signature + "========================"); String methodName = signature.getName();//代理方法名 System.out.println("========================aop代理方法名:" + methodName + "========================"); // AOP 代理的名字 System.out.println("========================aop代理的名字:" + signature.getDeclaringTypeName() + "========================"); signature.getDeclaringType();// AOP代理類的類(class)資訊 /** * 通過RequestContextHolder獲取請求資訊,如session 資訊 ; * 注: 關於呼叫 JoinPoint 和 RequestContextHolder。 通過JoinPoint可以獲得通知的簽名資訊,如目標方法名、目標方法引數資訊等。 通過RequestContextHolder來獲取請求資訊,Session資訊。 */ // 獲取RequestAttributes RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); // 從requestAttributes中獲取HttpServletRequest資訊 HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST); // 獲取session資訊 HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION); System.out.println("請求 : " + request + " , HttpSession : " + session); Enumeration<String> enumerations = request.getParameterNames(); // Map<String,String> parameterMaps=new HashMap<>(); Map<String, String> parameterMaps = Maps.newHashMap(); while (enumerations.hasMoreElements()) { String parameter = enumerations.nextElement(); parameterMaps.put(parameter, request.getParameter(parameter)); } // String str=JSON.toJSONString(parameterMaps); String str = JSON.toJSONString(parameterMaps);// alibaba.fastjson if (args.length > 0) { System.out.println("請求引數資訊為 : " + str); } } /** * 後置返回通知 * 需要注意: * 如果第一個引數是JoinPoint,則第二個引數是返回值的資訊 * 如果引數中的第一個不是JoinPoint,則第一個引數是returning中對應的引數, * returning 限定了只有目標方法返回值與通知方法相應引數型別時才能 * 執行後置返回通知,否則不執行; * 對於returning對應的通知方法引數為Object型別將匹配任何目標返回值 * @param joinPoint * @param keys * value = "execution(* com.wxx.demo.controller..*.*(..))" */ @AfterReturning(pointcut = "validate()",returning = "keys") public void doAfterReturn(JoinPoint joinPoint,Object keys){ System.out.println("========================後置返回通知執行========================"); if (keys instanceof HelloModel){ HelloModel hello = (HelloModel) keys; hello.setHello("hello aop i am @AfterReturning!"); System.out.println("========================後置返回通知修改後的引數:" + keys.toString() + "========================"); } } /** * 後置異常通知 * @param e */ @AfterThrowing(pointcut = "validate()",throwing = "e") public void doAfterThrowing(Exception e){ //if (e instanceof FieldError) Map<String,Object> map = new HashMap<>(); map.put("resCode",500); map.put("resMsg","Illegal parameters"); writeContent(JSONObject.toJSONString(map)); } /** * 後置最終通知 * */ @After(value = "validate()") public void doAfter(){ System.out.println("========================後置通知最終執行了========================"); } /** * 攔截web層異常, * 記錄異常日誌,並返回友好資訊到前端 * 目前只攔截Exception,是否要攔截Error需再做考慮 * * @param e 異常物件 */ @AfterThrowing(pointcut = "webPointcut()", throwing = "e") public void handleThrowing(Exception e) { e.printStackTrace(); logger.error("發現異常!" + e.getMessage()); logger.error(JSON.toJSONString(e.getStackTrace())); //這裡輸入友好性資訊 Map<String, Object> map = new HashMap<>(); map.put("resCode", "500"); map.put("resMsg", "laoma is dead"); writeContent(JSONObject.toJSONString(map)); } /** * 將內容輸入瀏覽器 * * @param content */ private void writeContent(String content) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); response.reset(); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Type", "text/plain;charset=UTF-8"); response.setHeader("icop-content-type", "exception"); PrintWriter writer = null; try { writer = response.getWriter(); } catch (IOException e) { e.printStackTrace(); } writer.print(content); writer.flush(); writer.close(); } } 複製程式碼
- Controller方法程式碼
/** * 引數校驗 * @param helloModel * @param bindingResult */ @GetMapping("/validate") public HelloModel validate(@Valid HelloModel helloModel, BindingResult bindingResult) { HelloValidate.validate(bindingResult); return helloModel; }複製程式碼
- 程式碼執行例項,瀏覽器訪問http://localhost:8086/api/validate?hello= &phone="123456789"執行正常的日誌資訊及頁面返回
========================前置通知======================== ========================aop代理資訊:com.wxx.demo.controller.HelloController@6cdae95d======================== ========================aop代理物件:com.wxx.demo.controller.HelloController@6cdae95d======================== ========================aop通知簽名:HelloModel com.wxx.demo.controller.HelloController.validate(HelloModel,BindingResult)======================== ========================aop代理方法名:validate======================== ========================aop代理的名字:com.wxx.demo.controller.HelloController======================== 請求 : org.apache.catalina.connector.RequestFacade@5288ea44 , HttpSession : org.apache.catalina.session.StandardSessionFacade@89f7377 請求引數資訊為 : {"phone":"“123456789”","hello":" "} ========================後置通知最終執行了======================== ========================後置返回通知執行======================== ========================後置返回通知修改後的引數:HelloModel{hello='hello aop i am @AfterReturning!', phone='“123456789”', email='null'}========================複製程式碼
- 執行異常的日誌及頁面返回
========================前置通知======================== ========================aop代理資訊:com.wxx.demo.controller.HelloController@6cdae95d======================== ========================aop代理物件:com.wxx.demo.controller.HelloController@6cdae95d======================== ========================aop通知簽名:HelloModel com.wxx.demo.controller.HelloController.validate(HelloModel,BindingResult)======================== ========================aop代理方法名:validate======================== ========================aop代理的名字:com.wxx.demo.controller.HelloController======================== 請求 : org.apache.catalina.connector.RequestFacade@5288ea44 , HttpSession : org.apache.catalina.session.StandardSessionFacade@89f7377 請求引數資訊為 : {"phone":"","hello":" "} ========================後置通知最終執行了======================== 2018-12-28 17:53:19.570 ERROR 10632 --- [nio-8086-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/api] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: 手機號不能為空!] with root cause java.lang.IllegalArgumentException: 手機號不能為空! at org.springframework.util.Assert.isTrue(Assert.java:92) ~[spring-core-4.3.21.RELEASE.jar:4.3.21.RELEASE] 複製程式碼
採坑總結:該例項為springmvc的引數校驗和異常處理用aop統一處理,學習中遇到的坑
啟動報錯
Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut
複製程式碼
原因是配置不同通知的時候引數是否配置比如: