学而时习之,不亦说乎
前言
反射 + 注解
需求场景
业务功能需要跟第三方银行对接,需要对接创建账户,支付接口。
请求参数接入规则如下:
- 需要将各请求字段按照固定长度,按规定顺序,拼接成一个大的字符串
- 不同的字段类型,填充固定长度规则不同:
- 针对字符串类型的字段,不满部分用*后置填充。
- 针对数字类型的字段,不满部分用0前置填充
- 针对金额类型的字段,转化以分作为单位,不满部分用0前置填充
- 再针对这个拼接的字符串进行md5加密,作为签名,追加到这个字符串后面
具体接口文档:
| 参数 | 顺序 | 类型 | 长度 |
|---|---|---|---|
| name | 1 | 字符串 | 10 |
| idCard | 2 | 字符串 | 18 |
| age | 3 | 数字 | 3 |
| mobile | 4 | 字符串 | 11 |
| 参数 | 顺序 | 类型 | 长度 |
|---|---|---|---|
| accountNo | 1 | 字符串 | 10 |
| amount | 2 | 金额 | 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();
}
分析以上实现方案,可发现可能存在如下几点问题:
- 相同类型的字段重复的格式化处理,代码上重复。
- 参数顺序,靠拼接次序硬编码决定,容易人为出错。
- 字符串拼接、加签和发请求的逻辑,代码上重复
优化实现
利用注解 + 反射,把重复的代码抽离,将字段的额外信息绑定在注解上,具体实现如下:
- 首先创建两个注解,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 "";
}
- 定义入参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;
}
- 定义核心的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();
}
- 最终优化后,创建账户和金额支付的两个方法便简化直接调用apiCall方法即可。
@Override
public String createAccount(BankCreateAccount createAccount) {
return this.apiCall(createAccount);
}
@Override
public String doPay(BankDoPay bankDoPay) {
return this.apiCall(bankDoPay);
}
结束语
通过以上的优化,减少了大量的重复代码,其实在日常的业务开发过程中,许多涉及类结构性的通用处理,都可以按照这个模式来减少重复代码,减少代码耦合,提高扩展性。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情