在上一篇《JMJava框架设计(一):【主线】IoC容器设计(1)概述》中,大致设计了JMJava框架中的IoC部分,为了落地这些设计,首先需要大致确定IoC容器(包含控制器)最终对外提供的接口,以明确进一步详细设计的目标。
一、主要入口:JMIoc类设计
(一)总体设计
我们将JMIoc类(JM Java IoC)定义为JMJava中提供IoC能力的主要载体。IoC能力在项目中通常是全局能力,因此,JMIoc类的使用我们设计为单例模式。
/**
* IoC实现类
* @author 刘极渺(Miles.Liu)
*
* @date 2024-04-10 18:49
*/
public final class JMIoc {
/**
* 维护单例实例,volatile保证在多线程中的可见性
*/
private volatile static JMIoc INSTANCE = null;
/**
* 获取单例实例时,首先尝试返回已存在的实例,如果没有,则需在同步锁内进行实例创建
* 在同步锁内,为避免重复创建实例,需要首先判断实例是否已经创建
* @return
*/
public static final JMIoc getSingleInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (JMIoc.class) {
if (INSTANCE == null) {
INSTANCE = new JMIoc();
}
}
return INSTANCE;
}
//TODO:设计其它接口
}
如果作为独立使用的功能类,可以将JMIoc设计为只提供静态方法的类,这样在使用时会更方便,但考虑JMJava是一个完整框架,而IoC能力只是其中的一个能力部分,所以可能需要将JMIoc和其它能力组件进行集成,因此将JMIoc设计为单例模式的类。
设想中最终JMJava的主类可能类似:
/**
* JMJava主类
* @author 刘航(Miles.Liu)
*
* @date 2024-04-10 10:48
*/
public final class JM {
/**
* 集成JMIoc
*/
private static JMIoc ioc;
/**
* 初始化
* @throws Exception
*/
public static void initialize() throws Exception {
ioc= JMIoc.getSingleInstance();
}
}
//IoC能力可通过 JM.ioc使用
这样的设计可以最大程度简化框架的使用,使用者只需记住JM这个类,即可使用框架大部分能力。
(二)JMIoc初始化接口设计
如概要设计所述,IoC容器需要首先进行初始化,而初始化前需要指定类扫描范围等配置参数,因此,JMIoc的初始化接口设计可能包含以下两种方案:
方案一: 声明一个IoC相关的配置类,在初始化时传入配置参数:
public final class JMIoc {
public void initialize(JMIocConfig config) {
}
}
//使用代码示例
{
JMIocConfig iocConfig = new JMIocConfig();
//iocConfig.setXXX(...)
JM.ioc.initialize(iocConfig);
}
方案二: 声明一个IoC相关的配置类,分别声明配置接口和无参数的初始化方法:
public final class JMIoc {
private JMIocConfig iocConfig = new JMIocConfig();
public JMIocConfig config() {
return iocConfig;
}
public void initialize() {
//引用this.iocConfig获取配置
}
}
//使用代码示例
{
//JM.ioc.config().setXXX(...)
JM.ioc.initialize();
}
方案一直观看来比较简单,但在实际使用中需要框架使用者多记忆一个JMIocConfig类,同时也不利于IDE中代码补全功能的最大化使用,而方案二提供的使用方式就更加顺畅。因此选择方案二。
对于JMIocConfig,就目前已知的需求而言,至少需要提供配置包扫描范围的接口,初步定义如下:
/**
*
* @author 刘极渺(Miles.Liu)
*
* @date 2024-04-12 09:46
*/
public class JMIocConfig {
private List<String> packageScope = new ArrayList<>();
/**
* 获取包扫描范围列表
* @return
*/
public List<String> getPackageScope() {
return packageScope;
}
/**
* 添加单个需要扫描的包范围,例如"cn.jmtt.java"
* @param pkg
*/
public void addPackageScan(String pkg) {
if (JMStringUtil.isEmpty(pkg)) {
return;
}
this.packageScope.add(pkg);
}
/**
* 批量添加需要扫描的包范围
* @param pkgs
*/
public void addPackageScan(String[] pkgs) {
if (pkgs == null) {
return;
}
for (String pkg : pkgs) {
this.packageScope.add(pkg);
}
}
}
(三)JMIoc获取组件实例接口设计
依据之前的概要设计,IoC有关实例创建/获取的主要接口需求只有两个,分别是以组件类型为参数获取组件实例和以组件名称为参数获取组件实例。
其中,根据组件类型获取组件实例的接口设计方案比较明确,但根据组件名称获取组件实例的接口设计有待考虑,在很多实际使用情况下,对于返回Object类型的方法调用,往往需要对返回结果进行显式类型转换,这点很不方便,因此,考虑增加一个可约束返回值的方法,便于开发者使用。
相关接口定义如下:
public final class JMIoc {
//……
/**
* 以组件名称为参数,获取或创建组件实例
* @param beanName 组件名称,默认为组件类的完整名称
* @return 组件实例
*/
public final Object getBean(String beanName) {
//TODO
return null;
}
/**
* 以组件类型为参数,获取或创建组件实例
* @param <T>
* @param beanClass 组件类型
* @return 组件实例
*/
public <T> T getBean(Class<T> beanClass) {
//TODO
return null;
}
/**
* 以组件名称为参数,获取或创建组件实例, 如果获取结果不是beanClass指定的实例,抛出异常
* @param <T>
* @param beanClass 返回结果的类型
* @param beanName 组件名称,默认为组件类的完整名称
* @return 组件实例
*/
public <T> T getBean(Class<T> beanClass, String beanName) {
//TODO
return null;
}
}
二、类扫描相关接口设计
(一)类扫描结果:JMIocClassSet
类扫描的最终目标是根据指定的包范围,获得程序运行上下文中所有的类,并且提供对这些类的查找和访问。因此,首先应对类扫描结果进行接口设计。
定义JMIocClassSet接口作为类扫描的结果,之所以定义为接口是为了实现接口定义和实现解耦,对照JMIoc的接口设计,JMIocClassSet类至少应提供以下接口:
1. 根据类名称获取组件类
支撑JMIoc中根据组件名称获取组件实例的接口,应对获取组件实例时可能需要查询组件类定义的需求。可以配套提供contains()接口,用来根据名称判断对应的组件类是否存在。
/**
* 用来维护一个Class的集合
* @author 刘极渺(Miles.Liu)
*
* @date 2024-04-16 13:25
*/
public interface JMIocClassSet {
/**
* 根据完整的类名查询类
* 与原生的Class.forName()的区别在于:此方法的查找范围限于类扫描的范围
* @param className
* @return
*/
public Class<?> getByName(String className);
/**
* 判断指定类型是否在类扫描结果范围内
* @param cls
* @return
*/
public boolean contains(Class<?> cls);
}
2. 根据自定义条件查找所有符合的组件类
根据“一定的条件”查找所有符合的组件类定义是基础的需求,但难点在于如何实现可灵活指定的“一定的条件”,此时无法准确预计可能出现的查询方式,因此应当将查询条件和查询逻辑解耦。
考虑到通用查询的行为可以理解为针对一个列表,根据一定的条件逐个匹配,对于匹配成功的结果进行返回,同时对于类索引这种查找对象,因为数量有限,无须考虑增加索引等性能优化操作,因此可以定义一个通用的匹配接口JMFilter,并且通过泛型的方式指定匹配对象的类型:
/**
* 通用匹配接口
* @author 刘极渺(Miles.Liu)
*
* @date 2024-04-10 11:16
*/
public interface JMFilter<T> {
/**
* 判断对象是否匹配成功
* @param o
* @return
*/
public boolean match(T o);
/**
* 返回一个新的匹配器,实现当前匹配器与指定匹配器的与操作
* @param filter
* @return
*/
public JMFilter<T> and(JMFilter<T> filter);
/**
* 返回一个新的匹配器,实现当前匹配器与指定匹配器的或操作
* @param filter
* @return
*/
public JMFilter<T> or(JMFilter<T> filter);
/**
* 返回一个新的匹配器,实现当前匹配器的非操作
* @return
*/
public JMFilter<T> not();
}
基于JMFilter接口,完善JMIocClassSet,增加自定义查询接口:
/**
* 用来维护一个Class的集合
* @author 刘极渺(Miles.Liu)
*
* @date 2024-04-16 13:25
*/
public interface JMIocClassSet {
/**
* 根据完整的类名查询类
* 与原生的Class.forName()的区别在于:此方法的查找范围限于类扫描的范围
* @param className
* @return
*/
public Class<?> getByName(String className);
/**
* 判断指定类型是否在类扫描结果范围内
* @param cls
* @return
*/
public boolean contains(Class<?> cls);
/**
* 根据任意的匹配器,筛选Class列表
* @param filter
* @return
*/
public List<Class<?>> select(JMFilter<Class<?>> filter);
}
原则上,类扫描过程一旦结束,扫描结果就不应再被修改,所以JMIocClassSet不必提供修改已有类和增加新类的接口。
(二)自定义类扫描器:JMIocCustomClassLoader
定义了类扫描的结果,下来就需要设计类扫描的过程。
根据概要设计,类扫描的过程主要有两个阶段,第一个阶段是根据JMIocConfig中的包扫描配置逐一收集符合条件的类,称为默认类扫描;第二个阶段是根据框架使用者提供的自定义类扫描器,加载无法使用默认类扫描加载的其它类,称为自定义类扫描。
为了向开发者提供一个执行自定义类扫描的接口,需要定义一个标准接口,便于IoC控制器识别出自定义类扫描器,并在默认加载行为后逐个调用。这个接口的实现需要能够访问已有的类扫描结果,并且返回需要加入类扫描结果的类型列表。
定义接口:JMIocCustomClassLoader
/**
* 自定义类扫描器</br>
* </br>
*
* @author 刘极渺(Miles.Liu)
* @date 2024-04-16 14:09
*/
public interface JMIocCustomClassLoader {
public List<Class<?>> loadClasses(JMIocClassSet classSet);
}
当框架使用者需要自定义类加载逻辑时,可以实现JMIocCustomClassLoader接口,并保证实现类在类扫描范围内,框架类扫描的流程如下:
三、组件定义相关接口设计
JMJava中,定义一个组件类可以通过在类上标记@JMBean注解完成,组件类与非组件类的区别在于组件类中的注入及生命周期回调会被IoC控制器自动处理,而非组件类中即使定义了依赖注入或生命周期回调,也不会被执行。
组件类根据使用场景,可以通过不同的注解或接口实现设置组件的属性、特性、用途等。
(一)组件名称定义 @JMName
@JMName注解用于定义组件的名称,这个名称主要用于IoC控制器通过名称获取组件实例。
package cn.jmtt.java.ioc.bean;
/**
* BeanA的组件名称默认为: "cn.jmtt.java.ioc.bean.BeanA"
*/
@JMBean
public class BeanA {
}
/**
* 通过@JMName将BeanB的名称定义为:"someBeanB"
*/
@JMBean
@JMName("someBeanB")
public class BeanB {
}
(二)组件作用范围定义 @JMScope
package cn.jmtt.java.ioc.bean;
/**
* 不使用@JMScope
* BeanA的实例创建方式默认为单例模式
*/
@JMBean
public class BeanA {
}
/**
* 通过@JMScope将BeanB的实例创建方式定义为每次获取时创建新的对象
*/
@JMBean
@JMScope(JMScopeDef.Temporary)
public class BeanB {
}
JMScopeDef枚举值 | 含义 |
JMScopeDef.Singleton | 组件全局单例,每次注入的对象相同。 |
JMScopeDef.Temporary | 组件在每次注入时,都创建新的实例。 |
(三)组件顺序/优先级定义 @JMOrder
对于某些有特殊使用途径的组件类或非组件类,都可以标记@JMOrder注解,用以标明执行加载顺序、执行顺序、选用优先级等特性。
例如对于以下自定义类扫描器的定义,最终三个自定义类扫描器的执行顺序为:CustomClassLoaderB > CustomClassLoaderA > CustomClassLoaderC。
package cn.jmtt.java.ioc.bean;
/**
* 不使用@JMOrder注解,优先级默认为:JMOrderDef.NORMAL
*/
public class CustomClassLoaderA implements JMIocCustomClassLoader {
}
/**
* 通过@JMOrder将CustomClassLoaderB的执行优先级设置为高于默认优先级
*/
@JMOrder(JMOrderDef.HIGHER)
public class CustomClassLoaderB implements JMIocCustomClassLoader{
}
/**
* 通过@JMOrder将CustomClassLoaderC的执行优先级设置为最低优先级
*/
@JMOrder(JMOrderDef.LOWEST)
public class CustomClassLoaderC implements JMIocCustomClassLoader{
}
@JMOrder的参数类型为int, 参数值越小,代表顺序越靠前或优先级越高,常用的优先级定义如下:
public interface JMOrderDef {
public static final int HIGHEST = Integer.MIN_VALUE;
public static final int HIGHER = -100;
public static final int NORMAL = 0;
public static final int LOWER = 100;
public static final int LOWEST = Integer.MAX_VALUE;
}
(四)组件冲突处理定义 @JMOnDuplicate
@JMOnDuplicate注解可用于组件类或非组件类,当某些同类型类的功能可能存在冲突时,可通过@JMOnDuplicate注解设置冲突的处理方式,处理方式由JMDuplicationHandle枚举类定义,包含以下四种, 其中默认值为JMDuplicationHandle.Error:
public enum JMDuplicationHandle {
/**
* 忽略,保持已存在的值
*/
Ignor,
/**
* 覆盖之前的值
*/
Override,
/**
* 将两个值合并
*/
Merge,
/**
* 抛出异常
*/
Error
}
例如对于自定义类扫描器,如果希望在自定义类扫描器返回结果与已有扫描结果存在交集时,不抛出异常并忽略已存在的类(默认情况下会抛出类重复加载的异常),可以加上@JMOnDuplicate注解。
package cn.jmtt.java.ioc.bean;
/**
* 不使用@JMOrder注解,优先级默认为:JMOrderDef.NORMAL
*/
@JMOnDuplicate(JMDuplicationHandle.Ignor)
public class CustomClassLoaderA implements JMIocCustomClassLoader {
//……
}
(五)组件定义 JMIocBeanDefinition
一个组件的定义至少应包含以下内容:
1. 组件的类型(Class<?>)
2. 组件的作用范围(实例化方式)
3. 组件的名称
4. 组件实例的生成器
5. 组件的优先顺序(在获取或注入时有多个符合条件的组件类时,决定如何选择结果)
初步定义JMIocBeanDefinition:
public class JMIocBeanDefinition {
private Class<?> beanClass;
private JMScopeDef scope = JMScopeDef.Singleton;
private String beanName;
private int order = JMOrderDef.NORMAL;
private JMIocBeanFactory factory;
//…… GET/SET方法
}
其中,JMIocBeanFactory用于实际生成组件实例,根据不同的实例类型,可以有不同的多态实现,基本定义如下:
public interface JMIocBeanFactory {
/**
* 获取组件实例
* @return
*/
public Object getInstance(Class<?> beanClass);
}
为了实现Ioc控制器对于组件定义的管理、查询操作的封装,定义JMIocBeanDefinitionSet类,用于对于组件定义的统一管理:
public interface JMIocBeanContext {
/**
* 根据类查找组件定义
* @param selector
* @return
*/
public JMIocBeanDefinition getByClass(Class<T> clazz);
/**
* 根据实例名查找组件定义
* @param beanName
* @return
*/
public JMIocBeanDefinition getByName(String beanName);
/**
* 根据类型从容器中筛选组件定义
* @param selector
* @return
*/
public List<JMIocBeanDefinition> select(JMFilter<JMIocBeanDefinition> filter);
}
由于IoC容器初始化阶段,需要完成静态注入以及相关的依赖注入,所以JMIocBeanContext的构建只能发生在IoC容器初始化完成之前,如果在静态注入完成后再修改组件定义,可能引发复杂的重新注入等情况,因此JMIocBeanContext不应提供组件定义的注册、修改接口。(暂定,有待后续详细论证)
(六)自定义组件加载器 JMIocCustomBeanLoader
框架默认的组件加载行为是在类扫描结果中,查找所有包含@JMBean注解的类,并生成组件定义,但框架使用过程中,可能需要根据自定义行为创建组件定义,这就需要为框架使用者提供自定义组件加载的接口。
定义JMIocCustomBeanLoader接口,并约定IoC控制器在完成了默认组件定义加载行为后,需要在类扫描结果中查找所有可实例化的JMIocCustomBeanLoader实现,并根据优先级(由@JMOrder注解定义)依次执行processLoad方法,从而实现自定义组件加载。
public interface JMIocCustomBeanLoader {
/**
* 可以通过JMIocBeanDefinitionRegister 进行Bean的注册
* @param bdr
*/
public void processLoad(JMIocBeanDefinitionRegister bdr);
}
对于如何将自定义组件加入到JMIocBeanContext中的问题,最简单的方式应该是将JMIocBeanContext的实例以参数或全局定义的方式传递给processLoad方法,并在JMIocBeanContext类中提供注册接口,但前面讨论过这种方式可能给框架造成麻烦,因此,这里采用一个“包装器”(JMIocBeanDefinitionRegister)来实现自定义组件的注册。
JMIocBeanDefinitionRegister主要提供两种注册组件定义的接口方法,初步定义如下:
public interface JMIocBeanDefinitionRegister {
//以名称方式注册组件定义
public void registerBean(String name, JMIocBeanDefinition bd);
//以类型方式注册组件定义
public void registerBean(Class<?> clazz, JMIocBeanDefinition bd);
}
至于如何实现“包装器”,在后续设计中再做考虑。
(七)自定义组件实例生成器 JMIocCustomBeanFactory
JMJava默认只提供了两种实例生成策略,一是单例模式,二是常规对象创建模式,如果框架使用者需要自定义实例生成策略(例如生成特殊代理对象等),需要框架提供自定义组件实例生成器定义的接口。
在框架使用者添加自定义组件实例生成逻辑时,设置自定义组件实例生成逻辑对哪些组件生效是必要的,如何面向开发者提供设置接口是必须解决的问题。
暂时考虑大多数情况下,自定义组件实例生成器是面向指定类型或接口实现生效,因此,定义@JMTarget注解,用以标注实例生成器的应用范围。
定义接口JMIocCustomBeanFactory用以帮助IoC控制器查找并应用自定义及组件实例生成器:
public interface JMIocCustomBeanFactory extends JMIocBeanFactory{
}
IoC容器在为组件定义创建实例生成器之前,会从类扫描结果中查找所有JMIocCustomBeanFactory的可实例化实现,并根据这些实现拥有的@JMOrder、@JMOnDuplicate、@JMTarget、@JMScope 注解为每个组件定义创建最匹配的实例生成器。
例如如果希望针对实现了JMApi接口的组件定义特殊的实例生成逻辑,可定义如下自定义组件实例生成器:
@JMTarget(JMApi.class) //设置实例生成器面向所有JMApi的实现(子类)生效
@JMOrder(JMOrderDef.HiGHEST) //设置为最高优先级
@JMOnDuplicate(JMDuplicationHandle.Override) //当有其他同优先级实例生成器也匹配时,覆盖其它生成器
@JMScope(JMScopeDef.Singleton) //只针对设置为全局单例的组件类生效
public class JMIocCustomBeanFactoryForJMApi implements JMIocCustomBeanFactory {
@Override
public Object getInstance(Class<?> beanClass) {
Object instance = null;
//TODO 创建实例
return instance ;
}
}
四、完善后的JMIoc接口设计
(一)JMIoc类
基于尽可能减少框架使用复杂度的考虑,以JMIoc类为载体,将本文提到的接口设计进行总结和集成,最终JMIoc类接口定义如下:
public final class JMIoc {
//……
/**
* 获取配置对象,可以进行配置项获取或设置
*/
public JMIocConfig config() {
return iocConfig;
}
/**
* 执行初始化
*/
public void initialize() {
//……
}
/**
* 获取类扫描结果,可进行类查询
*/
public JMIocClassSet classSet() {
//……
}
/**
* 获取组件定义上下文,可进组件定义查询
*/
public JMIocBeanContext beanContext() {
//……
}
/**
* 以组件名称为参数,获取或创建组件实例
* @param beanName 组件名称,默认为组件类的完整名称
* @return 组件实例
*/
public final Object getBean(String beanName) {
//……
}
/**
* 以组件类型为参数,获取或创建组件实例
* @param <T>
* @param beanClass 组件类型
* @return 组件实例
*/
public <T> T getBean(Class<T> beanClass) {
//……
}
/**
* 以组件名称为参数,获取或创建组件实例, 如果获取结果不是beanClass指定的实例,抛出异常
* @param <T>
* @param beanClass 返回结果的类型
* @param beanName 组件名称,默认为组件类的完整名称
* @return 组件实例
*/
public <T> T getBean(Class<T> beanClass, String beanName) {
//……
}
}
使用示例:
public class TestMain {
public static void main(String args) {
//设置类扫描范围
JMIoc.config().addPackageScan("cn.jmtt.java");
JMIoc.config().addPackageScan("com.milesliu");
//初始化
JMIoc.initialize();
}
}
(二)自定义类扫描器接口使用示例
@JMOrder(JMOrderDef.LOWEST) //最后执行
@JMOnDuplicate(JMDuplicationHandle.Ignor) //忽略重复的类
public class MyClassLoader implements JMIocCustomClassLoader {
@Override
public List<Class<?>> loadClasses(JMIocClassSet classSet) {
List<Class<?>> list = new ArrayList<>();
//TODO 加载类逻辑
return list;
}
}
(三)自定义组件加载器接口使用示例
将所有实现JMApi的类定义为组件:
@JMOrder(JMOrderDef.LOWEST) //最后执行
@JMOnDuplicate(JMDuplicationHandle.Override) //覆盖重复定义
public class MyBeanLoader implements JMIocCustomBeanLoader {
@Override
public void processLoad(JMIocBeanDefinitionRegister bdr) {
//构造用于查询JMApi子类的筛选器
JMFilter<Class<?>> filter = JMClassFilterBySuperClass.get(JMApi.class);
//从类扫描结果中筛选所有JMApi的子类
List<Class<?>> classes = JMIoc.classSet().select(filter);
//逐一创建组件定义
for (Class<?> cls : classes) {
JMIocBeanDefinition bd = null;
//TODO 根据需要创建组件定义
bdr.register(bd);
}
}
// 实现根据父类查询子类的筛选器
static class JMClassFilterBySuperClass implements JMFilter<Class<?>> {
private Class<?> superClass;
public static JMClassFilterBySuperClass get(Class<?> superCls) {
return new JMClassFilterBySuperClass(superCls);
}
private JMClassFilterBySuperClass(Class<?> superClass) {
super();
this.superClass = superClass;
}
@Override
public boolean match(Class<?> cls) {
if (superClass == null) {
return false;
}
return superClass.isAssignableFrom(cls);
}
}
}