在上篇博客中, 我们了解了什么是 AOP 以及在 Spring 中如何使用 AOP, 本篇博客继续深入讲解下 AOP 的高级用法.
1. 声明带参数的切点
假设我们有一个接口 CompactDisc 和它的实现类 BlankDisc:
- package chapter04.soundsystem;
- /**
- * 光盘
- */
- public interface CompactDisc {
- void play();
- void play(int songNumber);
- }
- package chapter04.soundsystem;
- import java.util.List;
- /**
- * 空白光盘
- */
- public class BlankDisc implements CompactDisc {
- /**
- * 唱片名称
- */
- private String title;
- /**
- * 艺术家
- */
- private String artist;
- /**
- * 唱片包含的歌曲集合
- */
- private List<String> songs;
- public BlankDisc(String title, String artist, List<String> songs) {
- this.title = title;
- this.artist = artist;
- this.songs = songs;
- }
- @Override
- public void play() {
- System.out.println("Playing" + title + "by" + artist);
- for (String song : songs) {
- System.out.println("-Song:" + song);
- }
- }
- /**
- * 播放某首歌曲
- *
- * @param songNumber
- */
- @Override
- public void play(int songNumber) {
- System.out.println("Play Song:" + songs.get(songNumber - 1));
- }
- }
现在我们的需求是记录每首歌曲的播放次数, 按照以往的做法, 我们可能会修改 BlankDisc 类的逻辑, 在播放每首歌曲的代码处增加记录播放次数的逻辑, 但现在我们使用切面, 在不修改 BlankDisc 类的基础上, 实现相同的功能.
首先, 新建切面 SongCounter 如下所示:
- package chapter04.soundsystem;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Before;
- import org.aspectj.lang.annotation.Pointcut;
- import java.util.HashMap;
- import java.util.Map;
- @Aspect
- public class SongCounter {
- private Map<Integer, Integer> songCounts = new HashMap<>();
- /**
- * 可重用的切点
- *
- * @param songNumber
- */
- @Pointcut("execution(* chapter04.soundsystem.CompactDisc.play(int)) && args(songNumber)")
- public void songPlayed(int songNumber) {
- }
- @Before("songPlayed(songNumber)")
- public void countSong(int songNumber) {
- System.out.println("播放歌曲计数:" + songNumber);
- int currentCount = getPlayCount(songNumber);
- songCounts.put(songNumber, currentCount + 1);
- }
- /**
- * 获取歌曲播放次数
- *
- * @param songNumber
- * @return
- */
- public int getPlayCount(int songNumber) {
- return songCounts.getOrDefault(songNumber, 0);
- }
- }
重点关注下切点表达式 execution(* chapter04.soundsystem.CompactDisc.play(int)) && args(songNumber), 其中 int 代表参数类型, songNumber 代表参数名称.
新建配置类 SongCounterConfig:
- package chapter04.soundsystem;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.EnableAspectJAutoProxy;
- import java.util.ArrayList;
- import java.util.List;
- @Configuration
- @EnableAspectJAutoProxy
- public class SongCounterConfig {
- @Bean
- public CompactDisc yehuimei() {
- List<String> songs = new ArrayList<>();
- songs.add("东风破");
- songs.add("以父之名");
- songs.add("晴天");
- songs.add("三年二班");
- songs.add("你听得到");
- BlankDisc blankDisc = new BlankDisc("叶惠美", "周杰伦", songs);
- return blankDisc;
- }
- @Bean
- public SongCounter songCounter() {
- return new SongCounter();
- }
- }
注意事项:
1) 配置类要添加 @EnableAspectJAutoProxy 注解启用 AspectJ 自动代理.
2) 切面 SongCounter 要被声明 bean, 否则切面不会生效.
最后, 新建测试类 SongCounterTest 如下所示:
- package chapter04.soundsystem;
- 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.assertEquals;
- @RunWith(SpringJUnit4ClassRunner.class)
- @ContextConfiguration(classes = SongCounterConfig.class)
- public class SongCounterTest {
- @Autowired
- private CompactDisc compactDisc;
- @Autowired
- private SongCounter songCounter;
- @Test
- public void testSongCounter() {
- compactDisc.play(1);
- compactDisc.play(2);
- compactDisc.play(3);
- compactDisc.play(3);
- compactDisc.play(3);
- compactDisc.play(3);
- compactDisc.play(5);
- compactDisc.play(5);
- assertEquals(1, songCounter.getPlayCount(1));
- assertEquals(1, songCounter.getPlayCount(2));
- assertEquals(4, songCounter.getPlayCount(3));
- assertEquals(0, songCounter.getPlayCount(4));
- assertEquals(2, songCounter.getPlayCount(5));
- }
- }
运行测试方法 testSongCounter(), 测试通过, 输出结果如下所示:
播放歌曲计数: 1
Play Song: 东风破
播放歌曲计数: 2
Play Song: 以父之名
播放歌曲计数: 3
Play Song: 晴天
播放歌曲计数: 3
Play Song: 晴天
播放歌曲计数: 3
Play Song: 晴天
播放歌曲计数: 3
Play Song: 晴天
播放歌曲计数: 5
Play Song: 你听得到
播放歌曲计数: 5
Play Song: 你听得到
2. 限定匹配带有指定注解的连接点
在之前我们声明的切点中, 切点表达式都是使用全限定类名和方法名匹配到某个具体的方法, 但有时候我们需要匹配到使用某个注解的所有方法, 此时就可以在切点表达式使用 @annotation 来实现, 注意和之前在切点表达式中使用 execution 的区别.
为了更好的理解, 我们还是通过一个具体的例子来讲解.
首先, 定义一个注解 Action:
- package chapter04;
- import java.lang.annotation.*;
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface Action {
- String name();
- }
然后定义 2 个使用 @Action 注解的方法:
- package chapter04;
- import org.springframework.stereotype.Service;
- @Service
- public class DemoAnnotationService {
- @Action(name = "注解式拦截的 add 操作")
- public void add() {
- System.out.println("DemoAnnotationService.add()");
- }
- @Action(name = "注解式拦截的 plus 操作")
- public void plus() {
- System.out.println("DemoAnnotationService.plus()");
- }
- }
接着定义切面 LogAspect:
- package chapter04;
- import org.aspectj.lang.JoinPoint;
- import org.aspectj.lang.annotation.After;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Pointcut;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.springframework.stereotype.Component;
- import java.lang.reflect.Method;
- @Aspect
- @Component
- public class LogAspect {
- @Pointcut("@annotation(chapter04.Action)")
- public void annotationPointCut() {
- }
- @After("annotationPointCut()")
- public void after(JoinPoint joinPoint) {
- MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
- Method method = methodSignature.getMethod();
- Action action = method.getAnnotation(Action.class);
- System.out.println("注解式拦截" + action.name());
- }
- }
注意事项:
1) 切面使用了 @Component 注解, 以便 Spring 能自动扫描到并创建为 bean, 如果这里不添加该注解, 也可以通过 Java 配置或者 xml 配置的方式将该切面声明为一个 bean, 否则切面不会生效.
2)@Pointcut("@annotation(chapter04.Action)"), 这里我们在定义切点时使用了 @annotation 来指定某个注解, 而不是之前使用 execution 来指定某些或某个方法.
我们之前使用的切面表达式是 execution(* chapter04.concert.Performance.perform(..)) 是匹配到某个具体的方法, 如果想匹配到某些方法, 可以修改为如下格式:
execution(* chapter04.concert.Performance.*(..))
然后, 定义配置类 AopConfig:
- package chapter04;
- import org.springframework.context.annotation.ComponentScan;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.EnableAspectJAutoProxy;
- @Configuration
- @ComponentScan
- @EnableAspectJAutoProxy
- public class AopConfig {
- }
注意事项: 配置类需要添加 @EnableAspectJAutoProxy 注解启用 AspectJ 自动代理, 否则切面不会生效.
最后新建 Main 类, 在其 main() 方法中添加如下测试代码:
- package chapter04;
- import org.springframework.context.annotation.AnnotationConfigApplicationContext;
- public class Main {
- public static void main(String[] args) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
- DemoAnnotationService demoAnnotationService = context.getBean(DemoAnnotationService.class);
- demoAnnotationService.add();
- demoAnnotationService.plus();
- context.close();
- }
- }
输出结果如下所示:
DemoAnnotationService.add()
注解式拦截 注解式拦截的 add 操作
DemoAnnotationService.plus()
注解式拦截 注解式拦截的 plus 操作
可以看到使用 @Action 注解的 add() 和 plus() 方法在执行完之后, 都执行了切面中定义的 after() 方法.
如果再增加一个使用 @Action 注解的 subtract() 方法, 执行完之后, 也会执行切面中定义的 after() 方法.
3. 项目中的实际使用
在实际的使用中, 切面很适合用来记录日志, 既满足了记录日志的需求又让日志代码和实际的业务逻辑隔离开了,
下面看下具体的实现方法.
首先, 声明一个访问日志的注解 AccessLog:
- package chapter04.log;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- /**
- * 访问日志 注解
- */
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface AccessLog {
- boolean recordLog() default true;
- }
然后定义访问日志的切面 AccessLogAspectJ:
- package chapter04.log;
- import com.alibaba.fastjson.JSON;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Pointcut;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.springframework.stereotype.Component;
- @Aspect
- @Component
- public class AccessLogAspectJ {
- @Pointcut("@annotation(AccessLog)")
- public void accessLog() {
- }
- @Around("accessLog()")
- public void recordLog(ProceedingJoinPoint proceedingJoinPoint) {
- try {
- Object object = proceedingJoinPoint.proceed();
- AccessLog accessLog = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getAnnotation(AccessLog.class);
- if (accessLog != null && accessLog.recordLog() && object != null) {
- // 这里只是打印出来, 一般实际使用时都是记录到公司的日志中心
- System.out.println("方法名称:" + proceedingJoinPoint.getSignature().getName());
- System.out.println("入参:" + JSON.toJSONString(proceedingJoinPoint.getArgs()));
- System.out.println("出参:" + JSON.toJSONString(object));
- }
- } catch (Throwable throwable) {
- // 这里可以记录异常日志到公司的日志中心
- throwable.printStackTrace();
- }
- }
- }
上面的代码需要在 pom.xml 中添加如下依赖:
- <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.59</version>
- </dependency>
然后定义配置类 LogConfig:
- package chapter04.log;
- import org.springframework.context.annotation.ComponentScan;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.EnableAspectJAutoProxy;
- @Configuration
- @ComponentScan
- @EnableAspectJAutoProxy
- public class LogConfig {
- }
注意事项: 不要忘记添加 @EnableAspectJAutoProxy 注解, 否则切面不会生效.
然后, 假设你的对外接口是下面这样的:
- package chapter04.log;
- import org.springframework.stereotype.Service;
- @Service
- public class MockService {
- @AccessLog
- public String mockMethodOne(int index) {
- return index + "MockService.mockMethodOne";
- }
- @AccessLog
- public String mockMethodTwo(int index) {
- return index + "MockService.mockMethodTwo";
- }
- }
因为要记录日志, 所以每个方法都添加了 @AccessLog 注解.
最后新建 Main 类, 在其 main() 方法中添加如下测试代码:
- package chapter04.log;
- import org.springframework.context.annotation.AnnotationConfigApplicationContext;
- public class Main {
- public static void main(String[] args) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LogConfig.class);
- MockService mockService = context.getBean(MockService.class);
- mockService.mockMethodOne(1);
- mockService.mockMethodTwo(2);
- context.close();
- }
- }
输出日志如下所示:
方法名称: mockMethodOne
入参:[1]
出参:"1MockService.mockMethodOne"
方法名称: mockMethodTwo
入参:[2]
出参:"2MockService.mockMethodTwo"
如果某个方法不需要记录日志, 可以不添加 @AccessLog 注解:
- public String mockMethodTwo(int index) {
- return index + "MockService.mockMethodTwo";
- }
也可以指定 recordLog 为 false:
- @AccessLog(recordLog = false)
- public String mockMethodTwo(int index) {
- return index + "MockService.mockMethodTwo";
- }
这里只是举了个简单的记录日志的例子, 大家也可以把切面应用到记录接口耗时等更多的场景.
4. 源码及参考
源码地址: https://github.com/zwwhnly/spring-action.git , 欢迎下载.
Craig Walls 《Spring 实战 (第 4 版)》
汪云飞《Java EE 开发的颠覆者: Spring Boot 实战》
AOP(面向切面编程)_百度百科 https://baike.baidu.com/item/AOP/1332219
5. 最后
来源: https://www.cnblogs.com/zwwhnly/p/11422874.html