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

简介: 本文通过自定义注解和反射模拟了Spring BeanFactory的一个简单的实例动态注入和管理功能。

如何实现一个低配版Spring BeanFactory

@TOC

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

平台 地址
CSDN https://blog.csdn.net/sinat_28690417
简书 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就可以看到如下的效果了。

在这里插入图片描述

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

/*
 * @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(面向切面编程)。

更多

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

在这里插入图片描述

知识星球(扫码加入获取源码和文章资源链接)

在这里插入图片描述

相关文章
|
6月前
|
存储 Java Spring
深入解析Spring框架的核心:BeanFactory体系结构探究
深入解析Spring框架的核心:BeanFactory体系结构探究
87 0
|
7月前
|
Java Spring
|
8月前
|
Java Spring 容器
Spring中BeanFactory和FactoryBean的区别?
一位工作了4年的小伙伴,去京东面试被问到这样一个问题,Spring中的BeanFactory和FactoryBean有什么区别?因为没有看过源码,当时就感觉这是一个文字游戏,感觉没什么区别? 那今天,我就给大家来聊清楚。另外,往期面试题解析中配套的文档我已经准备好,想获得的可以在我的煮叶简介中找到。好了,我们先来看BeanFactory。
32 0
|
1月前
|
Java 测试技术 数据库连接
【Spring源码解读!底层原理高级进阶】【下】探寻Spring内部:BeanFactory和ApplicationContext实现原理揭秘✨
【Spring源码解读!底层原理高级进阶】【下】探寻Spring内部:BeanFactory和ApplicationContext实现原理揭秘✨
|
12天前
|
XML Java 数据格式
Bean工厂探秘:解析Spring底层工厂体系BeanFactory的神奇之道
Bean工厂探秘:解析Spring底层工厂体系BeanFactory的神奇之道
19 0
Bean工厂探秘:解析Spring底层工厂体系BeanFactory的神奇之道
|
1月前
|
XML Java 开发者
【Spring源码解读 底层原理高级进阶】【上】探寻Spring内部:BeanFactory和ApplicationContext实现原理讲解
【Spring源码解读 底层原理高级进阶】【上】探寻Spring内部:BeanFactory和ApplicationContext实现原理讲解
|
3月前
|
Java 容器 Spring
Spring5源码(13)-BeanFactory简介
Spring5源码(13)-BeanFactory简介
29 1
|
3月前
|
XML Java 数据格式
Spring5源码(8)-BeanFactory和FactoryBean的区别
Spring5源码(8)-BeanFactory和FactoryBean的区别
31 0
|
3月前
|
Java C++ Spring
深入Spring原理-1.BeanFactory与ApplicationContext的区别
深入Spring原理-1.BeanFactory与ApplicationContext的区别
48 0
|
4月前
|
XML Java 数据格式
[读书笔记]Spring中BeanFactory和ApplicationContext的联系和区别
[读书笔记]Spring中BeanFactory和ApplicationContext的联系和区别
32 0