Spring Boot 通过 Mvc 扩展方便进行货币单位转换的代码详解
作者:carl-zhao
由于公司是支付平台,所以很多项目都涉及到金额,业务方转递过来的金额是单位是元,而我们数据库保存的金额单位是分。一般金额的流向有以下几个方向:
- 外部业务方请求我们服务,传递过来的金额单位是元,需要把元转换成分。比如:下单接口。
- 内部系统之间的流转,不管是向下传递还是向上传递系统间的流程都是分,不需要扭转。比如:调用支付引擎(向下传递),支付引擎回调收单业务(向上传递)。
- 向业务方返回数据,这个时候需要把分转换成元。比如:商户调用查询订单接口。
- 内部系统的展示,这个时候需要把分转换成元。比如:显示收入金额的报表。
如果我们对于请求参数是金额类型的参数逐一处理,这样重复的操作就会显得相当的不优雅。对于请求参数我们可以使用 Spring MVC 提供的扩展扩展。对于金额操作我们可以分为:
- 业务方传入金额单位为元,需要把业务方传入的元转换成分,可以使用 Spring MVC Restful 请求参数扩展
RequestBodyAdvice
接口。 - 业务方需要查询数据,需要把数据库保存的分转换成元,可以使用 Spring MVC Restful 响应参数扩展
ResponseBodyAdvice
接口。
下面我们就来看一下代码实现。
1、FenToYuan.java
定义一个标注注解,用于标注到需要把元转换成分的 BigDecimal
类型的参数上面。
FenToYuan.java
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FenToYuan { }
2、YuanToFenRequestBodyAdvice.java
实现 Spring MVC Restful 请求参数扩展类,如果请求参数标注了 @RequestBody
注解,并且请求参数的字段类型为 BigDecimal
就会把传入的参数由元转换成分。
YuanToFenRequestBodyAdvice.java
@Slf4j @ControllerAdvice public class YuanToFenRequestBodyAdvice extends RequestBodyAdviceAdapter { @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return methodParameter.hasParameterAnnotation(RequestBody.class); } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { if(body == null) { return null; } Class<?> clazz = body.getClass(); PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(clazz); for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { String name = propertyDescriptor.getName(); if("class".equals(name)){ continue; } Field field = ReflectionUtils.findField(clazz, name); Class<?> fieldClazz = field.getType(); if(!fieldClazz.equals(BigDecimal.class) ){ continue; } if(!field.isAnnotationPresent(YuanToFen.class)) { continue; } Method readMethod = propertyDescriptor.getReadMethod(); Method writeMethod = propertyDescriptor.getWriteMethod(); try { BigDecimal yuanAmount = (BigDecimal) readMethod.invoke(body); BigDecimal fenAmount = AmountUtils.yuan2Fen(yuanAmount); writeMethod.invoke(body, fenAmount); } catch (Exception e) { log.error("amount convert yuan to fen fail", e); } } return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType); } }
3、YuanToFen.java
标注注解,当响应参数需要由分转换成元的时候,就标注这个注解。响应值就会把数据库或者下游传递过来的金额为分的参数转换成元。
YuanToFen.java
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface YuanToFen { }
4、FenToYuanResponseBodyAdvice.java
当 Spring MVC 方法上标注了 ResponseBody
或者类上标注了 RestController
注解时,如果响应对象的 BigDecimal
标注了 @YuanToFen
注解就会进行金额分转换成元。
FenToYuanResponseBodyAdvice.java
@Slf4j @ControllerAdvice public class FenToYuanResponseBodyAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { return returnType.hasParameterAnnotation(ResponseBody.class) || returnType.getDeclaringClass().isAnnotationPresent(RestController.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if(body == null) { return null; } Class<?> clazz = body.getClass(); PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(clazz); for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { String name = propertyDescriptor.getName(); if("class".equals(name)){ continue; } Field field = ReflectionUtils.findField(clazz, name); Class<?> fieldClazz = field.getType(); if(!fieldClazz.equals(BigDecimal.class) ){ continue; } if(!field.isAnnotationPresent(FenToYuan.class)) { continue; } Method readMethod = propertyDescriptor.getReadMethod(); Method writeMethod = propertyDescriptor.getWriteMethod(); try { BigDecimal fenAmount = (BigDecimal) readMethod.invoke(body); BigDecimal yuanAmount = AmountUtils.fen2yuan(fenAmount); writeMethod.invoke(body, yuanAmount); } catch (Exception e) { log.error("amount convert fen to yuan fail", e); } } return body; } }
5、AmountUtils.java
金钱工具类,提供了金钱的元转分以及分转元这两个功能。
AmountUtils.java
public abstract class AmountUtils { /** * 金额单位元转分 */ public static BigDecimal yuan2Fen(BigDecimal amount) { if (amount == null) { return BigDecimal.ZERO; } return amount.movePointRight(2).setScale(0, BigDecimal.ROUND_DOWN); } /** * 金额单位分转元 */ public static BigDecimal fen2yuan(BigDecimal amount) { return null2Zero(amount).movePointLeft(2).setScale(2, BigDecimal.ROUND_HALF_UP); } /** * 把 null 当作 0 处理 */ public static BigDecimal null2Zero(Number amount) { if (amount == null) { return BigDecimal.ZERO; } if (amount instanceof BigDecimal) { return (BigDecimal) amount; } else { return new BigDecimal(amount.toString()); } } }
6、Order.java
实体类,用于接收请求对象以及响应测试金额转换。
Order.java
@Data public class Order { private String orderId; private String productName; @FenToYuan @YuanToFen private BigDecimal orderAmount; }
7、OrderController.java
订单控制类,提供了两个方法:订单创建(/order/apply)标注了 @RequestBody
,会把传入的金额由元转换成分,然后打印到控制台。订单查询(order/query) 声明方法的类上标注了 @RestController
,通过关键字 new
创建一个订单金额为 1000
分的订单。
OrderController.java
@RestController @RequestMapping("order") public class OrderController { @RequestMapping("apply") public void apply(@RequestBody Order order) { System.out.println(JSON.toJSONString(order)); } @RequestMapping("query/{id}") public Order query(@PathVariable String id) { Order order = new Order(); order.setOrderId(id); order.setOrderAmount(new BigDecimal("1000")); order.setProductName("test"); return order; } }
8、测试
使用工具 Postman 发送 http 进行功能测试。
8.1 元转分测试
通过 postman 请求 http:localhost:8080/order/apply
发送以下请求:
控制台打印如下:
业务方传入金额为 1 元,控制台打印的结果是 100 分。
8.2 测试分转元
通过 postman 请求 http:localhost:8080/order/query/1
发送以下请求:
这个时候得到订单金额为 10 元。查询订单的逻辑如下:
这个时候订单的金额是 1000 分,转换成 10 元完成了我们的目标功能。
当然这种方式是有一个缺陷的,就是它不能递归的进行金额转换,后面可以借鉴 Hibernate 的递归校验逻辑来进行递归金额参数的转换。
到此这篇关于Spring Boot 通过 Mvc 扩展方便进行货币单位转换的文章就介绍到这了,更多相关Spring Boot货币单位转换内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!