简介
利用基于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章