zoukankan      html  css  js  c++  java
  • mybatis源码分析(二)------------配置文件的解析

    这篇文章中,我们将讲解配置文件中 properties,typeAliases,settings和environments这些节点的解析过程。

    一 properties的解析

     private void propertiesElement(XNode context) throws Exception {
        if (context != null) {
    //解析properties的子节点,并将这些子节点内容转为属性对象Properties Properties defaults
    = context.getChildrenAsProperties();
    //获取properties节点中resource的属性值 String resource
    = context.getStringAttribute("resource");
    //获取properties节点中url的属性值 String url
    = context.getStringAttribute("url");
    //resource和url不能同时存在
    if (resource != null && url != null) { throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); } if (resource != null) {
    // 从文件系统中加载并解析属性文件,此时会覆盖在xml中配置的properties子节点的同名key-value defaults.putAll(Resources.getResourceAsProperties(resource)); }
    else if (url != null) {
    // 从url中解析并加载属性文件 defaults.putAll(Resources.getUrlAsProperties(url)); }
    // 获取configuration中已经定义的属性对象Properties Properties vars
    = configuration.getVariables(); if (vars != null) {
    // configuration中的key-value会覆盖上面两种情况中的key-value defaults.putAll(vars); }
    // 将解析出的内容set到parser中 parser.setVariables(defaults);
    // 将解析出的内容set到configuration中,configuration会装载所解析的配置文件中所有的节点内容,后面会使用到这个对象 configuration.setVariables(defaults); } }

     二 settings的解析

    先看下settings的配置,下面只是settings配置中的一部分:

    <!-- settings是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 -->
        <settings>
            <!-- 该配置影响的所有映射器中配置的缓存的全局开关。默认值true -->
            <setting name="cacheEnabled" value="true"/>
            <!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。默认值false  -->
            <setting name="lazyLoadingEnabled" value="true"/>
            <!-- 是否允许单一语句返回多结果集(需要兼容驱动)。 默认值true -->
            <setting name="multipleResultSetsEnabled" value="true"/>
        </settings>

    源码部分:

     private void settingsElement(XNode context) throws Exception {
        if (context != null) {
    // 获取settings节点下所有的子节点信息,然后封装成Properties对象 Properties props
    = context.getChildrenAsProperties(); // Check that all settings are known to the configuration class
    // 获取Configuration的元信息对象
    MetaClass metaConfig = MetaClass.forClass(Configuration.class); for (Object key : props.keySet()) {
    // 检测Configuration中是否存在相关的属性,如果不存在,那么抛出异常
    if (!metaConfig.hasSetter(String.valueOf(key))) { throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); } }
    //以下是把获取到的setting信息封装到Configuration中,所以才需要上面的检测 configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty(
    "autoMappingBehavior", "PARTIAL"))); configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory"))); configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false)); configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true)); configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true)); configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true)); configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false)); configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE"))); configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null)); configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false)); configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false)); configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION"))); configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER"))); configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString")); configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true)); configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage"))); configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false)); configuration.setLogPrefix(props.getProperty("logPrefix")); configuration.setLogImpl(resolveClass(props.getProperty("logImpl"))); configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory"))); } }

     二 typeAliases的解析

    先看下在配置文件中的写法:

        <typeAliases>
            <typeAlias alias="goods" type="com.yht.mybatisTest.entity.Goods"/>
        </typeAliases>

    源码部分:

     private void typeAliasesElement(XNode parent) {
        if (parent != null) {
    // 循环处理typeAliases下所有的子节点
    for (XNode child : parent.getChildren()) {
    // 这是针对子节点是package的处理
    if ("package".equals(child.getName())) { String typeAliasPackage = child.getStringAttribute("name"); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else {
    // 如果子节点是typeAlias String alias
    = child.getStringAttribute("alias"); //获取alias的属性值 String type = child.getStringAttribute("type"); //获取type的属性值 try {
    // 获取type对应的类型 Class
    <?> clazz = Resources.classForName(type); if (alias == null) {
    // 注册别名到类型的映射 进入该方法 typeAliasRegistry.registerAlias(clazz); }
    else { typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } }

    进入registerAlias方法:

      public void registerAlias(Class<?> type) {
    // 如果在配置文件中没有配置alias属性,这里会获取type的类名 String alias
    = type.getSimpleName();
    // 获取注解上的别名 Alias aliasAnnotation
    = type.getAnnotation(Alias.class); if (aliasAnnotation != null) { alias = aliasAnnotation.value(); }
    // 进入此方法 registerAlias(alias, type); }
      public void registerAlias(String alias, Class<?> value) {
        if (alias == null) throw new TypeException("The parameter alias cannot be null");
    // 将别名转为小写 String key
    = alias.toLowerCase(Locale.ENGLISH); // issue #748
    // 如果TYPE_ALIASES已经存在该别名,并且对应的类型不为空,同时已经存在的类型不等于将要注册的类型value,那么抛出异常 if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) { throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'."); }
    // 将别名和对应的全限定名放入到HashMap中,TYPE——ALIASES是一个HashMap TYPE_ALIASES.put(key, value); }
    接着看下Mybatis内部常见别名注册:
     private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
    
      public TypeAliasRegistry() {
        registerAlias("string", String.class);
    
        registerAlias("byte", Byte.class);
        registerAlias("long", Long.class);
        registerAlias("short", Short.class);
        registerAlias("int", Integer.class);
        registerAlias("integer", Integer.class);
        registerAlias("double", Double.class);
        registerAlias("float", Float.class);
        registerAlias("boolean", Boolean.class);
    
        registerAlias("byte[]", Byte[].class);
        registerAlias("long[]", Long[].class);
        registerAlias("short[]", Short[].class);
        registerAlias("int[]", Integer[].class);
        registerAlias("integer[]", Integer[].class);
        registerAlias("double[]", Double[].class);
        registerAlias("float[]", Float[].class);
        registerAlias("boolean[]", Boolean[].class);
    
        registerAlias("_byte", byte.class);
        registerAlias("_long", long.class);
        registerAlias("_short", short.class);
        registerAlias("_int", int.class);
        registerAlias("_integer", int.class);
        registerAlias("_double", double.class);
        registerAlias("_float", float.class);
        registerAlias("_boolean", boolean.class);
    
        registerAlias("_byte[]", byte[].class);
        registerAlias("_long[]", long[].class);
        registerAlias("_short[]", short[].class);
        registerAlias("_int[]", int[].class);
        registerAlias("_integer[]", int[].class);
        registerAlias("_double[]", double[].class);
        registerAlias("_float[]", float[].class);
        registerAlias("_boolean[]", boolean[].class);
    
        registerAlias("date", Date.class);
        registerAlias("decimal", BigDecimal.class);
        registerAlias("bigdecimal", BigDecimal.class);
        registerAlias("biginteger", BigInteger.class);
        registerAlias("object", Object.class);
    
        registerAlias("date[]", Date[].class);
        registerAlias("decimal[]", BigDecimal[].class);
        registerAlias("bigdecimal[]", BigDecimal[].class);
        registerAlias("biginteger[]", BigInteger[].class);
        registerAlias("object[]", Object[].class);
    
        registerAlias("map", Map.class);
        registerAlias("hashmap", HashMap.class);
        registerAlias("list", List.class);
        registerAlias("arraylist", ArrayList.class);
        registerAlias("collection", Collection.class);
        registerAlias("iterator", Iterator.class);
    
        registerAlias("ResultSet", ResultSet.class);
      }

    四 environments的解析

    在mybatis中事务管理器和数据源是在environments中配置的,配置如下:

        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC" />
                <dataSource type="POOLED">
                    <property name="driver" value="${driver}" />
                    <property name="url" value="${url}" />
                    <property name="username" value="${username}" />
                    <property name="password" value="${password}" />
                </dataSource>
            </environment>
        </environments>

    对应的源码如下:

      private void environmentsElement(XNode context) throws Exception {
        if (context != null) {
          if (environment == null) {
    // 获取environments中default的属性值 environment
    = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) {
    // 获取id的属性 String id
    = child.getStringAttribute("id");
    // 判断子节点id的属性和父节点environments的default属性是否相同,相同返回true,否则返回false
    if (isSpecifiedEnvironment(id)) {
    // 解析transactionManager节点 TransactionFactory txFactory
    = transactionManagerElement(child.evalNode("transactionManager"));
    // 解析DataSource节点 DataSourceFactory dsFactory
    = dataSourceElement(child.evalNode("dataSource"));
    // 获取DataSource对象 DataSource dataSource
    = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource);
    // 构建environment对象并设置到configuration中 configuration.setEnvironment(environmentBuilder.build()); } } } }

    现在还有一个问题值得思考:对于<property name="driver" value="${driver}" /> 中,value值是如何被赋值的?接下里,我们跟踪源码进行分析:

    进入XMLConfigBuilder类的environmentsElement方法,有这么一行代码: DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));是解析DataSource节点的,上述的过程就是在这个方法中发生的,进入该方法:

      private DataSourceFactory dataSourceElement(XNode context) throws Exception {
        if (context != null) {
    // 获取DataSource的type属性 String type
    = context.getStringAttribute("type");
    // 解析DataSource所有的子节点信息并封装为属性对象Properties,进入该方法 Properties props
    = context.getChildrenAsProperties(); DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a DataSourceFactory."); }

    进入context.getChildrenAsProperties();方法:

      public Properties getChildrenAsProperties() {
        Properties properties = new Properties();
    //进入getChildren()方法
    for (XNode child : getChildren()) { String name = child.getStringAttribute("name"); String value = child.getStringAttribute("value"); if (name != null && value != null) { properties.setProperty(name, value); } } return properties; } public List<XNode> getChildren() { List<XNode> children = new ArrayList<XNode>(); NodeList nodeList = node.getChildNodes(); if (nodeList != null) { for (int i = 0, n = nodeList.getLength(); i < n; i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) {
    // 对DataSource中所有的子节点构建成XNode对象,并放到children集合中,进入这个构造方法 children.add(
    new XNode(xpathParser, node, variables)); } } } return children; }
    // 这个是DataSource的子节点的构造方法,具体的说就是把 <property name="driver" value="${driver}" /> 构造成一个XNode节点
    public XNode(XPathParser xpathParser, Node node, Properties variables) { this.xpathParser = xpathParser; this.node = node; this.name = node.getNodeName(); this.variables = variables;
    //value赋值过程在这个方法中
    this.attributes = parseAttributes(node); this.body = parseBody(node); }
      private Properties parseAttributes(Node n) {
        Properties attributes = new Properties();
    // 获取子节点的所有属性 如:[name="driver", value="${driver}"] NamedNodeMap attributeNodes
    = n.getAttributes(); if (attributeNodes != null) { for (int i = 0; i < attributeNodes.getLength(); i++) { Node attribute = attributeNodes.item(i);
    // 对${dirver}的解析赋值在parse方法中 String value
    = PropertyParser.parse(attribute.getNodeValue(), variables); attributes.put(attribute.getNodeName(), value); } } return attributes; }
    // 根据varibles中已经存储的值,对给定的${driver},在varibles中找到driver对应的真实值,并进行替换
    public static String parse(String string, Properties variables) { VariableTokenHandler handler = new VariableTokenHandler(variables); GenericTokenParser parser = new GenericTokenParser("${", "}", handler); return parser.parse(string); }

     五 TypeHandler的解析

    当向数据库中存储或者取出数据时,需要将数据库中的字段类型和java类型做一个转换,比如从数据库中取出的CHAR类型,转换为java中的String类型,这个功能就委托给类型处理器TypeHandler来处理。

    mybatis已经提供了一些常见的类型处理器,如StringTypeHandler,ArrayTypeHandler,LongTypeHandler等,能够满足大多数的开发需求。对于某些特殊的需求,我们也可以自定义类型处理器,需要继承

    BaseTypeHandler这个抽象类。下面我们自定义一个MyStringTypeHandler类型处理器,用于扩展StringTypeHandler的功能:

    package com.yht.mybatisTest.typeHandlers;
    
    import java.sql.CallableStatement;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    import org.apache.ibatis.type.BaseTypeHandler;
    import org.apache.ibatis.type.JdbcType;
    
    /**
     * @author chenyk
     * @date 2018年8月22日
     */
    
    public class MyStringTypeHandler extends BaseTypeHandler<String>{
    
         @Override
          public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
              throws SQLException {
              System.out.println("新增逻辑");
              ps.setString(i, parameter);
          }
    
          @Override
          public String getNullableResult(ResultSet rs, String columnName)
              throws SQLException {
              System.out.println("新增逻辑");
              return rs.getString(columnName);
          }
    
          @Override
          public String getNullableResult(ResultSet rs, int columnIndex)
              throws SQLException {
              System.out.println("新增逻辑");
            return rs.getString(columnIndex);
          }
    
          @Override
          public String getNullableResult(CallableStatement cs, int columnIndex)
              throws SQLException {
              System.out.println("新增逻辑");
              return cs.getString(columnIndex);
          }
    
    }

    类型处理器在配置文件中有两种配置方法:

        <!-- 手动配置 -->
        <typeHandlers>
            <typeHandler jdbcType="CHAR" javaType="String" handler="com.yht.mybatisTest.typeHandlers.MyStringTypeHandler" />
        </typeHandlers>
        
        <!-- 自动扫描 -->
        <typeHandlers>
            <package name="com.yht.mybatisTest.typeHandlers"/>
        </typeHandlers>

    看源码部分:

    private void typeHandlerElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
    // 扫描指定的包 然后注册TypeHandler String typeHandlerPackage
    = child.getStringAttribute("name"); typeHandlerRegistry.register(typeHandlerPackage); } else {
    // 获取javaType属性值,jdbcType属性值,handler属性值,然后注册TypeHandler String javaTypeName
    = child.getStringAttribute("javaType"); String jdbcTypeName = child.getStringAttribute("jdbcType"); String handlerTypeName = child.getStringAttribute("handler"); Class<?> javaTypeClass = resolveClass(javaTypeName); JdbcType jdbcType = resolveJdbcType(jdbcTypeName); Class<?> typeHandlerClass = resolveClass(handlerTypeName); if (javaTypeClass != null) { if (jdbcType == null) { typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { typeHandlerRegistry.register(typeHandlerClass); } } } } }

    根据不同的情况,注册TypeHandler共有四种方法,这里我们选一种进行分析:typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);

      private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
        if (javaType != null) {
          Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
          if (map == null) {
            map = new HashMap<JdbcType, TypeHandler<?>>();
    // 存储JavaType到Map<JdbcType,TypeHandler>的映射 TYPE_HANDLER_MAP.put(javaType, map); } map.put(jdbcType, handler);
    if (reversePrimitiveMap.containsKey(javaType)) { register(reversePrimitiveMap.get(javaType), jdbcType, handler); } }
    //存储所有的TypeHandler ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler); }

    mybatis内置的TypeHandler有哪些呢?贴出源码:

     private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(JdbcType.class);
      private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<Type, Map<JdbcType, TypeHandler<?>>>();
      private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
      private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<Class<?>, TypeHandler<?>>();
    
      public TypeHandlerRegistry() {
        register(Boolean.class, new BooleanTypeHandler());
        register(boolean.class, new BooleanTypeHandler());
        register(JdbcType.BOOLEAN, new BooleanTypeHandler());
        register(JdbcType.BIT, new BooleanTypeHandler());
    
        register(Byte.class, new ByteTypeHandler());
        register(byte.class, new ByteTypeHandler());
        register(JdbcType.TINYINT, new ByteTypeHandler());
    
        register(Short.class, new ShortTypeHandler());
        register(short.class, new ShortTypeHandler());
        register(JdbcType.SMALLINT, new ShortTypeHandler());
    
        register(Integer.class, new IntegerTypeHandler());
        register(int.class, new IntegerTypeHandler());
        register(JdbcType.INTEGER, new IntegerTypeHandler());
    
        register(Long.class, new LongTypeHandler());
        register(long.class, new LongTypeHandler());
    
        register(Float.class, new FloatTypeHandler());
        register(float.class, new FloatTypeHandler());
        register(JdbcType.FLOAT, new FloatTypeHandler());
    
        register(Double.class, new DoubleTypeHandler());
        register(double.class, new DoubleTypeHandler());
        register(JdbcType.DOUBLE, new DoubleTypeHandler());
    
        register(String.class, new StringTypeHandler());
        register(String.class, JdbcType.CHAR, new StringTypeHandler());
        register(String.class, JdbcType.CLOB, new ClobTypeHandler());
        register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
        register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler());
        register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
        register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
        register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
        register(JdbcType.CHAR, new StringTypeHandler());
        register(JdbcType.VARCHAR, new StringTypeHandler());
        register(JdbcType.CLOB, new ClobTypeHandler());
        register(JdbcType.LONGVARCHAR, new ClobTypeHandler());
        register(JdbcType.NVARCHAR, new NStringTypeHandler());
        register(JdbcType.NCHAR, new NStringTypeHandler());
        register(JdbcType.NCLOB, new NClobTypeHandler());
    
        register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
        register(JdbcType.ARRAY, new ArrayTypeHandler());
    
        register(BigInteger.class, new BigIntegerTypeHandler());
        register(JdbcType.BIGINT, new LongTypeHandler());
    
        register(BigDecimal.class, new BigDecimalTypeHandler());
        register(JdbcType.REAL, new BigDecimalTypeHandler());
        register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
        register(JdbcType.NUMERIC, new BigDecimalTypeHandler());
    
        register(Byte[].class, new ByteObjectArrayTypeHandler());
        register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
        register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
        register(byte[].class, new ByteArrayTypeHandler());
        register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
        register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
        register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
        register(JdbcType.BLOB, new BlobTypeHandler());
    
        register(Object.class, UNKNOWN_TYPE_HANDLER);
        register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
        register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
    
        register(Date.class, new DateTypeHandler());
        register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
        register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
        register(JdbcType.TIMESTAMP, new DateTypeHandler());
        register(JdbcType.DATE, new DateOnlyTypeHandler());
        register(JdbcType.TIME, new TimeOnlyTypeHandler());
    
        register(java.sql.Date.class, new SqlDateTypeHandler());
        register(java.sql.Time.class, new SqlTimeTypeHandler());
        register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());
    
        // issue #273
        register(Character.class, new CharacterTypeHandler());
        register(char.class, new CharacterTypeHandler());
      }

    到现在为止,对于配置文件中常见节点的解析过程做了分析,接下来在下一篇文章中我们继续对映射文件的解析过程进行讲解。

  • 相关阅读:
    IO 单个文件的多线程拷贝
    day30 进程 同步 异步 阻塞 非阻塞 并发 并行 创建进程 守护进程 僵尸进程与孤儿进程 互斥锁
    day31 进程间通讯,线程
    d29天 上传电影练习 UDP使用 ScketServer模块
    d28 scoket套接字 struct模块
    d27网络编程
    d24 反射,元类
    d23 多态,oop中常用的内置函数 类中常用内置函数
    d22 封装 property装饰器 接口 抽象类 鸭子类型
    d21天 继承
  • 原文地址:https://www.cnblogs.com/51life/p/9512225.html
Copyright © 2011-2022 走看看