使用SpringAOP轻松织入周边服务

305 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

上篇文章介绍了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进行增强。