Spring 23 AOP - 各种配置方法和实际应用

Spring 23 AOP - 各种配置方法和实际应用

@AfterReturning 顾名思义,这是在方法成功执行之后的切面。所谓成功执行,就是方法返回了结果,中间没有出现任何异常。 这种切面主要用于日志,预处理数据等功能 这个技术还一个重要的方面就是需要获得返回值,以便进行一些操作,来看一个简单的例子: 在Aspect类中打开getter方法的切点,

@AfterReturning

顾名思义,这是在方法成功执行之后的切面。所谓成功执行,就是方法返回了结果,中间没有出现任何异常。 这种切面主要用于日志,预处理数据等功能 这个技术还一个重要的方面就是需要获得返回值,以便进行一些操作,来看一个简单的例子: 在Aspect类中打开getter方法的切点,然后新创建一个切面方法:
@AfterReturning(pointcut = "forGetterMethod()", returning = "result")
public void afterReturning(JoinPoint joinPoint, String result) {
    System.out.println("After returning: successfully AOP executed.");
    System.out.println(result);
}
这里我们使用预先定义好的针对getter方法的切点声明,来定义了一个切面方法,在getter方法成功返回后执行。这里使用了注解的参数pointcut=切点声明returning="result",后者就是指定了返回值的变量名称,这个变量名称必须与之后传递给切面方法的变量名称一致。 只需要用对应的类型来接收返回值,在切面方法里就可以获得返回值了。然后运行一下MainDemoApp:
accountDAO.setName("jenny");
accountDAO.getName();
可以看到,我们没有在控制台打印getter方法的返回值,切面方法在控制台打印出了getter方法的返回值。

@AfterReturning 预先修改返回值

@AfterReturning最牛逼的是,如果返回值是一个引用类型的变量,是可以直接修改方法的返回值。所以在大型项目中如果应用这种切面修改返回值,一定要通知其他人。看一个例子: 新创建一个Account类,随便起点属性,如下:
public class Account {

    private String name;
    private String code;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public Account(String name, String code) {
        this.name = name;
        this.code = code;
    }

    public Account() {
    }

    @Override
    public String toString() {
        return "Account{" +
                "name='" + name + '\'' +
                ", code='" + code + '\'' +
                '}';
    }
}
然后我们为AccountDAO类添加一个私有域List<Accont> accountList,添加好getter和setter方法,这些代码都省略了,然后修改上边的切面方法如下:
@AfterReturning(pointcut = "forGetterMethod()", returning = "result")
public void afterReturning(JoinPoint joinPoint, List<Account> result) {
    System.out.println("After returning: successfully AOP executed.");
    Account newAccount = new Account("Minko", "Minkopig");
    result.add(newAccount);
}
这个切面方法由于返回结果比较特定,其实应该专门应用于getAccountList()方法,不过这里我们少修改一些。这里我们取得result之后,给result添加了一个新的Account对象。 然后修改MainDemoApp,运行如下代码:
Account jenny = new Account("Jenny", "BigWife");
Account cony = new Account("Cony", "Grapefruit");

ArrayList<Account> accounts =new ArrayList<>();
accounts.add(jenny);
accounts.add(cony);

accountDAO.setAccountList(accounts);

List<Account> accountList = accountDAO.getAccountList();

System.out.println(accountList);
这段代码输出的会是什么呢?是两个对象还是三个对象的List呢,答案是三个对象,AOP代理截取了方法的返回值然后做了修改,才传递给接收返回值的变量accountList,可见存进去两个对象,返回三个对象,有点令人疑惑。 用切面进行数据预处理是一个比较好的技术,但一定不要忘记自己做过这件事情,否则会给测试和协作带来很多麻烦。

@AfterThrowing

同样顾名思义,这个切面是在方法执行的过程中,抛出了异常之后执行的。很显然,这里也能够获取到底发生了什么异常。这里的原理实际上是AOP代理去获取了方法中抛出的异常,然后再返回给切面方法。 这个切面通常用于日志记录,处理异常,通过邮件或者消息通知开发组等功能。一般想抓住一种特定异常,设置一个切点就可以非常方便的应用于任何容器中的组件。 我们来给AccountDAO添加一个抛出异常的方法:
public void getException() {
    throw new RuntimeException("Intend to throw a runtime-exception");
}
然后在MainDemoApp里给执行这个方法加上try-catch:
try {
    accountDAO.getException();

} catch (Exception ex) {
    System.out.println(ex);
}
如果此时执行一下,很显然这个异常被try-catch语句获得,控制台结果如下:
java.lang.RuntimeException: Intend to throw a runtime-exception
现在我们为Aspect类添加一个新的切面:
@AfterThrowing(pointcut = "forGetterMethod()", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Throwable exception) {
    System.out.println("After throwing: successfully AOP executed.");
    System.out.println("Exception is "+ exception);
}
注解的参数throwing = "exception"就是定义异常对应的变量名,与传递给切面方法的参数名需要一致,这里再打印一下异常。 再执行一下MainDemoApp,看结果:
After throwing: successfully AOP executed.
Exception is java.lang.RuntimeException: Intend to throw a runtime-exception
java.lang.RuntimeException: Intend to throw a runtime-exception
可以发现异常先被切面方法捕获,之后再传递给try-catch语句。这说明,异常虽然被AOP代理拿到,但依然会继续传播到原始调用处。 你可能会想到,是不是有方法让异常停止在切面这里不继续传播,@Around注解就可以实现这个功能,将在后边学习。

@After

这个切面是不管方法执行没执行成功,都会执行。 一般用于那些无论如何都要处理的功能和代码以及日志功能,比如Web应用中返回错误需要关闭资源的时候就会用到。 需要注意的是@After注解无法获取异常,如果要处理异常,必须使用@AfterThrowing,实际上@After通常是搭配@AfterThrowing以合理的顺序执行的。 之前编写了一个.getAccountList()方法返回一个Account列表,一个.getException()返回异常,现在来编写一个针对所有get方法的@After切面:
@After("forGetterMethod()")
public void afterAll(JoinPoint joinPoint) {
    String method = joinPoint.getSignature().toString();
    System.out.println("This is executed after " + method);
}
之后在MainDemoApp里运行上边的两个方法,可以看到,无论方法成功返回与否,这个切面都运行了。

@Around

这个切面会在方法执行前和执行后都执行,其实就相当于在AOP代理和实际执行的方法之间又加了一层代理。有点像@Before和@After的结合,但是能实现更好的控制功能。 这个切面基本上可以做全部功能,也可以预处理参数和返回值,处理异常。还能实现比如:这个方法执行了多长时间?之类的功能。 这个切面的理论与之前的切面有点区别,在使用这个切面的时候,可以获得一个proceeding join point的引用,这实际上是一个指向目标方法的引用,可以通过这个引用来执行目标方法。

@Around基础用法

先通过一个小例子看一下使用方法,我们关闭其他的切面方法,就保留一个getter方法的切点声明,然后编写一个@Around切面方法:
@Around("forGetterMethod()")
public Object getRuntime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    long begin = System.currentTimeMillis();

    Object result = proceedingJoinPoint.proceed();

    System.out.println("Result from aspect " + result);

    long end = System.currentTimeMillis();

    long duration = end - begin;

    System.out.println("\n Running duration is " + duration);

    return result;
}
这里边有几个点需要解释:
  1. public Object getRuntime,这里返回类型是一个Object,是为了接收目标方法的返回值,然后将其返回给AOP代理。
  2. ProceedingJoinPoint proceedingJoinPoint,这个就是前边提到的指向目标方法的引用。
  3. throws Throwable,这个切面方法还带有异常抛出。
  4. Object result = proceedingJoinPoint.proceed(),这句话实际上是执行目标函数,然后将返回值放到result里。
  5. return result;,是向AOP代理返回这个返回值
可以看到,这个东西就是目标方法外边的一个包装容器。 执行MainDemoApp,只执行.setAccountList(),先不执行抛出异常的方法,可以发现控制台执行了切面方法,也打印出了方法执行的结果。
Result from aspect [Account{name='Jenny', code='BigWife'}, Account{name='Cony', code='Grapefruit'}]

Running duration is 0
[Account{name='Jenny', code='BigWife'}, Account{name='Cony', code='Grapefruit'}]
这里打印的顺序有点乱,这是因为我们自己的输出是在标准输出流,而Spring的相关输出是在logger output stream。可以将其统一到一个日志流里,这里我们利用util中的Logger工具类来实现,完整的Aspect类如下:
import aop.learn.Account;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.logging.Logger;

@Aspect
@Component
@Order(1)
public class MyDemoLoggingAspect {

    private static Logger logger = Logger.getLogger(MyDemoLoggingAspect.class.getName());

    @Pointcut("execution(* aop.learn.AccountDAO.get*(..))")
    private void forGetterMethod() {
    }

    @Around("forGetterMethod()")
    public Object getRuntime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long begin = System.currentTimeMillis();

        logger.info("Call Method");
        Object result = proceedingJoinPoint.proceed();

        logger.info("Result from aspect " + result);

        long end = System.currentTimeMillis();

        long duration = end - begin;

        logger.info("\nRunning duration is " + duration);

        return result;
    }

}
MainDemoApp如下:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

public class MainDemoApp {

    private static Logger logger = Logger.getLogger(MainDemoApp.class.getName());

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
        AccountDAO accountDAO = context.getBean("accountDAO", AccountDAO.class);
        //调用各种方法

        Account jenny = new Account("Jenny", "BigWife");
        Account cony = new Account("Cony", "Grapefruit");

        ArrayList<Account> accounts =new ArrayList<>();
        accounts.add(jenny);
        accounts.add(cony);

        accountDAO.setAccountList(accounts);

        List<Account> accountList = accountDAO.getAccountList();

        logger.info(accountList.toString());

        context.close();
    }
}
统一在logger里输出之后,可以看到控制台的顺序:
信息: Call Method
信息: Result from aspect [Account{name='Jenny', code='BigWife'}, Account{name='Cony', code='Grapefruit'}]
信息:
Running duration is 2
信息: [Account{name='Jenny', code='BigWife'}, Account{name='Cony', code='Grapefruit'}]
这是初步的@Around使用的例子。

@Around处理异常

@Around处理异常比@AfterThrowing更灵活一些,可以处理,压制,停止异常,也可以继续抛出异常。 这次我们来注释掉刚才的切面方法,新建一个切面,准备处理.getException()方法中的异常:
@Around("forGetterMethod()")
public Object handleException(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    Object result = null;

    try {
        result = proceedingJoinPoint.proceed();
    } catch (Exception ex) {
        logger.info("This is from aspect " + ex.toString());
        result = "Method has a exception";
    }
    return result;
}
这个切面里,我们采用老套路,先将结果赋值一个null,再进行错误处理,这里还可以任意设置方法的返回值。 在MainDemoApp里,按如下代码执行:
try {
    accountDAO.getException();

} catch (Exception ex) {
    logger.info("This is from try-catch: "+ex.toString());
}
运行之后可以发现,抛出的异常并没有被MainDemoApp的main方法捕获,而是被切面捕获,这个异常并没有继续扩散到main方法里。即使是在main方法里直接执行accountDAO.getException();,依然只有切面的消息出现,不会报错。 将错误全部捕获在切面里一定要小心,如果是重要的错误,还是不能全部拦截在切面里。所以这里也可以继续抛出错误:
@Around("forGetterMethod()")
public Object handleException(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    Object result = null;

    try {
        result = proceedingJoinPoint.proceed();
    } catch (Exception ex) {
        throw ex
    }
    return result;
}
这样再运行的时候,main方法就可以捕获到错误了。

AOP实战 - 为增删改查项目添加切面功能

看完了AOP的各种理论和配置方法,来看一下在实际项目中如何使用AOP。我们要给我们的增删改查小项目添加上日志,输出每次的具体操作。

配置AOP

首先需要将AspectJ的包导入到库中。 在学习的时候使用的包有一个配置类,其中有一个注解:@EnableAspectJAutoProxy,就是启用AOP的注解。现在到了增删改查项目中,使用的是XML配置结合注解的方式,需要先进行配置AOP。 打开Spring配置文件,修改一下头部,引入一些新的XML和命名空间,然后修改配置:
<?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/mvc
		http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/tx
		http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="cc.conyli"/>

    <mvc:annotation-driven/>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/view/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <bean id="myDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl"
                  value="jdbc:mysql://localhost:3306/web_customer_tracker?useSSL=false&serverTimezone=UTC"/>
        <property name="user" value="springstudent"/>
        <property name="password" value="springstudent"/>

        <property name="minPoolSize" value="5"/>
        <property name="maxPoolSize" value="20"/>
        <property name="maxIdleTime" value="30000"/>
    </bean>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="packagesToScan" value="cc.conyli.entity"/>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
            </props>
        </property>
    </bean>

    <bean id="myTransactionManager"
          class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>


    <tx:annotation-driven transaction-manager="myTransactionManager"/>

    <mvc:resources location="/resources/" mapping="/resources/**"></mvc:resources>

    <aop:aspectj-autoproxy />

</beans>
这些标红色的部分就是启用Spring对于@Aspect注解的支持,作用相当于Java配置类中的@EnableAspectJAutoProxy

编写切面代码

这里我们想对Controller,Service和DAO层全部都进行跟踪,可以分别针对这三层中的内容创建切点。先在项目内添加一个aspect包cc.conyli.aspect,在其中编写日志切面类:
package cc.conyli.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.logging.Logger;

@Aspect
@Component
public class CRMLoggingAspect {
    private Logger logger = Logger.getLogger(CRMLoggingAspect.class.getName());

    //创建controller包下边的所有类的所有方法+任意参数的切点
    @Pointcut("execution(* cc.conyli.controller.*.*(..))")
    private void forControllerPackage() {}

    //创建dao包下边的所有类的所有方法+任意参数的切点
    @Pointcut("execution(* cc.conyli.dao.*.*(..))")
    private void forDaoPackage() {}

    //创建service包下边的所有类的所有方法+任意参数的切点
    @Pointcut("execution(* cc.conyli.service.*.*(..))")
    private void forServicePackage() {}

    //上边三个切点的合集,即针对整个APP流
    @Pointcut("forControllerPackage() || forDaoPackage() || forServicePackage()")
    private void forAppFlow(){}

}
在切面类里,我们定义了一批切点,分别是针对三个包的切点和三个包以or运算连接起来的整个app的所有业务方法的切点。 然后我们来编写一个最简单的@Before看一下:
@Before("forAppFlow()")
private void beforeAppFlow(JoinPoint joinPoint) {
    String method = joinPoint.getSignature().toString();
    logger.info("\n======> This is appflow @Before method: " + method);
}
重新启动项目,可以发现,执行任何操作,控制台里都会打印出当前执行的方法的日志,如果调用数据库操作过程,也会打印出依次调用了三层的方法。 继续编写打印参数的方法:
@Before("forAppFlow()")
    private void beforeAppFlow(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().toString();
        logger.info("\n======> This is appflow @Before method: " + method);
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof Model) {
                logger.info(((Model) arg).asMap().toString());
            } else {
                logger.info("\n======>" + method + " args: " + arg);
            }
        }
    }
这样在每次调用任意方法的时候,我们都可以获得传递给这个方法的参数的详情。然后编写一个@AfterReturning来看看
@Override
public void deleteCustomer(int customerId) {
    Session session = sessionFactory.getCurrentSession();
    Customer customer = session.get(Customer.class, customerId);
    session.delete(customer);
}
这样就添加上了切面功能,在项目的每一个业务方法运行前和后都可以知道传入的参数和返回的结果。最关键的是,仅仅通过引入AOP和合理配置就完成了这一切,没有更改任何业务代码。不得不说Spring真是太棒了。
LICENSED UNDER CC BY-NC-SA 4.0
Comment