设计模式-反射+注解的组合拳

173 阅读4分钟

学而时习之,不亦说乎

前言

反射 + 注解

需求场景

业务功能需要跟第三方银行对接,需要对接创建账户,支付接口。

请求参数接入规则如下:

  1. 需要将各请求字段按照固定长度,按规定顺序,拼接成一个大的字符串
  2. 不同的字段类型,填充固定长度规则不同:
    • 针对字符串类型的字段,不满部分用*后置填充。
    • 针对数字类型的字段,不满部分用0前置填充
    • 针对金额类型的字段,转化以分作为单位,不满部分用0前置填充
  3. 再针对这个拼接的字符串进行md5加密,作为签名,追加到这个字符串后面

具体接口文档:

  1. 创建账户接口:http://127.0.0.1:8080/bank/api/createAccount
参数顺序类型长度
name1字符串10
idCard2字符串18
age3数字3
mobile4字符串11
  1. 支付接口:http://127.0.0.1:8080/bank/api/doPay
参数顺序类型长度
accountNo1字符串10
amount2金额10

常规实现

需求其实很简单,按照对接要求,直接开发对接方法即可,常规可能会这样实现:

public class BsThreeBankServiceImpl implements IBsThreeBankService {

    private static final String BANK_BASE_URL = "127.0.0.1:8080/bank/api/";

    @Override
    public String createAccount(String name, String idCard, Integer age, String mobile) {
        //定义创建账号第三方请求URL
        String createAccountUrl = BANK_BASE_URL + "create";
        //初始化请求参数
        StringBuilder builder = new StringBuilder();
        //姓名(字符串)固定长度10位,不够用*后置填充
        builder.append(String.format("%-10s", name).replace(' ', '*'));
        //身份证号(字符串),固定长度18位,不够用*后置填充
        builder.append(String.format("%-18s", idCard).replace(' ', '*'));
        //年龄(数字),固定长度3位,不满长度部分以0前置填充
        builder.append(String.format("%03d", age));
        //手机号(字符串),固定长度11位,不够用*后置填充
        builder.append(String.format("%-11s", mobile).replace(' ', '*'));
        //加上MD5作为签名
        builder.append(DigestUtils.md2Hex(builder.toString()));
        //利用huTool工具类,发送post请求
        return HttpRequest.post(createAccountUrl).body(builder.toString()).execute().body();

    }

    @Override
    public String doPay(Long accountNo, BigDecimal amount) {
        //定义创建账号第三方请求URL
        String doPayUrl = BANK_BASE_URL + "doPay";
        //初始化请求参数
        StringBuilder builder = new StringBuilder();
        //账户号(字符串),固定长度10位,不满长度部分以0前置填充
        builder.append(String.format("%10d", accountNo));
        //金额向下舍入2位到分,以分为单位,不满长度部分以0前置填充
        builder.append(String.format("%010d", amount.setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
        //加上MD5作为签名
        builder.append(DigestUtils.md2Hex(builder.toString()));
        return HttpRequest.post(doPayUrl).body(builder.toString()).execute().body();
    }

分析以上实现方案,可发现可能存在如下几点问题:

  1. 相同类型的字段重复的格式化处理,代码上重复。
  2. 参数顺序,靠拼接次序硬编码决定,容易人为出错。
  3. 字符串拼接、加签和发请求的逻辑,代码上重复

优化实现

利用注解 + 反射,把重复的代码抽离,将字段的额外信息绑定在注解上,具体实现如下:

  1. 首先创建两个注解,BankAPI、BankAPIField
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface BankAPI {
    /**
     * 接口描述
     * @return
     */
    String desc() default "";

    /**
     * 接口url
     * @return
     */
    String url() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface BankAPIField {

    /**
     * 顺序
     * @return
     */
    int order() default 0;

    /**
     * 字段长度
     * @return
     */
    int length() default 0;

    /**
     * 字段类型,S:字符串,N:数字,M:金额
     * @return
     */
    String dateType() default "";
}
  1. 定义入参POJO类,BankCreateAccount,BankDoPay,让他们继承一个空的抽象类AbstractBankApi,利用以上定义的两个注解,按接口文档进行填充定义。
@Data
@BankAPI(url = "/createAccount",desc = "账户创建")
public class BankCreateAccount extends AbstractBankApi {

    @BankAPIField(order = 1,length = 10,dateType = "S")
    private String name;
    @BankAPIField(order = 2,length = 18,dateType = "S")
    private String idCard;
    @BankAPIField(order = 3,length = 10,dateType = "N")
    private Integer age;
    @BankAPIField(order = 3,length = 11,dateType = "S")
    private String mobile;
}
@Data
@BankAPI(url = "/doPay",desc = "金额支付")
public class BankDoPay extends AbstractBankApi {

    @BankAPIField(order = 1,length = 10,dateType = "N")
    private Long accountNo;

    @BankAPIField(order = 2,length = 10,dateType = "M")
    private BigDecimal amount;
}
  1. 定义核心的apiCall方法,其中就用到了反射相关功能,来实现参数排序、格式化、拼装、发送请求的功能。
 private String apiCall(AbstractBankApi api){
        //获取bankApi注解中的请求接口URL
        BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class);
        String url = bankAPI.url();
        //初始化请求参数
        StringBuilder builder = new StringBuilder();
        //获得所有字段
        Field[] fieldList = api.getClass().getDeclaredFields();

        Arrays.stream(fieldList)
                //查找标记了注解的字段
                .filter(field -> field.isAnnotationPresent(BankAPIField.class))
                //根据注解中的order对字段排序
                .sorted(Comparator.comparingInt(a -> a.getAnnotation(BankAPIField.class).order()))
                //设置可以访问私有字段
                .peek(field -> field.setAccessible(true))
                .forEach(field -> {
                    //获得注解
                    BankAPIField bankAPIField = field.getAnnotation(BankAPIField.class);
                    Object value = "";
                    try {
                        //反射获取字段值
                        value = field.get(api);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    //根据字段类型以正确的填充方式格式化字符串
                    switch (bankAPIField.dateType()) {
                        case "S": {
                            builder.append(String.format("%-" + bankAPIField.length() + "s", value.toString()).replace(' ', '*'));
                            break;
                        }
                        case "N": {
                            builder.append(String.format("%" + bankAPIField.length() + "s", value.toString()).replace(' ', '0'));
                            break;
                        }
                        case "M": {
                            if (!(value instanceof BigDecimal)){
                                throw new RuntimeException(String.format("{} 的 {} 必须是BigDecimal", api, field));
                            }
                            builder.append(String.format("%0" + bankAPIField.length() + "d", ((BigDecimal) value).setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
                            break;
                        }
                        default:
                            break;
                    }
                });

        //加上MD5作为签名
        builder.append(DigestUtils.md2Hex(builder.toString()));
        return HttpRequest.post(url).body(builder.toString()).execute().body();
    }
  1. 最终优化后,创建账户和金额支付的两个方法便简化直接调用apiCall方法即可。
    @Override
    public String createAccount(BankCreateAccount createAccount) {
        return this.apiCall(createAccount);
    }


    @Override
    public String doPay(BankDoPay bankDoPay) {
        return this.apiCall(bankDoPay);
    }

结束语

通过以上的优化,减少了大量的重复代码,其实在日常的业务开发过程中,许多涉及类结构性的通用处理,都可以按照这个模式来减少重复代码,减少代码耦合,提高扩展性。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情