本文以自动处理金额字段为参考,将返回结果自动匹配相对应的单位(金额单位:【元,万元,亿】),且支持单位是否放置额外字段等。

本文涉及技术点:自定义注解、AOP、反射、递归

一、前提背景

α、回参原始样例

  1. {
  2. "code": 0,
  3. "msg": "成功",
  4. "data": {
  5. "money": "20000000",
  6. "moneyUnit": null
  7. }
  8. }

β、需要处理的两个场景

1、转换money,且将单位跟在money后。

  1. {
  2. "code": 0,
  3. "msg": "成功",
  4. "data": {
  5. "money": "2000.00万元",
  6. "moneyUnit": null
  7. }
  8. }

2、转换money,将单位放入额外字段moneyUnit。

  1. {
  2. "code": 0,
  3. "msg": "成功",
  4. "data": {
  5. "money": "2000.00",
  6. "moneyUnit": "万元"
  7. }
  8. }

二、设计思路

α、利用AOP在spring返回responseBody时切入处理

b、根据反射拿到DTO内部的属性和值

γ、若DTO内含DTO,用递归处理

δ、最终解析为基础类型进入处理方法,处理方法中根据自定义注解实现多种场景配置

三、代码演示

α、主逻辑代码

1、实现ResponseBodyAdvice类,重写supports,beforeBodyWrite方法。

  1. // ControllerAdvice用于对Controller进行“切面”环绕的注解
  2. @ControllerAdvice
  3. @Slf4j
  4. // 实现ResponseBodyAdvice类
  5. public class ResponseDealAdvice implements ResponseBodyAdvice {
  6. @Override
  7. public boolean supports(MethodParameter methodParameter, Class aClass) {
  8. return true;
  9. }
  10. // 重写此方法
  11. @Override
  12. public Object beforeBodyWrite(Object o, MethodParameter methodParameter,
  13. MediaType mediaType, Class aClass,
  14. ServerHttpRequest serverHttpRequest,
  15. ServerHttpResponse serverHttpResponse) {
  16. try {
  17. // 对统一返回类Result开始反射解析(统一返回类见四-α))
  18. if (o instanceof Result) {
  19. Result resultContentVO = (Result) o;
  20. // 获取统一返回类中的实际返回类的所有属性
  21. Field[] declaredFields = resultContentVO.getData().getClass()
  22. .getDeclaredFields();
  23. // 调用递归方法(方法见三-α-2)
  24. recursion(result, declaredFields);
  25. }
  26. } catch (Exception e) {
  27. log.error("ResponseDealAdvice beforeBodyWrite error:" + e);
  28. }
  29. return o;
  30. }
  31. //省略下方代码
  32. ..........
  33. }

2、递归处理DTO方法,递归解析,一直解析到基础类型。

目前DTO仅支持:List、DTO(内部类)、基础数据类型

  1. /**
  2. * 递归处理DTO属性
  3. * 支持DTO类型:list,内部DTO,基本数据类型
  4. * @param o
  5. * @param declaredFields
  6. */
  7. void recursion(Object o, Field[] declaredFields) {
  8. try {
  9. for (Field declaredField : declaredFields) {
  10. Field declaredField = declaredFields[i];
  11. //如果属性是List就解析list的对象
  12. if (declaredField.getType().getName().contains("List")) {
  13. // 反射获取DTO里xxxList的get方法
  14. Method get = o.getClass().getMethod("get"
  15. + declaredField.getName().substring(0, 1).toUpperCase()
  16. + declaredField.getName().substring(1));
  17. // 反射获取xxxList实例
  18. List invoke = (List) get.invoke(o);
  19. if (!CollectionUtils.isEmpty(invoke)) {
  20. // 遍历xxxList
  21. invoke.forEach(t -> {
  22. try {
  23. // 获取所有属性
  24. Field[] declaredFields1 = t.getClass()
  25. .getDeclaredFields();
  26. // 对属性遍历
  27. for (Field field : declaredFields1) {
  28. // 如果List中的属性有list和DTO递归处理
  29. if (field.getType().getName().contains("List")
  30. || field.getType().getName().contains("$")) {
  31. recursion(t, declaredFields1);
  32. }
  33. // 剩下(基本类型)进入处理方法(方法见三-α-3)
  34. dealMoney(t, field);
  35. }
  36. } catch (Exception e) {
  37. log.error("ResponseDealAdvice listForEach error:" + e);
  38. }
  39. });
  40. }
  41. } else if (declaredField.getType().getName().contains("$")) {
  42. // 如果属性是内部DTO
  43. // 获取内部DTO所有的属性
  44. Field[] nestedClassFields = declaredField.getType().getDeclaredFields();
  45. // 反射拿到内部类的名字 !!!ps:内部DTO名字保持和类型一致!!!
  46. String nestedClassName = declaredField.getType().getName()
  47. .substring(declaredField.getType().getName().indexOf("$")));
  48. Method method = o.getClass().getMethod("get" + nestedClassName);
  49. // 反射获取到内部DTO实例
  50. Object invoke = method.invoke(o);
  51. // 进入到递归方法
  52. recursion(invoke, nestedClassFields);
  53. } else {
  54. // 非List,非DTO,基本类型直接处理(方法见三-α-3)
  55. dealMoney(o, declaredField);
  56. }
  57. }
  58. } catch (Exception e) {
  59. log.error("ResponseDealAdvice recursion error:" + e);
  60. }
  61. }

3、基础类型统一处理方法

  1. /**
  2. * 统一处理金额
  3. *
  4. * @param t
  5. * @param filed
  6. */
  7. void dealMoney(Object t, Field filed) {
  8. try {
  9. // 有DealMoney注解的属性进入(注解类见四-β)
  10. if (filed.isAnnotationPresent(DealMoney.class)) {
  11. // 获取字段名
  12. String filedName = filed.getName();
  13. // 反射获取字段的get方法
  14. Method getMethodMain = t.getClass().getMethod("get"
  15. + filedName.substring(0, 1).toUpperCase()
  16. + filedName.substring(1));
  17. // 根据发射的get方法获取实例的money值
  18. String money = (String) getMethodMain.invoke(t);
  19. // money为空处理
  20. if (money == null || money.equals("-")) {
  21. return;
  22. }
  23. // 统一处理money,计算money是元/万元/亿元哪种,放入新DTO类返回(方法见三-α-4)
  24. MoneyVO moneyAndUnit = getMoneyAndUnit(new BigDecimal(money));
  25. // 通过反射获取注解字段的set方法
  26. Method setMethodMain = t.getClass().getMethod("set"
  27. + filedName.substring(0, 1).toUpperCase()
  28. + filedName.substring(1), String.class);
  29. // 获取字段的注解值
  30. DealMoney annotation = filed.getAnnotation(DealMoney.class);
  31. if (annotation.HavaUnit() == 1) {
  32. // 默认1 注解字段+额外字段填充
  33. // 填充注解字段
  34. setMethodMain.invoke(t, moneyAndUnit.getMoneyAmount());
  35. // 获取额外字段单位的set方法
  36. Method setMethodUnit = t.getClass().getMethod("set"
  37. + filedName.substring(0, 1).toUpperCase()
  38. + filedName.substring(1) + "Unit", String.class);
  39. // 填充单位字段
  40. setMethodUnit.invoke(t, moneyAndUnit.getMoneyUnit());
  41. } else if (annotation.HavaUnit() == 2) {
  42. // 2 金额+单位填充到注解字段中
  43. setMethodMain.invoke(t, moneyAndUnit.getMoneyAmount()
  44. + moneyAndUnit.getMoneyUnit());
  45. } else {
  46. // 没有输出错误
  47. log.error("ResponseDealAdvice dealMoney error: missing havaUnit type"
  48. + annotation.HavaUnit());
  49. }
  50. }
  51. } catch (Exception e) {
  52. log.error("ResponseDealAdvice dealMoney error:" + e);
  53. }
  54. }

4、统一处理money类

  1. public static MoneyVO getMoneyAndUnit(BigDecimal newMoney){
  2. // VO类见三-α-5
  3. MoneyVO moneyVO = new MoneyVO();
  4. moneyVO.setMoneyUnit("元");
  5. moneyVO.setMoneyAmount("-");
  6. // 入参为空,直接返回
  7. if(newMoney==null) return moneyVO;
  8. if(newMoney.compareTo(new BigDecimal(10000))>=0){
  9. // 值大于1w,设置单位,处理money值回填vo
  10. moneyVO.setMoneyAmount(
  11. newMoney.divide(BigDecimal.valueOf(10000),
  12. 2, BigDecimal.ROUND_HALF_UP).toString());
  13. moneyVO.setMoneyUnit("万元");
  14. } else if (newMoney.compareTo(new BigDecimal(1000000000)) >= 0) {
  15. // 值大于1w,设置单位,处理money值回填vo
  16. moneyVO.setMoneyAmount(
  17. newMoney.divide(BigDecimal.valueOf(1000000000),
  18. 2, BigDecimal.ROUND_HALF_UP).toString());
  19. moneyVO.setMoneyUnit("亿元");
  20. } else moneyVO.setMoneyAmount(newMoney.setScale(2, BigDecimal.ROUND_HALF_UP).toString());
  21. return moneyVO;
  22. }

5、统一处理money方法所需VO类

  1. @Getter
  2. @Setter
  3. public class MoneyVO implements Serializable {
  4. /**
  5. * 金额量
  6. */
  7. private String moneyAmount;
  8. /**
  9. * 金额单位
  10. */
  11. private String moneyUnit;
  12. }

四、辅助代码

α、统一返回类

  1. @Data
  2. public class Result<T> {
  3. private Integer code;
  4. private String msg;
  5. private T data;
  6. private Result(Integer code, String msg){
  7. this.code = code;
  8. this.msg = msg;
  9. }
  10. /**
  11. * 成功返回,包含对象
  12. * @param t
  13. * @param <T>
  14. * @return
  15. */
  16. public static <T> Result<T> success(T t){
  17. Result<T> result = new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMsg());
  18. result.setData(t);
  19. return result;
  20. }
  21. /**
  22. * 成功返回,无对象
  23. * @return
  24. */
  25. public static <T> Result<T> success(){
  26. return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMsg());
  27. }
  28. /**
  29. * 失败返回
  30. * @param code
  31. * @param msg
  32. * @return
  33. */
  34. public static <T> Result<T> error(Integer code, String msg){
  35. return new Result<>(code, msg);
  36. }
  37. }

β、统一注解类

  1. @Target(ElementType.FIELD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Component
  4. public @interface DealMoney {
  5. /**
  6. * 解决单位问题
  7. * 默认为
  8. * 1:金额和单位字段都填充
  9. * 2:金额和单位合并填充金额字段
  10. */
  11. int HavaUnit() default 1;
  12. }

五、测试代码

α、测试DTO

  1. @Data
  2. public class TestDTO {
  3. @DealMoney(HavaUnit = 1)
  4. private String money;
  5. private String moneyUnit;
  6. private List<InnerClassDTO> aList;
  7. @NoArgsConstructor
  8. @Data
  9. public static class InnerClassDTO {
  10. @DealMoney(HavaUnit = 2)
  11. private String innerMoney;
  12. private String innerMoneyUnit;
  13. private Inner2ClassDTO inner2Class;
  14. }
  15. @NoArgsConstructor
  16. @Data
  17. public static class Inner2ClassDTO {
  18. @DealMoney(HavaUnit = 1)
  19. private String innerMoney;
  20. private String innerMoneyUnit;
  21. }
  22. }

β、测试controller

  1. @RestController
  2. @RequestMapping("/test")
  3. public class TestController {
  4. @RequestMapping("/unit")
  5. public Result<TestDTO> test(){
  6. TestDTO testDTO = new TestDTO();
  7. testDTO.setMoney("20000000");
  8. List<TestDTO.InnerClassDTO> aList = new ArrayList<>();
  9. TestDTO.InnerClassDTO innerDTO = new TestDTO.InnerClassDTO();
  10. innerDTO.setInnerMoney("10000000");
  11. TestDTO.Inner2ClassDTO innner2DTO = new TestDTO.Inner2ClassDTO();
  12. innner2DTO.setInnerMoney("5000");
  13. innerDTO.setInner2Class(innner2DTO);
  14. aList.add(innerDTO);
  15. testDTO.setAList(aList);
  16. return Result.success(testDTO);
  17. }
  18. }

γ、测试结果

  1. {
  2. "code": 0,
  3. "msg": "成功",
  4. "data": {
  5. "money": "2000.00",
  6. "moneyUnit": "万元",
  7. "alist": [
  8. {
  9. "innerMoney": "1000.00万元",
  10. "innerMoneyUnit": null,
  11. "inner2Class": {
  12. "innerMoney": "5000.00",
  13. "innerMoneyUnit": "元"
  14. }
  15. }
  16. ]
  17. }
  18. }