从约定编程开始
首先抛开Spring AOP的各种复杂概念,看一个简单的约定编程示例,后面再从这个约定编程示例的角度出发回过头看AOP的各种概念时,可能会好理解许多。
首先定义一个简单的接口和其实现类,逻辑很简单:
public interface HelloService {
void sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello(String name) {
if (name == null || name.trim() == "") {
throw new RuntimeException("parameter is null !");
}
System.out.println("hello " + name);
}
}
这个接口和实现类就定义了我们最简单的服务,下面创建一个拦截器接口来定义一些拦截方法:
public interface Interceptor {
/**
* 事前方法
*/
boolean before();
/**
* 事后方法
*/
void after();
/**
* 取代原有事件方法
* @param invocation 回调参数,使用invocation的proceed方法,回调原有事件
* @return 原有事件返回对象
*/
Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException;
/**
* 事后返回方法,事件没有发生异常时执行
*/
void afterRunning();
/**
* 事后异常方法,事件发生异常时执行
*/
void afterThrowing();
/**
* 是否使用around方法取代原有方法的标识
*/
boolean useAround();
}
其中用到了一个Invocation对象,该对象的定义如下:
public class Invocation {
private Object[] params;
private Method method;
private Object target;
public Invocation(Object target, Method method, Object[] params) {
this.target = target;
this.method = method;
this.params = params;
}
/**
* 以反射的形式调用原有方法
* @return 方法调用结果
*/
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, params);
}
}
该对象通过构造方法,设置相应的调用target对象和该对象方法method以及其参数。其中proceed方法即是在完成该对象构造之后,对target对象以反射的形式对method进行调用,并传入params参数。
下面根据该拦截器接口创建一个拦截器的实现类:
public class MyInterceptor implements Interceptor {
@Override
public boolean before() {
System.out.println("before ...");
return true;
}
@Override
public void after() {
System.out.println("after ...");
}
@Override
public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
System.out.println("around before ...");
Object obj = invocation.proceed();
System.out.println("around after ...");
return obj;
}
@Override
public void afterRunning() {
System.out.println("afterRunning ...");
}
@Override
public void afterThrowing() {
System.out.println("afterThrowing ...");
}
@Override
public boolean useAround() {
return true;
}
}
其中useAround方法负责控制是否需要使用around方法来反射调用target的方法,如果为false则直接调用target的方法。此外,该代码中需要关注的是around方法,其通过Invocation对象调用其proceed方法来实现反射调用target的方法。
有了以上的定义,我们希望将HelloService中提供的服务通过拦截器织入到一个约定的流程中,而这个约定的流程定义如下:
为了实现这样一个约定的流程,我们需要对原有的HelloService对象进行代理,这样就需要另一个来实现代理的类ProxyBean,该类中具有一个getProxyBean()静态方法,定义如下(具体的实现后面会说明):
/**
* 绑定代理对象
* @param target 被代理的对象
* @param interceptor 拦截器
* @return 代理对象
*/
public static Object getProxyBean(Object target, Interceptor interceptor);
该方法有几个需要注意的地方:
- target(被代理的对象)需要具有接口,interceptor对象是上述定义的拦截器接口对象。
- 该方法返回的对象(记为proxy)可以使用target对象所具有的接口类型进行转换。
有了这个proxy代理对象,下面对上述的约定过程进行详细的说明。当通过proxy代理对象的调用target方法时,其执行的过程如下:
- 使用proxy调用方法时首先会执行before方法。
- 如果拦截器的useAround方法返回true,则执行拦截器的around方法,其中通过Invocation对象的proceed方法来反射执行target的方法,而不调用target对象对应的方法。如果useAround方法返回为false,则直接调用target的方法。
- 无论怎么样,在完成调用事件之后,都会执行拦截器的after方法。
- 在执行around方法或者回调target的方法时发生异常,就还行拦截器的afterThrowing方法,如果没有异常,则执行afterReturing方法。
明确了该流程,下面看一下具体ProxyBean类中的getProxyBean方法的实现:
public class ProxyBean implements InvocationHandler {
private Object target = null;
private Interceptor interceptor = null;
/**
* 绑定代理对象
* @param target 被代理的对象
* @param interceptor 拦截器
* @return 代理对象
*/
public static Object getProxyBean(Object target, Interceptor interceptor) {
ProxyBean proxyBean = new ProxyBean();
proxyBean.target = target; // 保存被代理的对象
proxyBean.interceptor = interceptor; // 保存拦截器
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
proxyBean); // 创建代理对象
return proxy; // 返回代理对象
}
}
注意该代码中主要使用到的就是Proxy.newProxyInstance方法,该方法的定义如下:
/**
* Returns an instance of a proxy class for the specified interfaces
* that dispatches method invocations to the specified invocation
* handler.
*
* @param loader the class loader to define the proxy class
* @param interfaces the list of interfaces for the proxy class
* to implement
* @param h the invocation handler to dispatch method invocations to
* @return a proxy instance with the specified invocation handler of a
* proxy class that is defined by the specified class loader
* and that implements the specified interfaces
*/
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h);
其中具有的三个参数解释为:
- classloader:被代理对象的类加载器
- interfaces:代理对象实现的接口
- invocationHandler:用来分配方法调用的InvocationHandler(当前ProxyBean类)
这里的ProxyBean类即是用来处理代理方法的InvocationHandler,其实现了InvocationHandler接口并实现了如下的invoke方法:
/**
* 代理对象处理方法逻辑
* @param proxy 代理对象
* @param method 当前方法
* @param args 运行参数
* @return 方法调用结果
* @throws Throwable 异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 定义异常标识
boolean exceptionFlag = false;
Invocation invocation = new Invocation(target, method, args);
Object retObj = null;
try {
if (this.interceptor.before()) {
retObj = this.interceptor.around(invocation);
} else {
retObj = method.invoke(target, args);
}
} catch (Exception e) {
// 产生异常时设置异常标识
exceptionFlag = true;
}
this.interceptor.after();
if (exceptionFlag) {
this.interceptor.afterThrowing();
} else {
this.interceptor.afterRunning();
return retObj;
}
return null;
}
此时,通过上述getProxyBean方法得到了目标对象target到代理对象proxy的绑定。此时通过proxy调用target对象的方法是,会通过这的invoke方法进行调用。而在该invoke方法中实现了上述的约定过程。从而将目标对象的方法织入了约定的流程。下面进行一下测试:
@Test
public void testProxy() {
HelloService helloService = new HelloServiceImpl();
HelloService proxy = (HelloService) ProxyBean.getProxyBean(helloService, new MyInterceptor());
proxy.sayHello("yitian");
System.out.println("-------------name is null!--------------");
proxy.sayHello(null);
}
这里我们将最开始定义的HelloService对象作为目标对象target,然后通过ProxyBean.getProxyBean方法得到该对象的代理对象,并使用MyInterceptor实现相应约定下的各个方法。最后调用proxy代理对象的sayHello方法,即进入到了invoke方法中根据约定的流程进行处理。第一次调用sayHello方法中存在参数,第二次中不存在参数,因此会抛出异常,从而测试afterReturning方法和afterThrowing方法的执行。下面运行该测试,得到的输出如下:
before ...
around before ...
hello yitian
around after ...
after ...
afterRunning ...
-------------name is null!--------------
before ...
around before ...
after ...
afterThrowing ...
可以看到该输出正式我们在上面约定的流程,从而将目标对象HelloService织入了约定的流程。此时如果需要代理其他对象,仅需要为ProxyBean.getProxyBean方法传入具有相应接口的对象既可以完成其的动态代理。
实际上上述的约定编程的过程和Spring AOP的实现在原理上是一致的,Spring AOP的根本也是根据约定的各种流程,将服务织入其中,下面回过来看一下AOP的相关概念。
Spring AOP的相关概念
为什么使用AOP?
AOP最为经典的应用场景是数据库事务的管理。传统的JDBC中的事务操作充斥着大量的try-catch-finally语句,例如如下:
@Service
public class JdbcServiceImpl implements JdbcService {
@Autowired
private DataSource dataSource;
@Override
public int insertUser(User user) {
Connection connection = null;
int result = 0;
try {
// 创建数据库连接
connection = dataSource.getConnection();
// 设置是否自动提交
connection.setAutoCommit(false);
// 开启事务并设置隔离级别
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
PreparedStatement statement = connection.prepareStatement(
"insert into t_user (user_name, sex, note) values (?, ?, ?)");
statement.setString(1, user.getUserName());
statement.setInt(2, user.getSex().getId());
statement.setString(3, user.getNote());
// 执行SQL
result = statement.executeUpdate();
// 提交事务
connection.commit();
} catch (SQLException e) {
// 发生异常时回滚事务
if (connection != null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 关闭数据库连接
try {
if (connection != null || !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return result;
}
}
同时也可以意识到JDBC中的数据库事务操作很容易抽象为一个具有约定过程的编程模式,该约定的过程如下:
那么是不是也可以使用上述约定编程的模式将,其中公共的方法交由数据库事务管理器共同实现,用户仅实现自己的SQL执行方法,并将其织入到约定的流程中去呢?答案当然是肯定的。因此AOP的使用可以很好的简化代码,提供更清晰和方便的逻辑过程,同时提高代码的可维护性和可靠性。
AOP基本概念和术语
根据上面约定编程的理解,下面看一下Spring AOP的相关概念。
- 连接点(join point):对应的被拦截的对象,Spring AOP只支持方法,因此这里的连接点是目标对象的的被拦截的方法。例如上述中HelloServiceImpl对象的sayHello方法。
- 切点(point out):有时切面不单单应用于单个方法,也可以是多个类的不同方法。这时需要通过正则表达式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。
- 通知(advice):就是按照约定的流程中具有的方法,分为前置通知(before advice),后置通知(after advice),环绕通知(around advice),事后返回通知(afterReturning advice)和异常通知(afterThrowing advice),他们会被织入到根据约定的流程中执行。
- 目标对象(target):被动态代理的对象,例如上述的HelloServiceImpl对象。
- 引入(introduction):是指引入新的类或其他方法,来增强现有Bean的功能。
- 织入(weaving):它是一个通过动态代理技术,为原有目标对象生成代理对象,然后将使用定义匹配的连接点拦截,并按照约定将各类通知织入约定流程的过程。
- 切面(aspect):是一个定义切点,各类通知和引入的内容,Spring AOP将通过它的其中定义的信息来增强Bean的功能或者将对应的方法织入流程。
下面使用上述的HelloServiceImpl对象的约定过程来看一下相对应的概念:
AOP开发实例
引入AOP依赖:
<!-- 配置Aspect依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建连接点
下面使用@Aspect注解的方式进行AOP的相关开发实例。首先确定连接点(join point),也就是需要拦截的方法,下面创建一个UserService类,其中包含一个printUser方法,该方法作为join point。
public interface UserService {
/**
* 这里的printUser方法即为连接点方法
*/
void printUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void printUser(User user) {
if (user == null) {
throw new RuntimeException("检查用户参数是否为空!");
}
System.out.println(user);
}
}
开发切面
有了连接点,现在就需要创建一个切面来描述AOP所需要的其他概念,下面使用@Aspect注解来定义一个切面:
@Aspect
public class MyAspect {
@Pointcut("execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.printUser(..))")
public void pointCut() {
}
@Before("pointCut()")
public void before() {
System.out.println("before ...");
}
@After("pointCut()")
public void after() {
System.out.println("after ...");
}
@AfterReturning("pointCut()")
public void afterRunning() {
System.out.println("after returning ...");
}
@AfterThrowing("pointCut()")
public void afterThrowing() {
System.out.println("after throwing ...");
}
}
其中@PointCut注解标注这个方法为一个切点,其作用是向Spring描述指定类的指定方法需要启用AOP编程。并通过切点定义了@Before,@After,@AfterReturning和@AfterThrowing相应的通知。这些通知的注解使用了PointCut进行定义,表示这些方法是在什么连接点下启用AOP编程的。也就是上述定义的通知均是针对切点中匹配的连接点printUser来执行的。
此外在PointCut定义时使用到了下面的正则式:
execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.printUser(..))
其中:
- execution表示在执行的时候,拦截里面的正则式匹配的方法
- * 表示任意返回类型的方法
- cn.zyt.springbootlearning.service.impl.UserServiceImpl指定目标对象的全限定名
- printUser指定目标对象的连接点方法
- (..)表示任意参数进行匹配
测试AOP
有了如上切面的定义,下面对切面进行测试。创建如下的Controller用于测试:
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/print")
@ResponseBody
public User printUser(Long id, String username, int sex, String note) {
User user = new User(id, username, sex, note);
userService.printUser(user);
return user;
}
}
为了将定义的切面注入到IoC容器中,进行如下的设置:
@Configuration
public class AspectConfiguration {
@Bean(name = "myAspect")
public MyAspect initMyAspect() {
return new MyAspect();
}
}
启动Spring Boot项目,使用如下的URL进行测试:
http://localhost:8080/user/print?id=1&useName=yitian&sex=1¬e=none
可以看到Console中的输出如下:
before ...
User{id=1, userName='null', note='none', sex=MALE}
after ...
after returning ...
显然Spring成功的通过动态代理技术吧我们定义的切面以及其中定义的连接点和各种通知织入到了流程中。
环绕通知
在上述切面中定义的通知类没有环绕通知,环绕通知是所有通知中功能最强大的,但也不好控制。一般而言使用环绕通知需要大幅度修改原有目标对象的服务逻辑,它是一个取代原有目标对象方法的通知,同时也提供了回调原有目标对象方法的能力。下面下上述定义的切面中加入环绕通知:
/**
* 环绕通知
*/
@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before ...");
// 回调目标对象的原有方法
joinPoint.proceed();
System.out.println("around after ...");
}
下面用上面同样的测试URL进行测试,得到如下的输出:
around before ...
before ...
User{id=1, userName='null', note='none', sex=MALE}
around after ...
after ...
after returning ...
当在joinPoint.proceed()方法一行加入断点进行debug时,可以看到如下的调试内容:
可以看到ProceedingJoinPoint对象是一个被Spring封装过的代理对象,它包含目标对象target,被代理的joinpoint method:printUser等内容。因此可以通过该方法在回调目标对象方法的同时增强该方法前后相关的内容。
引入
上面在进行UserService的printUser的逻辑时,检查参数是否为null,如果是则直接抛出异常,但事实上我们应该可以对参数进行检测,如果为空则不再print用户信息。当然直接修改printUser的方法逻辑可以容易的实现该功能。但这里假设UserService对象是第三方所提供的,我们无法直接修改其源码内容。此时Spring允许使用AOP的方式增强该接口的功能,也就是使用引入的方式为该接口引入新的接口,例如这里引入一个用户信息监测的接口UserValidator,其定义如下:
public interface UserValidator {
/**
* 检查用户对象是否为空
*/
boolean validate(User user);
}
public class UserValidatorImpl implements UserValidator {
@Override
public boolean validate(User user) {
System.out.println("为UserService接口引入新接口:" + UserValidator.class.getSimpleName());
return user != null;
}
}
这样即可以通过AOP引入的方式将UserValidator接口增强到UserService接口中,在MyAspect类总加入如下代码来实现参数验证的功能:
/**
* 为UserService接口引入UserValidator接口
*/
@DeclareParents(value = "cn.zyt.springbootlearning.service.impl.UserServiceImpl", defaultImpl = UserValidatorImpl.class)
public UserValidator userValidator;
这里使用的@DeclareParents注解的作用是引入新的接口来增强类的功能,有两个必须配置的属性:
- value:指明需要增强的目标类
- defaultImpl:指明引入的类
这里需要注意value属性中+号的使用:+号代表该UserServiceImpl 类的所有子类,而目前UserServiceImpl 没有子类,所以这里不需要带+号。具体的问题见:https://blog.csdn.net/yitian_z/article/details/104441427
在UserController中使用如下方法进行测试:
@RequestMapping("/checkandprint")
@ResponseBody
public User checkAndPrintUser(Long id, String username, int sex, String note) {
User user = new User(id, username, sex, note);
UserValidator userValidator = (UserValidator) userService;
if (userValidator.validate(user)) {
userService.printUser(user);
}
return user;
}
使用如下URL进行请求:
http://localhost:8080/user/checkandprint?id=1&useName=yitian&sex=1¬e=none
得到的输出如下:
为UserService接口引入新接口:UserValidator
around before ...
before ...
User{id=1, userName='null', note='none', sex=MALE}
around after ...
after ...
after returning ...
从代码中可以看到,这里直接将useService对象强转为了userValidator对象,然后就可以使用其validate方法来对参数进行验证。能够这样处理的原因,是因为Spring AOP在对UserServiceImpl对象进行动态代理时,在UserService原有接口的同时,多挂载了UserValidator接口,因此这里可以使用强制类型转换。而底层的原理,还记得之前的Proxy.newProxyInstance()方法吗:
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h);
该方法第二个参数是目标对象的所有instance接口类型,因此在Spring得到代理对象时,会将增强的UseValidator接口也传递进去,从而是代理对象同时具有两个接口。这就是引入的功能。
获取通知参数
在MyAspect当前具有的通知中,都是没有参数传递的。当然也可以传递参数给通知,需要在切点加入相应的正则式就可以了。
/**
* 在前置通知中获取参数(带参数的前置通知)
* @param joinPoint 连接点
* @param user 参数名
*/
@Before("pointCut() && args(user)")
public void beforeParam(JoinPoint joinPoint, User user) {
Object[] args = joinPoint.getArgs();
System.out.println("before ... ");
}
该前置通知中,除了使用pointCut设置切点之外,使用了args(user)来约定将连接点printUser方法中名为user的参数传递进来。需要注意的是,对于非环绕通知而言,JoinPoint类型的参数会被自动传递到通知中去,对对于环绕通知而言,可以使用ProceedingJoinPoint类型的参数获取参数。下面对该方法进行调试,得到如下内容:
通过debug的内容可以看到,joinpoint类型的参数中,args属性和约定传入的user对象参数的值一致的,也就是非环绕通知中的JointPoint类型的参数会将连接点(printUser)方法中的参数自动传递给通知。
织入
织入是一个生成动态代理对象并将切面和目标对象方法编织成为约定流程的过程。Spring中推荐使用接口加实现类的方式来实现动态代理,也就是JDK的方式。但实际上动态代理的实现方法有很多,例如CGLIB,javasist,ASM等。Spring采用JDK+CGLIB的方式来实现动态代理,对于JDK而言它需要被代理的对象具有接口,而对于CGLIB则不需要。因此在默认情况下,当目标对象具有接口时,Spring会使用JDK的方式进行代理,如果没有接口时则使用CGLIB的方式生成代理对象。
因此当UserServiceImpl类没有实现UserService接口的情况下,在UserController中使用如下的非接口注入方式,也可以依赖注入:
@Autowired
private UserServiceImpl userServiceImpl;
此时,Spring将使用CGLIB的方式来生成相应的代理对象。
多个切面
上面的主要过程是对一个连接点方法使用一个切面进行运行。当对一个连接点方法同时运行多个切面的话,其运行过程以及切面的运行顺序就是怎样的呢?下面新创建一个连接点,然后创建多个切面进行测试。
在UserService接口中定义一个新的连接点方法和接口:
public interface UserService {
void printUser(User user);
/**
* AOP多个切面测试
*/
void manyAspects();
}
// 实现类
@Service
public class UserServiceImpl implements UserService {
// ...
@Override
public void manyAspects() {
System.out.println("测试多个Aspect的运行顺序");
}
}
定义多个切面,同时拦截目标对象的manyAspects方法:
@Aspect
public class ManyAspect1 {
@Pointcut("execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.manyAspects(..))")
public void manyAspects() {
}
@Before("manyAspects()")
public void before() {
System.out.println("ManyAspect1: before ...");
}
@After("manyAspects()")
public void after() {
System.out.println("ManyAspect1: after ...");
}
@AfterReturning("manyAspects()")
public void afterReturning() {
System.out.println("ManyAspect1: afterReturning ...");
}
@AfterThrowing("manyAspects()")
public void afterThrowing() {
System.out.println("ManyAspect1: afterThrowing ...");
}
}
@Aspect
public class ManyAspect2 {
@Pointcut("execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.manyAspects(..))")
public void manyAspects() {
}
@Before("manyAspects()")
public void before() {
System.out.println("ManyAspect2: before ...");
}
@After("manyAspects()")
public void after() {
System.out.println("ManyAspect2: after ...");
}
@AfterReturning("manyAspects()")
public void afterReturning() {
System.out.println("ManyAspect2: afterReturning ...");
}
@AfterThrowing("manyAspects()")
public void afterThrowing() {
System.out.println("ManyAspect2: afterThrowing ...");
}
}
@Aspect
public class ManyAspect3 {
@Pointcut("execution(* cn.zyt.springbootlearning.service.impl.UserServiceImpl.manyAspects(..))")
public void manyAspects() {
}
@Before("manyAspects()")
public void before() {
System.out.println("ManyAspect3: before ...");
}
@After("manyAspects()")
public void after() {
System.out.println("ManyAspect3: after ...");
}
@AfterReturning("manyAspects()")
public void afterReturning() {
System.out.println("ManyAspect3: afterReturning ...");
}
@AfterThrowing("manyAspects()")
public void afterThrowing() {
System.out.println("ManyAspect3: afterThrowing ...");
}
}
在UserController中加入如下方法进行测试:
@RequestMapping("/manyAspects")
public void manyAspects() {
userService.manyAspects();
}
使用如下的URL进行请求:
http://localhost:8080/user/manyAspects
结果输出如下:
ManyAspect1: before ...
ManyAspect2: before ...
ManyAspect3: before ...
测试多个Aspect的运行顺序
ManyAspect3: after ...
ManyAspect3: afterReturning ...
ManyAspect2: after ...
ManyAspect2: afterReturning ...
ManyAspect1: after ...
ManyAspect1: afterReturning ...
从输出结果中可以看到,对于前置通知都是从小到大运行,对于后置通知都是从大到小的顺序运行,因此这里默认是一个责任链模式。如果需要指定切面的运行顺序,也可以使用@Order(index)注解,标注在Aspect类上使用index指明运行顺序,例如@Order(1),这表示被标注的切面首先运行,依次类推,其运行方式依然是指定顺序的责任链模式。