Spring中你可能不知道的事(二)
在上一节中,我介绍了Spring中极为重要的BeanPostProcessor BeanFactoryPostProcessor Import ImportSelector,还介绍了一些其他的零碎知识点,正如我上一节所说的,Spring实在是太庞大了,是众多Java开发大神的结晶,很多功能,很多细节,可能一辈子都不会用到,不会发现,作为普通开发的我们,只能尽力去学习,去挖掘,也许哪天可以用到呢。
让我们进入正题吧。
Full Lite
在上一节中的第一块内容,我们知道了Spring中除了可以注册我们最常用的配置类,还可以注册一个普通的Bean,今天我就来做一个补充说明。
如果你接到一个需求,要求写一个配置类,完成扫描,你会怎么写?
作为经常使用Spring的来说,这是一个入门级别的问题,并且在20秒钟之内就可以完成编码:
@Configuration
@ComponentScan
public class AppConfig {
}
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(ServiceImpl.class).query();
}
}
@Component
public class ServiceImpl{
public void query() {
System.out.println("正在查询中");
}
}
运行:
但是你有没有尝试过把AppConfig类上的@Configuration注解给去除?你在心里肯定会犯嘀咕,这不能去除啊,这个@Configuration注解申明了咱们的AppConfig是一个Spring配置类,去除了@Configuration注解,怎么可能可以呢?但是事实胜于雄辩,当我们把@Configuration注解给删除,再次运行,你会见证到奇迹:
@ComponentScan
public class AppConfig {
}
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(ServiceImpl.class).query();
}
}
一点问题都没有!!!是不是到这里已经颠覆了你对Spring的认知。
其实,在Spring内部,把带上了@Configuration的配置类称之为Full配置类,把没有带上@Configuration,但是带上了@Component @ComponentScan @Import @ImportResource等注解的配置类称之为Lite配置类。
原谅我,实在找不到合适的中文翻译来表述这里的Full和Lite。
也许你会觉得这并没什么用,只是“茴的四种写法”而已。
别急,让我们看下去,将会继续刷新你的三观:
@ComponentScan
public class AppConfig {
}
注意现在的AppConfig类上没有加上@Configuration注解。
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean(AppConfig.class).getClass().getSimpleName());
}
}
我们注册了Lite配置类,并且从Spring容器中取出了Lite配置类,打印出它的类名。
运行:
可以看到从容器取出来的就是AppConfig类,各位看官肯定会想,这不是废话吗,难道从容器取出来会变成了一只老母鸡?
别急嘛,让我们继续。
我们再在AppConfig类加上@Configuration注解,使其变成Full配置类,然后还是一样,注册这个配置类,取出这个配置类,打印类名:
你会惊讶的发现,的确从容器里取出了一个老母鸡,哦,不,是一个奇怪的类,从类名我们可以看到CGLIB这个关键字,CGLIB是动态代理的一种实现方式,也就是说我们的Full配置类被CGLIB代理了。
你是不是从来都没有注意过,竟然会有如此奇怪的设定,但是更让人惊讶的事情还在后头,让我们想想,为什么好端端的类,Spring要用Cglib代理?这又不是AOP。Spring内部肯定做了一些什么!没错,确实做了!!!
下面让我们看看Spring到底做了什么:
public class ServiceImpl {
public ServiceImpl() {
System.out.println("ServiceImpl类的构造方法");
}
}
ServiceImpl类中有一个构造方法,打印了一句话。
public class OtherImpl {
}
再定义一个OtherImpl类,里面什么都没有。
public class AppConfig {
@Bean
public ServiceImpl getServiceImpl() {
return new ServiceImpl();
}
@Bean
public OtherImpl getOtherImpl() {
getServiceImpl();
return new OtherImpl();
}
}
这个AppConfig没有加上@Configuration注解,是一个Lite配置类,里面定义了两个@Bean方法,其中getServiceImpl方法创建并且返回了ServiceImpl类的对象,getOtherImpl方法再次调用了getServiceImpl方法。
然后我们注册这个配置类:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
}
}
运行:
发现打印了两次"ServiceImpl类的构造方法",这也很好理解,因为new了两次ServiceImpl嘛,肯定会执行两次ServiceImpl构造方法呀。
我们在把@Configuration注解给加上,让AppConfig称为一个Full配置类,再次运行:
你会惊讶的发现只打印了一次"ServiceImpl类的构造方法",说明只调用了一次ServiceImpl类的构造方法,其实这也说的通啊,因为Bean默认是Singleton的,所以只会创建一次对象嘛。
但是问题来了,为什么我们明明new了两次ServiceImpl类,但是真正只new了一次?结合上面的内容,很容易知道答案,因为Full配置类被Cglib代理了,它已经不是我们原先定义的AppConfig类了,最终执行的是代理对象。
好了,这个问题就讨论到这里,至于为什么说(如何证明)带上@Configuration注解的配置类称之为Full配置类,不带的称之为Lite配置类,Cglib是怎么代理Full配置类的,代理的规则又是什么,这就涉及到Spring的源码解析了,就不在今天的讨论内容之中了。
ImportBeanDefinitionRegistrar
大家一定使用过Mybatis,甚至使用过Mybatis的扩展,我在使用的时候,觉得太特么的神奇了,只要在配置类上打一个MapperScan注解,指定需要扫描哪些包,然后这些包里面只有接口,根本没有实现类,为什么可以完成数据库的一系列操作,不知道大家有没有和我一样的疑惑,直到我知道了ImportBeanDefinitionRegistrar这个神奇的接口,关于这个接口,我不知道该怎么去描述这个接口的作用,因为这个接口实在是太强大了,实在不是用简单的文字可以描述清楚的。下面我就利用这个接口来完成一个假的MapperScan,从中慢慢体验这个接口的强大,对了,这个接口要和Import注解配合使用。
首先需要定义一个注解:
@Import(CodeBearMapperScannerRegistrar.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeBearMapperScanner {
String value();
}
其中value就是需要扫描的包名,在这个注解类中又打了一个Import注解,来引ImportBeanDefinitionRegistrar类。
再定义一个注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeBearSql {
String value();
}
这个注解是打在方法上的,接收的是一个sql语句。
然后要定义一个类,去实现ImportBeanDefinitionRegistrar接口,重写提供的方法。
public class CodeBearMapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
try {
AnnotationAttributes annoAttrs =
AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(CodeBearMapperScanner.class.getName()));
String packageValue = annoAttrs.getString("value");
String pathValue = packageValue.replace(".", "/");
File[] files = resourceLoader.getResource(pathValue).getFile().listFiles();
for (File file : files) {
String name = file.getName().replace(".class", "");
Class<?> aClass = Class.forName(packageValue + "." + name);
if (aClass.isInterface()&&!aClass.isAnnotation()) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition();
AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
beanDefinition.setBeanClass(CodeBeanFactoryBean.class);
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(packageValue + "." + name);
registry.registerBeanDefinition(name, beanDefinition);
}
}
} catch (Exception ex) {
}
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
其中ResourceLoaderAware接口的作用不大,我只是利用这个接口,获得了ResourceLoader ,然后通过ResourceLoader去获得包下面的类而已。这方法的核心就是循环文件列表,根据包名和文件名,反射获得Class,接着判断Class是不是接口,如果是接口的话,动态注册Bean。如何动态去注册Bean呢?我在这里利用的是BeanDefinitionBuilder,通过BeanDefinitionBuilder获得一个BeanDefinition,此时BeanDefinition是一个很纯净的BeanDefinition,经过一些处理,再把最终的BeanDefinition注册到Spring容器。
关键就在于处理的这两行代码了,这里可能还看不懂,我们继续看下去。
我们需要再定义一个类,去实现FactoryBean,InvocationHandler两个接口:
public class CodeBeanFactoryBean implements FactoryBean, InvocationHandler {
private Class clazz;
public CodeBeanFactoryBean(Class clazz) {
this.clazz = clazz;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
CodeBearSql annotation = method.getAnnotation(CodeBearSql.class);
String sql= annotation.value();
System.out.println(sql);
return sql;
}
@Override
public Object getObject() throws Exception {
Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, this);
return o;
}
@Override
public Class<?> getObjectType() {
return clazz;
}
}
关于FactoryBean接口,在上一节中有介绍,这里就不再阐述了。
这个类有一个构造方法,接收的是一个Class,这里接收的就是用来进行数据库操作的接口。getObject方法中,就利用传进来的接口和动态代理来创建一个代理对象,此时这个代理对象就是FactoryBean生产的一个Bean了,只要对JDK动态代理有一定了解的人都知道,返回出来的代理对象实现了我们用来进行数据库操作的接口。
我们需要把这个Bean交给Spring去管理,所以就有了CodeBearMapperScannerRegistrar中的这行代码:
beanDefinition.setBeanClass(CodeBeanFactoryBean.class);
因为创建CodeBeanFactoryBean对象需要一个Class参数。所以就有了CodeBearMapperScannerRegistrar中的这行代码:
//packageValue + "." +name 就是接口的全名称
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(packageValue + "." + name);
invoke方法比较简单,就是获得CodeBearSql注解上的sql语句,然后打印一下,当然这里只是模拟下,所以并没有去查询数据库。
下面让我们测试一下吧:
public interface UserRepo {
@CodeBearSql(value = "select * from user")
void get();
}
@Configuration
@CodeBearMapperScanner("com.codebear")
@ComponentScan
public class AppConfig {
}
@Service
public class Test {
@Autowired
UserRepo userRepo;
public void get(){
userRepo.get();
}
}
运行结果:
可以看到我们的功能已经实现了。其实Mybatis的MapperScan注解也是利用了ImportBeanDefinitionRegistrar接口去实现的。
可以看到第二块内容,其实已经比较复杂了,不光光有ImportBeanDefinitionRegistrar,还整合FactoryBean,还融入了动态代理。如果我们不知道FactoryBean,可能这个需求就很难实现了。所以每一块知识点都很重要。
这一节的内容到这里就结束了。
Spring中你可能不知道的事(二)的更多相关文章
- Spring中你可能不知道的事(一)
Spring作为Java的王牌开源项目,相信大家都用过,但是可能大家仅仅用到了Spring最常用的功能,Spring实在是庞大了,很多功能可能一辈子都不会用到,今天我就罗列下Spring中你可能不知道 ...
- 关于Spring Boot你不得不知道的事--Spring Boot的基本操作
1 Pom文件 1.1 spring-boot-starter-parent 表示当前pom文件从spring-boot-starter-parent继承下来,在spring-boot-starter ...
- 关于Spring Boot你不得不知道的事
1 Spring Boot官网[2.1.5 CURRENT GA] 1.1 Pivotal Wiki Pivotal Software, Inc. is a software and services ...
- Java你可能不知道的事(3)HashMap
概述 HashMap对于做Java的小伙伴来说太熟悉了.估计你们每天都在使用它.它为什么叫做HashMap?它的内部是怎么实现的呢?为什么我们使用的时候很多情况都是用String作为它的key呢?带着 ...
- java你可能不知道的事(2)--堆和栈
在java语言的学习和使用当中你可能已经了解或者知道堆和栈,但是你可能没有完全的理解它们.今天我们就一起来学习堆.栈的特点以及它们的区别.认识了这个之后,你可能对java有更深的理解. Java堆内存 ...
- ES6 你可能不知道的事 – 基础篇
序 ES6,或许应该叫 ES2015(2015 年 6 月正式发布),对于大多数前端同学都不陌生. 首先这篇文章不是工具书,不会去过多谈概念,而是想聊聊关于每个特性 你可能不知道的事,希望能为各位同学 ...
- overflow:hidden 你所不知道的事
overflow:hidden 你所不知道的事 overflow:hidden这个CSS样式是大家常用到的CSS样式,但是大多数人对这个样式的理解仅仅局限于隐藏溢出,而对于清除浮动这个含义不是很了解. ...
- java你可能不知道的事(2)--堆和栈<转>
在java语言的学习和使用当中你可能已经了解或者知道堆和栈,但是你可能没有完全的理解它们.今天我们就一起来学习堆.栈的特点以及它们的区别.认识了这个之后,你可能对java有更深的理解. Java堆内存 ...
- JavaScript中你可能不知道的九件事
今天凑巧去W3School扫了一遍JavaScript教程,发现从中看到了不少自己曾经没有注意过的细节. 我这些细节列在这里.分享给可能相同不知道的朋友: 1.使用 document.write() ...
随机推荐
- Yii2 console执行定时脚本
为什么要做crontab脚本 我们的项目使用YII2开发,并不是很大的一个电商平台,pv.IP访问量并不是很高,但客户的数据是日积月累已经产生100万条数据了,之前更新订单等数据使用定时脚本直接访问内 ...
- python函数默认参数陷阱
对于学习python的人都有这样的困惑 def foo(a=[]): a.append(5) return a Python新手希望这个函数总是返回一个只包含一个元素的列表:[5].结果却非常不同,而 ...
- hibernate-release-5.2.9.Final
链接:https://pan.baidu.com/s/1UaoaaXb5C9gJp3Yu7sjUVQ 提取码:orgx
- vs2013下配置x64版c++
最近在ddctf的比赛遇到了x64版的逆向,一大堆寄存器调试的头昏,然后比赛结束后在自己电脑上配置下x64版的c++环境记录下: 首先我们需要新建项目不再废话,然后选择:debug->配置管理器 ...
- WinForm 中 comboBox控件之数据绑定
一.IList 现在我们直接创建一个List集合,然后绑定 1 IList<string> list = new List<string>(); 2 list.Add(&quo ...
- 源自于NEO的KeyValue 数据库面世啦
虽然想把标题取得大一点,但终究不是什么太大不了的工作,还是安分守己的开始介绍吧. 项目组成 这个项目叫做LightDB 由三个部分构成 Lightdb.lib 是对rocksdb做了一层封装, ...
- MongoDB 用Robomong可视化工具操作的 一些简单语句
一.数据更新 db.getCollection('表名').update({ "字段":{$in:["值"]} }, //更新条件 {$set:{ " ...
- Vue中scoped css和css module比较
scoped css 官方文档 scoped css可以直接在能跑起来的vue项目中使用. 使用方法: <style scoped> h1 { color: #f00; } </st ...
- ORACLE启动报错ORA-03113: end-of-file on communication channel
使用过程中发现oracle运行很慢(其实应该先关注空间问题),就准备关机重启一下,关不掉就强制关闭,然后启动就报错了. 1.SQL> startup ORACLE instance starte ...
- css实用属性
background-size: 100% 100%; 背景通过拉伸实现填充 自适应 overflow: hidden; ...