文章目录
一:代理模式
静态代理
通过租房子的案例感受静态代理
房东需要将房子出租,如果都亲力亲为太麻烦了,所以就可以找中介(代理)
代理模式中涉及的角色:
- 标准 (接口) —》租房子规范 ,房东,代理 都要遵循这套规范,提前定义好,保证真实对象和代理有同样的标准
- 目标(真实对象,被代理者)—》房东
- 代理—》中介 (在里面要调用真实对象的方法,因为真正租房的还是房东,同时可以提供辅助额外功能)
- 客户—》租客 ,找中介 不找房东!
关系图:
代码展示
房东(被代理者)
//房东
public class Host implements Rent {
public Object RentHouse(double money) {
System.out.println("房子被出租,租金是"+money+"每月");
return new Object();
}
}
中介(代理者)
//中介
public class Proxy implements Rent{
private Host host;
public Proxy(Host host){
this.host=host;
}
public Object RentHouse(double money) {
//看房子
look();
//把房子给租客
Object house = host.RentHouse(money*0.8);
return house;
}
public void look(){
System.out.println("中介看了房子");
}
租客(客户)
public class Customer {
public static void main(String[] args) {
//找中介
Proxy proxy=new Proxy(new Host());
proxy.RentHouse(10000);
}
}
标准(接口)
public class Customer {
public static void main(String[] args) {
//找中介
Proxy proxy=new Proxy(new Host());
proxy.RentHouse(10000);
}
}
上述代码由中介(代理者)调用方法
静态代理的好处
(1)保护真实对象 (房东)
(2)真实对象只需要专注主要的业务逻辑(收租),额外事情代理完成
缺点
代理需要自己构建
动态代理
1.JDK动态代理:
本质依旧是调用真实对象,但程序员不需要提供代理对象,而是要提供:生成代理对象的模板 ---->代理帮助完成的操作
代码实现:
(1)定义接口Rent
public interface Rent {
public abstract Object rent(double money);
}
(2)定义房东Host
public class Host implements Rent {
public Object rent(double money) {
//房东实现业务逻辑
System.out.println("房东房子租出去了,租金:" + money);
return new Object();
}
}
(3)定义调用处理者Emp(模板)
员工是调用处理者(中介公司的员工),需要实现InvocationHandler接口去创建标准、模板,让JDK参照这套标准去生成一个动态代理对象。
其中invoke方法,以后代理对象去调用租房的方法的时候,就会走入invoke。
public class Emp implements InvocationHandler {
// 创建租户:
private Host host;
public void setHost(Host host) {this.host = host;}
/*
一旦实现InvocationHandler接口,就重写invoke方法
invoke方法有三个参数:
proxy 代理对象
method 真实对象的方法 ---》当前案例中 :房东里面的rent方法
args 指的就是【15】中方法的参数 调用代理对象的时候传入的方法的参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在真实对象的方法外可以补充功能:
System.out.println("看房子");
// 调用真实对象的方法:
// invoke参数传递:第一个参数: 真实对象 第二个参数:args参数
Object o = method.invoke(host, args);
System.out.println("售后服务");
return o;
}
}
第一个invoke是InvocationHandler 接口中的方法,当你使用动态代理时,任何对代理对象的方法调用都会被转发到这个 invoke 方法。它相当于 代理调用的入口点
当在Custmer中调用proxy.rent(5000);时JVM通过反射拿到 Method 对象(即 Rent.class.getMethod(“rent”)),然后传给 InvocationHandler.invoke()。你再用 method.invoke(host, args) 调用真实对象(host)的方法
你再用 method.invoke(host, args) 调用真实对象(host)的方法
第二个invoke是 Method 类的 invoke 方法,用于通过反射调用某个对象的特定方法。这里的 method 是被调用的方法,host 是真实对象,args 是方法参数
(4)定义租客:Customer
public class Customer {
public static void main(String[] args) {
// 【19】准备中介员工:
Emp emp = new Emp();
emp.setHost(new Host());
/*
newProxyInstance方法有三个参数:
ClassLoader loader---》类加载器,给哪个类做代理需要通过反射去找,反射需要用到类加载器
Class<?>[] interfaces---》代理类实现的接口
InvocationHandler---》代理对象真正需要做的事,必须自己指定(emp)
将准备好的emp传入newProxyInstance的第三个参数:
*/
Rent proxy = (Rent)Proxy.newProxyInstance(Customer.class.getClassLoader(),new Class[]{Rent.class},emp);
// 【15】代理租房
proxy.rent(5000); // 这个rent在调用的时候就会去执行invoke了
}
}
整体流程总结:
(1)提供租房的标准和规范:Rent接口
(2)房东要租房必须实现Rent接口。
(3)JDK动态代理生成代理对象的逻辑:
调用Proxy.newProxyInstance来生成代理对象,传入参数:
a. 类加载器,因为底层用反射,反射需要类加载器,给哪个类做代理需要通过反射去找。
b. 生成的代理类应该实现的接口,因为代理也要遵照Rent接口
c. 真正要做的事:通过员工实现具体的模板 -----其实在这里就是指定了代理真正要做的事。
(4)生成代理对象以后,调用对象的租房方法,到哪里去找租房方法?当你调用proxy.rent方法的时候,这时候就要参照模板了
办事的员工的模板不能随便写,反射就找InvocationHandler的实现类里面的invoke方法,invoke方法中就是代理真正要做的事情。方法的参数:
a. proxy代理对象
b. method 真实对象的方法
c. args 调用代理对象的时候传入的方法的参数
2.CGLIB动态代理:
CGLIB代理的基本概念:
1.不需要接口:与JDK代理不同,CGLIB可以直接代理普通类
2.基于继承:代理对象是被代理类的子类
3.增强子:核心类,用来创建代理对象
代理的步骤
public class Customer {
public static void main(String[] args) {
// 租户找代理
// 创建增强子 ,通过这个增强子对象的create方法可以构建代理对象:
Enhancer en = new Enhancer();
// 利用增强子设置代理对象的父类---》将房东类作为父类
en.setSuperclass(Host.class);
// 设置回调 调用Emp中的拦截方法
en.setCallback(new Emp(new Host()));
// 利用增强子创建代理对象,这个代理对象是房东的子类,所以可以用房东接收:
Host proxy = (Host)en.create();
// 有了代理,就可以租房调用方法了:
proxy.rent(5000);//点入rent方法,发现只有真实的房东的方法
}
}
模板
public class Emp implements MethodInterceptor {
// 【6】定义房东:
private Host host;
public Emp(Host host) {
this.host = host;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 【5】调用真实对象的方法:第一个参数需要传入房东 构建房东【6】
Object obj = method.invoke(host, objects);
return obj;
}
}
intercept方法参数
Object o:代理对象本身
Method method:被调用的方法
Object[] objects:方法参数
MethodProxy methodProxy:方法的代理
JDK代理与CGLIB代理的区别
1.JDK动态代理实现接口,Cglib动态代理继承思想
2.JDK动态代理(目标对象存在接口时)执行效率高于Cglib
3.如果目标对象有接口实现,选择JDK代理,如果没有接口实现选择Cglib代理
二: Spring AOP
AOP是面向切面编程
如果现在需要在某个代码的基础上进行功能的扩展,如果直接动手修改代码,这种方式太不友好了,现在我们要使用spring aop来处理
整体在原来纵向程序中横切了一刀形成切面,切面其实本质就是代理对象
AOP基本概念:
连接点:项目中任何一个方法都可以看成一个连接点
切点:就是我们平时说的目标方法,或说对哪个方法做扩展,做增强。比如上图中service层中的方法。
通知:要加入的扩展功能/额外功能
- 前置通知 — 在切点方法前加入的功能 — 需要实现MethodBeforeAdvice接口
- 后置通知 — 在切点方法后加入的功能 — 需要实现AfterReturningAdvice接口
- 异常通知 —在切点方法发生异常后加入的功能 — 需要实现ThrowsAdvice接口
- 环绕通知 = 前置通知 + 后置通知 + 异常通知 — 通知需要实现MethodInterceptor接口
切面:切点 + 通知 = 切面 (即:代理对象)
织入:将通知加入到切点的这个过程即为织入 (即:创建代理对象的过程)
作用:
当我们想对Service层某个方法做增强时可以不改变原先Service层代码,只增加代码并改变Xml文件将公共功能(如日志、事务)抽取成切面,通过动态代理织入目标方法
三:SpringAOP—Schema-based方式
Schema-based:所有的通知都需要实现特定类型的接口
案例代码:
(1)添加依赖:spring-context
<!--spring的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.5</version>
</dependency>
<!--上面spring的依赖中包含aop了,但是还需要额外导入命名空间的依赖,运行时生效的-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
<scope>runtime</scope>
</dependency>
(2)构建Service层
接口
public interface UserService {
public abstract void a();
public abstract void b(int num);
}
实现类
@Service
public class UserServiceImpl implements UserService {
@Override
public void a() {System.out.println("UserServiceImpl.a");}
@Override
public void b(int num) {System.out.println("UserServiceImpl.b");}
}
用@Service注解来构建UserServiceImpl的对象
(3)定义spring配置文件,扫描@Service注解所在的包(需要添加context命名空间)
添加完后的配置文件
<!--加入扫描注解所在的包:多个包用逗号分隔开-->
<context:component-scan base-package="com.msb.service"></context:component-scan>
(4)构建测试类
public class Test {
public static void main(String[] args) {
ApplicationContext ac=new ClassPathXmlApplicationContext("applicationContext.xml");
UserService us = (UserService)ac.getBean("userServiceImpl");
us.a();
us.b(12);
}
}
运行结果
前置通知:实现MethodBeforeAdvice接口
(1)加入切面,对b方法进行扩展
service层不动,测试类不动,不修改源码,只修改配置文件即可,把你要扩展的事加入到通知中即可
Schema-based这种方式是需要实现接口的,实现MethodBeforeAdvice接口,重写before方法
1.创建新的类
public class MyBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, @Nullable Object[] args, @Nullable Object target) throws Throwable {
System.out.println("前置通知---------");
}
}
2.在xml文件中织入切面 : 需要导入aop的命名空间
命名空间
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
3.定义切点
(1)id属性就是切点的名字
(2)expression 切点表达式 作用:定位到切点 切点在哪个类中 类中的哪个方法 返回值 参数
(3)execution(返回值类型 方法的定位(参数))
4.给切点添加前置通知
(1)pointcut-ref 给哪个切点加入通知
(2)advice-ref 给切点加入什么通知
<!--创建前置通知的对象-->
<bean id="before" class="com.msb3.MyBeforeAdvice.MyBeforeAdvice"></bean>
<!--织入切面-->
<aop:config>
<!--定义切点-->
<aop:pointcut id="p1" expression="execution(void com.msb1.service.impl.UserServiceImpl.b(int))"/>
<!--将前置通知加入切点-->
<aop:advisor advice-ref="before" pointcut-ref="p1"></aop:advisor>
</aop:config>
我们再运行:运行结果:
此时我们已经添加了前置通知
思考:
加入切面后,测试类中调用的b方法是哪个?
我们再Test类中加入
System.out.println(us.getClass().getName());
当我们将切面的代码删除运行结果:
当我们恢复切面的代码运行结果:
我们得出结论:原本我们调用b方法的是真实对象,但加入切面后,调用b方法的是JDK的动态代理对象
后置通知:实现AfterReturningAdvice接口
与前置通知方法没区别
异常通知:实现ThrowsAdvice接口
异常通知只有在切入点出现异常时才会被触发。如果方法没有异常,异常通知是不会执行的
MethodInterceptor接口没有方法,但是我们必须严格提供一个下面的方法:
public void afterThrowing(Exception e)
1.定义异常通知类
public class MyThrowAdvice implements ThrowsAdvice {
public void afterThrowing(Exception ex){
System.out.println("-----异常通知-------");
}
}
2.在applicationContext.xml中配置切面
3.在切入点中写个异常
结果: 若有异常,异常通知才会被触发,若没异常异常通知不执行
环绕通知:实现MethodInterceptor接口
环绕通知可以对前置,后置,异常通知一起进行配置,并且都可以写在invoke方法中。
public class MyAroundAdvice implements MethodInterceptor{
/
// invocation参数就是一个方法调用器,为了调用切点方法,通过proceed方法进行调用
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object obj = null;
try{
// 前置通知:
System.out.println("------前置通知------");
// 调用切点方法:
obj = invocation.proceed();
// 后置通知:
System.out.println("------后置通知------");
}catch (Exception ex){
System.out.println("--------异常通知-------出现异常的类型为:" + ex.getClass().getName());
}
return obj;
}
}
其余步骤与上面三个无异
通知方法中的各种参数:
returnValue:切点方法的返回值
method:切点的方法
args:切点方法中的参数
target:切点对象,真实对象
invocation:整个切点的方法、切点对象
四:SpringAOP—AspectJ方式
优点: AspectJ方式可以将不同的通知定义在一个类的不同方法中,不需要考虑接口
前置,后置,异常通知:
定义MyAspectJAdvice类(不用考虑接口的问题)
public class MyAspectJAdvice {
public void before(){
System.out.println("--------前置通知----------");
}
public void after(){
System.out.println("--------后置通知----------");
}
public void mythrow(){
System.out.println("--------异常通知----------");
}
}
但是配置好以后,Spring容器无法区分该类中的方法
解决方法; 在配置文件中的切面配置中,指明哪些方法是前置,哪些是后置,哪些是异常
配置文件(还是要导入context和aop命名空间)
<!--扫描@Service注解所在的包-->
<context:component-scan base-package="com.msb.service1.impl"></context:component-scan>
<!--构建MyAspectAdvicec对象-->
<bean id="aspectj" class="com.msb.AspectJAdvice.MyAspectJAdvice"></bean>
<!--配置切面-->
<aop:config>
<!--加入aop:aspect标签是aspectj的配置方式-->
<aop:aspect ref="aspectj">
<!--配置切点-->
<aop:pointcut id="p" expression="execution(void com.msb.service1.impl.UserServiceImpl.b(int))"/>
<!--配置前置通知-->
<aop:before method="before" pointcut-ref="p"></aop:before>
<!--配置后置通知-->
<!--<aop:after method="after" pointcut-ref="p"></aop:after>-->
<aop:after-returning method="after" pointcut-ref="p"></aop:after-returning>
<!--配置异常通知-->
<aop:after-throwing method="mythrow" pointcut-ref="p"></aop:after-throwing>
</aop:aspect>
</aop:config>
注意:
配置后置通知中:
aop:after的方式: 如果出现异常,那么后置通知也会执行
aop:after-returning的方式:如果出现异常,那么后置通知就不执行了
配置通知时;
aop:before method=“?” 代表将?作为前置通知放到切点前面
<aop:aspect ref=“aspectj”>可以理解为将aspectj这个类与切点连接
通知方法中有参数的情况:
只要有一个通知方法有参数那么这个类中所有的通知方法都要有参数
public class MyAspectJAdvice {
public void before(int n){
System.out.println("------前置通知-------");
}
public void after(int n){
System.out.println("------后置通知-------");
}
public void mythrow(int n,Exception ex){
System.out.println("------异常通知-------,yichang:" + ex);
}
}
配置文件
配置文件的改动如图框出所示
切点配置的其他方式
配置切点,如下切点只能使用一个,否则报错
<!--指定service.impl包下的UserServiceImpl的类的返回值为void和参数类型为int的b方法-->
<aop:pointcut id="p" expression="execution(void com.msb.service.impl.UserServiceImpl.b(int)) and args(n)"/>
<!--指定service.impl包下的UserServiceImpl的类的所有返回值为void的b方法-->
<aop:pointcut id="p1" expression="execution(void com.msb.service.impl.UserServiceImpl.b(..)) and args(n)"/>
<!--指定service.impl包下的UserServiceImpl的类的所有返回值为void的所有方法-->
<aop:pointcut id="p2" expression="execution(void com.msb.service.impl.UserServiceImpl.*(..)) and args(n)"/>
<!--指定service.impl包下的所有类的所有返回值为void的所有方法-->
<aop:pointcut id="p3" expression="execution(void com.msb.service.impl.*.*(..)) and args(n)"/>
<!--指定service.impl包下的所有类的所有方法-->
<aop:pointcut id="p4" expression="execution(* com.msb.service.impl.*.*(..)) and args(n)"/>
环绕通知:
环绕通知的方法必须加入参数来获取到切点方法,参数为ProceedingJoinPoint类型
由于这个类型在aop的命名空间依赖包下,所以依赖scope要把runtime去除
public Object around(ProceedingJoinPoint p,int n) throws Throwable {
System.out.println("-----环绕通知的前置通知----" + n);
// 执行切点方法:
Object o = p.proceed();
return o;
}
配置文件中配置环绕通知与上面的无异
五:Schema-based和Aspectj的区别
Schema-based: 是基于接口实现的。每个通知都需要实现特定的接口类型,才能确定通知的类型。由于类已经实现了接口,所以配置起来相对比较简单。尤其是不需要在配置中指定参数和返回值类型。
AspectJ: 是基于配置实现的。通过不同的配置标签告诉Spring通知的类型。AspectJ方式对于通知类写起来比较简单。但是在配置文件中参数和返回值需要特殊进行配置。
因为Schame-based是运行时增强,AspectJ是编译时增强。所以当切面比较少时,性能没有太多区别。但是当切面比较多时,最好选择AspectJ方式,因为AspectJ编译一次就可以了
六:注解实现Spring AOP(只能用于AspectJ方式)
MyAspectJAdvice通知类
@Component
@Aspect
public class MyAspectJAdvice {
//配置切点(随便创建一个方法)
@Pointcut("execution(void com.msb.service_impl.UserServiceImpl.*(..))")
public void a(){ }
//不同的通知方法,加入对应的注解,并将上面切点对应的方法”a()“传入注解的参数
@Before("a()")
public void before(){
System.out.println("--------前置通知----------");
}
@After("a()")
public void after(){
System.out.println("--------后置通知----------");
}
@AfterThrowing(pointcut = "a()",throwing = "ex")
public void mythrow(Exception ex){
System.out.println("--------异常通知----------");
}
//环绕通知
@Around("a()")
public Object around(ProceedingJoinPoint p) throws Throwable {
System.out.println("--环绕通知的前置通知--------");
Object o=p.proceed();
return o;
}
}
解释:
@Component注解,就是为了构建MyAspectJAdvic对象
@Aspect注解代表我现在要加入 简化AspectJ方式的注解
@Pointcut注解,是配置切点
配置文件
<!--扫描@Service注解所在的包,以便构建service层对象
扫描@Component注解所在的包,以便MyAspectJAdvic对象 -->
<context:component-scan base-package="com.msb.service,com.msb.advice"></context:component-scan>
<!--扫描AOP的注解-->
<aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>
流程总结
- 在通知类上加@Component注解,@Aspect注解
- 随便创建一个方法并加上@Pointcut注解,配置切点
- 在不同的通知方法上,加入对应的注解
- 在配置文件中扫描@Component和@Aspect注解(构建对象)
- 扫描AOP注解