前提

前面的几篇文章已经基本介绍完了JSR-310日期时间类库的基本使用,这篇文章主要介绍在主流的框架中如何使用这些类库。因为涉及到数据库操作,先准备好一张表和对应的实体。

CREATE TABLE `t_user`(
  id BIGINT PRIMARY KEY COMMENT '主键',
  username VARCHAR(10) COMMENT '姓名',
  birthday DATE COMMENT '生日',
  create_time DATETIME COMMENT '创建时间',
  KEY idx_name(`username`),
  KEY idx_create_time(`create_time`)
)COMMENT '用户表';
@Data
public class User{

   private Long id;
   private String name;
   private LocalDate birthday;
   private OffsetDateTime createTime;
}

这里如果不考虑时区的影响,createTime也可以使用LocalDateTime类型。另外,为了连接测试数据库,这里引入’光’连接池的依赖:

<dependency>
	<groupId>com.zaxxer</groupId>
	<artifactId>HikariCP</artifactId>
	<version>3.2.0</version>
</dependency>

JDBC中使用JSR-310日期时间类库

说实话,由于JDBC类库在方法参数或者返回值类型很久没更新,对于带日期时间的属性,统一使用java.sql.Timestamp类型,对于日期类型的属性则统一使用java.sql.Date,因此需要进行类型转换。代码如下:

public class JdbcSample {

	public static void main(String[] args) throws Exception {
		HikariConfig config = new HikariConfig();
		config.setMaximumPoolSize(10);
		config.setJdbcUrl("jdbc:mysql://localhost:3306/test?useSSL=false&characterEncoding=utf8");
		config.setUsername("root");
		config.setPassword("root");
		config.setDriverClassName("com.mysql.jdbc.Driver");
		DataSource dataSource = new HikariDataSource(config);
		Connection connection = dataSource.getConnection();
		connection.setAutoCommit(false);
		PreparedStatement p = connection.prepareStatement("INSERT INTO t_user(id,username,birthday,create_time) VALUES (?,?,?,?)");
		p.setLong(1, 1L);
		p.setString(2, "Throwable");
		p.setDate(3, Date.valueOf(LocalDate.of(1993, 3, 10)));
		p.setTimestamp(4, Timestamp.from(OffsetDateTime.now().toInstant()));
		//LocalDateTime -> p.setTimestamp(4, Timestamp.from(LocalDateTime.now()));
		int updateCount = p.executeUpdate();
		connection.commit();
		System.out.println(String.format("更新数据%d条", updateCount));
		p = connection.prepareStatement("SELECT * FROM t_user WHERE id = ?");
		p.setLong(1, 1L);
		ResultSet resultSet = p.executeQuery();
		User user = null;
		if (resultSet.next()) {
			user = new User();
			user.setId(resultSet.getLong("id"));
			user.setName(resultSet.getString("username"));
			user.setBirthday(resultSet.getDate("birthday").toLocalDate());
			user.setCreateTime(OffsetDateTime.ofInstant(resultSet.getTimestamp("create_time").toInstant(), ZoneId.systemDefault()));
			//LocalDateTime -> user.setCreateTime(resultSet.getTimestamp("create_time").toLocalDateTime());
		}
		System.out.println(user);
	}
}
// 输出结果
更新数据1条
User(id=1, name=Throwable, birthday=1993-03-10, createTime=2019-01-06T23:09:01+08:00)

除了需要做少量类型转换,没有其他的兼容性问题。

Mybatis中使用JSR-310日期时间类库

既然JDBC已经可以使用JSR-310的日期时间类库,那么基于JDBC封装的ORM框架必定也可以支持。除了需要引入Mybatis本身的依赖,还需要引入mybatis-typehandlers-jsr310依赖(这里注意一点,Mybatis某个版本之后已经内置了mybatis-typehandlers-jsr310的所有依赖类,所以不需要额外引入):

<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis</artifactId>
	<version>3.4.6</version>
</dependency>
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis-typehandlers-jsr310</artifactId>
	<version>1.0.2</version>
</dependency>

新建一个Mapper接口类UserMapper

public interface UserMapper {

	@Insert("INSERT INTO t_user(id,username,birthday,create_time) VALUES (#{id},#{name},#{birthday},#{createTime})")
	int insert(User user);

	@Select("SELECT id,username as name,birthday,create_time as createTime FROM t_user WHERE id = #{id}")
	User selectById(Long id);
}

核心代码如下:

public class MybatisSample {

	public static void main(String[] args) throws Exception {
		HikariConfig config = new HikariConfig();
		config.setMaximumPoolSize(10);
		config.setJdbcUrl("jdbc:mysql://localhost:3306/test?useSSL=false&characterEncoding=utf8");
		config.setUsername("root");
		config.setPassword("root");
		config.setDriverClassName("com.mysql.jdbc.Driver");
		DataSource dataSource = new HikariDataSource(config);
		TransactionFactory transactionFactory = new JdbcTransactionFactory();
		Environment environment = new Environment("development", transactionFactory, dataSource);
		Configuration configuration = new Configuration(environment);
		configuration.addMapper(UserMapper.class);
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
		SqlSession sqlSession = sqlSessionFactory.openSession();
		UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
		User user = new User();
		user.setId(1L);
		user.setCreateTime(OffsetDateTime.now());
		user.setBirthday(LocalDate.of(1993, 3, 10));
		user.setName("Throwable");
		int updateCount = userMapper.insert(user);
		System.out.println(String.format("更新数据%d条", updateCount));
		sqlSession.commit();
		User result = userMapper.selectById(1L);
		System.out.println(result);
		sqlSession.close();
	}
}
// 输出结果
更新数据1条
User(id=1, name=Throwable, birthday=1993-03-10, createTime=2019-01-06T23:30:09+08:00)

虽然多引入了一个依赖,但是使用起来十分简单,甚至可以做到开发态无感知,Mybatis这一点做得比较完善。

Jackson中使用JSR-310日期时间类库

Jackson从2.x某个版本中,官方就基于JDK8的新特性开发了第三方类库jackson-modules-java8,这个第三方类库包括三个模块jackson-module-parameter-namesjackson-datatype-jdk8jackson-datatype-jsr310,这三个模块是独立打包的,可以按需引入。这里做的实例需要引入下面的依赖:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.8</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.8</version>
</dependency>

在官方文档中已经很详细给出具体的使用例子,这里也简单做一个例子:

public class JacksonMain {

	private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
	private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	public static void main(String[] args) throws Exception {
		ObjectMapper objectMapper = new ObjectMapper();
		JavaTimeModule javaTimeModule = new JavaTimeModule();
		javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DATE_FORMATTER));
		javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
		javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
		javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
		objectMapper.registerModule(javaTimeModule);
		Sample sample = new Sample();
		sample.setLocalDate(LocalDate.now());
		sample.setLocalDateTime(LocalDateTime.now());
		System.out.println(objectMapper.writeValueAsString(sample));
	}

	@Data
	public static class Sample {

		private LocalDate localDate;
		private LocalDateTime localDateTime;
	}
}
// 某个时刻输出如下
{"localDate":"2019-01-07","localDateTime":"2019-01-07 23:40:12"}

ObjectMapper实例中可以注册自定义的JavaTimeModule模块,JavaTimeModule模块中已经存在了不少默认的日期时间类的序列化和反序列化器,必要时可以像上面的例子一样重写对应的日期时间类型的序列化和反序列化器并且覆盖已经配置的默认实现,这样子就能实现我们想要的格式化输出。JavaTimeModule默认的序列化和反序列化器配置如下:

    public JavaTimeModule() {
        super(PackageVersion.VERSION);
        this.addDeserializer(Instant.class, InstantDeserializer.INSTANT);
        this.addDeserializer(OffsetDateTime.class, InstantDeserializer.OFFSET_DATE_TIME);
        this.addDeserializer(ZonedDateTime.class, InstantDeserializer.ZONED_DATE_TIME);
        this.addDeserializer(Duration.class, DurationDeserializer.INSTANCE);
        this.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
        this.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE);
        this.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE);
        this.addDeserializer(MonthDay.class, MonthDayDeserializer.INSTANCE);
        this.addDeserializer(OffsetTime.class, OffsetTimeDeserializer.INSTANCE);
        this.addDeserializer(Period.class, JSR310StringParsableDeserializer.PERIOD);
        this.addDeserializer(Year.class, YearDeserializer.INSTANCE);
        this.addDeserializer(YearMonth.class, YearMonthDeserializer.INSTANCE);
        this.addDeserializer(ZoneId.class, JSR310StringParsableDeserializer.ZONE_ID);
        this.addDeserializer(ZoneOffset.class, JSR310StringParsableDeserializer.ZONE_OFFSET);
        this.addSerializer(Duration.class, DurationSerializer.INSTANCE);
        this.addSerializer(Instant.class, InstantSerializer.INSTANCE);
        this.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE);
        this.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE);
        this.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE);
        this.addSerializer(MonthDay.class, MonthDaySerializer.INSTANCE);
        this.addSerializer(OffsetDateTime.class, OffsetDateTimeSerializer.INSTANCE);
        this.addSerializer(OffsetTime.class, OffsetTimeSerializer.INSTANCE);
        this.addSerializer(Period.class, new ToStringSerializer(Period.class));
        this.addSerializer(Year.class, YearSerializer.INSTANCE);
        this.addSerializer(YearMonth.class, YearMonthSerializer.INSTANCE);
        this.addSerializer(ZonedDateTime.class, ZonedDateTimeSerializer.INSTANCE);
        this.addSerializer(ZoneId.class, new ToStringSerializer(ZoneId.class));
        this.addSerializer(ZoneOffset.class, new ToStringSerializer(ZoneOffset.class));
        this.addKeySerializer(ZonedDateTime.class, ZonedDateTimeKeySerializer.INSTANCE);
        this.addKeyDeserializer(Duration.class, DurationKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(Instant.class, InstantKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(LocalDateTime.class, LocalDateTimeKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(LocalDate.class, LocalDateKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(LocalTime.class, LocalTimeKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(MonthDay.class, MonthDayKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(OffsetDateTime.class, OffsetDateTimeKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(OffsetTime.class, OffsetTimeKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(Period.class, PeriodKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(Year.class, YearKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(YearMonth.class, YearMothKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(ZonedDateTime.class, ZonedDateTimeKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(ZoneId.class, ZoneIdKeyDeserializer.INSTANCE);
        this.addKeyDeserializer(ZoneOffset.class, ZoneOffsetKeyDeserializer.INSTANCE);
    }

如果熟练掌握Jackson的解析原理和源码,可以尝试继承JSR310FormattedSerializerBase或者JSR310DateTimeDeserializerBase实现自定义序列化或反序列化器,从更底层控制日期时间类的序列化和反序列化。

SpringMVC中使用JSR-310日期时间类库

SpringMVC中默认的HTTP消息转换器就是使用Jackson实现的,前面已经提到了Jackson可以完美支持JSR-310,那么SpringMVC也必定可以支持,只需要对ObjectMapper做一些额外的配置即可。这里简单以SpringBoot应用做示例,引入依赖:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.8</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.8</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

由于org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration中会通过Jackson2ObjectMapperBuilder去构造内部使用的ObjectMapper,我们只需要提供一个自定义的Jackson2ObjectMapperBuilder类型的Bean即可。

// 配置类
@Configuration
public class JacksonBuilderAutoConfiguration {

	private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
	private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	@Bean
	public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
		Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
		JavaTimeModule javaTimeModule = new JavaTimeModule();
		javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DATE_FORMATTER));
		javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
		javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
		javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
		builder.modules(javaTimeModule);
		return builder;
	}
}

// 启动类
@SpringBootApplication
public class Application implements CommandLineRunner {

	@Autowired
	private ObjectMapper objectMapper;

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

	@Override
	public void run(String... args) throws Exception {
		Sample sample = new Sample();
		sample.setLocalDate(LocalDate.now());
		sample.setLocalDateTime(LocalDateTime.now());
		System.out.println(objectMapper.writeValueAsString(sample));
	}

	@Data
	public static class Sample {

		private LocalDate localDate;
		private LocalDateTime localDateTime;
	}
}

// 运行启动类,某个时刻输出如下
{"localDate":"2019-01-07","localDateTime":"2019-01-07 23:58:08"}

这里只要保证SpringMVC内部使用的ObjectMapper类型的BeanJSR-310日期时间类型的序列化和反序列化生效即可,因为默认配置的MappingJackson2HttpMessageConverterHTTP消息转换器就是使用内置的ObjectMapper类型的BeanJSON的序列化和反序列化。

小结

实战层面来看,使用的框架都是基于JDK类库的实现,只要JDK类库的功能可以实现,那么在应用的时候要有信心主流的框架一定会支持对应的特性。

参考资料:

(本文完 e-a-201818 c-2-d)