Auto-configuration的规范

尽管springboot的官方文档很糟糕,但是关于Auto-configuration还是写得比较好的,位于springboot reference中的Creating Your Own Auto-configuration。

至于为什么要用 Auto-configuration,可以见我写得这一篇文章:@SpringBootApplication中几点解惑

springboot官方提供的一个样例,非常不错,看了以后,auto-configuration就学会了:Master Spring Boot auto-configuration

auto-configuration的代码分层架构

官方推荐分成2个工程,一个starter,另一个autoconfigure。
但是我个人认为就那么一个starter就好了,何必分成2个工程,感觉多此一举的,因为starter纯粹就是一个空工程。
不过,话反过来说,学会了2个分开的工程,合成一个工程也是超级简单的,所以无所谓一个工程还是2个工程。

命名常规

官方的auto-configuration,比如yyy来举例,那么应该是 spring-boot-starter-yyy.jar,spring-boot-autoconfigure-yyy.jar
非官方的auto-configuration,比如xxx来举例,那么应该是 xxx-spring-boot-starter.jar,xxx-spring-boot-autoconfigure.jar
上面已经描述很清晰了,不再解释了。

spring-factories

对于auto-configuration工程来说,已经取消了Component auto scan的功能,代而取之的是采用spring-factories的方式,这个spring-factories文件位于autoconfigure这个工程中。

spring-provides

这个文件在springboot的官方文档中是没有提到的,那这个spring-provides文件用来干什么的?我查了有个人这么解释的:

It's for tooling. STS (and other IDEs if they chose to) can index those files and make autocomplete suggestions based on stuff that isn't yet on the classpath.

哦,原来是给STS,或者IDEA用的,这个文件放置在starter这个工程中,那就不用去管这个文件了。

好了,springboot的 auto-configuration就这么简单。

protobuf和springboot的结合

protobuf的java example见:protobuf的java学习版

在那篇文章中我们也说了,protobuf仅用于一些对性能要求特别高的场景,缺点就是和传统的java编程还是有一些不耦合的地方,需要进行转换,导致了编程有些麻烦。

在这篇文章中,我们来看一下 protobuf如何和springboot进行结合:

  1. 新建一个maven的springboot工程,在这个工程的pom中加入依赖:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java-util</artifactId>
            <version>3.10.0</version>
        </dependency>
        <dependency>
            <groupId>com.googlecode.protobuf-java-format</groupId>
            <artifactId>protobuf-java-format</artifactId>
            <version>1.4</version>
        </dependency>
  1. 本例子还是使用官方的 addressbook.proto 这个例子,对这个文件中的java输出包改成:
    option java_package = “com.champbay.blog.protobuf.model”;
    其他不变,然后在这个目录中运行:protoc –java_out=. addressbook.proto,将生成 com/champbay/blog/protobuf/model/AddressBookProtos.java

  2. 创建一个Configuration来加载 ProtobufHttpMessageConverter 的Bean

package com.champbay.blog.protobuf.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;

@Configuration
public class WebConfig {

    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

}

此 ProtobufHttpMessageConverter 的作用是对 protobuf 进行 encode 和 decode

  1. 创建一个 Rest Controller:
package com.champbay.blog.protobuf.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.champbay.blog.protobuf.model.AddressBookProtos;
import com.champbay.blog.protobuf.model.AddressBookProtos.Person;

@Controller
public class TestController {

    @RequestMapping(value = "/test", consumes = "application/x-protobuf", produces = "application/x-protobuf")
    @ResponseBody
    public AddressBookProtos.Person getPersonProto(@RequestBody AddressBookProtos.Person person) {
        System.out.println("id: " + person.getId());
        System.out.println("name: " + person.getName());
        System.out.println("email: " + person.getEmail());

        //这个地方需要加入将protobuf的对象转换成普通的java pojo的方法 !

        Person.Builder builder = Person.newBuilder();
        builder.setId(2);
        builder.setName("myhello");
        builder.setEmail("myhello@aaa.com");

        return builder.build();
    }

}

从这个类中能看到需要加入将protobuf的对象转换成普通的java pojo的方法,这个方法可以参考: Protobuf与POJO的相互转化 – 通过Json,也能看出protobuf要想方便使用,这个道路还是比较难的,可谓是“鱼和熊掌不可兼得”

5,最后通过 HttpClient 来调用这个 rest。

具体可以见源代码:https://github.com/champbay/protobuf-springboot-test

@SpringBootApplication中几点解惑

@SpringBootApplication这个annotation是由3个annotation组合而成:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

假如你愿意,也可以分别用这3个annotation来代替@SpringBootApplication这一个annotation。

在讲下面之前,严重吐槽一下 springboot 的官方文档,它对所涉及的内容都提到那么一点点,但是每一点都是语焉不详,非常的晦涩。

@ComponentScan中的basePackages 和 @SpringBootApplication中的scanBasePackages有什么关系?

我们稍微复杂点的应用程序,一般都有core这样的jar包引入,core.jar包的包名和springboot应用的包名前面部分基本上一样。这个架构如下图所示:

core的包为:
    com.champbay.core

application的结构如下:
    com.champbay.app
    -->com.champbay.app.util
    -->com.champbay.app.service
    -->com.champbay.app.web
    -->com.champbay.app.Application.java

官方文档中有一段描述如下:

We generally recommend that you locate your main application class in a root package above other classes. 
The @SpringBootApplication annotation is often placed on your main class, 
and it implicitly defines a base “search package” for certain items.

上面这段话的描述反映了下面几点:
1,springboot工程有一个main函数的类。
2,这个main函数的类应该位于 com.champbay.app 下面。
3,这个main函数的类上有@SpringBootApplication的注解。
4,这个@SpringBootApplication注解,隐式地表示了凡是在com.champbay.app以及com.champbay.app下面的子包(com.champbay.app.util,com.champbay.app.service,com.champbay.app.web等)中的诸如:@Component,@Repository,@Controller等都将自动扫描。

那么,Application.java启动的时候,会自动扫描com.champbay.app下面的所有子包,但是对于com.champbay.core下面的就不会自动加载。为了解决这个问题,需要在@ComponentScan设置basePackages为com.champbay即可,这样com.champbay.core就会被自动扫描注册那些bean。

我们为了能够自动扫描那些包,需要设置@ComponentScan的属性basePackages,但是总不能因为这样的需求而不用@SpringBootApplication,而用3个annotation来写太麻烦了,所以,在@SpringBootApplication中,定义了一个scanBasePackages,效果和@ComponentScan的basePackages完全一样。我们来看看Spring文档关于这一段的描述,看完后又想XXX了,简直太惜字如金了:

@SpringBootApplication also provides aliases to customize the attributes of @EnableAutoConfiguration and @ComponentScan.
然后呢?然后就没有了

好吧,让我们来看看@SpringBootApplication的源代码吧:

/**
     * Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
     * for a type-safe alternative to String-based package names.
     * @return base packages to scan
     * @since 1.3.0
     */
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

如此就完全明白了。scanBasePackages就是basePackages的别名!

我们从上面看到,当在 com.champbay.app 不同包下面,有@Component、@Repository、@Controller的时候,需要指定scanBasePackages。
但是我们从另外一篇文章:Auto-configuration的规范中可以看到,springboot不建议用这种方式来自动加载,而是采用如下的方式:

Spring Boot checks for the presence of a META-INF/spring.factories file within your published jar. 
The file should list your configuration classes under the EnableAutoConfiguration key, as shown in the following example:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

Auto-configurations must be loaded that way only. 
Make sure that they are defined in a specific package space and that they are never the target of component scanning. 
Furthermore, auto-configuration classes should not enable component scanning to find additional components. 
Specific @Imports should be used instead.

也就是说,不要用scanBasePackages这样的方式,而是采用 META-INF/spring.factories 的方式。

springboot下报错“MethodInvokingJobDetailFactoryBean is not serializable”

quartz是在java语言中用得最为广泛的任务调度框架,spring对quartz进行了集成,方便了开发和使用。

在springboot下,我们对一个采用quartz的app进行多个集群部署的时候,对于quartz的集群采用数据库集群方式,报了“MethodInvokingJobDetailFactoryBean is not serializable”的错误。

原因是spring为了让开发更方便,耦合性更低,对quartz中 JobDetailFactoryBean 用 MethodInvokingJobDetailFactoryBean 进行了封装,假如quartz是单机运行那是没有关系的,但是当采用数据库方式进行集群的时候,quartz会把 JobDetailFactoryBean 序列化存储到数据库中,假如用 MethodInvokingJobDetailFactoryBean 的话,就会报错,说不能序列化。

解决的办法参考这篇文章:使用Spring + quartz集群持久化时注意事项,但是这篇文章中只讲了在spring下如何修改。

下面讲一下在 springboot 中如何修改:
一、需要将原先定义的pojo 的 Job 改成实现 Job 接口:

@Component("myJobHandler")
public class MyJobHandler implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("do it ......");
    }

}

二、在配置中如下设置:

@Bean(name = "myJobDetailFactoryBean")
    public JobDetailFactoryBean getMyJobDetailFactoryBean() {
        JobDetailFactoryBean jobDetail = new JobDetailFactoryBean();
        jobDetail.setJobClass(com.champbay.quartztest.MyJobHandler.class);
        jobDetail.setDurability(true);
        return jobDetail;
    }

    @Bean(name = "myCronTriggerFactoryBean")
    public CronTriggerFactoryBean getMyCronTriggerFactoryBean(
            @Qualifier("myJobDetailFactoryBean") JobDetailFactoryBean jobDetail) {
        CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
        tigger.setJobDetail(jobDetail.getObject());
        // 每1分钟     
        tigger.setCronExpression("0 0/1 * * * ?");
        return tigger;
    }

本文结束。

如何在springboot中禁用某些jar的自动配置功能

我们知道在springboot中有这么一个自动配置的功能:假如你在工程中加入某个jar,比如mysql的jar,或者mongodb的jar,或者redis的jar,那么就算不写配置文件,springbboot也会启动相关的功能来简化构建这些客户端的复杂度。

但是有那么一种场景,比如我只想在工程中使用某个jar的某个类的功能,所以我必须要引入某个jar,但是又不想让springboot自动配置,那该怎么办?

其实很简单,只需要在启动类上进行配置,如下用mongodb来举例:

@SpringBootApplication(exclude={MongoAutoConfiguration.class})
public class Application {

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

}

这个时候来问题了,我假如想不包含 redis 的自动配置,那该如何写这个 XXXConfiguration.class 呢,这个到哪里找?
在回答这个问题之前,我们先来看一个错误提示,以redis的自动配置来举例,当设置不正确的时候,springboot启动的时候会进行报错,报错的内容大致如下:

Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled

with ‘debug’ enabled,这个是什么鬼?这个debug在哪里设置啊,难道在 logback 中?不是吧。带着这个问题,我查了资料,找到这篇文章:http://www.it1352.com/908199.html,这篇文章里讲了该如何在springboot中进行设置,哦,原来就是在 application.properties 中假如 debug=true 即可。顺便吐槽一下这篇文章的网站,里面广告太多了,感觉是一个。。。

好了,我们继续上面的那个redis那个例子,我们把 debug=true 设置好,好了,真相出来了:

    RedisAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'org.springframework.data.redis.connection.jedis.JedisConnection', 'org.springframework.data.redis.core.RedisOperations', 'redis.clients.jedis.Jedis'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)

   RedisAutoConfiguration.RedisConfiguration#redisTemplate matched:
      - @ConditionalOnMissingBean (names: redisTemplate; SearchStrategy: all) did not find any beans (OnBeanCondition)

   RedisAutoConfiguration.RedisConfiguration#stringRedisTemplate matched:
      - @ConditionalOnMissingBean (types: org.springframework.data.redis.core.StringRedisTemplate; SearchStrategy: all) did not find any beans (OnBeanCondition)

   RedisAutoConfiguration.RedisConnectionConfiguration matched:
      - @ConditionalOnClass found required class 'org.apache.commons.pool2.impl.GenericObjectPool'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)

   RedisAutoConfiguration.RedisConnectionConfiguration#redisConnectionFactory matched:
      - @ConditionalOnMissingBean (types: org.springframework.data.redis.connection.RedisConnectionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition)

   RedisCacheConfiguration matched:
      - Cache org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration automatic cache type (CacheCondition)

   RedisRepositoriesAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'redis.clients.jedis.Jedis', 'org.springframework.data.redis.repository.configuration.EnableRedisRepositories'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
      - @ConditionalOnProperty (spring.data.redis.repositories.enabled=true) matched (OnPropertyCondition)
      - @ConditionalOnMissingBean (types: org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean; SearchStrategy: all) did not find any beans (OnBeanCondition)

于是,我们就可以这么设置我们的启动类:

@SpringBootApplication(exclude={RedisRepositoriesAutoConfiguration.class,RedisAutoConfiguration.class})
public class Application {

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

}

上面的RedisCacheConfiguration是个私有类,不能用,不用也达到了效果。

全文结束。

SpringBootTest中启用不同的profiles

在springboot中,我们的工程常常有下面的配置文件:

application.properties
application-prod.properties
application-dev.properties

其中,application.properties是默认就加载的,prod和dev是需要在application.properties中指定spring.profiles.active=xxx来确定。

在测试的时候,需要测试环境,不管是prod还是dev都不适用的,所以在测试的时候,需要有个地方来设定配置文件。

查了资料,首先查到一篇文章:如何在@SpringBootTest中动态地启用不同的profiles,这篇文章讲了一大堆,我也没有全部看全,可能场景不适合我吧,总觉得这个需求应该非常普遍简单的,总不至于这么复杂的来解决吧。

继续查资料,看到这一篇文章:https://stackoverflow.com/questions/41985262/spring-boot-test-overriding-bootstrap-properties,这里有个人回答了几种办法,其中使用 Add @ActiveProfiles(‘test’),大家 test class 这种方式我觉得应该是最最正确的解决办法。

为此,我做了一个很小的测试工程放在 github 上,用来验证@ActiveProfiles(‘test’),大家可以去下来junit测试看看,看了后就能明白了。

工程地址:
https://github.com/champbay/springboot-test.git

springboot如何正确使用tomcat连接池

在springboot的application.properties中,我们常常这么设置:

spring.datasource.url=jdbc:mysql://192.168.0.1:3306/dbname
spring.datasource.username=aaa
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

假如只有上面这些关于连接池的设置,那么springboot将根据 org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder 中的下面次序去找有没有相应的jar:

private static final String[] DATA_SOURCE_TYPE_NAMES = new String[] {
            "org.apache.tomcat.jdbc.pool.DataSource",
            "com.zaxxer.hikari.HikariDataSource",
            "org.apache.commons.dbcp.BasicDataSource", // deprecated
            "org.apache.commons.dbcp2.BasicDataSource" };

假如工程是内置tomcat容器的,那么默认就是 org.apache.tomcat.jdbc.pool.DataSource,同时tomcat连接池的那些最大最小参数都是默认值:
initialSize 10
maxActive 100
maxIdle 100
minIdle 10
可以看到这些默认值基本上也能满足大多数场景了。

假如觉得上面的这些值还不够好,那么需要在application.properties中这么设置才可以:
spring.datasource.tomcat.initialSize=10
spring.datasource.tomcat.maxActive=200
spring.datasource.tomcat.maxIdle=200
spring.datasource.tomcat.minIdle=10

这是因为在 org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration.Tomcat 这个类中如下定义:

@Bean
        @ConfigurationProperties(prefix = "spring.datasource.tomcat")
        public org.apache.tomcat.jdbc.pool.DataSource dataSource(
                DataSourceProperties properties) {
            org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(
                    properties, org.apache.tomcat.jdbc.pool.DataSource.class);
            DatabaseDriver databaseDriver = DatabaseDriver
                    .fromJdbcUrl(properties.determineUrl());
            String validationQuery = databaseDriver.getValidationQuery();
            if (validationQuery != null) {
                dataSource.setTestOnBorrow(true);
                dataSource.setValidationQuery(validationQuery);
            }
            return dataSource;
        }

可以看到 prefix = “spring.datasource.tomcat”,所以需要在配置中的前缀应该是 spring.datasource.tomcat,不这么设置,实际上是没有启用这些参数的。

最后,如何验证这些参数是否生效:在 org.apache.tomcat.jdbc.pool.DataSourceProxy 这个类的getConnection()方法中加个断点,然后运行后访问数据库时,查看这个 datasource 的这些参数。

下一篇将是 《springboot如何正确使用dbpc2连接池》