zoukankan      html  css  js  c++  java
  • Mybatis源码学习之反射工具(三)

    简述

    MyBatis在进行参数处理、结果映射等操作时,会涉及大量的反射操作。Java中的反射虽然功能强大,但是代码编写起来比较复杂且容易出错,为了简化反射操作的相关代码,MyBatis提供了专门的反射模块,该模块位于org.apache.ibatis.reflection包中,它对常见的反射操作做了进一步封装,提供了更加简洁方便的反射API。

    Reflector & ReflectorFactory

    Reflector

    Reflector是MyBatis中反射模块的基础,每个Reflector对象都对应一个类,在Reflector中缓存了反射操作需要使用的类的元信息。Reflector 中各个字段的含义如下:

    
    /**
     * 缓存了反射操作需要使用的类的元信息。
     * 允许在属性名和getter/setter方法之间轻松映射
     *
     * @author Clinton Begin
     */
    public class Reflector {
    
        /**
         * 对应Class类型
         */
        private final Class<?> type;
        /**
         * 可读属性的名称集合,可读属性就是存在相应getter方法的属性,初始值为空数组
         */
        private final String[] readablePropertyNames;
        /**
         * 可写属性的名称集合,可写属性就是存在相应setter方法的属性,初始值为空数组
         */
        private final String[] writeablePropertyNames;
        /**
         * 记录了属性相应的setter方法,key是属性名称,value是Invoker对象,它是对setter方法对应Method对象的封装,
         */
        private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
        /**
         * 属性相应的getter方法集合,key是属性名称,value也是Invoker对象
         * <p>
         */
        private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
        /**
         * 记录了属性相应的setter方法的参数值类型,key是属性名称,value是setter方法的参数类型
         */
        private final Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
        /**
         * 记录了属性相应的getter方法的返回值类型,key是属性名称,value是getter方法的返回值类型
         */
        private final Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
        /**
         * 记录了默认构造方法
         */
        private Constructor<?> defaultConstructor;
        /**
         * 记录了所有属性名称集合
         */
        private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();
    
        /**
         * 构造函数,对上述字段初始化
         */
        public Reflector(Class<?> clazz) {
            type = clazz;
            addDefaultConstructor(clazz);
            addGetMethods(clazz);
            addSetMethods(clazz);
            addFields(clazz);
            readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
            writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
            for (String propName : readablePropertyNames) {
                caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
            }
            for (String propName : writeablePropertyNames) {
                caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
            }
        }
    
        /**
         * 获取指定Class对象的默认构造方法(包括无参构造方法)
         */
        private void addDefaultConstructor(Class<?> clazz) {
            Constructor<?>[] consts = clazz.getDeclaredConstructors();
            for (Constructor<?> constructor : consts) {
                //默认构造方法没有参数
                if (constructor.getParameterTypes().length == 0) {
                    //允许利用反射检查任意类的私有变量
                    if (canAccessPrivateMethods()) {
                        try {
                            constructor.setAccessible(true);
                        } catch (Exception e) {
                            // Ignored. This is only a final precaution, nothing we can do.
                        }
                    }
                    if (constructor.isAccessible()) {
                        this.defaultConstructor = constructor;
                    }
                }
            }
        }
    
        /**
         * 负责解析类中定义的getter方法
         */
        private void addGetMethods(Class<?> cls) {
            Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>();
            Method[] methods = getClassMethods(cls);
            for (Method method : methods) {
                if (method.getParameterTypes().length > 0) {
                    continue;
                }
                String name = method.getName();
                if ((name.startsWith("get") && name.length() > 3)
                        || (name.startsWith("is") && name.length() > 2)) {
                    //获取字段名
                    name = PropertyNamer.methodToProperty(name);
                    addMethodConflict(conflictingGetters, name, method);
                }
            }
            resolveGetterConflicts(conflictingGetters);
        }
    
        private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
            //遍历conflictingGetters集合
            for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
                Method winner = null;
                //获取属性名
                String propName = entry.getKey();
                for (Method candidate : entry.getValue()) {
                    if (winner == null) {
                        winner = candidate;
                        continue;
                    }
                    Class<?> winnerType = winner.getReturnType();
                    Class<?> candidateType = candidate.getReturnType();
                    if (candidateType.equals(winnerType)) {
                        if (!boolean.class.equals(candidateType)) {
                            throw new ReflectionException(
                                    "Illegal overloaded getter method with ambiguous type for property "
                                            + propName + " in class " + winner.getDeclaringClass()
                                            + ". This breaks the JavaBeans specification and can cause unpredictable results.");
                        } else if (candidate.getName().startsWith("is")) {
                            winner = candidate;
                        }
                    } else if (candidateType.isAssignableFrom(winnerType)) {
                        // OK getter type is descendant
                    } else if (winnerType.isAssignableFrom(candidateType)) {
                        winner = candidate;
                    } else {
                        //返回值相同,存在二义性,抛出异常
                        throw new ReflectionException(
                                "Illegal overloaded getter method with ambiguous type for property "
                                        + propName + " in class " + winner.getDeclaringClass()
                                        + ". This breaks the JavaBeans specification and can cause unpredictable results.");
                    }
                }
                addGetMethod(propName, winner);
            }
        }
    
        /**
         * 收集get方法
         */
        private void addGetMethod(String name, Method method) {
            if (isValidPropertyName(name)) {
                getMethods.put(name, new MethodInvoker(method));
                Type returnType = TypeParameterResolver.resolveReturnType(method, type);
                getTypes.put(name, typeToClass(returnType));
            }
        }
    
        /**
         * 收集set方法
         */
        private void addSetMethods(Class<?> cls) {
            Map<String, List<Method>> conflictingSetters = new HashMap<String, List<Method>>();
            Method[] methods = getClassMethods(cls);
            for (Method method : methods) {
                String name = method.getName();
                if (name.startsWith("set") && name.length() > 3) {
                    if (method.getParameterTypes().length == 1) {
                        name = PropertyNamer.methodToProperty(name);
                        addMethodConflict(conflictingSetters, name, method);
                    }
                }
            }
            resolveSetterConflicts(conflictingSetters);
        }
    
        private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
            List<Method> list = conflictingMethods.get(name);
            if (list == null) {
                list = new ArrayList<Method>();
                conflictingMethods.put(name, list);
            }
            list.add(method);
        }
    
        private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
            for (String propName : conflictingSetters.keySet()) {
                List<Method> setters = conflictingSetters.get(propName);
                Class<?> getterType = getTypes.get(propName);
                Method match = null;
                ReflectionException exception = null;
                for (Method setter : setters) {
                    Class<?> paramType = setter.getParameterTypes()[0];
                    if (paramType.equals(getterType)) {
                        // should be the best match
                        match = setter;
                        break;
                    }
                    if (exception == null) {
                        try {
                            match = pickBetterSetter(match, setter, propName);
                        } catch (ReflectionException e) {
                            // there could still be the 'best match'
                            match = null;
                            exception = e;
                        }
                    }
                }
                if (match == null) {
                    throw exception;
                } else {
                    addSetMethod(propName, match);
                }
            }
        }
    
        private Method pickBetterSetter(Method setter1, Method setter2, String property) {
            if (setter1 == null) {
                return setter2;
            }
            Class<?> paramType1 = setter1.getParameterTypes()[0];
            Class<?> paramType2 = setter2.getParameterTypes()[0];
            if (paramType1.isAssignableFrom(paramType2)) {
                return setter2;
            } else if (paramType2.isAssignableFrom(paramType1)) {
                return setter1;
            }
            throw new ReflectionException("Ambiguous setters defined for property '" + property + "' in class '"
                    + setter2.getDeclaringClass() + "' with types '" + paramType1.getName() + "' and '"
                    + paramType2.getName() + "'.");
        }
    
        private void addSetMethod(String name, Method method) {
            if (isValidPropertyName(name)) {
                setMethods.put(name, new MethodInvoker(method));
                Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
                setTypes.put(name, typeToClass(paramTypes[0]));
            }
        }
    
        private Class<?> typeToClass(Type src) {
            Class<?> result = null;
            if (src instanceof Class) {
                result = (Class<?>) src;
            } else if (src instanceof ParameterizedType) {
                result = (Class<?>) ((ParameterizedType) src).getRawType();
            } else if (src instanceof GenericArrayType) {
                Type componentType = ((GenericArrayType) src).getGenericComponentType();
                if (componentType instanceof Class) {
                    result = Array.newInstance((Class<?>) componentType, 0).getClass();
                } else {
                    Class<?> componentClass = typeToClass(componentType);
                    result = Array.newInstance((Class<?>) componentClass, 0).getClass();
                }
            }
            if (result == null) {
                result = Object.class;
            }
            return result;
        }
    
        private void addFields(Class<?> clazz) {
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                if (canAccessPrivateMethods()) {
                    try {
                        field.setAccessible(true);
                    } catch (Exception e) {
                        // Ignored. This is only a final precaution, nothing we can do.
                    }
                }
                if (field.isAccessible()) {
                    if (!setMethods.containsKey(field.getName())) {
                        // issue #379 - removed the check for final because JDK 1.5 allows
                        // modification of final fields through reflection (JSR-133). (JGB)
                        // pr #16 - final static can only be set by the classloader
                        int modifiers = field.getModifiers();
                        if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) {
                            addSetField(field);
                        }
                    }
                    if (!getMethods.containsKey(field.getName())) {
                        addGetField(field);
                    }
                }
            }
            if (clazz.getSuperclass() != null) {
                addFields(clazz.getSuperclass());
            }
        }
    
        private void addSetField(Field field) {
            if (isValidPropertyName(field.getName())) {
                setMethods.put(field.getName(), new SetFieldInvoker(field));
                Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
                setTypes.put(field.getName(), typeToClass(fieldType));
            }
        }
    
        private void addGetField(Field field) {
            if (isValidPropertyName(field.getName())) {
                getMethods.put(field.getName(), new GetFieldInvoker(field));
                Type fieldType = TypeParameterResolver.resolveFieldType(field, type);
                getTypes.put(field.getName(), typeToClass(fieldType));
            }
        }
    
        private boolean isValidPropertyName(String name) {
            return !(name.startsWith("$") || "serialVersionUID".equals(name) || "class".equals(name));
        }
    
        /*
         * 此方法返回一个数组,该数组包含该类中声明的所有方法和任何超类
         * 我们使用此方法不是为了代替 Class.getMethods(),
         * 因为我们想访问类中的私有方法.
         *
         * @param cls Class对象
         * @return 包含该类中所有方法的数组
         */
        private Method[] getClassMethods(Class<?> cls) {
            //用于记录指定类中定义的全部方法的唯一签名以及对应的Method对象
            Map<String, Method> uniqueMethods = new HashMap<String, Method>();
            Class<?> currentClass = cls;
            while (currentClass != null) {
                //记录当前类中定义的所有方法
                addUniqueMethods(uniqueMethods, currentClass.getDeclaredMethods());
    
                // 记录接口中定义的方法
                Class<?>[] interfaces = currentClass.getInterfaces();
                for (Class<?> anInterface : interfaces) {
                    addUniqueMethods(uniqueMethods, anInterface.getMethods());
                }
                //当前类的父类
                currentClass = currentClass.getSuperclass();
            }
    
            Collection<Method> methods = uniqueMethods.values();
            //转换成数组返回
            return methods.toArray(new Method[methods.size()]);
        }
    
        /**
         * 为每个方法生成唯一签名,并记录到uniqueMethods集合中
         */
        private void addUniqueMethods(Map<String, Method> uniqueMethods, Method[] methods) {
            for (Method currentMethod : methods) {
                if (!currentMethod.isBridge()) {
                    //得到方法签名
                    String signature = getSignature(currentMethod);
                    //根据方法签名排重
                    if (!uniqueMethods.containsKey(signature)) {
                        if (canAccessPrivateMethods()) {
                            try {
                                currentMethod.setAccessible(true);
                            } catch (Exception e) {
                                // Ignored. This is only a final precaution, nothing we can do.
                            }
                        }
                        //记录签名与方法的对应关系
                        uniqueMethods.put(signature, currentMethod);
                    }
                }
            }
        }
    
        /**
         * 获取方法签名,eg:java.lang.String#getSignature:java.lang.reflect.Method
         */
        private String getSignature(Method method) {
            StringBuilder sb = new StringBuilder();
            //方法返回类型
            Class<?> returnType = method.getReturnType();
            if (returnType != null) {
                sb.append(returnType.getName()).append('#');
            }
            sb.append(method.getName());
            Class<?>[] parameters = method.getParameterTypes();
            for (int i = 0; i < parameters.length; i++) {
                if (i == 0) {
                    sb.append(':');
                } else {
                    sb.append(',');
                }
                sb.append(parameters[i].getName());
            }
            return sb.toString();
        }
    
        /**
         * 获取访问私有方法的权限
         */
        private static boolean canAccessPrivateMethods() {
            try {
                SecurityManager securityManager = System.getSecurityManager();
                if (null != securityManager) {
                    //允许利用反射检查任意类的私有变量
                    securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));
                }
            } catch (SecurityException e) {
                return false;
            }
            return true;
        }
    
        /*
         * Gets the name of the class the instance provides information for
         *
         * @return The class name
         */
        public Class<?> getType() {
            return type;
        }
    
        public Constructor<?> getDefaultConstructor() {
            if (defaultConstructor != null) {
                return defaultConstructor;
            } else {
                throw new ReflectionException("There is no default constructor for " + type);
            }
        }
    
        public boolean hasDefaultConstructor() {
            return defaultConstructor != null;
        }
    
        public Invoker getSetInvoker(String propertyName) {
            Invoker method = setMethods.get(propertyName);
            if (method == null) {
                throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
            }
            return method;
        }
    
        public Invoker getGetInvoker(String propertyName) {
            Invoker method = getMethods.get(propertyName);
            if (method == null) {
                throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
            }
            return method;
        }
    
        /*
         * Gets the type for a property setter
         *
         * @param propertyName - the name of the property
         * @return The Class of the propery setter
         */
        public Class<?> getSetterType(String propertyName) {
            Class<?> clazz = setTypes.get(propertyName);
            if (clazz == null) {
                throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
            }
            return clazz;
        }
    
        /*
         * Gets the type for a property getter
         *
         * @param propertyName - the name of the property
         * @return The Class of the propery getter
         */
        public Class<?> getGetterType(String propertyName) {
            Class<?> clazz = getTypes.get(propertyName);
            if (clazz == null) {
                throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
            }
            return clazz;
        }
    
        /*
         * Gets an array of the readable properties for an object
         *
         * @return The array
         */
        public String[] getGetablePropertyNames() {
            return readablePropertyNames;
        }
    
        /*
         * Gets an array of the writeable properties for an object
         *
         * @return The array
         */
        public String[] getSetablePropertyNames() {
            return writeablePropertyNames;
        }
    
        /*
         * Check to see if a class has a writeable property by name
         *
         * @param propertyName - the name of the property to check
         * @return True if the object has a writeable property by the name
         */
        public boolean hasSetter(String propertyName) {
            return setMethods.keySet().contains(propertyName);
        }
    
        /*
         * Check to see if a class has a readable property by name
         *
         * @param propertyName - the name of the property to check
         * @return True if the object has a readable property by the name
         */
        public boolean hasGetter(String propertyName) {
            return getMethods.keySet().contains(propertyName);
        }
    
        public String findPropertyName(String name) {
            return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
        }
    }
    

    Reflector.addGetMethods()方法主要负责解析类中定义的getter方法,Reflector.addSetMethods()方法负责解析类中定义的setter方法,两者的逻辑类似,具体实现见源码。

    ReflectorFactory接口

    ReflectorFactory接口主要实现了对Reflector对象的创建和缓存,该接口定义如下:

    public interface ReflectorFactory {
    
        /**
         * 是否会缓存Reflector对象
         */
        boolean isClassCacheEnabled();
    
        /**
         * 设置是否缓存Reflector对象
         */
        void setClassCacheEnabled(boolean classCacheEnabled);
    
        /**
         * 创建指定Class的Reflector对象
         */
        Reflector findForClass(Class<?> type);
    }

    MyBatis只为该接口提供了DefaultReflectorFactory这一个实现类,它与Reflector的关系如下:

    image

    DefaultReflectorFactory各字段及方法的含义如下:

    public class DefaultReflectorFactory implements ReflectorFactory {
        /**
         * 默认开启对Reflector对象的缓存
         */
        private boolean classCacheEnabled = true;
    
        /**
         * 使用集合ConcurrentHashMap实现对Reflector的缓存
         */
        private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<Class<?>, Reflector>();
    
        public DefaultReflectorFactory() {
        }
    
        @Override
        public boolean isClassCacheEnabled() {
            return classCacheEnabled;
        }
    
        @Override
        public void setClassCacheEnabled(boolean classCacheEnabled) {
            this.classCacheEnabled = classCacheEnabled;
        }
    
        /**
         * 为指定的Class创建Reflector对象,并将Reflector对象缓存到reflectorMap中
         */
        @Override
        public Reflector findForClass(Class<?> type) {
            //检查是否开启缓存
            if (classCacheEnabled) {
                // synchronized (type) removed see issue #461
                Reflector cached = reflectorMap.get(type);
                if (cached == null) {
                    //创建Reflector对象
                    cached = new Reflector(type);
                    //放入ConcurrentHashMap集合缓存
                    reflectorMap.put(type, cached);
                }
                return cached;
            } else {
                //没有开启缓存,直接创建Reflector对象并返回
                return new Reflector(type);
            }
        }
    
    }

    除了使用MyBatis提供的DefaultReflectorFactory实现,我们还可以在mybatis-config.xml中配置自定义的ReflectorFactory实现类,从而实现功能上的扩展。

    TypeParameterResolver

    TypeParameterResolver是一个工具类,提供了一系列静态方法来解析指定类中的字段、方法返回值或方法参数的类型。

    在开始介绍TypeParameterResolver之前,先简单介绍一下Type接口的基础知识。Type是所有类型的父接口,它有四个子接口和一个实现类,

    image

    1、Class比较常见,它表示的是原始类型。

    Class类的对象表示JVM中的一个类或接口,每个Java类在JVM里都表现为一个Class对象。在程序中可以通过“类名.class”、“对象.getClass()”或是“Class.forName(”类名”)”等方式获取Class对象。数组也被映射为Class 对象,所有元素类型相同且维数相同的数组都共享同一个 Class 对象。

    2、ParameterizedType表示的是参数化类型

    例如List<String>、Map<Integer,String>、Service<User>这种带有泛型的类型。

    ParameterizedType接口中常用的方法有三个,分别是:

    Type getRawType()—返回参数化类型中的原始类型,例如ListString>的原始类型为List。 
    
    Type[] getActualTypeArguments()—获取参数化类型的类型变量或是实际类型列表,例如MapInteger, String>的实际泛型列表IntegerString。需要注意的是,该列表的元素类型都是Type,也就是说,可能存在多层嵌套的情况。 
    
    Type getOwnerType()—返回是类型所属的类型,例如存在A<T>类,其中定义了内部类InnerA<I>,则InnerA<I>所属的类型为A<T>,如果是顶层类型则返回null。这种关系比较常见的示例是MapK,V>接口与Map.Entry<K,V>接口,MapK,V>接口是Map.Entry<K,V>接口的所有者。 · TypeVariable表示的是类型变量,它用来反映在JVM编译该泛型前的信息。例如List<T>中的T就是类型变量,它在编译时需被转换为一个具体的类型后才能正常使用。

    TypeParameterResolver中各个静态方法之间的调用关系大致如图所示

    image

    TypeParameterResolver中通过resolveFieldType()方法、resolveReturnType()方法、resolveParamTypes()方法分别解析字段类型、方法返回值类型和方法参数列表中各个参数的类型。具体实现可查看其源码及官方的单元测试方法。

    ObjectFactory接口

    MyBatis中有很多模块会使用到ObjectFactory接口,该接口提供了多个create()方法的重载,通过这些create()方法可以创建指定类型的对象。ObjectFactory接口的定义如下:

    /**
     * Mybatis 使用ObjectFactory去创建需要的对象
     * @author Clinton Begin
     */
    public interface ObjectFactory {
    
        /**
         * 设置配置信息
         * @param properties 配置信息
         */
        void setProperties(Properties properties);
    
        /**
         * 通过默认构造函数创建指定类的对象
         * @param type 对象类型
         * @return
         */
        <T> T create(Class<T> type);
    
        /**
         * 根据参数列表,从指定类型中选择合适的构造器创建对象
         * @param type 对象类型
         * @param constructorArgTypes 参数类型
         * @param constructorArgs 参数值
         * @return
         */
        <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);
    
        /**
         * 检测指定类型是否是集合类型
         * @param type 对象类型
         * @return 集合类型返回true否则返回false
         * @since 3.1.0
         */
        <T> boolean isCollection(Class<T> type);
    
    }
    

    DefaultObjectFactory是MyBatis提供的ObjectFactory接口的唯一实现,它是一个反射工厂,其create()方法通过调用instantiateClass()方法实现。DefaultObjectFactory.instantiateClass()方法会根据传入传入的参数列表选择合适的构造函数实例化对象,具体实现如下:

    private  <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        try {
          Constructor<T> constructor;
          //通过无参构造函数创建对象
          if (constructorArgTypes == null || constructorArgs == null) {
            constructor = type.getDeclaredConstructor();
            if (!constructor.isAccessible()) {
              constructor.setAccessible(true);
            }
            return constructor.newInstance();
          }
          //根据参数列表查找合适的构造函数,并实例化对象
          constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[constructorArgTypes.size()]));
          if (!constructor.isAccessible()) {
            constructor.setAccessible(true);
          }
          return constructor.newInstance(constructorArgs.toArray(new Object[constructorArgs.size()]));
        } catch (Exception e) {
          StringBuilder argTypes = new StringBuilder();
          if (constructorArgTypes != null && !constructorArgTypes.isEmpty()) {
            for (Class<?> argType : constructorArgTypes) {
              argTypes.append(argType.getSimpleName());
              argTypes.append(",");
            }
            argTypes.deleteCharAt(argTypes.length() - 1); // remove trailing ,
          }
          StringBuilder argValues = new StringBuilder();
          if (constructorArgs != null && !constructorArgs.isEmpty()) {
            for (Object argValue : constructorArgs) {
              argValues.append(String.valueOf(argValue));
              argValues.append(",");
            }
            argValues.deleteCharAt(argValues.length() - 1); // remove trailing ,
          }
          throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + e, e);
        }
      }

    除了使用MyBatis提供的DefaultObjectFactory实现,我们还可以在mybatis-config.xml配置文件中指定自定义的ObjectFactory接口实现类,从而实现功能上的扩展。

  • 相关阅读:
    DjangoRestFramework学习二之序列化组件、视图组件 serializer modelserializer
    DjangoRestFramework 学习之restful规范 APIview 解析器组件 Postman等
    vue学习目录 vue初识 this指向问题 vue组件传值 过滤器 钩子函数 路由 全家桶 脚手架 vuecli element-ui axios bus
    vue框架的搭建 安装vue/cli 脚手架 安装node.js 安装cnpm
    Redis集合 安装 哨兵集群 安全配置 redis持久化
    linux vue uwsgi nginx 部署路飞学城 安装 vue
    爬虫第三部分综合案例
    creating server tcp listening socket 127.0.0.1:6379: bind No error
    爬虫第二部分
    外键为什么加上索引?
  • 原文地址:https://www.cnblogs.com/liukaifeng/p/10052623.html
Copyright © 2011-2022 走看看