Spring学习(八)Spring AOP
Spring AOP
在学习Spring AOP之前,需要先学习Java的代理设计模式。详细可以参考
:设计模式之代理模式(静态代理、Java动态代理、Cglib动态代理)。
代理模式的优点:
- 可以使得真实角色的操作更加纯粹,不需要去关注一些公共的业务
- 公共业务就交给代理角色,实现了业务的分工
- 公共业务扩展的时候方便集中管理
AOP大白话
不使用代理的开发中都是通过dao层,service层,controller层最后到前端的纵向开发。如果此时我们想给service中的比如User的业务里增加一个log日志功能,由于开闭原则,我们不能直接去service里修改代码。所以我们使用一个代理,横向开发去增加一个log功能,这就是Aop的实现机制。
什么是AOP?
AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离(可以多个横向开发),从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
图中很好地解释了spring对于业务逻辑的加减乘除扩展验证参数和日志功能做了很好的展示。
AOP在Spring中的作用
AOP在Spring中提供声明式事务:允许用户自定义切面。
- 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的那部分,就是横切关注度。比如日志、安全、缓存、事务等等。说白了就是和我们业务无关的一些基础功能,这些基础功能在很多个业务模块里都能用到。
- 切面(Aspect):横切关注点被模块化后的特殊对象,就是横切关注点被类化,如上图里的验证参数和前置后置日志。(验证切面、权限切面、日志切面等等等等)。由切点和增强组成,既包含了横切逻辑的定义。也包含了连接点的定义。
- 通知(Advice):切面必须要完成的工作。就是类中的方法。是切面的详细实现。
- 目标对象(Target):被通知对象(被增强的对象)。
- 代理对象(Proxy):向模板对象应用通知之后被动态创建的对象(目标对象增强后的对象)。能够简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共同拥有功能(横切关注点)。代理对象对于使用者而言是透明的。是程序执行过程中的产物。
- 连接点:程序运行的某个特定的位置。比如在类初始化前、初始化后、方法调用前、方法调用后等等位置。
- 切入点(PointCut):切面通知执行的“地点”的定义。就是定义通知应该切入到哪些连接点上。
- 织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。
- 增强:织入到目标类的连接点上的代码。(就是给原有的类里的某个地方新增了一段代码,称为增强)
下图就很好地诠释了这些词:
我们的目的是在业务逻辑UserService的getUser方法运行过程中加入一个存储进日志的功能。那么我们首先横切关注点为日志,将其抽象化为日志类(切面),日志切面里有具体的实现方法(通知)。在getUser()调用前调用后或者其他位置都有连接点,假设我们在getUser()后通知,定义一个切入点来告诉通知要去织入哪个连接点。然后在目标对象UserSerivce运行过程中,我们织入日志切面到UserSerivce的getUser()方法调用后面,然后动态创建了增强了原有对象的代理对象。
在SpringAop中,通过Advice(通知)来定义横切逻辑,Spring中支持5种类型的Advice:
通知类型 | 连接点 | 实现接口 |
---|---|---|
前置通知 | 方法前 | org.springframework.aop.MethodBeforeAdvice |
后置通知 | 方法后 | org.springframework.aop.AfterReturningAdvice |
环绕通知 | 方法前后 | org.aoplliance.intercept.MethodInterceptor |
异常抛出通知 | 方法抛出异常 | org.springframework.aop.ThrowsAdvice |
引介通知 | 类中增加新的方法属性 | org.springframework.aop.IntroductionInterceptor |
使用Aop在不改变原有代码的情况下,增加新的功能,这个功能的函数我们称之为通知。
Spring AOP实战
使用spring实现AOP我们还需要先导入一个依赖包
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
实现方式一:使用spring的API接口实现
首先创建一个UserSerivce接口,我们规定必须有增删改查方法。然后其对应的实现类UserSerivceImpl也写好。
//接口 UserService
public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}
//实现类 UserServiceImpl
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("增加了一个用户");
}
public void delete() {
System.out.println("删除了一个用户");
}
public void update() {
System.out.println("修改了一个用户");
}
public void query() {
System.out.println("查询用户");
}
}
我们想在增删改查方法的前后增加一个日志记录的功能(横切关注点),然后我们去将该关注点抽象为beforeLog类和afterLog类。这两个类分别实现了Spring AOP中的MethodBeforeAdvice和AfterReturningAdvice接口。
// com.hj.log.beforeLog类
public class beforeLog implements MethodBeforeAdvice {
// method:要执行的目标对象的方法
// objects:参数
// target:目标对象
public void before(Method method, Object[] objects, Object target) throws Throwable {
System.out.println(target.getClass().getName() + "的" + method.getName() + "被执行了");
}
}
// com.hj.log.afterLog类
public class afterLog implements AfterReturningAdvice {
//和before的差不多,多一个returnValue,执行后是可以获得那个方法的返回值的(执行前是没得获得的)
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了"+method.getName()+"方法,返回结果为:"+returnValue);
}
}
好了,接下来只要在xml配置文件中再定义好这些类的方法作为通知,并且定义切入点为增删改查调用前后就好了。
我们要使用aop功能,必须添加xmlns:aop="http://www.springframework.org/schema/aop"
约束和 http://www.springframework.org/schema/aop
以及http://www.springframework.org/schema/aop/spring-aop.xsd
然后
<?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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.hj.service.UserServiceImpl"/>
<bean id="beforelog" class="com.hj.log.beforeLog"/>
<bean id="afterlog" class="com.hj.log.afterLog"/>
<!--方式一:使用原生spring API接口实现-->
<!--配置aop-->
<aop:config>
<!--切入点: expression:表达式,execution(要执行的位置!,
比如这个例子就是在UserServiceImpl的*所有方法(增删改查)都可以执行)-->
<aop:pointcut id="pointcut" expression="execution(* com.hj.service.UserServiceImpl.*(..))"/>
<!--配置通知-->
<aop:advisor advice-ref="beforelog" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterlog" pointcut-ref="pointcut"/>
</aop:config>
</beans>
大功告成!
接下来可以在测试中看到我们的增删改查方法前后都有了日志通知。
public class MyTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 动态代理代理的是接口,getbean获取的是动态代理对象,所以这个对象的类型并不是实现类的类型,而是接口类型。所以这里直接UserService而不是UserServiceImpl
UserService userService = context.getBean("userService", UserService.class);
userService.add();
//com.hj.service.UserServiceImpl的add被执行了
//增加了一个用户
//执行了add方法,返回结果为:null
userService.delete();
//com.hj.service.UserServiceImpl的delete被执行了
//删除了一个用户
//执行了delete方法,返回结果为:null
}
}
这样我们增删改查的业务逻辑就很纯粹,日志的业务逻辑也很纯粹,spring帮我们把他们合在一起了。
实现方式二:使用自定义类来实现
实现方式一是需要接口的。那如果没有接口怎么办?
我们也可以实现自定义类的aop
我们新创一个自定义类MyClass.java,类里定义了目标对象方法前和方法后需要调用的方法。
public class MyClass {
public void before(){
System.out.println("--------------方法执行前--------------");
}
public void after(){
System.out.println("--------------方法执行后--------------");
}
}
现在去配置文件中配置
<?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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--方式二:自定义类实现-->
<!--注册bean-->
<bean id="userService" class="com.hj.service.UserServiceImpl"/>
<bean id="diy" class="com.hj.diy.MyClass"/>
<aop:config>
<!--自定义切面,ref要引用的类-->
<aop:aspect ref="diy">
<!--切入点: expression:表达式,execution(要执行的位置!,
比如这个例子就是在UserServiceImpl的*所有方法(增删改查)都可以执行)-->
<aop:pointcut id="point" expression="execution(* com.hj.service.UserServiceImpl.*(..))"/>
<!--配置通知-->
<aop:before method="before" pointcut-ref="point"/>
<aop:after method="after" pointcut-ref="point"/>
</aop:aspect>
</aop:config>
</beans>
测试类不变。可以得到一样的效果。
public class MyTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 动态代理代理的是接口,getbean获取的是动态代理对象,所以这个对象的类型并不是实现类的类型,而是接口类型。所以这里直接UserService而不是UserServiceImpl
UserService userService = context.getBean("userService", UserService.class);
userService.add();
userService.delete();
//--------------方法执行前--------------
//增加了一个用户
//--------------方法执行后--------------
}
}
实现方式三:使用注解实现
使用注解实现aop。首先创建一个切面类AnnotationPointCut.java
//方式三:使用注解方式实现AOP
@Component //自动写bean
@Aspect //标注这个类是一个切面
public class AnnotationPointCut {
@Before("execution(* com.hj.service.UserServiceImpl.*(..))")
public void before(){
System.out.println("方法执行前");
}
@After("execution(* com.hj.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("方法执行后");
}
}
使用@Component来将该类注册bean,再用@Aspect声明该类是一个切面。然后使用@Before和@After来标注方法为通知。
然后在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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--方式三:注解实现(在AnnotationPointCut类里实现了)-->
<!--扫描com.hj.pojo下的component组件-->
<context:component-scan base-package="com.hj.diy"/>
<!--开启注解的支持-->
<context:annotation-config/>
<!--aop自动代理 proxy-target-class="false"表示用JDK动态代理,true就是用cglib动态代理-->
<aop:aspectj-autoproxy/>
</beans>
然后测试类可以不变,直接运行即可
public class MyTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 动态代理代理的是接口,getbean获取的是动态代理对象,所以这个对象的类型并不是实现类的类型,而是接口类型。所以这里直接UserService而不是UserServiceImpl
UserService userService = context.getBean("userService", UserService.class);
userService.add();
userService.delete();
}
}
小结
三种实现方式是有一定优先级的,我们使用三种方式同时实现aop的时候,发现输出是这个样子的。
com.hj.service.UserServiceImpl的add被执行了
--------------方法执行前--------------
方法执行前
增加了一个用户
方法执行后
--------------方法执行后--------------
执行了add方法,返回结果为:null
com.hj.service.UserServiceImpl的delete被执行了
说明方法一、方法二和方法三的执行顺序是固定的,一的before比二比三要先,然后after也比二比三要晚。
aop就是一种横向编程的思想!目的是在不改变原本类的情况下实现一些方法的增强!