一、Java中代理
1. 概念
代理(Proxy) 是一种设计模式,提供了对目标对象另外的访问方式:即通过代理对象访问目标对象。这样做的好处是 可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
2. Java 代理(三种)
- 静态代理
- JDK动态代理(接口代理)
- CGLib动态代理(子类代理)
1)静态代理
静态代理概念:代理类实现与目标对象相同的接口,通过构造器或set方法给代理对象注入目标对象。在实现代理对象接口方法时,内部调用目标对象真正实现方法并且可以添加额外的业务控制,从而实现在不修改目标对象的基础上,对目标对象进行扩展。
- 优点:能够在不修改目标对象功能的前提下,对目标功能进行扩展
- 缺点:一个目标对象,一个代理对象,导致代理类过多;代理对象需要与目标对象实现一样的接口,一旦接口增加方法,目标对象与代理对象都要维护
2)JDK动态代理
动态代理:代理类在程序运行时创建的代理方式,有以下特点
- 代理对象不需要实现接口
- 动态代理(也叫做接口代理)要求目标对象一定要实现接口
- 代理对象的生成是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
3)CGLib代理
CGLib代理概念:CGLib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。静态代理和动态代理模式都要求目标对象实现接口,但是有时目标对象只是一个单独的对象,并没有实现任何的接口,这时就可以使用以目标对象子类的方式类实现代理,即CGLib代理。
使用CGLib子类代理:
- 需要引入
cglib
的jar文件,但是Spring的核心包中已经包括了Cglib功能 - 引入功能包后,就可以在内存中动态构建子类
- 代理的类不能为final,否则会报错
- 目标对象的方法如果为 final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法
二、Spring AOP概述
1. 什么是AOP?
AOP(Aspect Oriented Programming)
,即面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以实现业务逻辑各个部分的隔离,从而使得业务逻辑各个部分的耦合性降低,提高程序的可重用性,同时提高开发效率。简单来说AOP使用动态代理技术,实现在不修改Java源代码的情况下,运行时实现方法功能的增强。
2. 作用及优势
- 作用:在程序运行期间,不修改源码对已有方法进行增强。
- 优势:减少重复代码,提高开发效率 / 统一管理统一调用,方便维护
3. 实现原理
原理:动态代理技术
说明:在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式:如果目标对象实现接口,使用JDK代理;目标对象没有实现接口,使用CGLib代理。
4. 常用术语
Target(目标对象)
:被代理的对象;Joinpoint(连接点)
:在Spring中,连接点指的都是方法(指的是那些要被增强功能的候选方法),Spring只支持方法类型的连接点;Pointcut(切入点)
:切入点是指要对哪些 Joinpoint 进行拦截的定义;Aspect(切面)
:切面指的是切入点和通知的结合;Advice(通知/增强)
:通知即拦截到 Joinpoint 之后所要做的事情,通知的类型: 前置通知/后置通知/异常通知/最终通知/环绕通知;Weaving(织入)
:织入指的是增强用于目标对象,创建代理对象的过程,spring采用动态代理织入,AspectJ采用编译期织入和类装载期织入;Proxy(代理)
:一个类被AOP织入增强后,即产生一个结果代理类。
5. 关键概念
1)什么是切面类?
切面类简单来看就是重复的代码形成的类就是切面类
2)常见的切面类有哪些?
事务切面类、日志切面类、权限切面类
3)面向切面编程?
面向重复代码编程,重复代码只要写一次,自动调用自动运行
三、Spring AOP 编程
1. 开发阶段
- 根据业务需求,编写核心业务代码
- 把公用代码抽取出来,制作成通知,通知所在的类就是切面类
- 通过配置的方式,建立切入点(业务功能)和 通知 的关系
2. 运行阶段
Spring框架监控切入点方法的执行,一旦监控到切入点方法被执行,使用动态代理机制,创建目标对象的代理对象,根据通知类型,在代理对象当前执行方法的对应位置,织入通知功能,完成完整的代码逻辑执行。
3. 需求说明
在业务层方法执行的前后,自动加入日志的输出
4. 代码
1)pom.xml(添加依赖)
<!--aspectj 开源的面向切面编程的组织。spring使用aspectj作为aop的实现 -->
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
如果maven无法下载org.aspectj.aspectjweave,项目出现红色感叹号
下载aspectjweave jar
到本地,再把该jar包安装到本地maven仓库,cmd命令 mvn install:install-file -Dfile=C:\Users\F7689048\Desktop\jar\aspectjweaver-1.8.7.jar -DgroupId=org.aspectj -DartifactId=aspectjweaver -Dversion=1.8.7 -Dpackaging=jar
2)applicationContext.xml
<!--3.Aop配置-->
<aop:config>
<!--切面类的配置,通过ref引用切面类(日志工具类)-->
<aop:aspect ref="logger">
<!--配置切入点表达式:对符合切入点表达式规则的类自动生成代理对象-->
<aop:pointcut id="pt" expression="execution(* com.zz.service.impl.UserServiceImpl.*())"/>
<!--【前置通知】再执行目标对象方法之前执行切面类的insertLog方法-->
<aop:before method="insertLog" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
3)测试
在执行目标对象方法之前执行记录日志的操作(因为在applicationContext中配置了前置通知)
四、切入点表达式
1. 官方文档
2. 各种写法
<?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">
<!--1.创建service-->
<bean id="userService" class="com.zz.service.impl.UserServiceImpl"/>
<!--2.创建日志切面类-->
<bean id="logger" class="com.zz.utils.Logger"/>
<!--3.Aop配置-->
<!--
3.1 切入点表达式配置
作用:对符合切入点表达式规则的类自动生成代理对象。
a.语法
execution(
modifiers-pattern? 访问修饰符,可选
ret-type-pattern 方法返回值类型 (必须)
declaring-type-pattern?name-pattern(param-pattern) 包名类名?方法名称(方法参数)
throws-pattern?)
a1. 完整写法
execution(public void com.itheima.service.impl.UserServiceImpl.save())
a2. 省略访问修饰符/返回值是任意
execution(* com.itheima.service.impl.UserServiceImpl.save())
a3. 包名用*
execution(* com.*.*.*.UserServiceImpl.save())
a3. 省略子包
com 包及其所有子包下的UserServiceImpl类会生成代理对象
execution(* com..UserServiceImpl.save())
a4. 对com包或子包下的ServiceImpl结尾的类生成代理对象
execution(* com..*ServiceImpl.save())
a5. 方法用*/参数用*
execution(* *()) 对所有的方法所在的类生成代理对象
execution(* *(*)) 参数用*表示参数任意,但必须有参数
execution(* *(..)) 参数用*表示参数任意,参数可有可无
a6. 【最常用写法】
execution(* com.itheima.service.impl.*.*(..))
a7. 拦截所有的save或者update方法
execution(* save(..)) || execution(* update(..))
execution(* save(..)) or execution(* update(..))
a7. 不拦截update方法
!execution(* update(..))
not execution(* update(..)) 注意not前面要有空格
a8. 对IOC容器中所有的Service结尾的类生成代理对象
bean(*Service)
-->
<aop:config>
<!--切面类的配置,通过ref引用切面类(日志工具类)-->
<aop:aspect ref="logger">
<!--配置切入点表达式:对符合切入点表达式规则的类自动生成代理对象-->
<aop:pointcut id="pt" expression="bean(*Service)"/>
<!--【前置通知】再执行目标对象方法之前执行切面类的insertLog方法-->
<aop:before method="insertLog" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
</beans>
五、常用标签及通知类型
1. applicationContext.xml常用标签
标签 | 作用 |
---|---|
<aop:config> | 声明aop配置 |
<aop:aspect> | 配置切面 |
<aop:pointcut> | 配置切入点表达式 |
<aop:before> | 配置前置通知 |
<aop:after-returning> | 配置后置通知 |
<aop:after-throwing> | 配置异常通知 |
<aop:after> | 配置最终通知 |
<aop:around> | 配置环绕通知 |
2. 通知类型
- 前置通知:在目标方法执行前执行
- 后置通知:在目标方法正常返回后执行,它和异常通知只能执行一个
- 异常通知:在目标方法发生异常后执行,它和后置通知只能执行一个
- 最终通知:无论目标方法正常返回,还是发生异常都会执行
- 环绕通知:综合了前面四类通知,可以手动控制通知的执行时间点和顺序
try{
[前置通知]
执行目标对象方法
[后置通知]
}catch(Exception e){
[异常通知]
}finally{
[最终通知]
}
3. 环绕通知
环绕通知,是Spring框架提供的一种可以手动控制通知执行时间点和顺序的特殊通知类型,它使用起来更加灵活。
applicationContext.xml
<!--3.Aop配置 -->
<aop:config>
<!--切面类的配置,通过ref引用切面类(日志工具类) -->
<aop:aspect ref="logger">
<!--配置切入点表达式:对符合切入点表达式规则的类自动生成代理对象 -->
<aop:pointcut id="pt" expression="bean(*Service)" />
<!--配置环绕通知-->
<aop:around method="around" pointcut-ref="pt" />
</aop:aspect>
</aop:config>
logger.java
/**
* 环绕通知,环绕目标方法前后执行 推荐使用环绕通知,更好的地方:
* 1.可以修改方法参数
* 2.可以修改方法返回结果
* 3.获取当前执行的方法、类的信息
*/
public Object around(ProceedingJoinPoint pjp) {
// 获取方法参数
Object[] args = pjp.getArgs();
System.out.println("获取目标对象:" + pjp.getTarget().getClass().getName());
System.out.println("获取当前执行的方法名称:" + pjp.getSignature().getName());
try {
System.out.println("【环绕前】");
// 执行目标对象方法
Object retV = pjp.proceed(args);
System.out.println("【环绕后】");
return retV;
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("【环绕异常】");
return null;
} finally {
System.out.println("【环绕最终】");
}
}
六、使用注解实现AOP编程
1.logger.java
@Component // 创建切面类对象
@Aspect // 指定当前类为切面类
public class Logger {
// 统一定义切入点表达式
@Pointcut("execution(* com.zz..*ServiceImpl.*(..))")
public void pt(){}
/**
* 环绕通知,环绕目标方法前后执行
* 推荐使用环绕通知,更好的地方:
* 1.可以修改方法参数
* 2.可以修改方法返回结果
* 3.获取当前执行的方法、类的信息
*/
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
// 获取方法参数
Object[] args = pjp.getArgs();
System.out.println("获取目标对象:" + pjp.getTarget().getClass().getName());
System.out.println("获取当前执行的方法名称:" + pjp.getSignature().getName());
try {
System.out.println("【环绕前】");
// 执行目标对象方法
Object retV = pjp.proceed(args);
System.out.println("【环绕后】");
return retV ;
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("【环绕异常】");
return null;
} finally {
System.out.println("【环绕最终】");
}
}
2.applicationContext.xml
<!--开启注解扫描-->
<context:component-scan base-package="com.zz"/>
<!--Aop自动代理配置-->
<aop:aspectj-autoproxy/>