如何实现一个低配版​​Spring BeanFactory​​?


目录


  • 如何实现一个低配版`Spring BeanFactory`?
  • 准备工作
  • 核心源码
  • 效果
  • 总结
  • 更多



手机用户请​​横屏​​​获取最佳阅读体验,​​REFERENCES​​中是本文参考的链接,如需要链接和更多资源,可以关注其他博客发布地址。


平台

地址



简书

https://www.jianshu.com/u/3032cc862300

个人博客

https://yiyuery.club


结合Spring BeanFactory实例扫描和注入思想进行深入编码实战:工厂化管理运行中实例对象


准备工作

  • 包扫描工具类定义
/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/5/20 20:57
* @email: https:yiyuery.github.io/NoteBooks
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.swagger.common.component;

import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
* <p>
* 扫描指定路径下class
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019-06-05 22:46
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019-06-05
* @modify reason: {方法名}:{原因}
* ...
*/
@Slf4j
public class ClasspathPackageScanner {
private String basePackage;
private ClassLoader cl;

/**
* Construct an instance and specify the base package it should scan.
* @param basePackage The base package to scan.
*/
public ClasspathPackageScanner(String basePackage) {
this.basePackage = basePackage;
this.cl = getClass().getClassLoader();

}

/**
* Construct an instance with base package and class loader.
* @param basePackage The base package to scan.
* @param cl Use this class load to locate the package.
*/
public ClasspathPackageScanner(String basePackage, ClassLoader cl) {
this.basePackage = basePackage;
this.cl = cl;
}

/**
* Get all fully qualified names located in the specified package
* and its sub-package.
*
* @return A list of fully qualified names.
* @throws IOException
*/
public List<String> getFullyQualifiedClassNameList() throws IOException {
log.info("Start Scan...", basePackage);
return doScan(basePackage, new ArrayList<>());
}

/**
* Actually perform the scanning procedure.
*
* @param basePackage
* @param nameList A list to contain the result.
* @return A list of fully qualified names.
*
* @throws IOException
*/
private List<String> doScan(String basePackage, List<String> nameList) throws IOException {
// replace dots with splashes
String splashPath = StringUtil.dotToSplash(basePackage);

// get file path
URL url = cl.getResource(splashPath);
String filePath = StringUtil.getRootPath(url);

// Get classes in that package.
// If the web server unzips the jar file, then the classes will exist in the form of
// normal file in the directory.
// If the web server does not unzip the jar file, then classes will exist in jar file.
// contains the name of the class file. e.g., Apple.class will be stored as "Apple"
List<String> names = null;
if (isJarFile(filePath)) {
// jar file
if (log.isDebugEnabled()) {
log.debug("{} fina a jar file", filePath);
}

names = readFromJarFile(filePath, splashPath);
} else {
// directory
if (log.isDebugEnabled()) {
log.debug("{} find a directory", filePath);
}

names = readFromDirectory(filePath);
}

for (String name : names) {
if (isClassFile(name)) {
nameList.add(toFullyQualifiedName(name, basePackage));
} else {
// this is a directory
// check this directory for more classes
// do recursive invocation
doScan(basePackage + "." + name, nameList);
}
}

if (log.isDebugEnabled()) {
for (String n : nameList) {
log.debug("load {}", n);
}
}

return nameList;
}

/**
* Convert short class name to fully qualified name.
* e.g., String -> java.lang.String
*/
private String toFullyQualifiedName(String shortName, String basePackage) {
StringBuilder sb = new StringBuilder(basePackage);
sb.append('.');
sb.append(StringUtil.trimExtension(shortName));

return sb.toString();
}

private List<String> readFromJarFile(String jarPath, String splashedPackageName) throws IOException {
if (log.isDebugEnabled()) {
log.debug("从JAR包中读取类: {}", jarPath);
}

JarInputStream jarIn = new JarInputStream(new FileInputStream(jarPath));
JarEntry entry = jarIn.getNextJarEntry();

List<String> nameList = new ArrayList<>();
while (null != entry) {
String name = entry.getName();
if (name.startsWith(splashedPackageName) && isClassFile(name)) {
nameList.add(name);
}

entry = jarIn.getNextJarEntry();
}

return nameList;
}

private List<String> readFromDirectory(String path) {
File file = new File(path);
String[] names = file.list();

if (null == names) {
return null;
}

return Arrays.asList(names);
}

private boolean isClassFile(String name) {
return name.endsWith(".class");
}

private boolean isJarFile(String name) {
return name.endsWith(".jar");
}

/**
* For test purpose.
*/
public static void main(String[] args) throws Exception {
ClasspathPackageScanner scan = new ClasspathPackageScanner("com.example.swagger");
scan.getFullyQualifiedClassNameList();
}
}

class StringUtil{
private StringUtil() {

}
/**
* "file:/home/whf/cn/fh" -> "/home/whf/cn/fh"
* "jar:file:/home/whf/foo.jar!cn/fh" -> "/home/whf/foo.jar"
*/
public static String getRootPath(URL url) {
String fileUrl = url.getFile();
int pos = fileUrl.indexOf('!');

if (-1 == pos) {
return fileUrl;
}

return fileUrl.substring(5, pos);
}

/**
* "cn.fh.lightning" -> "cn/fh/lightning"
* @param name
* @return
*/
public static String dotToSplash(String name) {
return name.replaceAll("\\.", "/");
}

/**
* "Apple.class" -> "Apple"
*/
public static String trimExtension(String name) {
int pos = name.indexOf('.');
if (-1 != pos) {
return name.substring(0, pos);
}

return name;
}

/**
* /application/home -> /home
* @param uri
* @return
*/
public static String trimURI(String uri) {
String trimmed = uri.substring(1);
int splashIndex = trimmed.indexOf('/');

return trimmed.substring(splashIndex);
}
}
  • 自动注入注解定义​​AutoRegister​​​: 类似于​​Spring​​​的​​@Component​​注解
/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/5/20 20:57
* @email: https:yiyuery.github.io/NoteBooks
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.swagger.api;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* <p>
* 自动注入注解定义`AutoRegister`: 类似于`Spring`的`@Component`注解
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019-06-05 21:46
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019-06-05
* @modify reason: {方法名}:{原因}
* ...
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface AutoRegister {

String name() default "";
}
  • 配置类属性填充注解定义​​PropNameSpace​​​ ,读取​​Properties​​前缀自动注入
/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/5/20 20:57
* @email: https:yiyuery.github.io/NoteBooks
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.swagger.api;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* <p>
* 配置类属性填充注解定义`PropNameSpace` ,读取`Properties`前缀自动注入
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019-06-05 21:43
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019-06-05
* @modify reason: {方法名}:{原因}
* ...
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface PropNameSpace {
/**
* 入参前缀
* @return
*/
String prefix() default "";
}
  • 实例工厂定义
/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/5/20 20:57
* @email: https:yiyuery.github.io/NoteBooks
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.swagger.common.context;

import java.util.HashMap;
import java.util.Map;

/**
* <p>
*
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019-06-05 22:45
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019-06-05
* @modify reason: {方法名}:{原因}
* ...
*/
public class AppBeanContext {

private static class SingletonHolder {
private static final AppBeanContext INSTANCE = new AppBeanContext();
}

public static final AppBeanContext getInstance() {
return SingletonHolder.INSTANCE;
}

private static final Map<String, Object> HOLDER = new HashMap<>();

/**
* 注册实例
* @param name
* @param bean
*/
public void registerBean(String name, Object bean) {
if (HOLDER.containsKey(name)) {
throw new IllegalStateException("bean repetition register!");
}
HOLDER.putIfAbsent(name, bean);
}

/**
* 获取实例
* @param name
* @param clazz
* @param <T>
* @return
*/
public <T> T getBean(String name, Class<T> clazz) {
if (!HOLDER.containsKey(name)) {
throw new IllegalArgumentException("bean repetition register!");
}
return clazz.cast(HOLDER.get(name));
}
}

​思路​


  • 首先通过指定​​package​​路径下的class文件扫描
  • 然后通过自定义注解完成,判断是否需要自动注入。
  • 如果含有​​AutoRegister​​注解,则通过反射生成实例并存放到实例工厂中
  • 如果含有​​PropNameSpace​​注解,则自动读取yaml文件中的属性来进行填充

核心源码

  • 启动初始化类
/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: https:yiyuery.club
* @date: 2019/5/20 20:57
* @email: xiazhaoyang@live.com
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.swagger.common.component;

import com.example.swagger.api.AutoRegister;
import com.example.swagger.api.ITransformPlugin;
import com.example.swagger.api.PropNameSpace;
import com.example.swagger.common.base.AbstractOfficeTransformPlugin;
import com.example.swagger.common.base.AbstractSwaggerTransformPlugin;
import com.example.swagger.common.configuration.ApplicationYamlLoader;
import com.example.swagger.common.configuration.OfficeTransformConfig;
import com.example.swagger.common.context.AppBeanContext;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.Field;
import java.util.List;

/**
* <p>
* 启动初始化类
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019-06-07 05:33
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019-06-07
* @modify reason: {方法名}:{原因}
* ...
*/
@Slf4j
public class AppStarterInitial {
static {
log.info("start init...");
beforeStart();
log.info("end init...");
}

private static void beforeStart() {
try {
ClasspathPackageScanner scan = new ClasspathPackageScanner("com.example.swagger");
List<String> classNames = scan.getFullyQualifiedClassNameList();
for (String clazz : classNames) {
Class<?> c = Thread.currentThread().getContextClassLoader().loadClass(clazz);
if (c.getAnnotations().length > 0) {
//获取自动注入对象
if (c.getAnnotationsByType(AutoRegister.class).length > 0) {
Object bean = c.newInstance();
//遍历属性中所有Field
String prefixKey = "";
if (c.getAnnotationsByType(PropNameSpace.class).length > 0) {
PropNameSpace[] annotationsByType = c.getAnnotationsByType(PropNameSpace.class);
prefixKey = annotationsByType[0].prefix();
}
Field[] declaredFields = c.getDeclaredFields();

for (Field field : declaredFields) {
field.setAccessible(Boolean.TRUE);
String filedKey = field.getAnnotationsByType(JsonProperty.class).length > 0 ? field.getAnnotationsByType(JsonProperty.class)[0].value() : field.getName();
String propKey = prefixKey + "." + filedKey;
field.set(bean, ApplicationYamlLoader.getPropsByKey(propKey,field.getType()));
}
String beanName = c.getAnnotationsByType(AutoRegister.class)[0].name();
AppBeanContext.getInstance().registerBean(
StringUtils.isNotBlank(beanName)?beanName: StringUtils.lowerCase(c.getSimpleName()).substring(0,1)+c.getSimpleName().substring(1),
bean);
}
}
}
} catch (Throwable e) {
log.error("init error", e);
}
}
}

这个主要为了完成实例自动扫描和注入、参数配置类自动扫描填充参数的功能。

项目启动类集成这个类即可在实例化之前触发对应实例并注入到实例工厂中以备使用。

/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/5/20 20:57
* @email: https:yiyuery.github.io/NoteBooks
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.swagger;

import com.example.swagger.common.component.AppStarterInitial;
import com.example.swagger.common.enums.FileTransformEnum;
import com.example.swagger.common.enums.SwaggerGenEnum;

/**
* <p>
* 启动类
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019-06-05 22:57
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019-06-05
* @modify reason: {方法名}:{原因}
* ...
*/
public class SwaggerTransformApplication extends AppStarterInitial {

public static void main(String[] args){
//TODO ...
}

}

效果

如此一来,我们的低配版​​Spring BeanFactory​​就可以看到如下的效果了。

[Spring] 如何实现一个低配版`Spring BeanFactory`?_依赖注入

​图片中打印的这个配置类:​

/*
* @ProjectName: 编程学习
* @Copyright: 2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
* @address: http://xiazhaoyang.tech
* @date: 2019/5/20 20:57
* @email: https:yiyuery.github.io/NoteBooks
* @description: 本内容仅限于编程技术学习使用,转发请注明出处.
*/
package com.example.swagger.common.configuration;

import com.example.swagger.api.AutoRegister;
import com.example.swagger.api.PropNameSpace;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* <p>
*
* </p>
*
* @author xiazhaoyang
* @version v1.0.0
* @date 2019-06-05 21:50
* @modificationHistory=========================逻辑或功能性重大变更记录
* @modify By: {修改人} 2019-06-05
* @modify reason: {方法名}:{原因}
* ...
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@AutoRegister
@PropNameSpace(prefix = "enhance.config")
public class SwaggerEnhanceConfig {

@JsonProperty("api-docs-url")
private String apiDocsUrl;
}

​yaml:​

enhance:
config:
api-docs-url: http://localhost:9000/swagger-resources/v2/api-docs?group=UI

通过图片我们可以看到,已经成功实现了配置类实例的内部参数填充了。

总结

本文通过自定义注解和反射模拟了Spring BeanFactory的一个简单的实例动态注入和管理功能。有兴趣的小伙伴可以深入了解下Spring的三大核心思想:IOC(控制反转),DI(依赖注入),AOP(面向切面编程)。

更多


扫码关注“架构探险之道”,获取更多源码和文章资源


[Spring] 如何实现一个低配版`Spring BeanFactory`?_DI_02