AOP相关知识概述
「AOP基本概念」
AOP:全称是Aspect Oriented Programming,即:面向切面编程。简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。(俗话:不通过修改源代码方式,在主干功能里面添加新功能)
作用:在程序运行期间,不修改源码对已有方法进行增强。优势:减少重复代码、提高开发效率、维护方便
实现方式:动态代理技术
如,有个业务层接口:
// 客户的业务层接口
public interface ICustomerService {
// 保存客户
void saveCustomer(Customer customer);
// 查询所有客户
List<Customer> findAllCustomer();
// 删除客户
void removeCustomer(Customer customer);
// 根据id查询客户
Customer findCustomerById(Long custId);
// 修改客户
void updateCustomer(Customer customer);
}
客户的业务层实现类:
/**
* 客户的业务层实现类
* 事务必须在此控制
* 业务层都是调用持久层的方法
*/
public class CustomerServiceImpl implements ICustomerService {
private ICustomerDao customerDao = new CustomerDaoImpl();
@Override
public void saveCustomer(Customer customer) {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
customerDao.saveCustomer(customer);
tx.commit();
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
}
@Override
public List<Customer> findAllCustomer() {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
List<Customer> customers = customerDao.findAllCustomer();
tx.commit();
return customers;
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
}
@Override
public void removeCustomer(Customer customer) {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
customerDao.removeCustomer(customer);
tx.commit();
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
}
@Override
public Customer findCustomerById(Long custId) {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
Customer c = customerDao.findCustomerById(custId);
tx.commit();
return c;
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
}
@Override
public void updateCustomer(Customer customer) {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
customerDao.updateCustomer(customer);
tx.commit();
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
}
}
以上代码有什么问题呢?我们的事务控制是重复性的代码。这还只是一个业务类,如果有多个业务了,每个业务类中都会有这些重复性的代码。那么解决方案就是使用动态代理
「动态代理概念」
动态代理特点:
(1)字节码随用随创建,随用随加载。
(2)它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。装饰者模式就是静态代理的一种体现。
(3)在不改变源码的基础上,对已有的方法进行增强。
动态代理两种方式:
- 基于接口的动态代理
- 提供者:JDK官方的Proxy类
- 要求:被代理类最少实现一个接口
- 例如:CustomerDaoImpl实现CustomerDao接口,那么就可以使用此方式创建CustomerDao接口实现类代理对象
- 基于子类的动态代理
- 提供者:第三方的CGLib(如果报asmxxxx异常,需要导入asm.jar)
- 要求:被代理类不能用final修饰的类(最终类)
- 例如:class Customer extends User,创建User类子类的代理对象
「Proxy类创建代理对象」
使用JDK官方的Proxy类创建代理对象(重要)
创建的方式:Proxy.newProxyInstance 。三个参数如下:
- ClassLoader:和被代理对象使用相同的类加载器
- Class[]:字节码数组(增强方法所在的类)。和被代理对象具有相同的行为。实现相同的接口
- InvocationHandler:如何代理
InvocationHandler里面的 invoke()方法,执行被代理对象的任何方法,都会经过该方法。参数:
- proxy:代理对象的引用。不一定每次都用得到
- method:当前执行的方法对象
- args:执行方法所需的参数
返回值为当前执行方法的返回值
演员接口:
public interface IActor {
// 基本演出
public void basicAct(float money);
// 危险演出
public void dangerAct(float money);
}
实现了接口的演员类:
public class Actor implements IActor{
@Override
public void basicAct(float money) {
System.out.println("Actor基本表演,片酬:" + money);
}
@Override
public void dangerAct(float money) {
System.out.println("Actor杂技表演,片酬:" + money);
}
}
创建代理对象:
public class Client {
public static void main(String[] args) {
// 剧组找演员,直接的方式
final Actor actor = new Actor();
actor.basicAct(5000);
actor.dangerAct(8000);
// 代理方式,间接的方式
IActor proxyActor = (IActor) Proxy.newProxyInstance(
actor.getClass().getClassLoader(),
actor.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName(); // basicAct() 或者 dangerAct()
Float money = (Float) args[0]; // 取到对应方法的入参
Object rtValue = null;
// 每个经纪公司对不同演出收费不一样,此处开始判断
if ("basicAct".equals(name)) {
if (money > 2000) {
rtValue = method.invoke(actor, money / 2);
}
}
if ("dangerAct".equals(name)) {
if (money > 7000) {
rtValue = method.invoke(actor, money / 2);
}
}
return rtValue;
}
});
proxyActor.basicAct(5000);
proxyActor.dangerAct(8000);
}
}
「Enhancer类创建代理对象」
使用CGLib的Enhancer类创建代理对象(重要)
Enhancer.create(Class, Callback)
方法的参数:
- Class:被代理对象的字节码
- Callback:如何代理
create()里面的intercep()方法,执行被代理对象的任何方法,都会经过该方法。在此方法内部就可以对被代理对象的任何方法进行增强。参数如下:
- Object o:代理对象的引用。不一定每次都用得到
- Method method:当前执行的方法对象
- Object[] objects:执行方法所需的参数
- MethodProxy methodProxy:当前执行方法的代理对象。(一般不用)
没有实现任何接口的演员类:
// 没有实现任何接口
public class Actor {
public void basicAct(float money) {
System.out.println("Actor(无实现接口)基本表演,片酬:" + money);
}
public void dangerAct(float money) {
System.out.println("Actor(无实现接口)杂技表演,片酬:" + money);
}
}
创建代理对象:
public class Client {
public static void main(String[] args) {
// 剧组找演员,直接的方式
final Actor actor = new Actor();
actor.basicAct(5000);
actor.dangerAct(8000);
Actor cglibActor = (Actor) Enhancer.create(actor.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
String name = method.getName();
Float money = (Float) objects[0];
Object rtValue = null;
// 每个经纪公司对不同演出收费不一样,此处开始判断
if ("basicAct".equals(name)) {
if (money > 2000) {
rtValue = method.invoke(actor, money / 2);
}
}
if ("dangerAct".equals(name)) {
if (money > 7000) {
rtValue = method.invoke(actor, money / 2);
}
}
return null;
}
});
cglibActor.basicAct(5000);
cglibActor.dangerAct(8000);
}
}
解决第一小节里的案例问题
第一小节,使用了CustomerServiceImpl业务层实现类,每个方法中都有重复的事务控制。
那我我们解决问题的思路就是:使用动态代理技术创建业务层的代理对象,在执行CustomerServiceImpl时,对里面的方法进行增强(里面只有调用dao层的操作),加入事务的支持。 最后返回的是代理对象
/**
* 用于创建客户业务层对象工厂
*/
public class BeanFactory {
// 获取客户业务层对象的代理对象
public static ICustomerService getCustomerService() {
// 定义客户业务层对象
final ICustomerService customerService = new CustomerServiceImpl();
// 生成它的代理对象
ICustomerService proxyCustomerService = (ICustomerService) Proxy.newProxyInstance(
customerService.getClass().getClassLoader(), customerService.getClass().getInterfaces(),
new InvocationHandler() {
// 执行客户业务层任何方法,都会在此处被拦截,我们对那些方法增强,加入事务
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
Object rtValue = null;
try {
// 开启事务
HibernateUtil.beginTransaction();
// 执行操作
rtValue = method.invoke(customerService, args);
// 提交事务
HibernateUtil.commit();
} catch (Exception e) {
HibernateUtil.rollback();
e.printStackTrace();
} finally {
// 释放资源.hibernate在我们事务操作(提交/回滚)之后,已经帮我们关了。如果他没关,我们在此处关
}
return null;
}
}
);
return proxyCustomerService;
}
}
「Spring中的AOP术语」
在spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
实现了接口,使用Proxy;未实现接口,使用CGLib
AOP相关术语
- Joinpoint(连接点)
- 所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。--【
接口或类里可以被增强的方法】 - 例如,上文例子中的:basicAct() 和 dangerAct()
- 所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。--【
- Pointcut(切入点)
- 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。--【
真正被增强的方法】 - 切入点一定是连接点,但连接点不一定是切入点
- 例如,上文例子中的:basicAct() 和 dangerAct() 都进行了增强,那么这两个都是切入点
- 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。--【
- Advice(通知/增强)
- 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。--【
实际增强的逻辑部分称为通知(增强)】 - 通知的类型:前置通知,后置通知,异常通知/例外通知,最终通知,环绕通知。--【
根据在切入点方法的位置】
- 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。--【
- Introduction(引介)
- 引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field。--一般不用
- Target(目标对象)
- 代理的目标对象。--【
被代理对象】 - 例如,上文例子中的:Actor
- 代理的目标对象。--【
- Weaving(织入)
- 是指把增强应用到目标对象来创建新的代理对象的过程。
- spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
- Proxy(代理)
- 一个类被AOP织入增强后,就产生一个结果代理类。--【
代理对象】 - 例如,上文例子中的:proxyActor 和 cglibActor
- 一个类被AOP织入增强后,就产生一个结果代理类。--【
- Aspect(切面)
- 是切入点和通知(引介)的结合。--【
是动作,把通知应用到切入点过程】
- 是切入点和通知(引介)的结合。--【
注意事项
a、开发阶段
编写核心业务代码(开发主线)
把公用代码抽取出来,制作成通知。(开发阶段最后再做)
在配置文件中,声明切入点与通知间的关系,即切面。
b、运行阶段(Spring框架完成的)
Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
Spring框架一般都是基于AspectJ实现AOP操作。
AspectJ不是Spring组成部分,独立AOP框架,一般把AspectJ和Spirng框架一起使用,进行AOP操作
基于XML的AOP配置
「AOP案例」
第一步:准备客户的业务层接口和实现类(需要增强的类)
// 客户业务层接口
public interface CustomerService {
void saveCustomer(); // 保存客户
void updateCustomer(Integer i); // 修改客户
}
// 客户业务层实现类
public class CustomerServiceImpl implements CustomerService{
@Override
public void saveCustomer() {
System.out.println("CustomerServiceImpl:执行保存客户");
}
@Override
public void updateCustomer(Integer i) {
System.out.println("CustomerServiceImpl:执行修改客户");
}
}
第二步:把客户的业务层配置到spring容器中
<bean id="customerService" class="com.code.service.CustomerServiceImpl" />
第三步:制作通知(增强的类)
public class Logger {
public void beforePrintLog() {
System.out.println("[before]Logger类中的PrintLog方法记录日志...");
}
public void afterPrintLog() {
System.out.println("[after]Logger类中的PrintLog方法记录日志...");
}
}
下面是基于XML的AOP配置
备注:需要额外导入如下两个jar包:
第一步:把通知类用bean标签配置起来
<bean id="logger" class="com.code.aop.Logger" />
第二步:引入aop,并使用 aop:config 声明aop配置
<?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">
...
<!-- aop的配置代码 -->
<aop:config>
</aop:config>
</beans>
第三步:使用 aop:aspect 配置切面
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
...
</aop:aspect>
</aop:config>
第四步:使用 aop:pointcut 配置切入点表达式
<aop:pointcut id="pt1" expression="execution(public void com.code.service.CustomerServiceImpl.saveCustomer())"/>
第五步:使用 aop:before 和 aop:after 配置前置\后置通知
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<aop:after method="afterPrintLog" pointcut-ref="pt2"></aop:after>
总体配置如下:
<?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 id="customerService" class="com.code.service.CustomerServiceImpl" />
<bean id="logger" class="com.code.aop.Logger" />
<!-- aop的配置代码 -->
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<aop:pointcut id="pt1" expression="execution(public void com.code.service.CustomerServiceImpl.saveCustomer())"/>
<aop:before method="beforePrintLog" pointcut-ref="pt1" />
<aop:after method="afterPrintLog" pointcut-ref="pt1" />
</aop:aspect>
<aop:aspect id="logAdvice2" ref="logger">
<aop:pointcut id="pt2" expression="execution(public void com.code.service.CustomerServiceImpl.updateCustomer(..))"/>
<aop:before method="beforePrintLog" pointcut-ref="pt2" />
<aop:after method="afterPrintLog" pointcut-ref="pt2" />
</aop:aspect>
</aop:config>
</beans>
测试结果:
「切入点表达式」
execution(表达式) : 知道对哪个类里面的哪个方法进行增强
表达式语法:execution([修饰符] [返回值类型] 包名.类名.方法名(参数))
写法说明:
- 全匹配方式:
execution(public void com.code.service.CustomerServiceImpl.saveCustomer())
- 访问修饰符可以省略
execution(void com.code.service.CustomerServiceImpl.saveCustomer())
- 返回值可以使用*号,表示任意返回值
execution(* com.code.service.CustomerServiceImpl.saveCustomer())
- 包名可以使用*号,表示任意包,但是有几级包,需要写几个*
execution(* *.*.*.CustomerServiceImpl.saveCustomer())
- 使用..来表示当前包及其子包
execution(* com..CustomerServiceImpl.saveCustomer())
- 类名可以使用*号,表示任意类
execution(* com..*.saveCustomer())
- 方法名可以使用*号,表示任意方法
execution(* com..*.*())
- 参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
execution(* com..*.*(*))
- 参数列表可以使用..表示有无参数均可,有参数可以是任意类型
execution(* com..*.*(..))
- 全通配方式:
execution(* *..*.*(..))
实际开发中,一般都是对业务型方法进行增强,所以写法一般是 * com.test.service.impl.*.*(..)
「AOP配置常用标签」
|<aop:config>|
作用:用于声明开始aop的配置。所有aop相关的都要写在这个标签下面
|<aop:aspect>|
作用:用于配置切面。此标签要出现在<aop:config>内部
属性:
- id:给切面提供一个唯一标识
- ref:引用配置好的通知类bean的id
|<aop:pointcut>|
作用:用于配置切入点表达式
属性:
- expression:用于定义切入点表达式
- id:用于给切入点表达式提供一个唯一标识
|<aop:before>|
作用:用于配置前置通知
属性:
- method:指定通知中方法的名称
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
|<aop:after-returning>|
作用:用于配置后置通知
属性:
- method:指定通知中方法的名称
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
|<aop:after-throwing>|
作用:用于配置异常通知
属性:
- method:指定通知中方法的名称
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
|<aop:after>|
作用:用于配置最终通知
属性:
- method:指定通知中方法的名称
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
|<aop:around>|
作用:用于配置环绕通知
属性:
- method:指定通知中方法的名称
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
「通知类型说明」
配置通知的类型:
aop:before:用于配置前置通知。前置通知的执行时间点:切入点方法执行之前执行aop:after-returning:用于配置后置通知。后置通知的执行时间点:切入点方法正常执行之后。它和异常通知只能有一个执行aop:after-throwing:用于配置异常通知。异常通知的执行时间点:切入点方法执行产生异常后执行。它和后置通知只能执行一个。aop:after:用于配置最终通知。最终通知的执行时间点:无论切入点方法执行时是否有异常,它都会在其后面执行。aop:around:用于配置环绕通知。他和前面四个不一样,他不是用于指定通知方法何时执行的。
<aop:before method="beforePrintLog" pointcut-ref="pt1"/>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"/>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"/>
<aop:after method="afterPrintLog" pointcut-ref="pt1"/>
<aop:around method="aroundPringLog" pointcut-ref="pt1"/>
环绕通知说明
/**
* 环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制增强部分什么时候执行的方式。
* 问题:
* 当我们配置了环绕通知之后,增强的代码执行了,业务核心方法没有执行。
* 分析:
* 通过动态代理我们知道在invoke方法中,有明确调用业务核心方法:method.invoke()
* 我们配置的环绕通知中,没有明确调用业务核心方法
* 解决:
* spring框架为我们提供了一个接口:【ProceedingJoinPoint】,它可以作为环绕通知的方法参数
* 在环绕通知执行时,spring框架会为我们提供该接口的实现类对象,我们直接使用就行
* 该接口中有一个方法proceed(),此方法就相当于method.invoke()
* @param pjp
*/
public void aroundPrintLog(MethodInvocationProceedingJoinPoint pjp) {
try {
System.out.println("前置通知:Logger类中的aroundPrintLog方法记录日志");
pjp.proceed();
System.out.println("后置通知:Logger类中的aroundPrintLog方法记录日志");
} catch (Throwable e) {
System.out.println("异常通知:Logger类中的aroundPrintLog方法记录日志");
e.printStackTrace();
} finally {
System.out.println("最终通知:Logger类中的aroundPrintLog方法记录日志");
}
}
基于注解的AOP配置
「AOP案例」
第一步:准备客户的业务层接口和实现类
public interface CustomerService {
void saveCustomer();
void updateCustomer(Integer i);
int deleteCustomer();
}
public class CustomerServiceImpl implements CustomerService {
...略(就是实现方法调用dao层代码)
}
第二步:把资源使用注解让spring来管理
@Service("customerService")
public class CustomerServiceImpl implements CustomerService {
...
}
第三步:在配置文件中指定spring要扫描的包
<?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
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.code.service" />
</beans>
下面是基于注解的AOP配置 (注解+XML)
第一步:把通知类也使用注解配置,并在通知类上使用 @Aspect 注解声明为切面:
@Component("logger")
@Aspect // 表明当前类是一个切面类
public class Logger {
public void printLog() {
System.out.println("Logger Logger Logger...");
}
}
第二步:在增强的方法上使用@Before注解配置前置通知
@Component("logger")
@Aspect
public class Logger {
@Before("execution(* com.code.service.*.*(..))")
public void printLog() {
System.out.println("Logger Logger Logger...");
}
}
第三步:在spring配置文件中开启spring对注解AOP的支持(开启生成代理对象)
<!--开启spring对注解AOP的支持-->
<aop:aspectj-autoproxy />
测试结果:
「AOP配置常用注解」
|@Aspect|
作用:把当前类声明为切面类
@Aspect
public class Logger {}
相当于之前的:<aop:aspect id="logAdvice" ref="logger">
|@Before|
作用:把当前方法看成是前置通知。
属性:value:用于指定切入点表达式,还可以指定切入点表达式的引用。
@Before("execution(* com.code.service.*.*(..))")
public void printLog() {}
相当于之前的:
<aop:pointcut id="pt1" expression="execution(* com.code.service.*.*(..))"/>
<aop:before method="printLog" pointcut-ref="pt1" />
|@AfterReturning|
作用:把当前方法看成是后置通知。
属性:value:用于指定切入点表达式,还可以指定切入点表达式的引用。
|@AfterThrowing|
作用:把当前方法看成是异常通知。
属性:value:用于指定切入点表达式,还可以指定切入点表达式的引用。
|@After|
作用:把当前方法看成是最终通知。
属性:value:用于指定切入点表达式,还可以指定切入点表达式的引用。
|@Around|
作用:把当前方法看成是环绕通知。
属性:value:用于指定切入点表达式,还可以指定切入点表达式的引用。
|@Pointcut|
作用:指定切入点表达式(相同的切入点抽取)
属性:value:指定表达式的内容
//相同切入点抽取
@Pointcut(value = "execution(* com.demo.spring5.aopanno.User.add(..))")
public void pointdemo() {}
@Before(value = "pointdemo()")
public void before() {
System.out.println("before.........");
}
|@Order|
作用:有多个增强类对同一个方法进行增强,设置增强类优先级
在增强类上面添加注解 @Order(数字类型值),数字类型值越小优先级越高
@Aspect
@Order(1)
public class Logger {}
「纯注解方式」
不使用XML的配置方式:
@Configuration
@ComponentScan(basePackages="com.itheima")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class SpringConfiguration {}