spring 漫游

spring-boot-autoconfiguration

Condition

AutoConfiguration的condition包下面有一套条件注解,用来决定什么时候创建bean,基本上所有的自动配置类都会用到里面的注解来“智能”的决定什么时候创建bean。

注解 解释
@ConditionalOnBean 当BeanFactory中存在某个bean时满足条件,可以匹配多个
@ConditionalOnClass 当ClassPath下存在某个类时满足条件,可以匹配多个
@ConditionalOnCloudPlatform 巴拉巴拉
@ConditionalOnExpression 可以定义SpEL语句进行匹配
@ConditinalOnJava 匹配当前运行JVM版本
@ConditionalOnJndi
@ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditinalOnNotWebApplication
@ConditionalOnProperty 当满足某个属性时满足条件
@ConditionalOnResource 当classpath下存在某个资源文件时满足条件,可以匹配多个
@ConditinalOnSingleCandidate
@ConditinalOnWebApplication

举几个🌰:

@Configuration
@ConditionalOnProperty(prefix = "cn.shareif.auth", value = "enabled", matchIfMissing = true)
public class ShareifAuthAutoConfiguration {

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new AuthFilter());
        bean.addUrlPatterns("/*");
        return bean;
    }
}

设置Web拦截器,当然设置拦截器有更简单的方法,比如@WebFilter注解,此处仅仅是举一个@ConditinalOnProperty的栗子。当项目的配置文件中(比如 application.properties)定义了cn.shareif.auth.enabled=true 时,就会触发这个自动配置类,如果没有配置属性,默认自动触发。

@Slf4j
@Configuration
@ConditionalOnResource(resources = "classpath:lib/certificate.so")
public class LibResourceAutoConfiguration {

    @Bean
    public CertificateHelper load() {
        // load from resources and return a helper bean consist of this resource.
        log.info("Oh, you have loaded the resource");
        return new CertificateHelper();
    }
}

当classpath中存在某个资源文件时,进行自动配置。

Jackson

无起步依赖。

Springboot默认的Json实现是Jackson,其对应的自动配置类为JacksonAutoConfiguration,同时通过JacksonHttpMessageConvertersConfiguration 配置HttpMessageConverter实现类用于Http请求中Json到Java对象或者Java对象到Json的转换。

JacksonAutoConfiguration的触发条件为存在类:com.fasterxml.jackson.databind.ObjectMapper.class。此配置将提供一些基础bean,比如ObjectMapper bean,JsonComponentModule bean。ObjectMapper bean是POJO和String转换的关键角色,而JsonComponentModule bean是配置由@JsonComponent定义的序列化和反序列化组件的关键角色。摘取源码中对@JsonComponent的注释(Redis章节有实际应用):

/**
 * {@link Component} that provides {@link JsonSerializer} and/or {@link JsonDeserializer}
 * implementations to be registered with Jackson when {@link JsonComponentModule} is in
 * use. Can be used to annotate {@link JsonSerializer} or {@link JsonDeserializer}
 * implementations directly or a class that contains them as inner-classes. For example:
 * 
 * 用法举例:
 * @JsonComponent
 * public class CustomerJsonComponent {
 *
 *     public static class SomeDTOSerializer extends JsonSerializer<Customer> {
 *         // ...
 *     }
 *
 *     public static class SomeDTODeserializer extends JsonDeserializer<Customer> {
 *         // ...
 *     }
 *
 * }
 *
 */

JacksonHttpMessageConvertersConfiguration配置类可以提供两个bean,一个为MappingJackson2HttpMessageConverter,另一个为MappingJackson2XmlHttpMessageConverter 。前者解析媒体类型为application/json的数据,后者解析application/xml的数据。

MappingJackson2HttpMessageConverter实现了关键接口GenericHttpMessageConverter 用于将http请求转换为指定泛型类型的目标对象,将指定泛型类型的源对象转换为http响应。

总结,在Springboot启动后,就已经自动配置了一系列的bean来支持Json和Java对象的转化,如果追求性能,完全不用自己再new一个ObjectMapper甚至多个ObjectMapper,ObjectMapper是线程安全的,可以放心大胆的用单例模式。同时由于注解@JsonComponent 和bean JsonComponentModule ,可以非常方便的自定义某些类型的序列化与反序列化方法。所以一个项目里面有多个ObjectMapper是无意义的。一个项目里有一个json实现库就够了,而且Springboot完全可以针对不同的json库进行自动配置,比如gson。从微服务依赖的角度以及序列化反序列化动作一致性的角度来讲,最好不要引入多个ObjectMapper或者多个json库。

附:如何用Springboot的自己创建的ObjectMapper

@Component
public class SpringUtil implements BeanFactoryAware {

    private static BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        SpringUtil.beanFactory = beanFactory;
    }

    public static Object getBean(String beanName){
        return beanFactory.getBean(beanName);
    }

    public static Object getBean(Class<?> clazz) {
        return beanFactory.getBean(clazz);
    }
}
@Slf4j
public class JacksonUtil {
    private static final ObjectMapper objectMapper;

    static {
        objectMapper = SpringUtil.getBean(ObjectMapper.class);
    }

    public static String toJson(Object obj) {
        Assert.notNull(obj, "Object must not be null");
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.error("convert to string failed, {}", e.getStackTrace());
            return null;
        }
    }

    public static <T> T fromJson(String json, Class<T> clazz) {
        Assert.notNull(json, "json string must not be null");
        Assert.notNull(clazz, "Class Type must not be null");

        try {
            return objectMapper.readValue(json, clazz);
        } catch (Exception e) {
            log.error("convert to obj failed, {}", e.getStackTrace());
            return null;
        }
    }
}

当然不用静态方法的形式暴露功能,而直接在bean里autowired也是可以滴。

References:

  1. https://www.baeldung.com/spring-boot-jsoncomponent

Mongo

有两种Mongo自动配置类,一种是连接真实的mongo配置,一种是内存式的mongo配置。配置顺序为先配置内存式mongo(如果条件满足),再配置真实mongo。

  1. 内存式mongo自动配置依赖:

    <!-- https://mvnrepository.com/artifact/de.flapdoodle.embed/de.flapdoodle.embed.mongo -->
    <dependency>
        <groupId>de.flapdoodle.embed</groupId>
        <artifactId>de.flapdoodle.embed.mongo</artifactId>
        <version>x.x.x</version>
        <scope>test</scope>
    </dependency>
       
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-mongodb -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
        <version>x.x.x.RELEASE</version>
    </dependency>
    
  2. 真实mongo配置可以直接依赖:

    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-mongodb -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
        <version>x.x.x.RELEASE</version>
    </dependency>
    

所以,当要使用mongodb时,引入spring-boot-starter-data-mongodb即可,如果需要用到内存式mongo,则在此基础上再引入de.flapdoodle.embed.mongo即可。AutoConfiguration就是这么叼,他能根据你引入的包来自动配置对应的bean。

首先,内存式mongo的自动配置类条件:

@Configuration
@EnableConfigurationProperties({ MongoProperties.class, EmbeddedMongoProperties.class })
@AutoConfigureBefore(MongoAutoConfiguration.class)
@ConditionalOnClass({ MongoClient.class, MongodStarter.class })
public class EmbeddedMongoAutoConfiguration {}

该配置早于MongoAutoConfiguration配置:

@Configuration
@ConditionalOnClass(MongoClient.class)
@EnableConfigurationProperties(MongoProperties.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory")
public class MongoAutoConfiguration {}

MongoAutoConfiguration配置仅当容器内缺少MongoDbFactory bean时才会生效。

最后,MongoDataAutoConfiguration会完成Spring Data for Mongo的支持,这样,在使用的时候直接注入对应的bean即可。该配置提供MongoTemplate, GridFsTemplate 用于存储小文档和大文档。

对Mongo配置项感兴趣的话,可以直接查看MongoProperties配置类

至此,如果没有错误抛出,就可以正常使用mongo了~

Quartz [to do]

定时任务调度,起步依赖:

Kafka

Spring提供的对Kafka客户端的封装,起步依赖:

<dependency>
      <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

自动配置相关类

  • KafkaAutoConfiguration
@Configuration
@ConditionalOnClass(KafkaTemplate.class)
@EnableConfigurationProperties(KafkaProperties.class)
@Import({ KafkaAnnotationDrivenConfiguration.class, KafkaStreamsAnnotationDrivenConfiguration.class })
public class KafkaAutoConfiguration {}

KafkaTemplate由spring-kafka包引入。同时完成自动配置后,还会引入KafkaAnnotationDrivenConfigurationKafkaStreamsAnnotationDrivenConfiguration 两个配置,前者主要用来支持spring-kafka提供的相关注解,后者和Kafka Stream相关,不做讨论,实际上起步依赖中也不会包含Kafka-Stream相关的东西,所以后者并不会生效。

自动配置提供的最重要的bean就是KafkaTemplate了,通过该类提供的方法,可以完成kafka消息的发送。

同时spring-kafka-test提供了内存式的kafka来进行测试,只需要引入依赖:

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka-test</artifactId>
</dependency>
  • 修改配置文件:
spring:  
  kafka:
    bootstrap-servers: ${spring.embedded.kafka.brokers}
    consumer:
      group-id: honeyroom
  • 测试示例(消费者):
@Slf4j
@Component
public class DemoConsumer {

    @KafkaListener(topics = "test-consumer")
    public void handle(String message) {
        log.info("receive message: {}", message);
        System.out.println("========== kafka message ===========");
        System.out.println(message);
    }
}

其中,DemoMessage结构为:

@Data
public class DemoMessage {
    private String key;
    private String value;
    private DateTime timestamp; //joda time
}
  • 测试示例(生产者):
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class KafkaTests {
    @ClassRule
    public static EmbeddedKafkaRule embeddedKafka = new EmbeddedKafkaRule(1, false, 5).kafkaPorts(9092);

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void testKafka() throws Exception {
        DemoMessage message = new DemoMessage();
        message.setKey("key");
        message.setValue("value");
        message.setTimestamp(DateTime.now());
        kafkaTemplate.send("test.consumer", objectMapper.writeValueAsString(message));
        Thread.sleep(1000000);
    }
}

通过在本地执行脚本监听,可以看到测试发出的kafka消息。

./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test.consumer --from-beginning

kafka shell文件在Kafka binary package 的bin目录下。

Redis

spring提供的Redis客户端的封装,起步依赖:

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

自动配置类RedisAutoConfiguration:

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {}

熟悉的配方,熟悉的套路,引入起步依赖后能满足@ConditionalOnClass(RedisOperations.class)条件,然后配置文件获取RedisProperties.class的配置,最后再导入LettuceConnectionConfiguration.class 或者 JedisConnectionConfiguration.class,这俩都是一个功能,就是提供RedisTemplate所依赖的bean,比如 RedisConnectionFactory.class,默认LettuceConnectionConfiguration.class生效。

运行基于Docker的Redis

#1. 获取镜像
docker pull redis

#2. 运行镜像
docker run -p 6379:6379 -d redis:latest redis-server --requirepass "123456"

#3. 进入容器内的Redis命令行界面
docker exec -ti 1729cb4a78c4 redis-cli

#4. 进入后进行鉴权
auth 123456
  • -p 映射容器端口到主机端口
  • -d 后台运行容器并打印容器ID
  • redis:latest docker 镜像
  • redis-server --requirepass "123456" 容器内命令

自动配置生成的StringRedisTemplate 。慎用,如果项目偏向于用json,而且项目用到了spring mvc,可以使用如下方法配置:

  • Jackson中,对于joda time的序列化和反序列化自定义方法

    @JsonComponent
    public class DateTimeComponent {
        public static final String DEFAULT_DATETIME_FORMATTER = "yyyy-MM-dd'T'HH:mm:ss'Z'";
      
        public static class SomeDTOSerializer extends JsonSerializer<DateTime> {
            @Override
            public void serialize(DateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                gen.writeString(value.toString(DEFAULT_DATETIME_FORMATTER));
            }
        }
      
        public static class SomeDTODeserializer extends JsonDeserializer<DateTime> {
            @Override
            public DateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
                return DateTime.parse(p.getValueAsString(), DateTimeFormat.forPattern(DEFAULT_DATETIME_FORMATTER));
            }
        }
    }
    

    请注意,此时实际上是对spring mvc中的Jackson做了配置,如需运用在Redis中,还需要以下配置:

  • RedisConfig

    @Configuration
    @AllArgsConstructor
    public class RedisConfig {
        private ObjectMapper objectMapper; //类构造器注入
      
        @Autowired
        public void jsonRedisTemplate(RedisTemplate redisTemplate) {
            redisTemplate.setKeySerializer(RedisSerializer.string());
            // 使用和MVC的Jackson同一个转换器,mvc的jackson转换器比较方便定义特殊类型的转换,比如joda time
            redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
            redisTemplate.setHashKeySerializer(RedisSerializer.string());
            redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
        }
    }
    
  • 和其他template一样,使用上相当简单:

@Slf4j
@Service
@AllArgsConstructor
public class RedisService {
    private RedisTemplate<Object, Object> redisTemplate;
    private ObjectMapper objectMapper;
    private static final int DEFAULT_EXPIRE_INTERVAL = 300;
    private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;

    public boolean save(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            redisTemplate.expire(key, DEFAULT_EXPIRE_INTERVAL, DEFAULT_TIME_UNIT);
            return true;
        } catch (Exception e) {
            log.error("can't cache data, {}", e.getMessage());
            return false;
        }
    }

    public <T> T get(String key, Class<T> clazz) {
        try {
            Object value = redisTemplate.opsForValue().get(key);
            return objectMapper.convertValue(value, clazz);
        } catch (Exception e) {
            log.error("can't get cached data, {}", e.getMessage());
            return null;
        }
    }
}

写个LLT调用一下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RedisTest {
    @Autowired
    RedisService redisService;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void testSave() throws Exception {
        DemoMessage message = new DemoMessage();
        message.setKey("key");
        message.setValue("value");
        message.setTimestamp(DateTime.now());
        redisService.save("test", message);
    }

    @Test
    public void testGet() throws Exception {
        DemoMessage demoMessage = redisService.get("test", DemoMessage.class);
        System.out.println("====demomessage===: {}" + demoMessage.toString());
        Assert.notNull(demoMessage);
        Assert.isTrue("key".equals(demoMessage.getKey()));
        Assert.isTrue("value".equals(demoMessage.getValue()));
        Assert.notNull(demoMessage.getTimestamp());
    }
}

WHY ?

  • 为什么要用@JsonComponent注解,为什么要用和http消息解析相同的ObjectMapper,为什么建议整个项目用同一个ObjectMapper

    个人觉得,这是减少冗余代码的最佳方案,使用springboot提供的注解自定义序列化反序列化方法,能将代码量降到最低,让开发复杂度降到最低。在Redis和Http中使用同一个ObjectMapper,能够统一项目内部与项目外部之间的数据转换行为,同时还能将代码量降到最低。当然如果有特殊情况(比如我需要Http的时候DateTime转换成字符串,而存Redis的时候转换成时间戳),当然可以不使用同一个ObjectMapper

    如果一个项目中充斥着new ObjectMapper(),绝对是灾难,因为你无法为特殊类型统一行为,一旦需要自定义特殊类型的转换,效率将极其低下,何不将这些时间花在优化结构上,而不是去做那些毫无意义的重复劳动。

  • 为什么要做一个中间层RedisService,而不是直接在业务中@Autowired RedisTemplate

    好处显而易见,就犹如web开发定义的Controller层,Service层,DAO层类似,在Controller层定义接口,数据校验等;在Service层做事务处理等,在DAO层访问数据。RedisService的作用类似,“承上启下”-对上提供面向业务的接口,对下来处理一些通用的公共的“琐事”,比如设置过期时间。通过这种分层,能让代码逻辑更加清晰,可读性,可维护性都变得更好。编程是一项社会活动,写出的代码就如同你说出来的话一样,劝你好好说话。

    Redis一定要设置过期时间,否则迟早出事故。

Spring Integration

概览