Spring-boot整合AOP及AOP相關學習

王小胖醬發表於2018-12-28

一、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開發步驟

  1. 定義普通業務元件
  2. 定義切入點,一個切入點可能橫切多個業務元件
  3. 定義增強處理,增強處理就是在AOP框架為普通業務元件織入的處理動作

所以進行AOP程式設計的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理物件的方法=增強處理+被代理物件的方法。

二、Spring整合AOP開發

  1. pom檔案新增依賴

    <!--aop-->
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>複製程式碼

  2. 切面類、切入點、通知、連線點程式碼

    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();
        }
    
    }
    
    複製程式碼

  3. Controller方法程式碼

    /**
     * 引數校驗
     * @param helloModel
     * @param bindingResult
     */
    @GetMapping("/validate")
    public HelloModel validate(@Valid HelloModel helloModel, BindingResult bindingResult) {
        HelloValidate.validate(bindingResult);
        return helloModel;
    }複製程式碼

  4. 程式碼執行例項,瀏覽器訪問http://localhost:8086/api/validate?hello= &phone="123456789"執行正常的日誌資訊及頁面返回Spring-boot整合AOP及AOP相關學習


    ========================前置通知========================
    ========================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'}========================複製程式碼

  5. 執行異常的日誌及頁面返回

    ========================前置通知========================
    ========================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]
    複製程式碼


Spring-boot整合AOP及AOP相關學習

採坑總結:該例項為springmvc的引數校驗和異常處理用aop統一處理,學習中遇到的坑

啟動報錯

Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut 
複製程式碼

原因是配置不同通知的時候引數是否配置比如:

Spring-boot整合AOP及AOP相關學習Spring-boot整合AOP及AOP相關學習



相關文章