文章目录
- 1. 复现错误
- 2. 分析错误
- 3. 解决问题
- 3.1 解决方法一
- 3.2 解决方法二
- 4. 分析spring中的jdk和cglib的动态代理
- 4.1 动态代理对比
- 4.2 原理区别
- 4.3 性能区别
- 4.4 各自局限
- 4.5 静态代理和动态的本质区别
1. 复现错误
今天在执行quartz
定时任务时,报出如下错误:
org.quartz.SchedulerException: Job threw an unhandled exception.
at org.quartz.core.JobRunShell.run(JobRunShell.java:213)
at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.xxx.CollectionTaskServiceImpl' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:351)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1127)
at com.xxx.SpringApplicationContext.getBean(SpringApplicationContext.java:19)
at com.xxx.quartz.CollectionTaskJob.execute(CollectionTaskJob.java:27)
at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
... 1 common frames omitted
org.quartz.SchedulerException: Job threw an unhandled exception.
2. 分析错误
将org.quartz.SchedulerException: Job threw an unhandled exception.
翻译成中文,即org.quartz.SchedulerException:作业抛出了一个未经处理的异常。
这个未经处理的异常是什么?我们随着错误往下看,发现这个错误: No qualifying bean of type 'com.xxx.CollectionTaskServiceImpl' available
。
我们继续看错误,错误发生在SpringApplicationContext.getBean
的方法中。
结合No qualifying bean of type 'com.xxx.CollectionTaskServiceImpl' available
错误可知,SpringApplicationContext
拿不到CollectionTaskServiceImpl
这个类。
如是SpringApplicationContext
的源码:
@Component
public class SpringApplicationContext implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringApplicationContext.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> requiredType){
return applicationContext.getBean(requiredType);
}
}
SpringApplicationContext
实现了 ApplicationContextAware
接口,并由@Component
注解。
我们再去往下看,错误在CollectionTaskJob
类的execute
方法中,如下代码:
@Slf4j
@DisallowConcurrentExecution
public class CollectionTaskJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
CollectionTaskServiceImpl collectionTaskServiceImpl = SpringApplicationContext.getBean(CollectionTaskServiceImpl.class);
//此处省略逻辑代码
}
}
我们再去看CollectionTaskServiceImpl
类,如下代码所示:
@Service
public class CollectionTaskServiceImpl implements CollectionTaskService {
//此处省略逻辑代码
}
CollectionTaskServiceImpl
实现了CollectionTaskService
接口,并由@Service
注解。
按道理说,CollectionTaskServiceImpl
类注入到spring
容器中,通过SpringApplicationContext
能够拿得到,但结果是拿不到的。
但为什么拿不到呢?我们需要写个测试类,如下代码所示:
@Component
public class Test implements CommandLineRunner, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void run(String... args) throws Exception {
Map<String, CollectionTaskServiceImpl> beansOfType =
applicationContext.getBeansOfType(CollectionTaskServiceImpl.class);
System.out.println();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
测试类Test
实现了CommandLineRunner
和ApplicationContextAware
接口,此时,我们运行代码:
你会清楚的看到,beansOfType
的容器为0
,确实没有拿到。
我们将CollectionTaskServiceImpl
修改为CollectionTaskService
:
@Override
public void run(String... args) throws Exception {
Map<String, CollectionTaskService> beansOfType =
applicationContext.getBeansOfType(CollectionTaskService.class);
System.out.println();
}
重新运行:
此时,拿到了CollectionTaskServiceImpl
的对象,但注意红框处,它采用的是jdk aop
的动态代理。
然后,我修改CollectionTaskServiceImpl
类,不实现CollectionTaskService
接口,如下代码所示:
@Service
public class CollectionTaskServiceImpl {
//此处省略逻辑代码
}
而run
方法依然是CollectionTaskServiceImpl
,如下代码所示:
@Override
public void run(String... args) throws Exception {
Map<String, CollectionTaskServiceImpl> beansOfType =
applicationContext.getBeansOfType(CollectionTaskServiceImpl.class);
System.out.println();
}
重新运行代码:
如此,也能拿到了CollectionTaskServiceImpl
的对象,但注意红框处,它采用的是spring cglib
的动态代理。
分析到这里大体就明白了,可以有如下两种解决方法。
3. 解决问题
3.1 解决方法一
修改CollectionTaskJob
类的execute
方法,在SpringApplicationContext.getBean
方法中传入CollectionTaskService.class
接口,如下代码所示:
@Slf4j
@DisallowConcurrentExecution
public class CollectionTaskJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
CollectionTaskServiceImpl collectionTaskServiceImpl = (CollectionTaskServiceImpl) SpringApplicationContext.getBean(CollectionTaskService.class);
//此处省略逻辑代码
}
}
3.2 解决方法二
修改CollectionTaskServiceImpl
类,不实现CollectionTaskService
即可。
4. 分析spring中的jdk和cglib的动态代理
4.1 动态代理对比
JDK
动态代理是实现了被代理对象所实现的接口,CGLib
是继承了被代理对象。
JDK
和CGLib
都是在运行期生成字节码,JDK
是直接写Class
字节码。CGLib
使用ASM
框架Class
字节码,Cglib
代理实现更复杂,生成代理类的效率比JDK
代理低。
JDK
调用代理方法,是通过反射机制调用,CGLib
是通过FastClass
机制直接调用方法,CGLib
执行效率更高。
4.2 原理区别
java
动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler
来处理。核心是实现InvocationHandler
接口,使用invoke()
方法进行面向切面的处理,调用相应的通知。
而cglib
动态代理是利用asm
开源包,对代理对象类的class
文件加载进来,通过修改其字节码生成子类来处理。
核心是实现MethodInterceptor
接口,使用intercept()
方法进行面向切面的处理,调用相应的通知。
- 如果目标对象实现了接口,默认情况下会采用
JDK
的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用
CGLIB
实现AOP
- 如果目标对象没有实现了接口,必须采用
CGLIB
库,spring
会自动在JDK
动态代理和CGLIB
之间转换
4.3 性能区别
-
CGLib
底层采用ASM
字节码生成框架,使用字节码技术生成代理类,在jdk6
之前比使用Java
反射效率要高。唯一需要注意的是,CGLib
不能对声明为final
的方法进行代理,因为CGLib
原理是动态生成被代理类的子类。 -
在
jdk6、jdk7、jdk8
逐步对JDK
动态代理优化之后,在调用次数较少的情况下,JDK
代理效率高于CGLIB
代理效率,只有当进行大量调用的时候,jdk6
和jdk7
比CGLIB
代理效率低一点,但是到jdk8
的时候,jdk
代理效率高于CGLIB
代理。
4.4 各自局限
-
JDK
的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK
的动态代理。 -
cglib
是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final
修饰的类进行代理。
类型 | 机制 | 回调方式 | 适用场景 | 效率 |
JDK动态代理 | 委托机制,代理类和目标类都实现了同样的接口,InvocationHandler持有目标类,代理类委托InvocationHandler去调用目标类的原始方法 | 反射 | 目标类是接口类 | 效率瓶颈在反射调用稍慢 |
CGLIB动态代理 | 继承机制,代理类继承了目标类并重写了目标方法,通过回调函数MethodInterceptor调用父类方法执行原始逻辑 | 通过FastClass方法索引调用 | 非接口类、非final类,非final方法 | 第一次调用因为要生成多个Class对象,比JDK方式慢。多次调用因为有方法索引比反射快,如果方法过多,switch case过多其效率还需测试 |
4.5 静态代理和动态的本质区别
-
静态代理只能通过手动完成代理操作,如果被代理类增加新的方法,代理类需要同步新增,违背开闭原则。
-
动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则。
-
若动态代理要对目标类的增强逻辑扩展,结合策略模式,只需要新增策略类便可完成,无需修改代理类的代码。