基于JavaConfig的Spring切面编程

简介

利用基于JavaConfig的Spring切面编程(Aspect oriented programming AOP), 实现日志操作脱离核心代码的示例。使用切面编程的好处:

  • 服务模块更简洁,专注实现核心功能
  • 次要关注点的代码转到的切面, 方便统一管理

切面术语

通知(advice) - what and when

定义切面时什么以及何时使用, Spring提供了5种类型的通知以及对应的Spring注解:

  • Before: 目标方法调用之前调用通知, 【对应@Before注解】
  • After:目标方法完成调用之后调用通知, 【对应@After注解】
  • AfterReturning:目标方法成功执行返回之后调用通知, 【对应@AfterReturning注解】
  • AfterThrowing:目标方法抛出异常之后调用通知,【对应@AfterThrowing注解】
  • Around:目标方法调用之后和调用之后执行, 【对应@Around注解】

连接点(Join point)

在应用执行的过程中能够插入切面的一个点。Spring只支持方法级别的连接点, 可以利用AspectJ补充SpringAOP功能。

切点(Point Cut) - where

切点定义了切面在何处执行。

切面(Aspect)

切面由通知和切点组成

引入(Introduction)

允许向现有的类添加新方法和属性

织入(Weaving)

把切面用用到目标对象并创建新的代理对象的过程,Spring在运行期把切面织入到Spring管理的bean中。当代理对象拦截到方法调用时, 在调用目标bean之前, 会执行切面逻辑。

切面实现

构建目标对象

构建目标对象字符串全角和半角转换类, 并声明为bean。

package com.notepad.springnote.inject;

import org.springframework.stereotype.Component;

/**
 * Description:  字符串处理函数
 * <p>
 * Create:       2018/6/17 18:55
 *
 * @author Yang Meng(eyangmeng@163.com)
 */

@Component
public class StringProcess {

    /**
     * 字符串的全角转半角
     *
     * @param content 字符串
     * @return 半角字符串
     */
    public String stringQ2B(String content) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < content.length(); i++) {
            int asciiCode = (int) content.charAt(i);
            if (asciiCode >= Q_START && asciiCode <= Q_END) {
                asciiCode -= Q_B_INTERVAL;
            } else if (asciiCode == Q_BLANK) {
                asciiCode = B_BLANK;
            }
            stringBuilder.append((char) asciiCode);
        }
        return stringBuilder.toString();
    }

    /**
     * 字符串半角转全角
     *
     * @param content 字符串
     * @return 全角字符串
     */
    public String stringB2Q(String content) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < content.length(); i++) {
            int asciiCode = (int) content.charAt(i);
            if (asciiCode >= B_START && asciiCode <= B_END) {
                asciiCode += Q_B_INTERVAL;
            } else if (asciiCode == B_BLANK) {
                asciiCode = Q_BLANK;
            }
            stringBuilder.append((char) asciiCode);
        }
        return stringBuilder.toString();
    }

    /** 全角字符起点和终点 */
    private static final int Q_START = 65281;
    private static final int Q_END = 65374;
    private static final int Q_BLANK = 12288;

    /** 半角字符起点和终点 */
    private static final int B_START = 33;
    private static final int B_END = 126;
    private static final int B_BLANK = 32;

    /** 全角和半角间隔 */
    private static final int Q_B_INTERVAL = 65248;
}

构建切面

目的在字符串半角转全角过程中,利用日志记录转换前后的情况。

定义切面
  • 切面与一般的JavaClass定义类似, 只是在Class上添加@Aspect注解, 声明为切面。示例如下:

    package com.notepad.springnote.aspects;
    
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
    * Description: 字符串切面
    * <p>
    *     1. 构造字符串切面, 完成切面的学习
    *     2. @Aspect注解
    * </p>
    * Create:      2018/7/18 0:13
    *
    * @author Yang Meng(eyangmeng@163.com)
    */
    
    @Aspect
    public class StringAspect {
    
    // 
    
      private static final Logger LOG = LoggerFactory.getLogger(StringAspect.class);
    }
定义切点

定义了切面之后, 我们开始定义切点, 下文的切点定义需放到public class StringAspect代码中。

  • @Pointcut分别定义切点, 包含连接点的定义(Spring只支持方法级别的连接点), 示例如下:

    /** 无参数的切点:()
    * (1) execution: 匹配连接点的执行方法
    * (2) * : 对方法的返回值不关心, 不限制返回值类型
    * (3) com.notepad.springnote.inject.StringProcess.stringB2Q: 目标方法
    * (4) (..): 对方法的参数不关注, 不限制参数及类型
    */
      @Pointcut("execution(* com.notepad.springnote.inject.StringProcess.stringB2Q(..))")
      public void process() {}
  • 无参数的切点有时不能满足我们的要求, 我们需要关注目标方法的输入, 以获取初始值, 为此定义带参数的切点

    /** 带参数的切点
       *
       *      1. execution中指明参数类型, 非jdk类型指定全路径, eg. com.xxx.yyy.ClassT
       *      2. args中指明参数名称和个数, 与目标方法保持一致, 否则会导致无法匹配目标方法
       *
       */
      @Pointcut("execution(* com.notepad.springnote.inject.StringProcess.stringB2Q(String)) && args(content)")
      public void argsProcess(String content) {}
定义通知

定义了切面,切点, 我们最后定义通知, 下文的示例代码需放到定义的切面中(public class StringAspect代码)

  • 利用@Before定义无参数的前置通知

    /**
    * 定义前置通知
    * (1) "process()": 定义的切点
    */1
    @Before("process()")
      public void beforeProcess() {
          LOG.info("begin start processing.");
      }
    
  • 利用@Before定义带参数的前置通知

      /**
       * 定义Before通知, 记录初始参数
       *      1. 定义的通知方法参数名称与@Pointcut中的保持一致
       *
       * @param content 待转换的字符串
       */
      @Before("argsProcess(content)")
      public void beforeArgsProcess(String content) {
          LOG.info("the original content: {}", content);
      } 
  • 相比与输入,我们更关注目标方法的输出即返回值,利用@AfterReturning定义通知

      /**
       * 方法返回结果之后, 并且获取返回结果
       *
       *      1. argNames和returning中的参数名字一致, 则方法中名称不限制
       *      2. 不使用argNames, 保持returning参数名称和方法中一致
       *      3. returning: 通知绑定的返回参数名称
       *      4. TODO: argNames没弄清楚作用(参数之间用逗号分隔)
       *      5. 如果定义的切点中有参数, 即value中有参数,
       *            则这些参数需放置待argNames中, 如process(a), argNames="a,st"
       *
       * @param str 方法的返回参数
       */
      @AfterReturning(value = "process()", argNames = "st", returning = "st")
      public void afterProcess(String str) {
          LOG.info("@AfterReturning: after process, get result: {}", str);
      }
  • @Around@Before@After都强大, 可以在目标方法执行前后执行相关操作。注意的一点是,@Around 方法的第一个参数是ProceedingJoinPoint用来调用被通知的方法, 并且如果目标方法有返回值@Around需要进行返回,否则导致目标方法的返回值无效。

    /**
     * 定义Around通知, 记录处理全过程
     *
     * @param jp  @Around通知需使用ProceedingJoinPoint, 用来调用被通知的方法
     * @param content 待处理字符串
     */
    @Around("argsProcess(content)")
    public Object aroundProcessExecutor(ProceedingJoinPoint jp, String content) {
        LOG.info("around advice: the original content: {}", content);
        try {
            Object res = jp.proceed();
            LOG.info("around advice: the result content: {}", (String) res);
            return res;
        } catch (Throwable e) {
            LOG.error("around advice: catch throwable - [{}]", e.getMessage());
            return null;
        }
    }

定义JavaConfig

定义了目标对象, 切面之后, 基于JavaConfig实现Spring切面, 因此我们定义一个JavaConfig,为了激活定义的注解, 对JavaConfig中添加@EnableAspectJAutoProxy, 否则定义的切面无法使用。

@Configuration
@ComponentScan(basePackages = "com.notepad.springnote")
@EnableAspectJAutoProxy
public class InjectConfig {

    @Bean
    public StringAspect stringAspect() {
        return new StringAspect();
    }
}

切面测试

构建单元测试, 验证我们定义的切面是否起作用。示例如下:

package com.notepad.springnote.aspects;

import com.notepad.springnote.config.InjectConfig;
import com.notepad.springnote.inject.StringProcess;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.Assert.*;

/**
 * Description: 字符串切面测试
 * <p>
 * Create:      2018/7/18 0:23
 *
 * @author Yang Meng(eyangmeng@163.com)
 */

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = InjectConfig.class)
public class StringAspectTest {

    @Autowired
    private StringAspect stringAspect;

    @Autowired
    private StringProcess stringProcess;

    @Test
    public void testAspectParams() throws Exception {
        String test = "20180718年世界杯法国获得冠军";
        String newStr = stringProcess.stringB2Q(test);
    }

}
  • 日志输出如下
16:45:28.844] [main] [INFO] c.n.springnote.aspects.StringAspect - around advice: the original content: 20180718年世界杯法国获得冠军
[16:45:28.846] [main] [INFO] c.n.springnote.aspects.StringAspect - the original content: 20180718年世界杯法国获得冠军
[16:45:28.846] [main] [INFO] c.n.springnote.aspects.StringAspect - begin start processing.
[16:45:28.856] [main] [INFO] c.n.springnote.aspects.StringAspect - around advice: the result content: 20180718年世界杯法国获得冠军
after process.
java.lang.String
[16:45:28.856] [main] [INFO] c.n.springnote.aspects.StringAspect - @AfterReturning: after process, get result: 20180718年世界杯法国获得冠军

参考

Spring实战 Spring In Action(第四版) - 第4章

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值