Spring AOP项目应用——方法入参校验 & 日志横切

时间:2023-03-09 19:54:16
Spring AOP项目应用——方法入参校验 & 日志横切

转载:https://blog.****.net/Daybreak1209/article/details/80591566

应用一:方法入参校验

由于系统多个方法入参均对外封装了统一的Dto,其中Dto中几个必传参数在每个方法中都会进行相同的校验逻辑。笔者考虑采用Spring AOP进行优化,拦截方法进行参数校验。测试case实现如下:

Before

  1. /**
  2. * 被代理的目标类
  3. */
  4. @Service
  5. public class PayOrderTarget {
  6. @Autowired
  7. private PaymentOrderService paymentOrderService;
  8. public Result testQuery(QueryPaymentDto paymentOrderDto){
  9. PaymentOrderDto paymentOrderDto1 = paymentOrderService.queryPaymentOrder(paymentOrderDto);
  10. return ResultWrapper.success(paymentOrderDto1);
  11. }
  12. }
  1. /**
  2. * 通知类,横切逻辑
  3. */
  4. @Component
  5. @Aspect
  6. public class Advices {
  7. @Before("execution(* com.payment.util.springaop.PayOrderTarget.*(..))")
  8. public Result before(JoinPoint proceedingJoinPoint) {
  9. // 拦截获取入参
  10. Object[] args = proceedingJoinPoint.getArgs();
  11. // 入参校验
  12. if (args ==null){
  13. return ResultWrapper.fail();
  14. }
  15. String input = JSON.toJSON(args).toString();
  16. Map<String, String> map = Splitter.on(",").withKeyValueSeparator(":").split(input);
  17. if (map.containsKey("businessId") || map.containsKey("payOrderId")){
  18. System.out.println("key null");
  19. return ResultWrapper.fail();
  20. }
  21. if (map.get("businessId")==null || map.get("payOrderId")==null ){
  22. System.out.println("value null");
  23. return ResultWrapper.fail();
  24. }
  25. System.out.println("----------前置通知----------");
  26. System.out.println(proceedingJoinPoint.getSignature().getName());
  27. return ResultWrapper.success();
  28. }
  29. }

测试类正常调用查询方法

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration(locations = {"classpath*:spring/spring-context.xml"})
  3. public class Test {
  4. @Autowired
  5. private PayOrderTarget payOrderTarget;
  6. @org.junit.Test
  7. public void test(){
  8. QueryPaymentDto paymentOrderDto=new QueryPaymentDto();
  9. paymentOrderDto.setPayOrderId(11l);
  10. paymentOrderDto.setBusinessId(112L);
  11. paymentOrderDto.setPageSize(1);
  12. payOrderTarget.testQuery(paymentOrderDto);
  13. }
  14. }

执行结果可对入参Dto进行拦截,执行before中的校验逻辑。但即便return fail之后,目标方法还是会被执行到。笔者是想实现参数校验失败,则直接返回,不执行接下来查询db的操作。此时则需要使用Around切入。

Around

  1. @Around("execution(* com.payment.util.springaop.PayOrderTarget.*(..))")
  2. public Result around(ProceedingJoinPoint proceedingJoinPoint) {
  3. // 获取java数组
  4. Object[] args = proceedingJoinPoint.getArgs();
  5. JSONArray jsonArray = JSONArray.parseArray(JSONArray.toJSONString(args));
  6. String businessId = null;
  7. String payOrderId = null;
  8. for (int i = 0; i < jsonArray.size(); i++) {
  9. businessId = jsonArray.getJSONObject(0).getString("businessId");
  10. payOrderId = jsonArray.getJSONObject(0).getString("payOrderId");
  11. }
  12. ;
  13. //参数校验
  14. if (businessId == null || payOrderId == null) {
  15. System.out.println("value null");
  16. return ResultWrapper.fail();
  17. }
  18.         //执行目标方法
  19. try {
  20. proceedingJoinPoint.proceed();
  21. } catch (Throwable throwable) {
  22. throwable.printStackTrace();
  23. }
  24. System.out.println("----------Around通知----------");
  25. return ResultWrapper.success();
  26. }

简单介绍下,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 在方法抛出异常后调用通知
  1. /**
  2. * login接口类 被代理接口类
  3. */
  4. public interface ILoginService {
  5. boolean login(String userName, String password);
  6. void quary() throws Exception;
  7. }
  8. /**
  9. * login实现类,被代理目标类
  10. */
  11. @Service
  12. public class LoginServiceImpl implements ILoginService {
  13. public boolean login(String userName, String password) {
  14. System.out.println("login:" + userName + "," + password);
  15. return true;
  16. }
  17. @Override
  18. public void quary() throws Exception{
  19. System.out.println("******quary*******");
  20. // 测试方法抛出异常后,解注After-throwing配置,会执行logArg切入,不抛异常不执行切点
  21. // int i=10/0;
  22. }
  23. }

日志包装类,将各类日志情况进行方法封装

  1. /**
  2. * 日志处理类
  3. */
  4. public interface ILogService {
  5. //无参的日志方法
  6. void log();
  7. //有参的日志方法
  8. void logArg(JoinPoint point);
  9. //有参有返回值的方法
  10. void logArgAndReturn(JoinPoint point, Object returnObj);
  11. }
  12. /**
  13. * 日志实现类
  14. */
  15. @Component("logService")
  16. @Aspect
  17. public class LogServiceImpl implements ILogService {
  18. @Override
  19. public void log() {
  20. System.out.println("*************Log*******************");
  21. }
  22. @Override
  23. public void logArg(JoinPoint point) {
  24. System.out.println("方法:"+point.getSignature().getName());
  25. Object[] args = point.getArgs();
  26. System.out.println("目标参数列表:");
  27. if (args != null) {
  28. for (Object obj : args) {
  29. System.out.println(obj + ",");
  30. }
  31. }
  32. }
  33. @Override
  34. public void logArgAndReturn(JoinPoint point, Object returnObj) {
  35. //此方法返回的是一个数组,数组中包括request以及ActionCofig等类对象
  36. Object[] args = point.getArgs();
  37. System.out.println("目标参数列表:");
  38. if (args != null) {
  39. for (Object obj : args) {
  40. System.out.println(obj + ",");
  41. }
  42. System.out.println("执行结果是:" + returnObj);
  43. }
  44. }
  45. }

配置(注释着重看下):

  1. <aop:config>
  2. <!-- 切入点 LoginServiceImpl类中所有方法都会被拦截,执行增强方法-->
  3. <aop:pointcut expression="execution(* com.payment.util.aoplogger.LoginServiceImpl.*(..))" id="myPointcut" />
  4. <!-- 切面-->
  5. <aop:aspect id="dd" ref="logService">
  6. <!-- LoginServiceImpl类方法执行前,增强执行日志service的log方法-->
  7. <!--<aop:before method="log" pointcut-ref="myPointcut" />-->
  8. <!-- LoginServiceImpl类方法执行后,增强执行日志service的logArg方法-->
  9. <!--<aop:after method="logArg" pointcut-ref="myPointcut"/>-->
  10. <!-- LoginServiceImpl类方法执行抛异常后,增强执行日志service的logArg方法-->
  11. <aop:after-throwing method="logArg" pointcut-ref="myPointcut"/>
  12. <!--<aop:after-returning method="logArgAndReturn" returning="returnObj" pointcut-ref="myPointcut"/>-->
  13. </aop:aspect>
  14. </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  等同于其他增强方式