引言

springboot给我们的开发带来了极大的便利,并通过启动器的方式方便我们添加依赖而不用过多的关注配置,那么springboot是如何进行工作的?一起探究下。

一般我们都会在pom.xml中继承spring-boot-parent

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

或者在<dependencyManagement>标签里通过依赖spring-boot-dependencies来引入springboot

 <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.16.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

我们只要关注我们需要哪些依赖,而不需要关注依赖的具体版本,因为上面两种方式(当然方式可以有其他的,只要正确引入springboot即可)都是通过springboot的父项目来进行版本的管理。比如我们想开发一个web项目,就直接引入即可,而不需要标注版本:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
</dependency>

主启动类

默认的主启动类

在springboot项目中都会有个启动类,而启动类都有一个@SpringBootApplication注解,我们先探究下这个注解的作用。

//@SpringBootApplication 来标注一个主程序类
//说明这是一个Spring Boot应用
@SpringBootApplication
public class SpringbootApplication {

   public static void main(String[] args) {
     //以为是启动了一个方法,没想到启动了一个服务
      SpringApplication.run(SpringbootApplication.class, args);
   }

}

@SpringBootApplication注解实际上是SpringBoot提供的一个复合注解,我们来看一看其源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
       . . .    
}

看得很清楚,其是一个合成体,但其中最重要的三个注解分别是:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

下面我们一一来分析这三个主要的注解:


@SpringBootConfiguration

这个注解比较简单,源码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}

这说明 @SpringBootConfiguration 也是来源于 @Configuration,二者功能都是将当前类标注为配置类,并将当前类里以 @Bean 注解标记的方法的实例注入到srping容器中,实例名即为方法名。

至于@Configuration,我想在非SpringBoot时代大家应该不陌生吧,作用是配置Spring容器,也即 JavaConfig 形式的 Spring IoC 容器的配置类所使用。

@EnableAutoConfiguration (重点)

这个注解实现类springboot的约定大于配置。这个注解可以帮助我们自动载入应用程序所需要的所有默认配置

源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    ...
}

这个注解上有两个比较特殊的注解@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage :自动配置包

源码如下:

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

@import :Spring底层注解@import , 给容器中导入一个组件

AutoConfigurationPackages.Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;

@Import({AutoConfigurationImportSelector.class}) :给容器导入组件

AutoConfigurationImportSelector :自动配置导入选择器


@EnableAutoConfiguration 注解启用自动配置,其可以帮助 SpringBoot 应用将所有符合条件的 @Configuration 配置都加载到当前 IoC 容器之中,可以简要用图形示意如下:

img

我们对照源码,简单分析一下这个流程:

  • @EnableAutoConfiguration 借助 AutoConfigurationImportSelector 的帮助,而后者通过实现 selectImports() 方法来导出 Configuration
  • image-20201223214625405

这里对getAutoConfiguration方法进行注释说明:

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
   // 检查自动装配开关
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
   // 获取EnableAutoConfiguration中的参数,exclude()/excludeName()
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
   // 获取需要自动装配的所有配置类,读取META-INF/spring.factories
   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
   // 去重,List转Set再转List
   configurations = removeDuplicates(configurations);
   // 从EnableAutoConfiguration的exclude/excludeName属性中获取排除项
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   // 检查需要排除的类是否在configurations中,不在报错
   checkExcludedClasses(configurations, exclusions);
   // 从configurations去除exclusions
   configurations.removeAll(exclusions);
   // 对configurations进行过滤,剔除掉@Conditional条件不成立的配置类
   configurations = filter(configurations, autoConfigurationMetadata);
   // 把AutoConfigurationImportEvent绑定在所有AutoConfigurationImportListener子类实例上
   fireAutoConfigurationImportEvents(configurations, exclusions);
   // 返回(configurations, exclusions)组
   return new AutoConfigurationEntry(configurations, exclusions);
}
  • AutoConfigurationImportSelector 类的 selectImports() 方法里面通过调用Spring Core 包里 SpringFactoriesLoader 类的 loadFactoryNames()方法(接上图)
  • image-20201223215109575
  • 最终通过 SpringFactoriesLoader.loadFactoryNames() 读取了 ClassPath 下面的 META-INF/spring.factories 文件来获取所有导出类。

    image-20201223225228323

  • 这个文件在这个位置

image-20201222234845038

而spring.factories 文件里关于 EnableAutoConfiguration 的配置其实就是一个键值对结构,样子大概长下面这样:

image-20201222234906132

说了这么多,如果从稍微宏观一点的角度 概括总结 上述这一过程那就是:

从 ClassPath下扫描所有的 META-INF/spring.factories 配置文件,并将spring.factories 文件中的 EnableAutoConfiguration 对应的配置项通过反射机制实例化为对应标注了 @Configuration 的形式的IoC容器配置类,然后注入IoC容器

@ComponentScan

@ComponentScan 对应于XML配置形式中的 context:component-scan,用于将一些标注了特定注解的bean定义批量采集注册到Spring的IoC容器之中,这些特定的注解大致包括:

  • @Controller
  • @Component
  • @Service
  • @Repository

等等

对于该注解,还可以通过 basePackages 属性来更细粒度的控制该注解的自动扫描范围,比如:

@ComponentScan(basePackages = {"com.njit.controller","cn.njit.entity"})

疑问点

1.@AutoConfigurationPackage和@ComponentScan区别(这里的可以理解需要验证下)

@AutoConfigurationPackage默认的情况下就是将:主配置类(@SpringBootApplication)的所在包及其子包里边的组件扫描到Spring容器中。比如说,你用了Spring Data JPA,可能会在实体类上写@Entity注解。这个@Entity注解由@AutoConfigurationPackage扫描并加载,而我们平时开发用的@Controller/@Service/@Component/@Repository这些注解是由ComponentScan来扫描并加载的。

  • 简单理解:这二者扫描的对象是不一样的。
  • 可以理解,前者是用来扫描springboot的自动装配,后者主要关心我们自己的代码中的@service、@controller等这些我们自己声明的需要被自动注入的bean
  • 文档中的话:
  • it will be used when scanning for code @Entity classes. It is generally recommended that you place EnableAutoConfiguration (if you're not using @SpringBootApplication) in a root package so that all sub-packages and classes can be searched.

2.spring.factories中所有的配置类都会被加载?

SpringBoot所有自动配置类都是在启动的时候进行扫描并加载,通过spring.factories可以找到自动配置类的路径,但是不是所有存在于spring,factories中的配置都进行加载,而是通过@ConditionalOnClass注解进行判断条件是否成立(只要导入相应的stater,条件就能成立),如果条件成立则加载配置类,否则不加载该配置类。

主启动方法(main方法)

我们只要运行主启动类中的main方法,整个服务就被开启了.这里我们主要关注两个:SpringApplication以及它的run方法

SpringApplication的实例化

@SpringBootApplication
public class MySpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringBootApplication.class, args);
    }
}
  • 首先我们进入run方法
// 调用静态类,参数对应的就是MySpringBootApplication.class以及main方法中的args
public static ConfigurableApplicationContext run(Class<?> primarySource,String... args) {
    return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
    return (new SpringApplication(sources)).run(args);
}

它实际上会构造一个SpringApplication的实例,并把我们的启动类MySpringBootApplication.class作为参数传进去,然后运行它的run方法

  • 然后我们进入SpringApplication构造器
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    //把MySpringBootApplication.class设置为属性存储起来
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    //设置应用类型是Standard还是Web
    this.webApplicationType = deduceWebApplicationType();
    //设置初始化器(Initializer),最后会调用这些初始化器
    setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class));
    //设置监听器(Listener)
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

先将HelloWorldMainApplication.class存储在this.primarySources属性中

  • 设置应用类型-进入deduceWebApplicationType()方法
private WebApplicationType deduceWebApplicationType() {
    if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
            && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    for (String className : WEB_ENVIRONMENT_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    return WebApplicationType.SERVLET;
}

// 相关常量
private static final String REACTIVE_WEB_ENVIRONMENT_CLASS = "org.springframework."
        + "web.reactive.DispatcherHandler";
private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework."
        + "web.servlet.DispatcherServlet";
private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",
        "org.springframework.web.context.ConfigurableWebApplicationContext" };

这里主要是通过类加载器判断REACTIVE相关的Class是否存在,如果不存在,则web环境即为SERVLET类型。这里设置好web环境类型,在后面会根据类型初始化对应环境。因为在pom中引入的web的启动器,它又依赖了spring-webmvc,spring-webmvc中存在DispatcherServlet这个类,也就是我们以前SpringMvc的核心Servlet,通过类加载能加载DispatcherServlet这个类,那么我们的应用类型自然就是WebApplicationType.SERVLET。

  • 设置初始化器(Initializer)

//设置初始化器(Initializer),最后会调用这些初始化器
setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class));

我们先来看看getSpringFactoriesInstances( ApplicationContextInitializer.class)

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

// 这里的入参type就是ApplicationContextInitializer.class
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    // 使用Set保存names来避免重复元素
    Set<String> names = new LinkedHashSet<>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 根据names来进行实例化
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    // 对实例进行排序
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

这里面首先会根据入参type读取所有的names(是一个String集合),然后根据这个集合来完成对应的实例化操作,这里和前面的EnableAutoConfiguration后面的是同一个方法(但是入参不同,这里只是读取取配置文件中Key为:org.springframework.context.ApplicationContextInitializer的value)。

// 入参就是ApplicationContextInitializer.class
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
  String factoryClassName = factoryClass.getName();

  try {
      //从类路径的META-INF/spring.factories中加载所有默认的自动配置类
      Enumeration<URL> urls = classLoader != null?classLoader.getResources("META-INF/spring.factories"):ClassLoader.getSystemResources("META-INF/spring.factories");
      ArrayList result = new ArrayList();

      while(urls.hasMoreElements()) {
          URL url = (URL)urls.nextElement();
          Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
          //获取ApplicationContextInitializer.class的所有值
          String factoryClassNames = properties.getProperty(factoryClassName);
          result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
      }

      return result;
  } catch (IOException var8) {
      throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + "META-INF/spring.factories" + "]", var8);
  }
}

接着就进入createSpringFactoriesInstances方法中,开始下面的实例化操作

// parameterTypes: 上一步得到的names集合
private <T> List<T> createSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
        Set<String> names) {
    List<T> instances = new ArrayList<T>(names.size());
    for (String name : names) {
        try {
            Class<?> instanceClass = ClassUtils.forName(name, classLoader);
            //确认被加载类是ApplicationContextInitializer的子类
            Assert.isAssignable(type, instanceClass);
            Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
            //反射实例化对象
            T instance = (T) BeanUtils.instantiateClass(constructor, args);
            //加入List集合中
            instances.add(instance);
        }
        catch (Throwable ex) {
            throw new IllegalArgumentException(
                    "Cannot instantiate " + type + " : " + name, ex);
        }
    }
    return instances;
}

确认被加载的类确实是org.springframework.context.ApplicationContextInitializer的子类,然后就是得到构造器进行初始化,最后放入到实例列表中.

因此,所谓的初始化器就是org.springframework.context.ApplicationContextInitializer的实现类,这个接口是这样定义的:

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

    void initialize(C applicationContext);

}

它在Spring上下文被刷新之前进行初始化的操作。典型地比如在Web应用中,注册Property Sources或者是激活Profiles。Property Sources比较好理解,就是配置文件。Profiles是Spring为了在不同环境下(如DEV,TEST,PRODUCTION等),加载不同的配置项而抽象出来的一个实体。

  • 最后设置监听器(Listener)

下面开始设置监听器:

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 这里的入参type是:org.springframework.context.ApplicationListener.class
private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<String>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

可以发现,这个加载相应的类名,然后完成实例化的过程和上面在设置初始化器时如出一辙,同样,还是以spring-boot-autoconfigure这个包中的spring.factories为例,看看相应的Key-Value:

org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

这10个监听器会贯穿springBoot整个生命周期。至此,对于SpringApplication实例的初始化过程就结束了。

总的来说做了这四件事:

1、推断应用的类型是普通的项目还是Web项目

2、查找并加载所有可用初始化器 , 设置到initializers属性中

3、找出所有的应用程序监听器,设置到listeners属性中

4、推断并设置main方法的定义类,找到运行的主类

SpringApplication.run方法

run方法一共有8个步骤,具体可以看下面参考链接。

spring boot的配置文件

springboot中支持两种格式的配置文件,而且配置文件的名字是固定的:application.propertiesapplication.yml,springboot在自动装配的时候会给配置类注入默认的属性,如果我们需要自定义属性,就直接在配置文件中定义就好,springboot在自动装配的时候会优先使用我们配置的属性,没有的话才会使用默认的属性。

给属性注入值

有时候我们需要自定义一些组件,比如一些工具类或者我们的service类中有需要自定义的属性,我们可以通过下面这两种方式去注入属性。不过我们更推荐使用后者的方式,更方便而且支持的能力更多。

new

通过@value注解

这种方式需要一个一个属性上写,如果比较少的话可以,如果属性多的话就会比较麻烦。这个spring的注解,可以通过表达式去获取,也可以设置如果没找到的默认值。

@Component
@Data
public class User {

    // 设置默认值(${value:defalutValue})
    @Value("${myuser.name:defalut}")
    private String name;

    @Value("${myuser.age:12}")
    private Integer age;

    // 支持复杂的类型
    @Value("${myuser.age:12}")
    private List<String> perfect;

}

然后在配置文件中写配置(properties和yml格式都可以)

myuser.name=hahaha
myuser.age=5
myuser.perfect="eat","sleep"

然后测试正常:

@SpringBootTest
@RunWith(SpringRunner.class)
public class AutoPropertiesTest {
    @Autowired
    private User user;

    @Test
    public void test(){
        System.out.println(user);
    }
}

--- 输出结果
 User(name=hahaha, age=5, perfect=["eat", "sleep"])

通过@ConfigurationProperties注解

只要在自己的类上添加一个注解,并指定前缀或者后缀等规则即可。(SpringBoot自动装配中就是使用这种方式

@Component
@ConfigurationProperties(prefix = "myuser")
@Data
public class User {
//    @Value("${myuser.name:defalut}")
    private String name;

//    @Value("${myuser.age:12}")
    private Integer age;

    // 支持复杂的类型
//    @Value("${myuser.age:12}")
    private List<String> perfect;

}

这样测试,会发现结果和上次的是一样的:

User(name=hahaha, age=5, perfect=["eat", "sleep"])

自动配置总结

通过在配置文件中添加debug=true就可以观察到spring boot自动装配(哪些生效哪些没有)的结果:

# 匹配到的自动装配的配置
Positive matches:
-----------------
 CodecsAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.http.codec.CodecConfigurer'(OnClassCondition)

 CodecsAutoConfiguration.JacksonCodecConfiguration matched:
      - @ConditionalOnClass found required class 'com.fasterxml.jackson.databind.ObjectMapper' (OnClassCondition)
......
# 匹配不成功的配置
Negative matches:
-----------------

   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)

   AopAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'org.aspectj.lang.annotation.Aspect' (OnClassCondition) 
......

整体流程

1

spring boot会默认给我们加载大量的自动配置类,并通过@ConditionalXXX去判断是否注入组件,在注入是会引入Properties配置类(封装配置文件的相关属性,通过ConfigurationProperties注解),配置类中有默认值,我们可以在配置文件中设置这些值去覆盖默认值,这样我们只要配置我们自定义的一些配置就好,其他的springboot就默认帮我们注入好了,我们只要使用即可。

扩展springMVC

springboot已经帮我们配置好了很多springmvc的配置,如果我们还想添加一些自己的配置,springboot已经给我们预留了方案。

在spring boot1.0+,我们可以使用WebMvcConfigurerAdapter来扩展springMVC的功能,其中自定义的拦截器并不会拦截静态资源(js、css等)。

在Spring Boot2.0版本中,WebMvcConfigurerAdapter这个类被弃用了那么我们扩展有两种方法,继承WebMvcConfigurationSupport或者 实现WebMvcConfigurer接口

注:如果配置类上使用@EnableWebMvc,则会接管默认的配置,如果是扩展则不需要加上这个注解

1.继承WebMvcConfigurationSupport(不推荐)

@Configuration
public class MyMvcConfig extends WebMvcConfigurationSupport {

}

我们可以看到里面有很多的方法可以重写:

image-20210103123003840

继承WebMvcConfigurationSupport之后,可以使用这些add…方法添加自定义的拦截器、试图解析器等等这些组件。如下:

@Configuration
public class MyMvcConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/login.html").setViewName("login");
    }
}

但这里有个问题就是,当你继承了WebMvcConfigurationSupport并将之注册到容器中之后,Spring Boot有关MVC的自动配置就不生效了。

可以看一下Spring Boot启动WebMVC自动配置的条件:

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

其中一个条件就是@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),只有当容器中没有WebMvcConfigurationSupport这个类型的组件的时候,才会启动自动配置。

所以当我们继承WebMvcConfigurationSupport之后,除非你自己对代码把控的相当的好,在继承类中重写了一系列有关WebMVC的配置,否则可能就会遇到静态资源访问不到,返回数据不成功这些一系列问题了。

2.实现WebMvcConfigurer接口(推荐)

我们知道,Spring Boot2.0是基于Java8的,Java8有个重大的改变就是接口中可以有default方法,而default方法是不需要强制实现的。上述的WebMvcConfigurerAdapter类就是实现了WebMvcConfigurer这个接口,所以我们不需要继承WebMvcConfigurerAdapter类,可以直接实现WebMvcConfigurer接口,用法与继承这个适配类是一样的:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {


    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/aiden").setViewName("success");
    }

    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
       WebMvcConfigurer wmc = new WebMvcConfigurer() {
           @Override
           public void addViewControllers(ViewControllerRegistry registry) {
               registry.addViewController("/").setViewName("login");
               registry.addViewController("/index.html").setViewName("login");
           }
       };
       return wmc;
    }

}

这两种方法都可以作为WebMVC的扩展,去自定义配置。区别就是继承WebMvcConfigurationSupport会使Spring Boot关于WebMVC的自动配置失效,需要自己去实现全部关于WebMVC的配置,而实现WebMvcConfigurer接口的话,Spring Boot的自动配置不会失效,可以有选择的实现关于WebMVC的配置。

自定义starter

所谓的 Starter ,其实就是一个普通的 Maven 项目,因此我们自定义 Starter ,需要首先创建一个普通的 Maven 项目,创建完成后,添加 Starter 的自动化配置类即可,如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.1.8.RELEASE</version>
</dependency>

我们首先创建一个 HelloProperties 类,用来接受 application.properties 中注入的值,如下:

@ConfigurationProperties(prefix = "javaboy")
public class HelloProperties {
    private static final String DEFAULT_NAME = "NJITZYD";
    private static final String DEFAULT_MSG = "自定义的默认值";
    private String name = DEFAULT_NAME;
    private String msg = DEFAULT_MSG;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

这个配置类很好理解,将 application.properties 中配置的属性值直接注入到这个实例中, @ConfigurationProperties 类型安全的属性注入,即将 application.properties 文件中前缀为 javaboy 的属性注入到这个类对应的属性上, 最后使用时候,application.properties 中的配置文件,大概如下:

javaboy.name=zhangsan
javaboy.msg=java

配置完成 HelloProperties 后,接下来我们来定义一个 HelloService ,然后定义一个简单的 say 方法, HelloService 的定义如下:

public class HelloService {
    private String msg;
    private String name;
    public String sayHello() {
        return name + " say " + msg + " !";
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

接下来就是我们的重轴戏,自动配置类的定义,用了很多别人定义的自定义类之后,我们也来自己定义一个自定义类:

@Configuration
@EnableConfigurationProperties(HelloProperties.class)
@ConditionalOnClass(HelloService.class)
public class HelloServiceAutoConfiguration {
    @Autowired
    HelloProperties helloProperties;

    @Bean
    HelloService helloService() {
        HelloService helloService = new HelloService();
        helloService.setName(helloProperties.getName());
        helloService.setMsg(helloProperties.getMsg());
        return helloService;
    }
}

关于这一段自动配置,解释如下:

  • 首先 @Configuration 注解表明这是一个配置类。
  • @EnableConfigurationProperties 注解是使我们之前配置的 @ConfigurationProperties 生效,让配置的属性成功的进入 Bean 中。
  • @ConditionalOnClass 表示当项目当前 classpath 下存在 HelloService 时,后面的配置才生效。
  • 自动配置类中首先注入 HelloProperties ,这个实例中含有我们在 application.properties 中配置的相关数据。
  • 提供一个 HelloService 的实例,将 HelloProperties 中的值注入进去。

做完这一步之后,我们的自动化配置类就算是完成了,接下来还需要一个 spring.factories 文件,用来告诉springboot吴自动注入我们的starter。

我们首先在 Maven 项目的 resources 目录下创建一个名为 META-INF 的文件夹,然后在文件夹中创建一个名为 spring.factories 的文件,文件内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.njit.config.HelloServiceAutoConfiguration

到这里,代码已经写完了,我么可以把自定义的starte打包到本地以及上传公司的私服中,这样就可以使用了。

测试

在另一个项目中引用我们自定义的starter:

<dependency>
    <groupId>com.njit</groupId>
    <artifactId>helllo-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

然后在配置文件中编写配置:

javaboy.name=zhangsan
javaboy.msg=java

配置完成后,方便起见,我这里直接在单元测试方法中注入 HelloSerivce 实例来使用,代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UsemystarterApplicationTests {

    @Autowired
    HelloService helloService;
    @Test
    public void contextLoads() {
        System.out.println(helloService.sayHello());
    }
}

可以看到控制台正常的使用到:

image-20210103150908365

参考

springboot自动注入以及scan和autopage之间的区别

自动装配原理

自动装配如何条件加载

自动加载在什么时候进行过滤的细节解答(很重要的参考)

springboot中main方法启动流程(重要参考)


njitzyd
58 声望7 粉丝