跟我动手搭框架一之IOC容器实现
本篇文章面对的是有开发经验的Java developer 因为我们将要实现的Spring的IOC容器,
前些天由于工作中要开发公司的Callback系统,一直在研究Netty及IO模型,对于Netty这种非阻塞异步框架,非常崇拜,于是萌发一个想法,用Netty作为web容器,替换Tomcat研究性能.出于这种初衷,就开始为SmileBoot项目开始慢慢积累开发知识.本篇属于小编SmileBoot中的一个模块,为什么要起名Smile呢?因为小编始终认为,我们要带着好的心态,才能学更多的东西,其实小编也是一个菜鸟,之所以要写下来,就是为了记忆和理解更深.因为如果把自己理解的东西,能清楚的讲给其他人,那么才算是真正的理解.
目录
- 1.原理分析及设计
- 2.实现方案
- 2.1 拿到扫描范围
- 2.2 更具扫描范围加载范围内所有字节码文件
- 2.3 定义自己的上下文对象接口及实现类
- 3.测试可用性
- 4.扩展性
- 5.下篇预告
1.原理分析及设计
Spring的源码,这里不跟着阅读,直接去实现,然后刚兴趣的童鞋,可以自己在看看,原理是一样的.
1.加载项目中所有的Class文件到Set集合
2.遍历Set将标记有IOC的组件的Class,获取到,注册到IOC容器,这个里面的重点是如何将Class里面的组件,注入进来.
@SmileComponent
在这里@SmileComponent注解是用来标记,需要加入到IOC容器的类
@SmileBean
@SmileBean是用来标记方法中返回值作为Bean,是将要被注册到IOC容器的对象
@InsertBean
@InserBean是标记,该字段是一个Bean,需要从IOC容器中获取,然后注入到该对象中
2.实现方案
1.获取所有的Class字节码,在这中间我们有一个困难那就是如果知道,开发者的所有字节码呢?这个时候我们就可以用注解的形式,在启动类上做一个标记,那么我们就能获取到启动类的字节码,从而获取到将要扫描的跟目录.
我们看下Spring是如何实现的吧
-
@SpringBootApplication
-
public class OtoSaasApplication {
-
public static void main(String[] args) {
-
SpringApplication.run(OtoSaasApplication.class, args);
-
}
-
}
在这段代码中,有一个注解@SpringBootApplication ,了解Spring的开发同事,都是知道这个注解其实包括了多个注解的,其中一个就是@ComponentScan
-
@Retention(RetentionPolicy.RUNTIME)
-
@Target({ElementType.TYPE})
-
@Documented
-
@Repeatable(ComponentScans.class)
-
public @interface ComponentScan {
-
@AliasFor("basePackages")
-
String[] value() default {}
-
}
那么我们就可以知道,其实也main方法所包含的注解,拿到根目录的.这个有一个Spring的特性,那就是如果启动类在最外层的,那么默认就是扫描,其子目录中的Class,如果不是在根目录,那么要指定扫描的范围.
2.1拿到扫描范围
那么我们想,如果用户不指定,我们怎么拿到根目录呢?
好,如果有疑惑的话,那么久带着疑惑,看下面这段代码吧!
我们定一个注解@SmileBootApplication
目录就是获取到用户的根目录,这里关于注解不在解释,如果有不了解实现注解的可以看小编SpringBoot实践中的自定义注解
-
@Target({ElementType.TYPE})
-
@Retention(RetentionPolicy.RUNTIME)
-
@Documented
-
public @interface SmileBootApplication {
-
String[] basePackages() default {};
-
}
-
@SmileBootApplication
-
public class SmileApplication {
-
public static void main(String[] args) {
-
SmileApplication.run(SmileApplication.class, args);
-
}
-
}
我们定义一个方法也就是在run方法中,根据class,文件,获取到注解的根目录
-
public static String getBaseRootPackage(Class<?> cls) {
-
SmileBootApplication declaredAnnotation = null;
-
try {
-
declaredAnnotation = cls.getDeclaredAnnotation(SmileBootApplication.class);
-
} catch (Exception e) {
-
throw new IllegalArgumentException("请添加@SmileBootApplication");
-
}
-
/**
-
* 获取注解上的扫描目录
-
* 如果没有指定,就从当前目录获取
-
*/
-
String[] strings = declaredAnnotation.basePackages();
-
String baseRootPackage = "";
-
if (strings.length == 0) {
-
baseRootPackage = cls.getPackage().getName();
-
}
-
return baseRootPackage;
-
}
看到这里,我们已经拿到了项目的根目录,或者说是将要扫描的范围了
2.2 获取指定目录下的所有字节码文件
这个时候我们要知道一个基础的方法,那就是
-
/**
-
* @param className 完整类路径
-
* @param isInitialized 是否初始化 第2个boolean参数表示类是否需要初始化Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化
-
* @param classLoader 类加载器
-
* @return
-
*/
-
Class.forName(className, isInitialized, classLoader);
我们要用到一个工具类ClassUtils,该类中可以将根目录中所有字节码(.java,.jar文件)加载到Set<Class<?>>set中
这个工具也不是小编写的,是参考了很多博客大拿,发现都有用到,但是具体出自哪位,就不晓得了,那么也分享给大家
可以参考
2.3 定义自己的上下文对象接口及实现类
-
/**
-
* @Package: pig.boot.ioc.context
-
* @Description: 上下文
-
* @author: liuxin
-
* @date: 2017/11/17 下午11:52
-
*/
-
public interface ApplicationContext {
-
Object getBean(String var1);
-
<T> T getBean(String name, Class<T> requiredType);
-
<T> T getBean(Class<T> name);
-
boolean containsBean(String var1);
-
void scan(String basePackRoot);
-
}
-
-
public class SmileApplicationContext implements ApplicationContext {
-
/**
-
* 扫描所有的类,并装载
-
*
-
* @param basePackRoot
-
*/
-
@Override
-
public void scan(String basePackRoot) {
-
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
-
Set<Class<?>> classesByPackage = null;
-
try {
-
/**
-
* recursively 是否从根目录,向下查找
-
*/
-
classesByPackage = ClassUtils.getClassesByPackageName(classLoader, basePackRoot, true);
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
/**
-
* 加载到所有的bean
-
*/
-
allBeans.addAll(classesByPackage);
-
/**
-
*扫描所有的标记,加入到容器
-
*/
-
classesByPackage.forEach(this::scanComponent);
-
/**
-
* 将没有注册的bean检查,然后注入
-
*/
-
processEarlyBeans();
-
}
-
}
-
这个类的重点方法就是scan扫描所有的标记,并从set中拿到每个字节码,传给scanComponent 方法去解析注册
这个时候我们可能遇到一种情况,就是BeanA中需要注册BeanB但是可能BeanB此时并没有解析到,那么这个时候,就要考虑,把暂时实例化不了的,放入到delayBeans,等所有能解析的解析之后,在回过头加载,我们来看这个方法,在看之前我们定义这样一个类 BeanDefinition
用处就是讲Bean的class和实例化对象都保存起来
-
/**
-
* @Package: pig.boot.ioc.context
-
* @Description: bean描述
-
* @author: liuxin
-
* @date: 2017/11/17 下午11:53
-
*/
-
public class BeanDefinition {
-
Class<?> clazz;
-
Object instance;
-
public BeanDefinition(Class<?> clazz, Object instance) {
-
this.clazz = clazz;
-
this.instance = instance;
-
}
-
}
-
/**
-
* 扫描所有被标记的组件
-
*/
-
public void scanComponent(Class<?> nextCls) {
-
SmileComponent declaredAnnotation = nextCls.getDeclaredAnnotation(SmileComponent.class);
-
Object beanInstance = null;
-
if (declaredAnnotation != null) {
-
try {
-
beanInstance = nextCls.newInstance();
-
String beanName = declaredAnnotation.vlaue();
-
if (beanName.isEmpty()) {
-
beanName = nextCls.getSimpleName();
-
}
-
/**
-
* 保证bean名称的唯一性
-
*/
-
Long beanId = beanIds.get();
-
//将类名,首字母小写,并检查是否存在,如果存在就后面添加id,这个id是原子操作,保证唯一.
-
beanName= getUniqueBeanNameByClassAndBeanId(nextCls,beanId);
-
/**
-
* 实例化里面的需要注入的字段都获取到
-
* 如果返回true就可以直接添加到IOC容器
-
* lastChance=true 如果注入失败就报错,false不报错,因为第一次,可能所有类没有初始化,所以等待延迟加载方法去,加载
-
*/
-
if (autowireFields(beanInstance, nextCls, false)) {
-
registeredBeans.put(beanName, new BeanDefinition(nextCls, beanInstance));
-
} else {
-
/**
-
* 上面那种情况,可能会出现,当要注入,但是被注入的未加载到IOC容器中的情况,所以对于这种,就添加到earlyBeans中,后期注入
-
*/
-
delayBeans.put(beanName, new BeanDefinition(nextCls, beanInstance));
-
}
-
-
/**
-
* 获取方法上的bean
-
* 因为方法肯定是有返回值,的返回值就是实例化对象,所以可以直接,加入到IOC容器
-
*/
-
createBeansByMethodsOfClass(beanInstance, nextCls);
-
} catch (Exception e) {
-
-
}
-
-
}
-
}
-
3.测试可用性
-
@SmileBootApplication
-
public class SmileApplication {
-
public static void main(String[] args) {
-
SmileApplicationContext run = SmileApplication.run(SmileApplication.class, args);
-
System.out.println(run.getBean(BeanB.class).toString());
-
System.out.println(run.getBean(BeanA.class).beanB().toString());
-
//BeanB{content='hi. iam is beanB'}
-
//BeanB{content='hi. iam is beanB'}
-
-
}
-
}
-
/**
-
* @Package: pig.boot.ioc.context
-
* @Description: 获取参数
-
* @author: liuxin
-
* @date: 2017/11/17 下午11:55
-
*/
-
@SmileComponent
-
public class BeanA {
-
private String content;
-
@InsertBean
-
private BeanB beanb;
-
public BeanA() {
-
}
-
public BeanA(String content) {
-
this.content = content;
-
}
-
@SmileBean
-
public BeanB beanB() {
-
return new BeanB("hi. iam is beanB");
-
}
-
-
}
-
4.可扩展性
-
定义上下文对象接口类,developer,可以定义自己的上下文类
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
ApplicationContextInitialezer 初始化类执行,获取初始化条件和初始化方法,指定合适的上下文实现类
不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责。
- 小编最喜欢的方法是抽象,即具有共同特征的方法,用抽象类去实现,具体的方法有继承类去实现
- 接口隔离,即依赖最小的接口,eg.A接口有五个方法 B此时要用3个,C要用2个,但是他们不得不全部实现.此时我们可以把A接口拆分为2个. 当D5个方法的时候,就继承2个接口,就可以
5.下篇预告
定义@SmileGetMapping,@SmilePostMapping,注解,绑定处理逻辑handler. 放入SmileNettyTaskHandler中,交给Netty处理异步处理
附录
好的代码就想一本书,读的书越多,思路就越广,想法就越多
每个开发人员要把自己当做一个工程师,而不是一个coding 的码农,工程师考虑问题要从顶层设计考虑,而不是为了单纯解决一个问题而code.
文章来源: springlearn.blog.csdn.net,作者:西魏陶渊明,版权归原作者所有,如需转载,请联系作者。
原文链接:springlearn.blog.csdn.net/article/details/102425340
- 点赞
- 收藏
- 关注作者
评论(0)