设计模式之代理模式及AOP实现原理

代理模式是什么?

日常生活中,有许多代理模式的实际应用和事件:

  1. 歌手会唱歌,那么开演唱会和签约合同这些事很难自己一个人去办了,时间精力都有限,所以找个经纪人帮他做这些事,他只负责唱歌就好了。经纪人不仅负责前面的签约合同等各种事宜,歌手唱完歌后,还要处理后面的事。这时经纪人就是代理了。
  2. 某音某网红小姐姐,因为视频拍的好,粉丝特别多。这时,小姐姐想创建个粉丝群,这样就可以跟粉丝们讨论,并拍出更好的视频。群里很多粉丝,小姐姐一个人又要拍视频又要管理群很累,那么,可以找个人当这个小姐姐的代理人,由他来处理粉丝群的消息并整合粉丝们的讨论话题。这样,小姐姐只负责拍视频,其他事让那个代理来做,事情就方便了很多。

讲到这里,大概已经清楚了代理模式是干啥的,代理模式就是创建个代理用来做我们有些不想做或者做不了的事,我们只要做我们自己会做的事就行了。说的官方一点就是:代理模式的核心就是通过代理去控制对对象的访问。

Java中的代理模式解释:

代理模式分为静态代理和动态代理

用例子解释什么是静态代理和动态代理。

静态代理

比如:
周董要开演唱会,但是开演唱会之前还要宣传、布置场地等等,于是他请了一位经纪人帮他做那些事,他只负责唱歌。既然要唱歌,那先定义个歌手的接口,给个唱歌的方法。

public interface Singer {
    //唱歌的方法,
    // 注意:接口中的方法默认是public abstract 的,所以实现了接口必须重写接口方法。
    void sing();
}

定义周董这个类,他是歌手,就要实现这个类啦!这样才能唱歌。

public class JayChou implements Singer {
    @Override
    public void sing() {
        System.out.println("周董唱了《青花瓷》,《七里香》,《晴天》......");
    }
}

现在找个经纪人处理一下其他的事

public class Proxyer implements Singer {
    private JayChou jayChou;

    public Proxyer(JayChou jayChou) {
        this.jayChou = jayChou;
    }

    public void siteLayout(){
        System.out.println("经纪人布置场地...");
    }
    public void clear(){
        System.out.println("经纪人收拾后续事情。。。");
    }
    @Override
    public void sing() {

        siteLayout();//经纪人先布置场地

        jayChou.sing();//然后周董唱歌

        clear();//经纪人收拾后续事情
    }
}


现在可以开始演唱会啦!

public class TestDemo {
    public static void main(String[] args) {
        JayChou jayChou = new JayChou();//周董出场

        Proxyer proxyer = new Proxyer(jayChou);//周董委托的经纪人

        proxyer.sing();//经纪人先布置场地,周董再来唱歌
    }
}

好了,整个演唱会圆满完成!运行结果:
在这里插入图片描述
到这里,静态代理模式差不多就搞懂了。
在这里插入图片描述
静态代理还有几个细节:

  1. 代理类可以自定义自己的方法,比如演唱会结束还可以去定义个 “搞歌曲投票的活动” 的方法。
  2. 如果周董觉得这个经纪人不错,想让他只为自己做事,可以把Proxy经纪人的类的构造方法改成 public Proxyer(){ this.jayChou = new JayChou;} 这也被称为透明代理,周董这个对象对外界是透明的。

由于静态代理必须要实现目标对象的接口,所以会使代理类会很多,维护起来很麻烦,所以有了动态代理
接下来 是 动态代理的隆重登场!!

动态代理

定义:动态代理是使用反射和字节码技术,在运行期创建指定接口类的子类 以及其实例对象技术,通过这个技术可以无侵入性的为代码进行增强。

上面这句话信息量很大,标红的词一个一个解释:

  1. 反射和字节码技术:动态代理使用的反射+字节码技术,访问jvm中对应的类,就可以动态访问类,并对其动态创建代理类
  2. 运行期:运行期,生成字节码,再加载到虚拟机,为什么是运行期不是编译器,因为上面说到动态代理用到了字节码技术,所以运行期会生成字节码,再去动态获取
  3. 接口或类的子类:就是通过接口或类的子类的方式,去判断你要用哪种方式的动态代理。是的,动态代理也有两种方式:JDK动态代理,CGLIB动态代理。请不要晕,下文会尽量讲详细点。。
  4. 无侵入性:首先要知道什么是侵入性,开发人员写代码都应该尽量满足 “高内聚,低耦合” 的编程思想,而对于Struts这种框架,比如action和actionForm必须要继承Action和ActionForm,非要实现或继承特有的接口或类才可以使用功能,这样就改变了Java的结构这就是侵入式的。而Spring,Hibernate这种框架就是非侵入式的,使用这种框架编写业务类时不用继承特定的类,通过配置,完成依赖注入就可以使用,比如JavaBean,就算撤走框架,对我的代码也没影响。但是呢,侵入式代码可以让用户和框架更好的结合,利用框架的功能更充分一些。凡事都有优缺点的!
  5. 请从侵入性这个话题回到咱们的动态代理!!

讲了这么多,头都是晕的,还是没搞懂动态代理是什么鬼。
还是周董开演唱会,他经纪人收了很多代理费,周董不干了,不要你这个经纪人了,找别人!
首先要知道Java是提供 了一个Proxy类可以用它的newInstance的方法就能生成某个对象的代理对象
在这里插入图片描述
看到这个方法有三个参数,类装载器(一般是被代理的类的装载器)指定被代理类的接口实现了Handler接口,生成代理对象后干的事

import java.lang.reflect.Proxy;

public class TestDemo {
    public static void main(String[] args) {
        JayChou jayChou = new JayChou();//周董出场
        //以下是通过Proxy.newProxyInstance创建了个新代理
        Singer newProxy = (Singer) Proxy.newProxyInstance(jayChou.getClass().getClassLoader(),jayChou.getClass().getInterfaces(),(proxy, method, args1) -> {

            //判断方法是否为要唱歌的方法,动态代理的方法都需要通过invoke()去调用
            if(method.getName().equals("sing")){
                method.invoke(jayChou,args);
                System.out.println("新经纪人来做事了");
            }else{
                return method.invoke(jayChou,args);
            }
            return null;
        });
        //新经纪人来做事了
        newProxy.sing();
    }
}

看这代码,简单来说就是Java有个Proxy类的newProxyInstance方法可以更快捷的让我们使用动态代理 去创建代理的手段,其中传三个参数,类装载器接口Handler。然后通过invoke()的方法去调用动态代理的方法。
注意:第三个参数也可以为了省事,写个匿名内部类: new InvocationHandler(){public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{…}} 是不是跟上面代码比起来感觉有点东西?

几个细节记一记:

  1. 代理对象有目标对象一样的方法,因为代理对象实现了目标对象的接口
  2. 用户调用代理对象的方法时,其实就是在调用处理器的(Handler) 的invoke() 方法。上面代码就做到了拦截的作用
    在这里插入图片描述
    到现在为止,我们已经学会了静态代理和动态代理了。

在这里插入图片描述
上文提到动态代理有两种方式,JDK动态代理,CGLIB动态代理
我们上面讲的就是JDK动态代理,是不是感觉充分利用了反射机制?做个JDK动态代理的总结,再去看看什么是CGLIB动态代理吧!

JDK动态代理总结:

JDK动态代理-----运行时会动态创建代理类,底层是反射机制

原理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
优点减少了对业务接口的依赖,降低了耦合度
缺点:JDK实现动态代理需要 实现类通过接口定义业务方法,这里接口是个重点,它必须要支持接口,代理才可以使用,JDK动态代理必须要通过接口才能实现动态代理。

这时,产生一个问题,要是没有接口也想用动态代理怎么办啊?
第二种方式隆重登场——CGLIB动态代理

CGLIB动态代理

原理:通过字节码技术为一个类创建子类,并为这个子类中采用方法拦截的技术去拦截父类所有方法的调用,这个时候织入横切逻辑。( 因为这种采用的是继承,所以不能对final修饰的类去代理 )

知道了原理,怎么使用呢?

使用CGLIB动态代理需要实现MethodInterceptor接口,并且重写intercept的方法,这个方法里对原始(原来)要执行的方法前后做增强处理(增强处理就相当于可以在你代理的方法前后加些自己的别的方法)。该类 的 代理对象可以使用代码中的字节码增强器来获取。
具体代码就不贴了,总之,CGLIB动态代理将继承用到了极致,其中底层实现有些许复杂。给张CGLIB动态代理的调用图,希望能有帮助!
在这里插入图片描述

CGLIB动态代理总结

作用:CGLIB是一个很强大、性能高的代码生成包,在运行期扩展Java类并实现Java接口。并为许多AOP提供拦截方法的作用。
优点:CGLIB动态代理比JDK的创建的动态代理对象性能更高,JDK动态代理是通过反射机制创建代理对象的。
缺点:1. CGLIB创建代理对象时花费的时间比JDK要多的多。所以一般对于那些单例的对象,因为不用频繁去创建对象,所以适合用CGLIB,否则用JDK这种比较好。
2. 因为CGLIB采用的是动态创建子类的方式,所以对final修饰的类是不能代理的。


代理模式与其他设计模式的区别

1. 和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
2. 和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。


以上就是代理模式的静态代理和动态代理的知识啦! 什么??上面讲到AOP,不讲完不许跑!那就再延伸讲个AOP(面向切面编程)

什么是AOP?

AOP(面向切面编程) :指对很多功能都有重复的代码抽取,再在运行的时候往业务方法上动态植入 " 切面类代码"。

几个小知识:
切面: 关注点(重复代码)形成的类,就是切面(类)。
切入点执行目标对象方法,动态植入切面代码。可以通过切入点表达式,指定拦截某些类的某些方法;给指定的类在运行时植入切面类代码。
切入点表达式:指定某些类的某些方法被拦截

AOP一般是指Spring 的AOP模块

AOP的编程思想就是把业务逻辑和横切的问题进行分离,从而达到解耦的目的,使代码的重用性和开发效率提高,因为它把关注点代码与业务代码分离了;

接下来是案例分析,没错,AOP也有几种实现方式:

输出方法执行时间案例

首先创建个类,加个@Conponent的注解,它可以将这个对象加到IOC容器中(IOC一般指spring中的ioc容器,管理着整个spring应用的对象的生命周期,也称为控制反转

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.util.Calendar;

//性能统计

@Component//组件层,放到ioc容器里管理
public class PerformanceStatistics {


    public Object calcMethodTime(ProceedingJoinPoint point){
        Object targetReturnValue = null;
        long start = System.currentTimeMillis();
        try{
            //执行代理目标的方法 这里具体是谁,不知道,运行期间才知道
            targetReturnValue = point.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println(point.getTarget().getClass()+"::"+
                point.getSignature().getName()
                +"执行共花了"+(end-start)+"毫秒");
        return targetReturnValue;
    }

    
    public void before(JoinPoint point){
        String nowDate = getNowDate();
        System.out.println(point.getTarget().getClass().getName()+":"+point.getSignature().getName()+"::"+nowDate);
    }
    //得到现在的具体时间的方法
    public String getNowDate(){
        Calendar calendar = Calendar.getInstance();
        return calendar.get(Calendar.YEAR)+"/"+(calendar.get(calendar.MONTH)+1)+"/"+calendar.get(Calendar.DATE)+" "+calendar.get(Calendar.HOUR)+":"+calendar.get(Calendar.MINUTE)+":"+calendar.get(Calendar.MILLISECOND);
    }
    
    public void after(JoinPoint point){
        String nowDate = getNowDate();
        System.out.println(point.getTarget().getClass().getName()+":"+point.getSignature().getName()+"::"+nowDate);
    }
}

注意:单单写这个类没有效果的,需要去配置
try里面就是我要的方法,下面是执行结果:执行所用时间及执行时刻
在这里插入图片描述
完成这一操作还要在spring中配置,我在applicationContext.xml中添加了许多配置,一些主要的配置贴出来
在这里插入图片描述

 <!--配置注解式事务-->
    <bean id="myTransaction" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--开启注解式事务的支持-->
    <tx:annotation-driven transaction-manager="myTransaction"/>

    <!--开启注解扫描-->
    <context:component-scan base-package="com.sc.service"/>
    <!--spring 生成dao 对象,不需要手动去session.getMapper 配置dao接口的代理对象-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.sc.dao"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
    <!--把那个类使用spring bean管理-->
    <bean class="com.sc.aop.PerformanceStatistics" id="performanceStatistics">
    </bean>
    <!--使用AOP-->
    <aop:config proxy-target-class="true">
        <aop:pointcut id="pointcut"
                      expression="execution(* com.sc.service.impl.*.*(..))"/>
        <aop:aspect ref="performanceStatistics">
            <aop:before method="before" pointcut-ref="pointcut"/>
            <aop:after method="after" pointcut-ref="pointcut"/>
            <aop:around method="calcMethodTime" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

以上有一些API,解释一下:
—@Aspect 指定一个类为切面类
—@Pointcut(“execution(com.sc.xx…*(…))”) 指定切入点表达式,表示连接点的集合
—@Before(”pointcut_()“)前置通知,定义目标方法之前会执行的方法
—@After(”pointcut_()“)后置通知,定义目标方法之后会执行的方法(始终会执行)
—@AfterReturning(”pointcut_()“)返回后通知,执行方法结束前会执行,但异常就不执行
—@AfterThrowing(”pointcut_()“)异常通知,出现异常时执行
—@Around(”pointcut_()“)环绕通知,环绕目标方法执行

上述用到的就是AOP 使用了动态代理模式的过程,咱有两种动态代理模式啊,JDK,CGLIB。那么,AOP用到的是哪种呢?重点来了!!!

Spring AOP 使用何种动态代理?

答案是:Spring AOP 对JDK动态代理和CGLIB动态代理都可以使用
如果你去面试,面试官最喜欢问的就是有没有看过源码啊?
AOP对应源码是说他会先判断目标类是否是接口或者目标类是否为Proxy类型,如果是就使用JDK动态代理。如果使用了CGLIB来进行动态代理或者目标类没有接口,那么就是用CGLIB动态代理去创建代理对象。如果以上判断都为false,那么就使用JDK 的代理方式生成代理对象

AOP总结

  1. Spring AOP 的代理使用逻辑就是:如果目标对象实现了接口,默认会使用JDK的动态代理实现AOP;如果目标对象没有实现接口,就采用CGLIB库,Spring会自动在JDK动态代理和CGLIB动态代理之间转换
  2. AOP实现原理其实就是Java 动态代理,但是JDK动态代理必须实现接口,所以Spring的AOP大多数是用CGLIB这个库实现的,CGLIB使用了ASM这个直接操纵字节码的框架的原理,所以可以不用实现接口也能完成动态代理
  3. AOP是面向切面编程,主要记录日志,控制访问权限,与IOC组成Spring的核心

记一些问题:

  1. 引入Aop相关jar包(依赖)时,jdk最好是1.7以上的,不然会产生很多问题。
  2. 什么是织入? 织入(wearing)就是把代理逻辑加入到目标对象上 的过程
  3. 初始化时织入还是获取对象时织入? 初始化的时候,目标对象已被代理,放到spring容器中
  4. 前面那个xml的关于AOP的配置已经做好了部分优化,使用时会很方便。减少代码量。

到这里,已经讲的差不多了,可能有些部分没有细细的去分析,所以想更深入了解,可以去看看源码和一些大牛的底层分析。
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值