本文以自动处理金额字段为参考,将返回结果自动匹配相对应的单位(金额单位:【元,万元,亿】),且支持单位是否放置额外字段等。
本文涉及技术点:自定义注解、AOP、反射、递归
一、前提背景
α、回参原始样例
{"code": 0,"msg": "成功","data": {"money": "20000000","moneyUnit": null}}
β、需要处理的两个场景
1、转换money,且将单位跟在money后。
{"code": 0,"msg": "成功","data": {"money": "2000.00万元","moneyUnit": null}}
2、转换money,将单位放入额外字段moneyUnit。
{"code": 0,"msg": "成功","data": {"money": "2000.00","moneyUnit": "万元"}}
二、设计思路
α、利用AOP在spring返回responseBody时切入处理
b、根据反射拿到DTO内部的属性和值
γ、若DTO内含DTO,用递归处理
δ、最终解析为基础类型进入处理方法,处理方法中根据自定义注解实现多种场景配置
三、代码演示
α、主逻辑代码
1、实现ResponseBodyAdvice类,重写supports,beforeBodyWrite方法。
// ControllerAdvice用于对Controller进行“切面”环绕的注解@ControllerAdvice@Slf4j// 实现ResponseBodyAdvice类public class ResponseDealAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter methodParameter, Class aClass) {return true;}// 重写此方法@Overridepublic Object beforeBodyWrite(Object o, MethodParameter methodParameter,MediaType mediaType, Class aClass,ServerHttpRequest serverHttpRequest,ServerHttpResponse serverHttpResponse) {try {// 对统一返回类Result开始反射解析(统一返回类见四-α))if (o instanceof Result) {Result resultContentVO = (Result) o;// 获取统一返回类中的实际返回类的所有属性Field[] declaredFields = resultContentVO.getData().getClass().getDeclaredFields();// 调用递归方法(方法见三-α-2)recursion(result, declaredFields);}} catch (Exception e) {log.error("ResponseDealAdvice beforeBodyWrite error:" + e);}return o;}//省略下方代码..........}
2、递归处理DTO方法,递归解析,一直解析到基础类型。
目前DTO仅支持:List、DTO(内部类)、基础数据类型
/*** 递归处理DTO属性* 支持DTO类型:list,内部DTO,基本数据类型* @param o* @param declaredFields*/void recursion(Object o, Field[] declaredFields) {try {for (Field declaredField : declaredFields) {Field declaredField = declaredFields[i];//如果属性是List就解析list的对象if (declaredField.getType().getName().contains("List")) {// 反射获取DTO里xxxList的get方法Method get = o.getClass().getMethod("get"+ declaredField.getName().substring(0, 1).toUpperCase()+ declaredField.getName().substring(1));// 反射获取xxxList实例List invoke = (List) get.invoke(o);if (!CollectionUtils.isEmpty(invoke)) {// 遍历xxxListinvoke.forEach(t -> {try {// 获取所有属性Field[] declaredFields1 = t.getClass().getDeclaredFields();// 对属性遍历for (Field field : declaredFields1) {// 如果List中的属性有list和DTO递归处理if (field.getType().getName().contains("List")|| field.getType().getName().contains("$")) {recursion(t, declaredFields1);}// 剩下(基本类型)进入处理方法(方法见三-α-3)dealMoney(t, field);}} catch (Exception e) {log.error("ResponseDealAdvice listForEach error:" + e);}});}} else if (declaredField.getType().getName().contains("$")) {// 如果属性是内部DTO// 获取内部DTO所有的属性Field[] nestedClassFields = declaredField.getType().getDeclaredFields();// 反射拿到内部类的名字 !!!ps:内部DTO名字保持和类型一致!!!String nestedClassName = declaredField.getType().getName().substring(declaredField.getType().getName().indexOf("$")));Method method = o.getClass().getMethod("get" + nestedClassName);// 反射获取到内部DTO实例Object invoke = method.invoke(o);// 进入到递归方法recursion(invoke, nestedClassFields);} else {// 非List,非DTO,基本类型直接处理(方法见三-α-3)dealMoney(o, declaredField);}}} catch (Exception e) {log.error("ResponseDealAdvice recursion error:" + e);}}
3、基础类型统一处理方法
/*** 统一处理金额** @param t* @param filed*/void dealMoney(Object t, Field filed) {try {// 有DealMoney注解的属性进入(注解类见四-β)if (filed.isAnnotationPresent(DealMoney.class)) {// 获取字段名String filedName = filed.getName();// 反射获取字段的get方法Method getMethodMain = t.getClass().getMethod("get"+ filedName.substring(0, 1).toUpperCase()+ filedName.substring(1));// 根据发射的get方法获取实例的money值String money = (String) getMethodMain.invoke(t);// money为空处理if (money == null || money.equals("-")) {return;}// 统一处理money,计算money是元/万元/亿元哪种,放入新DTO类返回(方法见三-α-4)MoneyVO moneyAndUnit = getMoneyAndUnit(new BigDecimal(money));// 通过反射获取注解字段的set方法Method setMethodMain = t.getClass().getMethod("set"+ filedName.substring(0, 1).toUpperCase()+ filedName.substring(1), String.class);// 获取字段的注解值DealMoney annotation = filed.getAnnotation(DealMoney.class);if (annotation.HavaUnit() == 1) {// 默认1 注解字段+额外字段填充// 填充注解字段setMethodMain.invoke(t, moneyAndUnit.getMoneyAmount());// 获取额外字段单位的set方法Method setMethodUnit = t.getClass().getMethod("set"+ filedName.substring(0, 1).toUpperCase()+ filedName.substring(1) + "Unit", String.class);// 填充单位字段setMethodUnit.invoke(t, moneyAndUnit.getMoneyUnit());} else if (annotation.HavaUnit() == 2) {// 2 金额+单位填充到注解字段中setMethodMain.invoke(t, moneyAndUnit.getMoneyAmount()+ moneyAndUnit.getMoneyUnit());} else {// 没有输出错误log.error("ResponseDealAdvice dealMoney error: missing havaUnit type"+ annotation.HavaUnit());}}} catch (Exception e) {log.error("ResponseDealAdvice dealMoney error:" + e);}}
4、统一处理money类
public static MoneyVO getMoneyAndUnit(BigDecimal newMoney){// VO类见三-α-5MoneyVO moneyVO = new MoneyVO();moneyVO.setMoneyUnit("元");moneyVO.setMoneyAmount("-");// 入参为空,直接返回if(newMoney==null) return moneyVO;if(newMoney.compareTo(new BigDecimal(10000))>=0){// 值大于1w,设置单位,处理money值回填vomoneyVO.setMoneyAmount(newMoney.divide(BigDecimal.valueOf(10000),2, BigDecimal.ROUND_HALF_UP).toString());moneyVO.setMoneyUnit("万元");} else if (newMoney.compareTo(new BigDecimal(1000000000)) >= 0) {// 值大于1w,设置单位,处理money值回填vomoneyVO.setMoneyAmount(newMoney.divide(BigDecimal.valueOf(1000000000),2, BigDecimal.ROUND_HALF_UP).toString());moneyVO.setMoneyUnit("亿元");} else moneyVO.setMoneyAmount(newMoney.setScale(2, BigDecimal.ROUND_HALF_UP).toString());return moneyVO;}
5、统一处理money方法所需VO类
@Getter@Setterpublic class MoneyVO implements Serializable {/*** 金额量*/private String moneyAmount;/*** 金额单位*/private String moneyUnit;}
四、辅助代码
α、统一返回类
@Datapublic class Result<T> {private Integer code;private String msg;private T data;private Result(Integer code, String msg){this.code = code;this.msg = msg;}/*** 成功返回,包含对象* @param t* @param <T>* @return*/public static <T> Result<T> success(T t){Result<T> result = new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMsg());result.setData(t);return result;}/*** 成功返回,无对象* @return*/public static <T> Result<T> success(){return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMsg());}/*** 失败返回* @param code* @param msg* @return*/public static <T> Result<T> error(Integer code, String msg){return new Result<>(code, msg);}}
β、统一注解类
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Componentpublic @interface DealMoney {/*** 解决单位问题* 默认为* 1:金额和单位字段都填充* 2:金额和单位合并填充金额字段*/int HavaUnit() default 1;}
五、测试代码
α、测试DTO
@Datapublic class TestDTO {@DealMoney(HavaUnit = 1)private String money;private String moneyUnit;private List<InnerClassDTO> aList;@NoArgsConstructor@Datapublic static class InnerClassDTO {@DealMoney(HavaUnit = 2)private String innerMoney;private String innerMoneyUnit;private Inner2ClassDTO inner2Class;}@NoArgsConstructor@Datapublic static class Inner2ClassDTO {@DealMoney(HavaUnit = 1)private String innerMoney;private String innerMoneyUnit;}}
β、测试controller
@RestController@RequestMapping("/test")public class TestController {@RequestMapping("/unit")public Result<TestDTO> test(){TestDTO testDTO = new TestDTO();testDTO.setMoney("20000000");List<TestDTO.InnerClassDTO> aList = new ArrayList<>();TestDTO.InnerClassDTO innerDTO = new TestDTO.InnerClassDTO();innerDTO.setInnerMoney("10000000");TestDTO.Inner2ClassDTO innner2DTO = new TestDTO.Inner2ClassDTO();innner2DTO.setInnerMoney("5000");innerDTO.setInner2Class(innner2DTO);aList.add(innerDTO);testDTO.setAList(aList);return Result.success(testDTO);}}
γ、测试结果
{"code": 0,"msg": "成功","data": {"money": "2000.00","moneyUnit": "万元","alist": [{"innerMoney": "1000.00万元","innerMoneyUnit": null,"inner2Class": {"innerMoney": "5000.00","innerMoneyUnit": "元"}}]}}
