Author: Leilei Chen
Link: https://llchen60.com/ Eliminate duplicate code with annotation reflection/
1.1 case scenario
Assuming that the bank provides some API interfaces, the serialization of parameters is a little special. Instead of using JSON, we need to put the parameters together in turn to form a large string:
1) According to the order of API documents provided by the bank, all parameters form fixed length data and are spliced together as an entire string
2) Because each parameter has a fixed length, it needs to be filled if the length is not reached
-
If the length of the string type parameter is less than the length, it shall be filled with the following line right, that is, the string content shall be left
-
The length part of the parameter of the number type is filled with 0 to the left, that is, the actual number is right
-
For the representation of currency type, the amount needs to be rounded down to 2 digits to minutes, and the unit is divided into minutes. It is also filled in as a number type
-
Parameter MD5 operation as signature
1.2 preliminary code implementation
public class BankService { //Create user method public static String createUser(String name, String identity, String mobile, int age) throws IOException { StringBuilder stringBuilder = new StringBuilder(); //The string is left, and the extra space is filled_ stringBuilder.append(String.format("%-10s", name).replace(' ', '_')); //The string is left, and the extra space is filled_ stringBuilder.append(String.format("%-18s", identity).replace(' ', '_')); //The number is to the right, and the excess is filled with 0 stringBuilder.append(String.format("%05d", age)); //String to the left, extra places to use_ fill stringBuilder.append(String.format("%-11s", mobile).replace(' ', '_')); //Finally, add MD5 as the signature stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString())); return Request.Post("http://localhost:45678/reflection/bank/createUser") .bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON) .execute().returnContent().asString(); } //Payment method public static String pay(long userId, BigDecimal amount) throws IOException { StringBuilder stringBuilder = new StringBuilder(); //The number is to the right, and the excess is filled with 0 stringBuilder.append(String.format("%020d", userId)); //The amount shall be rounded down to 2 digits to minutes, which shall be taken as the number to the right, and the redundant places shall be filled with 0 stringBuilder.append(String.format("%010d", amount.setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue())); //Finally, add MD5 as the signature stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString())); return Request.Post("http://localhost:45678/reflection/bank/pay") .bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON) .execute().returnContent().asString(); } }
This can basically meet the needs, but there are some problems:
-
The processing logic repeats with each other, and a Bug will appear if you are careless
-
The logic of string splicing, tagging and request sending in the processing flow is repeated in all methods
-
The parameter type and order of the input parameters of the actual method are not necessarily consistent with the interface requirements and are prone to error
-
Code level parameters are hard coded and cannot be clearly checked
1.3 optimizing code using interfaces and reflection
1.3.1 implement the POJO class that defines all interface parameters
@Data public class CreateUserAPI { private String name; private String identity; private String mobile; private int age; }
1.3.2 definition annotation itself
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Inherited public @interface BankAPI { String desc() default ""; String url() default ""; } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented @Inherited public @interface BankAPIField { int order() default -1; int length() default -1; String type() default ""; }
1.3.3 reflection matching annotation to realize dynamic interface parameter assembly
private static String remoteCall(AbstractAPI api) throws IOException { //Get the request address from the BankAPI annotation BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class); bankAPI.url(); StringBuilder stringBuilder = new StringBuilder(); Arrays.stream(api.getClass().getDeclaredFields()) //Get all fields .filter(field -> field.isAnnotationPresent(BankAPIField.class)) //Find fields marked with annotations .sorted(Comparator.comparingInt(a -> a.getAnnotation(BankAPIField.class).order())) //Sort the fields according to the order in the annotation .peek(field -> field.setAccessible(true)) //Set private fields that can be accessed .forEach(field -> { //Get comments BankAPIField bankAPIField = field.getAnnotation(BankAPIField.class); Object value = ""; try { //Get field value by reflection value = field.get(api); } catch (IllegalAccessException e) { e.printStackTrace(); } //Format the string with the correct padding based on the field type switch (bankAPIField.type()) { case "S": { stringBuilder.append(String.format("%-" + bankAPIField.length() + "s", value.toString()).replace(' ', '_')); break; } case "N": { stringBuilder.append(String.format("%" + bankAPIField.length() + "s", value.toString()).replace(' ', '0')); break; } case "M": { if (!(value instanceof BigDecimal)) throw new RuntimeException(String.format("{} of {} Must be BigDecimal", api, field)); stringBuilder.append(String.format("%0" + bankAPIField.length() + "d", ((BigDecimal) value).setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue())); break; } default: break; } }); //Signature logic stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString())); String param = stringBuilder.toString(); long begin = System.currentTimeMillis(); //Send request String result = Request.Post("http://localhost:45678/reflection" + bankAPI.url()) .bodyString(param, ContentType.APPLICATION_JSON) .execute().returnContent().asString(); log.info("Call bank API {} url:{} parameter:{} time consuming:{}ms", bankAPI.desc(), bankAPI.url(), param, System.currentTimeMillis() - begin); return result; }
Dynamically obtain class information through reflection, and complete the assembly process at runtime.
The advantage of this is that the development will be much more convenient and intuitive, and then the logic and details will be hidden and concentrated in one method to reduce repetition and the occurrence of bug s in maintenance.
1.3.4 application in code
@BankAPI(url = "/bank/createUser", desc = "Create user interface") @Data public class CreateUserAPI extends AbstractAPI { @BankAPIField(order = 1, type = "S", length = 10) private String name; @BankAPIField(order = 2, type = "S", length = 18) private String identity; @BankAPIField(order = 4, type = "S", length = 11) //Note that the order here needs to follow the order in the API table private String mobile; @BankAPIField(order = 3, type = "N", length = 5) private int age; } @BankAPI(url = "/bank/pay", desc = "Payment interface") @Data public class PayAPI extends AbstractAPI { @BankAPIField(order = 1, type = "N", length = 20) private long userId; @BankAPIField(order = 2, type = "M", length = 10) private BigDecimal amount; }