想必同学们在开发当中一定涉及到金钱的属性,一旦涉及到钱就必须要保证不失精度,无论怎么转换一分钱也不能差,如果因为代码的疏忽,金额因为四舍五入或者类型转换时差 1 分的话,想象一下 10000 个订单至少差距 100 块钱,这个损失客户和企业都承担不起。所以今天就来给大家介绍一个工具 Joda,joda有很多种,比如Joda-Money,Jode-Time。今天就来介绍一下SpringBoot中使用Joda-Money来帮助我们解决金额转化问题。
官方文档:https://www.joda.org/joda-money/
使用须知
本文的目的不是让大家以后碰到金钱属性的时候使用Joda-Money,而是简单介绍一下在springboot项目中怎么使用而已。我也从朋友那了解到,做金融行业的跟金钱打交道很多,尤其在换算利率的时候必须保证不失精度,通常最好的做法就是数据库设计成bigint类型,单位是分,入库扩大 100 倍 ,出库缩小 100 倍,如果想要更精确就改成 10000倍。当然我也有一些关于钱的转换的一些工具包,如果大家感兴趣可以加微信找我要,这里就不介绍了。
Demo使用的是springboot、mybatis、h2、HikariPC、lombok
下面直接贴代码
首先我们想使用Joda-Money,就要有一个依赖joda,目前官网只有一个版本,我会把pom所用到的依赖都贴出来,方便大家看。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
接下来创建一个商品的model。
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Goods {
private Long id;
private String name;
private Money price;
private Date createTime;
private Date updateTime;
}
接下里编写mapper,
目前mybatis都使用注解都方式了,我也喜欢注解都方式,因为我看一个个的xml里的标签有点晕。
如果有哪里的写法不明白请在下方联系作者。
@Mapper
public interface GoodsMapper {
@Insert("insert into goods (name, price, create_time, update_time)"
+ "values (#{name}, #{price}, now(), now())")
@Options(useGeneratedKeys = true)
Long save(Goods goods);
@Select("select * from goods where id = #{id}")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "create_time", property = "createTime"),
// map-underscore-to-camel-case = true 可以实现一样的效果
// @Result(column = "update_time", property = "updateTime"),
})
Goods findById(@Param("id") Long id);
}
下面就到了最重要的核心类了,金额的转换类。
这里有必要简单解释一下BaseTypeHandler
这个类是MyBatis为我们提供来应对Java和jdbc字段类型不匹配的情况。也就是说你model声明的字段和数据库不匹配时需要在这个环节处理,在我们启用了我们自定义的这个TypeHandler之后,数据的读写都会被这个类所过滤。写入通过setNonNullParameter方法过滤,读通过getNullableResult方法过滤,parseMoney方法读作用是读取金额从分转成元。
/**
* 在 Money 与 Long 之间转换的 TypeHandler,处理 CNY 人民币
*/
public class MoneyTypeHandler extends BaseTypeHandler<Money> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Money parameter, JdbcType jdbcType) throws SQLException {
System.out.println("setNonNullParameter======"+ps+"--"+i+"--"+parameter+"--"+jdbcType);
ps.setLong(i, parameter.getAmountMinorLong());
}
@Override
public Money getNullableResult(ResultSet rs, String columnName) throws SQLException {
System.out.println("getNullableResult======"+rs.toString()+"--"+columnName);
return parseMoney(rs.getLong(columnName));
}
@Override
public Money getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseMoney(rs.getLong(columnIndex));
}
@Override
public Money getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseMoney(cs.getLong(columnIndex));
}
private Money parseMoney(Long value) {
System.out.println("value===="+value);
return Money.of(CurrencyUnit.of("CNY"), value / 100.0);
}
}
然后我们编写启动类
小小说明
@SpringBootApplication
@MapperScan("cn.mapper")
@Slf4j
public class MoneyDemoApplication implements ApplicationRunner {
@Autowired
private GoodsMapper goodsMapper;
public static void main(String[] args) {
SpringApplication.run(MoneyDemoApplication.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
Goods goods = Goods.builder().name("白桃薯条").price(Money.of(CurrencyUnit.of("CNY"), 20)).build();
Long id = goodsMapper.save(goods);
log.info("Goods {} => {}", id, goods);
goods = goodsMapper.findById(id);
log.info("Goods {}", goods);
}
}
因为写demo我就没有去创建实体数据库,我采用的java关系型数据库h2,所以会多出来一个创建表的配置类
create table goods (
id bigint not null auto_increment,
name varchar(255),
price bigint not null,
create_time timestamp,
update_time timestamp,
primary key (id)
);
还有一个配置类
mybatis.type-handlers-package=cn.handler
mybatis.configuration.map-underscore-to-camel-case=true
项目结构如下
一切准备就绪就让我们启动项目吧
控制台信息如下,截图比较小,我上面在typeHandler中输出里value的值是2000,但是通过我的joda 和typehandler的结合可以方便的对金额进行转换了。
截图中我们可以看得出来,springboot2.0后默认使用的是HikariPC的数据库连接池,据说比druid性能更好,感兴趣的可以了解下。