Enum类动态添加枚举

2,104 阅读5分钟

前言

最近在写一个后台流程处理程序,前台通过mxGraph导出一个xml文件,后台读取并解析xml文件。后台配置节点处理逻辑。我把这个流程处理程序单独封装为一个jar包,然后程序里面依赖这个jar包,但是遇到一个问题,jar包中提供了一个抽象的节点,依赖jar包的程序只需要自定义节点然后继承抽象节点即可,程序会自动根据xml的配置去执行相对应的节点逻辑。

但是呢,遇到一个问题,之前写demo的时候是写了一个enum类,自定义的节点都注册写到enum类中,例如(BUTTON("button", ButtonNode.class))解析完xml后,程序根据xml配置的节点名称button映射到enum类中对应的节点ButtonNode,然后通过反射去调用ButtonNode的方法。但是现在将公共部分抽取为一个jar包,enum类自然也写到了jar包中,那么自定义节点在注册的时候肯定不能把jar包解压,然后修改enum类,然后再打包,这样做显然不是很明智。

于是尝试利用反射来动态添加枚举值到enum类中。

一、enum类的反射

enum类和普通类最大的区别就是,enum类自动继承了Enum抽象类,然后enum没有默认的无参构造器。

测试枚举类如下:


public enum TestCalss {

    DELAYED("delayed", TestNode.class);;

    String nodeName;
    Class<? extends Node> nodeClass;

    TestCalss(String nodeName, Class<? extends Node> nodeClass) {
        this.nodeName = nodeName;
        this.nodeClass = nodeClass;
    }

    public String getNodeName() {
        return nodeName;
    }

    public Class<? extends Node> getNodeClass() {
        return nodeClass;
    }

    public static Class<? extends Node> getNodeClassByNodeName(String nodeName) {

        TestCalss nodeTypeEnum = getEnumByNodeName(nodeName);

        return nodeTypeEnum.getNodeClass();
    }

    public static Class<? extends Node> getNodeClassByNode(Node node) {

        String nodeName = node.getName();

        return getNodeClassByNodeName(nodeName);
    }

    public static TestCalss getEnumByNodeName(String nodeName) {
        for (TestCalss anEnum : values()) {
            if (anEnum.getNodeName().equals(nodeName)) {
                return anEnum;
            }
        }
        return null;
    }

}

第一次尝试反射:

反射的代码如下:

Class clazz = TestCalss.class;
Class<?>[] parameterTypes = new Class[2];
parameterTypes[0] = String.class;
parameterTypes[1] = Class.class;

Constructor constructor = clazz.getDeclaredConstructor(parameterTypes);

Object test = constructor.newInstance("test", pers.cz.node.TestNode.class);
System.out.println(test.toString());

然后右击运行:

image.png

提示没有找到方法,明明TestCalss的构造函数和我写的参数个数和参数类型都是对应的,为什么呢?原因在这里,

image.png

父类已经有一个构造器,enum类的特殊之处就是虽然继承了父类,但是没有显示出来,又因为父类已经有一个有参构造器,所以子类必须通过super(name, ordinal)来调用。 写个常规的类,来看下,正常的应该是什么样子的: 父类:

image.png

子类:

image.png

由上面的例子可以知道,TestClass这个enum的真实的构造器应该是TestCalss(name, ordinal,String nodeName, Class<? extends Node> nodeClass)。至于这个真实构造器的参数顺序,这个无所谓,加到前面或者后面都可以。

第二次尝试反射:

Class clazz = TestCalss.class;
Class<?>[] parameterTypes = new Class[4];
parameterTypes[0] = String.class;
parameterTypes[1] = int.class;
parameterTypes[2] = String.class;
parameterTypes[3] = Class.class;
Constructor constructor = clazz.getDeclaredConstructor(parameterTypes);
Object test = constructor.newInstance("test", pers.cz.node.TestNode.class);
System.out.println(test.toString());

报错信息如下:

image.png

很显然这次构造器已经获取到了,但是构造器是private修饰的 解决方法:在实例化之前添加一句,用于修改访问权限

constructor.setAccessible(true);

image.png

但是运行后依然报错:

image.png

原因在这里:

image.png

反射中不允许这样的操作,这是规定,所以直接通过反射来操作枚举并不可以行。所以这也是枚举类适合做单例模式的原因。

但是我百度了一下还真找到了动态添加枚举值的方式。www.cnblogs.com/lhp2012/p/9…

通过反射动态为enum添加枚举值

代码如下:

package pers.cz.utils;

import pers.cz.node.Node;
import pers.cz.node.TestNode;
import sun.reflect.ConstructorAccessor;
import sun.reflect.FieldAccessor;
import sun.reflect.ReflectionFactory;

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @program: jef-flow-spring-boot-starter
 * @description: 动态添加Enum
 * @author: Cheng Zhi
 * @create: 2023-02-07 12:38
 **/
public class DynamicEnumUtil {

    private static ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();

    private static Map<Class<?>, ConstructorAccessor> accessCache;
    static {
        accessCache = new ConcurrentHashMap<Class<?>, ConstructorAccessor>();
    }
    private static void setFailsafeFieldValue(Field field, Object target, Object value) throws NoSuchFieldException,
            IllegalAccessException {

        // let's make the field accessible
        field.setAccessible(true);

        // next we change the modifier in the Field instance to
        // not be final anymore, thus tricking reflection into
        // letting us modify the static final field
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        int modifiers = modifiersField.getInt(field);

        // blank out the final bit in the modifiers int
        modifiers &= ~Modifier.FINAL;
        modifiersField.setInt(field, modifiers);

        FieldAccessor fa = reflectionFactory.newFieldAccessor(field, false);
        fa.set(target, value);
    }

    private static void blankField(Class<?> enumClass, String fieldName) throws NoSuchFieldException,
            IllegalAccessException {
        for (Field field : Class.class.getDeclaredFields()) {
            if (field.getName().contains(fieldName)) {
                AccessibleObject.setAccessible(new Field[] { field }, true);
                setFailsafeFieldValue(field, enumClass, null);
                break;
            }
        }
    }

    private static void cleanEnumCache(Class<?> enumClass) throws NoSuchFieldException, IllegalAccessException {
        blankField(enumClass, "enumConstantDirectory"); // Sun (Oracle?!?) JDK 1.5/6
        blankField(enumClass, "enumConstants"); // IBM JDK
    }

    /**
     * 通过反射生成并获取一个构造函数声明类
     * @param enumClass
     * @param additionalParameterTypes
     * @return
     * @throws NoSuchMethodException
     */
    private static ConstructorAccessor getConstructorAccessor(Class<?> enumClass, Class<?>[] additionalParameterTypes)
            throws NoSuchMethodException {
        // 防止频繁的创建对象,先从缓存中获取。
        ConstructorAccessor constructorAccessor; /*= accessCache.get(enumClass);
        if (constructorAccessor != null) {
            return constructorAccessor;
        }*/
        Class<?>[] parameterTypes = new Class[additionalParameterTypes.length + 2];
        parameterTypes[0] = String.class;
        parameterTypes[1] = int.class;
        System.arraycopy(additionalParameterTypes, 0, parameterTypes, 2, additionalParameterTypes.length);
        //parameterTypes[0] = String.class;
        //parameterTypes[1] = int.class;
        //parameterTypes[2] = String.class;
        //parameterTypes[3] = Class.class;
        Constructor<?> declaredConstructor = enumClass.getDeclaredConstructor(parameterTypes);
        constructorAccessor = reflectionFactory.newConstructorAccessor(declaredConstructor);
        //accessCache.put(enumClass, constructorAccessor);
        return constructorAccessor;
    }

    private static Object makeEnum(Class<?> enumClass, String value, int ordinal, Class<?>[] additionalTypes,
                                   Object[] additionalValues) throws Exception {
        Object[] parms = new Object[additionalValues.length + 2];
        parms[0] = value;
        parms[1] = Integer.valueOf(ordinal);
        System.arraycopy(additionalValues, 0, parms, 2, additionalValues.length);
        Object object = getConstructorAccessor(enumClass, additionalTypes).newInstance(parms);
        return enumClass.cast(object);
    }

    /**
     * 判断枚举是否已存在
     * @param values
     * @param enumName
     * @param <T>
     * @return
     */
    public static <T extends Enum<?>> boolean contains(List<T> values, String enumName){
        for (T value : values) {
            if (value.name().equals(enumName)) {
                return true;
            }
        }
        return false;
    }


    /**
     * Add an enum instance to the enum class given as argument
     *
     * @param <T> the type of the enum (implicit)
     * @param enumType the class of the enum to be modified
     * @param enumName the name of the new enum instance to be added to the class.
     */
    @SuppressWarnings("unchecked")
    public static <T extends Enum<?>> void addEnum(Class<T> enumType, String enumName, Class<?>[] additionalTypes, Object[] additionalValues) {

        // 0. Sanity checks
        if (!Enum.class.isAssignableFrom(enumType)) {
            throw new RuntimeException("class " + enumType + " is not an instance of Enum");
        }

        // 1. Lookup "$VALUES" holder in enum class and get previous enum instances
        Field valuesField = null;
        Field[] fields = enumType.getDeclaredFields();
        for (Field field : fields) {
            if (field.getName().contains("$VALUES")) {
                valuesField = field;
                break;
            }
        }
        AccessibleObject.setAccessible(new Field[] { valuesField }, true);

        try {

            // 2. Copy it
            T[] previousValues = (T[]) valuesField.get(enumType);
            List<T> values = new ArrayList<T>(Arrays.asList(previousValues));

            // 3. build new enum
            T newValue = (T) makeEnum(enumType, enumName, values.size(), additionalTypes, additionalValues);

            if(contains(values,enumName)){
                LogUtils.debug("Enum:" + enumName + " 已存在");
                return;
            }

            // 4. add new value
            values.add(newValue);

            // 5. Set new values field
            setFailsafeFieldValue(valuesField, null, values.toArray((T[]) Array.newInstance(enumType, 0)));

            // 6. Clean enum cache
            cleanEnumCache(enumType);

        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage(), e);
        }
    }

}

总体思想是,通过反射把枚举属性全部取出来,增加一个新的枚举后再放回去。 代码来源:www.niceideas.ch/roller2/bad… 作者原话: The enum instances are stored in a static variable of the enum class named VALUES.Herewesearchforthisstaticvariable,makeitaccessibleandkeepthereferenceforfurtherusage.TheenuminstancescontainedintheVALUES. Here we search for this static variable, make it accessible and keep the reference for further usage. The enum instances contained in the VALUES static variable are copied (stored) in another (new) list. Then the new enum instance is built using another method named makeEnum which will be discussed later. The new enum instance is added to the new enum list. The new enum instance list (containin the new enum instance) is set to the VALUESfield,overwritingthepreviouscontent.Thecleaningofthecacheanswerstosomethingspecialandtricky.TheproblemisthattheVALUES field, overwriting the previous content. The cleaning of the cache answers to something special and tricky. The problem is that the VALUES is purely generated by the compiler. So the static java code located in java.lang.Class cannot be statically linked to it. Hence it needs to access VALUESvariableusingruntimereflectionwhichisalittlecosty(comparedtostaticmethodcalllinking).Hencethecodeinjava.lang.Classwhichneedsanaccesstotheenumvaluesmakesacopyofthemuponfirstusageinaprivateinstancevariable,theenumcacheThus,addingthenewenuminstanceintheVALUES variable using runtime reflection which is a little costy (compared to static method call linking). Hence the code in java.lang.Class which needs an access to the enum values makes a copy of them upon first usage in a private instance variable, the enum cache Thus, adding the new enum instance in the VALUES list is not sufficient, one needs to make sure the java.lang.Class instance caches are cleared as well.

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