Spring入门(十一):Spring AOP使用进阶
- 2019 年 10 月 3 日
- 筆記
在上篇博客中,我们了解了什么是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实战》
5. 最后
欢迎扫码关注微信公众号:「申城异乡人」,定期分享Java技术干货,让我们一起进步。