在上篇博客中,咱们了解了什么是AOP以及在Spring中如何使用AOP,本篇博客继续深刻讲解下AOP的高级用法。java
假设咱们有一个接口CompactDisc和它的实现类BlankDisc:git
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类的基础上,实现相同的功能。github
首先,新建切面SongCounter以下所示:spring
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:json
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自动代理。ide
2)切面SongCounter要被声明bean,不然切面不会生效。测试
最后,新建测试类SongCounterTest以下所示:ui
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:你听获得
在以前咱们声明的切点中,切点表达式都是使用全限定类名和方法名匹配到某个具体的方法,但有时候咱们须要匹配到使用某个注解的全部方法,此时就能够在切点表达式使用@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()方法。
在实际的使用中,切面很适合用来记录日志,既知足了记录日志的需求又让日志代码和实际的业务逻辑隔离开了,
下面看下具体的实现方法。
首先,声明一个访问日志的注解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";
}
复制代码
这里只是举了个简单的记录日志的例子,你们也能够把切面应用到记录接口耗时等更多的场景。
源码地址:github.com/zwwhnly/spr…,欢迎下载。
Craig Walls 《Spring实战(第4版)》
汪云飞《Java EE开发的颠覆者:Spring Boot实战》
最后,欢迎关注个人微信公众号:「申城异乡人」,全部博客会同步更新。