Spring从0-1带你手搓(完结)

对于Java后端开发的同学来说,Spring框架已经是事实上的标准,如果对Spring和Spring Boot还不熟悉,那需要立刻抓紧时间学习SpringSpring Boot。对于已经能熟练使用Spring框架的同学来说,要进一步理解Spring的设计思想,提升自己的架构能力,不如自己动手,从零开始编写一个Spring框架。

本教程的目标就是以Spring框架为原型,专注于实现Spring的核心功能,编写一个迷你版的Spring框架,我们把它命名为Summer Framework,与Spring主要区别在于,它俩的图标有所不同:

Spring Framework

Summer Framework

Summer Framework设计目标如下:

  • context模块:实现ApplicationContext容器与Bean的管理;
  • aop模块:实现AOP功能;
  • jdbc模块:实现JdbcTemplate,以及声明式事务管理;
  • web模块:实现Web MVC和REST API;
  • boot模块:实现一个简化版的“Spring Boot”,用于打包运行。

我们会一步一步实现各模块,并在此基础上开发完整的应用程序。

1.实现IoC容器

Spring的核心就是能管理一组Bean,并能自动配置依赖关系的IoC容器。而我们的Summer Framework的核心context模块就是要实现IoC容器。

2.设计目标

Spring的IoC容器分为两类:BeanFactory和ApplicationContext,前者总是延迟创建Bean,而后者则在启动时初始化所有Bean。实际使用时,99%都采用ApplicationContext,因此,Summer Framework仅实现ApplicationContext,不支持BeanFactory。

早期的Spring容器采用XML来配置Bean,后期又加入了自动扫描包的功能,即通过<context:component-scan base-package="org.example"/>的配置。再后来,又加入了Annotation配置,并通过@ComponentScan注解实现自动扫描。如果使用Spring Boot,则99%都采用@ComponentScan注解方式配置,因此,Summer Framework也仅实现Annotation配置+@ComponentScan扫描方式完成容器的配置。

此外,Summer Framework仅支持Singleton类型的Bean,不支持Prototype类型的Bean,因为实际使用中,99%都采用Singleton。依赖注入则与Spring保持一致,支持构造方法、Setter方法与字段注入。支持@Configuration和BeanPostProcessor。至于Spring的其他功能,例如,层级容器、MessageSource、一个Bean允许多个名字等功能,一概不支持!

下表列出了Spring Framework和Summer Framework在IoC容器方面的异同:

功能

Spring Framework

Summer Framework

IoC容器

支持BeanFactory和ApplicationContext

仅支持ApplicationContext

配置方式

支持XML与Annotation

仅支持Annotation

扫描方式

支持按包名扫描

支持按包名扫描

Bean类型

支持Singleton和Prototype

仅支持Singleton

Bean工厂

支持FactoryBean和@Bean注解

仅支持@Bean注解

定制Bean

支持BeanPostProcessor

支持BeanPostProcessor

依赖注入

支持构造方法、Setter方法与字段

支持构造方法、Setter方法与字段

多容器

支持父子容器

不支持

3.Annotation配置

从使用者的角度看,使用IoC容器时,需要定义一个入口配置,它通常长这样:

@ComponentScanpublic class AppConfig {

}

AppConfig只是一个配置类,它的目的是通过@ComponentScan来标识要扫描的Bean的包。如果没有明确写出包名,那么将基于AppConfig所在包进行扫描,如果明确写出了包名,则在指定的包下进行扫描。

在扫描过程中,凡是带有注解@Component的类,将被添加到IoC容器进行管理:

@Componentpublic class Hello {

}

我们用到的许多第三方组件也经常会纳入IoC容器管理。这些第三方组件是不可能带有@Component注解的,引入第三方Bean只能通过工厂模式,即在@Configuration工厂类中定义带@Bean的工厂方法:

@Configurationpublic class DbConfig {

    @Bean

    DataSource createDataSource(...) {

        return new HikariDataSource(...);

    }



    @Bean

    JdbcTemplate createJdbcTemplate(...) {

        return new JdbcTemplate(...);

    }

}

基于Annotation配置的IoC容器基本用法就是上面所述。下面,我们就一步一步来实现IoC容器。

3.1实现ResourceResolver

在编写IoC容器之前,我们首先要实现@ComponentScan,即解决“在指定包下扫描所有Class”的问题。

Java的ClassLoader机制可以在指定的Classpath中根据类名加载指定的Class,但遗憾的是,给出一个包名,例如,org.example,它并不能获取到该包下的所有Class,也不能获取子包。要在Classpath中扫描指定包名下的所有Class,包括子包,实际上是在Classpath中搜索所有文件,找出文件名匹配的.class文件。例如,Classpath中搜索的文件org/example/Hello.class就符合包名org.example,我们需要根据文件路径把它变为org.example.Hello,就相当于获得了类名。因此,搜索Class变成了搜索文件。

我们先定义一个Resource类型表示文件:

public record Resource(String path, String name) {

}

再仿造Spring提供一个ResourceResolver,定义scan()方法来获取扫描到的Resource:

public class ResourceResolver {

    String basePackage;



    public ResourceResolver(String basePackage) {

        this.basePackage = basePackage;

    }



    public <R> List<R> scan(Function<Resource, R> mapper) {

        ...

    }

}

这样,我们就可以扫描指定包下的所有文件。有的同学会问,我们的目的是扫描.class文件,如何过滤出Class?

注意到scan()方法传入了一个映射函数,我们传入Resource到Class Name的映射,就可以扫描出Class Name:

// 定义一个扫描器:

ResourceResolver rr = new ResourceResolver("org.example");List<String> classList = rr.scan(res -> {

    String name = res.name(); // 资源名称"org/example/Hello.class"

    if (name.endsWith(".class")) { // 如果以.class结尾

        // 把"org/example/Hello.class"变为"org.example.Hello":

        return name.substring(0, name.length() - 6).replace("/", ".").replace("\\", ".");

    }

    // 否则返回null表示不是有效的Class Name:

    return null;

});

这样,ResourceResolver只负责扫描并列出所有文件,由客户端决定是找出.class文件,还是找出.properties文件。

在ClassPath中扫描文件的代码是固定模式,可以在网上搜索获得,例如StackOverflow的这个回答。这里要注意的一点是,Java支持在jar包中搜索文件,所以,不但需要在普通目录中搜索,也需要在Classpath中列出的jar包中搜索,核心代码如下:

// 通过ClassLoader获取URL列表:

Enumeration<URL> en = getContextClassLoader().getResources("org/example");while (en.hasMoreElements()) {

    URL url = en.nextElement();

    URI uri = url.toURI();

    if (uri.toString().startsWith("file:")) {

        // 在目录中搜索

    }

    if (uri.toString().startsWith("jar:")) {

        // 在Jar包中搜索

    }

}

几个要点:

ClassLoader首先从Thread.getContextClassLoader()获取,如果获取不到,再从当前Class获取,因为Web应用的ClassLoader不是JVM提供的基于Classpath的ClassLoader,而是Servlet容器提供的ClassLoader,它不在默认的Classpath搜索,而是在/WEB-INF/classes目录和/WEB-INF/lib的所有jar包搜索,从Thread.getContextClassLoader()可以获取到Servlet容器专属的ClassLoader;

Windows和Linux/macOS的路径分隔符不同,前者是\,后者是/,需要正确处理;

扫描目录时,返回的路径可能是abc/xyz,也可能是abc/xyz/,需要注意处理末尾的/。

这样我们就完成了能扫描指定包以及子包下所有文件的ResourceResolver。

3.2实现PropertyResolver

Spring的注入分为@Autowired和@Value两种。对于@Autowired,涉及到Bean的依赖,而对于@Value,则仅仅是将对应的配置注入,不涉及Bean的依赖,相对比较简单。为了注入配置,我们用PropertyResolver保存所有配置项,对外提供查询功能。

本节我们来实现PropertyResolver,它支持3种查询方式:

按配置的key查询,例如:getProperty("app.title");

以${abc.xyz}形式的查询,例如,getProperty("${app.title}"),常用于@Value("${app.title}")注入;

带默认值的,以${abc.xyz:defaultValue}形式的查询,例如,getProperty("${app.title:Summer}"),常用于@Value("${app.title:Summer}")注入。

Java本身提供了按key-value查询的Properties,我们先传入Properties,内部按key-value存储:

public class PropertyResolver {



    Map<String, String> properties = new HashMap<>();



    public PropertyResolver(Properties props) {

        // 存入环境变量:

        this.properties.putAll(System.getenv());

        // 存入Properties:

        Set<String> names = props.stringPropertyNames();

        for (String name : names) {

            this.properties.put(name, props.getProperty(name));

        }

    }

}

这样,我们在PropertyResolver内部,通过一个Map<String, String>存储了所有的配置项,包括环境变量。对于按key查询的功能,我们可以简单实现如下:

@Nullablepublic String getProperty(String key) {

    return this.properties.get(key);

}

下一步,我们准备解析${abc.xyz:defaultValue}这样的key,先定义一个PropertyExpr,把解析后的key和defaultValue存储起来:

record PropertyExpr(String key, String defaultValue) {}

然后按${...}解析:

PropertyExpr parsePropertyExpr(String key) {

    if (key.startsWith("${") && key.endsWith("}")) {

        // 是否存在defaultValue?

        int n = key.indexOf(':');

        if (n == (-1)) {

            // 没有defaultValue: ${key}

            String k = key.substring(2, key.length() - 1);

            return new PropertyExpr(k, null);

        } else {

            // 有defaultValue: ${key:default}

            String k = key.substring(2, n);

            return new PropertyExpr(k, key.substring(n + 1, key.length() - 1));

        }

    }

    return null;

}

我们把getProperty()改造一下,即可实现查询${abc.xyz:defaultValue}:

@Nullablepublic String getProperty(String key) {

    // 解析${abc.xyz:defaultValue}:

    PropertyExpr keyExpr = parsePropertyExpr(key);

    if (keyExpr != null) {

        if (keyExpr.defaultValue() != null) {

            // 带默认值查询:

            return getProperty(keyExpr.key(), keyExpr.defaultValue());

        } else {

            // 不带默认值查询:

            return getRequiredProperty(keyExpr.key());

        }

    }

    // 普通key查询:

    String value = this.properties.get(key);

    if (value != null) {

        return parseValue(value);

    }

    return value;

}

每次查询到value后,我们递归调用parseValue(),这样就可以支持嵌套的key,例如:

${app.title:${APP_NAME:Summer}}

这样可以先查询app.title,没有找到就再查询APP_NAME,还没有找到就返回默认值Summer。

注意到Spring的${...}表达式实际上可以做到组合,例如:

jdbc.url=jdbc:mysql//${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME}

而我们实现的${...}表达式只能嵌套,不能组合,因为要实现Spring的表达式,需要编写一个完整的能解析表达式的复杂功能,而不能仅仅依靠判断${开头、}结尾。由于解析表达式的功能过于复杂,因此我们决定不予支持。

Spring还支持更复杂的#{...}表达式,它可以引用Bean、调用方法、计算等:

#{appBean.version() + 1}

为此Spring专门提供了一个spring-expression库来支持这种更复杂的功能。按照一切从简的原则,我们不支持#{...}表达式。

4.实现类型转换

除了String类型外,@Value注入时,还允许boolean、int、Long等基本类型和包装类型。此外,Spring还支持Date、Duration等类型的注入。我们既要实现类型转换,又不能写死,否则,将来支持新的类型时就要改代码。

我们先写类型转换的入口查询:

@Nullablepublic <T> T getProperty(String key, Class<T> targetType) {

    String value = getProperty(key);

    if (value == null) {

        return null;

    }

    // 转换为指定类型:

    return convert(targetType, value);

}

再考虑如何实现convert()方法。对于类型转换,实际上是从String转换为指定类型,因此,用函数式接口Function<String, Object>就很合适:

public class PropertyResolver {

    // 存储Class -> Function:

    Map<Class<?>, Function<String, Object>> converters = new HashMap<>();



    // 转换到指定Class类型:

    <T> T convert(Class<?> clazz, String value) {

        Function<String, Object> fn = this.converters.get(clazz);

        if (fn == null) {

            throw new IllegalArgumentException("Unsupported value type: " + clazz.getName());

        }

        return (T) fn.apply(value);

    }

}

这样我们就已经实现了类型转换,下一步是把各种要转换的类型放到Map里。在构造方法中,我们放入常用的基本类型转换器:

public PropertyResolver(Properties props) {

    ...

    // String类型:

    converters.put(String.class, s -> s);

    // boolean类型:

    converters.put(boolean.class, s -> Boolean.parseBoolean(s));

    converters.put(Boolean.class, s -> Boolean.valueOf(s));

    // int类型:

    converters.put(int.class, s -> Integer.parseInt(s));

    converters.put(Integer.class, s -> Integer.valueOf(s));

    // 其他基本类型...

    // Date/Time类型:

    converters.put(LocalDate.class, s -> LocalDate.parse(s));

    converters.put(LocalTime.class, s -> LocalTime.parse(s));

    converters.put(LocalDateTime.class, s -> LocalDateTime.parse(s));

    converters.put(ZonedDateTime.class, s -> ZonedDateTime.parse(s));

    converters.put(Duration.class, s -> Duration.parse(s));

    converters.put(ZoneId.class, s -> ZoneId.of(s));

}

如果再加一个registerConverter()接口,我们就可以对外提供扩展,让用户自己编写自己定制的Converter,这样一来,我们的PropertyResolver就准备就绪,读取配置的初始化代码如下:

// Java标准库读取properties文件:

Properties props = new Properties();

props.load(fileInput); // 文件输入流// 构造PropertyResolver:

PropertyResolver pr = new PropertyResolver(props);// 后续代码调用...// pr.getProperty("${app.version:1}", int.class)

5.使用YAML配置

Spring Framework并不支持YAML配置,但Spring Boot支持。因为YAML配置比.properties要方便,所以我们把对YAML的支持也集成进来。

首先引入依赖org.yaml:snakeyaml:2.0,然后我们写一个YamlUtils,通过loadYamlAsPlainMap()方法读取一个YAML文件,并返回Map:

public class YamlUtils {

    public static Map<String, Object> loadYamlAsPlainMap(String path) {

        return ...

    }

}

我们把YAML格式:

app:

  title: Summer Framework

  version: ${VER:1.0}

读取为Map,其中,每个key都是完整路径,相当于把它变为.properties格式:

app.title=Summer Framework

app.version=${VER:1.0}

这样我们无需改动PropertyResolver的代码,使用YAML时,可以按如下方式读取配置:

Map<String, Object> configs = YamlUtils.loadYamlAsPlainMap("/application.yml");

Properties props = new Properties();

props.putAll(config);

PropertyResolver pr = new PropertyResolver(props);

读取YAML的代码比较简单,注意要点如下:

  1. SnakeYaml默认读出的结构是树形结构,需要“拍平”成abc.xyz格式的key;
  2. SnakeYaml默认会自动转换int、boolean等value,需要禁用自动转换,把所有value均按String类型返回。
5.1创建BeanDefinition

现在,我们可以用ResourceResolver扫描Class,用PropertyResolver获取配置,下面,我们开始实现IoC容器。

在IoC容器中,每个Bean都有一个唯一的名字标识。Spring还允许为一个Bean定义多个名字,这里我们简化一下,一个Bean只允许一个名字,因此,很容易想到用一个Map<String, Object>保存所有的Bean:

public class AnnotationConfigApplicationContext {

    Map<String, Object> beans;

}

这么做不是不可以,但是丢失了大量Bean的定义信息,不便于我们创建Bean以及解析依赖关系。合理的方式是先定义BeanDefinition,它能从Annotation中提取到足够的信息,便于后续创建Bean、设置依赖、调用初始化方法等:

public class BeanDefinition {

    // 全局唯一的Bean Name:

    String name;



    // Bean的声明类型:

    Class<?> beanClass;



    // Bean的实例:

    Object instance = null;



    // 构造方法/null:

    Constructor<?> constructor;



    // 工厂方法名称/null:

    String factoryName;



    // 工厂方法/null:

    Method factoryMethod;



    // Bean的顺序:

    int order;



    // 是否标识@Primary:

    boolean primary;



    // init/destroy方法名称:

    String initMethodName;

    String destroyMethodName;



    // init/destroy方法:

    Method initMethod;

    Method destroyMethod;

}

对于自己定义的带@Component注解的Bean,我们需要获取Class类型,获取构造方法来创建Bean,然后收集@PostConstruct和@PreDestroy标注的初始化与销毁的方法,以及其他信息,如@Order定义Bean的内部排序顺序,@Primary定义存在多个相同类型时返回哪个“主要”Bean。一个典型的定义如下:

@Componentpublic class Hello {

    @PostConstruct

    void init() {}



    @PreDestroy

    void destroy() {}

}

对于@Configuration定义的@Bean方法,我们把它看作Bean的工厂方法,我们需要获取方法返回值作为Class类型,方法本身作为创建Bean的factoryMethod,然后收集@Bean定义的initMethod和destroyMethod标识的初始化于销毁的方法名,以及其他@Order、@Primary等信息。一个典型的定义如下:

@Configurationpublic class AppConfig {

    @Bean(initMethod="init", destroyMethod="close")

    DataSource createDataSource() {

        return new HikariDataSource(...);

    }

}

6.Bean的声明类型

这里我们要特别注意一点,就是Bean的声明类型。对于@Component定义的Bean,它的声明类型就是其Class本身。然而,对于用@Bean工厂方法创建的Bean,它的声明类型与实际类型不一定是同一类型。上述createDataSource()定义的Bean,声明类型是DataSource,实际类型却是某个子类,例如HikariDataSource,因此要特别注意,我们在BeanDefinition中,存储的beanClass是声明类型,实际类型不必存储,因为可以通过instance.getClass()获得:

public class BeanDefinition {

    // Bean的声明类型:

    Class<?> beanClass;



    // Bean的实例:

    Object instance = null;

}

这也引出了下一个问题:如果我们按照名字查找Bean或BeanDefinition,要么拿到唯一实例,要么不存在,即通过查询Map<String, BeanDefinition>即可完成:

public class AnnotationConfigApplicationContext {

    Map<String, BeanDefinition> beans;



    // 根据Name查找BeanDefinition,如果Name不存在,返回null

    @Nullable

    public BeanDefinition findBeanDefinition(String name) {

        return this.beans.get(name);

    }

}

但是通过类型查找Bean或BeanDefinition,我们没法定义一个Map<Class, BeanDefinition>,原因就是Bean的声明类型与实际类型不一定相符,举个例子:

@Configurationpublic class AppConfig {

    @Bean

    AtomicInteger counter() {

        return new AtomicInteger();

    }

    

    @Bean

    Number bigInt() {

        return new BigInteger("1000000000");

    }

}

当我们调用getBean(AtomicInteger.class)时,我们会获得counter()方法创建的唯一实例,但是,当我们调用getBean(Number.class)时,counter()方法和bigInt()方法创建的实例均符合要求,此时,如果有且仅有一个标注了@Primary,就返回标注了@Primary的Bean,否则,直接报NoUniqueBeanDefinitionException错误。

因此,对于getBean(Class)方法,必须遍历找出所有符合类型的Bean,如果不唯一,再判断@Primary,才能返回唯一Bean或报错。

我们编写一个找出所有类型的findBeanDefinitions(Class)方法如下:

// 根据Type查找若干个BeanDefinition,返回0个或多个:

List<BeanDefinition> findBeanDefinitions(Class<?> type) {

    return this.beans.values().stream()

        // 按类型过滤:

        .filter(def -> type.isAssignableFrom(def.getBeanClass()))

        // 排序:

        .sorted().collect(Collectors.toList());

    }

}

我们再编写一个findBeanDefinition(Class)方法如下:

// 根据Type查找某个BeanDefinition,如果不存在返回null,如果存在多个返回@Primary标注的一个:

@Nullablepublic BeanDefinition findBeanDefinition(Class<?> type) {

    List<BeanDefinition> defs = findBeanDefinitions(type);

    if (defs.isEmpty()) { // 没有找到任何BeanDefinition

        return null;

    }

    if (defs.size() == 1) { // 找到唯一一个

        return defs.get(0);

    }

    // 多于一个时,查找@Primary:

    List<BeanDefinition> primaryDefs = defs.stream().filter(def -> def.isPrimary()).collect(Collectors.toList());

    if (primaryDefs.size() == 1) { // @Primary唯一

        return primaryDefs.get(0);

    }

    if (primaryDefs.isEmpty()) { // 不存在@Primary

        throw new NoUniqueBeanDefinitionException(String.format("Multiple bean with type '%s' found, but no @Primary specified.", type.getName()));

    } else { // @Primary不唯一

        throw new NoUniqueBeanDefinitionException(String.format("Multiple bean with type '%s' found, and multiple @Primary specified.", type.getName()));

    }

}

现在,我们已经定义好了数据结构,下面开始获取所有BeanDefinition信息,实际分两步:

public class AnnotationConfigApplicationContext {

    Map<String, BeanDefinition> beans;



    public AnnotationConfigApplicationContext(Class<?> configClass, PropertyResolver propertyResolver) {

        // 扫描获取所有Bean的Class类型:

        Set<String> beanClassNames = scanForClassNames(configClass);



        // 创建Bean的定义:

        this.beans = createBeanDefinitions(beanClassNames);

    }

    ...

}

第一步是扫描指定包下的所有Class,然后返回Class名字,这一步比较简单:

Set<String> scanForClassNames(Class<?> configClass) {

    // 获取@ComponentScan注解:

    ComponentScan scan = ClassUtils.findAnnotation(configClass, ComponentScan.class);

    // 获取注解配置的package名字,未配置则默认当前类所在包:

    String[] scanPackages = scan == null || scan.value().length == 0 ? new String[] { configClass.getPackage().getName() } : scan.value();



    Set<String> classNameSet = new HashSet<>();

    // 依次扫描所有包:

    for (String pkg : scanPackages) {

        logger.atDebug().log("scan package: {}", pkg);

        // 扫描一个包:

        var rr = new ResourceResolver(pkg);

        List<String> classList = rr.scan(res -> {

            // 遇到以.class结尾的文件,就将其转换为Class全名:

            String name = res.name();

            if (name.endsWith(".class")) {

                return name.substring(0, name.length() - 6).replace("/", ".").replace("\\", ".");

            }

            return null;

        });

        // 扫描结果添加到Set:

        classNameSet.addAll(classList);

    }



    // 继续查找@Import(Xyz.class)导入的Class配置:

    Import importConfig = configClass.getAnnotation(Import.class);

    if (importConfig != null) {

        for (Class<?> importConfigClass : importConfig.value()) {

            String importClassName = importConfigClass.getName();

            classNameSet.add(importClassName);

        }

    }

    return classNameSet;

}

注意到扫描结果是指定包的所有Class名称,以及通过@Import导入的Class名称,下一步才会真正处理各种注解:

Map<String, BeanDefinition> createBeanDefinitions(Set<String> classNameSet) {

    Map<String, BeanDefinition> defs = new HashMap<>();

    for (String className : classNameSet) {

        // 获取Class:

        Class<?> clazz = null;

        try {

            clazz = Class.forName(className);

        } catch (ClassNotFoundException e) {

            throw new BeanCreationException(e);

        }

        // 是否标注@Component?

        Component component = ClassUtils.findAnnotation(clazz, Component.class);

        if (component != null) {

            // 获取Bean的名称:

            String beanName = ClassUtils.getBeanName(clazz);

            var def = new BeanDefinition(

                beanName, clazz, getSuitableConstructor(clazz),

                getOrder(clazz), clazz.isAnnotationPresent(Primary.class),

                // init/destroy方法名称:

                null, null,

                // 查找@PostConstruct方法:

                ClassUtils.findAnnotationMethod(clazz, PostConstruct.class),

                // 查找@PreDestroy方法:

                ClassUtils.findAnnotationMethod(clazz, PreDestroy.class));

            addBeanDefinitions(defs, def);

            // 查找是否有@Configuration:

            Configuration configuration = ClassUtils.findAnnotation(clazz, Configuration.class);

            if (configuration != null) {

                // 查找@Bean方法:

                scanFactoryMethods(beanName, clazz, defs);

            }

        }

    }

    return defs;

}

上述代码需要注意的一点是,查找@Component时,并不是简单地在Class定义查看@Component注解,因为Spring的@Component是可以扩展的,例如,标记为Controller的Class也符合要求:

@Controllerpublic class MvcController {...}

原因就在于,@Controller注解的定义包含了@Component:

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpublic @interface Controller {

    String value() default "";

}

所以,判断是否存在@Component,不但要在当前类查找@Component,还要在当前类的所有注解上,查找该注解是否有@Component,因此,我们编写了一个能递归查找注解的方法:

public class ClassUtils {

    public static <A extends Annotation> A findAnnotation(Class<?> target, Class<A> annoClass) {

        A a = target.getAnnotation(annoClass);

        for (Annotation anno : target.getAnnotations()) {

            Class<? extends Annotation> annoType = anno.annotationType();

            if (!annoType.getPackageName().equals("java.lang.annotation")) {

                A found = findAnnotation(annoType, annoClass);

                if (found != null) {

                    if (a != null) {

                        throw new BeanDefinitionException("Duplicate @" + annoClass.getSimpleName() + " found on class " + target.getSimpleName());

                    }

                    a = found;

                }

            }

        }

        return a;

    }

}

带有@Configuration注解的Class,视为Bean的工厂,我们需要继续在scanFactoryMethods()中查找@Bean标注的方法:

void scanFactoryMethods(String factoryBeanName, Class<?> clazz, Map<String, BeanDefinition> defs) {

    for (Method method : clazz.getDeclaredMethods()) {

        // 是否带有@Bean标注:

        Bean bean = method.getAnnotation(Bean.class);

        if (bean != null) {

            // Bean的声明类型是方法返回类型:

            Class<?> beanClass = method.getReturnType();

            var def = new BeanDefinition(

                ClassUtils.getBeanName(method), beanClass,

                factoryBeanName,

                // 创建Bean的工厂方法:

                method,

                // @Order

                getOrder(method),

                // 是否存在@Primary标注?

                method.isAnnotationPresent(Primary.class),

                // init方法名称:

                bean.initMethod().isEmpty() ? null : bean.initMethod(),

                // destroy方法名称:

                bean.destroyMethod().isEmpty() ? null : bean.destroyMethod(),

                // @PostConstruct / @PreDestroy方法:

                null, null);

            addBeanDefinitions(defs, def);

        }

    }

}

注意到@Configuration注解本身又用@Component注解修饰了,因此,对于一个@Configuration来说:

@Configurationpublic class DateTimeConfig {

    @Bean

    LocalDateTime local() { return LocalDateTime.now(); }



    @Bean

    ZonedDateTime zoned() { return ZonedDateTime.now(); }

}

实际上创建了3个BeanDefinition:

  • DateTimeConfig本身;
  • LocalDateTime;
  • ZonedDateTime。

不创建DateTimeConfig行不行?不行,因为后续没有DateTimeConfig的实例,无法调用local()和zoned()方法。因为当前我们只创建了BeanDefinition,所以对于LocalDateTime和ZonedDateTime的BeanDefinition来说,还必须保存DateTimeConfig的名字,将来才能通过名字查找DateTimeConfig的实例。

有的同学注意到我们同时存储了initMethodName和initMethod,以及destroyMethodName和destroyMethod,这是因为在@Component声明的Bean中,我们可以根据@PostConstruct和@PreDestroy直接拿到Method本身,而在@Bean声明的Bean中,我们拿不到Method,只能从@Bean注解提取出字符串格式的方法名称,因此,存储在BeanDefinition的方法名称与方法,其中总有一个为null。

最后,仔细编写BeanDefinition的toString()方法,使之能打印出详细的信息。我们编写测试,运行,打印出每个BeanDefinition如下:

define bean: BeanDefinition [name=annotationDestroyBean, beanClass=com.itranswarp.scan.destroy.AnnotationDestroyBean, factory=null, init-method=null, destroy-method=destroy, primary=false, instance=null]



define bean: BeanDefinition [name=nestedBean, beanClass=com.itranswarp.scan.nested.OuterBean$NestedBean, factory=null, init-method=null, destroy-method=null, primary=false, instance=null]



define bean: BeanDefinition [name=createSpecifyInitBean, beanClass=com.itranswarp.scan.init.SpecifyInitBean, factory=SpecifyInitConfiguration.createSpecifyInitBean(String, String), init-method=null, destroy-method=null, primary=false, instance=null]



...

现在,我们已经能扫描并创建所有的BeanDefinition,只是目前每个BeanDefinition内部的instance还是null,因为我们后续才会创建真正的Bean。

7.创建Bean实例

当我们拿到所有BeanDefinition之后,就可以开始创建Bean的实例了。

在创建Bean实例之前,我们先看看Spring支持的4种依赖注入模式:

  1. 构造方法注入,例如:
@Componentpublic class Hello {

    JdbcTemplate jdbcTemplate;

    public Hello(@Autowired JdbcTemplate jdbcTemplate) {

        this.jdbcTemplate = jdbcTemplate;

    }

}
  1. 工厂方法注入,例如:
@Configurationpublic class AppConfig {

    @Bean

    Hello hello(@Autowired JdbcTemplate jdbcTemplate) {

        return new Hello(jdbcTemplate);

    }

}
  1. Setter方法注入,例如:
@Componentpublic class Hello {

    JdbcTemplate jdbcTemplate;



    @Autowired

    void setJdbcTemplate(JdbcTemplate jdbcTemplate) {

        this.jdbcTemplate = jdbcTemplate;

    }

}
  1. 字段注入,例如:
@Componentpublic class Hello {

    @Autowired

    JdbcTemplate jdbcTemplate;

}

然而我们仔细分析,发现这4种注入方式实际上是有区别的。

区别就在于,前两种方式,即构造方法注入和工厂方法注入,Bean的创建与注入是一体的,我们无法把它们分成两个阶段,因为无法中断方法内部代码的执行。而后两种方式,即Setter方法注入和属性注入,Bean的创建与注入是可以分开的,即先创建Bean实例,再用反射调用方法或字段,完成注入。

我们再分析一下循环依赖的问题。循环依赖,即A、B互相依赖,或者A依赖B,B依赖C,C依赖A,形成了一个闭环。IoC容器对Bean进行管理,可以解决部分循环依赖问题,但不是所有循环依赖都能解决。

我们先来看不能解决的循环依赖问题,假定下列代码定义的A、B两个Bean:

class A {

    final B b;

    A(B b) { this.b = b; }

}

class B {

    final A a;

    B(A a) { this.a = a; }

}

这种通过构造方法注入依赖的两个Bean,如果存在循环依赖,是无解的,因为我们不用IoC,自己写Java代码也写不出正确创建两个Bean实例的代码。

因此,我们把构造方法注入和工厂方法注入的依赖称为强依赖,不能有强依赖的循环依赖,否则只能报错。

后两种注入方式形成的依赖则是弱依赖,假定下列代码定义的A、B两个Bean:

class A {

    B b;

}

class B {

    A a;

}

这种循环依赖则很容易解决,因为我们可以分两步,先分别实例化Bean,再注入依赖:

// 第一步,实例化:

A a = new A();

B b = new B();

// 第二步,注入:

a.b = b;

b.a = a;

所以,对于IoC容器来说,创建Bean的过程分两步:

  1. 创建Bean的实例,此时必须注入强依赖;
  2. 对Bean实例进行Setter方法注入和字段注入。

第一步如果遇到循环依赖则直接报错,第二步则不需要关心有没有循环依赖。

我们先实现第一步:创建Bean的实例,同时注入强依赖。

在上一节代码中,我们已经获得了所有的BeanDefinition:

public class AnnotationConfigApplicationContext {

    PropertyResolver propertyResolver;

    Map<String, BeanDefinition> beans;



    public AnnotationConfigApplicationContext(Class<?> configClass, PropertyResolver propertyResolver) {

        this.propertyResolver = propertyResolver;

        // 扫描获取所有Bean的Class类型:

        Set<String> beanClassNames = scanForClassNames(configClass);

        // 创建Bean的定义:

        this.beans = createBeanDefinitions(beanClassNames);

    }

}

下一步是创建Bean的实例,同时注入强依赖。此阶段必须检测循环依赖。检测循环依赖其实非常简单,就是定义一个Set<String>跟踪当前正在创建的所有Bean的名称:

public class AnnotationConfigApplicationContext {

    Set<String> creatingBeanNames;

    ...

}

创建Bean实例我们用方法createBeanAsEarlySingleton()实现,在方法开始处检测循环依赖:

// 创建一个Bean,但不进行字段和方法级别的注入。如果创建的Bean不是Configuration,则在构造方法/工厂方法中注入的依赖Bean会自动创建public Object createBeanAsEarlySingleton(BeanDefinition def) {

    if (!this.creatingBeanNames.add(def.getName())) {

        // 检测到重复创建Bean导致的循环依赖:

        throw new UnsatisfiedDependencyException();

    }

    ...

}

由于@Configuration标识的Bean实际上是工厂,它们必须先实例化,才能实例化其他普通Bean,所以我们先把@Configuration标识的Bean创建出来,再创建普通Bean:

public AnnotationConfigApplicationContext(Class<?> configClass, PropertyResolver propertyResolver) {

    this.propertyResolver = propertyResolver;

    // 扫描获取所有Bean的Class类型:

    Set<String> beanClassNames = scanForClassNames(configClass);

    // 创建Bean的定义:

    this.beans = createBeanDefinitions(beanClassNames);



    // 创建BeanName检测循环依赖:

    this.creatingBeanNames = new HashSet<>();



    // 创建@Configuration类型的Bean:

    this.beans.values().stream()

            // 过滤出@Configuration:

            .filter(this::isConfigurationDefinition).sorted().map(def -> {

                // 创建Bean实例:

                createBeanAsEarlySingleton(def);

                return def.getName();

            }).collect(Collectors.toList());



    // 创建其他普通Bean:

    List<BeanDefinition> defs = this.beans.values().stream()

            // 过滤出instance==null的BeanDefinition:

            .filter(def -> def.getInstance() == null)

            .sorted().collect(Collectors.toList());

    // 依次创建Bean实例:

    defs.forEach(def -> {

        // 如果Bean未被创建(可能在其他Bean的构造方法注入前被创建):

        if (def.getInstance() == null) {

            // 创建Bean:

            createBeanAsEarlySingleton(def);

        }

    });

}

剩下的工作就是把createBeanAsEarlySingleton()补充完整:

public Object createBeanAsEarlySingleton(BeanDefinition def) {

    // 检测循环依赖:

    if (!this.creatingBeanNames.add(def.getName())) {

        throw new UnsatisfiedDependencyException();

    }



    // 创建方式:构造方法或工厂方法:

    Executable createFn = def.getFactoryName() == null ?

        def.getConstructor() : def.getFactoryMethod();



    // 创建参数:

    Parameter[] parameters = createFn.getParameters();

    Object[] args = new Object[parameters.length];

    for (int i = 0; i < parameters.length; i++) {

        // 从参数获取@Value和@Autowired:

        Value value = ...

        Autowired autowired = ...

        // 检查Value和Autowired

        ...

        // 参数类型:

        Class<?> type = param.getType();

        if (value != null) {

            // 参数设置为查询的@Value:

            args[i] = this.propertyResolver.getRequiredProperty(value.value(), type);

        } else {

            // 参数是@Autowired,查找依赖的BeanDefinition:

            BeanDefinition dependsOnDef = name.isEmpty() ? findBeanDefinition(type) : findBeanDefinition(name, type);

            // 获取依赖Bean的实例:

            Object autowiredBeanInstance = dependsOnDef.getInstance();

            if (autowiredBeanInstance == null) {

                // 当前依赖Bean尚未初始化,递归调用初始化该依赖Bean:

                autowiredBeanInstance = createBeanAsEarlySingleton(dependsOnDef);

            }

            // 参数设置为依赖的Bean实例:

            args[i] = autowiredBeanInstance;

        }

    }

    // 已拿到所有方法参数,创建Bean实例:

    Object instance = ...

    // 设置实例到BeanDefinition:

    def.setInstance(instance);

    // 返回实例:

    return def.getInstance();

}

注意到递归调用:

public Object createBeanAsEarlySingleton(BeanDefinition def) {

    ...

    Object[] args = new Object[parameters.length];

    for (int i = 0; i < parameters.length; i++) {

        ...

        // 获取依赖Bean的实例:

        Object autowiredBeanInstance = dependsOnDef.getInstance();

        if (autowiredBeanInstance == null && !isConfiguration) {

            // 当前依赖Bean尚未初始化,递归调用初始化该依赖Bean:

            autowiredBeanInstance = createBeanAsEarlySingleton(dependsOnDef);

        }

        ...

    }

    ...

}

假设如下的Bean依赖:

@Componentclass A {

    // 依赖B,C:

    A(@Autowired B, @Autowired C) {}

}

@Componentclass B {

    // 依赖C:

    B(@Autowired C) {}

}

@Componentclass C {

    // 无依赖:

    C() {}

}

如果按照A、B、C的顺序创建Bean实例,那么系统流程如下:

  1. 准备创建A;
  2. 检测到依赖B:未就绪;
    1. 准备创建B:
    2. 检测到依赖C:未就绪;
      1. 准备创建C;
      2. 完成创建C;
    3. 完成创建B;
  3. 检测到依赖C,已就绪;
  4. 完成创建A。

如果按照B、C、A的顺序创建Bean实例,那么系统流程如下:

  1. 准备创建B;
  2. 检测到依赖C:未就绪;
    1. 准备创建C;
    2. 完成创建C;
  3. 完成创建B;
  4. 准备创建A;
  5. 检测到依赖B,已就绪;
  6. 检测到依赖C,已就绪;
  7. 完成创建A。

可见无论以什么顺序创建,C总是最先被实例化,A总是最后被实例化。

8.初始化Bean

在创建Bean实例的过程中,我们已经完成了强依赖的注入。下一步,是根据Setter方法和字段完成弱依赖注入,接着调用用@PostConstruct标注的init方法,就完成了所有Bean的初始化。

这一步相对比较简单,因为只涉及到查找依赖的@Value和@Autowired,然后用反射完成调用即可:

public AnnotationConfigApplicationContext(Class<?> configClass, PropertyResolver propertyResolver) {

    ...



    // 通过字段和set方法注入依赖:

    this.beans.values().forEach(def -> {

        injectBean(def);

    });



    // 调用init方法:

    this.beans.values().forEach(def -> {

        initBean(def);

    });

}

使用Setter方法和字段注入时,要注意一点,就是不仅要在当前类查找,还要在父类查找,因为有些@Autowired写在父类,所有子类都可使用,这样更方便。注入弱依赖代码如下:

// 在当前类及父类进行字段和方法注入:

void injectProperties(BeanDefinition def, Class<?> clazz, Object bean) {

    // 在当前类查找Field和Method并注入:

    for (Field f : clazz.getDeclaredFields()) {

        tryInjectProperties(def, clazz, bean, f);

    }

    for (Method m : clazz.getDeclaredMethods()) {

        tryInjectProperties(def, clazz, bean, m);

    }

    // 在父类查找Field和Method并注入:

    Class<?> superClazz = clazz.getSuperclass();

    if (superClazz != null) {

        // 递归调用:

        injectProperties(def, superClazz, bean);

    }

}



// 注入单个属性

void tryInjectProperties(BeanDefinition def, Class<?> clazz, Object bean, AccessibleObject acc) {

    ...

}

弱依赖注入完成后,再循环一遍所有的BeanDefinition,对其调用init方法,完成最后一步初始化:

void initBean(BeanDefinition def) {

    // 调用init方法:

    callMethod(def.getInstance(), def.getInitMethod(), def.getInitMethodName());

}

处理@PreDestroy方法更简单,在ApplicationContext关闭时遍历所有Bean,调用destroy方法即可。

9.实现BeanPostProcessor

现在,我们已经完成了扫描Class名称、创建BeanDefinition、创建Bean实例、初始化Bean,理论上一个可用的IoC容器就已经就绪。

然而,BeanPostProcessor的出现改变了这一切。Spring允许用户自定义一种特殊的Bean,即实现了BeanPostProcessor接口,它有什么用呢?其实就是替换Bean。我们举个例子,下面的代码是基于Spring代码:

@Configuration@ComponentScanpublic class AppConfig {



    public static void main(String[] args) {

        var ctx = new AnnotationConfigApplicationContext(AppConfig.class);

        // 可以获取到ZonedDateTime:

        ZonedDateTime dt = ctx.getBean(ZonedDateTime.class);

        System.out.println(dt);

        // 错误:NoSuchBeanDefinitionException:

        System.out.println(ctx.getBean(LocalDateTime.class));

    }



    // 创建LocalDateTime实例

    @Bean

    public LocalDateTime localDateTime() {

        return LocalDateTime.now();

    }



    // 实现一个BeanPostProcessor

    @Bean

    BeanPostProcessor replaceLocalDateTime() {

        return new BeanPostProcessor() {

            @Override

            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

                // 将LocalDateTime类型实例替换为ZonedDateTime类型实例:

                if (bean instanceof LocalDateTime) {

                    return ZonedDateTime.now();

                }

                return bean;

            }

        };

    }

}

运行可知,我们定义的@Bean类型明明是LocalDateTime类型,但却被另一个BeanPostProcessor替换成了ZonedDateTime,于是,调用getBean(ZonedDateTime.class)可以拿到替换后的Bean,调用getBean(LocalDateTime.class)会报错,提示找不到Bean。那么原始的Bean哪去了?答案是被BeanPostProcessor扔掉了。

可见,BeanPostProcessor是一种特殊Bean,它的作用是根据条件替换某些Bean。上述的例子中,LocalDateTime被替换为ZonedDateTime其实没啥意义,但实际应用中,把原始Bean替换为代理后的Bean是非常常见的,比如下面的基于Spring的代码:

@Configuration@ComponentScanpublic class AppConfig {



    public static void main(String[] args) {

        var ctx = new AnnotationConfigApplicationContext(AppConfig.class);

        UserService u = ctx.getBean(UserService.class);

        System.out.println(u.getClass().getSimpleName()); // UserServiceProxy

        u.register("bob@example.com", "bob12345");

    }



    @Bean

    BeanPostProcessor createProxy() {

        return new BeanPostProcessor() {

            @Override

            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

                // 实现事务功能:

                if (bean instanceof UserService u) {

                    return new UserServiceProxy(u);

                }

                return bean;

            }

        };

    }

}

@Componentclass UserService {

    public void register(String email, String password) {

        System.out.println("INSERT INTO ...");

    }

}

// 代理类:class UserServiceProxy extends UserService {

    UserService target;



    public UserServiceProxy(UserService target) {

        this.target = target;

    }



    @Override

    public void register(String email, String password) {

        System.out.println("begin tx");

        target.register(email, password);

        System.out.println("commit tx");

    }

}

如果执行上述代码,打印出的Bean类型不是UserService,而是UserServiceProxy,因此,调用register()会打印出begin tx和commit tx,说明“事务”生效了。

迄今为止,创建Proxy似乎没有什么影响。让我们把代码再按实际情况扩展一下,UserService是用户编写的业务代码,需要注入JdbcTemplate:

@Componentclass UserService {

    @Autowired JdbcTemplate jdbcTemplate;

    

    public void register(String email, String password) {

        jdbcTemplate.update("INSERT INTO ...");

    }

}

而PostBeanProcessor一般由框架本身提供事务功能,所以它会动态创建一个UserServiceProxy:

class UserServiceProxy extends UserService {

    UserService target;



    public UserServiceProxy(UserService target) {

        this.target = target;

    }



    @Override

    public void register(String email, String password) {

        System.out.println("begin tx");

        target.register(email, password);

        System.out.println("commit tx");

    }

}

调用用户注册的页面由MvcController控制,因此,将UserService注入到MvcController:

@Controllerclass MvcController {

    @Autowired UserService userService;

    

    @PostMapping("/register")

    void register() {

        userService.register(...);

    }

}

一开始,由IoC容器创建的Bean包括:

  • JdbcTemplate
  • UserService
  • MvcController

接着,由于BeanPostProcessor的介入,原始的UserService被替换为UserServiceProxy:

  • JdbcTemplate
  • UserServiceProxy
  • MvcController

那么问题来了:注意到UserServiceProxy是从UserService继承的,它也有一个@Autowired JdbcTemplate,那JdbcTemplate实例应注入到原始的UserService还是UserServiceProxy?

从业务逻辑出发,JdbcTemplate实例必须注入到原始的UserService,否则,代理类UserServiceProxy执行target.register()时,相当于对原始的UserService调用register()方法,如果JdbcTemplate没有注入,将直接报NullPointerException错误。

这时第二个问题又来了:MvcController需要注入的UserService,应该是原始的UserService还是UserServiceProxy?

还是从业务逻辑出发,MvcController需要注入的UserService必须是UserServiceProxy,否则,事务不起作用。

我们用图描述一下注入关系:

┌───────────────┐
│MvcController  │
├───────────────┤   ┌────────────────┐
│- userService ─┼─▶│UserServiceProxy│
└───────────────┘   ├────────────────┤
                    │- jdbcTemplate  │
                    ├────────────────┤   ┌────────────────┐
                    │- target       ─┼─▶│UserService     │
                    └────────────────┘   ├────────────────┤   ┌────────────┐
                                         │- jdbcTemplate ─┼─▶│JdbcTemplate│
                                         └────────────────┘   └────────────┘

注意到上图的UserService已经脱离了IoC容器的管理,因为此时UserService对应的BeanDefinition中,存放的instance是UserServiceProxy。

可见,引入BeanPostProcessor可以实现Proxy机制,但也让依赖注入变得更加复杂。

但是我们仔细分析依赖关系,还是可以总结出两条原则:

  1. 一个Bean如果被Proxy替换,则依赖它的Bean应注入Proxy,即上图的MvcController应注入UserServiceProxy;
  2. 一个Bean如果被Proxy替换,如果要注入依赖,则应该注入到原始对象,即上图的JdbcTemplate应注入到原始的UserService。

基于这个原则,要满足条件1是很容易的,因为只要创建Bean完成后,立刻调用BeanPostProcessor就实现了替换,后续其他Bean引用的肯定就是Proxy了。先改造创建Bean的流程,在创建@Configuration后,接着创建BeanPostProcessor,再创建其他普通Bean:

public AnnotationConfigApplicationContext(Class<?> configClass, PropertyResolver propertyResolver) {

    ...

    // 创建@Configuration类型的Bean:

    this.beans.values().stream()

            // 过滤出@Configuration:

            .filter(this::isConfigurationDefinition).sorted().map(def -> {

                createBeanAsEarlySingleton(def);

                return def.getName();

            }).collect(Collectors.toList());



    // 创建BeanPostProcessor类型的Bean:

    List<BeanPostProcessor> processors = this.beans.values().stream()

            // 过滤出BeanPostProcessor:

            .filter(this::isBeanPostProcessorDefinition)

            // 排序:

            .sorted()

            // 创建BeanPostProcessor实例:

            .map(def -> {

                return (BeanPostProcessor) createBeanAsEarlySingleton(def);

            }).collect(Collectors.toList());

    this.beanPostProcessors.addAll(processors);



    // 创建其他普通Bean:

    createNormalBeans();

    ...

}

再继续修改createBeanAsEarlySingleton(),创建Bean实例后,调用BeanPostProcessor处理:

public Object createBeanAsEarlySingleton(BeanDefinition def) {

    ...



    // 创建Bean实例:

    Object instance = ...;

    def.setInstance(instance);



    // 调用BeanPostProcessor处理Bean:

    for (BeanPostProcessor processor : beanPostProcessors) {

        Object processed = processor.postProcessBeforeInitialization(def.getInstance(), def.getName());

        // 如果一个BeanPostProcessor替换了原始Bean,则更新Bean的引用:

        if (def.getInstance() != processed) {

            def.setInstance(processed);

        }

    }

    return def.getInstance();

}

现在,如果一个Bean被替换为Proxy,那么BeanDefinition中的instance已经是Proxy了,这时,对这个Bean进行依赖注入会有问题,因为注入的是Proxy而不是原始Bean,怎么办?

这时我们要思考原始Bean去哪了?原始Bean实际上是被BeanPostProcessor给丢了!如果BeanPostProcessor能保存原始Bean,那么,注入前先找到原始Bean,就可以把依赖正确地注入给原始Bean。我们给BeanPostProcessor加一个postProcessOnSetProperty()方法,让它返回原始Bean:

public interface BeanPostProcessor {

    // 注入依赖时,应该使用的Bean实例:

    default Object postProcessOnSetProperty(Object bean, String beanName) {

        return bean;

    }

}

再继续把injectBean()改一下,不要直接拿BeanDefinition.getInstance(),而是拿到原始Bean:

void injectBean(BeanDefinition def) {

    // 获取Bean实例,或被代理的原始实例:

    Object beanInstance = getProxiedInstance(def);

    try {

        injectProperties(def, def.getBeanClass(), beanInstance);

    } catch (ReflectiveOperationException e) {

        throw new BeanCreationException(e);

    }

}

getProxiedInstance()就是为了获取原始Bean:

Object getProxiedInstance(BeanDefinition def) {

    Object beanInstance = def.getInstance();

    // 如果Proxy改变了原始Bean,又希望注入到原始Bean,则由BeanPostProcessor指定原始Bean:

    List<BeanPostProcessor> reversedBeanPostProcessors = new ArrayList<>(this.beanPostProcessors);

    Collections.reverse(reversedBeanPostProcessors);

    for (BeanPostProcessor beanPostProcessor : reversedBeanPostProcessors) {

        Object restoredInstance = beanPostProcessor.postProcessOnSetProperty(beanInstance, def.getName());

        if (restoredInstance != beanInstance) {

            beanInstance = restoredInstance;

        }

    }

    return beanInstance;

}

这里我们还能处理多次代理的情况,即一个原始Bean,比如UserService,被一个事务处理的BeanPostProcsssor代理为UserServiceTx,又被一个性能监控的BeanPostProcessor代理为UserServiceMetric,还原的时候,对BeanPostProcsssor做一个倒序,先还原为UserServiceTx,再还原为UserService。

测试

我们可以写一个测试来验证Bean的注入是否正确。先定义原始Bean:

@Componentpublic class OriginBean {

    @Value("${app.title}")

    public String name;



    @Value("${app.version}")

    public String version;



    public String getName() {

        return name;

    }

}

通过FirstProxyBeanPostProcessor代理为FirstProxyBean:

@Order(100)@Componentpublic class FirstProxyBeanPostProcessor implements BeanPostProcessor {

    // 保存原始Bean:

    Map<String, Object> originBeans = new HashMap<>();



    @Override

    public Object postProcessBeforeInitialization(Object bean, String beanName) {

        if (OriginBean.class.isAssignableFrom(bean.getClass())) {

            // 检测到OriginBean,创建FirstProxyBean:

            var proxy = new FirstProxyBean((OriginBean) bean);

            // 保存原始Bean:

            originBeans.put(beanName, bean);

            // 返回Proxy:

            return proxy;

        }

        return bean;

    }



    @Override

    public Object postProcessOnSetProperty(Object bean, String beanName) {

        Object origin = originBeans.get(beanName);

        if (origin != null) {

            // 存在原始Bean时,返回原始Bean:

            return origin;

        }

        return bean;

    }

}

// 代理Bean:class FirstProxyBean extends OriginBean {

    final OriginBean target;



    public FirstProxyBean(OriginBean target) {

        this.target = target;

    }

}

通过SecondProxyBeanPostProcessor代理为SecondProxyBean:

@Order(200)@Componentpublic class SecondProxyBeanPostProcessor implements BeanPostProcessor {

    // 保存原始Bean:

    Map<String, Object> originBeans = new HashMap<>();



    @Override

    public Object postProcessBeforeInitialization(Object bean, String beanName) {

        if (OriginBean.class.isAssignableFrom(bean.getClass())) {

            // 检测到OriginBean,创建SecondProxyBean:

            var proxy = new SecondProxyBean((OriginBean) bean);

            // 保存原始Bean:

            originBeans.put(beanName, bean);

            // 返回Proxy:

            return proxy;

        }

        return bean;

    }



    @Override

    public Object postProcessOnSetProperty(Object bean, String beanName) {

        Object origin = originBeans.get(beanName);

        if (origin != null) {

            // 存在原始Bean时,返回原始Bean:

            return origin;

        }

        return bean;

    }

}

// 代理Bean:class SecondProxyBean extends OriginBean {

    final OriginBean target;



    public SecondProxyBean(OriginBean target) {

        this.target = target;

    }

}

定义一个Bean,用于检测是否注入了Proxy:

@Componentpublic class InjectProxyOnConstructorBean {

    public final OriginBean injected;



    public InjectProxyOnConstructorBean(@Autowired OriginBean injected) {

        this.injected = injected;

    }

}

测试代码如下:

var ctx = new AnnotationConfigApplicationContext(ScanApplication.class, createPropertyResolver());

// 获取OriginBean的实例,此处获取的应该是SendProxyBeanProxy:OriginBean proxy = ctx.getBean(OriginBean.class);

assertSame(SecondProxyBean.class, proxy.getClass());

// proxy的name和version字段并没有被注入:

assertNull(proxy.name);

assertNull(proxy.version);

// 但是调用proxy的getName()会最终调用原始Bean的getName(),从而返回正确的值:

assertEquals("Scan App", proxy.getName());

// 获取InjectProxyOnConstructorBean实例:

var inject = ctx.getBean(InjectProxyOnConstructorBean.class);// 注入的OriginBean应该为Proxy,而且和前面返回的proxy是同一实例:

assertSame(proxy, inject.injected);

从上面的测试代码我们也能看出,对于使用Proxy模式的Bean来说,正常的方法调用对用户是透明的,但是,直接访问Bean注入的字段,如果获取的是Proxy,则字段全部为null,因为注入并没有发生在Proxy,而是原始Bean。这也是为什么当我们需要访问某个注入的Bean时,总是调用方法而不是直接访问字段:

@Componentpublic class MailService {

    @Autowired

    UserService userService;



    public String sendMail() {

        // 错误:不要直接访问UserService的字段,因为如果UserService被代理,则返回null:

        ZoneId zoneId = userService.zoneId;

        // 正确:通过方法访问UserService的字段,无论是否被代理,返回值均是正确的:

        ZoneId zoneId = userService.getZoneId();

        ...

    }

}

10.完成IoC容器

现在,我们已经完成了IoC容器的基本功能。最后的收尾工作主要是提取接口。先定义给用户使用的ApplicationContext接口:

public interface ApplicationContext extends AutoCloseable {



    // 是否存在指定name的Bean?

    boolean containsBean(String name);



    // 根据name返回唯一Bean,未找到抛出NoSuchBeanDefinitionException

    <T> T getBean(String name);



    // 根据name返回唯一Bean,未找到抛出NoSuchBeanDefinitionException

    <T> T getBean(String name, Class<T> requiredType);



    // 根据type返回唯一Bean,未找到抛出NoSuchBeanDefinitionException

    <T> T getBean(Class<T> requiredType);



    // 根据type返回一组Bean,未找到返回空List

    <T> List<T> getBeans(Class<T> requiredType);



    // 关闭并执行所有bean的destroy方法

    void close();

}

再定义一个给Framework级别的代码用的ConfigurableApplicationContext接口:

public interface ConfigurableApplicationContext extends ApplicationContext {



    List<BeanDefinition> findBeanDefinitions(Class<?> type);



    @Nullable

    BeanDefinition findBeanDefinition(Class<?> type);



    @Nullable

    BeanDefinition findBeanDefinition(String name);



    @Nullable

    BeanDefinition findBeanDefinition(String name, Class<?> requiredType);



    Object createBeanAsEarlySingleton(BeanDefinition def);

}

让AnnotationConfigApplicationContext实现接口:

public class AnnotationConfigApplicationContext implements ConfigurableApplicationContext {

    ...

}

顺便在close()方法中把Bean的destroy方法执行了。

最后加一个ApplicationUtils类,目的是能通过getRequiredApplicationContext()方法随时获取到ApplicationContext实例。

搞定summer-context模块!

有的同学可能会问,为什么我们用了不到1000行核心代码,就实现了ApplicationContext?如果查看Spring的源码,可以看到,光是层次结构,就令人眼花缭乱:

BeanFactory

  HierarchicalBeanFactory

    ConfigurableBeanFactory

      AbstractBeanFactory

        AbstractAutowireCapableBeanFactory

          DefaultListableBeanFactory

    ApplicationContext

      ConfigurableApplicationContext

        AbstractApplicationContext

          AbstractRefreshableApplicationContext

            AbstractXmlApplicationContext

              ClassPathXmlApplicationContext

              FileSystemXmlApplicationContext

          GenericApplicationContext

            AnnotationConfigApplicationContext

            GenericXmlApplicationContext

            StaticApplicationContext

其实根本原因是我们大幅简化了需求。Spring最早提供了BeanFactory和ApplicationContext两种容器,前者是懒加载,后者是立刻初始化所有Bean。懒加载的特性会导致依赖注入变得更加复杂,虽然BeanFactory在实际项目中并没有什么卵用。然而一旦发布了接口,处于兼容性考虑,就没法再收回去了。再考虑到Spring最早采用XML配置,后来采用Annotation配置,还允许混合配置,这样一来,早期发布的XmlApplicationContext不能动,新的Annotation配置就必须添加新的实现类,所以,代码的复杂度随着需求增加而增加,保持兼容性又会导致需要更多的代码来实现新功能。

所以,没事不要瞎提需求。

11.实现AOP

实现了IoC容器后,我们继续实现AOP功能。

AOP即Aspect Oriented Programming,面向切面编程,它本质上就是一个Proxy模式,只不过可以让IoC容器在运行时再组合起来,而不是事先自己用Proxy模式写死了。而实现Proxy模式的核心是拦截目标Bean的方法调用。

既然原理是方法拦截,那么AOP的实现方式不外乎以下几种:

  1. 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
  2. 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
  3. 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。

从复杂度看,最简单的是方案3,因为不涉及到任何JVM底层。

方案3又有两种实现方式:

  1. 使用Java标准库的动态代理机制,不过仅支持对接口代理,无法对具体类实现代理;
  2. 使用CGLIB或Javassist这些第三方库,通过动态生成字节码,可以对具体类实现代理。

那么Spring的实现方式是啥?Spring实际上内置了多种代理机制,如果一个Bean声明的类型是接口,那么Spring直接使用Java标准库实现对接口的代理,如果一个Bean声明的类型是Class,那么Spring就使用CGLIB动态生成字节码实现代理。

除了实现代理外,还得有一套机制让用户能定义代理。Spring又提供了多种方式:

  1. 用AspectJ的语法来定义AOP,比如execution(public * com.itranswarp.service.*.*(..));
  2. 用注解来定义AOP,比如用@Transactional表示开启事务。

用表达式匹配,很容易漏掉或者打击面太大。用注解无疑是最简单的,因为这样被装配的Bean自己能清清楚楚地知道自己被安排了。因此,在Summer Framework中,我们只支持Annotation模式的AOP机制,并且采用动态生成字节码的方式实现。

明确了需求,我们来看如何实现动态生成字节码。Spring采用的是CGLIB,因此我们去CGLIB首页看一下,不看不要紧,一看吓一跳:

cglib is unmaintained ... migrating to something like ByteBuddy.

原来CGLIB已经不维护了,建议使用ByteBuddy。既然如此,我们就选择ByteBuddy实现AOP吧。

比较一下Spring Framework和Summer Framework对AOP的支持:

Spring Framework

Summer Framework

AspectJ方式

支持

不支持

Annotation方式

支持

支持

代理接口

支持

不支持

代理类

支持

支持

实现机制

CGLIB

ByteBuddy

下面我们就来准备实现AOP。

12.实现ProxyResolver

为了实现AOP,我们先思考如何在IoC容器中实现一个动态代理。

在IoC容器中,实现动态代理需要用户提供两个Bean:

  1. 原始Bean,即需要被代理的Bean;
  2. 拦截器,即拦截了目标Bean的方法后,会自动调用拦截器实现代理功能。

拦截器需要定义接口,这里我们直接用Java标准库的InvocationHandler,免去了自定义接口。

假定我们已经从IoC容器中获取了原始Bean与实现了InvocationHandler的拦截器Bean,那么就可以编写一个ProxyResolver来实现AOP代理。

ByteBuddy的官网上搜索很容易找到相关代码,我们整理为createProxy()方法:

public class ProxyResolver {

    // ByteBuddy实例:

    ByteBuddy byteBuddy = new ByteBuddy();



    // 传入原始Bean、拦截器,返回代理后的实例:

    public <T> T createProxy(T bean, InvocationHandler handler) {

        // 目标Bean的Class类型:

        Class<?> targetClass = bean.getClass();

        // 动态创建Proxy的Class:

        Class<?> proxyClass = this.byteBuddy

                // 子类用默认无参数构造方法:

                .subclass(targetClass, ConstructorStrategy.Default.DEFAULT_CONSTRUCTOR)

                // 拦截所有public方法:

                .method(ElementMatchers.isPublic()).intercept(InvocationHandlerAdapter.of(

                        // 新的拦截器实例:

                        new InvocationHandler() {

                            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                                // 将方法调用代理至原始Bean:

                                return handler.invoke(bean, method, args);

                            }

                        }))

                // 生成字节码:

                .make()

                // 加载字节码:

                .load(targetClass.getClassLoader()).getLoaded();

        // 创建Proxy实例:

        Object proxy;

        try {

            proxy = proxyClass.getConstructor().newInstance();

        } catch (RuntimeException e) {

            throw e;

        } catch (Exception e) {

            throw new RuntimeException(e);

        }

        return (T) proxy;

    }

}

注意InvocationHandler有两层:外层的invoke()传入的Object是Proxy实例,内层的invoke()将调用转发至原始Bean。

一共大约50行代码,我们就实现了AOP功能。有点不敢相信,赶快写个测试看看效果。

先定义一个OriginBean:

public class OriginBean {

    public String name;



    @Polite

    public String hello() {

        return "Hello, " + name + ".";

    }



    public String morning() {

        return "Morning, " + name + ".";

    }

}

我们要实现的AOP功能是增强带@Polite注解的方法,把返回值Hello, Bob.改为Hello, Bob!,让欢迎气氛更强烈一点,因此,编写一个InvocationHandler:

public class PoliteInvocationHandler implements InvocationHandler {

    @Override

    public Object invoke(Object bean, Method method, Object[] args) throws Throwable {

        // 修改标记了@Polite的方法返回值:

        if (method.getAnnotation(Polite.class) != null) {

            String ret = (String) method.invoke(bean, args);

            if (ret.endsWith(".")) {

                ret = ret.substring(0, ret.length() - 1) + "!";

            }

            return ret;

        }

        return method.invoke(bean, args);

    }

}

测试代码:

// 原始Bean:

OriginBean origin = new OriginBean();

origin.name = "Bob";// 调用原始Bean的hello():

assertEquals("Hello, Bob.", origin.hello());

// 创建Proxy:

OriginBean proxy = new ProxyResolver().createProxy(origin, new PoliteInvocationHandler());

// Proxy类名,类似OriginBean$ByteBuddy$9hQwRy3T:

System.out.println(proxy.getClass().getName());

// Proxy类与OriginBean.class不同:

assertNotSame(OriginBean.class, proxy.getClass());// proxy实例的name字段应为null:

assertNull(proxy.name);

// 调用带@Polite的方法:

assertEquals("Hello, Bob!", proxy.hello());// 调用不带@Polite的方法:

assertEquals("Morning, Bob.", proxy.morning());

测试通过,本节到此收工。

13.实现Around

现在我们已经实现了ProxyResolver,下一步,实现完整的AOP就很容易了。

我们先从客户端代码入手,看看应当怎么装配AOP。

首先,客户端需要定义一个原始Bean,例如OriginBean,用@Around注解标注:

@Component@Around("aroundInvocationHandler")public class OriginBean {



    @Value("${customer.name}")

    public String name;



    @Polite

    public String hello() {

        return "Hello, " + name + ".";

    }



    public String morning() {

        return "Morning, " + name + ".";

    }

}

@Around注解的值aroundInvocationHandler指出应该按什么名字查找拦截器,因此,客户端应再定义一个AroundInvocationHandler:

@Componentpublic class AroundInvocationHandler implements InvocationHandler {

    @Override

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // 拦截标记了@Polite的方法返回值:

        if (method.getAnnotation(Polite.class) != null) {

            String ret = (String) method.invoke(proxy, args);

            if (ret.endsWith(".")) {

                ret = ret.substring(0, ret.length() - 1) + "!";

            }

            return ret;

        }

        return method.invoke(proxy, args);

    }

}

有了原始Bean、拦截器,就可以在IoC容器中装配AOP:

@Configuration@ComponentScanpublic class AroundApplication {

    @Bean

    AroundProxyBeanPostProcessor createAroundProxyBeanPostProcessor() {

        return new AroundProxyBeanPostProcessor();

    }

}

注意到装配AOP是通过AroundProxyBeanPostProcessor实现的,而这个类是由Framework提供,客户端并不需要自己实现。因此,我们需要开发一个AroundProxyBeanPostProcessor:

public class AroundProxyBeanPostProcessor implements BeanPostProcessor {



    Map<String, Object> originBeans = new HashMap<>();



    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        Class<?> beanClass = bean.getClass();

        // 检测@Around注解:

        Around anno = beanClass.getAnnotation(Around.class);

        if (anno != null) {

            String handlerName;

            try {

                handlerName = (String) anno.annotationType().getMethod("value").invoke(anno);

            } catch (ReflectiveOperationException e) {

                throw new AopConfigException();

            }

            Object proxy = createProxy(beanClass, bean, handlerName);

            originBeans.put(beanName, bean);

            return proxy;

        } else {

            return bean;

        }

    }



    Object createProxy(Class<?> beanClass, Object bean, String handlerName) {

        ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) ApplicationContextUtils.getRequiredApplicationContext();

        BeanDefinition def = ctx.findBeanDefinition(handlerName);

        if (def == null) {

            throw new AopConfigException();

        }

        Object handlerBean = def.getInstance();

        if (handlerBean == null) {

            handlerBean = ctx.createBeanAsEarlySingleton(def);

        }

        if (handlerBean instanceof InvocationHandler handler) {

            return ProxyResolver.getInstance().createProxy(bean, handler);

        } else {

            throw new AopConfigException();

        }

    }



    @Override

    public Object postProcessOnSetProperty(Object bean, String beanName) {

        Object origin = this.originBeans.get(beanName);

        return origin != null ? origin : bean;

    }

}

上述AroundProxyBeanPostProcessor的机制非常简单:检测每个Bean实例是否带有@Around注解,如果有,就根据注解的值查找Bean作为InvocationHandler,最后创建Proxy,返回前保存了原始Bean的引用,因为IoC容器在后续的注入阶段要把相关依赖和值注入到原始Bean。

总结一下,Summer Framework提供的包括:

  • Around注解;
  • AroundProxyBeanPostProcessor实现AOP。

客户端代码需要提供的包括:

  • 带@Around注解的原始Bean;
  • 实现InvocationHandler的Bean,名字与@Around注解value保持一致。

没有额外的要求了。

14.实现Before和After

我们再继续思考,Spring提供的AOP拦截器,有Around、Before和After等好几种。如何实现Before和After拦截?

实际上Around拦截本身就包含了Before和After拦截,我们没必要去修改ProxyResolver,只需要用Adapter模式提供两个拦截器模版,一个是BeforeInvocationHandlerAdapter:

public abstract class BeforeInvocationHandlerAdapter implements InvocationHandler {



    public abstract void before(Object proxy, Method method, Object[] args);



    @Override

    public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        before(proxy, method, args);

        return method.invoke(proxy, args);

    }

}

客户端提供的InvocationHandler只需继承自BeforeInvocationHandlerAdapter,自然就需要覆写before()方法,实现了Before拦截。

After拦截也是一个拦截器模版:

public abstract class AfterInvocationHandlerAdapter implements InvocationHandler {

    // after允许修改方法返回值:

    public abstract Object after(Object proxy, Object returnValue, Method method, Object[] args);



    @Override

    public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object ret = method.invoke(proxy, args);

        return after(proxy, ret, method, args);

    }

}

15.扩展Annotation

截止目前,客户端只需要定义带有@Around注解的Bean,就能自动触发AOP。我们思考下Spring的事务机制,其实也是AOP拦截,不过它的注解是@Transactional。如果要扩展Annotation,即能自定义注解来启动AOP,怎么做?

假设我们后续编写了一个事务模块,提供注解@Transactional,那么,要启动AOP,就必须仿照AroundProxyBeanPostProcessor,提供一个TransactionProxyBeanPostProcessor,不过复制代码太麻烦了,我们可以改造一下AroundProxyBeanPostProcessor,用范型代码处理Annotation,先抽象出一个AnnotationProxyBeanPostProcessor:

public abstract class AnnotationProxyBeanPostProcessor<A extends Annotation> implements BeanPostProcessor {



    Map<String, Object> originBeans = new HashMap<>();

    Class<A> annotationClass;



    public AnnotationProxyBeanPostProcessor() {

        this.annotationClass = getParameterizedType();

    }

    ...

}

实现AroundProxyBeanPostProcessor就一行定义:

public class AroundProxyBeanPostProcessor extends AnnotationProxyBeanPostProcessor<Around> {

}

后续如果我们想实现@Transactional注解,只需定义:

public class TransactionalProxyBeanPostProcessor extends AnnotationProxyBeanPostProcessor<Transactional> {

}

就能自动根据@Transactional启动AOP。

16.实现JDBC和事务

我们已经实现了IoC容器和AOP功能,在此基础上增加JDBC和事务的支持就比较容易了。

Spring对JDBC数据库的支持主要包括:

  1. 提供了一个JdbcTemplate和NamedParameterJdbcTemplate模板类,可以方便地操作JDBC;
  2. 支持流行的ORM框架,如Hibernate、JPA等;
  3. 支持声明式事务,只需要通过简单的注解配置即可实现事务管理。

在Summer Framework中,我们准备提供一个JdbcTemplate模板,以及声明式事务的支持。对于ORM,反正手动集成也比较容易,就不管了。

Spring Framework

Summer Framework

JdbcTemplate

支持

支持

NamedParameterJdbcTemplate

支持

不支持

转换SQL错误码

支持

不支持

ORM

支持

不支持

手动管理事务

支持

不支持

声明式事务

支持

支持

下面开始正式开发Summer Framework的JdbcTemplate与声明式事务。

16.1实现JdbcTemplate

本节我们来实现JdbcTemplate。在Spring中,通过JdbcTemplate,基本封装了所有JDBC操作,可以覆盖绝大多数数据库操作的场景。

16.2配置DataSource

使用JdbcTemplate之前,我们需要配置JDBC数据源。Spring本身只提供了基础的DriverManagerDataSource,但Spring Boot有一个默认配置的数据源,并采用HikariCP作为连接池。这里我们仿照Spring Boot的方式,先定义默认的数据源配置项:

summer:

  datasource:

    url: jdbc:sqlite:test.db

    driver-class-name: org.sqlite.JDBC

    username: sa

    password:

再实现一个HikariCP支持的DataSource:

@Configurationpublic class JdbcConfiguration {



    @Bean(destroyMethod = "close")

    DataSource dataSource(

            // properties:

            @Value("${summer.datasource.url}") String url,

            @Value("${summer.datasource.username}") String username,

            @Value("${summer.datasource.password}") String password,

            @Value("${summer.datasource.driver-class-name:}") String driver,

            @Value("${summer.datasource.maximum-pool-size:20}") int maximumPoolSize,

            @Value("${summer.datasource.minimum-pool-size:1}") int minimumPoolSize,

            @Value("${summer.datasource.connection-timeout:30000}") int connTimeout

    ) {

        var config = new HikariConfig();

        config.setAutoCommit(false);

        config.setJdbcUrl(url);

        config.setUsername(username);

        config.setPassword(password);

        if (driver != null) {

            config.setDriverClassName(driver);

        }

        config.setMaximumPoolSize(maximumPoolSize);

        config.setMinimumIdle(minimumPoolSize);

        config.setConnectionTimeout(connTimeout);

        return new HikariDataSource(config);

    }

}

这样,客户端引入JdbcConfiguration就自动获得了数据源:

@Import(JdbcConfiguration.class)@ComponentScan@Configurationpublic class AppConfig {

}
16.3定义JdbcTemplate

下一步是定义JdbcTemplate,唯一依赖是注入DataSource:

public class JdbcTemplate {

    final DataSource dataSource;



    public JdbcTemplate(DataSource dataSource) {

        this.dataSource = dataSource;

    }

}

JdbcTemplate基于Template模式,提供了大量以回调作为参数的模板方法,其中以execute(ConnectionCallback)为基础:

public <T> T execute(ConnectionCallback<T> action) {

    try (Connection newConn = dataSource.getConnection()) {

        T result = action.doInConnection(newConn);

        return result;

    } catch (SQLException e) {

        throw new DataAccessException(e);

    }

}

即由JdbcTemplate处理获取连接、释放连接、捕获SQLException,上层代码专注于使用Connection:

@FunctionalInterfacepublic interface ConnectionCallback<T> {

    @Nullable

    T doInConnection(Connection con) throws SQLException;

}

其他方法其实也是基于execute(ConnectionCallback),例如:

public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) {

    return execute((Connection con) -> {

        try (PreparedStatement ps = psc.createPreparedStatement(con)) {

            return action.doInPreparedStatement(ps);

        }

    });

}

上述代码实现了ConnectionCallback,内部又调用了传入的PreparedStatementCreator和PreparedStatementCallback,这样,基于更新操作的update就可以这么写:

public int update(String sql, Object... args) {

    return execute(

        preparedStatementCreator(sql, args),

        (PreparedStatement ps) -> {

            return ps.executeUpdate();

        }

    );

}

基于查询操作的queryForList()就可以这么写:

public <T> List<T> queryForList(String sql, RowMapper<T> rowMapper, Object... args) {

    return execute(preparedStatementCreator(sql, args),

        (PreparedStatement ps) -> {

            List<T> list = new ArrayList<>();

            try (ResultSet rs = ps.executeQuery()) {

                while (rs.next()) {

                    list.add(rowMapper.mapRow(rs, rs.getRow()));

                }

            }

            return list;

        }

    );

}

剩下的一系列查询方法都是基于上述方法的封装,包括:

  • queryForList(String sql, RowMapper rowMapper, Object... args)
  • queryForList(String sql, Class clazz, Object... args)
  • queryForNumber(String sql, Object... args)

总之,就是一个工作量的问题,开发难度基本为0。

测试时,可以使用Sqlite这个轻量级数据库,测试用例覆盖到各种SQL操作,最后把JdbcTemplate加入到JdbcConfiguration中,就基本完善了。

17.实现声明式事务

Spring提供的声明式事务管理能极大地降低应用程序的事务代码。如果使用基于Annotation配置的声明式事务,则一个与数据库操作相关的类只需加上@Transactional注解,就实现了事务支持,非常方便:

@Transactional@Componentpublic class UserService {

}

Spring的声明式事务支持JDBC本地事务和JTA分布式事务两种,事务传播模型除了最常用的REQUIRED,还包括Java EE定义的SUPPORTS、REQUIRED_NEW、NESTED等多种模式。Summer Framework出于简化目的,仅支持JDBC本地事务,事务传播模型仅支持最常用的REQUIRED,这样可以大大简化代码:

Spring Framework

Summer Framework

JDBC事务

支持

支持

JTA事务

支持

不支持

REQUIRED传播模式

支持

支持

其他传播模式

支持

不支持

设置隔离级别

支持

不支持

下面我们就来编写声明式事务管理。

首先定义@Transactional,这里就不允许单独在方法处定义,直接在class级别启动所有public方法的事务:

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface Transactional {

    String value() default "platformTransactionManager";

}

默认值platformTransactionManager表示用名字为platformTransactionManager的Bean来管理事务。

下一步是定义接口PlatformTransactionManager:

public interface PlatformTransactionManager {

}

其实啥也没有,就是一个标识作用。

接着定义TransactionStatus,表示当前事务状态:

public class TransactionStatus {

    final Connection connection;



    public TransactionStatus(Connection connection) {

        this.connection = connection;

    }

}

目前仅封装了一个Connection,将来如果扩展,则可以将事务的传播模式存储在里面。

最后写个DataSourceTransactionManager,它持有一个ThreadLocal存储的TransactionStatus,以及一个DataSource:

public class DataSourceTransactionManager implements

        PlatformTransactionManager, InvocationHandler

{

    static final ThreadLocal<TransactionStatus> transactionStatus = new ThreadLocal<>();

    final DataSource dataSource;



    public DataSourceTransactionManager(DataSource dataSource) {

        this.dataSource = dataSource;

    }

}

因为DataSourceTransactionManager是真正执行开启、提交、回归事务的地方,在哪执行呢?就在invoke()内部:

@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    TransactionStatus ts = transactionStatus.get();

    if (ts == null) {

        // 当前无事务,开启新事务:

        try (Connection connection = dataSource.getConnection()) {

            final boolean autoCommit = connection.getAutoCommit();

            if (autoCommit) {

                connection.setAutoCommit(false);

            }

            try {

                // 设置ThreadLocal状态:

                transactionStatus.set(new TransactionStatus(connection));

                // 调用业务方法:

                Object r = method.invoke(proxy, args);

                // 提交事务:

                connection.commit();

                // 方法返回:

                return r;

            } catch (InvocationTargetException e) {

                // 回滚事务:

                TransactionException te = new TransactionException(e.getCause());

                try {

                    connection.rollback();

                } catch (SQLException sqle) {

                    te.addSuppressed(sqle);

                }

                throw te;

            } finally {

                // 删除ThreadLocal状态:

                transactionStatus.remove();

                if (autoCommit) {

                    connection.setAutoCommit(true);

                }

            }

        }

    } else {

        // 当前已有事务,加入当前事务执行:

        return method.invoke(proxy, args);

    }

}

这样就实现了声明式事务。

有的同学会问,如果一个方法开启了事务,那么,它内部调用其他方法,是怎么加入当前事务的?

这里我们先需要写一个获取当前事务连接的工具类:

public class TransactionalUtils {

    @Nullable

    public static Connection getCurrentConnection() {

        TransactionStatus ts = DataSourceTransactionManager.transactionStatus.get();

        return ts == null ? null : ts.connection;

    }

}

然后改造下JdbcTemplate获取连接的代码:

public class JdbcTemplate {

    public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {

        // 尝试获取当前事务连接:

        Connection current = TransactionalUtils.getCurrentConnection();

        if (current != null) {

            try {

                return action.doInConnection(current);

            } catch (SQLException e) {

                throw new DataAccessException(e);

            }

        }

        // 无事务,从DataSource获取新连接:

        try (Connection newConn = dataSource.getConnection()) {

            return action.doInConnection(newConn);

        } catch (SQLException e) {

            throw new DataAccessException(e);

        }

    }

    ...

}

这样,使用JdbcTemplate,如果有事务,自动加入当前事务,否则,按普通SQL执行(数据库隐含事务)。

最后,还需要提供一个TransactionalBeanPostProcessor,使得AOP机制生效,才能拦截@Transactional标注的Bean的public方法:

public class TransactionalBeanPostProcessor extends AnnotationProxyBeanPostProcessor<Transactional> {

}

把它们都整理一下,放到JdbcConfiguration中:

@Configurationpublic class JdbcConfiguration {



    @Bean(destroyMethod = "close")

    DataSource dataSource(

            // properties:

            @Value("${summer.datasource.url}") String url,

            @Value("${summer.datasource.username}") String username,

            @Value("${summer.datasource.password}") String password,

            @Value("${summer.datasource.driver-class-name:}") String driver,

            @Value("${summer.datasource.maximum-pool-size:20}") int maximumPoolSize,

            @Value("${summer.datasource.minimum-pool-size:1}") int minimumPoolSize,

            @Value("${summer.datasource.connection-timeout:30000}") int connTimeout

    ) {

        ...

        return new HikariDataSource(config);

    }



    @Bean

    JdbcTemplate jdbcTemplate(@Autowired DataSource dataSource) {

        return new JdbcTemplate(dataSource);

    }



    @Bean

    TransactionalBeanPostProcessor transactionalBeanPostProcessor() {

        return new TransactionalBeanPostProcessor();

    }



    @Bean

    PlatformTransactionManager platformTransactionManager(@Autowired DataSource dataSource) {

        return new DataSourceTransactionManager(dataSource);

    }

}

现在,应用程序只需导入JdbcConfiguration,从连接池到声明式事务全部齐活:

@Import(JdbcConfiguration.class)@ComponentScan@Configurationpublic class AppConfig {

}

最后我们总结下各个组件的作用:

  1. 由JdbcConfiguration创建的DataSource,实现了连接池;
  2. 由JdbcConfiguration创建的JdbcTemplate,实现基本SQL操作;
  3. 由JdbcConfiguration创建的PlatformTransactionManager,负责拦截@Transactional标识的Bean的public方法,自动管理事务;
  4. 由JdbcConfiguration创建的TransactionalBeanPostProcessor,负责给@Transactional标识的Bean创建AOP代理,拦截器正是PlatformTransactionManager。

应用程序除了导入一个JdbcConfiguration,加上默认配置项,什么也不用干,就可以开始写自动带声明式事务的代码:

@Transactional@Componentpublic class UserService {

    @Autowired

    JdbcTemplate jdbcTemplate;



    public User register(String email, String password) {

        jdbcTemplate.update("INSERT INTO ...", ...);

        return ...

    }

}

18.实现Web MVC

现在,我们已经实现了IoC容器、AOP、JdbcTemplate和声明式事务,离一个完整的框架只差一个Web MVC了。

我们先看看Spring的Web MVC主要提供了哪些组件和API支持:

  1. 一个DispatcherServlet作为核心处理组件,接收所有URL请求,然后按MVC规则转发;
  2. 基于@Controller注解的URL控制器,由应用程序提供,Spring负责解析规则;
  3. 提供ViewResolver,将应用程序的Controller处理后的结果进行渲染,给浏览器返回页面;
  4. 基于@RestController注解的REST处理机制,由应用程序提供,Spring负责将输入输出变为JSON格式;
  5. 多种拦截器和异常处理器等。

Spring的Web MVC功能十分强大,涉及到的内容也非常广。相比之下,Summer Framework的Web MVC必然要聚焦在核心组件上:

Spring Framework

Summer Framework

DispatcherServlet

支持

支持

@Controller注解

支持

支持

@RestController注解

支持

支持

ViewResolver

支持

支持

HandlerInterceptor

支持

不支持

Exception Handler

支持

不支持

CORS

支持

不支持

异步处理

支持

不支持

WebSocket

支持

不支持

不过,Spring Framework的Web MVC模块对Filter支持有限,要想愉快地使用Filter,最好通过Spring Boot提供的FilterRegistrationBean,Summer Framework为了便于应用程序开发自己的Filter,直接支持FilterRegistrationBean。

下面开始正式开发Summer Framework的Web MVC模块。

19.启动IoC容器

在开发Web MVC模块之前,我们首先回顾下Java Web应用程序到底有几方参与。

首先,Java Web应用一般遵循Servlet标准,这个标准定义了应用程序可以按接口编写哪些组件:Servlet、Filter和Listener,也规定了一个服务器(如Tomcat、Jetty、JBoss等)应该提供什么样的服务,按什么顺序加载应用程序的组件,最后才能跑起来处理来自用户的HTTP请求。

Servlet规范定义的组件有3类:

  1. Servlet:处理HTTP请求,然后输出响应;
  2. Filter:对HTTP请求进行过滤,可以有多个Filter形成过滤器链,实现权限检查、限流、缓存等逻辑;
  3. Listener:用来监听Web应用程序产生的事件,包括启动、停止、Session有修改等。

这些组件均由应用程序实现。

而服务器为一个应用程序提供一个“容器”,即Servlet Container,一个Server可以同时跑多个Container,不同的Container可以按URL、域名等区分,Container才是用来管理Servlet、Filter、Listener这些组件的:

┌─────────────────────────────────────┐
│Web Server                           │
│┌───────────────────────────────────┐│
││Servlet Container                  ││
││┌────────┐┌────────┐┌────────┐     ││
│││Servlet ││Servlet ││Servlet │ ... ││
││└────────┘└────────┘└────────┘     ││
││┌────────┐┌────────┐┌────────┐     ││
│││Filter  ││Filter  ││Filter  │ ... ││
││└────────┘└────────┘└────────┘     ││
││┌────────┐┌────────┐┌────────┐     ││
│││Listener││Listener││Listener│ ... ││
││└────────┘└────────┘└────────┘     ││
│└───────────────────────────────────┘│
│┌───────────────────────────────────┐│
││Servlet Container                  ││
││                                   ││
│└───────────────────────────────────┘│
└─────────────────────────────────────┘

另一个需要特别重要的问题是:组件由谁创建,由谁销毁。

在使用IoC容器时,注意到IoC容器也是一个Java类,IoC容器又管理着很多Bean,因此,创建顺序是:

  1. 执行应用程序的入口方法main();
  2. 在main()方法中,创建IoC容器的实例;
  3. IoC容器在它的内部创建各个Bean的实例。

现在,我们开发的是Web应用程序,它本身就是一堆组件,被Web服务器提供的Servlet“容器”管理,同时,又要加一个IoC容器,到底谁创建谁,谁管理谁,这个问题,必须要搞清楚。

首先,我们不能改变Servlet规范,所以,Servlet、Filter、Listener,以及IoC容器,都必须在Servlet容器内被管理:

         ┌────────────────────────────────────────────┐
         │Servlet Container                           │
         │                        ┌──────────────────┐│
         │                        │IoC Container     ││
         │  ┌──────┐   ┌───────┐  │  ┌────────────┐  ││
Request ─┼─▶│Filter│──▶│Servlet│──┼─▶│Controller  │  ││
         │  └──────┘   └───────┘  │  └────────────┘  ││
         │                        │         │        ││
         │  ┌────────┐            │         ▼        ││
         │  │Listener│            │  ┌────────────┐  ││
         │  └────────┘            │  │UserService │  ││
         │                        │  └────────────┘  ││
         │                        │         │        ││
         │                        │         ▼        ││
         │                        │  ┌────────────┐  ││
         │                        │  │JdbcTemplate│  ││
         │                        │  └────────────┘  ││
         │                        └──────────────────┘│
         └────────────────────────────────────────────┘

所以我们要捋清楚这些组件的创建顺序,以及谁创建谁。

对于一个Web应用程序来说,启动时,应用程序本身只是一个war包,并没有main()方法,因此,启动时执行的是Server的main()方法。以Tomcat服务器为例:

  1. 启动服务器,即执行Tomcat的main()方法;
  2. Tomcat根据配置或自动检测到一个xyz.war包后,为这个xyz.war应用程序创建Servlet容器;
  3. Tomcat继续查找xyz.war定义的Servlet、Filter和Listener组件,按顺序实例化每个组件(Listener最先被实例化,然后是Filter,最后是Servlet);
  4. 用户发送HTTP请求,Tomcat收到请求后,转发给Servlet容器,容器根据应用程序定义的映射,把请求发送个若干Filter和一个Servlet处理;
  5. 处理期间产生的事件则由Servlet容器自动调用Listener。

其中,第3步实例化又有很多方式:

  1. 通过在web.xml配置文件中定义,这也是早期Servlet规范唯一的配置方式;
  2. 通过注解@WebServlet、@WebFilter和@WebListener定义,由Servlet容器自动扫描所有class后创建组件,这和我们用Annotation配置Bean,由IoC容器自动扫描创建Bean非常类似;
  3. 先配置一个Listener,由Servlet容器创建Listener,然后,Listener自己调用相关接口,手动创建Servlet和Filter。

到底用哪种方式,取决于Web应用程序自己如何编写。对于使用Spring框架的Web应用程序来说,Servlet、Filter和Listener数量少,而且是固定的,应用程序自身编写的Controller数量不定,但由IoC容器管理,因此,采用方式3最合适。

具体来说,Tomcat启动一个基于Spring开发的Web应用程序时,按如下步骤初始化:

  1. 为Web应用程序准备Servlet容器;
  2. 根据配置实例化一个Spring提供的Listener;
    1. Spring提供的Listener在初始化时启动IoC容器;
    2. Spring提供的Listener在初始化时向Servlet容器注册Spring内置的一个DispatcherServlet。

当Tomcat把HTTP请求发送给Spring注册的Servlet后,因为它持有IoC容器的引用,就可以找到Controller实例,因此,可以把请求继续转发给对应的Controller,这样就完成了HTTP请求的处理。

另外注意到Web应用程序除了提供Controller外,并不必须与Servlet API打交道,因为被Spring提供的DispatcherServlet给隔离了。

所以,我们在开发Summer Framework的Web MVC模块时,应该以如下方式初始化:

  1. 应用程序必须配置一个Summer Framework提供的Listener;
  2. Tomcat完成Servlet容器的创建后,立刻根据配置创建Listener;
    1. Listener初始化时创建IoC容器;
    2. Listener继续创建DispatcherServlet实例,并向Servlet容器注册;
    3. DispatcherServlet初始化时获取到IoC容器中的Controller实例,因此可以根据URL调用不同Controller实例的不同处理方法。

我们先写一个只能输出Hello World的Servlet:

public class DispatcherServlet extends HttpServlet {

    @Override

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        PrintWriter pw = resp.getWriter();

        pw.write("<h1>Hello, world!</h1>");

        pw.flush();

    }

}

紧接着,编写一个ContextLoaderListener,它实现了ServletContextListener接口,能监听Servlet容器的启动和销毁,在监听到初始化事件时,完成创建IoC容器和注册DispatcherServlet两个工作:

public class ContextLoaderListener implements ServletContextListener {

    // Servlet容器启动时自动调用:

    @Override

    public void contextInitialized(ServletContextEvent sce) {

        // 创建IoC容器:

        var applicationContext = createApplicationContext(...);

        // 实例化DispatcherServlet:

        var dispatcherServlet = new DispatcherServlet();

        // 注册DispatcherServlet:

        var dispatcherReg = servletContext.addServlet("dispatcherServlet", dispatcherServlet);

        dispatcherReg.addMapping("/");

        dispatcherReg.setLoadOnStartup(0);

    }

}

这样,我们就完成了Web应用程序的初始化全部流程!

最后两个小问题:

  1. 创建IoC容器时,需要的配置文件从哪读?这里我们采用Spring Boot的方式,默认从classpath的application.yml或application.properties读。
  2. 需要的@Configuration配置类从哪获取?这是通过web.xml文件配置的:
<?xml version="1.0" encoding="UTF-8"?><web-app ...>

<context-param>

        <!-- 固定名称 -->

<param-name>configuration</param-name>

        <!-- 配置类的完整类名 -->

<param-value>com.itranswarp.summer.webapp.WebAppConfig</param-value>

</context-param>



<listener>

<listener-class>com.itranswarp.summer.web.ContextLoaderListener</listener-class>

</listener></web-app>

在ContextLoaderListener的contextInitialized()方法内,先获取ServletContext引用,再通过getInitParameter("configuration")拿到完整类名,就可以顺利创建IoC容器了。

用Maven打包后,把生成的xyz.war改为ROOT.war,复制到Tomcat的webapps目录下,清除掉其他webapp,启动Tomcat,输入http://localhost:8080可看到输出Hello, world!。

这样我们就跑通了一个Web应用程序启动的全部流程。

20.实现MVC

上一节我们把Web应用程序的流程跑通了,因此,本节重点就在如何继续开发DispatcherServlet,因为整个MVC的处理都是在DispatcherServlet内部完成的。

要处理MVC,我们先定义@Controller和@RestController:

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpublic @interface Controller {

    String value() default "";

}

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpublic @interface RestController {

    String value() default "";

}

以及@GetMapping、@PostMapping等注解,来标识MVC处理的方法。

DispatcherServlet内部负责从IoC容器找出所有@Controller和@RestController定义的Bean,扫描它们的方法,找出@GetMapping和@PostMapping标识的方法,这样就有了一个处理特定URL的处理器,我们抽象为Dispatcher:

class Dispatcher {

    // 是否返回REST:

    boolean isRest;

    // 是否有@ResponseBody:

    boolean isResponseBody;

    // 是否返回void:

    boolean isVoid;

    // URL正则匹配:

    Pattern urlPattern;

    // Bean实例:

    Object controller;

    // 处理方法:

    Method handlerMethod;

    // 方法参数:

    Param[] methodParameters;

}

方法参数也需要根据@RequestParam、@RequestBody等抽象出Param类型:

class Param {

    // 参数名称:

    String name;

    // 参数类型:

    ParamType paramType;

    // 参数Class类型:

    Class<?> classType;

    // 参数默认值

    String defaultValue;

}

一共有4种类型的参数,我们用枚举ParamType定义:

  • PATH_VARIABLE:路径参数,从URL中提取;
  • REQUEST_PARAM:URL参数,从URL Query或Form表单提取;
  • REQUEST_BODY:REST请求参数,从Post传递的JSON提取;
  • SERVLET_VARIABLE:HttpServletRequest等Servlet API提供的参数,直接从DispatcherServlet的方法参数获得。

这样,DispatcherServlet通过反射拿到一组Dispatcher对象,在doGet()和doPost()方法中,依次匹配URL:

public class DispatcherServlet extends HttpServlet {



    List<Dispatcher> getDispatchers = new ArrayList<>();

    List<Dispatcher> postDispatchers = new ArrayList<>();



    @Override

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String url = req.getRequestURI();

        // 依次匹配每个Dispatcher的URL:

        for (Dispatcher dispatcher : getDispatchers) {

            Result result = dispatcher.process(url, req, resp);

            // 匹配成功并处理后:

            if (result.processed()) {

                // 处理结果

                ...

                return;

            }

        }

        // 未匹配到任何Dispatcher:

        resp.sendError(404, "Not Found");

    }



    @Override

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        ...

    }

}

这里不能用Map<String, Dispatcher>的原因在于我们要处理类似/hello/{name}这样的URL,没法使用精确查找,只能使用正则匹配。

Dispatcher处理后返回类型包括:

  • void或null:表示内部已处理完毕;
  • String:如果以redirect:开头,则表示一个重定向;
  • String或byte[]:如果配合@ResponseBody,则表示返回值直接写入响应;
  • ModelAndView:表示这是一个MVC响应,包含Model和View名称,后续用模板引擎处理后写入响应;
  • 其它类型:如果是@RestController,则序列化为JSON后写入响应。

不符合上述要求的返回类型则报500错误。

这些处理逻辑都十分简单,我们重点看看如何处理ModelAndView类型,即MVC响应。

为了处理ModelAndView,我们需要一个模板引擎,因此,抽象出ViewResolver接口:

public interface ViewResolver {

    // 初始化ViewResolver:

    void init();



    // 渲染:

    void render(String viewName, Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp);

}

Spring内置FreeMarker引擎,因此我们也把FreeMarker集成进来,写一个FreeMarkerViewResolver:

public class FreeMarkerViewResolver implements ViewResolver {



    final String templatePath;

    final String templateEncoding;

    final ServletContext servletContext;



    Configuration config;



    public FreeMarkerViewResolver(ServletContext servletContext, String templatePath, String templateEncoding) {

        this.servletContext = servletContext;

        this.templatePath = templatePath;

        this.templateEncoding = templateEncoding;

    }



    @Override

    public void init() {

        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);

        cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);

        cfg.setDefaultEncoding(this.templateEncoding);

        cfg.setTemplateLoader(new ServletTemplateLoader(this.servletContext, this.templatePath));

        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);

        cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY);

        cfg.setLocalizedLookup(false);

        var ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_32);

        ow.setExposeFields(true);

        cfg.setObjectWrapper(ow);

        this.config = cfg;

    }



    @Override

    public void render(String viewName, Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        Template templ = null;

        try {

            templ = this.config.getTemplate(viewName);

        } catch (Exception e) {

            throw new ServerErrorException("View not found: " + viewName);

        }

        PrintWriter pw = resp.getWriter();

        try {

            templ.process(model, pw);

        } catch (TemplateException e) {

            throw new ServerErrorException(e);

        }

        pw.flush();

    }

}

这样我们就可以在DispatcherServlet内部,把处理ModelAndView和ViewResolver结合起来,最终向HttpServletResponse中输出HTML,完成HTTP请求的处理。

为了简化Web应用程序配置,我们提供一个WebMvcConfiguration配置:

@Configurationpublic class WebMvcConfiguration {

    private static ServletContext servletContext = null;

    static void setServletContext(ServletContext ctx) {

        servletContext = ctx;

    }



    @Bean(initMethod = "init")

    ViewResolver viewResolver( //

            @Autowired ServletContext servletContext, //

            @Value("${summer.web.freemarker.template-path:/WEB-INF/templates}") String templatePath, //

            @Value("${summer.web.freemarker.template-encoding:UTF-8}") String templateEncoding) {

        return new FreeMarkerViewResolver(servletContext, templatePath, templateEncoding);

    }



    @Bean

    ServletContext servletContext() {

        return Objects.requireNonNull(servletContext, "ServletContext is not set.");

    }

}

默认创建一个ViewResolver和ServletContext,注意ServletContext本身实际上是由Servlet容器提供的,但我们把它放入IoC容器,是因为许多涉及到Web的组件,如ViewResolver,需要注入ServletContext,才能从指定配置加载文件。

最后,整理代码,添加一些能方便用户开发的额外功能,例如处理静态文件等功能,我们的Web MVC模块就开发完毕!

注意事项

在整个HTTP处理流程中,入口是DispatcherServlet的service()方法,整个流程如下:

  1. Servlet容器调用DispatcherServlet的service()方法处理HTTP请求;
  2. service()根据GET或POST调用doGet()或doPost()方法;
  3. 根据URL依次匹配Dispatcher,匹配后调用process()方法,获得返回值;
  4. 根据返回值写入响应:
    1. void或null返回值无需写入响应;
    2. String或byte[]返回值直接写入响应(或重定向);
    3. REST类型写入JSON序列化结果;
    4. ModelAndView类型调用ViewResolver写入渲染结果。
  5. 未匹配到判断是否静态资源:
    1. 符合静态目录(默认/static/)则读取文件,写入文件内容;
    2. 网站图标(默认/favicon.ico)则读取.ico文件,写入文件内容;
  6. 其他情况返回404。

由于在处理的每一步都可以向HttpServletResponse写入响应,因此,后续步骤写入时,应判断前面的步骤是否已经写入并发送了HTTP Header。isCommitted()方法就是干这个用的:

if (!resp.isCommitted()) {

    resp.resetBuffer();

    writeTo(resp);

}
测试

DispatcherServlet处理HTTP请求时,一些组件是Servlet容器提供的,如:

  • HttpServletRequest;
  • HttpServletResponse;
  • HttpSession;
  • ServletContext。

要模拟这些对象用Mockito之类的框架代码量也很大,我们可以借用Spring提供的test模块,它实现了完善的MockHttpServletRequest、MockServletContext等对象,便于测试。我们导入:

  • org.springframework:spring-test:6.0.0
  • org.springframework:spring-web:6.0.0

注意设置<scope>test</scope>,即仅在测试代码中用到了Spring提供的Mock对象,业务代码并不会用到Spring的任何功能。一个简单的测试用例如下:

@Testvoid getGreeting() throws ServletException, IOException {

    // 创建MockHttpServletRequest:

    var req = createMockRequest("GET", "/greeting", null, Map.of("name", "Bob"));

    // 创建MockHttpServletResponse:

    var resp = createMockResponse();

    // 处理请求:

    this.dispatcherServlet.service(req, resp);

    // 验证200响应:

    assertEquals(200, resp.getStatus());

    // 验证响应内容:

    assertEquals("Hello, Bob", resp.getContentAsString());

}

21.开发Web应用

在我们开发完Summer Framework的所有组件后,就可以基于Summer Framework来开发一个真正的Web应用了!

我们来一步一步创建一个hello-webapp的应用,它基于Maven项目,符合webapp标准。

首先,我们在src/main/resources下定义配置文件application.yml:

app:

  title: Hello Application

  version: 1.0

summer:

  datasource:

    url: jdbc:sqlite:test.db

    driver-class-name: org.sqlite.JDBC

    username: sa

    password:

紧接着,定义IoC容器的配置类如下:

@ComponentScan@Configuration@Import({ JdbcConfiguration.class, WebMvcConfiguration.class })

public class HelloConfiguration {

}

以及相关的UserService、MvcController等Bean。

接下来是在src/main/webapp/WEB-INF目录下创建Servlet容器所需的配置文件web.xml:

<?xml version="1.0" encoding="UTF-8"?><web-app ...>

<display-name>Hello Webapp</display-name>



<context-param>

<param-name>configuration</param-name>

<param-value>com.itranswarp.hello.HelloConfiguration</param-value>

</context-param>



<listener>

<listener-class>com.itranswarp.summer.web.ContextLoaderListener</listener-class>

</listener></web-app>

Servlet容器会自动读取web.xml,根据配置的Listener启动Summer Framework的web模块的ContextLoaderListener,它又会读取web.xml配置的<context-param>获得配置类的全名com.itranswarp.hello.HelloConfiguration,最后用这个配置类完成IoC容器的创建。创建后自动注册Summer Framework的DispatcherServlet,以及Web应用程序定义的FilterRegistrationBean,这样就完成了整个Web应用程序的初始化。

其他用到的资源包括:

  • 存储在src/main/webapp/static目录下的静态资源;
  • 存储于src/main/webapp/favicon.ico的图标文件;
  • 存储在src/main/webapp/WEB-INF/templates目录下的模板。

最后,运行mvn clean package命令,在target目录得到最终的war包,改名为ROOT.war,复制到Tomcat的webapps目录下,启动Tomcat,可以正常访问http://localhost:8080:

21.1实现Boot

虽然基于Summer Framework可以开发一个完整的Web应用程序,但是,开发过程还是涉及到先打包,再复制到Tomcat的webapps目录,再启动Tomcat。在开发过程中,经常需要反复来好多次,每次停止、复制、启动,要调试还要接入远程,搞着搞着就会发现,这种开发模式太麻烦。

直接用Spring Framework开发Web应用也是一样,所以才有了Spring Boot,它最让人省心的一点,就是不用装Tomcat,不用复制war包,打个jar包直接就能跑!

所以,为了简化开发流程,我们也仿照Spring Boot,编写一个boot模块,能直接启动运行!

注意Spring Boot除了直接打包运行外,还提供很多其他功能,而Summer Framework的boot模块只提供打包运行功能,无其他额外功能。

下面开始正式开发Summer Framework的boot模块。

21.2启动嵌入式Tomcat

Spring Boot实现一个jar包直接运行的原理其实就是把Tomcat打包进去,自己再写个main()函数:

@SpringBootApplicationpublic class AppConfig {

    public static void main(String[] args) {

        SpringApplication.run(AppConfig.class, args);

    }

}

在SpringApplication.run()方法内,Spring Boot会启动嵌入式Tomcat,然后再初始化Spring的IoC容器,实际上就是一个jar包内包含了嵌入式Tomcat、Spring IoC容器、Web MVC模块以及应用程序自己开发的Bean。

因此,我们也提供一个SummerApplication,实现run()方法如下:

public class SummerApplication {

    public static void run(String webDir, String baseDir, Class<?> configClass, String... args) {

        // 读取application.yml配置:

        var propertyResolver = WebUtils.createPropertyResolver();

        // 创建Tomcat服务器:

        var server = startTomcat(webDir, baseDir, configClass, propertyResolver);

        // 等待服务器结束:

        server.await();

    }

}

这里多了两个参数:webDir和baseDir,这是为启动嵌入式Tomcat准备的,启动嵌入式Tomcat的代码如下:

Server startTomcat(String webDir, String baseDir, Class<?> configClass, PropertyResolver propertyResolver) throws Exception {

    int port = propertyResolver.getProperty("${server.port:8080}", int.class);

    // 实例化Tomcat Server:

    Tomcat tomcat = new Tomcat();

    tomcat.setPort(port);

    // 设置Connector:

    tomcat.getConnector().setThrowOnFailure(true);

    // 添加一个默认的Webapp,挂载在'/':

    Context ctx = tomcat.addWebapp("", new File(webDir).getAbsolutePath());

    // 设置应用程序的目录:

    WebResourceRoot resources = new StandardRoot(ctx);

    resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", new File(baseDir).getAbsolutePath(), "/"));

    ctx.setResources(resources);

    // 设置ServletContainerInitializer监听器:

    ctx.addServletContainerInitializer(new ContextLoaderInitializer(configClass, propertyResolver), Set.of());

    // 启动服务器:

    tomcat.start();

    return tomcat.getServer();

}

那么我们的IoC容器,以及注册Servlet、Filter是在哪进行的?答案是我们在startTomcat()内注册了一个ServletContainerInitializer监听器,这个监听器负责启动IoC容器与注册Servlet、Filter:

public class ContextLoaderInitializer implements ServletContainerInitializer {

    final Class<?> configClass;

    final PropertyResolver propertyResolver;



    public ContextLoaderInitializer(Class<?> configClass, PropertyResolver propertyResolver) {

        this.configClass = configClass;

        this.propertyResolver = propertyResolver;

    }



    @Override

    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {

        // 设置ServletContext:

        WebMvcConfiguration.setServletContext(ctx);

        // 启动IoC容器:

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(this.configClass, this.propertyResolver);

        // 注册Filter与DispatcherServlet:

        WebUtils.registerFilters(ctx);

        WebUtils.registerDispatcherServlet(ctx, this.propertyResolver);

    }

}

没有复用web模块的ContextLoaderListener是因为Tomcat不允许没有在web.xml中声明的Listener注册Filter与Servlet,而我们写boot模块原因之一也是要做到不需要web.xml。

这样我们就完成了boot模块的开发,它其实就包含两个组件:

  • SummerApplication:负责启动嵌入式Tomcat;
  • ContextLoaderInitializer:负责启动IoC容器,注册Filter与DispatcherServlet。

下面就可以编写一个基于boot的Web应用程序了。

21.3开发Boot应用

我们已经准备好boot模块了,下一步是使用boot来开发Web应用程序。

我们还是先定义一个符合Maven结构的Web应用程序hello-boot,先定义配置类HelloConfiguration:

@ComponentScan@Configuration@Import({ JdbcConfiguration.class, WebMvcConfiguration.class })

public class HelloConfiguration {

}

以及UserService、MvcController等业务Bean。

我们直接写个main()方法启动:

public class Main {

    public static void main(String[] args) throws Exception {

        SummerApplication.run("src/main/webapp", "target/classes", HelloConfiguration.class, args);

    }

}

直接从IDE运行,是没有问题的,能顺利启动Tomcat、创建IoC容器、注册Filter和DispatcherServlet,可以直接通过浏览器访问。

但是,如果打一个war包,直接运行java -jar xyz.war是不行的!会直接报错:找不到Main这个class!

这是为什么呢?我们要从JVM的类加载机制说起。

当我们用java启动一个Java程序时,需要用-cp参数设置classpath(默认为当前目录.);当我们用java -jar xyz.jar启动一个Java程序时,JVM忽略-cp参数,默认classpath为xyz.jar,这样,如果能在jar包中找到对应的class,就可以正常运行。

要注意的一点是,JVM从jar包加载class,是从jar包的根目录查找的。如果它要加载com.itranswarp.hello.Main,那么,xyz.jar必须按如下目录组织:

xyz.jar
└── com
    └── itranswarp
        └── hello
            └── Main.class

而我们在用Maven打war包时,结构是这样的:

xyz.war
└── WEB-INF
    └── classes
        └── com
            └── itranswarp
                └── hello
                    └── Main.class

自然无法加载Main。(注意jar包和war包仅扩展名不同,对JVM来说是完全一样的)

那为什么我们把xyz.war扔到Tomcat的webapps目录下就能正常运行呢?因为Tomcat启动后,并不使用JVM的ClassLoader加载class,而是为每个webapp创建一个单独的ClassLoader,这个ClassLoader在如下位置搜索class:

  • WEB-INF/classes目录;
  • WEB-INF/lib目录下的所有jar包。

因此,我们要运行的xyz.war包必须同时具有Web App的结构,又能在根目录下搜索到应用程序自己编写的Main:

xyz.jar
├── com
│   └── itranswarp
│       └── hello
│           └── Main.class
└── WEB-INF
    ├── classes
    └── libs

解决方案是在打包时复制所有编译的class到war包根目录,并添加启动类入口。修改pom.xml:

<project ...>

...



<build>

<finalName>${project.name}</finalName>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-war-plugin</artifactId>

<version>3.3.2</version>

<configuration>

<!-- 复制classes到war包根目录 -->

<webResources>

<resource>

<directory>${project.build.directory}/classes</directory>

</resource>

</webResources>

<archiveClasses>true</archiveClasses>

<archive>

<manifest>

<!-- main启动类 -->

<mainClass>com.itranswarp.hello.Main</mainClass>

</manifest>

</archive>

</configuration>

</plugin>

</plugins>

</build></project>

再次打包,运行,又会得到找不到Class的错误,不过这次是SummerApplication。

这又是什么原因呢?很明显Main已经找到了,但是SummerApplication在哪呢?它其实在WEB-INF/lib/summer-boot-1.x.x.jar,JVM不会在WEB-INF/lib下搜索Class,也不会在一个jar包内搜索“jar包内的jar包”。

怎么破?

答案是Main运行时先自解压,再让JVM能搜索到WEB-INF/lib/summer-boot-1.x.x.jar即可。

需要先修改main()方法代码:

public static void main(String[] args) throws Exception {

    // 判定是否从jar/war启动:

    String jarFile = Main.class.getProtectionDomain().getCodeSource().getLocation().getFile();

    boolean isJarFile = jarFile.endsWith(".war") || jarFile.endsWith(".jar");

    // 定位webapp根目录:

    String webDir = isJarFile ? "tmp-webapp" : "src/main/webapp";

    if (isJarFile) {

        // 解压到tmp-webapp:

        Path baseDir = Paths.get(webDir).normalize().toAbsolutePath();

        if (Files.isDirectory(baseDir)) {

            Files.delete(baseDir);

        }

        Files.createDirectories(baseDir);

        System.out.println("extract to: " + baseDir);

        try (JarFile jar = new JarFile(jarFile)) {

            List<JarEntry> entries = jar.stream().sorted(Comparator.comparing(JarEntry::getName)).collect(Collectors.toList());

            for (JarEntry entry : entries) {

                Path res = baseDir.resolve(entry.getName());

                if (!entry.isDirectory()) {

                    System.out.println(res);

                    Files.createDirectories(res.getParent());

                    Files.copy(jar.getInputStream(entry), res);

                }

            }

        }

        // JVM退出时自动删除tmp-webapp:

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {

            try {

                Files.walk(baseDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);

            } catch (IOException e) {

                e.printStackTrace();

            }

        }));

    }

    SummerApplication.run(webDir, isJarFile ? "tmp-webapp" : "target/classes", HelloConfiguration.class, args);

}

再修改pom.xml,加上Classpath:

<project ...>

...



<build>

<finalName>${project.name}</finalName>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-war-plugin</artifactId>

<version>3.3.2</version>

<configuration>

<!-- 复制classes到war包根目录 -->

<webResources>

<resource>

<directory>${project.build.directory}/classes</directory>

</resource>

</webResources>

<archiveClasses>true</archiveClasses>

<archive>

<manifest>

<!-- 添加Class-Path -->

<addClasspath>true</addClasspath>

<!-- Classpath前缀 -->

<classpathPrefix>tmp-webapp/WEB-INF/lib/</classpathPrefix>

<!-- main启动类 -->

<mainClass>com.itranswarp.hello.Main</mainClass>

</manifest>

</archive>

</configuration>

</plugin>

</plugins>

</build></project>

当我们打包后,我们来分析启动流程。我们先把war包解压到tmp-webapp,它的结构如下:

tmp-webapp
├── META-INF
│   └── MANIFEST.MF
├── WEB-INF
│   ├── classes
│   ├── lib
│   │   ├── summer-boot-1.0.3.jar
│   │   └── ... other jars ...
│   └── templates
│       └── ... templates.html
├── application.yml
├── com
│   └── itranswarp
│       └── hello
│           ├── Main.class
│           └── ... other classes ...
├── favicon.ico
├── logback.xml
└── static
    └── ... static files ...

可见,com/itranswarp/hello/Main.class、application.yml、logback.xml都位于war包的根目录,可以被JVM的ClassLoader直接加载,而想要加载WEB-INF/lib/summer-boot-1.x.x.jar,我们需要给出Classpath。通过java -jar xyz.war启动时,虽然-cp参数无效,但JVM会自动从META-INF/MANIFEST.MF中读取Class-Path条目,我们用Maven写入后内容如下:

Manifest-Version: 1.0

Created-By: Maven WAR Plugin 3.3.2

Build-Jdk-Spec: 17

Main-Class: com.itranswarp.hello.Main

Class-Path: tmp-webapp/WEB-INF/lib/summer-boot-1.0.3.jar tmp-webapp/WEB-

 INF/lib/summer-web-1.0.3.jar tmp-webapp/WEB-INF/lib/summer-context-1.0.

 3.jar tmp-webapp/WEB-INF/lib/snakeyaml-2.0.jar tmp-webapp/WEB-INF/lib/j

 ackson-databind-2.14.2.jar tmp-webapp/WEB-INF/lib/jackson-annotations-2

 .14.2.jar tmp-webapp/WEB-INF/lib/jackson-core-2.14.2.jar tmp-webapp/WEB

 -INF/lib/jakarta.annotation-api-2.1.1.jar tmp-webapp/WEB-INF/lib/slf4j-

 api-2.0.7.jar tmp-webapp/WEB-INF/lib/logback-classic-1.4.6.jar tmp-weba

 pp/WEB-INF/lib/logback-core-1.4.6.jar tmp-webapp/WEB-INF/lib/freemarker

 -2.3.32.jar tmp-webapp/WEB-INF/lib/summer-jdbc-1.0.3.jar tmp-webapp/WEB

 -INF/lib/summer-aop-1.0.3.jar tmp-webapp/WEB-INF/lib/byte-buddy-1.14.2.

 jar tmp-webapp/WEB-INF/lib/HikariCP-5.0.1.jar tmp-webapp/WEB-INF/lib/to

 mcat-embed-core-10.1.7.jar tmp-webapp/WEB-INF/lib/tomcat-annotations-ap

 i-10.1.7.jar tmp-webapp/WEB-INF/lib/tomcat-embed-jasper-10.1.7.jar tmp-

 webapp/WEB-INF/lib/tomcat-embed-el-10.1.7.jar tmp-webapp/WEB-INF/lib/ec

 j-3.32.0.jar tmp-webapp/WEB-INF/lib/sqlite-jdbc-3.41.2.1.jar

JVM会读取到Main-Class和Class-Path,由于已经解压,就能在tmp-webapp目录中顺利搜索到tmp-webapp/WEB-INF/lib/summer-boot-1.x.x.jar。后续Tomcat启动后,以tmp-webapp作为web目录本身就是标准的Web App,Tomcat的ClassLoader也能继续从WEB-INF/lib加载各种jar包。

我们总结一下,打包时做了哪些工作:

  • 复制所有编译的class到war包根目录;
  • 修改META-INF/MANIFEST.MF:
    • 添加Main-Class条目;
    • 添加Class-Path条目。

运行时的流程如下:

  1. JVM从war包加载Main类,执行main()方法;
  2. 立刻自解压war包至tmp-webapp目录;
  3. 后续加载SummerApplication时,JVM根据Class-Path能找到tmp-webapp/WEB-INF/lib/summer-boot-1.x.x.jar,因此可顺利加载;
  4. 启动Tomcat,将tmp-webapp做为Web目录;
  5. 作为Web App使用Tomcat的ClassLoader加载其他组件。

这样我们就实现了一个可以直接用java -jar xyz.war启动的Web应用程序!

有的同学会问,我们的boot应用,main()方法写了一堆自解压代码,而且,需要在pom.xml中配置很多额外的设置,对比Spring Boot应用,它对main()方法没有任何要求,而且,在pom.xml中也只需配置一个spring-boot-maven-plugin,没有其他额外配置,相比之下简单多了,那么,Spring Boot是如何实现的?

我们找一个Spring Boot打包的jar解压后就明白了,它的jar包结构如下:

xyz.jar
├── BOOT-INF
│   ├── classes
│   │   ├── application.yml
│   │   ├── logback-spring.xml
│   │   ├── static
│   │   │   └── ... static files ...
│   │   └── templates
│   │       └── ... templates ...
│   └── lib
│       ├── spring-boot-3.0.0.jar
│       └── ... other jars ...
├── META-INF
│   └── MANIFEST.MF
└── org
    └── springframework
        └── boot
            └── loader
                ├── JarLauncher.class
                └── ... other classes ...

Spring Boot并不能修改JVM的ClassLoader机制,因此,Spring Boot的jar包仍然需要在META-INF/MANIFEST.MF中声明Main-Class,只不过它声明的不是应用程序自己的Main,而是Spring Boot的JarLauncher:

Main-Class: org.springframework.boot.loader.JarLauncher

在jar包的根目录,JVM可以加载JarLauncher。一旦加载了JarLauncher后,Spring Boot会用自己的ClassLoader去加载其他的class和jar包,它在BOOT-INF/classes和BOOT-INF/lib下搜索。注意Spring Boot自定义的ClassLoader并不需要设置Class-Path,它可以完全自定义搜索路径,包括搜索“jar包中的jar包”。

因此,Spring Boot采用了两种机制来实现可执行jar包:

  1. 提供Maven插件,自动设置Main-Class,复制相关启动Class,按BOOT-INF组织class和jar包;
  2. 提供自定义的ClassLoader,可以在jar包中搜索BOOT-INF/classes和BOOT-INF/lib。

这样就使得编写Web应用程序时能简化打包和启动流程。代价就是编写一个自定义的Maven插件和自定义的ClassLoader工作量很大,有兴趣的同学可以试着实现Spring Boot的机制。

 期末寄语

哪有什么天才,只不过是一帮把你玩耍娱乐的时间用来学习的人。

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值