2024年9月

八,SpringBoot Web 开发访问静态资源(附+详细源码剖析)

@


1. 基本介绍

SpringBoot 中对于静态资源的访问:

  1. 只要将静态资源放在类路径下: /static, /public, /resources, /META-INF/resources 就可以被直接访问-对应文件(
    这是 Spring Boot 的默认设置好的
    )。关于这一点,我们从
    WebProperties.java
    这个类的源码上可以找到,对应的配置属性。

在这里插入图片描述

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
注意:classpath:/resources/ 表示服务器就会在 resources 路径下找,你在浏览器当中输入的url地址的时候,不可以输入 resources 目录,因为服务器就是会在 classpath:/resources/ 找的,而如果你写了resources在浏览器上的话,你想表达的就是:让浏览器从resources/resouces的路径下找,这是找不到的报404错误

注意:classpath:/resources/ 表示服务器就会在 resources路径下找,你在浏览器当中输入的url地址的时候,不可以输入 resources 目录,因为服务器就是会在 classpath:/resources/ 找的,而如果你写了resources 在浏览器上的话,你想表达的就是:让浏览器从resources/resouces的路径下找,这是找不到的报404错误

  1. 常见静态资源: js,css,图片(.jpg,.png,.gif,.bmp,.svg) ,字体文件(Fonts)等
  2. 访问方式: 默认:项目根路径/+静态资源名 比如:
    http://localhost:8080/hi.html
    。关于这一点,我们可以从 WebMvcProperties.java 类当中找到答案。

在这里插入图片描述

2. 快速入门

2.1 准备工作

在 pom .xml 文件中导入相关的 jar 依赖。如下

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>springboot_static_configuration</artifactId>
    <version>1.0-SNAPSHOT</version>


    <!--    导入SpringBoot 父工程-规定写法-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>

    <!--    导入web项目场景启动器:会自动导入和web开发相关的jar包所有依赖【库/jar】-->
    <!--    后面还会在说明spring-boot-starter-web 到底引入哪些相关依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

编写启动程序:

在这里插入图片描述

package com.rainbowsea.springboot;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication  // 标志启动场景
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}


从上面的
基本介绍
当中,我们知道了,Spring Boot 默认静态资源的访问路径有
4
个,我们这里就测试这四个路径是否
可以直接访问

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};

注意:classpath 表示的是类路径,就是如图下面的:
resources
目录,简单的说就是
classpath ===(等同于)resources

在这里插入图片描述

下面:我们分别在 resources 类路径下,创建对应的Spring Boot 默认的四个目录。如下图:
在这里插入图片描述

同时我们在这四个目录下,放入几张图片,用于访问测试。

在这里插入图片描述

启动程序运行测试:

在这里插入图片描述

打开浏览器进行直接访问静态资源文件:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

注意:

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
注意:classpath:/resources/ 表示服务器就会在 resources 路径下找,你在浏览器当中输入的url地址的时候,不可以输入 resources 目录,因为服务器就是会在 classpath:/resources/ 找的,而如果你写了resources在浏览器上的话,你想表达的就是:让浏览器从resources/resouces的路径下找,这是找不到的报404错误

在这里插入图片描述

resources/static 是Spring Bopt的默认静态路径,默认就是以“resources/static” 作为根路径访问的,所以,不需要再额外的加上 static 在浏览器上,如果你加了,那么你实际访问的是:resources/static/static这个路径,这个路径是不存在资源的,所以报错无法找到。

3. 改变静态资源访问前缀,定义为我们自己想要的

改变静态资源访问前缀,比如我们希望
http://locahost:8080/rainbowsea/*
下的请求路径,去请求静态资源,应用场景:静态资源访问前缀和控制器请求路径冲突。

我们这里需要用到
yaml
语法的内容,关于 yaml 语法想要了解的,大家可以移步至:✏️✏️✏️
七,Spring Boot 当中的 yaml 语法使用-CSDN博客

首先,我们在
resources
类路径下创建一个名为
application.yaml
的文件。

在这里插入图片描述

编写如下内容:

在这里插入图片描述

spring:
  mvc:
    static-path-pattern: /rainbowsea/**


运行测试:

在这里插入图片描述

被我们改为了我们自己的
/rainbowsea/**
注意:后面的
/**
不可以省略 。不然无法访问的。

在这里插入图片描述


4. 改变Spring Boot当中的默认的静态资源路径(实现自定义静态资源路径)

Spring Boot 也是支持我们自定义静态资源路径,提高了灵活性。

改变默认的静态资源路径,比如:我们自己在类路径下增加 test 目录,作为静态资源路径,并完成测试。

同样要想改变 Spring Boot 当中的默认静态资源,这里我们还是使用 yaml 语法进行。在
resources
类路径下创建一个名为
application.yaml
的文件。

yaml 编写如下:

在这里插入图片描述

spring:
  web:
    resources:
      # 修改/指定 静态资源的访问路径/位置
      static-locations: ["classpath:/test/"] # 仿写
# 注意:尽量在最左边开始写,才有更多的提示

在这里插入图片描述

在 resources 类路径下,创建一个 test目录,同时在该目录下,放入一个名为
5.jpg
的图片,访问测试。

在这里插入图片描述

打开浏览器访问测试:

在这里插入图片描述

本质上: static-locations修改的是 WebProperties类当中staticlocations属性的值(也就是 springboot

的默认静态路径)。

所以这里我们修改了 Spring Boot 的默认静态资源路径,之前的放置在Spring Boot 默认的静态路径下的资源就无法被访问到了。

在这里插入图片描述

想要:保留原来Spring Boot 的默认静态资源路径,只需要把原来的Spring Boot 默认的路径添加上就可以了。

在这里插入图片描述

运行测试:

在这里插入图片描述

5. 静态资源访问注意事项和细节

  1. 注意:直接放在 resources 类的根路径下,是访问不到的。因为我们从 WebProperties 类源码上,就知道了,Spring Boot的默认静态资源路径,就只有四个,而 resources 类路径是不属于这四个当中的。
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};

运行测试:我们在 resources 类路径下,放入 6.jpg 图片,试试,可不可以被直接访问。

在这里插入图片描述

打开浏览器访问测试:

在这里插入图片描述

  1. 当默认请求的路径的资源的名字和 Controller 控制器请求处理的路径一样,冲突的时候。**优先看Controller能不能处理;不能,处理的请求交给静态资源处理,如果静态资源也找不到则相应点资源,则报:404找不到的,页面。

静态资源被访问原理:静态映射是 /** , 也就是对所有请求拦截,请求进来,先看Controller能不能处理,不能处理的请求交给静态资源处理,如果静态资源找不到则相应 404页面

在这里插入图片描述

在这里插入图片描述

package com.rainbowsea.springboot.controller;


import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // @Controller + @ResponseBody
public class HiController {

    @RequestMapping("1.jpg")  // Controller 控制处理的请求的路径和静态资源的名字冲突
    public String hi(){
        return "hi";
    }
}

在这里插入图片描述


6. 总结:

  1. 理解Spring Boot 静态资源放在类路径下: /static, /public, /resources, /META-INF/resources 就可以被直接访问-对应文件(
    这是 Spring Boot 的默认设置好的
    )。关于这一点,我们从
    WebProperties.java
    这个类的源码上可以找到,对应的配置属性。
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
注意:classpath:/resources/ 表示服务器就会在 resources 路径下找,你在浏览器当中输入的url地址的时候,不可以输入 resources 目录,因为服务器就是会在 classpath:/resources/ 找的,而如果你写了resources在浏览器上的话,你想表达的就是:让浏览器从resources/resouces的路径下找,这是找不到的报404错误
  1. 访问方式: 默认:项目根路径/+静态资源名 比如:
    http://localhost:8080/hi.html
    。关于这一点,我们可以从 WebMvcProperties.java 类当中找到答案。
  2. 改变静态资源访问前缀,比如我们希望
    http://locahost:8080/rainbowsea/*
    下的请求路径,去请求静态资源,应用场景:静态资源访问前缀和控制器请求路径冲突。

在这里插入图片描述

  1. 改变Spring Boot当中的默认的静态资源路径(实现自定义静态资源路径)。

在这里插入图片描述

  1. 注意:直接放在 resources 类的根路径下,是访问不到的。因为我们从 WebProperties 类源码上,就知道了,Spring Boot的默认静态资源路径,就只有四个,而 resources 类路径是不属于这四个当中的。

7. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

写在前面

Spring的核心思想就是容器,当容器refresh的时候,外部看上去风平浪静,其实内部则是一片惊涛骇浪,汪洋一片。Springboot更是封装了Spring,遵循约定大于配置,加上
自动装配的机制
。很多时候我们只要引用了一个依赖,几乎是零配置就能完成一个功能的装配。

由spring提供的、在容器或bean生命周期各个阶段、供spring框架回调使用的函数方法,即为扩展点。扩展点体现了Spring框架的灵活性、业务亲和性。使开发人员可以在不修改spring源码的情况下,对容器和bean的行为、属性进行额外的管理。

想要把
自动装配
玩的转,就必须要了解spring对于
bean的构造生命周期
以及各个扩展接口,当然了解了bean的各个生命周期也能促进我们加深对spring的理解。业务代码也能合理利用这些扩展点写出更优雅的代码。

在网上搜索spring扩展点,发现很少有博文说的很全的,只有一些常用的扩展点的说明。所以在这篇文章里,我总结了几乎Spring & Springboot所有的扩展接口,各个扩展点的使用场景,并整理出一个bean在spring中从被加载到初始化到销毁的所有可扩展点的顺序调用图。

本文不讲原理,只将扩展点与使用方式讲清楚,特别是调用顺序,原理可以移步
IOC系列文章

Bean的生命周期
,后续会不断更新对应原理及源码解析。大家可以
把这篇文章当成一个工具书
,当忘了执行顺序时,或者忘了如何使用这个扩展方式时,可以再回过头来看看。

spring扩展点执行顺序

ApplicationContextInitializer

org.springframework.context.ApplicationContextInitializer

介绍

这是整个spring容器在刷新之前初始化
ConfigurableApplicationContext
的回调接口,简单来说,就是在
容器刷新refresh之前调用
此类的
initialize
方法。这个接口的主要目的是在 Spring 应用上下文初始化的早期阶段进行一些配置或调整,以便在上下文加载后可以使用这些配置。

此接口,
Spring Framework
自己没有提供任何的实现类,但在
SpringBoot
对它有较多的扩展实现。

使用场景

  1. 在应用启动时进行环境配置:可以使用
    ApplicationContextInitializer
    来在应用上下文初始化时进行一些环境相关的配置,例如设置系统属性、加载外部配置文件等。
public class EnvironmentInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        environment.setActiveProfiles("development");
        System.out.println("配置文件设置为development");
    }
}
  1. 注册自定义的
    BeanFactoryPostProcessor
    或者
    BeanPostProcessor

    ApplicationContextInitializer
    可以用来注册自定义的
    BeanFactoryPostProcessor
    或者
    BeanPostProcessor
    ,以便在 Bean 初始化之前或之后进行某些自定义处理。
public class CustomBeanFactoryPostProcessorInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        applicationContext.addBeanFactoryPostProcessor(beanFactory -> {
            // 添加自定义的 BeanFactoryPostProcessor
            System.out.println("添加了自定义BeanFactory后处理器...");
        });
    }
}
  1. 动态地添加
    PropertySource
    :可以在初始化过程中动态地添加
    PropertySource
    ,以便后续的 Bean 定义和初始化过程中可以使用这些属性。
public class PropertySourceInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        MutablePropertySources propertySources = applicationContext.getEnvironment().getPropertySources();
        propertySources.addFirst(new MapPropertySource("customPropertySource", Collections.singletonMap("customKey", "customValue")));
        System.out.println("已添加自定义属性源");
    }
}

Spring环境下添加扩展点

在Spring环境下自定义实现一个
ApplicationContextInitializer
让并且它生效的方式有三种:

  1. 手动调用的setXXX方法添加
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();

// Add initializer
context.addApplicationListener(new TestApplicationContextInitializer());

// Set config locations and refresh context
context.setConfigLocation("classpath:applicationContext.xml");
context.refresh();

// Use the context
// ...

context.close();
  1. Spring 的 XML 配置文件中注册
<context:initializer class="com.seven.springsrpingbootextentions.extentions.TestApplicationContextInitializer"/>
  1. web.xml 文件配置
<context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>com.seven.springsrpingbootextentions.extentions.TestApplicationContextInitializer</param-value>
</context-param>

SpringBoot环境下添加扩展点

示例,展示了如何实现一个ApplicationContextInitializer来添加一个自定义的属性源:

public class TestApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        MutablePropertySources propertySources = applicationContext.getEnvironment().getPropertySources();
        
        // 创建自定义的属性源
        Map<String, Object> customProperties = new HashMap<>();
        customProperties.put("custom.property", "custom value");
        MapPropertySource customPropertySource = new MapPropertySource("customPropertySource", customProperties);
        
        // 将自定义属性源添加到应用程序上下文的属性源列表中
        propertySources.addFirst(customPropertySource);
    }
}

在SpringBoot中让它生效的方式也有三种:

  1. 在启动类中用
    springApplication.addInitializers(new TestApplicationContextInitializer())
    语句加入
@SpringBootApplication
public class MySpringExApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(MySpringExApplication.class);
        application.addInitializers(new TestApplicationContextInitializer()); // 直接在SpringApplication中添加
        application.run(args);
    }
}
  1. application配置文件 配置
    com.seven.springsrpingbootextentions.extentions.TestApplicationContextInitializer
# application.yml文件
context:
  initializer:
    classes: com.seven.springsrpingbootextentions.extentions.TestApplicationContextInitializer
  1. Spring
    SPI
    扩展,在spring.factories中加入(官方推荐):
com.seven.springsrpingbootextentions.extentions.TestApplicationContextInitializer

SpringBoot内置的ApplicationContextInitializer

  • DelegatingApplicationContextInitializer
    :使用环境属性
    context.initializer.classes
    指定的初始化器(initializers)进行初始化工作,如果没有指定则什么都不做。通过它使得我们可以把自定义实现类配置在
    application.properties
    里成为了可能。

  • ContextIdApplicationContextInitializer
    :设置Spring应用上下文的ID,Id设置为啥值会参考环境属性:


    • spring.application.name
    • vcap.application.name
    • spring.config.name
    • spring.application.index
    • vcap.application.instance_index
    • 如果这些属性都没有,ID使用application。
  • ConfigurationWarningsApplicationContextInitializer
    :对于一般配置错误在日志中作出警告

  • ServerPortInfoApplicationContextInitializer
    :将内置servlet容器实际使用的监听端口写入到Environment环境属性中。这样属性
    local.server.port
    就可以直接通过
    @Value
    注入到测试中,或者通过环境属性Environment获取。

  • SharedMetadataReaderFactoryContextInitializer
    :创建一个 SpringBoot 和 ConfigurationClassPostProcessor 共用的 CachingMetadataReaderFactory对象。实现类为:ConcurrentReferenceCachingMetadataReaderFactory

  • ConditionEvaluationReportLoggingListener
    :将ConditionEvaluationReport写入日志。

BeanFactoryPostProcessor

org.springframework.beans.factory.config.BeanFactoryPostProcessor

介绍

这个接口是
beanFactory
的扩展接口,调用时机在spring在读取
beanDefinition
信息之后,实例化bean之前。虽然此时不能再注册beanDefinition,但是可以趁着bean没有实例化,可以修改 Spring 容器启动时修改其内部的
BeanDefinition
。通过实现
BeanFactoryPostProcessor
接口,开发者可以在 Bean 实例化之前修改 Bean 的定义元数据,例如Scope、依赖查找方式、初始化方法、修改属性值、添加额外的元数据等,进而影响初始化行为。

在应用程序启动时,Spring容器会自动检测并调用所有实现了BeanFactoryPostProcessor接口的类的postProcessBeanFactory方法。开发人员可以利用这个方法来实现自定义的逻辑,从而实现一些高级的自定义逻辑和功能扩展。此方法只调用一次,
同时记住不要在这里做Bean的实例化

使用场景

  1. 修改 Bean 属性:可以动态地改变某些配置属性或者注入额外的依赖。
public class PropertyModifierBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("myBean");
        MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
        propertyValues.addPropertyValue("propertyName", "newValue");
    }
}
  1. 动态注册 Bean:可以根据配置文件或者系统环境变量来决定是否注册某个 Bean。
public class ConditionalBeanRegistrar implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        if (someCondition()) {
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyBean.class);
            beanFactory.registerBeanDefinition("myConditionalBean", beanDefinitionBuilder.getBeanDefinition());
        }
    }

    private boolean someCondition() {
        // 自定义条件逻辑
        return true;
    }
}
  1. 修改 Bean 定义:可以修改 Bean 的作用域、初始化和销毁方法等定义信息。
public class ScopeModifierBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("myBean");
        beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
    }
}
  1. 属性占位符替换:可以使用
    PropertyPlaceholderConfigurer
    实现
    BeanFactoryPostProcessor
    接口,来替换 Bean 定义中的属性占位符。
public class CustomPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {

    @Override
    protected void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props)
            throws BeansException {
        super.processProperties(beanFactory, props);
        // 自定义属性处理逻辑
    }
}

其它使用场景:

  1. 配置中心集成
    :当需要从外部配置中心(如 Spring Cloud Config 或 Apache Zookeeper)动态加载配置并修改 Bean 定义时,可以使用
    BeanFactoryPostProcessor
  2. 多环境支持
    :根据不同的环境(如开发、测试、生产环境)动态修改 Bean 的定义,确保不同环境下的 Bean 配置有所不同。
  3. 动态注册 Bean
    :在应用运行时,根据条件动态注册或者取消 Bean,比如在某些特定条件下才需要注册某些 Bean。
  4. 复杂业务应用
    :有时候会需要根据复杂的业务规则动态调整 Bean 的配置,这时候
    BeanFactoryPostProcessor
    非常有用。

BeanDefinitionRegistryPostProcessor

org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor

介绍

BeanDefinitionRegistryPostProcessor为容器级后置处理器。
容器级的后置处理器会在Spring容器初始化后、刷新前执行一次,用于动态注册Bean到容器

通过 BeanFactoryPostProcessor 的子类 BeanDefinitionRegistryPostProcessor,可以注册一个你自己的BeanDefinition对象到容器中,等待容器内部依次调用进行对象实例化就能当bean用了。

BeanDefinitionRegistryPostProcessor用于在bean解析后实例化之前通过BeanDefinitionRegistry对BeanDefintion进行增删改查。

前文介绍的BeanFactoryPostProcessor是这个接口的父类
,因此实现BeanDefinitionRegistryPostProcessor这个接口,也可以重写其父类。但实现了BeanDefinitionRegistryPostProcessor的postProcessBeanFactory方法会先执行,再执行实现了BeanFactoryPostProcessor的postProcessBeanFactory。具体看调用顺序图

使用场景

  1. 修改现有的 BeanDefinition:可以在 Bean 实例化之前修改现有的
    BeanDefinition
    ,如更改其属性值或作用域。
public class BeanDefinitionModifier implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        System.out.println("在 postProcessBeanDefinitionRegistry 中修改现有的 BeanDefinition");

        if (registry.containsBeanDefinition("myExistingBean")) {
            BeanDefinition beanDefinition = registry.getBeanDefinition("myExistingBean");
            MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
            propertyValues.add("propertyName", "newValue");
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 此方法可以留空或用于进一步处理
    }
}
  1. 条件性地注册 Bean:基于某些条件(如环境变量、配置文件等)动态注册或取消注册某些 Bean。
public class ConditionalBeanRegistrar implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        System.out.println("在 postProcessBeanDefinitionRegistry 中根据条件注册 Bean");

        if (someCondition()) {
            AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
                    .genericBeanDefinition(ConditionalBean.class)
                    .getBeanDefinition();
            registry.registerBeanDefinition("conditionalBean", beanDefinition);
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 此方法可以留空或用于进一步处理
    }

    private boolean someCondition() {
        // 自定义条件逻辑
        return true;
    }
}
  1. 扫描和注册自定义注解的 Bean:实现自定义注解的扫描逻辑,并动态注册这些注解标注的 Bean。
public class CustomAnnotationBeanRegistrar implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        System.out.println("在 postProcessBeanDefinitionRegistry 中扫描并注册自定义注解的 Bean");

        // 自定义扫描逻辑,假设找到一个类 MyAnnotatedBean
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
                .genericBeanDefinition(MyAnnotatedBean.class)
                .getBeanDefinition();
        registry.registerBeanDefinition("myAnnotatedBean", beanDefinition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 此方法可以留空或用于进一步处理
    }
}
  1. 比如依赖Redis.jar,如果该依赖jar存在,则用
    redis当缓存
    ,否则就用
    本地缓存
    。这个需求完全可以在postProcessBeanDefinitionRegistry中利用Class.forName判断依赖,存在的话则注册对应class到容器。
@Configuration
public class AppConfig {

    @Bean
    public static BeanDefinitionRegistryPostProcessor customBeanDefinitionRegistryPostProcessor() {
        return new BeanDefinitionRegistryPostProcessor() {

            @Override
            public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
                System.out.println("在 postProcessBeanDefinitionRegistry 中根据条件注册缓存实现类");

                try {
                    // 检查 Redis 依赖是否存在
                    Class.forName("redis.clients.jedis.Jedis");
                    System.out.println("检测到 Redis 依赖,注册 RedisCacheService");

                    AbstractBeanDefinition redisCacheBeanDefinition = BeanDefinitionBuilder
                            .genericBeanDefinition(RedisCacheService.class)
                            .getBeanDefinition();
                    registry.registerBeanDefinition("cacheService", redisCacheBeanDefinition);

                } catch (ClassNotFoundException e) {
                    System.out.println("未检测到 Redis 依赖,注册 LocalCacheService");

                    AbstractBeanDefinition localCacheBeanDefinition = BeanDefinitionBuilder
                            .genericBeanDefinition(LocalCacheService.class)
                            .getBeanDefinition();
                    registry.registerBeanDefinition("cacheService", localCacheBeanDefinition);
                }
            }

            @Override
            public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
                // 此方法可以留空或用于进一步处理
            }
        };
    }
}
  1. Mybatis就是用BeanFactoryPostProcessor去注册的mapper

MapperScannerConfigurer
的主要功能是通过扫描指定的包路径,找到所有标注了
@Mapper
注解(或其他指定注解)的接口,并将这些接口注册为 Spring 的 BeanDefinition。这样,Spring 容器在启动时会自动创建这些 Mapper 接口的代理对象,并将其注入到需要的地方。

以下是
MapperScannerConfigurer
的核心代码片段:

// org.mybatis.spring.mapper.MapperScannerConfigurer#postProcessBeanDefinitionRegistry
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware {

    private String basePackage;
    private ApplicationContext applicationContext;

    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        //设置其资源加载器为当前的 ApplicationContext
        scanner.setResourceLoader(this.applicationContext);
        scanner.registerFilters();
        //调用 scanner.scan(this.basePackage) 方法,扫描指定的包路径,找到所有符合条件的 Mapper 接口,并将它们注册为 Spring 的 BeanDefinition。
        scanner.scan(this.basePackage);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 此方法可以留空或用于进一步处理
    }
}

BeanPostProcessor

org.springframework.beans.factory.config.BeanPostProcessor

介绍

BeanPostProcessor
接口定义了两个基本的Bean初始化回调方法,在属性赋值前后执行。

  • postProcessBeforeInitialization
    :在 Bean 初始化方法(如
    @PostConstruct

    InitializingBean.afterPropertiesSet
    或自定义初始化方法)调用之前执行;返回的对象将是实际注入到容器中的 Bean,如果返回 null,则该 Bean 不会被注册。可用于创建代理类
  • postProcessAfterInitialization
    :初始化bean之后,返回的对象将是实际注入到容器中的 Bean,如果返回 null,则该 Bean 不会被注册。

使用场景

  1. 初始化前后进行自定义逻辑:在 Bean 初始化之前或之后执行一些自定义的操作,例如设置一些属性、进行依赖注入、执行某些检查等。
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyBean) {
            System.out.println("bean初始化前: " + beanName);
            ((MyBean) bean).setName("Modified Name Before Initialization");
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyBean) {
            System.out.println("bean初始化后: " + beanName);
        }
        return bean;
    }
}

public class MyBean {
    private String name;

    public MyBean(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void init() {
        System.out.println("bean正在init: " + name);
    }

    @Override
    public String toString() {
        return "MyBean{name='" + name + "'}";
    }

  1. 代理对象的生成:在
    postProcessAfterInitialization
    方法中生成 Bean 的代理对象,用于 AOP(面向切面编程)或其他用途。
@Component
public class ProxyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyBean) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(bean.getClass());
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    System.out.println("Before method: " + method.getName());
                    Object result = proxy.invokeSuper(obj, args);
                    System.out.println("After method: " + method.getName());
                    return result;
                }
            });
            return enhancer.create();
        }
        return bean;
    }
}
  1. 日志记录和监控:记录 Bean 的初始化过程,进行性能监控、日志记录等。
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("开始初始化bean: " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("初始化bean结束: " + beanName);
        return bean;
    }
}

  1. 自动装配和注入:在初始化前后进行自动装配和注入,例如通过反射为某些字段注入值。
@Component
public class AutowireBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Field[] fields = bean.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(AutowireCustom.class)) {
                field.setAccessible(true);
                try {
                    field.set(bean, "Injected Value");
                } catch (IllegalAccessException e) {
                    throw new BeansException("Failed to autowire field: " + field.getName(), e) {};
                }
            }
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

@Retention(RetentionPolicy.RUNTIME)
public @interface AutowireCustom {
}

public class MyBean {
    @AutowireCustom
    private String customField;

    public MyBean() {
    }

    @Override
    public String toString() {
        return "MyBean{customField='" + customField + "'}";
    }
}

InstantiationAwareBeanPostProcessor

org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor

介绍

该接口继承了
BeanPostProcessor
接口,因为InstantiationAwareBeanPostProcessor也属于Bean级的后置处理器,区别如下:

BeanPostProcess
接口只
在bean的初始化阶段进行扩展
(注入spring上下文前后),而
InstantiationAwareBeanPostProcessor
接口在此基础上增加了3个方法,把可扩展的范围增加了实例化阶段和属性注入阶段。

该类主要的扩展点有以下6个方法,其中有两个是BeanPostProcessor的扩展,主要在bean生命周期的两大阶段:
实例化阶段

初始化阶段
,下面一起进行说明,按调用顺序为:

  • postProcessBeforeInstantiation
    :在Bean实例化之前调用,如果返回null,一切按照正常顺序执行;如果返回的是一个实例的对象,那么
    postProcessAfterInstantiation()
    会执行,其他的扩展点将不再触发。
  • postProcessAfterInstantiation
    :在Bean实例化之后调用,可以对已实例化的Bean进行进一步的自定义处理。
  • postProcessPropertyValues
    (方法在spring5.1版本后就已弃用):bean已经实例化完成,在属性注入时阶段触发,
    @Autowired

    @Resource
    等注解原理基于此方法实现;可以修改Bean的属性值或进行其他自定义操作,
    当postProcessAfterInstantiation返回true才执行。
  • postProcessBeforeInitialization
    (BeanPostProcessor的扩展):初始化bean之前,相当于把bean注入spring上下文之前;可用于创建代理类,如果返回的不是 null(也就是返回的是一个代理类) ,那么后续只会调用 postProcessAfterInitialization() 方法
  • postProcessAfterInitialization
    (BeanPostProcessor的扩展):初始化bean之后,相当于把bean注入spring上下文之后;返回值会影响 postProcessProperties() 是否执行,其中返回 false 的话,是不会执行。
  • postProcessProperties()
    :在 Bean 设置属性前调用;用于修改 bean 的属性,如果返回值不为空,那么会更改指定字段的值

注意:InstantiationAwareBeanPostProcessor和 BeanPostProcessor 是可以同时被实现的,并且也会同时生效,但是InstantiationAwareBeanPostProcessor的
执行时机要稍早于BeanPostProcessor
;具体看上面调用顺序图

InstantiationAwareBeanPostProcessor
提供了更细粒度的控制,可以在 Bean 的实例化和属性设置过程中插入自定义逻辑。无论是替换默认的实例化过程、控制依赖注入,还是修改属性值,
InstantiationAwareBeanPostProcessor
都提供了强大的灵活性和可扩展性,使得开发者可以在 Bean 的生命周期中进行更精细的控制。

使用场景

  1. 在实例化之前替换 Bean:替换默认的 Bean 实例化过程,可能是返回一个代理对象。
@Component
public class CustomInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {

    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        if (beanClass == MyBean.class) {
            System.out.println("实例化之前替换 Bean: " + beanName);
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(beanClass);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    System.out.println("调用方法: " + method.getName());
                    return proxy.invokeSuper(obj, args);
                }
            });
            return enhancer.create();
        }
        return null;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("初始化之后的 Bean: " + beanName);
        return bean;
    }
}
  1. 控制实例化后的依赖注入过程:在实例化后但在依赖注入之前进行一些自定义逻辑。
@Component
public class DependencyInjectionControlPostProcessor implements InstantiationAwareBeanPostProcessor {

    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyBean) {
            System.out.println("实例化之后控制依赖注入: " + beanName);
            return false; // 不进行默认的依赖注入
        }
        return true;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("初始化之后的 Bean: " + beanName);
        return bean;
    }
}
  1. 修改属性值:在属性值设置过程中进行干预,修改或添加属性值。
@Component
public class PropertyModificationPostProcessor implements InstantiationAwareBeanPostProcessor {

    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        if (bean instanceof MyBean) {
            System.out.println("设置属性值之前: " + beanName);
            // 修改属性值的逻辑
        }
        return pvs;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("初始化之后的 Bean: " + beanName);
        return bean;
    }
}

SmartInstantiationAwareBeanPostProcessor

org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor

介绍

SmartInstantiationAwareBeanPostProcessor
与其他扩展点最明显的不同,就是在实际的业务开发场景中应用到的机会并不多,主要是在Spring内部应用。

该扩展接口有3个触发点方法:

  • predictBeanType
    :该触发点发生在
    postProcessBeforeInstantiation
    之前(也就是在 InstantiationAwareBeanPostProcessor的方法之前,在图上并没有标明,因为
    一般不太需要扩展这个点
    ),这个方法用于预测Bean的类型,返回第一个预测成功的Class类型,如果不能预测,则返回null;当调用
    BeanFactory.getType(name)
    时当通过bean的名字无法得到bean类型信息时就调用该回调方法来决定类型信息。
  • determineCandidateConstructors
    :该触发点发生在
    postProcessBeforeInstantiation
    之后,用于决定使用哪个构造器构造Bean,返回的是该bean的所有构造函数列表;如果不指定,默认为null,即bean的无参构造方法。用户可以扩展这个点,来自定义选择相应的构造器来实例化这个bean。
  • getEarlyBeanReference
    :该触发点发生在
    postProcessAfterInstantiation
    之后,主要用于Spring循环依赖问题的解决,如果Spring中检测不到循环依赖,这个方法不会被调用;当存在Spring循环依赖这种情况时,当bean实例化好之后,为了防止有循环依赖,会提前暴露回调方法,用于bean实例化的后置处理,会在InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation方法触发执行之后执行;

注意:同InstantiationAwareBeanPostProcessor,由于SmartInstantiationAwareBeanPostProcessor 是 InstantiationAwareBeanPostProcessor的子类,因此也SmartInstantiationAwareBeanPostProcessor 也同样能扩展 InstantiationAwareBeanPostProcessor的所有方法。但是如果有两个类分别重写了 SmartInstantiationAwareBeanPostProcessor 和 InstantiationAwareBeanPostProcessor 的方法,那么
重写 InstantiationAwareBeanPostProcessor 的类的方法 会先于 重写了 SmartInstantiationAwareBeanPostProcessor的类的方法
(注意,这里说的是两者都有的方法)。

使用场景

  1. 自定义构造函数选择:在实例化 Bean 时,选择特定的构造函数。
@Component
public class CustomConstructorSelectionPostProcessor implements SmartInstantiationAwareBeanPostProcessor {

    @Override
    public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, String beanName) throws BeansException {
        if (beanClass == MyBean.class) {
            System.out.println("选择自定义构造函数: " + beanName);
            try {
                return new Constructor<?>[] { beanClass.getConstructor(String.class) };
            } catch (NoSuchMethodException e) {
                throw new BeansException("找不到指定的构造函数", e) {};
            }
        }
        return null;
    }
}

public class MyBean {
    private String name;

    public MyBean() {
        this.name = "Default Name";
    }

    public MyBean(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MyBean{name='" + name + "'}";
    }
}
  1. 解决循环依赖问题:通过提供早期 Bean 引用,解决循环依赖问题。
@Component
public class EarlyBeanReferencePostProcessor implements SmartInstantiationAwareBeanPostProcessor {

    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyBean) {
            System.out.println("获取早期 Bean 引用: " + beanName);
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(bean.getClass());
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    System.out.println("调用方法: " + method.getName());
                    return proxy.invokeSuper(obj, args);
                }
            });
            return enhancer.create();
        }
        return bean;
    }
}
  1. 预测 Bean 类型:在 Bean 实例化之前,预测 Bean 的类型。
@Component
public class BeanTypePredictionPostProcessor implements SmartInstantiationAwareBeanPostProcessor {

    @Override
    public Class<?> predictBeanType(Class<?> beanClass, String beanName) throws BeansException {
        if (beanClass == MyBean.class) {
            System.out.println("预测 Bean 类型: " + beanName);
            return MyBean.class;
        }
        return null;
    }
}

MergedBeanDefinitionPostProcessor

org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor

介绍

MergedBeanDefinitionPostProcessor 继承自 BeanPostProcessor。调用的时机是,在实例化之后进行的调用,只要是收集bean上的属性的,比如收集标记了某些注解的字段或者方法,都可以基于MergedBeanDefinitionPostProcessor来进行扩展。

对于不同方式导入的Bean定义,如果存在重复对同一个Bean的定义,则会根据allowBeanDefinitionOverriding属性是否设置为true,判断是否允许Bean定义的覆盖,如果不允许,则抛出异常。而在Bean实例化之前,会进行BeanDefinition类型的归一化,即 mergeBeanFintion ,统一转换为RootBeanfintion进行后续处理。当然,这里的merge更多指代的是父子Bean定义的合并。

也用于收集bean上的注解,比如常见的@Value、@NacosValue、@Mapper等,再将收集好的数据缓存在injectionMetadataCache中,以便后续比如属性注入的时候使用。

该接口有两个扩展方法:

  • postProcessMergedBeanDefinition
    :此方法在Spring将多个Bean定义合并为一个
    RootBeanDefinition
    之后,但在实例化Bean之前被调用。主要作用是让开发者有机会在Bean定义合并后,对其进行进一步的定制和调整。使用场景如下:
    • 自定义注解处理
      :处理自定义注解并将其应用于Bean定义。
    • 属性修改
      :在Bean实例化之前对Bean定义中的某些属性进行调整或设置默认值。
  • resetBeanDefinition
    :此方法在Bean定义被重置时调用。它通常用于清理或重置与特定Bean定义相关的状态或缓存。使用场景如下:
    • 状态清理
      :清理缓存或临时状态,以便Bean定义可以被重新解析。
    • 重置自定义元数据
      :在Bean定义被重置时,重置自定义的元数据或状态。

使用场景

  1. 对合并后的 Bean 定义信息进行修改:在 Bean 实例化之前,修改其定义信息,例如添加属性值或修改构造函数参数。
@Component
public class CustomMergedBeanDefinitionPostProcessor implements MergedBeanDefinitionPostProcessor, BeanPostProcessor {

    @Override
    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        if (beanType == MyBean.class) {
            System.out.println("合并后的 Bean 定义信息, Bean 名称: " + beanName);
            // 修改合并后的 Bean 定义信息
            beanDefinition.getPropertyValues().add("name", "修改后的名称");
        }
    }

    @Override
    public void resetBeanDefinition(String beanName) {
        System.out.println("重置 Bean 定义信息, Bean 名称: " + beanName);
        // 实现重置逻辑
    }

}

public class MyBean {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MyBean{name='" + name + "'}";
    }
}

  1. 实现通用的自定义逻辑:在所有 Bean 实例化之前,执行一些通用的自定义逻辑。
@Component
public class CommonLogicMergedBeanDefinitionPostProcessor implements MergedBeanDefinitionPostProcessor, BeanPostProcessor {

    @Override
    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        System.out.println("处理合并后的 Bean 定义信息, Bean 名称: " + beanName);
        // 添加通用的自定义逻辑,例如给所有 Bean 添加某个属性
        beanDefinition.getPropertyValues().add("commonProperty", "通用属性值");
    }

    @Override
    public void resetBeanDefinition(String beanName) {
        System.out.println("重置 Bean 定义信息, Bean 名称: " + beanName);
        // 实现重置逻辑
    }

}

public class MyBean {
    private String name;
    private String commonProperty;

    public void setName(String name) {
        this.name = name;
    }

    public void setCommonProperty(String commonProperty) {
        this.commonProperty = commonProperty;
    }

    @Override
    public String toString() {
        return "MyBean{name='" + name + "', commonProperty='" + commonProperty + "'}";
    }
}
  1. 条件性地重置 Bean 定义信息:在某些条件下重置 Bean 的定义信息,使得下一次的实例化可以使用更新后的定义信息。
@Component
public class ConditionalResetMergedBeanDefinitionPostProcessor implements MergedBeanDefinitionPostProcessor, BeanPostProcessor {

    @Override
    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        System.out.println("处理合并后的 Bean 定义信息, Bean 名称: " + beanName);
        // 这里可以根据条件决定是否修改 Bean 定义
        if (beanName.equals("conditionalBean")) {
            beanDefinition.getPropertyValues().add("name", "重置后的名称");
        }
    }

    @Override
    public void resetBeanDefinition(String beanName) {
        System.out.println("重置 Bean 定义信息, Bean 名称: " + beanName);
        // 这里可以实现条件性重置逻辑
    }
}

public class ConditionalBean {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "ConditionalBean{name='" + name + "'}";
    }
}

BeanNameAware

org.springframework.beans.factory.BeanNameAware

介绍

这个类是Aware扩展的一种,触发点在bean的初始化之前,也就是
postProcessBeforeInitialization
之前,这个类的触发点方法只有一个:
setBeanName

用于让 Bean 获得其在 Spring 容器中的名称。实现了
BeanNameAware
接口的 Bean 可以在初始化时获得自身的 Bean 名称,这在某些需要根据 Bean 名称进行逻辑处理的场景非常有用。

使用场景

  1. 记录或日志输出 Bean 名称:在某些应用场景中,开发者可能希望在 Bean 初始化时记录或输出 Bean 的名称。这对调试和日志记录非常有帮助。
@Component
public class LoggingBean implements BeanNameAware {

    private String beanName;

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("设置 Bean 名称: " + name);
    }

    public void doSomething() {
        System.out.println("正在执行某些操作, 当前 Bean 名称: " + beanName);
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        LoggingBean loggingBean = context.getBean(LoggingBean.class);
        loggingBean.doSomething();
    }
}
  1. 根据 Bean 名称实现条件性逻辑:有时,一个 Bean 可能需要根据其名称决定执行不同的逻辑。例如,可以在初始化过程或某些方法调用中根据 Bean 名称执行特定操作。
@Component
public class ConditionalLogicBean implements BeanNameAware {

    private String beanName;

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("设置 Bean 名称: " + name);
    }

    public void performAction() {
        if ("conditionalLogicBean".equals(beanName)) {
            System.out.println("执行特定逻辑, 因为这是 conditionalLogicBean");
        } else {
            System.out.println("执行普通逻辑");
        }
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ConditionalLogicBean conditionalLogicBean = context.getBean(ConditionalLogicBean.class);
        conditionalLogicBean.performAction();
    }
}
  1. 动态注册多个同类型的 Bean:在某些复杂的应用场景中,可能需要动态注册多个同类型的 Bean,并且需要根据名称区分它们。实现
    BeanNameAware
    接口可以很方便地获取和使用这些 Bean 的名称。
@Component("beanA")
public class DynamicBeanA implements BeanNameAware {

    private String beanName;

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("设置 Bean 名称: " + name);
    }

    public void execute() {
        System.out.println("执行 Bean: " + beanName);
    }
}

@Component("beanB")
public class DynamicBeanB implements BeanNameAware {

    private String beanName;

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("设置 Bean 名称: " + name);
    }

    public void execute() {
        System.out.println("执行 Bean: " + beanName);
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        DynamicBeanA beanA = (DynamicBeanA) context.getBean("beanA");
        DynamicBeanB beanB = (DynamicBeanB) context.getBean("beanB");
        beanA.execute();
        beanB.execute();
    }
}

BeanClassLoaderAware

org.springframework.beans.factory.BeanClassLoaderAware

介绍

用于让一个 Bean 获取到加载它的
ClassLoader
。实现这个接口的 Bean 会在其属性设置完成后、初始化方法调用之前被注入
ClassLoader
。该接口定义了一个方法:

  • void setBeanClassLoader(ClassLoader classLoader)
    :在某些需要动态加载类的场景中,获取
    ClassLoader
    是非常有用的。

使用场景

  1. 动态加载类:有时候,我们可能需要在运行时动态加载类,利用
    BeanClassLoaderAware
    可以方便地获取到
    ClassLoader
    来实现这一需求。
@Component
public class DynamicClassLoader implements BeanClassLoaderAware {

    private ClassLoader classLoader;

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
        System.out.println("已设置类加载器");
    }

    public void loadClass(String className) {
        try {
            Class<?> clazz = classLoader.loadClass(className);
            System.out.println("已加载类:" + clazz.getName());
        } catch (ClassNotFoundException e) {
            System.out.println("类未找到:" + className);
        }
    }
}

@SpringBootApplication
public class AppConfig {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AppConfig.class, args);
        DynamicClassLoader dynamicClassLoader = context.getBean(DynamicClassLoader.class);
        dynamicClassLoader.loadClass("java.util.ArrayList");
        dynamicClassLoader.loadClass("不存在的类");
    }
}
  1. 检查类的可用性:在某些情况下,我们可能需要检查某个类是否在当前的类路径中可用。利用
    BeanClassLoaderAware
    可以方便地实现这一需求。
@Component
public class ClassAvailabilityChecker implements BeanClassLoaderAware {

    private ClassLoader classLoader;

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
        System.out.println("已设置类加载器");
    }

    public boolean isClassAvailable(String className) {
        try {
            Class<?> clazz = classLoader.loadClass(className);
            System.out.println("类可用:" + clazz.getName());
            return true;
        } catch (ClassNotFoundException e) {
            System.out.println("类不可用:" + className);
            return false;
        }
    }
}

@SpringBootApplication
public class AppConfig {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AppConfig.class, args);
        ClassAvailabilityChecker checker = context.getBean(ClassAvailabilityChecker.class);
        checker.isClassAvailable("java.util.HashMap");
        checker.isClassAvailable("不存在的类");
    }
}
  1. 加载资源文件:通过
    BeanClassLoaderAware
    获取的
    ClassLoader
    ,我们还可以方便地加载资源文件。
@Component
public class ResourceLoader implements BeanClassLoaderAware {

    private ClassLoader classLoader;

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
        System.out.println("已设置类加载器");
    }

    public void loadResource(String resourcePath) {
        InputStream inputStream = classLoader.getResourceAsStream(resourcePath);
        if (inputStream != null) {
            System.out.println("资源已加载:" + resourcePath);
        } else {
            System.out.println("资源未找到:" + resourcePath);
        }
    }
}

@SpringBootApplication
public class AppConfig {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AppConfig.class, args);
        ResourceLoader resourceLoader = context.getBean(ResourceLoader.class);
        resourceLoader.loadResource("application.properties");
        resourceLoader.loadResource("不存在的资源");
    }
}

BeanFactoryAware

org.springframework.beans.factory.BeanFactoryAware

介绍

这个类只有一个触发点,发生在bean的实例化之后,注入属性之前,也就是Setter之前。这个类的扩展点方法为
setBeanFactory
,可以拿到
BeanFactory
这个属性,从而能够进行更复杂的 Bean 操作。例如,动态获取其他 Bean、检查 Bean 的状态等。

使用场景

  1. 动态获取其他 Bean:通过实现
    BeanFactoryAware
    接口,一个 Bean 可以在运行时动态获取其他 Bean。这在一些需要解耦的场景下非常有用。
@Component
public class DynamicBeanFetcher implements BeanFactoryAware {

    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        System.out.println("注入 BeanFactory 实例");
    }

    public void fetchAndUseBean() {
        MyBean myBean = beanFactory.getBean(MyBean.class);
        System.out.println("获取到的 Bean 实例: " + myBean);
    }
}

@Component
public class MyBean {
    @Override
    public String toString() {
        return "这是 MyBean 实例";
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        DynamicBeanFetcher fetcher = context.getBean(DynamicBeanFetcher.class);
        fetcher.fetchAndUseBean();
    }
}
  1. 检查 Bean 的状态:通过
    BeanFactoryAware
    ,可以在运行时检查某个 Bean 是否存在或者其状态,这对一些需要动态检查 Bean 状态的场景非常有用。
@Component
public class BeanStateChecker implements BeanFactoryAware {

    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        System.out.println("注入 BeanFactory 实例");
    }

    public void checkBeanState() {
        boolean exists = beanFactory.containsBean("myBean");
        System.out.println("MyBean 是否存在: " + exists);
    }
}

@Component("myBean")
public class MyBean {
    @Override
    public String toString() {
        return "这是 MyBean 实例";
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        BeanStateChecker checker = context.getBean(BeanStateChecker.class);
        checker.checkBeanState();
    }
}
  1. 创建复杂 Bean 的初始化逻辑:在一些复杂的业务场景中,有时需要在 Bean 初始化时执行一些复杂的逻辑,例如动态创建其他 Bean 并注入到当前 Bean 中。通过
    BeanFactoryAware
    可以实现这一点。
@Component
public class ComplexBeanInitializer implements BeanFactoryAware {

    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        System.out.println("注入 BeanFactory 实例");
    }

    public void initializeComplexBean() {
        MyBean myBean = beanFactory.getBean(MyBean.class);
        System.out.println("初始化复杂 Bean, 获取到的 MyBean 实例: " + myBean);
        // 在这里可以执行复杂的初始化逻辑
    }
}

@Component
public class MyBean {
    @Override
    public String toString() {
        return "这是 MyBean 实例";
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ComplexBeanInitializer initializer = context.getBean(ComplexBeanInitializer.class);
        initializer.initializeComplexBean();
    }
}

ApplicationContextAwareProcessor

org.springframework.context.support.ApplicationContextAwareProcessor

介绍

该类本身并没有扩展点,而是 BeanPostProcessor 扩展接口的具体实现,但是该类内部却有6个扩展点可供实现 ,这些扩展点的触发时机在bean实例化之后,初始化之前。

可以看到,该类用于执行各种驱动接口,在bean实例化之后,属性填充之后。其内部有6个扩展点可供实现,这几个接口都是Spring预留的重点扩展实现,与Spring的
Bean的生命周期
密切相关,以下按照扩展点调用顺序介绍:

  • EnvironmentAware
    :用于获取
    EnviromentAware
    的一个扩展类,这个变量非常有用, 可以获得系统内的所有参数;另外也可以通过注入的方式来获得Environment,用哪种方式需要以实现场景而决定。当然个人认为这个Aware没必要去扩展,因为spring内部都可以通过注入的方式来直接获得。

  • EmbeddedValueResolverAware
    :用于获取
    StringValueResolver
    的一个扩展类,
    StringValueResolver
    可以获取基于String类型的properties的变量;但一般我们都用
    @Value
    的方式来获取properties的变量,用哪种方式需要以实现场景而决定。如果实现了这个Aware接口,把
    StringValueResolver
    缓存起来,通过这个类去获取
    String
    类型的变量,效果是一样的。

  • ResourceLoaderAware
    :用于获取
    ResourceLoader
    的一个扩展类,
    ResourceLoader
    可以用于获取classpath内所有的资源对象。

  • ApplicationEventPublisherAware
    :用于获取
    ApplicationEventPublisher
    的一个扩展类,
    ApplicationEventPublisher
    可以用来发布事件;这个对象也可以通过spring注入的方式来获得,结合
    ApplicationListener
    来共同使用,下文在介绍
    ApplicationListener
    时会详细提到。

  • MessageSourceAware
    :用于获取
    MessageSource
    的一个扩展类,
    MessageSource
    主要用来做国际化。

  • ApplicationContextAware
    :用来获取
    ApplicationContext
    的一个扩展类,
    ApplicationContext
    就是spring上下文管理器,可以手动的获取任何在spring上下文注册的bean。较多的做法是扩展这个接口来缓存spring上下文,包装成静态方法。
    同时
    ApplicationContext
    也实现了
    BeanFactory

    MessageSource

    ApplicationEventPublisher
    等接口,也可以用来做相关接口的事情。

使用场景

  1. 动态获取其他 Bean:通过实现
    ApplicationContextAware
    接口,Bean 可以在运行时动态获取其他 Bean,这在一些需要解耦的场景下非常有用。
@Component
public class DynamicBeanFetcher implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        System.out.println("注入 ApplicationContext 实例");
    }

    public void fetchAndUseBean() {
        MyBean myBean = applicationContext.getBean(MyBean.class);
        System.out.println("获取到的 Bean 实例: " + myBean);
    }
}

@Component
public class MyBean {
    @Override
    public String toString() {
        return "这是 MyBean 实例";
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        DynamicBeanFetcher fetcher = context.getBean(DynamicBeanFetcher.class);
        fetcher.fetchAndUseBean();
    }
}
  1. 使用 ApplicationContext 进行事件发布:在一些场景中,Bean 可能需要发布事件。通过实现
    ApplicationContextAware
    接口,可以方便地获取
    ApplicationContext
    实例并发布事件。
@Component
public class EventPublisherBean implements ApplicationContextAware, ApplicationEventPublisherAware {

    private ApplicationContext applicationContext;
    private ApplicationEventPublisher eventPublisher;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        System.out.println("注入 ApplicationContext 实例");
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void publishCustomEvent(String message) {
        CustomEvent customEvent = new CustomEvent(this, message);
        eventPublisher.publishEvent(customEvent);
        System.out.println("发布自定义事件: " + message);
    }
}

public class CustomEvent extends ApplicationEvent {
    private String message;

    public CustomEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

@Component
public class CustomEventListener {

    @EventListener
    public void handleCustomEvent(CustomEvent event) {
        System.out.println("接收到自定义事件: " + event.getMessage());
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        EventPublisherBean publisher = context.getBean(EventPublisherBean.class);
        publisher.publishCustomEvent("这是一个自定义事件消息");
    }
}
  1. 获取环境信息:通过实现
    ApplicationContextAware
    接口,Bean 可以访问
    ApplicationContext
    ,并从中获取环境配置信息,例如读取配置文件中的属性值。
@Component
public class EnvironmentAwareBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        System.out.println("注入 ApplicationContext 实例");
    }

    public void printEnvironmentProperty() {
        Environment environment = applicationContext.getEnvironment();
        String propertyValue = environment.getProperty("example.property");
        System.out.println("读取到的环境属性值: " + propertyValue);
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
@PropertySource("classpath:application.properties")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        EnvironmentAwareBean environmentAwareBean = context.getBean(EnvironmentAwareBean.class);
        environmentAwareBean.printEnvironmentProperty();
    }
}

@PostConstruct

javax.annotation.PostConstruct

介绍

可以看出来其本身不是Spring定义的注解,但是Spring提供了具体的实现。这个并不算一个扩展点,其实就是一个标注。其作用是在bean的初始化阶段,如果对一个方法标注了
@PostConstruct
,会先调用这个方法。这里重点是要关注下这个标准的触发点,这个触发点是在
postProcessBeforeInitialization
之后,
InitializingBean.afterPropertiesSet
之前。

注意:

  • 使用@PostConstruct注解标记的方法不能有参数,除非是拦截器,可以采用拦截器规范定义的InvocationContext对象。
  • 使用@PostConstruct注解标记的方法不能有返回值,实际上如果有返回值,也不会报错,但是会忽略掉;
  • 使用@PostConstruct注解标记的方法不能被static修饰,但是final是可以的;

使用场景

使用场景与 InitializingBean 类似,具体看下文

InitializingBean

org.springframework.beans.factory.InitializingBean

介绍

这个类,顾名思义,也是用来初始化bean的。
InitializingBean
接口为bean提供了初始化方法的方式,它只在bean实例化、属性注入后的提供了一个扩展点
afterPropertiesSet
方法,凡是继承该接口的类,在初始前、属性赋值后,都会执行该方法。这个
扩展点的触发时机

postProcessAfterInitialization
之前。

注意:

  • 与InitializingBean#afterPropertiesSet()类似效果的是
    init-method
    ,但是需要注意的是InitializingBean#afterPropertiesSet()执行时机要略早于init-method;
  • InitializingBean#afterPropertiesSet()的调用方式是在bean初始化过程中真接调用bean的afterPropertiesSet();
  • bean自定义属性init-method是通过java反射的方式进行调用 ;

使用场景

  1. 初始化资源:可以在 Bean 初始化后自动启动一些资源,如数据库连接、文件读取等。
public class NormalBeanA implements InitializingBean{
    @Overrideimport org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class ResourceInitializer implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
        // 模拟资源初始化
        System.out.println("资源初始化:建立数据库连接");
    }

    public void performAction() {
        System.out.println("资源使用:执行数据库操作");
    }
}
    
@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ResourceInitializer initializer = context.getBean(ResourceInitializer.class);
        initializer.performAction();
    }
}
  1. 设置初始值
@Component
public class InitialValueSetter implements InitializingBean {

    private String initialValue;

    @Override
    public void afterPropertiesSet() {
        initialValue = "默认值";
        System.out.println("设置初始值:" + initialValue);
    }

    public void printValue() {
        System.out.println("当前值:" + initialValue);
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        InitialValueSetter valueSetter = context.getBean(InitialValueSetter.class);
        valueSetter.printValue();
    }
}
  1. 加载配置:可以在 Bean 初始化后加载必要的配置,如从文件或数据库中读取配置。
@Component
public class ConfigLoader implements InitializingBean {

    private String configValue;

    @Override
    public void afterPropertiesSet() {
        // 模拟配置加载
        configValue = "配置值";
        System.out.println("加载配置:" + configValue);
    }

    public void printConfig() {
        System.out.println("当前配置:" + configValue);
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ConfigLoader configLoader = context.getBean(ConfigLoader.class);
        configLoader.printConfig();
    }
}

SmartInitializingSingleton

org.springframework.beans.factory.SmartInitializingSingleton

介绍

这个接口中只有一个方法
afterSingletonsInstantiated
,其作用是是 在spring容器管理的所有单例对象(非懒加载对象)初始化完成之后调用的回调接口。其触发时机为
postProcessAfterInitialization
之后。

注意:

  • 实现SmartInitializingSingleton接口的bean的作用域必须是单例,afterSingletonsInstantiated()才会触发;
  • afterSingletonsInstantiated()触发执行时,
    非懒加载的单例bean
    已经完成实现化、属性注入以及相关的初始化操作;
  • afterSingletonsInstantiated()的执行时机是在DefaultListableBeanFactory#preInstantiateSingletons();

使用场景

  1. 全局初始化操作:可以在所有单例 Bean 初始化后执行一些全局性的初始化操作,比如设置缓存、启动全局调度任务等。
@Component
public class GlobalInitializer implements SmartInitializingSingleton {

    @Override
    public void afterSingletonsInstantiated() {
        // 模拟全局初始化操作
        System.out.println("全局初始化操作:启动全局调度任务");
    }
}
  1. 检查系统状态:可以用于在所有单例 Bean 初始化之后检查系统状态,确保系统运行在预期状态下。

  2. 加载全局配置:可以在所有单例 Bean 初始化后加载全局配置,如从文件或数据库中读取配置,并应用到系统中。

FactoryBean

org.springframework.beans.factory.FactoryBean

介绍

一般情况下,Spring通过
反射机制
利用bean的class属性指定支线类去实例化bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在bean中提供大量的配置信息。Spring为此提供了一个
org.springframework.bean.factory.FactoryBean
的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。
FactoryBean
接口对于Spring框架来说占有重要的地位,Spring自身就提供了70多个
FactoryBean
的实现。它们隐藏了实例化一些复杂bean的细节,给上层应用带来了便利。

触发点:例如其他框架技术与Spring集成的时候,如mybatis与Spring的集成,mybatis是通过SqlSessionFactory创建出Sqlsession来执行sql的,那么Service层在调用Dao层的接口来执行数据库操作时肯定得持有SqlSessionFactory,那么问题来了:Spring容器怎么才能持有SqlSessionFactory呢?答案就是SqlSessionFactoryBean,它实现了FactoryBean接口。

FactoryBean 与 BeanFactory 的区别

  • FactoryBean接口有三个方法:
    • getObject():用于获取bean,主
      要应用在创建一些复杂的bean的场景
    • getObjectType():返回这个工厂创建的 Bean 实例的类型。
    • isSingleton():用于判断返回bean是否属于单例,默认trure,通俗说就是工厂bean;
  • BeanFactory是Spring bean容器的根接口,ApplicationContext是Spring bean容器的高级接口,继承于BeanFactory,通俗理解就是生成bean的工厂;

使用场景

  1. 创建复杂对象:使用
    FactoryBean
    可以帮助我们创建那些需要复杂配置或初始化的对象。
class ComplexObject {
    private String name;
    private int value;

    public ComplexObject(String name, int value) {
        this.name = name;
        this.value = value;
    }

    @Override
    public String toString() {
        return "ComplexObject{name='" + name + "', value=" + value + "}";
    }
}

@Component
public class ComplexObjectFactoryBean implements FactoryBean<ComplexObject> {

    @Override
    public ComplexObject getObject() {
        // 创建复杂对象
        ComplexObject complexObject = new ComplexObject("复杂对象", 42);
        System.out.println("创建复杂对象:" + complexObject);
        return complexObject;
    }

    @Override
    public Class<?> getObjectType() {
        return ComplexObject.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ComplexObject complexObject = context.getBean(ComplexObject.class);
        System.out.println("获取复杂对象:" + complexObject);
    }
}
  1. 动态切换实现:假设我们需要根据某些条件动态切换 Bean 的具体实现类,可以使用
    FactoryBean
interface Service {
    void execute();
}

class ServiceImplA implements Service {
    @Override
    public void execute() {
        System.out.println("执行服务实现A");
    }
}

class ServiceImplB implements Service {
    @Override
    public void execute() {
        System.out.println("执行服务实现B");
    }
}

@Component
public class DynamicServiceFactoryBean implements FactoryBean<Service> {

    private boolean useServiceA = true; // 可以通过配置或条件动态设置

    @Override
    public Service getObject() {
        if (useServiceA) {
            System.out.println("创建服务实现A");
            return new ServiceImplA();
        } else {
            System.out.println("创建服务实现B");
            return new ServiceImplB();
        }
    }

    @Override
    public Class<?> getObjectType() {
        return Service.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        Service service = context.getBean(Service.class);
        service.execute();
    }
}
  1. 延迟初始化:
    FactoryBean
    可以用于延迟初始化某些 Bean,
    只有在第一次获取时才进行实例化
class LazyObject {
    public LazyObject() {
        System.out.println("懒对象被创建");
    }

    public void doSomething() {
        System.out.println("懒对象执行操作");
    }
}

@Component
public class LazyObjectFactoryBean implements FactoryBean<LazyObject> {

    @Override
    public LazyObject getObject() {
        System.out.println("创建懒对象实例");
        return new LazyObject();
    }

    @Override
    public Class<?> getObjectType() {
        return LazyObject.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

@Configuration
@ComponentScan(basePackages = "com.seven")
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        System.out.println("获取懒对象实例前");
        LazyObject lazyObject = context.getBean(LazyObject.class);
        System.out.println("获取懒对象实例后");
        lazyObject.doSomething();
    }
}

CommandLineRunner和ApplicationRunner

org.springframework.boot.CommandLineRunner

介绍

这两个是Springboot中新增的扩展点,之所以将这两个扩展点放在一起,是因为它两个功能特性高度相似,不同的只是名字、扩展方法形参数类型、执行先后的一些小的不同。

这两个接口触发时机为整个项目启动完毕后,自动执行。如果有多个
CommandLineRunner
,可以利用
@Order
来进行排序。

注意:

  • CommandLineRunner和ApplicationRunner都有一个扩展方法run(),但是run()形参数类型不同;
  • CommandLineRunner.run()方法的形参数类型是String... args,ApplicationRunner.run()的形参数类型是ApplicationArguments args;
  • CommandLineRunner.run()的执行时机要晚于ApplicationRunner.run()一点;
  • CommandLineRunner和ApplicationRunner触发执行时机是在Spring容器、Tomcat容器正式启动完成后,可以正式处理业务请求前,即项目启动的最后一步;
  • CommandLineRunner和ApplicationRunner可以应用的场景:项目启动前,热点数据的预加载、清除临时文件、读取自定义配置信息等;

使用场景

  1. 初始化数据:使用
    CommandLineRunner
    可以在应用启动后初始化一些必要的数据,例如从数据库加载某些配置或插入初始数据。
@Component
public class DataInitializer implements CommandLineRunner {

    @Override
    public void run(String... args) {
        System.out.println("初始化数据:插入初始数据");
        // 模拟插入初始数据
        insertInitialData();
    }

    private void insertInitialData() {
        System.out.println("插入数据:用户表初始数据");
    }
}
  1. 启动后执行任务:使用
    CommandLineRunner
    可以在应用启动后执行一些特定的任务,比如发送一个通知或启动一些背景任务。
@Component
public class TaskExecutor implements CommandLineRunner {

    @Override
    public void run(String... args) {
        System.out.println("启动后执行任务:发送启动通知");
        // 模拟发送启动通知
        sendStartupNotification();
    }

    private void sendStartupNotification() {
        System.out.println("通知:应用已启动");
    }
}
  1. 读取命令行参数:使用
    CommandLineRunner
    可以获取并处理命令行参数,这对于需要根据启动参数动态配置应用的场景非常有用。
@Component
public class CommandLineArgsProcessor implements CommandLineRunner {

    @Override
    public void run(String... args) {
        System.out.println("处理命令行参数:");
        for (String arg : args) {
            System.out.println("参数:" + arg);
        }
    }
}

@SpringBootApplication
public class AppConfig {
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, new String[]{"参数1", "参数2", "参数3"});
    }
}

ApplicationListener 和 ApplicationContextInitializer

org.springframework.context.ApplicationListener

介绍

准确的说,这个应该不算spring&springboot当中的一个扩展点,
ApplicationListener
可以监听某个事件的
event
,触发时机可以穿插在业务方法执行过程中,用户可以自定义某个业务事件。但是spring内部也有一些内置事件,这种事件,可以穿插在启动调用中。我们也可以利用这个特性,来自己做一些内置事件的监听器来达到和前面一些触发点大致相同的事情。

接下来罗列下spring主要的内置事件:

  • ContextRefreshedEvent
    ApplicationContext 被初始化或刷新时,该事件被发布。这也可以在
    ConfigurableApplicationContext
    接口中使用
    refresh()
    方法来发生。此处的初始化是指:所有的Bean被成功装载,后处理Bean被检测并激活,所有Singleton Bean 被预实例化,
    ApplicationContext
    容器已就绪可用。
  • ContextStartedEvent
    当使用
    ConfigurableApplicationContext
    (ApplicationContext子接口)接口中的 start() 方法启动
    ApplicationContext
    时,该事件被发布。你可以调查你的数据库,或者你可以在接受到这个事件后重启任何停止的应用程序。
  • ContextStoppedEvent
    当使用
    ConfigurableApplicationContext
    接口中的
    stop()
    停止
    ApplicationContext
    时,发布这个事件。你可以在接受到这个事件后做必要的清理的工作
  • ContextClosedEvent
    当使用
    ConfigurableApplicationContext
    接口中的
    close()
    方法关闭
    ApplicationContext
    时,该事件被发布。一个已关闭的上下文到达生命周期末端;它不能被刷新或重启
  • RequestHandledEvent
    这是一个 web-specific 事件,告诉所有 bean HTTP 请求已经被服务。只能应用于使用DispatcherServlet的Web应用。在使用Spring作为前端的MVC控制器时,当Spring处理用户请求结束后,系统会自动触发该事件

使用场景

  • 监听自定义事件:使用
    ApplicationListener
    可以监听和处理自定义事件。
// 定义自定义事件
class CustomEvent extends ApplicationEvent {
    private final String message;

    public CustomEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

// 监听自定义事件
@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {

    @Override
    public void onApplicationEvent(CustomEvent event) {
        System.out.println("监听到自定义事件:处理事件");
        handleCustomEvent(event);
    }

    private void handleCustomEvent(CustomEvent event) {
        System.out.println("处理自定义事件:" + event.getMessage());
    }
}

@Component
public class EventPublisher implements ApplicationEventPublisherAware {
    
    private ApplicationEventPublisher eventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void publishCustomEvent(final String message) {
        System.out.println("发布自定义事件:" + message);
        CustomEvent customEvent = new CustomEvent(this, message);
        eventPublisher.publishEvent(customEvent);
    }
}

@SpringBootApplication
public class AppConfig {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AppConfig.class, args);
        EventPublisher publisher = context.getBean(EventPublisher.class);
        publisher.publishCustomEvent("这是自定义事件的消息");
    }
}

@PreDestroy

javax.annotation.PreDestroy

介绍

@PreDestroy

@PostConstruct
一样,是 Java EE 中的一个注解,用于在 Spring 容器销毁 Bean 之前执行特定的方法。这个注解通常用于释放资源、关闭连接、清理缓存等操作。与
@PostConstruct
类似,
@PreDestroy
注解的方法会在 Bean 被销毁之前被调用。

使用场景

使用场景与 DisposableBean 类似,具体看下文。

DisposableBean

org.springframework.beans.factory.DisposableBean

介绍

这个扩展点也只有一个方法:
destroy()
,其触发时机为当此对象销毁、Spring容器关闭时,会自动执行这个方法。比如说运行
applicationContext.registerShutdownHook
时,就会触发这个方法。这个扩展点基本上用不到

注意:

  • DisposableBean是一个接口,为Spring bean提供了一种释放资源的方式 ,只有一个扩展方法destroy();
  • 实现DisposableBean接口,并重写destroy(),可以在Spring容器销毁bean的时候获得一次回调;
  • destroy()的回调执行时机是Spring容器关闭,需要销毁所有的bean时;
  • 与InitializingBean比较类似的是,InitializingBean#afterPropertiesSet()是在bean初始化的时候触发执行,DisposableBean#destroy()是在bean被销毁的时候触发执行

使用场景

  • 释放数据库连接,清理临时文件:在应用被关闭时,释放数据库连接以确保资源被正确地回收,删除临时文件以确保磁盘空间被正确释放。
@Component
public class DatabaseConnectionManager implements DisposableBean {

    @Override
    public void destroy() {
        System.out.println("释放数据库连接:关闭连接");
        // 模拟关闭数据库连接
        closeConnection();
    }

    private void closeConnection() {
        System.out.println("数据库连接已关闭");
    }
}

总结

我们从这些spring&springboot的扩展点当中,大致可以窥视到整个
bean的生命周期
。在业务开发或者写中间件业务的时候,可以合理利用spring提供给我们的扩展点,在spring启动的各个阶段内做一些事情。以达到自定义初始化的目的。

本人也在持续学习中,因此此篇总结如果有错误或者疏漏的地方,恳请评论或联系我进行指正,本文将持续迭代中~

关于作者

来自一线程序员Seven的探索与实践,持续学习迭代中~

本文已收录于我的个人博客:
https://www.seven97.top

公众号:seven97,欢迎关注~

概述

红黑树(Red-Black Tree,R-BTree)是一种自平衡的二叉查找树,在红黑树的每个节点都多出一个存储位表示节点的颜色,颜色只能是红(Red)或者黑(Black)。

红黑树的特性如下:

  • 每个节点或者是黑色的,或者是红色的
  • 根节点是黑色的
  • 每个叶子节点都是黑色的,这里的叶子节点指的是最底层的空节点,即图中值为 null 的节点,讨论时一般将其省略,值为 null 的根节点在红黑树中不看作叶子节点
  • 如果一个节点是红色的,则它的子节点必须是黑色的
  • 从任一个节点到叶子节点的所有路径下都包含相同数量的黑色节点

有了上面的几个性质作为限制,即可避免二叉查找树退化成单链表的情况。而有了性质 4 和性质 5,可保证任意节点到其每个叶子节点路径最长不会超过最短路径的 2 倍,因为当某条路径最短时,这条路径必然都由黑色节点构成。当某条路径长度最长时,这条路径必然由红色和黑色节点相间构成(性质 4)。而性质 5 又限定了任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。因此,最长路径长度等于最短路径长度的 2 倍


红黑树的旋转

红黑树的左旋:以 a 节点为支点左旋,指将 a 节点的右子节点(b)设为 a 节点的父节点,则 a 节点变为 b 节点的左节点,b 节点原本的左子节点 d 则按照平衡二叉树的规则重新分配位置

红黑树的右旋也是同理


红黑树的添加

红黑树的添加分为三步:

  1. 将红黑树看作一颗二叉查找树,并以二叉查找树的规则插入新节点,此时新节点一定位于树的末端,即是叶子节点(不考虑值为 null 的黑色节点)
  2. 将插入的节点涂成红色(如果涂成是黑色,那么这个节点所在路径比其他路径多出一个黑色节点,调整起来比较麻烦;如果涂成红色,此时所有路径上的黑色节点数量不变,仅可能会出现两个连续的红色节点的情况,通过变色和旋转进行调整即可)
  3. 通过左旋、右旋或着色操作,使之重新成为一颗红黑树

根据被插入的节点的父节点的情况,可以将具体的插入分为三种情况来处理

第一种:如果被插入的节点是根节点,则直接把此节点涂成黑色

第二种:如果被插入的节点的父节点是黑色的,没有破坏二叉树的性质,不需要调整

第三种:如果被插入的节点的父节点是红色的,则被插入的节点一定存在非空祖父节点(黑色),即被插入的节点也一定存在叔叔节点,即使叔叔节点(当前节点的祖父节点的另一个子节点)为空,我们也视之为存在,空节点本身就是黑色节点。然后根据叔叔节点的颜色,进一步分为三种情况来处理:

  • 叔叔节点是红色的,则将父节点和叔叔节点设为黑色,祖父节点设为红色。需要注意的是祖父节点设为红色后,可能会和它的父节点形成连续的红色节点,此时需要将当前节点设为祖父节点,递归向上调整
  • 叔叔节点是黑色的,当前节点是父节点的右子节点,则将父节点设为当前节点,以当前节点为支点左旋,再根据其他情况调整
  • 叔叔节点是黑色的,当前节点是父节点的左子节点,则将父节点设为黑色,将祖父节点设为红色,以祖父节点为支点右旋


红黑树的删除

红黑树的删除分为两步:

  1. 将红黑树看作一颗二叉查找树,根据二叉查找树的删除规则删除节点
    1. 如果被删除的节点没有子节点,那么直接将该节点删除
    2. 如果被删除的节点只有一个子节点,那么直接删除该节点,并用该节点的唯一子节点替换该节点
    3. 如果被删除的节点有两个子节点,那么先找出该节点的替换节点,然后把替换节点的数据覆盖该节点的数据,之后删除替换节点
  2. 因为红黑树在删除节点后可能会违背红黑树的特性,所以需要通过旋转和重新着色来修正,使之重新成为一棵红黑树
    1. 如果当前节点的子节点是“红+黑”节点,则直接把当前节点设为黑色
    2. 如果当前节点的子节点是“黑+黑”节点,且当前节点是根节点,则什么都不做
    3. 如果当前节点的子节点是“黑+黑”节点,且当前节点不是根节点,则又可以分如下情况处理
      • 当前节点的兄弟节点是红色,则将当前节点的兄弟节点设置为黑色,将父节点设置为红色,对父节点进行左旋,重新设置当前节点的兄弟节点
      • 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色,兄弟节点的两个子节点也都是黑色,则将当前节点的兄弟节点设置为红色,设置当前节点的父节点为新节点
      • 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色,兄弟节点的左子节点是红色且右子节点是黑色,则将当前节点的左子节点设置为黑色,将兄弟节点设置为红色,对兄弟节点进行右旋,重新设置当前节点的兄弟节点
      • 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色,兄弟节点的右子节点是红色且左子节点是任意颜色,则将当前节点的父节点的颜色赋值给兄弟节点,将父节点设置为黑色,将兄弟节点的右子节点设置为黑色,对父节点进行左旋,设置当前节点为根节点

什么是 Argo CD?

Argo CD 是一个声明式的 GitOps 持续交付工具,用于 Kubernetes 集群。它通过持续监控 Git 仓库中的 Kubernetes 资源配置文件,将这些配置自动应用到指定的 Kubernetes 集群中,确保集群的实际状态与仓库中的配置保持一致。Argo CD 支持各种 Kubernetes 清单格式,如 Kustomize、Helm Charts、Ksonnet、YAML 和 JSON,允许你通过 Git 仓库管理和部署 Kubernetes 资源。

Argo CD 的好处

  1. 声明式管理
    :Argo CD 采用声明式的管理方式,开发者只需在 Git 仓库中定义好应用的期望状态,Argo CD 就会自动将集群的实际状态与之同步。这样可以减少人为错误,并使配置管理更加清晰和可审计。

  2. GitOps 工作流
    :Argo CD 将 Git 仓库作为配置管理的唯一真理来源(Source of Truth),实现了 GitOps 的最佳实践。每一次应用的部署或更新都通过提交代码和合并请求触发,从而保证了自动化和审核跟踪。

  3. 持续同步和自愈
    :Argo CD 能够持续监控 Kubernetes 集群中的资源状态,并在检测到任何偏离期望状态的情况时自动纠正,使集群的状态始终与 Git 仓库中的配置一致。

  4. 多集群支持
    :Argo CD 可以管理多个 Kubernetes 集群,使得跨集群的应用部署和管理更加容易。

  5. 细粒度访问控制
    :Argo CD 提供了细粒度的访问控制机制,允许基于角色的访问控制(RBAC)以及通过 SSO 集成来控制对特定项目和应用的访问权限。

Argo CD 与 Jenkins 的对比

功能 Argo CD Jenkins
架构 专注于 Kubernetes 集群的声明式部署 通用的 CI/CD 工具,支持多种编程语言和环境
GitOps 支持 内置支持 GitOps 工作流,Git 是唯一的真理来源 需要通过插件或自定义脚本来支持 GitOps 工作流
部署自动化 自动同步 Kubernetes 资源配置,持续保持集群一致性 通过流水线(pipeline)手动配置部署过程
可观测性和回滚 内置监控和自动回滚功能 通过第三方工具或插件实现
插件支持 提供基础功能,无需大量插件 通过插件扩展功能,插件种类丰富
CI/CD 整合 专注于 CD 部分,通常与 Argo Workflows 等其他工具整合使用 既支持 CI 又支持 CD,整合度较高

使用 Argo CD 部署一个服务

接下来我们将展示如何使用 Argo CD 部署一个简单的服务。假设我们要在 Kubernetes 集群中部署一个 Nginx 服务。

1. 安装 Argo CD

首先,我们需要在 Kubernetes 集群中安装 Argo CD:

kubectl create namespaceargocd
kubectl apply
-n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

安装完成后,可以通过以下命令获取 Argo CD API Server 的外部访问地址:

kubectl get svc -n argocd

2. 访问 Argo CD

你可以通过 port-forward 访问 Argo CD 的 Web 界面:

kubectl port-forward svc/argocd-server -n argocd 8080:443

然后在浏览器中访问
https://localhost:8080
。默认的用户名是
admin
,可以通过以下命令获取初始密码:

kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath="{.data.password}" | base64 -d

3. 创建 Git 仓库并推送配置

在 GitHub 或其他 Git 服务上创建一个新仓库,并将以下内容保存为
nginx-deployment.yaml
文件,推送到你的 Git 仓库中。

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
-deployment
labels:
app: nginx
spec:
replicas:
3selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
-name: nginx
image: nginx:
1.14.2ports:- containerPort: 80 ---apiVersion: v1
kind: Service
metadata:
name: nginx
-service
spec:
selector:
app: nginx
ports:
-protocol: TCP
port:
80targetPort:80

4. 使用 YAML 文件定义 Argo CD 应用

创建一个新的 YAML 文件
argo-nginx-app.yaml
,并将以下内容添加到其中:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: nginx
-appnamespace: argocd
spec:
project:
defaultsource:
repoURL:
'<YOUR_GIT_REPOSITORY_URL>'targetRevision: HEAD
path:
'<YOUR_APP_PATH>'destination:
server: https:
//kubernetes.default.svc namespace: defaultsyncPolicy:
automated:
prune:
trueselfHeal:true


<your-username>

<your-repo>
替换为你的 GitHub 用户名和仓库名。

5. 应用 YAML 文件

通过以下命令将 Argo CD 应用的定义文件应用到 Kubernetes 集群:

kubectl apply -f argo-nginx-app.yaml

6. 验证部署

回到 Argo CD 的 Web 界面,你应该能看到名为
nginx-app
的应用。如果状态为 “Synced”,说明部署已经成功。

你也可以通过以下命令在 Kubernetes 中验证 Nginx 服务是否正常运行:

kubectl get pods -l app=nginx
kubectl
get svc nginx-service

总结

Argo CD 是一个强大的 Kubernetes 持续交付工具,能够简化和自动化 Kubernetes 集群中的应用管理。通过 GitOps 工作流,Argo CD 使得应用部署和配置更具透明性和可追溯性。与 Jenkins 等传统 CI/CD 工具相比,Argo CD 更专注于 Kubernetes 环境,尤其适合微服务和容器化应用的持续交付。

一步一步将PlantUML类图导出为自定义格式的XMI文件

说明:

PlantUML(Java)中获取类图以及plantuml xmi代码是如何遍历实体并获取属性的

以下是来自
https://stackoverflow.com/questions/76599075/parse-uml-class-diagram-with-plantuml-java-api
的代码:

String source = "@startuml\n" +
            PUT YOUR CLASS DIAGRAM HERE
            "@enduml";

SourceStringReader r = new SourceStringReader(source);
FileOutputStream file = new FileOutputStream("testGroovy2.png");
ClassDiagram cd = (ClassDiagram) r.getBlocks().get(0).getDiagram();
Collection<Quark<Entity>> classes = 
cd.getEntityFactory().root().getChildren();
classes.stream().forEach(c-> System.out.println(c));

从中知道如何获取ClassDiagram

查看
net.sourceforge.plantuml.xmi.XmiClassDiagramAbstract

XmiClassDiagramStandard
的定义:

public class XmiClassDiagramStandard extends XmiClassDiagramAbstract implements XmlDiagramTransformer {
    public XmiClassDiagramStandard(ClassDiagram classDiagram) throws ParserConfigurationException {
        super(classDiagram);
        Iterator var2 = classDiagram.getEntityFactory().leafs().iterator();

        while(var2.hasNext()) {
            Entity ent = (Entity)var2.next();
            Element cla = this.createEntityNode(ent);
            if (cla != null) {
                this.ownedElementRoot.appendChild(cla);
                this.done.add(ent);
            }
        }

    }
}

从中知道如何遍历entity

查看
net.sourceforge.plantuml.xmi.XmiClassDiagramStandard
中的
createEntityNode
方法:

    protected final Element createEntityNode(Entity entity) {
        Element cla = this.document.createElement("UML:Class");
        if (entity.getLeafType() == LeafType.NOTE) {
            return null;
        } else {
            cla.setAttribute("xmi.id", entity.getUid());
            cla.setAttribute("name", entity.getDisplay().get(0).toString());
            Stereotype stereotype = entity.getStereotype();
            if (stereotype != null) {
                Element stereo = this.document.createElement("UML:ModelElement.stereotype");
                Iterator var5 = stereotype.getMultipleLabels().iterator();

                while(var5.hasNext()) {
                    String s = (String)var5.next();
                    Element name = this.document.createElement("UML:Stereotype");
                    name.setAttribute("name", s);
                    stereo.appendChild(name);
                }

                cla.appendChild(stereo);
            }

            LeafType type = entity.getLeafType();
            if (type == LeafType.ABSTRACT_CLASS) {
                cla.setAttribute("isAbstract", "true");
            } else if (type == LeafType.INTERFACE) {
                cla.setAttribute("isInterface", "true");
            }

            if (entity.isStatic()) {
                cla.setAttribute("isStatic", "true");
            }

            if (entity.getVisibilityModifier() == VisibilityModifier.PRIVATE_FIELD || entity.getVisibilityModifier() == VisibilityModifier.PRIVATE_METHOD) {
                cla.setAttribute("visibility", entity.getVisibilityModifier().getXmiVisibility());
            }

            Element feature = this.document.createElement("UML:Classifier.feature");
            cla.appendChild(feature);

            Member m;
            Element operation;
            VisibilityModifier visibility;
            ListIterator var13;
            CharSequence cs;
            for(var13 = entity.getBodier().getFieldsToDisplay().iterator(); var13.hasNext(); feature.appendChild(operation)) {
                cs = (CharSequence)var13.next();
                m = (Member)cs;
                operation = this.document.createElement("UML:Attribute");
                operation.setAttribute("xmi.id", "att" + this.classDiagram.getUniqueSequence());
                operation.setAttribute("name", m.getDisplay(false));
                visibility = m.getVisibilityModifier();
                if (visibility != null) {
                    operation.setAttribute("visibility", visibility.getXmiVisibility());
                }

                if (m.isStatic()) {
                    operation.setAttribute("isStatic", "true");
                }
            }

            for(var13 = entity.getBodier().getMethodsToDisplay().iterator(); var13.hasNext(); feature.appendChild(operation)) {
                cs = (CharSequence)var13.next();
                m = (Member)cs;
                operation = this.document.createElement("UML:Operation");
                operation.setAttribute("xmi.id", "att" + this.classDiagram.getUniqueSequence());
                operation.setAttribute("name", m.getDisplay(false));
                visibility = m.getVisibilityModifier();
                if (visibility != null) {
                    operation.setAttribute("visibility", visibility.getXmiVisibility());
                }

                if (m.isStatic()) {
                    operation.setAttribute("isStatic", "true");
                }
            }

            return cla;
        }
    }

从中可以看出:

  • entity.getBodier().getFieldsToDisplay().iterator()
    是获取entity的attribute的迭代器
  • entity.getBodier().getMethodsToDisplay().iterator()
    是获取entity的operation的迭代器
  • name

    isAbstract

    isInterface

    isStatic
    这些属性是如何获取的

还有
https://github.com/plantuml/plantuml/pull/1298/files
是2023年2月的一个PR(写本文时尚未Merge),可以作为参考(有待阅读)。

使用JAVA代码将PlantUML类图导出为XMI

通过调用exportDiagram方法

import net.sourceforge.plantuml.FileFormat;  
import net.sourceforge.plantuml.FileFormatOption;  
import net.sourceforge.plantuml.SourceStringReader;

import java.io.FileOutputStream;

public class Main {  
    public static void main(String[] args) throws IOException {
        OutputStream output = new FileOutputStream("test.xmi");
        
        FileFormatOption fileFormatOption = new FileFormatOption(FileFormat.XMI_ARGO);

        String content = new String(Files.readAllBytes(Paths.get("a.puml")));
        SourceStringReader reader = new SourceStringReader(content);

        reader.getBlocks().get(0).getDiagram().exportDiagram(output, 0, fileFormatOption);
    }
}

其中
FileFormat
是一个枚举,定义了
XMI_STANDARD

XMI_STAR

XMI_ARGO
等格式

分析getDiagram的调用链

getDiagram
方法来自
net.sourceforge.plantuml.core.Diagram
提供的
exportDiagram
接口方法,其实现:

    public final ImageData exportDiagram(OutputStream os, int index, FileFormatOption fileFormatOption) throws IOException {
        long now = System.currentTimeMillis();

        ImageData var6;
        try {
            var6 = this.exportDiagramNow(os, index, fileFormatOption);
        } finally {
            if (OptionFlags.getInstance().isEnableStats()) {
                StatsUtilsIncrement.onceMoreGenerate(System.currentTimeMillis() - now, this.getClass(), fileFormatOption.getFileFormat());
            }

        }

        return var6;
    }

其中主要是调用了
net.sourceforge.plantuml.AbstractPSystem
中的
exportDiagramNow
接口方法。而其在
net.sourceforge.plantuml.UmlDiagram
中的实现如下:

    protected final ImageData exportDiagramNow(OutputStream os, int index, FileFormatOption fileFormatOption) throws IOException {
        fileFormatOption = fileFormatOption.withTikzFontDistortion(this.getSkinParam().getTikzFontDistortion());
        if (fileFormatOption.getFileFormat() == FileFormat.PDF) {
            return this.exportDiagramInternalPdf(os, index);
        } else {
            try {
                ImageData imageData = this.exportDiagramInternal(os, index, fileFormatOption);
                this.lastInfo = new XDimension2D((double)imageData.getWidth(), (double)imageData.getHeight());
                return imageData;
            } catch (NoStyleAvailableException var5) {
                this.exportDiagramError(os, var5, fileFormatOption, (String)null);
                return ImageDataSimple.error(var5);
            } catch (UnparsableGraphvizException var6) {
                Logme.error(var6);
                this.exportDiagramError(os, var6.getCause(), fileFormatOption, var6.getGraphvizVersion());
                return ImageDataSimple.error(var6);
            } catch (Throwable var7) {
                this.exportDiagramError(os, var7, fileFormatOption, (String)null);
                return ImageDataSimple.error(var7);
            }
        }
    }

可以看出其主要是调用了
net.sourceforge.plantuml.UmlDiagram.exportDiagramInternal
接口方法,由于当前的类是
ClassDiagram
,其将调用
net.sourceforge.plantuml.classdiagram.ClassDiagram
实现的
exportDiagramInternal
方法:

    protected final ImageData exportDiagramInternal(OutputStream os, int index, FileFormatOption fileFormatOption) throws IOException {
        return this.useLayoutExplicit != 0 ? this.exportLayoutExplicit(os, index, fileFormatOption) : super.exportDiagramInternal(os, index, fileFormatOption);
    }

其中
net.sourceforge.plantuml.classdiagram.ClassDiagram
继承
net.sourceforge.plantuml.objectdiagram.AbstractClassOrObjectDiagram
继承
net.sourceforge.plantuml.classdiagram.AbstractEntityDiagram
继承
net.atmp.CucaDiagram


net.atmp.CucaDiagram.exportDiagramInternal
方法中涉及XMI的部分如下:

                this.createFilesXmi(os, fileFormat);
                return ImageDataSimple.ok();

其调用了
net.atmp.CucaDiagram.createFilesXmi
方法,其定义如下:

    private void createFilesXmi(OutputStream suggestedFile, FileFormat fileFormat) throws IOException {
        CucaDiagramXmiMaker maker = new CucaDiagramXmiMaker(this, fileFormat);
        maker.createFiles(suggestedFile);
    }

其中调用了
net.sourceforge.plantuml.xmi.CucaDiagramXmiMaker

createFiles
方法,其定义如下:

    public void createFiles(OutputStream fos) throws IOException {
        try {
            Object xmi;
            if (this.diagram instanceof StateDiagram) {
                xmi = new XmiStateDiagram((StateDiagram)this.diagram);
            } else if (this.diagram instanceof DescriptionDiagram) {
                xmi = this.createDescriptionDiagram();
            } else {
                if (!(this.diagram instanceof ClassDiagram)) {
                    throw new UnsupportedOperationException("Diagram type " + this.diagram.getUmlDiagramType() + " is not supported in XMI");
                }

                xmi = this.createClassDiagram();
            }

            ((XmlDiagramTransformer)xmi).transformerXml(fos);
        } catch (ParserConfigurationException var3) {
            Log.error(var3.toString());
            Logme.error(var3);
            throw new IOException(var3.toString());
        } catch (TransformerException var4) {
            Log.error(var4.toString());
            Logme.error(var4);
            throw new IOException(var4.toString());
        }
    }

可以看出涉及创建xmi文件的关键2行为:

xmi = this.createClassDiagram();
((XmlDiagramTransformer)xmi).transformerXml(fos);


createClassDiagram
的定义如下:

    private XmlDiagramTransformer createClassDiagram() throws ParserConfigurationException {
        if (this.fileFormat == FileFormat.XMI_STANDARD) {
            return new XmiClassDiagramStandard((ClassDiagram)this.diagram);
        } else if (this.fileFormat == FileFormat.XMI_ARGO) {
            return new XmiClassDiagramArgo((ClassDiagram)this.diagram);
        } else if (this.fileFormat == FileFormat.XMI_SCRIPT) {
            return new XmiClassDiagramScript((ClassDiagram)this.diagram);
        } else if (this.fileFormat == FileFormat.XMI_STAR) {
            return new XmiClassDiagramStar((ClassDiagram)this.diagram);
        } else {
            throw new UnsupportedOperationException();
        }
    }

分析getDiagram方法后,调用内部API导出XMI

import net.sourceforge.plantuml.FileFormat;  
import net.sourceforge.plantuml.FileFormatOption;  
import net.sourceforge.plantuml.SourceStringReader;
import net.sourceforge.plantuml.xmi.XmiClassDiagramArgo;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import java.io.FileOutputStream;

public class Main {  
    public static void main(String[] args) throws IOException, ParserConfigurationException, TransformerException {
        OutputStream fos = new FileOutputStream("test2.xmi");
        
        FileFormatOption fileFormatOption = new FileFormatOption(FileFormat.XMI_ARGO);

        String content = new String(Files.readAllBytes(Paths.get("a.puml")));
        SourceStringReader reader = new SourceStringReader(content);
        ClassDiagram classDiagram = (ClassDiagram) reader.getBlocks().get(0).getDiagram();

        Object xmi = new XmiClassDiagramArgo(classDiagram);
        ((XmlDiagramTransformer)xmi).transformerXml(fos);
    }
}

读取一个uml文件并打印其中的属性信息

现在了解了一些后,使用以下Java代码来查看UML文件的信息

package org.example;

import net.sourceforge.plantuml.SourceStringReader;
import net.sourceforge.plantuml.abel.Entity;
import net.sourceforge.plantuml.abel.LeafType;
import net.sourceforge.plantuml.abel.Link;
import net.sourceforge.plantuml.classdiagram.ClassDiagram;
import net.sourceforge.plantuml.cucadiagram.Member;
import net.sourceforge.plantuml.skin.VisibilityModifier;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Iterator;

import java.util.ListIterator;


public class Main {
    public static void main(String[] args) throws IOException, ParserConfigurationException, TransformerException {
        OutputStream output = new FileOutputStream("test.xmi");
//        String source = "@startuml\n";
//        source += "Bob -> Alice : hello\n";
//        source += "@enduml\n";
        String content = new String(Files.readAllBytes(Paths.get("a.puml")));

        SourceStringReader reader = new SourceStringReader(content);
        ClassDiagram classDiagram = (ClassDiagram) reader.getBlocks().get(0).getDiagram();

        String titleName = classDiagram.getTitleDisplay().get(0).toString();
        System.out.println("title: " + titleName);
        Iterator var2 = classDiagram.getEntityFactory().leafs().iterator();

        while(var2.hasNext()) {
            Entity entity = (Entity)var2.next();

            System.out.println("----------------------------------");
            System.out.println("uid: " + entity.getUid());
            if (entity.getLeafType() == LeafType.NOTE) {
//                entity.isAloneAndUnlinked();
//                System.out.println("Entity type: " + entity.getLeafType());
            } else {

                Iterator var1 = classDiagram.getLinks().iterator();
                while(var1.hasNext()) {
                    Link link = (Link)var1.next();
                    if (link.contains(entity)) {
                        Entity other = link.getOther(entity);
                        if (other.getLeafType() == LeafType.NOTE) {
                            System.out.println("Note: "  + other.getDisplay().get(0).toString());
                        }
                    }
                }

                System.out.println("Entity type: " + entity.getLeafType());

                String entityName = entity.getDisplay().get(0).toString();
                System.out.println("Entity name: " + entityName);
                System.out.println("Package: " + entity.getQuark().getParent().toString());

                LeafType type = entity.getLeafType();

                if (type == LeafType.ABSTRACT_CLASS) {
                    System.out.println("isAbstract");
                } else if (type == LeafType.INTERFACE) {
                    System.out.println("isInterface");
                }

                if (entity.isStatic()) {
                    System.out.println(("isStatic"));
                }

                if (entity.getVisibilityModifier() == VisibilityModifier.PRIVATE_FIELD || entity.getVisibilityModifier() == VisibilityModifier.PRIVATE_METHOD) {
                    System.out.println("visibility: " + entity.getVisibilityModifier().getXmiVisibility());
                }

                Member m;
                VisibilityModifier visibility;
                ListIterator var13;
                CharSequence cs;
                for (var13 = entity.getBodier().getFieldsToDisplay().iterator(); var13.hasNext();) {
                    cs = (CharSequence)var13.next();
                    m = (Member) cs;
                    System.out.println("field name: " + m.getDisplay(false));
                    visibility = m.getVisibilityModifier();
                    if (visibility != null) {
                        System.out.println("visibility: " + visibility.getXmiVisibility());
                    }

                    if (m.isStatic()) {
                        System.out.println("isStatic");
                    }
                }

                for (var13 = entity.getBodier().getMethodsToDisplay().iterator(); var13.hasNext();) {
                    cs = (CharSequence)var13.next();
                    m = (Member) cs;
                    System.out.println("operation name: " + m.getDisplay(false));

                    visibility = m.getVisibilityModifier();
                    if (visibility != null) {
                        System.out.println("visibility: " + visibility.getXmiVisibility());
                    }

                    if (m.isStatic()) {
                        System.out.println("isStatic");
                    }
                }
            }

        }

// Write the first image to "png"
//        String desc = reader.outputImage(output).getDescription();
// Return a null string if no generation
    }
}

创建JAVA项目并打包为JAR包:将PlantUML导出为自定义格式的XMI文件

创建项目

使用IDEA创建一个maven项目,然后打开plantuml的maven仓库页面:
https://mvnrepository.com/artifact/net.sourceforge.plantuml/plantuml/1.2024.6
将dependency标签拷贝到pom.xml中

添加用于自定义XMI格式的类

由于不能引用
XmiClassDiagramAbstract
(提示 is not public in 'net.sourceforge.plantuml.xmi'. Cannot be accessed from outside package),所以在
src/main/java
下新建package
net.sourceforge.plantuml.xmi

然后添加类
XmiClassDiagramCustom
(extends XmiClassDiagramAbstract):

package net.sourceforge.plantuml.xmi;

import java.util.Iterator;
import java.util.ListIterator;
import javax.xml.parsers.ParserConfigurationException;

import net.sourceforge.plantuml.abel.Entity;
import net.sourceforge.plantuml.abel.LeafType;
import net.sourceforge.plantuml.abel.Link;
import net.sourceforge.plantuml.classdiagram.ClassDiagram;
import net.sourceforge.plantuml.cucadiagram.Member;
import net.sourceforge.plantuml.decoration.LinkDecor;
import net.sourceforge.plantuml.klimt.creole.Display;
import net.sourceforge.plantuml.skin.VisibilityModifier;
import net.sourceforge.plantuml.stereo.Stereotype;
import org.w3c.dom.Element;

public class XmiClassDiagramCustom extends XmiClassDiagramAbstract implements XmlDiagramTransformer {
    public XmiClassDiagramCustom(ClassDiagram classDiagram) throws ParserConfigurationException {
        super(classDiagram);
        Iterator var2 = classDiagram.getEntityFactory().leafs().iterator();

        Element titleElement = this.document.createElement("UML:Title");
        titleElement.setAttribute("name", classDiagram.getTitleDisplay().get(0).toString());
        this.ownedElementRoot.appendChild(titleElement);

        while(var2.hasNext()) {
            Entity ent = (Entity)var2.next();
            Element cla = this.createEntityNodeCustom(ent, classDiagram);
            if (cla != null) {
                this.ownedElementRoot.appendChild(cla);
                this.done.add(ent);
            }
        }

        var2 = classDiagram.getLinks().iterator();

        while(var2.hasNext()) {
            Link link = (Link)var2.next();
            this.addLink(link);
        }

    }

    protected final Element createEntityNodeCustom(Entity entity, ClassDiagram classDiagram) {
        Element cla = this.document.createElement("UML:Class");
        if (entity.getLeafType() == LeafType.NOTE) {
            return null;
        } else {
            cla.setAttribute("xmi.id", entity.getUid());
            cla.setAttribute("name", entity.getDisplay().get(0).toString());

            // Get Note:
            Iterator var1 = classDiagram.getLinks().iterator();
            while(var1.hasNext()) {
                Link link = (Link)var1.next();
                if (link.contains(entity)) {
                    Entity other = link.getOther(entity);
                    if (other.getLeafType() == LeafType.NOTE) {
                        cla.setAttribute("note", other.getDisplay().get(0).toString());
//                        System.out.println("Note: "  + other.getDisplay().get(0).toString());
                    }
                }
            }

            cla.setAttribute("package", entity.getQuark().getParent().toString());

            Stereotype stereotype = entity.getStereotype();
            if (stereotype != null) {
                Element stereo = this.document.createElement("UML:ModelElement.stereotype");
                Iterator var5 = stereotype.getMultipleLabels().iterator();

                while(var5.hasNext()) {
                    String s = (String)var5.next();
                    Element name = this.document.createElement("UML:Stereotype");
                    name.setAttribute("name", s);
                    stereo.appendChild(name);
                }

                cla.appendChild(stereo);
            }

            LeafType type = entity.getLeafType();
            if (type == LeafType.ABSTRACT_CLASS) {
                cla.setAttribute("isAbstract", "true");
            } else if (type == LeafType.INTERFACE) {
                cla.setAttribute("isInterface", "true");
            }

            if (entity.isStatic()) {
                cla.setAttribute("isStatic", "true");
            }

            if (entity.getVisibilityModifier() == VisibilityModifier.PRIVATE_FIELD || entity.getVisibilityModifier() == VisibilityModifier.PRIVATE_METHOD) {
                cla.setAttribute("visibility", entity.getVisibilityModifier().getXmiVisibility());
            }

            Element feature = this.document.createElement("UML:Classifier.feature");
            cla.appendChild(feature);

            Member m;
            Element operation;
            VisibilityModifier visibility;
            ListIterator var13;
            CharSequence cs;
            for(var13 = entity.getBodier().getFieldsToDisplay().iterator(); var13.hasNext(); feature.appendChild(operation)) {
                cs = (CharSequence)var13.next();
                m = (Member)cs;
                operation = this.document.createElement("UML:Attribute");
                operation.setAttribute("xmi.id", "att" + this.classDiagram.getUniqueSequence());
                operation.setAttribute("name", m.getDisplay(false));
                visibility = m.getVisibilityModifier();
                if (visibility != null) {
                    operation.setAttribute("visibility", visibility.getXmiVisibility());
                }

                if (m.isStatic()) {
                    operation.setAttribute("isStatic", "true");
                }
            }

            for(var13 = entity.getBodier().getMethodsToDisplay().iterator(); var13.hasNext(); feature.appendChild(operation)) {
                cs = (CharSequence)var13.next();
                m = (Member)cs;
                operation = this.document.createElement("UML:Operation");
                operation.setAttribute("xmi.id", "att" + this.classDiagram.getUniqueSequence());
                operation.setAttribute("name", m.getDisplay(false));
                visibility = m.getVisibilityModifier();
                if (visibility != null) {
                    operation.setAttribute("visibility", visibility.getXmiVisibility());
                }

                if (m.isStatic()) {
                    operation.setAttribute("isStatic", "true");
                }
            }

            return cla;
        }
    }

    // copy from XmiClassDiagramStar
    private void addLink(Link link) {
        if (!link.isHidden() && !link.isInvis()) {
            String assId = "ass" + this.classDiagram.getUniqueSequence();
            if (link.getType().getDecor1() != LinkDecor.EXTENDS && link.getType().getDecor2() != LinkDecor.EXTENDS) {
                Element association = this.document.createElement("UML:Association");
                association.setAttribute("xmi.id", assId);
                association.setAttribute("namespace", CucaDiagramXmiMaker.getModel(this.classDiagram));
                if (!Display.isNull(link.getLabel())) {
                    association.setAttribute("name", this.forXMI(link.getLabel()));
                }

                Element connection = this.document.createElement("UML:Association.connection");
                Element end1 = this.document.createElement("UML:AssociationEnd");
                end1.setAttribute("xmi.id", "end" + this.classDiagram.getUniqueSequence());
                end1.setAttribute("association", assId);
                end1.setAttribute("type", link.getEntity1().getUid());
                if (link.getQuantifier1() != null) {
                    end1.setAttribute("name", this.forXMI(link.getQuantifier1()));
                }

                Element endparticipant1 = this.document.createElement("UML:AssociationEnd.participant");
                if (link.getType().getDecor2() == LinkDecor.COMPOSITION) {
                    end1.setAttribute("aggregation", "composite");
                }

                if (link.getType().getDecor2() == LinkDecor.AGREGATION) {
                    end1.setAttribute("aggregation", "aggregate");
                }

                end1.appendChild(endparticipant1);
                connection.appendChild(end1);
                Element end2 = this.document.createElement("UML:AssociationEnd");
                end2.setAttribute("xmi.id", "end" + this.classDiagram.getUniqueSequence());
                end2.setAttribute("association", assId);
                end2.setAttribute("type", link.getEntity2().getUid());
                if (link.getQuantifier2() != null) {
                    end2.setAttribute("name", this.forXMI(link.getQuantifier2()));
                }

                Element endparticipant2 = this.document.createElement("UML:AssociationEnd.participant");
                if (link.getType().getDecor1() == LinkDecor.COMPOSITION) {
                    end2.setAttribute("aggregation", "composite");
                }

                if (link.getType().getDecor1() == LinkDecor.AGREGATION) {
                    end2.setAttribute("aggregation", "aggregate");
                }

                end2.appendChild(endparticipant2);
                connection.appendChild(end2);
                association.appendChild(connection);
                this.ownedElementRoot.appendChild(association);
            } else {
                this.addExtension(link, assId);
            }
        }
    }

    // copy from XmiClassDiagramStar
    private void addExtension(Link link, String assId) {
        Element association = this.document.createElement("UML:Generalization");
        association.setAttribute("xmi.id", assId);
        association.setAttribute("namespace", CucaDiagramXmiMaker.getModel(this.classDiagram));
        if (link.getLabel() != null) {
            association.setAttribute("name", this.forXMI(link.getLabel()));
        }

        if (link.getType().getDecor1() == LinkDecor.EXTENDS) {
            association.setAttribute("child", link.getEntity1().getUid());
            association.setAttribute("parent", link.getEntity2().getUid());
        } else {
            if (link.getType().getDecor2() != LinkDecor.EXTENDS) {
                throw new IllegalStateException();
            }

            association.setAttribute("child", link.getEntity2().getUid());
            association.setAttribute("parent", link.getEntity1().getUid());
        }

        this.ownedElementRoot.appendChild(association);
    }
}

添加导出类(入口)

添加package
org.export
,并添加类
UML2XMIExporter

package org.export;

import net.sourceforge.plantuml.SourceStringReader;
import net.sourceforge.plantuml.classdiagram.ClassDiagram;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;


import net.sourceforge.plantuml.xmi.XmiClassDiagramCustom;
import net.sourceforge.plantuml.xmi.XmlDiagramTransformer;

public class UML2XMIExporter {
    public static void main(String[] args) throws IOException, ParserConfigurationException, TransformerException {

        String content = new String(Files.readAllBytes(Paths.get(args[0])));
        OutputStream outputCustom = Files.newOutputStream(Paths.get(args[1]));

        SourceStringReader reader = new SourceStringReader(content);
        ClassDiagram classDiagram = (ClassDiagram) reader.getBlocks().get(0).getDiagram();

        // 导出XMI
        XmlDiagramTransformer xmi = new XmiClassDiagramCustom(classDiagram);
        xmi.transformerXml(outputCustom);

    }
}

打包

接下来,参考
https://blog.csdn.net/weixin_41229430/article/details/138963215
打包为jar包

创建文件: src/main/assembly/assembly.xml, 内容如下:

<assembly>
    <id>assembly</id>
    <formats>
        <format>zip</format>
        <format>jar</format>
        <format>tar.gz</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <fileSets>
        <!--   -->
        <fileSet>
            <directory>src/main/resources</directory>
            <outputDirectory>conf</outputDirectory>
            <includes>
                <include>*.xml</include>
                <include>*.properties</include>
                <include>**/*.xml</include>
                <include>**/*.properties</include>
            </includes>
            <fileMode>0644</fileMode>
        </fileSet>
        <fileSet>
            <directory>assembly/bin</directory>
            <outputDirectory>bin</outputDirectory>
            <fileMode>0755</fileMode>
        </fileSet>
    </fileSets>
    <dependencySets>
        <dependencySet>
            <outputDirectory>lib</outputDirectory>
        </dependencySet>
    </dependencySets>
</assembly>

然后在pom.xml中添加

    <build>
        <plugins>
            <!--主要用于将 Maven 项目打包成可执行的程序或分发包 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <id>make-assembly2</id><!--名字任意 -->
                        <phase>package</phase><!-- 绑定到package生命周期阶段上 -->
                        <goals>
                            <goal>single</goal><!-- 只运行一次 -->
                        </goals>
                        <configuration>
                            <descriptors>
                                <descriptor>${basedir}/src/main/assembly/assembly.xml</descriptor>
                            </descriptors>
                            <archive>
                                <manifest>
                                    <mainClass>org.export.UML2XMIExporter</mainClass>
                                    <addClasspath>true</addClasspath>
                                    <classpathPrefix>lib/</classpathPrefix>
                                    <useUniqueVersions>false</useUniqueVersions>
                                </manifest>
                            </archive>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

点击IDEA右侧的m图标(maven),双击Lifecycle下的package。然后在target目录下可以看到已经生成了jar包。

打开command prompt命令行(不要使用powershell)运行:

java -Dfile.encoding=UTF-8 -jar custom-plantuml-xmi-export-1.0-SNAPSHOT-jar-with-dependencies.jar input.puml output.xmi