手写Mybatis:第13章-通过注解配置执行SQL语句

news2024/12/22 5:47:16

文章目录

  • 一、目标:注解配置执行SQL
  • 二、设计:注解配置执行SQL
  • 三、实现:注解配置执行SQL
    • 3.1 工程结构
    • 3.2 注解配置执行SQL类图
    • 3.3 脚本语言驱动器
      • 3.3.1 脚本语言驱动器接口
      • 3.3.2 XML语言驱动器
    • 3.4 注解配置构建器
      • 3.4.1 定义增删改查注解
      • 3.4.2 注解配置解析
    • 3.5 Mapper XML解析调用
      • 3.5.1 修改配置项
      • 3.5.2 修改映射构建器助手
      • 3.5.3 XML配置构建器
      • 3.5.4 映射器注册机
  • 四、测试:注解配置执行SQL
    • 4.1 测试环境配置
      • 4.1.1 配置数据源配置
      • 4.1.2 修改IUserDao用户持久层
    • 4.2 单元测试
      • 4.2.1 插入测试
      • 4.2.2 查询测试(多条数据)
      • 4.2.3 修改测试
      • 4.2.4 删除测试
  • 五、总结:注解配置执行SQL

一、目标:注解配置执行SQL

💡 扩展ORM框架功能,实现配置方法注解的方式处理增删改查操作

在这里插入图片描述

  • 日常使用 Mybatis 框架,对于 XML 和注解配置也都是可以共用的,主要基于配置文件 mappers 中,引入的哪类资源。
    • XML 配置:resource="mapper/User_Mapper.xml"
    • 注解配置:class="com.lino.mybatis.test.dao.IUserDao"
  • 两种使用方式的共性:都需要基于这些提供的信息,获取出:SQL语句、入参、出参等,并把这些信息包装成一个整体的映射语句,串联整个流程。

二、设计:注解配置执行SQL

💡 如何通过注解方式处理SQL的配置?

  • 将注解的解析部分与 Mapper XML 进行策略处理,对于不同类型的使用方式,做到解析结果一致。
    • 那么后续的处理 SQL 执行和结果封装等流程就可以正常执行了。
  • 这部分代码逻辑变动,主要以 XMLConfigBuilder 配置构建器的 Mapper 解析开始。
    • 因为只有从这里开始才是判断一个 Mapper 到底是使用了 XML 配置还是注解配置。

在这里插入图片描述

  • 基于这样不同的 mapper 引入的类型信息,则需要在 XMLConfigBuilder 配置构建器,解析 Mapper 元素信息时进行判断,按照不同的获取类型,resource、class 进行不同的解析处理。
  • 只要在解析处理中把这两部分的差异做适配处理,最终后续的流程就可以正常进行。

在这里插入图片描述

  • 首先以加载解析 XML 为入口,处理不同配置 SQL 方式的解析操作。
    • 就是结合原有解析 Mapper XML 配置扩展注解 SQL 配置方式。
  • 具体的处理过程:主要在 XMLConfigBuilder#mapperElement 配置构建器解析 mapper 配置时,处理关于注解的解析部分。
    • 这些注解目前添加 @Select、@Insert、@Update、@Delete
    • 处理解析注解则会向 Configuration 配置项中添加 ResultMap、MappedStatement 信息。
    • 用于后续获取 Mapper 调用 DefaultSqlSession 对应的执行方法时,拿到映射器语句配置进行 SQL 执行和结果封装。

三、实现:注解配置执行SQL

3.1 工程结构

mybatis-step-12
|-src
	|-main
	|	|-java
	|		|-com.lino.mybatis
    |			|-annotations
    |			|	|-Delete.java
	|			|	|-Insert.java
	|			|	|-Select.java
    |			|	|-Update.java
    |			|-binding
    |			|	|-MapperMethod.java
	|			|	|-MapperProxy.java
	|			|	|-MapperProxyFactory.java
    |			|	|-MapperRegistry.java
    |			|-builder
    |			|	|-annotations
    |			|	|	|-MapperAnnotationBuilder.java
    |			|	|-xml
    |			|	|	|-XMLConfigBuilder.java
    |			|	|	|-XMLMapperBuilder.java
    |			|	|	|-XMLStatementBuilder.java
    |			|	|-BaseBuilder.java
    |			|	|-MapperBuilderAssistant.java
    |			|	|-ParameterExpression.java
    |			|	|-SqlSourceBuilder.java
    |			|	|-StaticSqlSource.java
	|			|-datasource
	|			|	|-druid
	|			|	|	|-DruidDataSourceFacroty.java
	|			|	|-pooled
	|			|	|	|-PooledConnection.java
	|			|	|	|-PooledDataSource.java
	|			|	|	|-PooledDataSourceFacroty.java
	|			|	|	|-PoolState.java
	|			|	|-unpooled
	|			|	|	|-UnpooledDataSource.java
	|			|	|	|-UnpooledDataSourceFacroty.java
	|			|	|-DataSourceFactory.java
	|			|-executor
	|			|	|-parameter
	|			|	|	|-ParameterHandler.java
	|			|	|-result
	|			|	|	|-DefaultResultContext.java
	|			|	|	|-DefaultResultHandler.java
	|			|	|-resultset
	|			|	|	|-DefaultResultSetHandler.java
	|			|	|	|-ResultSetHandler.java
	|			|	|	|-ResultSetWrapper.java
	|			|	|-statement
	|			|	|	|-BaseStatementHandler.java
	|			|	|	|-PreparedStatementHandler.java
	|			|	|	|-SimpleStatementHandler.java
	|			|	|	|-StatementHandler.java
	|			|	|-BaseExecutor.java
	|			|	|-Executor.java
	|			|	|-SimpleExecutor.java
    |			|-io
    |			|	|-Resources.java
    |			|-mapping
    |			|	|-BoundSql.java
    |			|	|-Environment.java
    |			|	|-MappedStatement.java
    |			|	|-ParameterMapping.java
    |			|	|-ResultMap.java
    |			|	|-ResultMapping.java
    |			|	|-SqlCommandType.java
    |			|	|-SqlSource.java
    |			|-parsing
    |			|	|-GenericTokenParser.java
    |			|	|-TokenHandler.java
	|			|-reflection
	|			|	|-factory
	|			|	|	|-DefaultObjectFactory.java
	|			|	|	|-ObjectFactory.java
	|			|	|-invoker
	|			|	|	|-GetFieldInvoker.java
	|			|	|	|-Invoker.java
	|			|	|	|-MethodInvoker.java
	|			|	|	|-SetFieldInvoker.java
	|			|	|-property
	|			|	|	|-PropertyNamer.java
	|			|	|	|-PropertyTokenizer.java
	|			|	|-wrapper
	|			|	|	|-BaseWrapper.java
	|			|	|	|-BeanWrapper.java
	|			|	|	|-CollectionWrapper.java
	|			|	|	|-DefaultObjectWrapperFactory.java
	|			|	|	|-MapWrapper.java
	|			|	|	|-ObjectWrapper.java
	|			|	|	|-ObjectWrapperFactory.java
	|			|	|-MetaClass.java
	|			|	|-MetaObject.java
	|			|	|-Reflector.java
	|			|	|-SystemMetaObject.java
	|			|-scripting
	|			|	|-defaults
	|			|	|	|-DefaultParameterHandler.java
	|			|	|	|-RawSqlSource.java
	|			|	|-xmltags
	|			|	|	|-DynamicContext.java
	|			|	|	|-MixedSqlNode.java
	|			|	|	|-SqlNode.java
	|			|	|	|-StaticTextSqlNode.java
	|			|	|	|-XMLLanguageDriver.java
	|			|	|	|-XMLScriptBuilder.java
	|			|	|-LanguageDriver.java
	|			|	|-LanguageDriverRegistry.java
    |			|-session
    |			|	|-defaults
    |			|	|	|-DefaultSqlSession.java
    |			|	|	|-DefaultSqlSessionFactory.java
    |			|	|-Configuration.java
    |			|	|-ResultContext.java
    |			|	|-ResultHandler.java
    |			|	|-RowBounds.java
    |			|	|-SqlSession.java
    |			|	|-SqlSessionFactory.java
    |			|	|-SqlSessionFactoryBuilder.java
    |			|	|-TransactionIsolationLevel.java
    |			|-transaction
    |			|	|-jdbc
    |			|	|	|-JdbcTransaction.java
    |			|	|	|-JdbcTransactionFactory.java
    |			|	|-Transaction.java
    |			|	|-TransactionFactory.java
    |			|-type
    |			|	|-BaseTypeHandler.java
    |			|	|-IntegerTypeHandler.java
    |			|	|-JdbcType.java
    |			|	|-LongTypeHandler.java
    |			|	|-StringTypeHandler.java
    |			|	|-TypeAliasRegistry.java
    |			|	|-TypeHandler.java
    |			|	|-TypeHandlerRegistry.java
	|-test
		|-java
		|	|-com.lino.mybatis.test
		|	|-dao
		|	|	|-IUserDao.java
		|	|-po
		|	|	|-User.java
		|	|-ApiTest.java
        |-resources
        	|-mybatis-config-datasource.xml

3.2 注解配置执行SQL类图

在这里插入图片描述

  • XMLConfigBuilder 配置构建器是解析 Mapper 的入口。
    • 以这条流程线中的方法 mapperElement 开始,处理 XML 解析的基础上,扩展注解的解析处理。
    • 通过从 xml 读取到的 class 配置,通过 Configuration 配置项类的添加 Mapper 方法,启动解析注解类语句的操作。
    • 也就是在 MapperRegistry 映射器注册机,随着 Configuration 配置项调用 addMapper 时所做的注解解析操作。
  • 新增加核心类 MapperAnnotationBuilder 注解配置构建器,就是专门解析注解为主的类。
    • 注解方式的解析主要通过 Method 类,获取方法的入参、出参信息。
    • 以及基于这样的信息,获取到 LanguageDriver 脚本语言驱动器,从而创建出 SqlSession 属性。
  • 再往下执行,就和前面的一样。
    • 获取 Mapper 调用 DefaultSqlSession 对应的方法。
    • 以及从 Configuartion 配置项中获取到解析的 SQL 信息做相应的参数设置和结果包装操作。

3.3 脚本语言驱动器

3.3.1 脚本语言驱动器接口

LanguageDriver.java

package com.lino.mybatis.scripting;

import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Element;

/**
 * @description: 脚本语言驱动
 */
public interface LanguageDriver {

    /**
     * 创建SQL源
     *
     * @param configuration 配置项
     * @param script        元素
     * @param parameterType 参数类型
     * @return SqlSource SQL源码
     */
    SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType);

    /**
     * 创建SQL源(annotation 注解方式)
     *
     * @param configuration 配置项
     * @param script        注解名称
     * @param parameterType 参数类型
     * @return SqlSource SQL源码
     */
    SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

    /**
     * 创建参数处理器
     *
     * @param mappedStatement 映射器语句类
     * @param parameterObject 参数对象
     * @param boundSql        sql语句
     * @return ParameterHandler 参数处理器
     */
    ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
}
  • 通过重载一个 createSqlSource 接口,把 script 的入参设置为 String 类型,来解析注解 SQL 的配置

3.3.2 XML语言驱动器

XMLLanguageDriver.java

package com.lino.mybatis.scripting.xmltags;

import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.scripting.defaults.DefaultParameterHandler;
import com.lino.mybatis.scripting.defaults.RawSqlSource;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Element;

/**
 * @description: XML 语言驱动器
 */
public class XMLLanguageDriver implements LanguageDriver {

    @Override
    public SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType) {
        // 用XML脚本构建器解析
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }

    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        // 暂时不解析动态 SQL
        return new RawSqlSource(configuration, script, parameterType);
    }

    @Override
    public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
    }
}
  • 用于解析注解方式的 createSqlSource 方法,其与 XML 解析来说,更加简单。
    • 因为这里不需要提供专门的 XML 脚本构建器。
    • 而是直接按照 SQL 的入参信息,创建 RawSqlSource 即可。

3.4 注解配置构建器

  • Mybatis 框架的实现中,有专门一个 annotations 注解包,来提供用于配置到 DAO 方法上的注解。
  • 这些注解包括了所有的增删改查操作,同时设定一些额外的返回参数等。

3.4.1 定义增删改查注解

  • 添加四个注解 @Insert、@Delete、@Update、@Select

Insert.java

package com.lino.mybatis.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @description: insert 语句注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Insert {

    String[] value();
}

Delete.java

package com.lino.mybatis.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @description: delete 语句注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Delete {

    String[] value();
}

Update.java

package com.lino.mybatis.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @description: update 语句注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Update {

    String[] value();
}

Select.java

package com.lino.mybatis.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @description: select 语句注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {

    String[] value();
}
  • Mybatis 源码中,除了上面的四个注解,还有如 @Arg、@InsertProvider、@Param、@ResultMap 等注解参数配置。

3.4.2 注解配置解析

MapperAnnotationBuilder.java

package com.lino.mybatis.builder.annotation;

import com.lino.mybatis.annotations.Delete;
import com.lino.mybatis.annotations.Insert;
import com.lino.mybatis.annotations.Select;
import com.lino.mybatis.annotations.Update;
import com.lino.mybatis.binding.MapperMethod;
import com.lino.mybatis.builder.MapperBuilderAssistant;
import com.lino.mybatis.mapping.SqlCommandType;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;

/**
 * @description: 注解配置构建器 Mapper
 */
public class MapperAnnotationBuilder {

    private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<>();

    private Configuration configuration;
    private MapperBuilderAssistant assistant;
    private Class<?> type;

    public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
        String resource = type.getName().replace(".", "/") + ".java (best guess)";
        this.assistant = new MapperBuilderAssistant(configuration, resource);
        this.configuration = configuration;
        this.type = type;

        sqlAnnotationTypes.add(Select.class);
        sqlAnnotationTypes.add(Insert.class);
        sqlAnnotationTypes.add(Update.class);
        sqlAnnotationTypes.add(Delete.class);
    }

    public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {
            assistant.setCurrentNamespace(type.getName());

            Method[] methods = type.getMethods();
            for (Method method : methods) {
                if (!method.isBridge()) {
                    // 解析语句
                    parseStatement(method);
                }
            }
        }
    }

    /**
     * 解析语句
     *
     * @param method 方法
     */
    private void parseStatement(Method method) {
        Class<?> parameterTypeClass = getParameterType(method);
        LanguageDriver languageDriver = getLanguageDriver(method);
        SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

        if (sqlSource != null) {
            final String mappedStatementId = type.getName() + "." + method.getName();
            SqlCommandType sqlCommandType = getSqlCommandType(method);
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

            String resultMapId = null;
            if (isSelect) {
                resultMapId = parseResultMap(method);
            }

            // 调用助手类
            assistant.addMappedStatement(
                    mappedStatementId,
                    sqlSource,
                    sqlCommandType,
                    parameterTypeClass,
                    resultMapId,
                    getReturnType(method),
                    languageDriver
            );
        }
    }

    /**
     * 重点:DAO方法的返回类型,如果为 List 则需要获取集合中的对象类型
     *
     * @param method 方法
     * @return 对象类型
     */
    private Class<?> getReturnType(Method method) {
        Class<?> returnType = method.getReturnType();
        if (Collection.class.isAssignableFrom(returnType)) {
            Type returnTypeParameter = method.getGenericReturnType();
            if (returnTypeParameter instanceof ParameterizedType) {
                Type[] actualTypeArguments = ((ParameterizedType) returnTypeParameter).getActualTypeArguments();
                if (actualTypeArguments != null && actualTypeArguments.length == 1) {
                    returnTypeParameter = actualTypeArguments[0];
                    if (returnTypeParameter instanceof Class) {
                        returnType = (Class<?>) returnTypeParameter;
                    } else if (returnTypeParameter instanceof ParameterizedType) {
                        returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
                    } else if (returnTypeParameter instanceof GenericArrayType) {
                        Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();
                        // (issue #525) support List<byte[]>
                        returnType = Array.newInstance(componentType, 0).getClass();
                    }
                }
            }
        }
        return returnType;
    }

    private String parseResultMap(Method method) {
        // generateResultMapName
        StringBuilder suffix = new StringBuilder();
        for (Class<?> c : method.getParameterTypes()) {
            suffix.append("-");
            suffix.append(c.getSimpleName());
        }
        if (suffix.length() < 1) {
            suffix.append("-void");
        }
        String resultMapId = type.getName() + "." + method.getName() + suffix;

        // 添加 ResultMap
        Class<?> returnType = getReturnType(method);
        assistant.addResultMap(resultMapId, returnType, new ArrayList<>());
        return resultMapId;
    }

    private SqlCommandType getSqlCommandType(Method method) {
        Class<? extends Annotation> type = getSqlAnnotationType(method);
        if (type == null) {
            return SqlCommandType.UNKNOWN;
        }
        return SqlCommandType.valueOf(type.getSimpleName().toUpperCase(Locale.ENGLISH));
    }

    private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
        try {
            Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
            if (sqlAnnotationType != null) {
                Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
                final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
                return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
            }
            return null;
        } catch (Exception e) {
            throw new RuntimeException("Could not find value method on SQL annotation. Cause: " + e);
        }
    }

    private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
        final StringBuilder sql = new StringBuilder();
        for (String fragment : strings) {
            sql.append(fragment);
            sql.append(" ");
        }
        return languageDriver.createSqlSource(configuration, sql.toString(), parameterTypeClass);
    }

    private Class<? extends Annotation> getSqlAnnotationType(Method method) {
        for (Class<? extends Annotation> type : sqlAnnotationTypes) {
            Annotation annotation = method.getAnnotation(type);
            if (annotation != null) {
                return type;
            }
        }
        return null;
    }

    private LanguageDriver getLanguageDriver(Method method) {
        Class<?> langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
        return configuration.getLanguageRegistry().getDriver(langClass);
    }

    private Class<?> getParameterType(Method method) {
        Class<?> parameterType = null;
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (Class<?> clazz : parameterTypes) {
            if (!RowBounds.class.isAssignableFrom(clazz) && !ResultHandler.class.isAssignableFrom(clazz)) {
                if (parameterType == null) {
                    parameterType = clazz;
                } else {
                    parameterType = MapperMethod.ParamMap.class;
                }
            }
        }
        return parameterType;
    }
}
  • 自定义注解的解析配置,主要在 MapperAnnotationBuilder 类中完成。
    • 整个类在构造函数中配置需要解析的注解,并提供解析方法处理语句的解析。
    • 这个类的解析操作,都是基于 Method 来获取参数类型、返回类型、注解方法等,完成解析过程。
  • 整个解析的核心流程。
    • 根据 Method#getParameterTypes 方法获取入参类型。
    • Configuration 配置项中获取默认的 LanguageDriver 脚本语言驱动。
    • 以及基于注解所提供的配置信息,也就是 value 值中的 SQL 来创建 SqlSource 语句。
  • 这些基本的信息创建完成以后,则根据 SqlCommandType 的命令类型为 SELECT 时,创建出 ResultMap 信息。
    • 这个 ResultMap 会被写入到 Configuration 配置项的 Map<String, ResultMap> resultMaps
  • 整体准备好这些基本配置以后,则会调用 MapperBuilderAssistant 映射构建器助手,存入映射器语句。往后的流程都是一样。
  • 注意getReturnType(Method method) 方法的使用。它有一个非常核心的问题点,在于要拿到方法的返回类型:
    • 如果是普通的基本类型或者对象类型,直接就可以返回。
    • 如果是集合类型,则需要通过 Collection.class.isAssignableFrom 判断,再进行集合中参数类型的获取。
      • 例如:List<User> 则需要根据 method.getGenericReturnType() 获取返回类型,并判断是否为 Class 进行返回具体的类型。

3.5 Mapper XML解析调用

  • Mybatis 的源码中,是基于 XML 配置构建器解析 Mapper 时进行判断处理,是 xml 还是注解。
  • 如果是注解则会调用到 MapperRegistry#addMapper 方法,并开始执行解析注解的相关操作。

3.5.1 修改配置项

Configuration.java

package com.lino.mybatis.session;

import com.lino.mybatis.binding.MapperRegistry;
import com.lino.mybatis.datasource.druid.DruidDataSourceFactory;
import com.lino.mybatis.datasource.pooled.PooledDataSourceFactory;
import com.lino.mybatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.SimpleExecutor;
import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.executor.resultset.DefaultResultSetHandler;
import com.lino.mybatis.executor.resultset.ResultSetHandler;
import com.lino.mybatis.executor.statement.PreparedStatementHandler;
import com.lino.mybatis.executor.statement.StatementHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.reflection.factory.DefaultObjectFactory;
import com.lino.mybatis.reflection.factory.ObjectFactory;
import com.lino.mybatis.reflection.wrapper.DefaultObjectWrapperFactory;
import com.lino.mybatis.reflection.wrapper.ObjectWrapperFactory;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.scripting.LanguageDriverRegistry;
import com.lino.mybatis.scripting.xmltags.XMLLanguageDriver;
import com.lino.mybatis.transaction.Transaction;
import com.lino.mybatis.transaction.jdbc.JdbcTransactionFactory;
import com.lino.mybatis.type.TypeAliasRegistry;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @description: 配置项
 */
public class Configuration {

    /**
     * 环境
     */
    protected Environment environment;
    /**
     * 映射注册机
     */
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);
    /**
     * 映射的语句,存在Map里
     */
    protected final Map<String, MappedStatement> mappedStatements = new HashMap<>(16);
    /**
     * 结果映射,存在Map里
     */
    protected final Map<String, ResultMap> resultMaps = new HashMap<>(16);

    ...

    public ResultMap getResultMap(String id) {
        return resultMaps.get(id);
    }

    public void addResultMap(ResultMap resultMap) {
        resultMaps.put(resultMap.getId(), resultMap);
    }
}
  • 添加 Map<String, ResultMap> resultMaps 结果映射的列表。

3.5.2 修改映射构建器助手

MapperBuilderAssistant.java

package com.lino.mybatis.builder;

import com.lino.mybatis.mapping.*;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import java.util.ArrayList;
import java.util.List;

/**
 * @description: 映射构建器助手,建造者
 */
public class MapperBuilderAssistant extends BaseBuilder {

    private String currentNamespace;
    private String resource;

    public MapperBuilderAssistant(Configuration configuration, String resource) {
        super(configuration);
        this.resource = resource;
    }

    public String getCurrentNamespace() {
        return currentNamespace;
    }

    public void setCurrentNamespace(String currentNamespace) {
        this.currentNamespace = currentNamespace;
    }

    public String applyCurrentNamespace(String base, boolean isReference) {
        if (base == null) {
            return null;
        }
        if (isReference) {
            if (base.contains(".")) {
                return base;
            }
        } else {
            if (base.startsWith(currentNamespace + ".")) {
                return base;
            }
            if (base.contains(".")) {
                throw new RuntimeException("Dots are not allowed in element names, please remove it from " + base);
            }
        }
        return currentNamespace + "." + base;
    }

    /**
     * 添加映射器语句
     */
    public MappedStatement addMappedStatement(String id, SqlSource sqlSource, SqlCommandType sqlCommandType,
                                              Class<?> parameterType, String resultMap, Class<?> resultType,
                                              LanguageDriver lang) {
        // 给id加上namespace前缀:com.lino.mybatis.test.dao.IUserDao.queryUserInfoById
        id = applyCurrentNamespace(id, false);
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlCommandType, sqlSource, resultType);

        // 结果映射, 给 MappedStatement#resultMaps
        setStatementResultMap(resultMap, resultType, statementBuilder);

        MappedStatement statement = statementBuilder.build();
        // 映射语句信息,建造完存放到配置项中
        configuration.addMappedStatement(statement);

        return statement;
    }

    private void setStatementResultMap(String resultMap, Class<?> resultType, MappedStatement.Builder statementBuilder) {
        // 因为暂时还没有在 Mapper XML 中配置 Map 返回结果,所以这里返回的是 null
        resultMap = applyCurrentNamespace(resultMap, true);

        List<ResultMap> resultMaps = new ArrayList<>();

        if (resultMap != null) {
            String[] resultMapNames = resultMap.split(",");
            for (String resultMapName : resultMapNames) {
                resultMaps.add(configuration.getResultMap(resultMapName.trim()));
            }
        }
        /*
         * 通常使用 resultType 即可满足大部分场景
         * <select id="queryUserInfoById" resultType="com.lino.mybatis.test.po.User">
         * 使用 resultType 的情况下,Mybatis 会自动创建一个 ResultMap,基于属性名称映射列到 JavaBean 的属性上。
         */
        else if (resultType != null) {
            ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
                    configuration, statementBuilder.id() + "-Inline", resultType, new ArrayList<>());
            resultMaps.add(inlineResultMapBuilder.build());
        }
        statementBuilder.resultMaps(resultMaps);
    }

    public ResultMap addResultMap(String id, Class<?> type, List<ResultMapping> resultMappings) {
        ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration, id, type, resultMappings);

        ResultMap resultMap = inlineResultMapBuilder.build();
        configuration.addResultMap(resultMap);
        return resultMap;
    }
}
  • 修改 applyCurrentNamespace 方法,添加对命名空间的全称判断。
  • 修改 setStatementResultMap 方法, 完善 resultMap != null 时的逻辑处理。
  • 添加 addResultMap 方法,添加返回值集合。

3.5.3 XML配置构建器

XMLConfigBuilder

package com.lino.mybatis.builder.xml;

import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.datasource.DataSourceFactory;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.transaction.TransactionFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;
import javax.sql.DataSource;
import java.io.InputStream;
import java.io.Reader;
import java.util.List;
import java.util.Properties;

/**
 * @description: XML配置构建器,建造者模式,集成BaseBuilder
 */
public class XMLConfigBuilder extends BaseBuilder {

    ...

    /**
     * <mappers>
     * <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
     * <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
     * <mapper resource="org/mybatis/builder/PostMapper.xml"/>
     * </mappers>
     */
    private void mapperElement(Element mappers) throws Exception {
        List<Element> mapperList = mappers.elements("mapper");
        for (Element e : mapperList) {
            String resource = e.attributeValue("resource");
            String mapperClass = e.attributeValue("class");
            // XML解析
            if (resource != null && mapperClass == null) {
                InputStream inputStream = Resources.getResourceAsStream(resource);
                // 在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource);
                mapperParser.parse();
            }
            // Annontation 注解解析
            else if (resource == null && mapperClass != null) {
                Class<?> mapperInterface = Resources.classForName(mapperClass);
                configuration.addMapper(mapperInterface);
            }
        }
    }
}
  • XMLConfigBuilder 配置构建器的 Mapper 解析处理中,根据从 XML 配置获取到的 resource、class 分别进行判断解析。
  • 如果 resource 为空,mapperClass 不为空,则进行注解的解析处理。
    • 在这段代码中则是根据 mapperClass 获取对应的接口,并通过 Configuration#addMapper 方法,添加到配置项中。
    • 而这个 Mapper 的添加会调用到 MapperRegistry 进而调用注解解析操作。

3.5.4 映射器注册机

MapperRegistry.java

package com.lino.mybatis.binding;

import cn.hutool.core.lang.ClassScanner;
import com.lino.mybatis.builder.annotation.MapperAnnotationBuilder;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.SqlSession;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @description: 映射器注册机
 */
public class MapperRegistry {

    private Configuration configuration;

    public MapperRegistry(Configuration configuration) {
        this.configuration = configuration;
    }

    /**
     * 将已添加的映射器代理加入到HashMap
     */
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(16);

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new RuntimeException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new RuntimeException("Error getting mapper instance. Cause: " + e, e);
        }
    }

    public <T> void addMapper(Class<T> type) {
        /* Mapper 必须是接口才会注册 */
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // 如果重复添加,报错
                throw new RuntimeException("Type " + type + " is already known to the MapperRegistry.");
            }
            // 注册映射器代理工厂
            knownMappers.put(type, new MapperProxyFactory<>(type));

            // 解析注解类语句配置
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(configuration, type);
            parser.parse();
        }
    }

    public <T> boolean hasMapper(Class<T> type) {
        return knownMappers.containsKey(type);
    }

    public void addMappers(String packageName) {
        Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
        for (Class<?> mapperClass : mapperSet) {
            addMapper(mapperClass);
        }
    }
}
  • addMapper 方法中,根据 Class 注册完映射器代理工厂后,则开始进行解析注解操作。

四、测试:注解配置执行SQL

4.1 测试环境配置

4.1.1 配置数据源配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--XML配置-->
        <!--        <mapper resource="mapper/User_Mapper.xml"/>-->
        <!--注解配置-->
        <mapper class="com.lino.mybatis.test.dao.IUserDao"/>
    </mappers>
</configuration>

4.1.2 修改IUserDao用户持久层

IUserDao.java

package com.lino.mybatis.test.dao;

import com.lino.mybatis.annotations.Delete;
import com.lino.mybatis.annotations.Insert;
import com.lino.mybatis.annotations.Select;
import com.lino.mybatis.annotations.Update;
import com.lino.mybatis.test.po.User;
import java.util.List;

/**
 * @Description: 用户持久层
 */
public interface IUserDao {

    /**
     * 根据ID查询用户信息
     *
     * @param uId ID
     * @return User 用户
     */
    @Select("SELECT id, userId, userName, userHead FROM user WHERE id = #{id}")
    User queryUserInfoById(Long uId);

    /**
     * 根据用户对象查询用户信息
     *
     * @param user 用户
     * @return User 用户
     */
    @Select("SELECT id, userId, userName, userHead FROM user WHERE id = #{id} and userId = #{userId}")
    User queryUserInfo(User user);

    /**
     * 查询用户对象列表
     *
     * @return List<User> 用户列表
     */
    @Select("SELECT id, userId, userName, userHead FROM user")
    List<User> queryUserInfoList();

    /**
     * 更新用户信息
     *
     * @param user 用户对象
     * @return 受影响的行数
     */
    @Update("UPDATE user SET userName = #{userName} WHERE id = #{id}")
    int updateUserInfo(User user);

    /**
     * 新增用户信息
     *
     * @param user 用户对象
     * @return 受影响的行数
     */
    @Insert("INSERT INTO user (userId, userName, userHead, createTime, updateTime) VALUES (#{userId}, #{userName}, #{userHead}, now(), now())")
    int insertUserInfo(User user);

    /**
     * 根据ID删除用户信息
     *
     * @param uId ID
     * @return 受影响的行数
     */
    @Delete("DELETE FROM user WHERE userId = #{userId}")
    int deleteUserInfoByUserId(String uId);
}

4.2 单元测试

4.2.1 插入测试

ApiTest.java

@Test
public void test_insertUserInfo() {
    // 1.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2.测试验证
    User user = new User();
    user.setUserId("10002");
    user.setUserName("张三");
    user.setUserHead("1_05");
    userDao.insertUserInfo(user);
    logger.info("测试结果:{}", "Insert OK");

    // 3.提交事务
    sqlSession.commit();
}

测试结果

16:32:32.510 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userId propertyType:class java.lang.String
16:32:32.510 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userName propertyType:class java.lang.String
16:32:32.510 [main] INFO  c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userHead propertyType:class java.lang.String
16:32:33.171 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:32:33.213 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
16:32:33.213 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"张三"
16:32:33.213 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"1_05"
16:32:33.213 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:Insert OK

在这里插入图片描述

  • 从测试日志信息和数据库的截图,可以看到数据已经插入到数据库,验证通过
  • 注意:执行完 SQL 以后,还执行一次 sqlSession.commit()
    • 这是因为在 DefaultSqlSessionFactory#openSession 开启 Session 创建事务工厂的时候,传入给事务工厂构造函数的事务是否自动提交为 false
    • 所以这里就需要我们去手动提交事务,否则是不会插入到数据库中的。

4.2.2 查询测试(多条数据)

ApiTest.java

@Test
public void test_queryUserInfoList() {
    // 1.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2.测试验证: 对象参数
    List<User> users = userDao.queryUserInfoList();
    logger.info("测试结果:{}", JSON.toJSONString(users));
}

测试结果

16:40:47.699 [main] INFO  c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IUserDao.queryUserInfoList parameter:null
16:40:48.361 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 1192171522.
16:40:48.395 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:[{"id":1,"userHead":"1_04","userId":"10001","userName":"小零哥"},{"id":4,"userHead":"1_05","userId":"10002","userName":"张三"}]
  • 现在我们再查询结果的时候,就可以查询到2条记录的集合了,这说明我们添加的 MapperMethod#execute 调用 sqlSession.selectList(command.getName(), param) 是测试通过的。

4.2.3 修改测试

ApiTest.java

@Test
public void test_updateUserInfo() {
    // 1.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2.测试验证
    int count = userDao.updateUserInfo(new User(1L, "10001", "小灵哥"));
    logger.info("测试结果:{}", count);

    // 3.提交事务
    sqlSession.commit();
}

测试结果

16:44:11.979 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:44:12.027 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"小灵哥"
16:44:12.028 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1
16:44:12.037 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:1

在这里插入图片描述

  • 这里测试验证把 ID=1 的用户,userName 修改为 小灵哥,通过测试日志和数据库截图,测试通过。

4.2.4 删除测试

ApiTest.java

@Test
public void test_deleteUserInfoByUserId() {
    // 1.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2.测试验证
    int count = userDao.deleteUserInfoByUserId("10002");
    logger.info("测试结果:{}", count == 1);

    // 3.提交事务
    sqlSession.commit();
}

测试结果

16:47:54.591 [main] INFO  c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:47:54.633 [main] INFO  c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
16:47:54.643 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:true

在这里插入图片描述

  • 这里把数据表中 userId = '10002' 的用户删除掉,通过测试日志和数据库截图,测试通过。

五、总结:注解配置执行SQL

  • 在原有解析 Mapper XML 的基础上扩展了使用注解方式的解析和处理,让整个框架功能更加完善。同时在扩展注解功能的结构时候,可以看到整个整合过程并不复杂,更多是类似模块式的拼装,通过开发出一个注解解析构建器,并在 Mapper 注册过程中完成调用和解析操作。
    • 所以如果一个框架的整体设计是完善的,那么在功能的迭代和添加过程中也会是非常清晰容易的。
  • 在整个内容的实现中,主要以串联核心流程为主,剔除掉一些分支过程。这主要是因为很多分支过程都是在处理一些各类场景的情况。
    • 而我们学习源码是需要掌握主脉络且不被太多分支流程干扰的,才能把主干流程理顺。
  • 本章处理注解时,只是添加了 @Insert、@Delete、@Update、@Select 四个注解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/970987.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Apple Watch 9或使用粘结剂喷射金属3D打印生产表壳

包括路透社在内的众多报道表明&#xff0c;苹果公司目前正在测试用于批量生产Apple Watch Series 9表壳的粘结剂喷射金属3D打印技术&#xff0c;该产品将于9月12日发布。报告称&#xff0c;该生产技术将不再需要将金属部件切割成产品形状&#xff0c;从而减少了产品制造所需的时…

股票交易与先胜而后战

胜在敌败在己&#xff0c;出自《孙子兵法》形篇。 孙子曰&#xff1a;昔之善战者&#xff0c;先为不可胜&#xff0c;以待敌之可胜。不可胜在己&#xff0c;可胜在敌。故善战者&#xff0c;能为不可胜&#xff0c;不能使敌之可胜。故曰&#xff1a;胜可知而不可为。 以前的文章…

接口自动化测试系列-登陆态问题方案

cookie实例 session requests.session()#验证码&#xff0c;我们测试环境用的万能验证码validate_code 1234# 获取登录认证SID#url地址url_login f"http://ops-web-{env}/login.jsp"#获取cookie,并转换成json格式sid requests.get(urlurl_login).cookies.get_dic…

【MySQL】6、MySQL主从复制与读写分离

6.NULL值 select length(null),length(),length(123);null 为空值&#xff0c;但也会占用空间&#xff1b;NULL不会被统计记录不占用空间&#xff0c;但会被统计记录什么是读写分离 读写分离&#xff0c;基本的原理是让主数据库处理事务性增、改、删操作&#xff08;INSERT…

贷款公司如何精准获客,大数据获客

近年来&#xff0c;贷款中介机构在金融服务领域发挥着越来越重要的作用。随着时代的发展&#xff0c;贷款中介机构不仅是贷款服务的提供者&#xff0c;也是能够帮助客户更准确获取客户的服务提供者。 为此&#xff0c;贷款中介机构应把握以下几个方面。 首先&#xff0c;贷款…

自动化与人工智能:‘爱校对’在政府网站巡检中的应用与局限性

在当今数字化时代&#xff0c;自动化和人工智能技术正逐渐渗透到各个领域&#xff0c;政府网站巡检也不例外。"爱校对"等自动化文本校对工具在政府网站巡检中具有许多应用&#xff0c;但同时也存在一些局限性。 应用方面&#xff1a;提高效率&#xff1a; 自动化文本…

Proxifier配置两次代理

proxifier配置两次代理。以前只会配置一次代理&#xff0c;然后在某个环境中&#xff0c;需要进行多级跳转&#xff0c;先登录跳板机A机器&#xff0c;然后从A机器跳转到B机器&#xff0c;网址ip1需要通过机器B的代理访问。 代理方式&#xff1a;配置两下就行了&#xff0c;如…

LeetCode 2511 最多可以摧毁的敌人城堡数目

LeetCode 2511 最多可以摧毁的敌人城堡数目 力扣题目链接&#xff1a;力扣题目链接 给你一个长度为 n &#xff0c;下标从 0 开始的整数数组 forts &#xff0c;表示一些城堡。forts[i] 可以是 -1 &#xff0c;0 或者 1 &#xff0c;其中&#xff1a; -1 表示第 i 个位置 没…

“一个都不能死”在线小游戏源代码

这款名为”一个都不能死”的网页在线小游戏是一款自适应PC和手机的网页游戏。你只需要准备一台服务器或主机&#xff0c;将游戏文件解压并上传到服务器上&#xff0c;就可以开始游戏了。 如果需要进行一些文字内容的修改&#xff0c;你可以在js/main.js文件中进行相应的修改。…

HDFS 集群动态节点管理

目录 一、动态扩容、节点上线 1.1 背景 1.2 扩容步骤 1.2.1 新机器基础环境准备 1.2.2 Hadoop 配置 1.2.3 手动启动 DataNode 进程 1.2.4 Web 页面查看情况 1.2.5 DataNode 负载均衡服务 二、动态缩容、节点下线 2.1 背景 2.2 缩容步骤 2.2.1 添加退役节点 …

dubbo服务管控

我们已经介绍了Dubbo在服务治理方面提供的特性&#xff0c;今天我们一起来看看Dubbo在其它方面提供的特性。同服务治理篇一样&#xff0c;本文的目的在于学会使用Dubbo在服务管控方面提供的特性&#xff0c;依旧不涉及任何实现原理。 工程结构 嗯~~ 是这样的&#xff0c;因为…

python webdriver 测试框架数据驱动json文件驱动的方式

简介&#xff1a; 数据驱动excel驱动方式,就是数据配置在excel里面&#xff0c;主程序调用的时候每次用从excel里取出的数据作为参数&#xff0c;进行操作&#xff0c; 需要掌握的地方是对excel的操作&#xff0c;要灵活的找到目标数据 测试数据.xlsx: 路径-D:\test\0627 E…

设计模式之代理模式与外观模式

目录 代理模式 简介 优缺点 角色职责 实现 运用场景 外观模式 简介 角色职责 优缺点 实现 使用场景 代理模式 简介 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时&#xff0c;访问对象不适合或者不能直接引用目标对象&#xff0c;代理对象作为…

京东搜索EE链路演进 | 京东云技术团队

导读 搜索系统中容易存在头部效应&#xff0c;中长尾的优质商品较难获得充分的展示机会&#xff0c;如何破除系统的马太效应&#xff0c;提升展示结果的丰富性与多样性&#xff0c;助力中长尾商品成长是电商平台搜索系统的一个重要课题。其中&#xff0c;搜索EE系统在保持排序…

北美电池测试标准解读:UL 1642、UL 2054、UL 2056、UL 2089 测试报告

在日常生活中人们与各类电子产品的交流和互动越来越多&#xff0c;由于电池与人们的接触日益密切&#xff0c;电池的安全性显得格外重要&#xff0c;因此越来越多商家注重保证电池的安全性&#xff0c;避免因电池安全事故带来的人身伤害和品牌名誉损失。特别是亚马逊今年开年来…

无涯教程-JavaScript - DMIN函数

描述 DMIN函数返回列表或数据库中符合您指定条件的列中的最小数字。 语法 DMIN (database, field, criteria)争论 Argument描述Required/Optionaldatabase 组成列表或数据库的单元格范围。 数据库是相关数据的列表,其中相关信息的行是记录,数据的列是字段。列表的第一行包含…

动辄百万的工业数据处理软件,现在60秒就能用上

刚刚过去的周五&#xff0c;TDengine团队正式发布了新版3.1.1.0, 并上线了新的官网。虽没有发布会&#xff0c;但对涛思数据而言&#xff0c;这是一个“蓄谋已久”的动作。因为新版TDengine里包含了一个核心模块taosX&#xff0c;它具备强大的数据抓取、清洗、转换、加载(ETL)功…

金九银十,软件测试最新面试题整理(超详细~)

1、创建坐席组的功能模块&#xff0c;如何进行测试用例设计&#xff1f; 解答&#xff1a; 功能测试&#xff0c;使用等价类划分法去分析创建坐席的每个输入项的有效及无效类&#xff0c;同步考虑边界值去设计对应的测试用例&#xff1a; 先进行冒烟测试&#xff0c;正常创建…

微信小程序新版canvas2d海报绘制(教你轻松搞定)

效果 说明&#xff1a; canvas官方很早已经发声不再维护了&#xff0c;所以很多方法都已经不再适用。目前官方推荐适用canvas2d来绘制生成海报。 canvas2d来绘制海报&#xff1a; canvas2d的优点&#xff1a; 例如&#xff1a; 不需要先预下载网络图片再绘制&#xff0c;前…

【Hello Algorithm】二叉树相关算法

本篇博客介绍&#xff1a;介绍二叉树的相关算法 二叉树相关算法 二叉树结构遍历二叉树递归序二叉树的交集非递归方式实现二叉树遍历二叉树的层序遍历 二叉树难题二叉树的序列化和反序列化lc431求二叉树最宽的层二叉树的后继节点谷歌面试题 二叉树结构 如果对于二叉树的结构还有…