1. 程式人生 > >Spring AOP專案應用——方法入參校驗 & 日誌橫切

Spring AOP專案應用——方法入參校驗 & 日誌橫切

應用一:方法入參校驗

由於系統多個方法入參均對外封裝了統一的Dto,其中Dto中幾個必傳引數在每個方法中都會進行相同的校驗邏輯。筆者考慮採用Spring AOP進行優化,攔截方法進行引數校驗。測試case實現如下:

Before

/**
 * 被代理的目標類
 */
@Service
public class PayOrderTarget {
    @Autowired
    private PaymentOrderService paymentOrderService;

    public Result testQuery(QueryPaymentDto paymentOrderDto){
        PaymentOrderDto paymentOrderDto1 = paymentOrderService.queryPaymentOrder(paymentOrderDto);
        return ResultWrapper.success(paymentOrderDto1);
    }
}
/**
 * 通知類,橫切邏輯
 */
@Component
@Aspect
public class Advices {

    @Before("execution(* com.payment.util.springaop.PayOrderTarget.*(..))")
    public Result before(JoinPoint proceedingJoinPoint) {
//        攔截獲取入參
        Object[] args = proceedingJoinPoint.getArgs();
//        入參校驗
        if (args ==null){
            return ResultWrapper.fail();
        }
        
        String input = JSON.toJSON(args).toString();
        Map<String, String> map = Splitter.on(",").withKeyValueSeparator(":").split(input);
        if (map.containsKey("businessId") || map.containsKey("payOrderId")){
            System.out.println("key null");
            return ResultWrapper.fail();
        }
        if (map.get("businessId")==null || map.get("payOrderId")==null ){
            System.out.println("value null");
            return ResultWrapper.fail();
        }

        System.out.println("----------前置通知----------");
        System.out.println(proceedingJoinPoint.getSignature().getName());
        return ResultWrapper.success();
    }
}

測試類正常呼叫查詢方法

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:spring/spring-context.xml"})
public class Test {

    @Autowired
    private PayOrderTarget payOrderTarget;

    @org.junit.Test
    public void test(){
        QueryPaymentDto paymentOrderDto=new QueryPaymentDto();
        paymentOrderDto.setPayOrderId(11l);
        paymentOrderDto.setBusinessId(112L);
        paymentOrderDto.setPageSize(1);
        payOrderTarget.testQuery(paymentOrderDto);
    }
}

執行結果可對入參Dto進行攔截,執行before中的校驗邏輯。但即便return fail之後,目標方法還是會被執行到。筆者是想實現引數校驗失敗,則直接返回,不執行接下來查詢db的操作。此時則需要使用Around切入。

Around

    @Around("execution(* com.payment.util.springaop.PayOrderTarget.*(..))")
    public Result around(ProceedingJoinPoint proceedingJoinPoint) {
//        獲取java陣列
        Object[] args = proceedingJoinPoint.getArgs();
        JSONArray jsonArray = JSONArray.parseArray(JSONArray.toJSONString(args));
        String businessId = null;
        String payOrderId = null;
        for (int i = 0; i < jsonArray.size(); i++) {
            businessId = jsonArray.getJSONObject(0).getString("businessId");
            payOrderId = jsonArray.getJSONObject(0).getString("payOrderId");
        }
;
        //引數校驗
        if (businessId == null || payOrderId == null) {
            System.out.println("value null");
            return ResultWrapper.fail();
        }
        //執行目標方法
        try {
            proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("----------Around通知----------");
        return ResultWrapper.success();
    }

簡單介紹下,before和after切入都是接收JoinPoint物件,該物件可獲取切點(即被代理物件)的入參、方法名等資料

方法名功能
Signature getSignature();獲取封裝了署名資訊的物件,在該物件中可以獲取到目標方法名,所屬類的Class等資訊
Object[] getArgs();獲取傳入目標方法的引數物件
Object getTarget();獲取被代理的物件
Object getThis();獲取代理物件

而Around接收ProceedingJoinPoint該介面繼承自JoinPoint,新增瞭如下兩個方法,通過呼叫proceedingJoinPoint.proceed方法才控制目標方法的呼叫。則在如上around中,進行引數校驗,不合法則return,未進入到proceedingJoinPoint.proceed()處,達到方法不合規直接返回不呼叫查詢邏輯。

方法名功能
Object proceed() throws Throwable 執行目標方法
Object proceed(Object[] var1) throws Throwable傳入的新的引數去執行目標方法

注:本case採用註解宣告,直接可執行。xml中增加如下配置(支援自動裝配@Aspect註解的bean)即可。

<aop:aspectj-autoproxy proxy-target-class="true"/>

應用二:日誌處理

實現將日誌列印抽取,成為一個公共類,切入到目標類的方法入口、出口處列印方法名+引數資訊;這個case重點引用了幾種不同的AOP增強方式,簡單介紹如下:

Before 在方法被呼叫之前呼叫通知
Around 通知包裹了被通知的方法,在被通知的方法呼叫之前和呼叫之後執行自定義的行為
After 在方法完成之後呼叫通知,無論方法執行是否成功
After-returning在方法返回結果後執行通知
After-throwing在方法丟擲異常後呼叫通知
/**
 * login介面類 被代理介面類
 */
public interface ILoginService {
    boolean login(String userName, String password);
    void quary() throws Exception;
}

/**
 * login實現類,被代理目標類
 */
@Service
public class LoginServiceImpl implements ILoginService {

    public boolean login(String userName, String password) {
        System.out.println("login:" + userName + "," + password);
        return true;
    }
    @Override
    public void quary() throws Exception{
        System.out.println("******quary*******");
//        測試方法丟擲異常後,解注After-throwing配置,會執行logArg切入,不拋異常不執行切點
//        int i=10/0;
    }
}

日誌包裝類,將各類日誌情況進行方法封裝

/**
 * 日誌處理類
 */
public interface ILogService {
    //無參的日誌方法
    void log();

    //有參的日誌方法
    void logArg(JoinPoint point);

    //有參有返回值的方法
    void logArgAndReturn(JoinPoint point, Object returnObj);
}

/**
 * 日誌實現類
 */
@Component("logService")
@Aspect
public class LogServiceImpl implements ILogService {

    @Override
    public void log() {
        System.out.println("*************Log*******************");
    }
    @Override
    public void logArg(JoinPoint point) {
        System.out.println("方法:"+point.getSignature().getName());
        Object[] args = point.getArgs();
        System.out.println("目標引數列表:");
        if (args != null) {
            for (Object obj : args) {
                System.out.println(obj + ",");
            }
        }
    }
    @Override
    public void logArgAndReturn(JoinPoint point, Object returnObj) {
        //此方法返回的是一個數組,陣列中包括request以及ActionCofig等類物件
        Object[] args = point.getArgs();
        System.out.println("目標引數列表:");
        if (args != null) {
            for (Object obj : args) {
                System.out.println(obj + ",");
            }
            System.out.println("執行結果是:" + returnObj);
        }
    }
}

配置(註釋著重看下):

    <aop:config>
        <!-- 切入點 LoginServiceImpl類中所有方法都會被攔截,執行增強方法-->
        <aop:pointcut expression="execution(* com.payment.util.aoplogger.LoginServiceImpl.*(..))" id="myPointcut" />
        <!-- 切面-->
        <aop:aspect id="dd" ref="logService">
            <!-- LoginServiceImpl類方法執行前,增強執行日誌service的log方法-->
            <!--<aop:before method="log" pointcut-ref="myPointcut" />-->

            <!-- LoginServiceImpl類方法執行後,增強執行日誌service的logArg方法-->
            <!--<aop:after method="logArg" pointcut-ref="myPointcut"/>-->
            <!-- LoginServiceImpl類方法執行拋異常後,增強執行日誌service的logArg方法-->
            <aop:after-throwing method="logArg" pointcut-ref="myPointcut"/>
            <!--<aop:after-returning method="logArgAndReturn" returning="returnObj" pointcut-ref="myPointcut"/>-->
        </aop:aspect>
    </aop:config>

注意:採用xml配置與使用@註解二選一(都寫上即執行兩次增強方法),對等關係如下:

<aop:aspect ref="advices">

@Aspect

public class Advices

<aop:pointcut expression="execution(* com.payment.util.springaop.PayOrderTarget.*(..))" id="pointcut1"/>

<aop:before method="before" pointcut-ref="pointcut1"/>

@Before("execution(* com.payment.util.springaop.PayOrderTarget.*(..))")      

<aop:after

@After  等同於其他增強方式