jsoncat: 仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架

国庆节的时候,我就已经把 jsoncat 的 IoC 功能给写了,具体可以看这篇文章《手写“SpringBoot”近况:IoC模块已经完成》 。

今天这篇文章就来简单分享一下自己写 IoC 的思路与具体的代码实现。

IoC (Inverse of Control:控制反转)AOP(Aspect-Oriented Programming:面向切面编程) 可以说是 Spring 框架提供的最核心的两个功能。但凡是了解过 Spring 的小伙伴,那肯定对这个两个概念非常非常了解。不了解的小伙伴,可以查看《面试被问了几百遍的 IoC 和 AOP ,还在傻傻搞不清楚?》这篇通俗易懂的文章。

考虑到这篇文章要手写 Spring 框架的 IoC 功能,所以,我这里还是简单介绍一下 IoC 。如果你不太清楚 IoC 这个概念,一定要搞懂之后再看后面具体的代码实现环节。

IoC 介绍

IoC(Inverse of Control:控制反转)是一种设计思想,也就是 将原本在程序中手动创建对象的控制权交由Spring框架来管理。 IoC 在其他语言中也有应用,并非 Spring 特有。

IoC 容器

IoC 容器是用来实现 IoC 的载体,被管理的对象就被存放在IoC容器中。IoC 容器在 Spring 中实际上就是个Map(key,value),Map 中存放了各种被管理的对象。

IoC 解决了什么问题

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

IoC 和 DI 别再傻傻分不清楚

IoC(Inverse of Control:控制反转)是一种设计思想 或者说是某种模式。这个设计思想就是 将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。 IoC 在其他语言中也有应用,并非 Spring 特有。IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种被管理的对象。

IoC 最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。

并且,老马(Martin Fowler)在一篇文章中提到将 IoC 改名为 DI,原文如下,原文地址:https://martinfowler.com/articles/injection.html

IoC实现思路

注意 :以下思路未涉及解决循环依赖的问题!

开始代码实现之前,我们先简单聊聊实现 IoC 的思路,搞清楚了思路之后,实现起来就非常简单了。

  1. 扫描指定包下的特定注解比如@Component标记的类,并将这些类保存起来。
  2. 遍历所有被特定注解比如@Component标记的类,然后将这些类通过反射实例化并通过一个 Map 保存起来,Map 的 key 为类名,value为类对象。
  3. 再一次遍历所有被特定注解比如@Component标记的类,并获取类中所有的字段,如果类被 @Autowired 注解标记的话,就进行第 4 步。
  4. 通过字段名 key,从bean容器中获取对应的对象 value。
  5. 判断获取到的对象是否为接口。如果是接口的话,需要获取接口对应的实现类,然后再将指定的实现类的实例化对象通过反射赋值给指定对象。如果不是接口的话,就直接将获取到的对象通过反射赋值给指定对象。

IoC 实现核心代码

核心注解

@Autowired :注解对象

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired { }

@Component :声明对象被IoC容器管理


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
String name() default "";
}

@Qualifier: 指定注入的bean(当接口有多个实现类的时候需要使用)

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
String value() default "";
}

工具类

简单封装一个反射工具类。工具类包含3个后面会用到的方法:

  1. scanAnnotatedClass() :扫描指定包下的被指定注解标记的类(使用Reflections这个反射框架一行代码即可解决扫描获取指定注解的类)。
  2. newInstance() : 传入 Class 即可返回 Class 对应的对象。
  3. setField() :为对象的指定字段赋值。
@Slf4j
public class ReflectionUtil {
/**
* scan the classes marked by the specified annotation in the specified package
*
* @param packageName specified package name
* @param annotation specified annotation
* @return the classes marked by the specified annotation in the specified package
*/
public static Set<Class<?>> scanAnnotatedClass(String packageName, Class<? extends Annotation> annotation) {
Reflections reflections = new Reflections(packageName, new TypeAnnotationsScanner());
Set<Class<?>> annotatedClass = reflections.getTypesAnnotatedWith(annotation, true);
log.info("The number of class Annotated with @RestController :[{}]", annotatedClass.size());
return annotatedClass;
} /**
* create object instance through class
*
* @param cls target class
* @return object created by the target class
*/
public static Object newInstance(Class<?> cls) {
Object instance = null;
try {
instance = cls.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
log.error("new instance failed", e);
}
return instance;
} /**
* set the value of a field in the object
*
* @param obj target object
* @param field target field
* @param value the value assigned to the field
*/
public static void setField(Object obj, Field field, Object value) { field.setAccessible(true);
try {
field.set(obj, value);
} catch (IllegalAccessException e) {
log.error("set field failed", e);
e.printStackTrace();
} } }

根据实现思路写代码

注意 :以下代码未涉及解决循环依赖的问题!以下是 IoC 实现的核心代码,完整代码地址:https://github.com/Snailclimb/jsoncat

1.扫描指定包下的特定注解比如@Component标记的类,并将这些类保存起来。

扫描指定注解@RestController@Component并保存起来:

public class ClassFactory {
public static final Map<Class<? extends Annotation>, Set<Class<?>>> CLASSES = new ConcurrentHashMap<>();
//1.扫描指定包下的特定注解比如`@Component`标记的类,并将这些类保存起来
public static void loadClass(String packageName) {
Set<Class<?>> restControllerSets = ReflectionUtil.scanAnnotatedClass(packageName, RestController.class);
Set<Class<?>> componentSets = ReflectionUtil.scanAnnotatedClass(packageName, Component.class);
CLASSES.put(RestController.class, restControllerSets);
CLASSES.put(Component.class, componentSets);
}
}

2.遍历所有被特定注解比如@Component标记的类,然后将这些类通过反射实例化并通过一个 Map 保存起来,Map 的 key 为类名,value为类对象。

public final class BeanFactory {
public static final Map<String, Object> BEANS = new ConcurrentHashMap<>(128); public static void loadBeans() { // 2.遍历所有被特定注解比如 @Component 标记的类,然后将这些类通过反射实例化并通过一个 Map 保存起来,Map 的 key 为类名,value为类对象
ClassFactory.CLASSES.forEach((annotation, classes) -> {
if (annotation == Component.class) {
//将bean实例化, 并放入bean容器中
for (Class<?> aClass : classes) {
Component component = aClass.getAnnotation(Component.class);
String beanName = "".equals(component.name()) ? aClass.getName() : component.name();
Object obj = ReflectionUtil.newInstance(aClass);
BEANS.put(beanName, obj);
}
} if (annotation == RestController.class) {
for (Class<?> aClass : classes) {
Object obj = ReflectionUtil.newInstance(aClass);
BEANS.put(aClass.getName(), obj);
}
}
});
}
}

3.再一次遍历所有被特定注解比如@Component标记的类,并获取类中所有的字段,如果类被 @Autowired 注解标记的话,就进行第 4 步。

public class DependencyInjection {

    public static void dependencyInjection(String packageName) {
Map<String, Object> beans = BeanFactory.BEANS;
if (beans.size() == 0) return;
//3.再一次遍历所有被特定注解比如 @Component 标记的类,并获取类中所有的字段,如果类被 `@Autowired` 注解标记的话,就进行第 4 步。
// 3.1.遍历bean容器中的所有对象
beans.values().forEach(bean -> {
// 3.2.获取对象所属的类声明的所有字段/属性
Field[] beanFields = bean.getClass().getDeclaredFields();
if (beanFields.length == 0) return;
//3.3.遍历对象所属的类声明的所有字段/属性
for (Field beanField : beanFields) {
//3.4.判断字段是否被 @Autowired 注解标记
if (beanField.isAnnotationPresent(Autowired.class)) {
//4.通过字段名 key,从bean容器中获取对应的对象 value。
//4.1.字段对应的类型
Class<?> beanFieldClass = beanField.getType();
//4.2.字段对应的类名
String beanName = beanFieldClass.getName();
if (beanFieldClass.isAnnotationPresent(Component.class)) {
Component component = beanFieldClass.getAnnotation(Component.class);
beanName = "".equals(component.name()) ? beanFieldClass.getName() : component.name();
}
//4.3.从bean容器中获取对应的对象
Object beanFieldInstance = beans.get(beanName);
//5.判断获取到的对象是否为接口。如果是接口的话,需要获取接口对应的实现类,然后再将指定的实现类的实例化对象通过反射赋值给指定对象。如果不是接口的话,就直接将获取到的对象通过反射赋值给指定对象。
if (beanFieldClass.isInterface()) {
//如果是接口,获取接口对应的实现类
Set<Class<?>> subClasses = getSubClass(packageName, beanFieldClass);
//没有实现类的话就抛出异常
if (subClasses.size() == 0) {
throw new InterfaceNotHaveImplementedClassException("interface does not have implemented class exception");
}
//实现类只有一个话,直接获取
if (subClasses.size() == 1) {
Class<?> aClass = subClasses.iterator().next();
beanFieldInstance = ReflectionUtil.newInstance(aClass);
}
//实现类多与一个的话,根据 Qualifier 注解的值获取
if (subClasses.size() > 1) {
Class<?> aClass = subClasses.iterator().next();
Qualifier qualifier = beanField.getDeclaredAnnotation(Qualifier.class);
beanName = qualifier == null ? aClass.getName() : qualifier.value();
beanFieldInstance = beans.get(beanName);
} }
// 如果最后获取到的字段对象为null,就抛出异常
if (beanFieldInstance == null) {
throw new CanNotDetermineTargetBeanException("can not determine target bean");
}
//通过反射设置指定对象中的指定字段的值
ReflectionUtil.setField(bean, beanField, beanFieldInstance);
}
}
}); } /**
* 获取接口对应的实现类
*/
@SuppressWarnings("unchecked")
public static Set<Class<?>> getSubClass(String packageName, Class<?> interfaceClass) {
Reflections reflections = new Reflections(packageName);
return reflections.getSubTypesOf((Class<Object>) interfaceClass);
}
}

我整理了一份优质原创PDF资源免费分享给大家,大部分内容都是我的原创,少部分来自朋友。


下载地址:https://cowtransfer.com/s/fbed14f0c22a4d

我是 Guide 哥,一 Java 后端开发,会一点前端,自由的少年。我们下期再见!微信搜“JavaGuide”回复“面试突击”领取我整理的 4 本原创PDF

手写一个HTTP框架:两个类实现基本的IoC功能的更多相关文章

  1. 剖析手写Vue,你也可以手写一个MVVM框架

    剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...

  2. 看年薪50W的架构师如何手写一个SpringMVC框架

    前言 做 Java Web 开发的你,一定听说过SpringMVC的大名,作为现在运用最广泛的Java框架,它到目前为止依然保持着强大的活力和广泛的用户群. 本文介绍如何用eclipse一步一步搭建S ...

  3. 自己手写一个SpringMVC 框架

    一.了解SpringMVC运行流程及九大组件 1.SpringMVC 的运行流程   · 用户发送请求至前端控制器DispatcherServlet · DispatcherServlet收到请求调用 ...

  4. 手写一个RPC框架

    一.前言 前段时间看到一篇不错的文章<看了这篇你就会手写RPC框架了>,于是便来了兴趣对着实现了一遍,后面觉得还有很多优化的地方便对其进行了改进. 主要改动点如下: 除了Java序列化协议 ...

  5. 【Spring系列】自己手写一个 SpringMVC 框架

    参考文章 一.了解SpringMVC运行流程及九大组件 1.SpringMVC的运行流程 1)用户发送请求至前端控制器DispatcherServlet 2)DispatcherServlet收到请求 ...

  6. 自己手写一个SpringMVC框架

    前端框架很多,但没有一个框架称霸,后端框架现在Spring已经完成大一统.所以学习Spring是Java程序员的必修课. Spring框架对于Java后端程序员来说再熟悉不过了,以前只知道它用的反射实 ...

  7. 从0 开始手写一个 RPC 框架,轻松搞定!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 来源:juejin.im/post/5c4481a4f265da613438aec3 之前在 RPC框架底层到底什么原理得知 ...

  8. 从 0 开始手写一个 Mybatis 框架,三步搞定!

    阅读本文大概需要 3 分钟. MyBatis框架的核心功能其实不难,无非就是动态代理和jdbc的操作,难的是写出来可扩展,高内聚,低耦合的规范的代码. 本文完成的Mybatis功能比较简单,代码还有许 ...

  9. 手写一个SpringMVC框架(转)

    一:梳理SpringMVC的设计思路 本文只实现自己的@Controller.@RequestMapping.@RequestParam注解起作用,其余SpringMVC功能读者可以尝试自己实现. 1 ...

随机推荐

  1. Kubernetes 存活、就绪探针

    在设计关键任务.高可用应用程序时,弹性是要考虑的最重要因素之一. 当应用程序可以快速从故障中恢复时,它便具有弹性. 云原生应用程序通常设计为使用微服务架构,其中每个组件都位于容器中.为了确保Kuber ...

  2. [Java]取得当前代码所在函数的函数名

    要取得当前运行代码的函数名,可以用: Thread.currentThread().getStackTrace()[1].getMethodName(); 但是,这行代码有些过长,嵌入业务代码稍显突兀 ...

  3. 阿里云前端路线 | CSS快速掌握

    1.1什么是CSS 翻译为“层叠样式表”或者“级联样式表”, 简称样式表. 1.2CSS的主要作用 它主要是用来给HTML网页来设置外观或者样式 外观或者样式:HTML网页中的文字的大小.颜色.字体, ...

  4. windows上部署rabbitmq遇到的一些问题及解决方法

    在目前这家公司,刚进公司的时候接手了一个服务,算是个比较完备的服务,其中几台电脑之间通信用到了rabbitmq,一开始没出什么问题,然后后来勒索病毒wanner cry来的时候,系服把所有服务器装了一 ...

  5. 【Azure DevOps系列】使ASP.NET Core应用程序托管到Azure Web App Service

    使用Azure DevOps Project设置ASP.NET项目 我们需要先在Azure面板中创建一个Azure WebApp服务,此处步骤我将省略,然后点击部署中心如下图所示: 此处我选择的是Az ...

  6. selenuim常用api

    1.通过代码跳转到页面的方法:前提:已安装驱动,并加入到path下 安装ie驱动或火狐驱动的情况: 2.当链接跳转按钮在页面最上方时,用window.scrollTo(0,0);显示在页面最上方后,再 ...

  7. mariadb 2

    mariadb第二章-增删改   MariaDB 数据类型 MariaDB数据类型可以分为数字,日期和时间以及字符串值. 使用数据类型的原则:够用就行, 尽量使用范围小的,而不用大的 常用的数据类型 ...

  8. Mock简明文档

    Mock简明文档 Mock.mock() Mock.mock( requestUrl?, requestType?, template|function(options) ) Mock.mock( t ...

  9. 关于orm的个人测试——SqlSugar与FreeSql

    前言 转眼已经过了金九,光阴真的是似箭啊,周六日常加班,忙里抽闲就想鼓捣个啥看看,刚好最近想着有没有必要换个orm,从当时原生到Dapper,又到现在的Sqlsugar,因为经常听到几个不错的orm, ...

  10. java执行器

    Executor 执行已提交的 Runnable 任务对象.此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节.调度等)分离开来的方法.Executor 接口并没有严格地要求执行是 ...