MyBatis启动之XMLConfigBuilder解析配置文件(二)

Stella981
• 阅读 744

MyBatis启动之XMLConfigBuilder解析配置文件(二)

前言

MyBatis启动之XMLConfigBuilder解析配置文件(二)

XMLConfigBuilderBaseBuilder(解析中会涉及到讲解)的其中一个子类,它的作用是把MyBatis的XML及相关配置解析出来,然后保存到Configuration中。本文就解析过程按照执行顺序进行分析,掌握常用配置的解析原理。

使用

调用XMLConfigBuilder进行解析,要进行两步操作,上篇文章中【MyBatis之启动分析(一)】有提到。

实例化XMLConfigBuilder对象。

    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
        // 调用父类的构造方法
        super(new Configuration());
        ErrorContext.instance().resource("SQL Mapper Configuration");
        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
      }

实例化Configuration

通过new Configuration()的方式实例化: MyBatis启动之XMLConfigBuilder解析配置文件(二)

typeAliasRegistry是一个类型别名注册器,实现原理就是维护一份HashMap,别名作为key,类的全限定名作为value。这里将框架中使用的类注册到类型别名注册器中。 TypeAliasRegistry.registerAlias代码如下:

    public void registerAlias(String alias, Class<!--?--> value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748
    //  在验证是否存在key和保存kv前,统一将key转换成小写
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (TYPE_ALIASES.containsKey(key) &amp;&amp; TYPE_ALIASES.get(key) != null &amp;&amp; !TYPE_ALIASES.get(key).equals(value)) {
      // 当注册的类型已存在时,抛出异常
      throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
    }
    // TYPE_ALIASES 为定义的一个HashMap
    TYPE_ALIASES.put(key, value);
    }

在实例化Configuration类过程中,在该类里除了实例化了TypeAliasRegistry还实例化了另外一个下面用到的的类:

    // 类型处理器注册器
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

TypeHandlerRegistryTypeAliasRegistry实例化逻辑相似,里面注册了一些常用类型和处理器,代码易懂。 TypeHandlerRegistry的属性

    // jdbc类型和TypeHandler的映射关系,key必须是JdbcType的枚举类型,读取结果集数据时,将jdbc类型转换成java类型
    private final Map<jdbctype, typehandler<?>&gt; JDBC_TYPE_HANDLER_MAP = new EnumMap<jdbctype, typehandler<?>&gt;(JdbcType.class);
    // Java类型与JdbcType类型的键值对,存在一对多的映射关系
    private final Map<type, map<jdbctype, typehandler<?>&gt;&gt; TYPE_HANDLER_MAP = new ConcurrentHashMap<type, map<jdbctype, typehandler<?>&gt;&gt;();
    // 没有相应的类型处理器时,使用的处理器
    private final TypeHandler<object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
    // 类型处理器类类型和类型处理器的映射关系
    private final Map<class<?>, TypeHandler<!--?-->&gt; ALL_TYPE_HANDLERS_MAP = new HashMap<class<?>, TypeHandler<!--?-->&gt;();
    // 空处理器的值,用来做校验
    private static final Map<jdbctype, typehandler<?>&gt; NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
    // 默认枚举类型处理器
    private Class<!--? extends TypeHandler--> defaultEnumTypeHandler = EnumTypeHandler.class;

TypeHandlerRegistry构造函数:

    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(Reader.class, new ClobReaderTypeHandler());
        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(InputStream.class, new BlobInputStreamTypeHandler());
        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());
    
        // mybatis-typehandlers-jsr310
        // 是否包含日期,时间相关的Api,通过判断是否加载java.time.Clock作为依据
        if (Jdk.dateAndTimeApiExists) {
          this.register(Instant.class, InstantTypeHandler.class);
          this.register(LocalDateTime.class, LocalDateTimeTypeHandler.class);
          this.register(LocalDate.class, LocalDateTypeHandler.class);
          this.register(LocalTime.class, LocalTimeTypeHandler.class);
          this.register(OffsetDateTime.class, OffsetDateTimeTypeHandler.class);
          this.register(OffsetTime.class, OffsetTimeTypeHandler.class);
          this.register(ZonedDateTime.class, ZonedDateTimeTypeHandler.class);
          this.register(Month.class, MonthTypeHandler.class);
          this.register(Year.class, YearTypeHandler.class);
          this.register(YearMonth.class, YearMonthTypeHandler.class);
          this.register(JapaneseDate.class, JapaneseDateTypeHandler.class);
        }
    
        // issue #273
        register(Character.class, new CharacterTypeHandler());
        register(char.class, new CharacterTypeHandler());
    }

里面调用了两个register()重载方法, type + handler 参的TypeHandlerRegistry.register(Class<t> javaType, TypeHandler<!--? extends T--> typeHandler)type + jdbc type + handler 参的TypeHandlerRegistry.register(Class<t> type, JdbcType jdbcType, TypeHandler<!--? extends T--> handler)

    // java type + handler
    public <t> void register(Class<t> javaType, TypeHandler<!--? extends T--> typeHandler) {
        register((Type) javaType, typeHandler);
    }
    
    private <t> void register(Type javaType, TypeHandler<!--? extends T--> typeHandler) {
        MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
        if (mappedJdbcTypes != null) {
          for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
            register(javaType, handledJdbcType, typeHandler);
          }
          if (mappedJdbcTypes.includeNullJdbcType()) {
            register(javaType, null, typeHandler);
          }
        } else {
          register(javaType, null, typeHandler);
        }
    }
    
    // java type + jdbc type + handler
    public <t> void register(Class<t> type, JdbcType jdbcType, TypeHandler<!--? extends T--> handler) {
        register((Type) type, jdbcType, handler);
    }
    
    // type + handler 和 type + jdbc type + handler 最终都调用此方法
    private void register(Type javaType, JdbcType jdbcType, TypeHandler<!--?--> handler) {
        if (javaType != null) {
          // 当 javaType 不为空时, 获取 java 类型的的映射
          Map<jdbctype, typehandler<?>&gt; map = TYPE_HANDLER_MAP.get(javaType);
          if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            // 若映射为空,新建一个映射关系
            map = new HashMap<jdbctype, typehandler<?>&gt;();
            // 保存至类型处理器映射关系中
            TYPE_HANDLER_MAP.put(javaType, map);
          }
          // 保存jdbcType和处理器关系,完成 java类型,jdbc类型,处理器三者之间的注册
          map.put(jdbcType, handler);
        }
        // 保存处理器信息中
        ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
    }
           
    // MappedJdbcTypes 注解        
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MappedJdbcTypes {
      JdbcType[] value();
      boolean includeNullJdbcType() default false;
    }
  • type + handler方法:先获取处理器的MappedJdbcTypes注解(自定义处理器注解),若注解的value值不为空时,由于该值为JdbcType[]类型,所以for循环 javaType+jdbcType+TypeHandler注册,若includeNullJdbcTypejdbcType是否包含null)为true,默认值为false,注册到相应映射中。若注解的valuenull,直接调用注册操作,里面不会注册type + jdbc type + handler关系。
  • type + jdbc type + handler方法:该方法将java类强制转换为java.lang.reflect.Type类型,然后调用最终注册的方法。

调用父类BaseBuilder的构造方法

BaseBuilder定义有三个属性

    protected final Configuration configuration;
    // 类型别名注册器
    protected final TypeAliasRegistry typeAliasRegistry;
    // 类型处理器注册器
    protected final TypeHandlerRegistry typeHandlerRegistry;

BaseBuilder构造方法

    public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
    }

这里属性,就是上面讲解到的。

调用 XMLConfigBuilder.parse() 作为解析入口。

parse()实现配置文件是否解析过

    public Configuration parse() {
        // 若parsed为true,配置文件解析过
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        // 标志已解析过
        parsed = true;
        // 从根节点 configuration 开始解析
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }

解析/configuration里的配置

    private void parseConfiguration(XNode root) {
        try {
          //issue #117 read properties first
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          typeAliasesElement(root.evalNode("typeAliases"));
          pluginElement(root.evalNode("plugins"));
          objectFactoryElement(root.evalNode("objectFactory"));
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          settingsElement(settings);
          // read it after objectFactory and objectWrapperFactory issue #631
          environmentsElement(root.evalNode("environments"));
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          typeHandlerElement(root.evalNode("typeHandlers"));
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }

从上面源码中,不难看出这里是解析/configuration中的各个子节点。

properties 节点解析

properties配置方式
    <!-- 方法一 -->
    <properties>
        <property name="username" value="${jdbc.username}" />
    </properties>
    
    <!-- 方法二 -->
    <properties resource="xxxConfig.properties">
    </properties>
    
    <!-- 方法三 -->
    <properties url="file:///D:/xxxConfig.properties">
    </properties>
propertiesElement()方法
    private void propertiesElement(XNode context) throws Exception {
        if (context != null) {
          // 获取 propertie 节点,并保存 Properties 中
          Properties defaults = context.getChildrenAsProperties();
          // 获取 resource 的值
          String resource = context.getStringAttribute("resource");
          // 获取 url 的值
          String url = context.getStringAttribute("url");
          if (resource != null &amp;&amp; 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) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
          } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
          }
          Properties vars = configuration.getVariables();
          if (vars != null) {
            defaults.putAll(vars);
          }
          // 将解析的值保存到 XPathParser 中
          parser.setVariables(defaults);
          // 将解析的值保存到 Configuration 中
          configuration.setVariables(defaults);
        }
    }

从上面源码中,resourceurl的配置形式不允许同时存在,否则抛出BuilderException异常。先解析propertie的配置值,再解析resourceurl的值。 当propertie存在与resourceurl相同的key时,propertie的配置会被覆盖,应为Properties实现的原理就是继承的Hashtable类来实现的。

settings 节点解析

settings配置方式
    <settings>
        <setting name="cacheEnabled" value="true" />
        ......
    </settings>

设置中各项的意图、默认值 (引用来源:w3cschool)

设置参数

描述

有效值

默认值

cacheEnabled

该配置影响的所有映射器中配置的缓存的全局开关。

true,false

true

lazyLoadingEnabled

延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。

true,false

false

aggressiveLazyLoading

当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载。

true,false,true

multipleResultSetsEnabled

是否允许单一语句返回多结果集(需要兼容驱动)。

true,false

true

useColumnLabel

使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。

true,false

true

useGeneratedKeys

允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。

true,false

False

autoMappingBehavior

指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。

NONE, PARTIAL, FULL

PARTIAL

defaultExecutorType

配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。

SIMPLE REUSE BATCH

SIMPLE

defaultStatementTimeout

设置超时时间,它决定驱动等待数据库响应的秒数。

Any positive integer

Not Set (null)

safeRowBoundsEnabled

允许在嵌套语句中使用分页(RowBounds)。

true,false

False

mapUnderscoreToCamelCase

是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。

true, false

False

localCacheScope

MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。

SESSION,STATEMENT

SESSION

jdbcTypeForNull

当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。

JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER

OTHER

lazyLoadTriggerMethods

指定哪个对象的方法触发一次延迟加载。

A method name list separated by commas

equals,clone,hashCode,toString

defaultScriptingLanguage

指定动态 SQL 生成的默认语言。

A type alias or fully qualified class name.

org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver

callSettersOnNulls

指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。注意基本类型(int、boolean等)是不能设置成 null 的。

true,false

false

logPrefix

指定 MyBatis 增加到日志名称的前缀。 Any String

Not set

logImpl

指定 MyBatis 所用日志的具体实现,未指定时将自动查找。

SLF4J, LOG4J, LOG4J2, JDK_LOGGING, COMMONS_LOGGING, STDOUT_LOGGING, NO_LOGGING

Not set

proxyFactory

指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具。

CGLIB JAVASSIST

CGLIB

settingsAsProperties()方法
    private Properties settingsAsProperties(XNode context) {
        if (context == null) {
          return new Properties();
        }
        // 获取setting节点的name和value,并保存至Properties返回
        Properties props = context.getChildrenAsProperties();
        // Check that all settings are known to the configuration class
        // 创建Configuration的MetaClass
        MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
        // 校验Configuration中是否有setting设置的name值
        for (Object key : props.keySet()) {
          if (!metaConfig.hasSetter(String.valueOf(key))) {
            throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
          }
        }
        return props;
    }

这里获取到setting的值,并返回Properties对象。然后做配置的name是否合法。 org.apache.ibatis.reflection.MetaClass类是保存着一个利用反射获取到的类信息,metaConfig.hasSetter(String.valueOf(key))是判断metaConfig对象中是否包含key属性。

vfsImpl()方法
    private void loadCustomVfs(Properties props) throws ClassNotFoundException {
          String value = props.getProperty("vfsImpl");
        if (value != null) {
          String[] clazzes = value.split(",");
          for (String clazz : clazzes) {
            if (!clazz.isEmpty()) {
              @SuppressWarnings("unchecked")
              Class<!--? extends VFS--> vfsImpl = (Class<!--? extends VFS-->)Resources.classForName(clazz);
              configuration.setVfsImpl(vfsImpl);
            }
          }
        }
    }

该方法是解析虚拟文件系统配置,用来加载自定义虚拟文件系统的资源。类保存在Configuration.vfsImpl中。

settingsElement()方法

这个方法的作用就是将解析的settings设置到 configuration

    private void settingsElement(Properties props) throws Exception {
        configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
        configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
        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"), false));
        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.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), 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")));
        @SuppressWarnings("unchecked")
        Class<!--? extends TypeHandler--> typeHandler = (Class<!--? extends TypeHandler-->)resolveClass(props.getProperty("defaultEnumTypeHandler"));
        configuration.setDefaultEnumTypeHandler(typeHandler);
        configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
        configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
        configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
        configuration.setLogPrefix(props.getProperty("logPrefix"));
        @SuppressWarnings("unchecked")
        Class<!--? extends Log--> logImpl = (Class<!--? extends Log-->)resolveClass(props.getProperty("logImpl"));
        configuration.setLogImpl(logImpl);
        configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
    }

typeAliases 节点解析

typeAliases配置方式
    <typealiases>
        <package name="com.ytao.main.model" />
        // 或
        <typealias type="com.ytao.main.model.Student" alias="student" />
        <typealias type="com.ytao.main.model.Person" />
    </typealiases>

该节点是配置类和别名的关系

  1. package节点是配置整个包下的类
  2. typeAlias节点是指定配置单个类,type为必填值且为类全限定名,alias为选填。 配置后,是该类时,可直接使用别名。
typeAliasesElement()方法
    private void typeAliasesElement(XNode parent) {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
              // 以 package 方式配置
              String typeAliasPackage = child.getStringAttribute("name");
              configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
              // 以 alias 方式配置
              String alias = child.getStringAttribute("alias");
              String type = child.getStringAttribute("type");
              try {
                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);
              }
            }
          }
        }
    }
使用 package 配置

当扫描package时,获取到包名后TypeAliasRegistry.registerAliases(typeAliasPackage)

    public void registerAliases(String packageName){
        registerAliases(packageName, Object.class);
    }

    public void registerAliases(String packageName, Class<!--?--> superType){
        ResolverUtil<class<?>&gt; resolverUtil = new ResolverUtil<class<?>&gt;();
        // 获取 package 下所有已 .class 结尾的文件
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        // 获取扫描出来的类
        Set<class<? extends class<?>&gt;&gt; typeSet = resolverUtil.getClasses();
        for(Class<!--?--> type : typeSet){
          // Ignore inner classes and interfaces (including package-info.java)
          // Skip also inner classes. See issue #6
          // 过滤类
          if (!type.isAnonymousClass() &amp;&amp; !type.isInterface() &amp;&amp; !type.isMemberClass()) {
            registerAlias(type);
          }
        }
    }

扫描到指定package下所有以.class结尾文件的类,并保存至Set集合中,然后遍历集合,过滤掉没有名称,接口,和底层特定类。 最后TypeAliasRegistry.registerAlias(Class<!--?--> type)注册到别名注册器中。

    public void registerAlias(Class<!--?--> type) {
        // 使用类的 simpleName 作为别名,也就是默认的别名命名规则
        String alias = type.getSimpleName();
        Alias aliasAnnotation = type.getAnnotation(Alias.class);
        if (aliasAnnotation != null) {
          alias = aliasAnnotation.value();
        } 
        // 上面分析的最终注册的方法
        registerAlias(alias, type);
    }

通过类注册到注册器中时,如果该注册类有使用@Aliasorg.apache.ibatis.type.Alias)注解,那么XML配置中配置的别名会被注解配置覆盖。

使用 typeAlias 配置

如果typeAliasalias有设置值,使用自定名称方式注册,否则使用默认方式注册,即类的simpleName作为别名。

plugins 节点解析

plugins配置方式
    <plugins>
        // 配置自定义插件,可指定在某个点进行拦截
        <plugin interceptor="com.ytao.main.plugin.DemoInterceptor">
            // 当前插件属性
            <property name="name" value="100" />
        </plugin>
    </plugins>

自定义插件需要实现org.apache.ibatis.plugin.Interceptor接口,同时在注解上指定拦截的方法。

pluginElement()方法
    private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            // 获取自定插件的类名
            String interceptor = child.getStringAttribute("interceptor");
            // 获取插件属性
            Properties properties = child.getChildrenAsProperties();
            // 实例化 Interceptor
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            // 设置插件属性到插件中
            interceptorInstance.setProperties(properties);
            // 将插件保存在 configuration 中
            configuration.addInterceptor(interceptorInstance);
          }
        }
    }

这里取<plugin>节点的interceptor可以使用别名设置。从源码中resolveClass方法

    // 
    protected Class<!--?--> resolveClass(String alias) {
        if (alias == null) {
          return null;
        }
        try {
          return resolveAlias(alias);
        } catch (Exception e) {
          throw new BuilderException("Error resolving class. Cause: " + e, e);
        }
    }
    
    // 
    protected Class<!--?--> resolveAlias(String alias) {
        return typeAliasRegistry.resolveAlias(alias);
    }
    
    // 
    public <t> Class<t> resolveAlias(String string) {
        try {
          if (string == null) {
            return null;
          }
          // issue #748
          // 将传入的 类 名称统一转换
          String key = string.toLowerCase(Locale.ENGLISH);
          Class<t> value;
          // 验证别名中是否有当前传入的key
          if (TYPE_ALIASES.containsKey(key)) {
            value = (Class<t>) TYPE_ALIASES.get(key);
          } else {
            value = (Class<t>) Resources.classForName(string);
          }
          return value;
        } catch (ClassNotFoundException e) {
          throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
        }
    }

以上源码为别名解析过程,其他别名的解析也是调用此方法,先去保存的别名中去找,是否有别名,如果没有就通过Resources.classForName生成实例。

objectFactory,objectWrapperFactory,reflectorFactory 节点解析

以上都是对实现类都是对MyBatis进行扩展。解析方法也类似,最后都是保存在configuration

    // objectFactory 解析
    private void objectFactoryElement(XNode context) throws Exception {
        if (context != null) {
          String type = context.getStringAttribute("type");
          Properties properties = context.getChildrenAsProperties();
          ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
          factory.setProperties(properties);
          configuration.setObjectFactory(factory);
        }
    }
    
    // objectWrapperFactory 解析
    private void objectWrapperFactoryElement(XNode context) throws Exception {
        if (context != null) {
          String type = context.getStringAttribute("type");
          ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance();
          configuration.setObjectWrapperFactory(factory);
        }
    }
    
    // reflectorFactory 解析
    private void reflectorFactoryElement(XNode context) throws Exception {
        if (context != null) {
           String type = context.getStringAttribute("type");
           ReflectorFactory factory = (ReflectorFactory) resolveClass(type).newInstance();
           configuration.setReflectorFactory(factory);
        }
    }
    

以上为解析objectFactory,objectWrapperFactory,reflectorFactory源码,经过前面的分析后,这里比较容易看懂。

environments 节点解析

environments配置方式
    <environments default="development">
        <environment id="development">
            <!-- 事务管理 -->
            <transactionmanager type="JDBC">
                <property name="prop" value="100" />
            </transactionmanager>
            <!-- 数据源 -->
            <datasource type="UNPOOLED">
                <!-- JDBC 驱动 -->
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <!-- 数据库的 url -->
                <property name="url" value="${jdbc.url}" />
                <!-- 数据库登录名 -->
                <property name="username" value="${jdbc.username}" />
                <!-- 数据库登录密码 -->
                <property name="password" value="${jdbc.password}" />
            </datasource>
        </environment>
        <!-- 一个环境,对应一个environment -->
        ......
    </environments>

该节点可设置多个环境,针对不同的环境单独配置。environments的属性default是默认环境,该值对应一个environment的属性id的值。

  • transactionManager为事务管理,属性type为事务管理类型,上面的介绍的new Configuration()有定义类型有:JDBC 和 MANAGED事务管理类型。
  • dataSource是数据源,type为数据源类型,与transactionManager同理,可知内建的数据源类型有:JNDI,POOLED,UNPOOLED数据源类型。
environmentsElement()方法
    private void environmentsElement(XNode context) throws Exception {
        if (context != null) {
          if (environment == null) {
            environment = context.getStringAttribute("default");
          }
          for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            // 验证 id
            if (isSpecifiedEnvironment(id)) {
              // 解析 transactionManager, 并实例化 TransactionFactory
              TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
              // 解析 dataSource,并实例化 DataSourceFactory
              DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
              // 获取 dataSource
              DataSource dataSource = dsFactory.getDataSource();
              Environment.Builder environmentBuilder = new Environment.Builder(id)
                  .transactionFactory(txFactory)
                  .dataSource(dataSource);
              configuration.setEnvironment(environmentBuilder.build());
            }
          }
        }
    }
    
    private boolean isSpecifiedEnvironment(String id) {
        if (environment == null) {
          throw new BuilderException("No environment specified.");
        } else if (id == null) {
          throw new BuilderException("Environment requires an id attribute.");
        } else if (environment.equals(id)) {
          return true;
        }
        return false;
    }

若没有配置environment环境或环境没有给id属性,则会抛出异常,若当前id是要使用的就返回true,否则返回falseTransactionFactory实例化过程比较简单,与创建DataSourceFactory类似。

数据源的获取

获取数据源,首先得创建DataSourceFactory,上面使用DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"))创建

    private DataSourceFactory dataSourceElement(XNode context) throws Exception {
        if (context != null) {
          String type = context.getStringAttribute("type");
          Properties props = context.getChildrenAsProperties();
          DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
          factory.setProperties(props);
          return factory;
        }
        throw new BuilderException("Environment declaration requires a DataSourceFactory.");
    }

这里就是获取到数据源得type后,利用上面所讲到得resolveClass()方法获取到DataSourceFactory。 以UNPOOLED为例,对应的DataSourceFactory实现类为UnpooledDataSourceFactory。实例化过程中就给该类的属性dataSource数据源赋值了

    /**
     * UnpooledDataSourceFactory 类
     */
    protected DataSource dataSource;
    
    public UnpooledDataSourceFactory() {
        this.dataSource = new UnpooledDataSource();
    }
    
    @Override
    public DataSource getDataSource() {
       return dataSource;
    }

UnpooledDataSource类里面有静态代码块所以数据源被加载

    /**
     * UnpooledDataSource 类
     */
    static {
        Enumeration<driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
          Driver driver = drivers.nextElement();
          registeredDrivers.put(driver.getClass().getName(), driver);
        }
    }

databaseIdProvider 节点解析

databaseIdProvider配置方式
    <databaseidprovider type="DB_VENDOR">
        <property name="SQL Server" value="sqlserver" />
        <property name="DB2" value="db2" />
        <property name="Oracle" value="oracle" />
        <property name="MySQL" value="mysql" />
    </databaseidprovider>
    
    <select id="select" resulttype="com.ytao.main.model.Student" databaseid="mysql">
        select
          *
        from student
    </select>

基于映射语句中的databaseId属性,可以根据不同数据库厂商执行不同的sql。

databaseIdProviderElement()方法
  private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
      String type = context.getStringAttribute("type");
      // 保持向后兼容
      if ("VENDOR".equals(type)) {
          type = "DB_VENDOR";
      }
      Properties properties = context.getChildrenAsProperties();
      databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
      databaseIdProvider.setProperties(properties);
    }
    Environment environment = configuration.getEnvironment();
    if (environment != null &amp;&amp; databaseIdProvider != null) {
      String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
      configuration.setDatabaseId(databaseId);
    }
  }

根据匹配的数据库厂商类型匹配数据源databaseIdProvider.getDatabaseId(environment.getDataSource())

  @Override
  public String getDatabaseId(DataSource dataSource) {
    if (dataSource == null) {
      throw new NullPointerException("dataSource cannot be null");
    }
    try {
      return getDatabaseName(dataSource);
    } catch (Exception e) {
      log.error("Could not get a databaseId from dataSource", e);
    }
    return null;
  }
  
  private String getDatabaseName(DataSource dataSource) throws SQLException {
    // 根据数据源获取数据库产品名称
    String productName = getDatabaseProductName(dataSource);
    if (this.properties != null) {
      for (Map.Entry<object, object> property : properties.entrySet()) {
        // 判断是否包含,选择使用的数据库产品
        if (productName.contains((String) property.getKey())) {
          return (String) property.getValue();
        }
      }
      // no match, return null
      return null;
    }
    return productName;
  }
    
  private String getDatabaseProductName(DataSource dataSource) throws SQLException {
    Connection con = null;
    try {
      // 数据库连接
      con = dataSource.getConnection();
      // 获取连接元数据
      DatabaseMetaData metaData = con.getMetaData();
      // 获取数据库产品名称
      return metaData.getDatabaseProductName();
    } finally {
      if (con != null) {
        try {
          con.close();
        } catch (SQLException e) {
          // ignored
        }
      }
    }
  }    

这里需要注意的是配置:比如使用mysql,我踩过这里的坑,这里Name为MySQL,我把y写成大写,结果匹配不上。 另外这里写个My也能匹配上,应为是使用的String.contains方法,只要包含就会符合,这里代码应该不够严谨。

typeHandlers 节点解析

typeHandlers配置方式
    <typehandlers>
        <package name="com.ytao.main.handler" />
        // 或
        <typehandler javaType="java.util.Date" jdbcType="TIMESTAMP" handler="com.ytao.main.handler.DemoDateHandler" />
    </typehandlers>

扫描整个包或者指定类型之间的映射,javaType, jdbcType非必需,handler必填项

typeHandlerElement()方法
  private void typeHandlerElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          // 获取包名
          String typeHandlerPackage = child.getStringAttribute("name");
          // 注册包下所有的类型处理器
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          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);
          }
        }
      }
    }
  }

源码分析会根据包下所有处理器或者指定处理器进行解析,最后会根据上面分析到的type + handlertype + jdbc type + handler不同情况注册。 另外这里还有个TypeHandlerRegistry.register(Class<!--?--> typeHandlerClass)注册类

  public void register(Class<!--?--> typeHandlerClass) {
    // 标志是否从 MappedTypes 注解中获取 javaType 注册
    boolean mappedTypeFound = false;
    // 获取 MappedTypes 的值
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<!--?--> javaTypeClass : mappedTypes.value()) {
        // 已 type + handler 的方式注册
        register(javaTypeClass, typeHandlerClass);
        // 标志已通过注解注册类型
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {
      // 通过 TypeHandler 注册
      register(getInstance(null, typeHandlerClass));
    }
  }
  
  // 实例化
  public <t> TypeHandler<t> getInstance(Class<!--?--> javaTypeClass, Class<!--?--> typeHandlerClass) {
    if (javaTypeClass != null) {
      try {
        // 获取有参构造函数
        Constructor<!--?--> c = typeHandlerClass.getConstructor(Class.class);
        // 实例化对象
        return (TypeHandler<t>) c.newInstance(javaTypeClass);
      } catch (NoSuchMethodException ignored) {
        // ignored
      } catch (Exception e) {
        throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
      }
    }
    try {
      // 获取无参构造函数
      Constructor<!--?--> c = typeHandlerClass.getConstructor();
      return (TypeHandler<t>) c.newInstance();
    } catch (Exception e) {
      throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
    }
  }  
  
  // 注册实例
  public <t> void register(TypeHandler<t> typeHandler) {
    boolean mappedTypeFound = false;
    MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<!--?--> handledType : mappedTypes.value()) {
        register(handledType, typeHandler);
        mappedTypeFound = true;
      }
    }
    // @since 3.1.0 - try to auto-discover the mapped type
    if (!mappedTypeFound &amp;&amp; typeHandler instanceof TypeReference) {
      try {
        TypeReference<t> typeReference = (TypeReference<t>) typeHandler;
        register(typeReference.getRawType(), typeHandler);
        mappedTypeFound = true;
      } catch (Throwable t) {
        // maybe users define the TypeReference with a different type and are not assignable, so just ignore it
      }
    }
    if (!mappedTypeFound) {
      register((Class<t>) null, typeHandler);
    }
  }  
  

以上的register方法中,了解type + jdbc type + handler后,其他的register重载方法比较容易理解,其他的都是基于它上面的封装。

mappers 节点解析

mappers配置方式
    <mappers>
        <package name="com.ytao.main.mapper" />
        // 或
        <mapper resource="mapper/studentMapper.xml" />
        // 或
        <mapper url="file:///D:/mybatis-3-mybatis-3.4.6/src/main/resources/mapper/studentMapper.xml" />
        // 或
        <mapper class="com.ytao.main.mapper.StudentMapper" />
    </mappers>

可通过以上四种形式配置mappers节点,<package><mapper>为互斥节点。

mapperElement()方法

该方法是负责解析<mappers>节点

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 如果配置 package 节点,则扫描
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          // 解析包下类Mapper接口,并注册到configuration的mapperRegistry中
          configuration.addMappers(mapperPackage);
        } else {
          // 获取mapper节点的resource,url,class属性
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 根据resource解析,并且url,class值必须为空,也就不能配置值。url,class同理,其它两个属性也不能配置值
          if (resource != null &amp;&amp; url == null &amp;&amp; mapperClass == null) {
            ErrorContext.instance().resource(resource);
            // 通过resource获取流
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 创建XMLMapperBuilder对象
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 解析映射配置文件
            mapperParser.parse();
          } else if (resource == null &amp;&amp; url != null &amp;&amp; mapperClass == null) {
            ErrorContext.instance().resource(url);
            // 通过url获取流
            InputStream inputStream = Resources.getUrlAsStream(url);
            // 和resource解析方式一样,创建XMLMapperBuilder对象,然后解析映射配置文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null &amp;&amp; url == null &amp;&amp; mapperClass != null) {
            // 加载class属性的接口
            Class<!--?--> mapperInterface = Resources.classForName(mapperClass);
            // 将接口注册到configuration的mapperRegistry中
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

<package>的包扫描到的类,然后单个单个注册到configuration的mapperRegistry中,这里和<mapper>使用class属性是一样逻辑。 解析package方式

  // Configuration 中定义了
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

  /**
   * 步骤一
   * 该函数于 Configuration 中  
   */  
  public void addMappers(String packageName) {
    // mapperRegistry定义在Configuration中的一个属性
    mapperRegistry.addMappers(packageName);
  }
  
  /**
   * 步骤二
   * 该函数于 MapperRegistry 中  
   */   
  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }
  
  /**
   * 步骤三
   * 该函数于 MapperRegistry 中  
   */       
  public void addMappers(String packageName, Class<?> superType) {
    // 通过 ResolverUtil 获取包下的类
    ResolverUtil<class<?>&gt; resolverUtil = new ResolverUtil<class<?>&gt;();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<class<? extends class<?>&gt;&gt; mapperSet = resolverUtil.getClasses();
    for (Class<!--?--> mapperClass : mapperSet) {
      // 遍历获取到的类,注册到 MapperRegistry
      addMapper(mapperClass);
    }
  }   
   
  /**
   * 步骤四
   * 该函数于 MapperRegistry 中
   */
  public <t> void addMapper(Class<t> type) {
    // mapper 类为 interface 接口
    if (type.isInterface()) {
      // 判断当前class是否已经注册过
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      // 校验是否加载完成
      boolean loadCompleted = false;
      try {
        // 保存 mapper 接口和 MapperProxyFactory 之间的映射
        knownMappers.put(type, new MapperProxyFactory<t>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        // 解析xml和注解
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        // 标志加载完成
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }   
  

解析mapperclass属性

  // 该函数于 Configuration 中  
  public <t> void addMapper(Class<t> type) {
    mapperRegistry.addMapper(type);
  }
  
  // ... 这里调用上面的【步骤四】

这两中方式是直接注册接口到mapperRegistry,另外两种是解析xml的方式就是获取映射文件的namespace,再注册进来,XMLMapperBuilder是负责解析映射配置文件的类,今后会单独详细分析这个类,这里不展开讲。

这里对XMLConfigBuilder解析配置文件到此分析完,本文对配置文件解析的流程大致了解流程和原理。相信遇到配置问题异常,大致能排查到根本原因。

个人博客:https://ytao.top

我的公众号 ytao

MyBatis启动之XMLConfigBuilder解析配置文件(二)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
3年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这