Spring Boot 初体验

Spring Boot的理念是抛弃XML配置(当然,如果你想采用XML配置也是可以的,但是Spring Boot推荐使用基于Java的配置),采用纯Java配置和properties或yml文件配置,通过提供一系列的Starter可以使开发者快速的搭建起一套开发环境。Starter将某一工具相关的依赖整合到了一起,通过依赖一个Starter会间接的依赖该Starter相关的所有依赖。使用的Starter在启用了自动配置时是可以自识别的,即只要把它们加入到Classpath中,在程序启动的时候可以进行自动的识别和启动。比如想使用Spring Data MongoDB,只需加入spring-boot-starter-data-mongodb依赖,然后通过配置文件配置MongoDB对应的配置信息即可。使用Spring Boot时,通常会使用Maven进行项目管理,然后指定spring-boot-starter-parent为父项目。spring-boot-starter-parent中间接的提供了一些``,将它作为父项目后在项目需要使用依赖时可以忽略版本号的配置,非常的方便。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
</parent>

定义好的``可以从artifactId为spring-boot-dependencies的pom.xml中查看,它属于spring-boot-starter-parent的父项目。如果需要改变依赖项的版本号,可以在子项目中重写对应的properties中的版本号。

比如想要开发一个Web项目,在项目的pom.xml文件中加入spring-boot-starter-web的依赖,在项目启动时就会自动的按照SpringMVC的配置为我们配置好。这是由Spring boot的autoconfigure模块来实现的,autoconfigure模块中定义了一系列的自动配置类,它们的自动配置规则基本都差不多。比如在classpath下拥有某Class的时候进行自动配置,或者在配置文件中配置了某个属性后进行自动配置,等等。每个自动配置类都使用了@Configuration标注。需要启用自动配置,需要在Spring Boot的入口类上加上@EnableAutoConfiguration注解。

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

查看spring-boot-starter-web的pom.xml文件可以看到它打包了以下依赖项:

 <dependencies>
   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter</artifactId>
     <version>2.0.3.RELEASE</version>
     <scope>compile</scope>
   </dependency>
   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-json</artifactId>
     <version>2.0.3.RELEASE</version>
     <scope>compile</scope>
   </dependency>
   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-tomcat</artifactId>
     <version>2.0.3.RELEASE</version>
     <scope>compile</scope>
   </dependency>
   <dependency>
     <groupId>org.hibernate.validator</groupId>
     <artifactId>hibernate-validator</artifactId>
     <version>6.0.10.Final</version>
     <scope>compile</scope>
   </dependency>
   <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-web</artifactId>
     <version>5.0.7.RELEASE</version>
     <scope>compile</scope>
   </dependency>
   <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-webmvc</artifactId>
     <version>5.0.7.RELEASE</version>
     <scope>compile</scope>
   </dependency>
 </dependencies>

其它的Starter也是类似的,打包了相关的依赖项。

SpringMVC的自动配置类由DispatcherServletAutoConfiguration定义,其主要源码如下所示。我们可以看到类上面拥有各种各样的Conditional打头的注解,它们都是用来定义对应的配置生效的条件。

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
@EnableConfigurationProperties(ServerProperties.class)
public class DispatcherServletAutoConfiguration {
    /*
     * The bean name for a DispatcherServlet that will be mapped to the root URL "/"
     */
    public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
    /*
     * The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
     */
    public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
    @Configuration
    @Conditional(DefaultDispatcherServletCondition.class)
    @ConditionalOnClass(ServletRegistration.class)
    @EnableConfigurationProperties(WebMvcProperties.class)
    protected static class DispatcherServletConfiguration {
        private final WebMvcProperties webMvcProperties;
        public DispatcherServletConfiguration(WebMvcProperties webMvcProperties) {
            this.webMvcProperties = webMvcProperties;
        }
        @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public DispatcherServlet dispatcherServlet() {
            DispatcherServlet dispatcherServlet = new DispatcherServlet();
            dispatcherServlet.setDispatchOptionsRequest(
                    this.webMvcProperties.isDispatchOptionsRequest());
            dispatcherServlet.setDispatchTraceRequest(
                    this.webMvcProperties.isDispatchTraceRequest());
            dispatcherServlet.setThrowExceptionIfNoHandlerFound(
                    this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
            return dispatcherServlet;
        }
        @Bean
        @ConditionalOnBean(MultipartResolver.class)
        @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
        public MultipartResolver multipartResolver(MultipartResolver resolver) {
            // Detect if the user has created a MultipartResolver but named it incorrectly
            return resolver;
        }
    }
    @Configuration
    @Conditional(DispatcherServletRegistrationCondition.class)
    @ConditionalOnClass(ServletRegistration.class)
    @EnableConfigurationProperties(WebMvcProperties.class)
    @Import(DispatcherServletConfiguration.class)
    protected static class DispatcherServletRegistrationConfiguration {
        private final ServerProperties serverProperties;
        private final WebMvcProperties webMvcProperties;
        private final MultipartConfigElement multipartConfig;
        public DispatcherServletRegistrationConfiguration(
                ServerProperties serverProperties, WebMvcProperties webMvcProperties,
                ObjectProvider<MultipartConfigElement> multipartConfigProvider) {
            this.serverProperties = serverProperties;
            this.webMvcProperties = webMvcProperties;
            this.multipartConfig = multipartConfigProvider.getIfAvailable();
        }
        @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
        @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public ServletRegistrationBean<DispatcherServlet> dispatcherServletRegistration(
                DispatcherServlet dispatcherServlet) {
            ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(
                    dispatcherServlet,
                    this.serverProperties.getServlet().getServletMapping());
            registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
            registration.setLoadOnStartup(
                    this.webMvcProperties.getServlet().getLoadOnStartup());
            if (this.multipartConfig != null) {
                registration.setMultipartConfig(this.multipartConfig);
            }
            return registration;
        }
    }
    //...忽略部分代码,完整的代码请参考对应的源码
}

@EnableConfigurationProperties用来定义该自动配置内部可以使用的配置参数对应的配置类,配置参数对应的配置类上都会使用@ConfigurationProperties指定配置属性对应的前缀,然后配置类上的每一个属性加上对应的配置属性前缀组成一个完整的配置文件中的属性名。比如下面的WebMvcProperties指定的属性前缀是spring.mvc,其属性dateFormat将匹配配置文件中的spring.mvc.dateFormat属性值。如果配置类的属性仍然是一个对象,则可以进行多级嵌套,多个层级之间通过英文的句号进行连接。

@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
    /**
     * Formatting strategy for message codes. For instance, `PREFIX_ERROR_CODE`.
     */
    private DefaultMessageCodesResolver.Format messageCodesResolverFormat;
    /**
     * Locale to use. By default, this locale is overridden by the "Accept-Language"
     * header.
     */
    private Locale locale;
    /**
     * Define how the locale should be resolved.
     */
    private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER;
    /**
     * Date format to use. For instance, `dd/MM/yyyy`.
     */
    private String dateFormat;
    /**
     * Whether to dispatch TRACE requests to the FrameworkServlet doService method.
     */
    private boolean dispatchTraceRequest = false;
    /**
     * Whether to dispatch OPTIONS requests to the FrameworkServlet doService method.
     */
    private boolean dispatchOptionsRequest = true;
    /**
     * Whether the content of the "default" model should be ignored during redirect
     * scenarios.
     */
    private boolean ignoreDefaultModelOnRedirect = true;
    /**
     * Whether a "NoHandlerFoundException" should be thrown if no Handler was found to
     * process a request.
     */
    private boolean throwExceptionIfNoHandlerFound = false;
    /**
     * Whether to enable warn logging of exceptions resolved by a
     * "HandlerExceptionResolver".
     */
    private boolean logResolvedException = false;
    /**
     * Path pattern used for static resources.
     */
    private String staticPathPattern = "/**";
    //...忽略部分代码
}

这样我们就可以直接在项目中定义Controller,并定义对应的处理器方法。

@Controller
public class SampleController {
    @RequestMapping("sample/helloworld")
    public void sample(Writer writer) throws IOException {
        writer.append("hello world!");
        writer.flush();
    }
}

接下来需要启动项目,项目可以按照纯Java项目进行启动,也可以打包为war包丢到Tomcat中。以下是直接按照纯Java项目进行启动的方式。通常建议在项目的根包路径下创建一个Class用于启动项目,而且在根包下面通常只有这样一个启动类。比如下面我们的Application如果是定义在com.elim.springboot包下面的,则项目在启动后Spring将自动将com.elim.springboot包作为根包进行bean定义扫描,这是由于类上加了@SpringBootApplication。启动类需要使用@SprintBootApplication注解进行标注,然后在main方法体中调用SpringApplication的run方法,然后传递需要作为配置类的Class作为第一个参数,如果有多个这样的Class,也可以传递一个数组,第二个参数为启动时传递的参数,通常就直接取main方法传递的参数,这样Spring Boot就启动了。@SprintBootApplication上是使用了@EnableAutoConfiguration注解的,所以下面的代码将启用Spring Boot的自动配置机制。另外@SpringBootApplication上使用了@Configuration@ComponentScan,这就相当于使用@SpringBootApplication标注的类等价于标注了@Configuration@ComponentScan。这允许我们在@SpringBootApplication类中定义Spring Bean、进行bean扫描等。

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

Spring Boot的核心是基于Java配置的Spring的进一步封装,所以我们在它的配置类上可以像在使用Java配置类时一样使用,比如加上@EnableAsync@ImportResource@Import等。当有多个配置类时,Spring Boot也是建议在主配置类上进行引入。

启动完成后就可以打开浏览器,通过/sample/helloworld访问上面定义的SampleController的sample方法了。

Spring Boot的核心配置文件是类根路径下的application.properties文件或application.yml文件。Spring Boot中需要的外部配置信息基本都从这里来,包括前面提到的自动配置的ConfigurationProperties。看下面代码中,类SampleController有一个appId属性,其值由test.appId这么一个占位符指定。

@Controller
public class SampleController {
    @Value("${test.appId}")
    private String appId;
  
    @RequestMapping("sample/helloworld")
    public void sample(Writer writer) throws IOException {
        writer.append("hello world!");
        writer.append(this.appId);
        writer.flush();
    }
}

那么在我们的application.properties文件中就可以指定这么一个占位符并进行赋值。程序运行时appId的值就将被替换为下面配置的Spring Boot

test.appId=Spring Boot

Spring Boot的配置文件其实除了application.properties或application.yml外,根据启动的profile的不同,还可以读取带profile后缀的配置文件。比如启动的profile是dev,则可以读取application-dev.properties或application-dev.yml文件。