大家好,今天小编来为大家解答cs漫画这个问题,漫画:AOP 面试造火箭事件始末很多人还不知道,现在让我们一起来看看吧!
本文已整理致我的github地址https://github.com/allentofight/easy-cs,欢迎大家star支持一下
这是一个困扰我司由来已久的难题,Dubbo了解过吧,对外提供的服务可能有多个方法,一般我们为了不给调用方埋坑,会在每个方法里把所有异常都catch住,只返回一个result,调用方会根据这个result里的success判断此次调用是否成功,举个例子
publicclassServiceResultTO<T>extendsSerializable{privatestaticfinallongserialVersionUID=xxx;privateBooleansuccess;privateStringmessage;privateTdata;}publicinterfaceTestService{ServiceResultTO<Boolean>test();}publicclassTestServiceImplimplementsTestService{@OverridepublicServiceResultTO<Boolean>test(){try{//此处写服务里的执行逻辑returnServiceResultTO.buildSuccess(Boolean.TRUE);}catch(Exceptione){returnServiceResultTO.buildFailed(Boolean.FALSE,"执行失败");}}}
比如现在以上这样的dubbo服务(TestService),它有一个test方法,为了执行正常逻辑时出现异常,我们在此方法执行逻辑外包了一层「try...catch...」如果只有一个test方法,这样做当然没问题,但问题是在工程里我们一般要要提供几十上百个service,每个service有几十个像test这样的方法,如果每个方法都要在执行的时候包一层「try...catch...」,虽然可行,但代码会比较丑陋,可读性也比较差,你能想想办法改进一下吗?
既然是用切面解决的,我先解释下什么是切面。我们知道,面向对象将程序抽象成多个层次的对象,每个对象负责不同的模块,这样的话各个对象分工明确,各司其职,也不互相藕合,确实有力地促进了工程开发与分工协作,但是新的问题来了,不同的模块(对象)间有时会出现公共的行为,这种公共的行为很难通过继承的方式来实现,如果用工具类的话也不利于维护,代码也显得异常繁琐。切面(AOP)的引入就是为了解决这类问题而生的,它要达到的效果是保证开发者在不修改源代码的前提下,为系统中不同的业务组件添加某些通用功能。
举个例子来说说
比如上面这个例子,三个service对象执行过程中都存在安全,事务,缓存,性能等相同行为,这些相同的行为显然应该在同一个地方管理,有人说我可以写一个统一的工具类,在这些对象的方法前/后都嵌入此工具类,那问题来了,这些行为都属于业务无关的,使用工具类嵌入的方式导致与业务代码紧藕合,很不合工程规范,代码可维护性极差!切面就是为了解决此类问题应运而生的,能做到相同功能的统一管理,对业务代码无侵入
以性能为例,这些对象负责的模块存在哪些相似的功能呢
比如说吧,每个service都有不同的方法,我想统计每个方法的执行时间,如果不用切面你需要在每个方法的首尾计算下时间,然后相减
如果我要统计每一个service中每个方法的执行时间可想而知不用切面的话就得在每个方法的首尾都加上类似上述的逻辑,显然这样的代码可维护性是非常差的,这还只是统计时间,如果此方法又要加上事务,风控等,是不是也得在方法首尾加上事务开始,回滚等代码,可想而知业务代码与非业务代码严重藕合,这样的实现方式对工程是一种灾难,是不能接受的!
那如果用切面该怎么做呢
在说解决方案前,首先我们要看下与切面相关的几个定义JoinPoint:程序在执行流程中经过的一个个时间点,这个时间点可以是方法调用时,或者是执行方法中异常抛出时,也可以是属性被修改时等时机,在这些时间点上你的切面代码是可以(注意是可以但未必)被注入的
Pointcut:JoinPoints只是切面代码可以被织入的地方,但我并不想对所有的JoinPoint进行织入,这就需要某些条件来筛选出那些需要被织入的JoinPoint,Pointcut就是通过一组规则(使用AspectJpointcutexpressionlanguage来描述)来定位到匹配的joinpoint
Advice:代码织入(也叫增强),Pointcut通过其规则指定了哪些joinpoint可以被织入,而Advice则指定了这些joinpoint被织入(或者增强)的具体时机与逻辑,是切面代码真正被执行的地方,主要有五个织入时机
BeforeAdvice:在JoinPoints执行前织入
AfterAdvice:在JoinPoints执行后织入(不管是否抛出异常都会织入)
Afterreturningadvice:在JoinPoints执行正常退出后织入(抛出异常则不会被织入)
Afterthrowingadvice:方法执行过程中抛出异常后织入
AroundAdvice:这是所有Advice中最强大的,它在JoinPoints前后都可织入切面代码,也可以选择是否执行原有正常的逻辑,如果不执行原有流程,它甚至可以用自己的返回值代替原有的返回值,甚至抛出异常。在这些advice里我们就可以写入切面代码了综上所述,切面(Aspect)我们可以认为就是pointcut和advice,pointcut指定了哪些joinpoint可以被织入,而advice则指定了在这些joinpoint上的代码织入时机与逻辑
画外音:织入(weaving),将切面作用于委托类对象以创建advicedobject的过程(即代理,下文会提)
列了一大堆概念真让人生气,请用你奶奶都能听得懂的语言来解释一下这些概念!
把技术解释得让非技术的人也听懂才叫本事,这才说明你真的懂了。
这也难不倒我,比如在餐馆里点菜,菜单有10个菜,这10个菜就是JoinPoint,但我只点了带有萝卜名字的菜,那么带有萝卜名字这个条件就是针对JoinPoint(10个菜)的筛选条件,即pointcut,最终只有胡萝卜,白萝卜这两个JoinPoint满足条件,然后我们就可以在吃胡萝卜前洗手(beforeadvice),或吃胡萝卜后买单(afteradvice),也可以统计吃胡萝卜的时间(aroundadvice),这些洗手,买单,统计时间的动作都是与吃萝卜这个业务动作解藕的,都是统一写在advice的逻辑里
能否用程序实现一下,talkischeap,showmeyourcode!
好嘞,让你看下我的实力
publicinterfaceTestService{//吃萝卜voideatCarrot();//吃蘑菇voideatMushroom();//吃白菜voideatCabbage();}@ComponentpublicclassTestServiceImplimplementsTestService{@OverridepublicvoideatCarrot(){System.out.println("吃萝卜");}@OverridepublicvoideatMushroom(){System.out.println("吃蘑菇");}@OverridepublicvoideatCabbage(){System.out.println("吃白菜");}}
假设有以上TestService,实现了吃萝卜,吃蘑菇,吃白菜三个方法,这三个方法都用切面织入,所以它们都是joinpoints,但现在我只想对吃萝卜这个joinpoints前后织入advice,该怎么办呢,首先当然要声明pointcut表达式,这个表达式表明只想织入吃萝卜这个joinpoint,指明了之后再让advice应用于此pointcut不就完了,比如我想在吃萝卜前洗手,吃萝卜后买单,可以写出如下切面逻辑
@Aspect@ComponentpublicclassTestAdvice{//1.定义PointCut@Pointcut("execution(*com.example.demo.api.TestServiceImpl.eatCarrot())")privatevoideatCarrot(){}//2.定义应用于JoinPoint中所有满足PointCut条件的advice,这里我们使用aroundadvice,在其中织入增强逻辑@Around("eatCarrot()")publicvoidhandlerRpcResult(ProceedingJoinPointpoint)throwsThrowable{System.out.println("吃萝卜前洗手");//原来的TestServiceImpl.eatCarrot逻辑,可视情况决定是否执行point.proceed();System.out.println("吃萝后买单");}}
可以看到通过AOP我们巧妙地在方法执行前后执行插入相关的逻辑,对原有执行逻辑无任何侵入!
小子果然有两把刷子,我们HR眼光不错,还有一个问题,开头我司的那个难题你用切面又是如何解决的呢。
这就要说到PointCut的AspectJpointcutexpressionlanguage声明式表达式,这个表达式支持的类型比较全面,可以用正则,注解等来指定满足条件的joinpoint,比如类名后加.*(..)这样的正则表达式就代表这个类里面的所有方法都会被织入,使用@annotation的方式也可以指定对标有这类注解的方法织入代码
恩,可以,继续
首先我们先定义一个如下注解
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfaceGlobalErrorCatch{}
然后将所有service中方法里的「try...catch...」移除掉,在方法签名上加上上述我们定义好的注解
publicclassTestServiceImplimplementsTestService{@Override@GlobalErrorCatchpublicServiceResultTO<Boolean>test(){//此处写服务里的执行逻辑booleanresult=xxx;returnServiceResultTO.buildSuccess(result);}}
然后再指定注解形式的pointcuts及aroundadvice
@Aspect@ComponentpublicclassTestAdvice{//1.定义所有带有GlobalErrorCatch的注解的方法为Pointcut@Pointcut("@annotation(com.example.demo.annotation.GlobalErrorCatch)")privatevoidglobalCatch(){}//2.将aroundadvice作用于globalCatch(){}此PointCut@Around("globalCatch()")publicObjecthandlerGlobalResult(ProceedingJoinPointpoint)throwsThrowable{try{returnpoint.proceed();}catch(Exceptione){System.out.println("执行错误"+e);returnServiceResultTO.buildFailed("系统错误");}}}
通过这样的方式,所有标记着GlobalErrorCatch注解的方法都会统一在handlerGlobalResult方法里执行,我们就可以在这个方法里统一catch住异常,所有service方法中又长又臭的「try...catch...」全部干掉,真香!
按照大佬提供的思路,我首先打印了TestServiceImp这个bean所属的类
@ComponentpublicclassTestServiceImplimplementsTestService{@OverridepublicvoideatCarrot(){System.out.println("吃萝卜");}}@Aspect@ComponentpublicclassTestAdvice{//1.定义PointCut@Pointcut("execution(*com.example.demo.api.TestServiceImpl.eatCarrot())")privatevoideatCarrot(){}//2.定义应用于PointCut的advice,这里我们使用aroundadvice@Around("eatCarrot()")publicvoidhandlerRpcResult(ProceedingJoinPointpoint)throwsThrowable{//省略相关逻辑}}@SpringBootApplication@EnableAspectJAutoProxypublicclassDemoApplication{publicstaticvoidmain(String[]args){ConfigurableApplicationContextcontext=SpringApplication.run(DemoApplication.class,args);TestServicetestService=context.getBean(TestService.class);System.out.println("testService="+testService.getClass());}}打印后我果然发现了端倪,这个bean的class居然不是TestServiceImpl!而是com.example.demo.impl.TestServiceImplEnhancerBySpringCGLIB$$705c68c7!
果然有长进,继续说,为啥会生成这样一个类
我们注意到类名中有一个EnhancerBySpringCGLIB,注意CGLiB,这个类就是通过它生成的动态代理
打住,先不要说动态代理,先谈谈啥是代理吧
代理在生活中随处可见,比如说我要买房,我一般不会直接和卖家对接,一般会和中介打交道,中介就是代理,卖家就是目标对象,我就是调用者,代理不仅实现了目标对象的行为(帮目标对象卖房),还可以添加上自己的动作(收保证金,签合同等),用UML图来表示就是下面这样Client是直接和Proxy打交道的,Proxy是Client要真正调用的RealSubject的代理,它确实执行了RealSubject的request方法,不过在这个执行前后Proxy也加上了额外的PreRequest(),afterRequest()方法,注意Proxy和RealSubject都实现了Subject这个接口,这样在Client看起来调用谁是没有什么分别的(面向接口编程,对调用方无感,因为实现的接口方法是一样的),Proxy通过其属性持有真正要代理的目标对象(RealSubject)以达到既能调用目标对象的方法也能在方法前后注入其它逻辑的目的
听得我要睡着了,根据这个UML来写下相应的实现类吧
没问题,不过在此之前我要先介绍一下代理的类型,代理主要分为两种类型:静态代理和动态代理,动态代理又有JDK代理和CGLib代理两种,我先解释下静态和动态的含义
好小子,逻辑清晰,继续吧
要理解静态和动态这两个含义,我们首先需要理解一下Java程序的运行机制首先Java源代码经过编译生成字节码,然后再由JVM经过类加载,连接,初始化成Java类型,可以看到字节码是关键,静态和动态的区别就在于字节码生成的时机静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在编译时已经将接口,被代理类(委托类),代理类等确定下来,在程序运行前代理类的.class文件就已经存在了动态代理:在程序运行后通过反射创建生成字节码再由JVM加载而成
好,那你写下静态代理吧
嘿嘿按这张UML类库依葫芦画瓢,傻瓜也会
publicinterfaceSubject{publicvoidrequest();}publicclassRealSubjectimplementsSubject{@Overridepublicvoidrequest(){//卖房System.out.println("卖房");}}publicclassProxyimplementsSubject{privateRealSubjectrealSubject;publicProxy(RealSubjectsubject){this.realSubject=subject;}@Overridepublicvoidrequest(){//执行代理逻辑System.out.println("卖房前");//执行目标对象方法realSubject.request();//执行代理逻辑System.out.println("卖房后");}publicstaticvoidmain(String[]args){//被代理对象RealSubjectsubject=newRealSubject();//代理Proxyproxy=newProxy(subject);//代理请求proxy.request();}}
哟哟哟,"傻瓜也会",看把你能的,那你说下静态代理有啥劣势
静态代理主要有两大劣势
代理类只代理一个委托类(其实可以代理多个,但不符合单一职责原则),也就意味着如果要代理多个委托类,就要写多个代理(别忘了静态代理在编译前必须确定)
第一点还不是致命的,再考虑这样一种场景:如果每个委托类的每个方法都要被织入同样的逻辑,比如说我要计算前文提到的每个委托类每个方法的耗时,就要在方法开始前,开始后分别织入计算时间的代码,那就算用代理类,它的方法也有无数这种重复的计算时间的代码
回答的不错,那该怎么改进
嘿嘿,这就要提到动态代理了,静态代理的这些劣势主要是是因为在编译前这些代理类是确定的,如果这些代理类是动态生成的呢,是不是可以省略一大堆代理的代码。
给你5分钟你先写一下JDK的动态代理并解释其原理
动态代理分为JDK提供的动态代理和SpringAOP用到的CGLib生成的代理,我们先看下JDK提供的动态代理该怎么写
这是代码
//委托类publicclassRealSubjectimplementsSubject{@Overridepublicvoidrequest(){//卖房System.out.println("卖房");}}importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Method;importjava.lang.reflect.Proxy;publicclassProxyFactory{privateObjecttarget;//维护一个目标对象publicProxyFactory(Objecttarget){this.target=target;}//为目标对象生成代理对象publicObjectgetProxyInstance(){returnProxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),newInvocationHandler(){@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{System.out.println("计算开始时间");//执行目标对象方法method.invoke(target,args);System.out.println("计算结束时间");returnnull;}});}publicstaticvoidmain(String[]args){RealSubjectrealSubject=newRealSubject();System.out.println(realSubject.getClass());Subjectsubject=(Subject)newProxyFactory(realSubject).getProxyInstance();System.out.println(subject.getClass());subject.request();}}```打印结果如下:```shell原始类:classcom.example.demo.proxy.staticproxy.RealSubject代理类:classcom.sun.proxy.$Proxy0计算开始时间卖房计算结束时间
我们注意到代理类的class为com.sun.proxy.$Proxy0,它是如何生成的呢,注意到Proxy是在java.lang.reflect反射包下的,注意看看Proxy的newProxyInstance签名
publicstaticObjectnewProxyInstance(ClassLoaderloader,Class<?>[]interfaces,InvocationHandlerh);
loader:代理类的ClassLoader,最终读取动态生成的字节码,并转成java.lang.Class类的一个实例(即类),通过此实例的newInstance()方法就可以创建出代理的对象
interfaces:委托类实现的接口,JDK动态代理要实现所有的委托类的接口
InvocationHandler:委托对象所有接口方法调用都会转发到InvocationHandler.invoke(),在invoke()方法里我们可以加入任何需要增强的逻辑主要是根据委托类的接口等通过反射生成的
这样的实现有啥好处呢
由于动态代理是程序运行后才生成的,哪个委托类需要被代理到,只要生成动态代理即可,避免了静态代理那样的硬编码,另外所有委托类实现接口的方法都会在Proxy的InvocationHandler.invoke()中执行,这样如果要统计所有方法执行时间这样相同的逻辑,可以统一在InvocationHandler里写,也就避免了静态代理那样需要在所有的方法中插入同样代码的问题,代码的可维护性极大的提高了。
说得这么厉害,那么SpringAOP的实现为啥却不用它呢
JDK动态代理虽好,但也有弱点,我们注意到newProxyInstance的方法签名
publicstaticObjectnewProxyInstance(ClassLoaderloader,Class<?>[]interfaces,InvocationHandlerh);
注意第二个参数Interfaces是委托类的接口,是必传的,JDK动态代理是通过与委托类实现同样的接口,然后在实现的接口方法里进行增强来实现的,这就意味着如果要用JDK代理,委托类必须实现接口,这样的实现方式看起来有点蠢,更好的方式是什么呢,直接继承自委托类不就行了,这样委托类的逻辑不需要做任何改动,CGlib就是这么做的
回答得不错,接下来谈谈CGLib动态代理吧
好嘞,开头我们提到的AOP就是用的CGLib的形式来生成的,JDK动态代理使用Proxy来创建代理类,增强逻辑写在InvocationHandler.invoke()里,CGlib动态代理也提供了类似的Enhance类,增强逻辑写在MethodInterceptor.intercept()中,也就是说所有委托类的非final方法都会被方法拦截器拦截,在说它的原理之前首先来看看它怎么用的
publicclassMyMethodInterceptorimplementsMethodInterceptor{@OverridepublicObjectintercept(Objectobj,Methodmethod,Object[]args,MethodProxyproxy)throwsThrowable{System.out.println("目标类增强前!!!");//注意这里的方法调用,不是用反射哦!!!Objectobject=proxy.invokeSuper(obj,args);System.out.println("目标类增强后!!!");returnobject;}}publicclassCGlibProxy{publicstaticvoidmain(String[]args){//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数Enhancerenhancer=newEnhancer();//设置目标类的字节码文件enhancer.setSuperclass(RealSubject.class);//设置回调函数enhancer.setCallback(newMyMethodInterceptor());//这里的creat方法就是正式创建代理类RealSubjectproxyDog=(RealSubject)enhancer.create();//调用代理类的eat方法proxyDog.request();}}
打印如下
代理类:classcom.example.demo.proxy.staticproxy.RealSubject$$EnhancerByCGLIB$$889898c5目标类增强前!!!卖房目标类增强后!!!
可以看到主要就是利用Enhancer这个类来设置委托类与方法拦截器,这样委托类的所有非final方法就能被方法拦截器拦截,从而在拦截器里实现增强
底层实现原理是啥
之前也说了它是通过继承自委托类,重写委托类的非final方法(final方法不能重载),并在方法里调用委托类的方法来实现代码增强的,它的实现大概是这样
publicclassRealSubject{@Overridepublicvoidrequest(){//卖房System.out.println("卖房");}}/**生成的动态代理类(简化版)**/publicclassRealSubject$$EnhancerByCGLIB$$889898c5extendsRealSubject{@Overridepublicvoidrequest(){System.out.println("增强前");super.request();System.out.println("增强后");}}
可以看到它并不要求委托类实现任何接口,而且CGLIB是高效的代码生成包,底层依靠ASM(开源的java字节码编辑类库)操作字节码实现的,性能比JDK强,所以SpringAOP最终使用了CGlib来生成动态代理
CGlib动态代理使用上有啥限制吗
第一点之前已经已经说了,只能代理委托类中任意的非final的方法,另外它是通过继承自委托类来生成代理的,所以如果委托类是final的,就无法被代理了(final类不能被继承)
小伙子,这次确实可以看出你作了非常充分的准备,不过你答的这些网上都能搜到答案,为了防止一些候选人背书本,我这里还有最后一个问题:JDK动态代理的拦截对象是通过反射的机制来调用被拦截方法的,CGlib呢,它通过什么机制来提升了方法的调用效率。
嘿嘿,我猜到了你不知道,我告诉你吧,由于反射的效率比较低,所以CGlib采用了FastClass的机制来实现对被拦截方法的调用。FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法,建议参考下https://www.cnblogs.com/cruze/p/3865180.html这个链接好好学学
还有一个问题,我们通过打印类名的方式知道了cglib生成了RealSubjectEnhancerByCGLIB$$889898c5这样的动态代理,那么有反编译过它的class文件来了解cglib代理类的生成规则吗
也在参考链接里,既然出来面试,对每个技术点都要深挖才行,像Redis,MQ这些中间件等平时只会用是不行的,对这些技术一定要做到原理级别的了解,鉴于你最后两题没答出来,我认为你造火箭能力还有待提高,先回去等通知吧
后记AOP是Spring一个非常重要的特性,通过切面编程有效地实现了不同模块相同行为的统一管理,也与业务逻辑实现了有效解藕,善用AOP有时候能起到出奇制胜的效果,举一个例子,我们业务中有这样的一个需求,需要在不同模块中一些核心逻辑执行前过一遍风控,风控通过了,这些核心逻辑才能执行,怎么实现呢,你当然可以统一封装一个风控工具类,然后在这些核心逻辑执行前插入风控工具类的代码,但这样的话核心逻辑与非核心逻辑(风控,事务等)就藕合在一起了,更好的方式显然应该用AOP,使用文中所述的注解+AOP的方式,将这些非核心逻辑解藕到切面中执行,让代码的可维护性大大提高了。
篇幅所限,文中没有分析JDK和CGlib的动态代理生成的实现,不过建议大家有余力的话还是可以看看,尤其是文末的参考链接,生成动态代理主要用到了反射的特性,不过我们知道反射存在一定的性能问题,为了提升性能,底层用了一些比如缓存字节码,FastClass之类的技术来提升性能,通读源码之后的,对反射的理解也会大大加深。
巨人的肩膀
SpringAOP是怎么运行的?彻底搞定这道面试必考题https://cloud.tencent.com/developer/article/1584491OK,关于cs漫画和漫画:AOP 面试造火箭事件始末的内容到此结束了,希望对大家有所帮助。