小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
上篇文章介绍了Java中的动态代理:Java如何实现动态代理 ,本篇文章就来看看在Spring框架中是如何运用动态代理的。
SpringAOP的使用
仍然是举一个发邮件的例子:
public interface SendEmailService {
void sendEmail(String emailContext);
}
@Service
public class SendEmailServiceImpl implements SendEmailService {
@Override
public void sendEmail(String emailContext) {
System.out.println("发送了一封邮件,内容为:" + emailContext);
}
}
对于发送邮件的业务来说,我们如何实现在发送邮件前后均记录一次当前的时间呢?学习了Java中的动态代理以后,可以很轻松地实现这一点:
public class SendEmailInvocationHandler<T> implements InvocationHandler {
private final T t;
public SendEmailInvocationHandler(T t) {
this.t = t;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("发送邮件前的时间为:" + LocalDateTime.now());
Object result = method.invoke(t, args);
System.out.println("发送邮件后的时间为:" + LocalDateTime.now());
return result;
}
}
自定义类继承自InvocationHandler,然后使用Proxy进行增强:
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
SendEmailService sendEmailService = context.getBean(SendEmailService.class);
SendEmailService sendEmailProxy = (SendEmailService) Proxy.newProxyInstance(sendEmailService.getClass().getClassLoader(), sendEmailService.getClass().getInterfaces(), new SendEmailInvocationHandler<>(sendEmailService));
sendEmailProxy.sendEmail("hello");
}
而在Spring中,增强一个方法根本就没有这么复杂,只需一些简单的代码即可实现:
@Aspect
@Component
public class SendEmailAspect {
@Before("execution(public com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public void printTimeBefore() {
System.out.println(LocalDateTime.now());
}
@After("execution(public com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public void printTimeAfter() {
System.out.println(LocalDateTime.now());
}
}
首先在该类中使用到了两个注解,@Aspect和@Component,@Component注解并不陌生,它是用来标记一个类作为Spring容器的一个Bean,即:将该类注册到Spring容器中。 而@Aspect注解则是将某个Bean标记为一个切面,SpringAOP的思想是在不改变原代码情况下,将这些额外的代码逻辑像织布一样织入到原代码的前面或者后面,它就被称之为一个切面。
切面类中共有5个切面注解:
- @Before:前置通知
- @After:后置通知
- @AfterReturning:返回通知
- @AfterThrowing:异常通知
- @Around:环绕通知
它们的意思都非常好理解,比如前置通知,则是在目标方法执行之前执行,后置通知,是在目标方法执行之后执行,等等,下面一一介绍各个通知的细节。
切点表达式
在介绍各个通知之前,我们需要知道一个概念,它就是切点表达式,通过切点表达式,我们可以指定对哪些包下的哪些类中的哪些接口进行增强,比如例子中的:
execution(public com.wwj.springaop.test.SendEmailService.sendEmail(..))
它表示对public修饰的com.wwj.springaop.test包下的SendEmailService接口中的sendEmail方法进行增强。而如果我们需要同时对多个类中的方法进行增强时,就可以使用*通配符进行匹配,比如:
execution(* com.wwj.springaop.test.*.*(..))
它表示需要增强任意权限修饰符的,在com.wwj.springaop.test包下的任意类的任意方法,(..)表示任意参数。
前置通知
顾名思义,前置通知会在目标方法执行之前执行,它比较简单,没有什么需要特点注意的地方。
后置通知
后置通知会在目标方法执行之后执行,需要注意的是该通知无论如何都会被执行,即使目标方法出现了异常。
返回通知
返回通知会在目标方法成功执行后执行,如果目标方法出现了异常,则该通知不执行。
异常通知
异常通知会在目标方法出现异常后执行,如果目标方法未出现异常,则该通知不执行。
环绕通知
环绕通知比较特殊,它能够实现前面四个通知的所有功能,我们放到后面说。
四大通知的测试
接下来,我们就对除了环绕通知外的四个通知进行测试,编写切面类:
@Aspect
@Component
public class SendEmailAspect {
@Before("execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public void before() {
System.out.println("前置通知执行了......");
}
@After("execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public void after() {
System.out.println("后置通知执行了......");
}
@AfterReturning("execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public void afterReturning() {
System.out.println("返回通知执行了......");
}
@AfterThrowing("execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public void afterThrowing() {
System.out.println("异常通知执行了......");
}
}
测试代码:
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
SendEmailService sendEmailService = context.getBean(SendEmailService.class);
sendEmailService.sendEmail("hello");
}
执行结果为:
前置通知执行了......
发送了一封邮件,内容为:hello
返回通知执行了......
后置通知执行了......
当目标方法正常执行时,通知的顺序为前置通知、返回通知、后置通知。 而如果目标方法出现异常呢?修改代码:
@Service
public class SendEmailServiceImpl implements SendEmailService {
@Override
public void sendEmail(String emailContext) {
System.out.println("发送了一封邮件,内容为:" + emailContext);
int i = 1 / 0;
}
}
重新执行代码,结果为:
前置通知执行了......
发送了一封邮件,内容为:hello
异常通知执行了......
后置通知执行了......
通知的顺序就变为了前置通知、异常通知、后置通知。
通知中的可传入参数
由于通知的特性,使得某些通知可以获取到目标方法的一些执行信息,比如:
@Before("execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public void before(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("前置通知执行了......目标方法为:" + methodName + ",参数为:" + args);
}
在前置通知中,可以通过直接传入JointPoint对象来获取目标方法的一些信息,比如方法名、参数等等。
后置通知也可以传入此参数:
@After("execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public void after(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("后置通知执行了......目标方法为:" + methodName + ",参数为:" + args);
}
而返回通知不仅能够获取到目标方法名和方法参数,还能够获取到目标方法的返回值,因为返回通知是在目标方法成功执行后才执行的:
@AfterReturning(value = "execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))",returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("返回通知执行了......目标方法为:" + methodName + ",参数为:" + args + ",返回值为:" + result);
}
异常通知与其类似,可以获取到目标方法的异常信息:
@AfterThrowing(value = "execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("异常通知执行了......目标方法为:" + methodName + ",参数为:" + args + ",异常信息为:" + e);
}
分别对目标方法正常执行和出现异常执行的情况进行测试,结果如下:
前置通知执行了......目标方法为:sendEmail,参数为:[hello]
发送了一封邮件,内容为:hello
返回通知执行了......目标方法为:sendEmail,参数为:[hello],返回值为:null
后置通知执行了......目标方法为:sendEmail,参数为:[hello]
前置通知执行了......目标方法为:sendEmail,参数为:[hello]
发送了一封邮件,内容为:hello
异常通知执行了......目标方法为:sendEmail,参数为:[hello],异常信息为:java.lang.ArithmeticException: / by zero
后置通知执行了......目标方法为:sendEmail,参数为:[hello]
最后
最后来介绍一下环绕通知吧,前面说了,它能够实现其它的四种通知的效果,代码如下:
@Around(value = "execution(* com.wwj.springaop.test.SendEmailService.sendEmail(..))")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
try {
System.out.println("前置通知执行了......目标方法为:" + methodName + ",参数为:" + args);
result = joinPoint.proceed();// 执行目标方法
System.out.println("返回通知执行了......目标方法为:" + methodName + ",参数为:" + args + ",返回值为:" + result);
} catch (Throwable e) {
System.out.println("异常通知执行了......目标方法为:" + methodName + ",参数为:" + args + ",异常信息为:" + e);
} finally {
System.out.println("后置通知执行了......目标方法为:" + methodName + ",参数为:" + args);
}
return result;
}
看到这些有没有觉得和JDK中的动态代理很像呢?SpringAOP的底层其实就是动态代理,若是要增强的类实现了接口,则使用JDK提供的方式进行增强;若是未实现任何接口,则使用CGLib进行增强。