个人博客地址:
http://xiaohe-blog.top
AOP :面向切面编程,全称 Aspect Oriented Programing,简而言之,在不惊动原始设计的基础上为其进行功能增强。
AOP本质就是Spring动态代理开发,通过代理类为原始类增加额外功能。
我们知道,Spring动态代理就是将service层的附加业务抽取出来,让它专注核心业务的实现。
AOP中的名词 :切面、连接点、切入点、通知。
连接点 :JoinPoint,所有业务方法都是连接点。不管有没有添加额外功能。
切入点 :PointCut,真正添加了额外功能的方法。
通知 :Advice,额外功能(同样它也调用了核心业务)。
切面 :Aspect,将切入点和通知结合。
一定要分清连接点与切入点,业务方法都是连接点(可以连接的点)。但只有真正连接了额外方法才能叫作切入点。两者可以随时转变,我今天给一个业务增加额外功能,它叫切入点,明天我不给它加了,它就变回连接点了。
现在我们要实现一个项目 :对用户的增删改,增删的同时记录此次操作所用时间,改的时候不需要记录用时。
public interface UserService {
public void add();
public void delete();
public void query();
}
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
public void add() {
System.out.println("开始执行add方法");
long start = System.currentTimeMillis();
userMapper.addUser();
long end = System.currentTimeMillis();
long time = end - start;
System.out.println("执行用时:" + time);
System.out.println("add方法执行结束");
}
public void delete() {
System.out.println("开始执行delete方法");
userMapper.deleteUser();
System.out.println("delete方法执行结束");
}
public void update() {
System.out.println("开始执行update方法");
userMapper.updateUser();
System.out.println("update方法执行结束");
}
}
现在add方法已经有记录时长的功能了,我们想要给delete加上这个功能,而update方法不需要这个功能。
在此处,连接点是 add、delete、update。而切入点是没有update的,因为你此时不需要为update添加额外功能。
那么我们如何使用AOP给这些切入点绑定通知呢?
AOP编程的开发步骤 :
我们已经编写了连接点,只需要掌握通知与切面即可完成AOP。
我们已经确认了切入点是delete :
public void delete() {
System.out.println("开始执行delete方法");
userMapper.deleteUser();
System.out.println("delete方法执行结束");
}
AOP的通知有很多种 :
这几种通知用的最多的是 环绕通知
,环绕通知中的核心业务可以在任意时刻执行,完成诸如核心业务前开启事务,核心业务后关闭事务这样的操作。
为了防止战线拉的太长,笔者将通知与切入点表达式放到了文章末,尽量快点让大家接领会到什么是AOP
要实现环绕通知,要先了解一个接口 :MethodInterceptor。
它只有一个方法 :invoke,届时我们的额外功能都会在此处编写。
MethodInvocation :核心业务,使用var1.proceed()可以调用核心业务。
返回值Object :invoke()返回值与核心业务的返回值相同。但是我们怎么知道核心业务返回什么呢?var1.proceed()执行的是核心业务,那么这个方法的返回值不就是核心业务的返回值吗 ?
所以我们可以将proceed的返回值返回 :
@Override
public Object invoke(MethodInvocation var1) throws Throwable {
// 执行切入点的核心业务.
Object proceed = var1.proceed();
return procedd;
}
于是我们的通知就可以这样写 :
// 通知类
public class MyAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation var1) throws Throwable {
System.out.println("通知开始执行");
long start = System.currentTimeMillis();
Object object = var1.proceed();
long end = System.currentTimeMillis();
long time = end - start;
System.out.println("执行用时:" + time);
System.out.println("通知执行结束");
return object;
}
}
切面的目的是指定切入点并且绑定通知
。这个过程可以使用注解形式完成,但是xml形式更加助于理解。
在Spring配置文件中,使用
// 为什么将通知注入Spring?
// 我们想要spring替我们执行额外功能,肯定要配置啊。
<bean id="myAdvice" class="com.entity.MyAdvice"></bean>
<aop:config>
<aop:pointcut id="pc"
expression="execution(public void com.entity.UserServiceImpl.delete())">
aop:pointcut>
<aop:advisor advice-ref="myAdvice" pointcut-ref="pc">aop:advisor>
aop:config>
(是不是觉得很麻烦 ?这已经很简单了,看到那个expression属性了没?它叫做“切入点表达式”,下面要花很大功夫讲它。)
这就是完整的aop开发步骤 :完成连接点、完成通知、编写切面。
上一节中使用getBean(“userService”)获得UserService的实现类还是UserServiceImpl吗?
很明显不是了,我们可以获取它,执行delete,可以看到执行的已经不是原本的delete方法,因为在我们看不见的地方,Spring给我们组装好了动态代理对象。
# 如果没有用到AOP,那么代码应该是这样的:
UserService userService = new UserServiceImpl();
# 用到了AOP,代码是这样的:
UserService userService = new UserServiceProxy();
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = app.getBean("userService");
userService.delete();
}
那么Spring创建的动态代理类在哪里呢 ?
Spring框架在运行时,通过
动态字节码技术
创建在JVM中,运行在JVM内部,等程序结束后,会和JVM一起消失。JVM运行一个类其实就是运行这个.java文件加载后的.class文件(字节码文件)。
动态字节码技术不需要我们编写.java文件,它通过第三方框架直接动态生成代理对象的字节码文件。
AOP的底层实现就是Spring动态代理的实现。通过动态代理创建出一个同时具有 额外功能 + 核心业务 的代理类
。
如果你对动态代理技术不太了解,可以阅读这位大佬的博客 :http://t.csdn.cn/Z6x35
那么问题来了,为什么我们通过getBean(“userService”)获得的Bean对象是动态代理对象而不是原对象呢?
不仅是 getBean(“userService”),就连 getBean(UserServiceImpl.class) 都无法获取UserServiceImpl。
肯定是 Spring 做的,它是如何实现的?
在之前的学习中,我们接触到了 BeanPostProcessor
这个类,这个类在 Spring 创建 bean 对象之后执行。
它有两个方法,一个before、一个after,before方法在 bean 调用构造函数之后&初始化之前执行,after在bean初始化之后执行。
Spring在 BeanPostProcessor 中的 After 方法将UserServiceImpl类改为UserServiceProxy 并返回给我们,所以我们获取的不是实现类而代理类。
spring通知共5种 :
- 前置通知 Before :MethodBeforeAdvice
- 后置通知 After :AfterAdvice
- 环绕通知 Around :MethodInterceptor
- 返回后通知 After-returning :AfterReturningAdvice
- 异常后通知 After-throwing :ThrowsAdvice
共有5中,常用的其实就一个环绕通知。但是前置通知和异常后通知需要讲一下。
接口为 MethodBeforeAdvice
Method :切入点,你想给delete增加额外方法,Method就是delete。
Object[] :切入点的所有参数。delete的参数。
Object :切入点所在类的实例。delete在UserService中,那Object就是UserService。
下面动手完成AOP的前置通知 :
AOP编程的步骤是一成不变的 :完成连接点、完成通知、编写切面。
public void delete() {
System.out.println("开始执行delete方法");
userMapper.deleteUser();
System.out.println("delete方法执行结束");
}
public class MyBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("前置通知执行...");
}
}
<bean id="myBeforeAdvice" class="com.entity.MyBeforeAdvice">bean>
<aop:config>
<aop:pointcut id="pc"
expression="execution(public void com.entity.UserServiceImpl.delete())">
aop:pointcut>
<aop:advisor advice-ref="myBeforeAdvice" pointcut-ref="pc">aop:advisor>
aop:config>
以后的拦截器跟这个原理很像。
接口为 ThrowsAdvice。其中没有方法,需要我们自定义
public class MyExceptionAdvice implements ThrowsAdvice {
public void afterThrowing(Exception ex) {
System.out.println("异常信息 :"+ex.getMessage());
}
}
以后的全局异常处理器跟这个原理很像。
刚才咱们在切面中写的代码是 :
expression="execution(public void com.entity.UserServiceImpl.delete())"
这个就是切入点表达式,试想一下,如果我们还有一个save、一个add需要添加这个通知,我们该怎么写?总不能再配置一个吧。这时就要学习切面表达式了。
方法切入点表达式由两部分组成 :修饰符+返回值、方法名(参数)。
如果我们想要给所有方法加上通知,该怎么做?使用通配符 *
因为分为两个部分,那么就是用两个 *,参数用两个 …代替,即为任意参数
定义所有login作为切入点
* login(..)
指定有两个String形参的所有login作为切入点
* login(String, String)
// 注意: 非java.lang包中的类型必须写全限定类名
如 * login(com.xiaohe.User)
指定第一个参数为String,其他参数随意的所有login作为切入点
* login(String, ..)
指定整个类为切入点,它的全部方法都会成为切入点。
* com.service.UserServiceImpl.*(..)
指定整个包为切入点,这个包中的所有类的所有方法都成为切入点。
* com.service.*.*()
太累了,写不下去了,种地去了。