本文已整理致个人github地址,欢迎你们 star 支持一下html
这是一个困扰我司由来已久的难题,Dubbo 了解过吧,对外提供的服务可能有多个方法,通常咱们为了避免给调用方埋坑,会在每一个方法里把全部异常都 catch 住,只返回一个 result,调用方会根据这个 result 里的 success 判断这次调用是否成功,举个例子java
public class ServiceResultTO<T> extends Serializable {
private static final long serialVersionUID = xxx;
private Boolean success;
private String message;
private T data;
}
public interface TestService {
ServiceResultTO<Boolean> test();
}
public class TestServiceImpl implements TestService {
@Override
public ServiceResultTO<Boolean> test() {
try {
// 此处写服务里的执行逻辑
return ServiceResultTO.buildSuccess(Boolean.TRUE);
} catch(Exception e) {
return ServiceResultTO.buildFailed(Boolean.FALSE, "执行失败");
}
}
}好比如今以上这样的 dubbo 服务(TestService),它有一个 test 方法,为了执行正常逻辑时出现异常,咱们在此方法执行逻辑外包了一层「try... catch...」若是只有一个 test 方法,这样作固然没问题,但问题是在工程里咱们通常要要提供几十上百个 service,每一个 service 有几十个像 test 这样的方法,若是每一个方法都要在执行的时候包一层 「try ...catch...」,虽然可行,但代码会比较丑陋,可读性也比较差,你能想一想办法改进一下吗?git
既然是用切面解决的,我先解释下什么是切面。咱们知道,面向对象将程序抽象成多个层次的对象,每一个对象负责不一样的模块,这样的话各个对象分工明确,各司其职,也不互相藕合,确实有力地促进了工程开发与分工协做,可是新的问题来了,不一样的模块(对象)间有时会出现公共的行为,这种公共的行为很难经过继承的方式来实现,若是用工具类的话也不利于维护,代码也显得异常繁琐。 切面(AOP)的引入就是为了解决这类问题而生的,它要达到的效果是保证开发者在不修改源代码的前提下,为系统中不一样的业务组件添加某些通用功能。程序员
举个例子来讲说github
好比上面这个例子,三个 service 对象执行过程当中都存在安全,事务,缓存,性能等相同行为,这些相同的行为显然应该在同一个地方管理,有人说我能够写一个统一的工具类,在这些对象的方法前/后都嵌入此工具类,那问题来了,这些行为都属于业务无关的,使用工具类嵌入的方式致使与业务代码紧藕合,很不合工程规范,代码可维护性极差!切面就是为了解决此类问题应运而生的,能作到相同功能的统一管理,对业务代码无侵入web
以性能为例,这些对象负责的模块存在哪些类似的功能呢面试
好比说吧,每一个 service 都有不一样的方法,我想统计每一个方法的执行时间,若是不用切面你须要在每一个方法的首尾计算下时间,而后相减正则表达式
![]()
若是我要统计每个 service 中每一个方法的执行时间可想而知不用切面的话就得在每一个方法的首尾都加上相似上述的逻辑,显然这样的代码可维护性是很是差的,这还只是统计时间,若是此方法又要加上事务,风控等,是否是也得在方法首尾加上事务开始,回滚等代码,可想而知业务代码与非业务代码严重藕合,这样的实现方式对工程是一种灾难,是不能接受的!shell
那若是用切面该怎么作呢express
在说解决方案前,首先咱们要看下与切面相关的几个定义
JoinPoint: 程序在执行流程中通过的一个个时间点,这个时间点能够是方法调用时,或者是执行方法中异常抛出时,也能够是属性被修改时等时机,在这些时间点上你的切面代码是能够(注意是能够但未必)被注入的
Pointcut: JoinPoints 只是切面代码**能够被织入(加强)**的地方,但我并不想对全部的 JoinPoint 进行织入,这就须要某些条件来筛选出那些须要被织入的 JoinPoint,Pointcut 就是经过一组规则(使用 AspectJ pointcut expression language 来描述) 来定位到匹配的 Joinpoint
Advice: 代码织入(也叫加强),Pointcut 经过其规则指定了哪些 JoinPoint 能够被织入,而 Advice 则指定了这些 Joinpoint 被织入(或者加强)的具体时机与逻辑,是切面代码真正被执行的地方,主要有五个织入时机
Before Advice: 在 JoinPoints 执行前织入 After Advice: 在 JoinPoints 执行后织入(不论是否抛出异常都会织入) After returning advice: 在 JoinPoints 执行正常退出后织入(抛出异常则不会被织入) After throwing advice: 方法执行过程当中抛出异常后织入 Around Advice: 这是全部 Advice 中最强大的,它在 JoinPoints 先后均可织入切面代码,也能够选择是否执行原有正常的逻辑,若是不执行原有流程,它甚至能够用本身的返回值代替原有的返回值,甚至抛出异常。 在这些 advice 里咱们就能够写入切面代码了 综上所述,切面(Aspect)咱们能够认为就是 pointcut 和 advice,pointcut 指定了哪些 joinpoint 能够被织入,而 advice 则指定了在这些 joinpoint 上的代码织入时机与逻辑。
列了一大堆概念真让人生气,请用你奶奶都能听得懂的语言来解释一下这些概念!
把技术解释得让非技术的人也听懂才叫本事,这才说明你真的懂了。
这也难不倒我,好比在餐馆里点菜,菜单有 10 个菜,这 10 个菜就是 JoinPoint,但我只点了带有萝卜名字的菜,那么带有萝卜名字这个条件就是针对 JoinPoint(10 个菜)的筛选条件,即 pointcut,最终只有胡萝卜,白萝卜这两个 JoinPoint 知足条件,而后咱们就能够在吃胡萝卜前洗手(before advice),或吃胡萝卜后买单(after advice),也能够统计吃胡萝卜的时间(around advice),这些洗手,买单,统计时间的动做都是与吃萝卜这个业务动做解藕的,都是统一写在 advice 的逻辑里
可否用程序实现一下,talk is cheap, show me your code!
好嘞,让你看下个人实力
public interface TestService {
// 吃萝卜
void eatCarrot();
// 吃蘑菇
void eatMushroom();
// 吃白菜
void eatCabbage();
}
@Component
public class TestServiceImpl implements TestService {
@Override
public void eatCarrot() {
System.out.println("吃萝卜");
}
@Override
public void eatMushroom() {
System.out.println("吃蘑菇");
}
@Override
public void eatCabbage() {
System.out.println("吃白菜");
}
}假设有以上 TestService, 实现了吃萝卜,吃蘑菇,吃白菜三个方法,这三个方法均可以织入切面代码,因此它们都是 JoinPoints,但如今我只想对吃萝卜这个 JoinPoints 先后织入 advice,首先固然要声明 PointCut 表达式,这个表达式代表只想织入吃萝卜这个 JoinPoint,指明了以后再让 advice 应用于此 pointcut 不就完了,好比我想在吃萝卜前洗手,吃萝卜后买单,能够写出以下切面逻辑
@Aspect
@Component
public class TestAdvice {
// 1. 定义 PointCut
@Pointcut("execution(* com.example.demo.api.TestServiceImpl.eatCarrot())")
private void eatCarrot(){}
// 2. 定义应用于 JoinPoint 中全部知足 PointCut 条件的 advice, 这里咱们使用 around advice,在其中织入加强逻辑
@Around("eatCarrot()")
public void handlerRpcResult(ProceedingJoinPoint point) throws Throwable {
// 3. TestServiceImpl.eatCarrot 执行前逻辑
System.out.println("吃萝卜前洗手");
// 原来的 TestServiceImpl.eatCarrot 逻辑,可视状况决定是否执行
point.proceed();
// 4. TestServiceImpl.eatCarrot 执行后逻辑
System.out.println("吃萝后买单");
}
}能够看到经过 AOP 咱们巧妙地在方法执行先后执行插入相关的逻辑,对原有执行逻辑无任何侵入!
小子果真有两把刷子,咱们 HR 眼光不错,还有一个问题,开头我司的那个难题你用切面又是如何解决的呢。
这就要说到 PointCut 的 AspectJ pointcut expression language 声明式表达式,这个表达式支持的表达类型比较全面,能够用正则,注解等来指定知足条件的 joinpoint , 好比类名后加 .*(..) 这样的正则表达式就表明这个类里面的全部方法都会被织入,使用 @annotation 的方式也能够指定对标有这类注解的方法织入代码
恩,能够,继续
首先咱们先定义一个以下注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GlobalErrorCatch {
}而后将全部 service 中方法里的 「try... catch...」移除掉,在方法签名上加上上述咱们定义好的注解
public class TestServiceImpl implements TestService {
@Override
@GlobalErrorCatch
public ServiceResultTO<Boolean> test() {
// 此处写服务里的执行逻辑
boolean result = xxx;
return ServiceResultTO.buildSuccess(result);
}
}而后再指定注解形式的 Pointcuts 及 around advice
@Aspect
@Component
public class TestAdvice {
// 1. 定义全部带有 GlobalErrorCatch 的注解的方法为 Pointcut
@Pointcut("@annotation(com.example.demo.annotation.GlobalErrorCatch)")
private void globalCatch(){}
// 2. 将 around advice 做用于 globalCatch(){} 此 PointCut
@Around("globalCatch()")
public Object handlerGlobalResult(ProceedingJoinPoint point) throws Throwable {
try {
return point.proceed();
} catch (Exception e) {
System.out.println("执行错误" + e);
return ServiceResultTO.buildFailed("系统错误");
}
}
}经过这样的方式,全部标记着 GlobalErrorCatch 注解的方法都会统一在 handlerGlobalResult 方法里执行,咱们就能够在这个方法里统一 catch 住异常,全部 service 方法中又长又臭的 「try...catch...」所有干掉,真香!
按照大佬提供的思路,我首先打印了 TestServiceImp 这个 bean 所属的类
@Component
public class TestServiceImpl implements TestService {
@Override
public void eatCarrot() {
System.out.println("吃萝卜");
}
}
@Aspect
@Component
public class TestAdvice {
// 1. 定义 PointCut
@Pointcut("execution(* com.example.demo.api.TestServiceImpl.eatCarrot())")
private void eatCarrot(){}
// 2. 定义应用于 PointCut 的 advice, 这里咱们使用 around advice
@Around("eatCarrot()")
public void handlerRpcResult(ProceedingJoinPoint point) throws Throwable {
// 省略相关逻辑
}
}
@SpringBootApplication
@EnableAspectJAutoProxy
public class DemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
TestService testService = context.getBean(TestService.class);
System.out.println("testService = " + testService.getClass());
}
}
打印后我果真发现了端倪,这个 bean 的 class 竟然不是 TestServiceImpl!而是com.example.demo.impl.TestServiceImpl
EnhancerBySpringCGLIB$$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 类库依葫芦画瓢,傻瓜也会
public interface Subject {
public void request();
}
public class RealSubject implements Subject {
@Override
public void request() {
// 卖房
System.out.println("卖房");
}
}
public class Proxy implements Subject {
private RealSubject realSubject;
public Proxy(RealSubject subject) {
this.realSubject = subject;
}
@Override
public void request() {
// 执行代理逻辑
System.out.println("卖房前");
// 执行目标对象方法
realSubject.request();
// 执行代理逻辑
System.out.println("卖房后");
}
public static void main(String[] args) {
// 被代理对象
RealSubject subject = new RealSubject();
// 代理
Proxy proxy = new Proxy(subject);
// 代理请求
proxy.request();
}
}
哟哟哟,"傻瓜也会",看把你能的,那你说下静态代理有啥劣势
静态代理主要有两大劣势
代理类只代理一个委托类(其实能够代理多个,但不符合单一职责原则),也就意味着若是要代理多个委托类,就要写多个代理(别忘了静态代理在编译前必须肯定) 第一点还不是致命的,再考虑这样一种场景:若是每一个委托类的每一个方法都要被织入一样的逻辑,好比说我要计算前文提到的每一个委托类每一个方法的耗时,就要在方法开始前,开始后分别织入计算时间的代码,那就算用代理类,它的方法也有无数这种重复的计算时间的代码
回答的不错,那该怎么改进
嘿嘿,这就要提到动态代理了,静态代理的这些劣势主要是是由于在编译前这些代理类是肯定的,若是这些代理类是动态生成的呢,是否是能够省略一大堆代理的代码。
给你 5 分钟你先写一下 JDK 的动态代理并解释其原理
动态代理分为 JDK 提供的动态代理和 Spring AOP 用到的 CGLib 生成的代理,咱们先看下 JDK 提供的动态代理该怎么写
这是代码
// 委托类
public class RealSubject implements Subject {
@Override
public void request() {
// 卖房
System.out.println("卖房");
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyFactory {
private Object target;// 维护一个目标对象
public ProxyFactory(Object target) {
this.target = target;
}
// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("计算开始时间");
// 执行目标对象方法
method.invoke(target, args);
System.out.println("计算结束时间");
return null;
}
});
}
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
System.out.println(realSubject.getClass());
Subject subject = (Subject) new ProxyFactory(realSubject).getProxyInstance();
System.out.println(subject.getClass());
subject.request();
}
}```
打印结果以下:
```shell
原始类:class com.example.demo.proxy.staticproxy.RealSubject
代理类:class com.sun.proxy.$Proxy0
计算开始时间
卖房
计算结束时间咱们注意到代理类的 class 为 com.sun.proxy.$Proxy0,它是如何生成的呢,注意到 Proxy 是在 java.lang.reflect 反射包下的,注意看看 Proxy 的 newProxyInstance 签名
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h);
loader: 代理类的ClassLoader,最终读取动态生成的字节码,并转成 java.lang.Class 类的一个实例(即类),经过此实例的 newInstance() 方法就能够建立出代理的对象 interfaces: 委托类实现的接口,JDK 动态代理要实现全部的委托类的接口 InvocationHandler: 委托对象全部接口方法调用都会转发到 InvocationHandler.invoke(),在 invoke() 方法里咱们能够加入任何须要加强的逻辑 主要是根据委托类的接口等经过反射生成的
这样的实现有啥好处呢
因为动态代理是程序运行后才生成的,哪一个委托类须要被代理到,只要生成动态代理便可,避免了静态代理那样的硬编码,另外全部委托类实现接口的方法都会在 Proxy 的 InvocationHandler.invoke() 中执行,这样若是要统计全部方法执行时间这样相同的逻辑,能够统一在 InvocationHandler 里写, 也就避免了静态代理那样须要在全部的方法中插入一样代码的问题,代码的可维护性极大的提升了。
说得这么厉害,那么 Spring AOP 的实现为啥却不用它呢
JDK 动态代理虽好,但也有弱点,咱们注意到 newProxyInstance 的方法签名
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h);
注意第二个参数 Interfaces 是委托类的接口,是必传的, JDK 动态代理是经过与委托类实现一样的接口,而后在实现的接口方法里进行加强来实现的,这就意味着若是要用 JDK 代理,委托类必须实现接口,这样的实现方式看起来有点蠢,更好的方式是什么呢,直接继承自委托类不就好了,这样委托类的逻辑不须要作任何改动,CGlib 就是这么作的
回答得不错,接下来谈谈 CGLib 动态代理吧
好嘞,开头咱们提到的 AOP 就是用的 CGLib 的形式来生成的,JDK 动态代理使用 Proxy 来建立代理类,加强逻辑写在 InvocationHandler.invoke() 里,CGlib 动态代理也提供了相似的 Enhance 类,加强逻辑写在 MethodInterceptor.intercept() 中,也就是说全部委托类的非 final 方法都会被方法拦截器拦截,在说它的原理以前首先来看看它怎么用的
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("目标类加强前!!!");
//注意这里的方法调用,不是用反射哦!!!
Object object = proxy.invokeSuper(obj, args);
System.out.println("目标类加强后!!!");
return object;
}
}
public class CGlibProxy {
public static void main(String[] args) {
//建立Enhancer对象,相似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer = new Enhancer();
//设置目标类的字节码文件
enhancer.setSuperclass(RealSubject.class);
//设置回调函数
enhancer.setCallback(new MyMethodInterceptor());
//这里的creat方法就是正式建立代理类
RealSubject proxyDog = (RealSubject) enhancer.create();
//调用代理类的eat方法
proxyDog.request();
}
}打印以下
代理类:class com.example.demo.proxy.staticproxy.RealSubject$$EnhancerByCGLIB$$889898c5
目标类加强前!!!
卖房
目标类加强后!!!能够看到主要就是利用 Enhancer 这个类来设置委托类与方法拦截器,这样委托类的全部非 final 方法就能被方法拦截器拦截,从而在拦截器里实现加强
底层实现原理是啥
以前也说了它是经过继承自委托类,重写委托类的非 final 方法(final 方法不能重载),并在方法里调用委托类的方法来实现代码加强的,它的实现大概是这样
public class RealSubject {
@Override
public void request() {
// 卖房
System.out.println("卖房");
}
}
/** 生成的动态代理类(简化版)**/
public class RealSubject$$EnhancerByCGLIB$$889898c5 extends RealSubject {
@Override
public void request() {
System.out.println("加强前");
super.request();
System.out.println("加强后");
}
}能够看到它并不要求委托类实现任何接口,并且 CGLIB 是高效的代码生成包,底层依靠 ASM(开源的 java 字节码编辑类库)操做字节码实现的,性能比 JDK 强,因此 Spring AOP 最终使用了 CGlib 来生成动态代理
CGlib 动态代理使用上有啥限制吗
第一点以前已经已经说了,只能代理委托类中任意的非 final 的方法,另外它是经过继承自委托类来生成代理的,因此若是委托类是 final 的,就没法被代理了(final 类不能被继承)
小伙子,此次确实能够看出你做了很是充分的准备,不过你答的这些网上都能搜到答案,为了防止一些候选人背书本,我这里还有最后一个问题: JDK 动态代理的拦截对象是经过反射的机制来调用被拦截方法的,CGlib 呢,它经过什么机制来提高了方法的调用效率。
嘿嘿,我猜到了你不知道,我告诉你吧,因为反射的效率比较低,因此 CGlib 采用了FastClass 的机制来实现对被拦截方法的调用。FastClass 机制就是对一个类的方法创建索引,经过索引来直接调用相应的方法,建议参考下https://www.cnblogs.com/cruze/p/3865180.html这个连接好好学学
还有一个问题,咱们经过打印类名的方式知道了 cglib 生成了 RealSubject
EnhancerByCGLIB$$889898c5 这样的动态代理,那么有反编译过它的 class 文件来了解 cglib 代理类的生成规则吗
也在参考连接里,既然出来面试,对每一个技术点都要深挖才行,像 Redis, MQ 这些中间件等平时只会用是不行的,对这些技术必定要作到原理级别的了解,鉴于你最后两题没答出来,我认为你造火箭能力还有待提升,先回去等通知吧
AOP 是 Spring 一个很是重要的特性,经过切面编程有效地实现了不一样模块相同行为的统一管理,也与业务逻辑实现了有效解藕,善用 AOP 有时候能起到出奇制胜的效果,举一个例子,咱们业务中有这样的一个需求,须要在不一样模块中一些核心逻辑执行前过一遍风控,风控经过了,这些核心逻辑才能执行,怎么实现呢,你固然能够统一封装一个风控工具类,而后在这些核心逻辑执行前插入风控工具类的代码,但这样的话核心逻辑与非核心逻辑(风控,事务等)就藕合在一块儿了,更好的方式显然应该用 AOP,使用文中所述的注解 + AOP 的方式,将这些非核心逻辑解藕到切面中执行,让代码的可维护性大大提升了。
篇幅所限,文中没有分析 JDK 和 CGlib 的动态代理生成的实现,不过建议你们有余力的话仍是能够看看,尤为是文末的参考连接,生成动态代理主要用到了反射的特性,不过咱们知道反射存在必定的性能问题,为了提高性能,底层用了一些好比缓存字码码,FastClass 之类的技术来提高性能,通读源码以后的,对反射的理解也会大大加深。
巨人的肩膀
最后欢迎你们关注个人公号,加我好友:「geekoftaste」,一块儿交流,共同进步!