How to use reflection gracefully

In my last article, I introduced you to what declarative reflection is and how it is used compared to what is commonly written. This article will introduce the structure and principle of Reflex framework.

Reflex architecture

Without further ado, let’s get straight to the picture above:You can see from the picture aboveReflexIs made up of the coreReflexClassAnd surrounding itField,StaticField,MethodThree parts. These three sections are definitions of structures used to declare and use reflections. The name also tells you about the type of the reflection structure, such as constructor, data type, whether it is static, and so on. inMethodModule right-handMethodParams,MethodReflexParamsIs an annotation used to declare function parameter types, which will be needed for the definition of function-specific reflection structures. The difference between the two is,MethodParamsThe accepted data type is Class<? >, andMethodReflexParamsThe accepted data type is a string with a full descriptor for the type, such as java.util.hashmap.MethodReflexParamsIt is mainly used for some classes that are not exposed by the JDK or SDK and cannot be directly accessed.

The working process

class ReflexDemoMapping {
    public static ReflexInt mId;
    public static ReflexStaticObject<String> TAG;
    public static ReflexMethod<Void> doSomething;
    @MethodParams(value = {String.class, String.class})
    public static ReflexStaticMethod<Void> printLog;

    static{ ReflexClass.load(ReflexDemoMapping.class, ReflexDemo.class); }}Copy the code

Referring to the previous code, ReflexClass is the outermost structure in the entire declaration. For this whole framework to work, it also needs to be initialized from here, layer by layer, inward. The framework is loaded on demand through Static blocks.

Reflexclass.load (Class mappingClass, Class realClass)

    public staticClass<? > load(Class<? > mappingClass, Class<? > realClass) {// Get all declared variables
        Field[] fields = mappingClass.getDeclaredFields();
        for (Field field : fields) {
            try {
                // Check if the variable is decorated as a static variable
                if (Modifier.isStatic(field.getModifiers())) {
                    // The core looks up the reflection structure for the variable from REFLEX_TYPESConstructor<? > constructor = REFLEX_TYPES.get(field.getType());if (null == constructor) {
                        continue;
                    }
                    // The core calls the reflection structure's constructor and assigns values to the fields in the mapping table
                    field.set(null, constructor.newInstance(realClass, field)); }}catch (Exception ignore) {
            }
        }
        return realClass;
    }
Copy the code

REFLEX_TYPES loads all Reflex structure types supported by Reflex.

private staticHashMap<Class<? >, Constructor<? >> REFLEX_TYPES =new HashMap<>();
    static{ REFLEX_TYPES.put(ReflexObject.class, ReflexObject.class.getConstructor(Class.class, Field.class)); .// Other types
            REFLEX_TYPES.put(ReflexStaticObject.class, 
						...// Other static typesREFLEX_TYPES.put(ReflexMethod.class, ReflexMethod.class.getConstructor(Class.class, Field.class)); .// Other function types
    }
Copy the code

To sum up: the load function mainly deals with finding reflection structures that need to be initialized in mappingClass (such as public static ReflexInt mId). It limits the search scope to static member variables and types that must be supported by Reflex, and assigns values to found structure variables.

Field reflection structure

For each reflection structure, there is really only one question: which field or function in the reflection target class does the variable declared in the mapping table correspond to? To solve this problem, we chose to force variables declared in the mapping class to be the same as variables or function names in the actual class, which avoids extra arguments and makes the mapping class more readable and has a higher density of information per line.

With that out of the way, let’s take a look at the interior of the reflection structure by referring to ReflexByte.

abstract class BaseField {
    protected Field mField;
    public BaseField(Class
        cls, Field field) throws NoSuchFieldException {
        mField = cls.getDeclaredField(field.getName());
        mField.setAccessible(true); }}Copy the code

In the parent class according to the field name in the mapping table to find the corresponding field in the actual class and modify its accessibility.

public final class ReflexByte extends BaseField{

    public ReflexByte(Class
        cls, Field field) throws NoSuchFieldException {
        super(cls, field);
    }

    public byte get(Object object) {
        try {
            return mField.getByte(object);
        } catch (Exception ignore) {
            return (byte) 0; }}public void set(Object obj, byte value) {
        try {
            mField.setByte(obj, value);
        } catch (Exception ignore) {
        }
    }
}
Copy the code

Since each reflection structure represents a different meaning, the operation logic of each type is decomposed into each reflection mechanism to make it more cohesive for the corresponding type of operation and processing.

Let’s look at the internal implementation of static variable structures:

abstract class BaseStaticField extends BaseField{

    public BaseStaticField(Class
        cls, Field field) throws NoSuchFieldException {
        super(cls, field);
        checkIsStatic(cls, mField);
    }

    protected void checkIsStatic(Class
        cls, Field field){
        if(! Modifier.isStatic(field.getModifiers())) {throw newNotStaticException(cls, field); }}}Copy the code

As always, the parent class for variable lookup and accessibility modification, the only difference is the static structure added static type detection.

public final class ReflexStaticByte extends BaseStaticField {

    public ReflexStaticByte(Class
        cls, Field field) throws NoSuchFieldException {
        super(cls, field);
    }

    public byte get(a) {
        try {
            return mField.getByte(null);
        } catch (Exception ignore) {
            return (byte) 0; }}public void set(byte value) {
        try {
            mField.setByte(null, value);
        } catch (Exception ignore) {
        }
    }
}
Copy the code

Because static variables can be reflected without passing in the target object, the get and set functions are simplified here. Other types of reflection structures are similar, with only minor internal differences for different types of operations that will not be covered here.

Method reflecting mechanism

Unlike the reflection mechanism of a variable, the reflection mechanism of a function needs to pay attention to three things:

  1. The function name
  2. Function into the reference
  3. Function the participation

To make it easier to use, function names can be obtained from the names of variables in the mapping class, function input parameters are declared on variables in the mapping table using the @methodParams or @methodreFlexParams annotation, and output parameters are defined by the generic

.

 protected Method mMethod;

   public BaseMethod(Class
        cls, Field field) throws NoSuchMethodException {
       if (field.isAnnotationPresent(MethodParams.class)) {
       		// Handle common type parameters
           mMethod = handleWithParams(cls, field);
       } else if (field.isAnnotationPresent(MethodReflexParams.class)) {
       		// Handle full descriptor arguments, in which case the parameter type is also considered to reflect some concrete structure
           mMethod = handleWithReflexParams(cls, field);
       } else {
       		// handle functions without arguments
           mMethod = handleWithNoParams(cls, field);
       }
       if (null == mMethod) {
           throw new NoSuchMethodException("Not find [" + field.getName() + "] function in [" + cls.getName() + "]");
       }
       mMethod.setAccessible(true);
   }
Copy the code

And provides a simple function operation

  @SuppressWarnings("unchecked")
    public RESULT call(Object receiver, Object... args) {
        try {
            return (RESULT) mMethod.invoke(receiver, args);
        }  catch (Throwable e) {
            e.printStackTrace();
        }
        return null;
    }
Copy the code

The function is executed without much validation, and the return value is not checked for consistency with the generic. If the return value is inconsistent with the declared generic, it is considered an error in mapping the class declaration and should be fixed by the developer, not checked by framework interceptors.

conclusion

Reflex does not have a complex process. It mainly uses the JVM’s loading mechanism for static variables and blocks, as well as the single principle, open and close principle, Demir principle and template mode. Through simple encapsulation, reflection API is fully cohesive, users only need to define and declare the mapping relationship, through the mapping table to communicate operations more clearly, improve the information content of a single line of code. GitHub: project source code

Elegance never goes out of style — Camille