各位好,本文分享一个工作中遇到的一个关于金额转换的需求的代码实践。
1-系统背景
基于springboot框架,PRC框架使用open feign;Java应用通过springmvc接口提供基于feign的访问服务,同时服务于其他spring应用和前端nodejs应用;
2-业务背景
系统之间金额的交互统一使用分,系统提供给前端或者前端提交给后端的金额数据还是元。
3-设计分析
这个问题的主要解决思路,是基于同一个Java接口提供两种不同的金额转换场景,如下
1-当Java应用访问时,不对金额进行转换,所有Java应用同样的技术标准,同样的金额字段类型定义(这里我们要求使用Long类型定义金额字段,默认保留两位到分)
2-当前端nodejs应用访问时,需要对金额进行转换,将分转换为元;
4-代码实践
主要是通过请求头来区分请求来源然后针对不同来源的请求做不同的转换处理,属于Java应用发出的请求时不做处理,属于前端nodejs请求时按照金额的标识进行处理,金额标识使用Java注解完成,在注解上可以标识金额的类型,转换单位和转换方向等,然后通过spring对请求和响应的切面对请求和响应的数据体进行金额字段的转换。
1-在Java请求头中添加应用标识,基于feign配置如下:
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* 类FeignConfiguration.java的实现描述:FeignConfiguration feign统一配置拦截器
*
*/
@Configuration
public class FeignConfiguration implements RequestInterceptor {
@Autowired
private Environment environment;
/**
** 统一配置
*
* @see feign.RequestInterceptor#apply(feign.RequestTemplate)
*/
@Override
public void apply(RequestTemplate template) {
//请求头设置spring应用名称
template.header("spring.application.name",
environment.getProperty("spring.application.name"));
}
}
2-增加货币注解Money类,同时增加金额可重复注解MoneyRepeatable。
Money代码如下:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import *.MoneyConvertDirection;
import *.MoneyUnit;
/**
* 类Money.java的实现描述:货币
*
*/
@Repeatable(MoneyRepeatable.class)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface Money {
/**
* 当前金额单位
*/
MoneyUnit unit() default MoneyUnit.CNY;
/**
* 转换目标金额单位
*/
MoneyUnit convertUnit() default MoneyUnit.CNY_FEN;
/**
* 是否必须转换金额,默认不转换
*/
boolean required() default false;
/**
* 转换方向,当金额注解重复注解时可以根据方向判定当前需要的注解,否则忽略该属性
*/
MoneyConvertDirection direction() default MoneyConvertDirection.REQUEST;
}
MoneyRepeatable类主要标识Money注解的可重复注解问题,真实场景下同一个类同一个属性可能同时在请求和响应中使用,因此需要在注解上增加可重复标记,代码如下:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 类MoneyRepeatable.java的实现描述:标记金额注解是可重复的注解
*
*/
@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@interface MoneyRepeatable {
Money[] value();
}
注意:这里MoneyRepeatable的@Target属性要和Money保持一致。
MoneyUnit类主要表示金额货币单位,代码如下:
import *.ICommonEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 类MoneyUnit.java的实现描述:货币单位枚举
*
* <pre>
* 世界货币单位表
* </pre>
*
* @see https://wiki.mbalib.com/wiki/%E4%B8%96%E7%95%8C%E8%B4%A7%E5%B8%81%E5%8D%95%E4%BD%8D%E8%A1%A8
*/
@AllArgsConstructor
@Getter
public enum MoneyUnit implements ICommonEnum {
/**
* 人民币元
*/
CNY("CNY", "元", 1),
/**
* 人民币辅币-角
*/
CNY_JIAO("jiao", "角", 10),
/**
* 人民币辅币-分
*/
CNY_FEN("fen", "分", 100),
/**
* 美元
*/
USD("usd", "元", 1),
/**
* 美元辅币-分
*/
USD_CENT("cent", "分", 100),
//TODO and so on
;
private String code;
private String desc;
/**
* 换算单位
*/
private int unit;
}
MoneyConvertDirection类,主要是表示金额的转换方向,考虑到同一个类同一个属性可能同时在请求和响应中使用,因此增加方向标识。代码如下:
import *.ICommonEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 类MoneyConvertDirection.java的实现描述:金额转换方向枚举
*
*/
@AllArgsConstructor
@Getter
public enum MoneyConvertDirection implements ICommonEnum {
/**
* request
*/
REQUEST("request", "请求"),
/**
* response
*/
RESPONSE("response", "响应"),;
private String code;
private String desc;
}
3-增加基于springmvc的请求和响应切面
请求切面,负责对请求的数据进行金额转换,代码如下:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonInputMessage;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ValueNode;
import *.MoneyConvertUtil;
import *.Money;
import *.MoneyClassCache;
import *.MoneyConvertDirection;
import lombok.extern.slf4j.Slf4j;
/**
* 类MoneyRequestBodyAdvice.java的实现描述:金额请求切面
*
*/
@Slf4j
@RestControllerAdvice
public class MoneyRequestBodyAdvice implements RequestBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/*
* 判断是否支持
* @see
* org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice#
* supports(org.springframework.core.MethodParameter,
* java.lang.reflect.Type, java.lang.Class)
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 请求参数为class为自定义
return MoneyClassCache.isSupportMoney(methodParameter.getParameterType())
|| (null != methodParameter.getParameterType() && methodParameter.getParameterType() instanceof Class<?>
&& methodParameter.getParameterType().getClassLoader() != null);
}
/*
* (non-Javadoc)
* @see
* org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice#
* beforeBodyRead(org.springframework.http.HttpInputMessage,
* org.springframework.core.MethodParameter, java.lang.reflect.Type,
* java.lang.Class)
*/
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType)
throws IOException {
long start = System.currentTimeMillis();
try {
if (!MoneyClassCache.isSupportMoney(parameter.getParameterType())) {
return inputMessage;
}
Field[] fieldList = parameter.getParameterType().getDeclaredFields();
if (null == fieldList || fieldList.length == 0) {
return inputMessage;
}
boolean isNotJavaAppRequest = MoneyConvertUtil.isNotJavaAppRequest();
JsonNode jsonNode = objectMapper.readTree(inputMessage.getBody());
resolve(fieldList, isNotJavaAppRequest, jsonNode);
return new MappingJacksonInputMessage(new ByteArrayInputStream(objectMapper.writeValueAsBytes(jsonNode)),
inputMessage.getHeaders());
} finally {
log.info("MoneyRequestConvert costTime:{}ms", System.currentTimeMillis() - start);
}
}
/**
* 解析请求中的金额字段并重新放入请求体
*
* @param fieldList
* @param isNotJavaAppRequest
* @param jsonObject
*/
private void resolve(Field[] fieldList, boolean isNotJavaAppRequest, JsonNode jsonNode) {
for (Field field : fieldList) {
JsonNode vNode = jsonNode.get(field.getName());
if (null == vNode || vNode instanceof NullNode) {
continue;
}
Money money = null;
Money[] ma = field.getDeclaredAnnotationsByType(Money.class);
if (null != ma && ma.length > 1) {
money = Arrays.stream(ma).filter(c -> c.direction() == MoneyConvertDirection.REQUEST).findFirst()
.orElse(null);
} else if (null != ma && ma.length == 1) {
money = ma[0];
}
if (null == money && (vNode instanceof ValueNode)){
continue;
}
if (null != money && MoneyConvertUtil.isNeedConvert(money, isNotJavaAppRequest)) {
BigDecimal v = vNode.decimalValue();
BigDecimal convertValue = MoneyConvertUtil.convert(money, v);
ObjectNode objectNode = (ObjectNode) jsonNode;
objectNode.put(field.getName(), convertValue.longValue());
log.info("convert field {} money from:{},to:{}", field.getName(), v, convertValue);
continue;
}
if (field.getType().getClassLoader() != null) {
JsonNode node = jsonNode.get(field.getName());
resolve(field.getType().getDeclaredFields(), isNotJavaAppRequest, node);
} else if (null != field.getGenericType() && field.getGenericType() instanceof ParameterizedType) {
Type actualType = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
if (null != ((Class<?>) actualType).getClassLoader()) {
Field[] nodeField = ((Class<?>) actualType).getDeclaredFields();
if (field.getType().isAssignableFrom(java.util.List.class)) {
ArrayNode nodeArray = (ArrayNode) jsonNode.get(field.getName());
for (int i = 0; i < nodeArray.size(); i++) {
JsonNode node = nodeArray.get(i);
resolve(nodeField, isNotJavaAppRequest, node);
}
}
}
}
}
}
/*
* (non-Javadoc)
* @see
* org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice#
* afterBodyRead(java.lang.Object,
* org.springframework.http.HttpInputMessage,
* org.springframework.core.MethodParameter, java.lang.reflect.Type,
* java.lang.Class)
*/
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
/*
* (non-Javadoc)
* @see
* org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice#
* handleEmptyBody(java.lang.Object,
* org.springframework.http.HttpInputMessage,
* org.springframework.core.MethodParameter, java.lang.reflect.Type,
* java.lang.Class)
*/
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
响应切面,负责对响应的数据进行金额转换,代码如下:
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.DecimalNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ValueNode;
import *.ResultCodeEnum;
import *.ResponseBean;
import *.ServiceException;
import *.MoneyConvertUtil;
import *.Money;
import *.MoneyClassCache;
import *.MoneyConvertDirection;
import *.DecimalUtils;
import lombok.extern.slf4j.Slf4j;
/**
* 类MoneyResponsBodyAdvice.java的实现描述:金额请求切面响应结果
*
* @author liqiankun 2023年4月23日 下午5:43:30
*/
@Slf4j
@RestControllerAdvice
public class MoneyResponsBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@PostConstruct
void init() {
// objectMapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
}
@SuppressWarnings("rawtypes")
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return returnType.getParameterType().isAssignableFrom(ResponseBean.class)
&& MoneyClassCache.isSupportMoney(returnType);
}
@SuppressWarnings("rawtypes")
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof ResponseBean) {
ResponseBean<?> resp = (ResponseBean) body;
Object data = resp.getData();
if (null != data) {
if (!MoneyClassCache.isSupportMoney(returnType)) {
return body;
}
long start = System.currentTimeMillis();
try {
boolean isNotJavaAppRequest = MoneyConvertUtil.isNotJavaAppRequest();
try {
JsonNode json = objectMapper.readTree(objectMapper.writeValueAsBytes(data));
resolve(data, json, isNotJavaAppRequest);
return ResponseBean.success(json).setCode(resp.getCode()).setMsg(resp.getMsg())
.setSuccess(resp.isSuccess());
} catch (JsonProcessingException e) {
throw new ServiceException(e, ResultCodeEnum.SERVICE_EXCEPTION);
} catch (IOException e) {
throw new ServiceException(e, ResultCodeEnum.SERVICE_EXCEPTION);
}
} finally {
log.info("MoneyReponseConvert costTime:{}ms", System.currentTimeMillis() - start);
}
}
}
return body;
}
private void resolve(Object value, JsonNode jsonNode, boolean isNotJavaAppRequest) {
if (value instanceof List) {
List<?> list = (List<?>) value;
for (int i = 0; i < list.size(); i++) {
JsonNode node = jsonNode.get(i);
resolveValue(list.get(i), node, isNotJavaAppRequest);
}
} else {
resolveValue(value, jsonNode, isNotJavaAppRequest);
}
}
private void resolveValue(Object value, JsonNode jsonNode, boolean isNotJavaAppRequest) {
Field[] fa = value.getClass().getDeclaredFields();
Arrays.stream(fa).forEach(field -> {
field.setAccessible(true);
Object v = null;
try {
v = field.get(value);
if (null == v) {
return;
}
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new ServiceException(e, ResultCodeEnum.SERVICE_EXCEPTION);
}
Money money = null;
Money[] ma = field.getDeclaredAnnotationsByType(Money.class);
if (null != ma && ma.length > 1) {
money = Arrays.stream(ma).filter(c -> c.direction() == MoneyConvertDirection.RESPONSE).findFirst()
.orElse(null);
} else if (null != ma && ma.length == 1) {
money = ma[0];
}
if (null == money && (jsonNode instanceof ValueNode)) {
return;
}
if (v.getClass().getClassLoader() == null && null != money
&& MoneyConvertUtil.isNeedConvert(money, isNotJavaAppRequest)) {
//分转元
BigDecimal convertValue = MoneyConvertUtil.convert(money, DecimalUtils.toBigDecimal(v));
ObjectNode node = (ObjectNode) jsonNode;
node.set(field.getName(), new DecimalNode(convertValue));
log.info("convert field {} money from:{},to:{}", field.getName(), v, convertValue);
return;
}
if (v.getClass().getClassLoader() != null) {
//自定义类型
JsonNode node = jsonNode.get(field.getName());
resolve(v, node, isNotJavaAppRequest);
} else if (null != field.getGenericType() && field.getGenericType() instanceof ParameterizedType) {
Type[] actualTypeArray = ((ParameterizedType) field.getGenericType()).getActualTypeArguments();
for (Type actualType : actualTypeArray) {
if (actualType instanceof WildcardType) {
continue;
}
if (actualType instanceof TypeVariable) {
resolve(v, jsonNode.get(field.getName()), isNotJavaAppRequest);
continue;
}
if (actualType instanceof Class && null != ((Class<?>) actualType).getClassLoader()) {
//支持集合属性
if (field.getType().isAssignableFrom(java.util.List.class)) {
ArrayNode nodeArray = (ArrayNode) jsonNode.get(field.getName());
List<?> vList = (List<?>) v;
for (int i = 0; i < nodeArray.size(); i++) {
JsonNode node = nodeArray.get(i);
resolveValue(vList.get(i), node, isNotJavaAppRequest);
}
}
}
}
}
});
}
}
同时增加MoneyClassCache工具类,便于在运行过程中存储解析结果,提升运行效率,代码如下:
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import *.Money;
/**
* 类MoneyClassCache.java的实现描述:金额标记类缓存
*
*/
public final class MoneyClassCache {
private static final Cache<String, String> CACHE = Caffeine.newBuilder().initialCapacity(20).maximumSize(10000L)
.build();
private static final String ZERO = "0";
private static final String ONE = "1";
private static void put(Class<?> clazz) {
CACHE.put(clazz.getCanonicalName(), ONE);
}
private static void put(Class<?> clazz, String value) {
CACHE.put(clazz.getCanonicalName(), value);
}
public static String get(Class<?> clazz) {
return CACHE.getIfPresent(clazz.getCanonicalName());
}
public static boolean isSupportMoney(MethodParameter methodParameter) {
Class<?> paramType = methodParameter.getParameterType();
boolean support = isSupportMoney(paramType);
if (support) {
return true;
}
Type genericType = methodParameter.getGenericParameterType();
return isGenericTypeClassSupportMoney(genericType);
}
private static boolean isGenericTypeClassSupportMoney(Type genericType) {
if (genericType instanceof TypeVariable) {
return false;
}
if (genericType instanceof ParameterizedType) {
Type actualType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
if (actualType instanceof WildcardType) {
return false;
}
if (actualType instanceof ParameterizedType) {
Type[] typeArray = ((ParameterizedType) actualType).getActualTypeArguments();
for (Type type : typeArray) {
if (isGenericTypeClassSupportMoney(type)) {
return true;
}
}
return false;
}
if (null != ((Class<?>) actualType).getClassLoader()) {
Class<?> actualTypeClass = (Class<?>) actualType;
if (actualTypeClass.isAssignableFrom(java.util.List.class)) {
return isGenericTypeClassSupportMoney(actualTypeClass);
}
return isSupportMoney(actualTypeClass);
}
}
if (genericType instanceof Class) {
Class<?> actualTypeClass = (Class<?>) genericType;
return isSupportMoney(actualTypeClass);
}
return false;
}
/**
* 判断指定class是否支持金额标记
*
* @param clazz
* @return
*/
public static boolean isSupportMoney(Class<?> clazz) {
//先获取缓存是否已经标记
String cache = get(clazz);
if (StringUtils.isNotBlank(cache)) {
return StringUtils.equals(ONE, cache);
}
synchronized (clazz) {
if (resolve(clazz)) {
//存放支持标记至缓存
put(clazz);
return true;
}
}
put(clazz, ZERO);
return false;
}
private static boolean resolve(Class<?> clazz) {
Field[] fieldList = clazz.getDeclaredFields();
for (Field field : fieldList) {
Money[] money = field.getDeclaredAnnotationsByType(Money.class);
if (null != money && money.length > 0) {
return true;
}
if (field.getType().getClassLoader() != null) {
return resolve((Class<?>) field.getType());
} else if (null != field.getGenericType() && field.getGenericType() instanceof ParameterizedType) {
Type genericType = field.getGenericType();
if (genericType instanceof TypeVariable) {
return false;
}
if (genericType instanceof ParameterizedType) {
Type[] actualTypeArray = ((ParameterizedType) genericType).getActualTypeArguments();
for (Type actualType : actualTypeArray) {
if (null != ((Class<?>) actualType).getClassLoader()) {
Class<?> actualTypeClass = (Class<?>) actualType;
if (actualTypeClass.isAssignableFrom(java.util.List.class)) {
return isGenericTypeClassSupportMoney(actualType);
}
if (resolve(actualTypeClass)) {
return true;
}
}
}
}
}
}
return false;
}
}
增加金额转换工具支持类MoneyConvertUtil,代码如下:
import java.math.BigDecimal;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import *.Money;
import *.MoneyUnit;
import *.DecimalUtils;
/**
* 类MoneyConvertUtil.java的实现描述:金额转换工具类
*
* @author liqiankun 2023年4月23日 下午3:30:03
*/
public final class MoneyConvertUtil {
/**
* 是否需要转换
*
* @param money
* @param isNotJavaAppRequest
* @return
*/
public static boolean isNeedConvert(Money money, boolean isNotJavaAppRequest) {
return money.required() || (!money.required() && isNotJavaAppRequest);
}
/**
* 金额转换
*
* @param money
* @param value
* @return
*/
public static BigDecimal convert(Money money, BigDecimal value) {
if (null != value) {
MoneyUnit unit = money.unit();
MoneyUnit convertUnit = money.convertUnit();
if (Objects.nonNull(unit) && Objects.nonNull(convertUnit)) {
if ((unit == MoneyUnit.CNY && (convertUnit == MoneyUnit.CNY_JIAO || convertUnit == MoneyUnit.CNY_FEN))
|| (unit == MoneyUnit.USD && (convertUnit == MoneyUnit.USD_CENT))) {
return value. multiply(DecimalUtils.toBigDecimal(money.convertUnit().getUnit()));
}
if ((convertUnit == MoneyUnit.CNY && (unit == MoneyUnit.CNY_JIAO || unit == MoneyUnit.CNY_FEN))
|| (convertUnit == MoneyUnit.USD && (unit == MoneyUnit.USD_CENT))) {
return value.divide(DecimalUtils.toBigDecimal(unit.getUnit()));
}
}
}
return value;
}
/**
* 是否为非Java应用请求
*
* @return
*/
public static boolean isNotJavaAppRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (null != requestAttributes) {
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
String springApplicationName = request.getHeader("spring.application.name");
return StringUtils.isBlank(springApplicationName);
}
return true;
}
}
金额处理工具类代码如下:
import java.math.BigDecimal;
/**
* bigdecimal工具类
*/
public class DecimalUtils {
/**
* 加法计算(result = x + y)
*
* @param x 被加数(可为null)
* @param y 加数 (可为null)
* @return 和 (可为null)
*/
public static BigDecimal add(BigDecimal x, BigDecimal y) {
if (x == null) {
return y;
}
if (y == null) {
return x;
}
return x.add(y);
}
/**
* 加法计算(result = a + b + c + d)
*
* @param a 被加数(可为null)
* @param b 加数(可为null)
* @param c 加数(可为null)
* @param d 加数(可为null)
* @return BigDecimal (可为null)
*/
public static BigDecimal add(BigDecimal a, BigDecimal b, BigDecimal c, BigDecimal d) {
BigDecimal ab = add(a, b);
BigDecimal cd = add(c, d);
return add(ab, cd);
}
/**
* 累加计算(result=x + result)
*
* @param x 被加数(可为null)
* @param result 和 (可为null,若被加数不为为null,result默认值为0)
* @return result 和 (可为null)
*/
public static BigDecimal accumulate(BigDecimal x, BigDecimal result) {
if (x == null) {
return result;
}
if (result == null) {
result = new BigDecimal("0");
}
return result.add(x);
}
/**
* 减法计算(result = x - y)
*
* @param x 被减数(可为null)
* @param y 减数(可为null)
* @return BigDecimal 差 (可为null)
*/
public static BigDecimal subtract(BigDecimal x, BigDecimal y) {
if (x == null || y == null) {
return null;
}
return x.subtract(y);
}
/**
* 乘法计算(result = x × y)
*
* @param x 乘数(可为null)
* @param y 乘数(可为null)
* @return BigDecimal 积
*/
public static BigDecimal multiply(BigDecimal x, BigDecimal y) {
if (x == null || y == null) {
return null;
}
return x.multiply(y);
}
public static BigDecimal multiply(BigDecimal x, String y) {
if (x == null || y == null) {
return null;
}
return x.multiply(toBigDecimal(y));
}
public static BigDecimal multiply(String x, String y) {
if (x == null || y == null) {
return null;
}
return toBigDecimal(x).multiply(toBigDecimal(y));
}
/**
* 除法计算(result = x ÷ y)
*
* @param x 被除数(可为null)
* @param y 除数(可为null)
* @return 商 (可为null,四舍五入,默认保留20位小数)
*/
public static BigDecimal divide(BigDecimal x, BigDecimal y) {
if (x == null || y == null || y.compareTo(BigDecimal.ZERO) == 0) {
return null;
}
// 结果为0.000..时,不用科学计数法展示
return stripTrailingZeros(x.divide(y, 20, BigDecimal.ROUND_HALF_UP));
}
/**
* 转为字符串(防止返回可续计数法表达式)
*
* @param x 要转字符串的小数
* @return String
*/
public static String toPlainString(BigDecimal x) {
if (x == null) {
return null;
}
return x.toPlainString();
}
/**
* 保留小数位数
*
* @param x 目标小数
* @param scale 要保留小数位数
* @return BigDecimal 结果四舍五入
*/
public static BigDecimal scale(BigDecimal x, int scale) {
if (x == null) {
return null;
}
return x.setScale(scale, BigDecimal.ROUND_HALF_UP);
}
/**
* 整型转为BigDecimal
*
* @param x(可为null)
* @return BigDecimal (可为null)
*/
public static BigDecimal toBigDecimal(Integer x) {
if (x == null) {
return null;
}
return new BigDecimal(x.toString());
}
/**
* 长整型转为BigDecimal
*
* @param x(可为null)
* @return BigDecimal (可为null)
*/
public static BigDecimal toBigDecimal(Long x) {
if (x == null) {
return null;
}
return new BigDecimal(x.toString());
}
/**
* 双精度型转为BigDecimal
*
* @param x(可为null)
* @return BigDecimal (可为null)
*/
public static BigDecimal toBigDecimal(Double x) {
if (x == null) {
return null;
}
return new BigDecimal(x.toString());
}
/**
* 单精度型转为BigDecimal
*
* @param x(可为null)
* @return BigDecimal (可为null)
*/
public static BigDecimal toBigDecimal(Float x) {
if (x == null) {
return null;
}
return new BigDecimal(x.toString());
}
/**
* 字符串型转为BigDecimal
*
* @param x(可为null)
* @return BigDecimal (可为null)
*/
public static BigDecimal toBigDecimal(String x) {
if (x == null) {
return null;
}
return new BigDecimal(x);
}
/**
* 对象类型转为BigDecimal
*
* @param x(可为null)
* @return BigDecimal (可为null)
*/
public static BigDecimal toBigDecimal(Object x) {
if (x == null) {
return null;
}
return new BigDecimal(x.toString());
}
/**
* 倍数计算,用于单位换算
*
* @param x 目标数(可为null)
* @param multiple 倍数 (可为null)
* @return BigDecimal (可为null)
*/
public static BigDecimal multiple(BigDecimal x, Integer multiple) {
if (x == null || multiple == null) {
return null;
}
return DecimalUtils.multiply(x, toBigDecimal(multiple));
}
/**
* 去除小数点后的0(如: 输入1.000返回1)
*
* @param x 目标数(可为null)
* @return
*/
public static BigDecimal stripTrailingZeros(BigDecimal x) {
if (x == null) {
return null;
}
return x.stripTrailingZeros();
}
}
请求和响应过程中,需要使用Jackson的ObjectMapper序列化和反序列化Json,同时需要注意BigDecimal金额数据的科学计数法问题,通常情况下会通过ObjectNode对象的put方法进行数据设置,但此过程中会调用BigDecimal的科学计数法,代码如下
/**
* Factory method for getting an instance of JSON numeric value
* that expresses given unlimited precision floating point value
*
* <p>In the event that the factory has been built to normalize decimal
* values, the BigDecimal argument will be stripped off its trailing zeroes,
* using {@link BigDecimal#stripTrailingZeros()}.</p>
*
* @see #JsonNodeFactory(boolean)
*/
@Override
public ValueNode numberNode(BigDecimal v)
{
if (v == null) {
return nullNode();
}
/*
* If the user wants the exact representation of this big decimal,
* return the value directly
*/
if (_cfgBigDecimalExact)
return DecimalNode.valueOf(v);
/*
* If the user has asked to strip trailing zeroes, however, there is
* this bug to account for:
*
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6480539
*
* In short: zeroes are never stripped out of 0! We therefore _have_
* to compare with BigDecimal.ZERO...
*/
return v.compareTo(BigDecimal.ZERO) == 0 ? DecimalNode.ZERO
: DecimalNode.valueOf(v.stripTrailingZeros());
}
详细可以查看源码:com.fasterxml.jackson.databind.node.JsonNodeFactory.numberNode(BigDecimal)
此时解决方案时直接通过调用ObjectNode对象的set方法,通过初始化DecimalNode对象进行赋值,则可以解决该问题,代码如下:
有兴趣的可以查看源码,初始化RequestBodyAdvice、ResponseBodyAdvice的源码如下:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.initControllerAdviceCache()
代码使用
在需要转换的对象属性上进行注解,标记方向和转换单位即可,代码如下:
@Setter
@Getter
public class MeneyReq {
@Money(unit = MoneyUnit.CNY,convertUnit = MoneyUnit.CNY_FEN)
private Long amount;
}
总结
1-切面思维,通过RequestBodyAdvice、ResponseBodyAdvice对springmvc的请求和响应进行处理,配合@RequestBody、@ResponseBody或者@RestController处理请求和响应,通过ObjectMapper对数据进行处理,便于使用统一的数据类型处理器
2-如果有多个Advice时需要使用Order指定切面顺序,可以通过@org.springframework.core.annotation.Order或者直接实现org.springframework.core.Ordered接口实现顺序的指定,其中order的值越大优先级越低,即顺序值是按照数字从小到大排序。