Eliminate duplicate code with annotation + reflection

Keywords: Java RESTful

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 part of the parameter length of the number type that is less than the length is filled with 0 to the left, that is, the actual number is right. For the representation of the currency type, the amount needs to be rounded down to 2 digits to minutes, which is in minutes. As a number type, it is also filled with 0 to the left

         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. In addition, the java series interview questions and answers are all sorted out. Wechat searches the Java technology stack and sends them in the background: the interview can be read online.

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.

The latest Java core technology tutorial and actual source code: https://github.com/javastacks/javastack

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;
}

Posted by bobvaz on Thu, 07 Oct 2021 13:26:35 -0700