Spring入门(十一):Spring AOP使用进阶
在上篇博客中,我们了解了什么是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. 源码及参考
源码地址:,欢迎下载。
craig walls 《spring实战(第4版)》
汪云飞《java ee开发的颠覆者:spring boot实战》
5. 最后
打个小广告,欢迎扫码关注微信公众号:「申城异乡人」,定期分享java技术干货,让我们一起进步。