首页 > 数据库 >由 Mybatis 源码畅谈软件设计(二):MappedStatement 和 SqlSource

由 Mybatis 源码畅谈软件设计(二):MappedStatement 和 SqlSource

时间:2024-12-23 10:52:56浏览次数:3  
标签:... MappedStatement SqlSource private 源码 SQL new public

作者:京东保险 王奕龙

本节我们来介绍 org.apache.ibatis.mapping.MappedStatement(映射SQL语句声明的类),它是 MyBatis 框架中的一个核心类,也是向后不断学习 Mybatis 源码的基础。在这部分源码中,最值得关注的设计原则是“信息隐藏”,它是在《软件设计哲学》中提到的一个观点,简单来说就是将把外部类不需要了解的信息隐藏在类内部,遵循最小知识原则,在与 MappedStatement 相关的类中,定义了很多内部类和内部接口,它们只在某些类内访问。此外,MappedStatement 的创建完美地遵循了 建造者模式,这也是学习该模式很好的实例。这部分源码较多,在看这部分源码时,也需要留意一下方法的长度,尝试着理解在《代码整洁之道》中强调的“每个方法只做一件事”的原则,考虑书中提到的按照方法行数拆分多个小方法的原则到底合适不合适,并关注方法的编排顺序,它是胡乱的安排方法的位置还是有一定规律?接下来,我们步入正文。

MappedStatement 负责存储和管理映射 SQL 语句的详细信息,每个 MappedStatement 对象对应 XML 映射文件中一个 <select>, <insert>, <update>, 或 <delete> 标签,其中重要字段内容信息如下:


public final class MappedStatement {
    
    // 每个 MappedStatement 对象都有一个唯一的 ID,用于在 MyBatis 配置中标识和引用该语句(Mapper接口中的方法的全路径名称)
    // eg: org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor
    private String id;
    // sqlSource 存储 SQL 语句,区分静态、动态SQL
    private SqlSource sqlSource;
    // 描述输入参数的类型和映射关系
    private ParameterMap parameterMap;
    // 描述返回结果的类型和映射关系
    private List<ResultMap> resultMaps;
    // SQL 类型
    private SqlCommandType sqlCommandType;
    // 数据库厂商标识,用于多数据库支持
    private String databaseId;
    // Mapper.xml 文件的路径,eg: org/apache/ibatis/builder/AuthorMapper.xml
    private String resource;
    
    // ...
}

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode 方法是将 xml 文件中配置的 SQL 实例化成 MappedStatement 对象的方法:


public class XMLStatementBuilder extends BaseBuilder {

    // ...
    
    private final MapperBuilderAssistant builderAssistant;

    public void parseStatementNode() {
        // 处理配置的参数 
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        // ...
        
        // 创建 SqlSource
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        // ...
        
        // 创建 MappedStatement 对象
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
                parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
    }
}

验证创建 MappedStatement 对象过程逻辑采用的单测为 org.apache.ibatis.session.SqlSessionTest.shouldExecuteSelectOneAuthorUsingMapperClass,解析的 xml 文件为 AuthorMapper.xml

在上述源码步骤中 SqlSource 的创建非常重要,该接口的源码注释如下:

Represents the content of a mapped statement read from an XML file or an annotation. It creates the SQL that will be passed to the database out of the input parameter received from the user.

表示从 XML 文件或注释读取的 SQL 语句,它可以根据用户输入的参数创建之后传递给数据库的SQL(org.apache.ibatis.mapping.SqlSource#getBoundSql 方法)。接下来我们重点看下这个接口:


public interface SqlSource {

    // 接受参数对象,并返回 BoundSql 对象。BoundSql 包含了生成的 SQL 语句以及相应的参数信息
    BoundSql getBoundSql(Object parameterObject);

}

MyBatis 提供了几个 SqlSource 的实现类,每个实现类适用于不同的场景:

  1. StaticSqlSource: 用于处理静态 SQL 语句
  2. DynamicSqlSource: 用于处理动态 SQL 语句(包含 ${} 占位符和 <if>, <choose>, <when>, <otherwise>, <foreach> 等动态标签)
  3. RawSqlSource: 这个实现类用于解析包含 #{} 占位符的 SQL 语句,其内部实现依然是 StaticSqlSource
  4. ProviderSqlSource: 用于处理使用 @Provider 注解的 SQL

它的实例化借助了 XMLScriptBuilder 实现:


public class XMLLanguageDriver implements LanguageDriver {
    // ...

    @Override
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }
}

XMLScriptBuilder 中定义了 内部接口 动态标签解析器 NodeHandler,它对多种标签解析做了实现,负责将不同标签内容解析成不同的 SqlNode 对象:


public class XMLScriptBuilder extends BaseBuilder {
    
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

    public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
        super(configuration);
        this.context = context;
        this.parameterType = parameterType;
        // 初始化SQL节点处理器
        initNodeHandlerMap();
    }
    
    private void initNodeHandlerMap() {
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }

    // 定义内部接口,因为其只与 XMLScriptBuilder 相关,隐藏在内部不泄露知识,降低复杂度
    private interface NodeHandler {
        void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
    }

    private class WhereHandler implements NodeHandler {
        // ...
    }
    
    // ...
}

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.NodeHandler#handleNode 方法的第 2 个入参为 SQL 的节点判断信息,我们需要了解下 SqlNode 接口:


public interface SqlNode {

    /**
     * 负责将当前节点的 SQL 片段应用到上下文中
     */
    boolean apply(DynamicContext context);
    
}

它的实现类如下所示:

SqlNode.png

  • StaticTextSqlNode 用于处理不包含占位符的纯文本节点
  • TextSqlNode 用于处理包含 ${} 占位符的文本节点,它会 直接将占位符中的内容拼接到 SQL 上,所以它能实现 动态 SQL,与处理 #{} 占位符不同,#{} 占位符在处理字符串类型时,会添加上 ' 单引号,避免 SQL 注入问题,注意它们的区别,后续针对动态 SQL 问题还会讲解
  • MixedSqlNode 比较特殊,因为 SQL 被解析完毕后,会有多个节点片段,这个对象是将所有的节点(SqlNode)保存起来,通过遍历来触发将 SQL 应用到上下文的 apply 方法:

public class MixedSqlNode implements SqlNode {
    // 保存 SQL 中所有的节点片段
    private final List<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        contents.forEach(node -> node.apply(context));
        return true;
    }
}

其他实现类根据其命名表示 SQL 中对应的不同标签节点,非常简单就不再赘述了。我们继续回到 XMLScriptBuilder#parseScriptNode` 方法,它会对 xml 文件中定义的 SQL 进行解析:


public class XMLScriptBuilder extends BaseBuilder {
    // ...
    
    public SqlSource parseScriptNode() {
        // 将 xml 文件中定义的 SQL 解析成不同的 SqlNode 对象,并都记录在 MixedSqlNode 对象中
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource;
        if (isDynamic) {
            // 包含动态标签的 SQL
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            // 静态 SQL 会将占位符 #{} 替换成 ?
            sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
    
    // ...
}

可以发现它会根据静态或动态 SQL 创建不同的 SqlSource 对象,那么我们接下来分别看一下两种不同 SQL 的解析过程。

静态 SQL 的解析

以如下 SQL 为例:


<select id="selectAuthor" parameterMap="selectAuthor" resultMap="selectAuthor">
    select id, username, password, email, bio, favourite_section
    from author where id = #{id,jdbcType=INTEGER, javaType=int}
</select>

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.parseDynamicTags 方法会执行具体的 SQL 解析逻辑,它会执行如下逻辑:如果包含 ${} 占位符,会被标识为动态 SQL,对应的类型为 TextSqlNode,否则为 StaticTextSqlNode 对象,如下逻辑:


public class XMLScriptBuilder extends BaseBuilder {

    public SqlSource parseScriptNode() {
        // 将 xml 文件中定义的 SQL 解析成不同的 SqlNode 对象,并都记录在 MixedSqlNode 对象中
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        // ...
    }
    
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<>();

        NodeList children = node.getNode().getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            XNode child = node.newXNode(children.item(i));
            if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
                String data = child.getStringBody("");
                TextSqlNode textSqlNode = new TextSqlNode(data);
                // 包含 ${} 占位符的会被判断为动态 SQL,并被记录为 TextSqlNode
                if (textSqlNode.isDynamic()) {
                    contents.add(textSqlNode);
                    isDynamic = true;
                } else {
                    contents.add(new StaticTextSqlNode(data));
                }
            } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
                // ...
            }
        }

        return new MixedSqlNode(contents);
    }
    
}

其中 textSqlNode.isDynamic() 步骤需要关注。TextSqlNode 对象中定义了 DynamicCheckerTokenParser 内部类,它实现了 TokenHandler 接口,在路径 org.apache.ibatis.parsing 下,根据包路径描述 “Parsing utils”,可知该接口是解析工具类通用接口,不过 DynamicCheckerTokenParser 并没有做什么需要特别关注的逻辑,只是用于标记 SQL 为静态 SQL 还是动态 SQL(isDynamic 字段):


private static class DynamicCheckerTokenParser implements TokenHandler {

    private boolean isDynamic;

    public DynamicCheckerTokenParser() {
        // Prevent Synthetic Access
    }

    public boolean isDynamic() {
        return isDynamic;
    }

    @Override
    public String handleToken(String content) {
        this.isDynamic = true;
        return null;
    }
}

textSqlNode.isDynamic() 方法中,创建了 DynamicCheckerTokenParser 对象和 GenericTokenParser 对象。


public class TextSqlNode implements SqlNode {

    // ...

    public boolean isDynamic() {
        DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
        GenericTokenParser parser = createParser(checker);
        parser.parse(text);
        return checker.isDynamic();
    }

    // ...
}

GenericTokenParser 译为通用的 Token 解析器,同样也在 org.apache.ibatis.parsing 包路径下,它会完成判断是否为动态 SQL 的逻辑(仅判断是否包含 ${} 占位符),之后会经常看到它的身影,其中包含三个字段:


public class GenericTokenParser {

    // 要匹配的 占位符左边界信息
    private final String openToken;
    // 要匹配的 占位符右边界信息
    private final String closeToken;
    // 组合 TokenHandler 来实现“替换占位符”的功能,符合单一职责的原则
    private final TokenHandler handler;

    // ...
    
    // 解析 SQL 操作
    public String parse(String text) {
        // ...
    }
    
}

继续回到 org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.parseDynamicTags 方法,如果在 SQL 中匹配到 ${}DynamicCheckerTokenParser 会将 isDynamic 标记为 true,当前 SQL 节点会被定义为 TextSqlNode 类型,否则为 StaticTextSqlNode 类型,因为在示例中定义的 SQL 是带有 #{} 占位符的 SQL,所以 SqlSource 会被解析成 RawSqlSource 类型,并在其中组合 StaticSqlSource 对象。


public class RawSqlSource implements SqlSource {

    private final SqlSource sqlSource;

    public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> clazz = parameterType == null ? Object.class : parameterType;
        // 静态 SQL 在此处被解析,包含 #{} 占位符的会被替换为 ?
        sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
    }

    // ...
}

RawSqlSource 的构造方法中,完成了替换占位符的操作,SqlSourceBuilder 对象需要重点关注,由其命名来看,它是 SqlSource 对象的“建造者(Builder)”,但是它并没有严格遵守建造者模式,不过是将复杂的构造对象的逻辑隐藏起来(信息隐藏):


public class SqlSourceBuilder extends BaseBuilder {

    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        // 用于将占位符替换为 ? 的 handler
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
                additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql;
        // 配置是否去掉 SQL 中的空格
        if (configuration.isShrinkWhitespacesInSql()) {
            sql = parser.parse(removeExtraWhitespaces(originalSql));
        } else {
            // 在这里将 #{} 占位符 替换为 ?,并由 ParameterMappingTokenHandler 解析参数映射信息
            sql = parser.parse(originalSql);
        }
        // 最终参数映射信息会保存在 StaticSqlSource 中
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }

}

ParameterMappingTokenHandlerSqlSourceBuilder 中的 内部类,它同样实现了 TokenHandler 接口,也是一个解析工具类,与上文提到过的 GenericTokenParser 解析器组合使用。它会将占位符中的信息解析为 ParameterMapping 信息,并将占位符替换为 ? (handleToken 方法):


private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    // 参数映射信息
    private final List<ParameterMapping> parameterMappings = new ArrayList<>();
    private final Class<?> parameterType;
    private final MetaObject metaParameters;

    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType,
                                        Map<String, Object> additionalParameters) {
        super(configuration);
        this.parameterType = parameterType;
        this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    public List<ParameterMapping> getParameterMappings() {
        return parameterMappings;
    }

    @Override
    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
        // 解析表达式中的参数映射信息
        Map<String, String> propertiesMap = parseParameterMapping(content);
        // 参数字段
        String property = propertiesMap.get("property");
        // ... 处理 parameterType 配置
        
        // ... 处理占位符参数中标记的 javaType, jdbcType 等配置
        // ... 封装 TypeHandler
        
    }
}

ParameterMappingTokenHandler#buildParameterMapping 方法会对参数映射信息进行封装,实际的占位符中表达式的解析是在 parseParameterMapping 中完成的,会继续调用到 ParameterExpression 的构造方法完成解析,ParameterExpression 继承了 HashMap,会将解析结果以键值对的形式保存:


public class ParameterExpression extends HashMap<String, String> {

    private static final long serialVersionUID = -2417552199605158680L;

    public ParameterExpression(String expression) {
        parse(expression);
    }

    private void parse(String expression) {
        int p = skipWS(expression, 0);
        if (expression.charAt(p) == '(') {
            // org/apache/ibatis/builder/SqlSourceBuilder.java:140,expression 表达式内容暂不支持,忽略这段逻辑
            expression(expression, p + 1);
        } else {
            // 只关注这里即可
            property(expression, p);
        }
    }

    // ... 
}

只需关注 property 方法即可,它的作用是对占位符表达式中的 javaType, jdbcType, mode, numericScale, resultMap, typeHandler, jdbcTypeName 属性进行解析,其中常用常见的是 javaType,jdbcType,示例 SQL 会被解析为如下内容并保存下来:


{
  "property": "id",
  "jdbcType": "INTEGER",
  "javaType": "int"
}

大家可以看一下 ParameterExpression 的具体实现,该类的实现遵循了《代码整洁之道》中 每个方法只做一件事,且方法足够短小 的原则,并将方法自上而下排列,读这个类的代码就像读报纸一样,被依赖次数越多的方法越靠下,这也暗示 越靠近类下方的方法越通用

SQL 中占位符被替换为 ?,结果如下:


select id, username, password, email, bio, favourite_section from author where id = ?

最终会被保存在 StaticSqlSource 中的 sql 字段中,表示封装的是静态 SQL:


public class StaticSqlSource implements SqlSource {
    
    // 替换完成 #{} 占位符的 SQL 内容
    private final String sql;
    // 已经解析完成的 ParameterMapping 对象
    private final List<ParameterMapping> parameterMappings;
    private final Configuration configuration;

    // ...
}

现在 RawSqlSource 的构造方法已执行完毕,其内部组合的 SqlSource 对象为 StaticSqlSource,至此 MappedStatementSqlSource 对象的解析便完毕了。


public class RawSqlSource implements SqlSource {
    // StaticSqlSource
    private final SqlSource sqlSource;
    
    // ...
}

动态 SQL 的解析

接下来我们看一个带有动态标签的 SQL 是如何解析的,以 org.apache.ibatis.session.SqlSessionTest#dynamicSqlParse 为例,它的 SQL 如下:


    <select id="selectAuthor" parameterMap="selectAuthor" resultMap="selectAuthor">
        select id, username, password, email, bio, favourite_section
        from author
        <where>
            <if test="id != null">
                id = #{id}
            </if>
        </where>
    </select>

它会被分成两部分,分别为:


    <select id="selectAuthor" parameterMap="selectAuthor" resultMap="selectAuthor">
        select id, username, password, email, bio, favourite_section
        from author
    </select>

    <where>
        <if test="id != null">
            id = #{id}
        </if>
    </where>

对于第一部分的处理与上述静态 SQL 解析的过程一致,不再赘述。第二部分是处理包含动态标签 <where><if> 的内容,它会执行到 XMLScriptBuilder#parseDynamicTags 方法的处理 ELEMENT_NODE 的逻辑中:


public class XMLScriptBuilder extends BaseBuilder {
    // ...
    
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<>();

        NodeList children = node.getNode().getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            XNode child = node.newXNode(children.item(i));
            if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
                // ...
            } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
                // 根据 node 节点名称选择对应的 Handler
                String nodeName = child.getNode().getNodeName();
                NodeHandler handler = nodeHandlerMap.get(nodeName);
                if (handler == null) {
                    throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
                }
                // 指定对应的处理方法
                handler.handleNode(child, contents);
                isDynamic = true;
            }
        }

        return new MixedSqlNode(contents);
    }
}

它会根据标签类型获取到对应的 NodeHandler,执行相应的处理方法。其中部分 Handler 会再调用到 XMLScriptBuilder#parseDynamicTags 方法,出现递归调用,不过具体的执行过程相对简单,但也需要自己 Debug 跟踪验证。

上述 SQL 被解析完成后,SqlNode 表示为:

sqlNode2.png

再回到 XMLScriptBuilder#parseScriptNode 方法:


public class XMLScriptBuilder extends BaseBuilder {
    // ...
    
    public SqlSource parseScriptNode() {
        // 将 xml 文件中定义的 SQL 解析成不同的 SqlNode 对象,并都记录在 MixedSqlNode 对象中
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource;
        if (isDynamic) {
            // 包含动态标签的 SQL
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            // ...
        }
        return sqlSource;
    }
}

动态 SQL 会被标记为 isDynamic == true,最终会被解析成 DynamicSqlSource 对象,如下是它的构造方法:


public class DynamicSqlSource implements SqlSource {

    private final Configuration configuration;
    private final SqlNode rootSqlNode;

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
    }

    // ...
}

非常简单,仅仅是赋值操作,因为它是动态 SQL,会根据入参生成 SQL 信息。到这里,动态 SQL 的解析操作也完成了。下面我们来看一下创建 MappedStatement 使用到的 建造者模式

建造者模式

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode 方法执行 org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement 逻辑创建 MappedStatement 对象时使用了 建造者模式


    public void parseStatementNode() {
        // 定义相关字段的逻辑...
  
        // 方法的入参非常多,之所以能这么写,放弃可复用性,那么该方法不会轻易发生变更
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
                parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
    }

实现该模式对应的类图关系如下:

MappedStatement 建造者模式.drawio.png

  • MapperBuilderAssistant: 对应建造者模式中的 Director,其中定义了构建 MappedStatement 对象各个字段值的逻辑
  • MappedStatement: 要构建的对象本身
  • Builder: 是 MapperStatement 中的静态内部类,定义了链式编程赋值的方法,用于构建 MappedStatement 对象

建造者模式适用于构建字段较多的复杂对象的场景,这样便能将构造 MappedStatement 的复杂逻辑隐藏起来。定义内部类 MappedStatement.Builder 并支持链式编程,是使用建造者模式时常见写法。

至此,创建 MappedStatement 中的要点和 SqlSource 的介绍已经完毕了。

标签:...,MappedStatement,SqlSource,private,源码,SQL,new,public
From: https://www.cnblogs.com/Jcloud/p/18623459

相关文章

  • ssm毕设人事管理系统源码+程序+论文
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容选题背景在当前信息化快速发展的背景下,人事管理作为企业运营的核心环节之一,其效率与准确性直接关系到企业的竞争力和运营效率。关于人事管理系统的研究,现有文......
  • ssm毕设人事管理系统源码+程序+论文
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容选题背景在当前信息化快速发展的背景下,人事管理作为企业运营的核心环节之一,其效率与准确性直接关系到企业的竞争力和可持续发展能力。关于人事管理系统的研究......
  • ssm毕设人事考勤管理系统源码+程序+论文
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容选题背景随着信息技术的快速发展,企业管理方式正向信息化、智能化方向转变。人事考勤管理作为企业日常运营的关键环节,其效率和准确性直接影响到企业的运营效率......
  • ssm毕设人文社团管理系统源码+程序+论文
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容选题背景关于人文社团管理系统的研究,现有研究主要集中在社团活动的组织与管理、会员信息管理等方面,但专门针对人文社团管理系统化、信息化建设的研究较少。随......
  • ssm毕设日语学习App源码+程序+论文
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容选题背景在全球化的今天,日语作为重要的国际交流语言之一,其学习需求日益增长。关于日语学习的研究,现有文献主要集中在教学方法、学习策略和语言习得理论等方面......
  • 《Java源码分析》:Java NIO 之 Selector(第二部分selector.select())
     作者简介:大家好,我是码炫码哥,前中兴通讯、美团架构师,现任某互联网公司CTO,兼职码炫课堂主讲源码系列专题代表作:《jdk源码&多线程&高并发》,《深入tomcat源码解析》,《深入netty源码解析》,《深入dubbo源码解析》,《深入springboot源码解析》,《深入spring源码解析》,《深入redis源......
  • 《Java源码分析》:Java NIO 之 Selector
     作者简介:大家好,我是码炫码哥,前中兴通讯、美团架构师,现任某互联网公司CTO,兼职码炫课堂主讲源码系列专题代表作:《jdk源码&多线程&高并发》,《深入tomcat源码解析》,《深入netty源码解析》,《深入dubbo源码解析》,《深入springboot源码解析》,《深入spring源码解析》,《深入redis源......
  • SAAS版 财务系统 云会计财务源码
     现代企业的财务管理面临着越来越多的挑战,包括复杂的会计规范、繁琐的报表填写和高昂的人力成本。为了解决这些问题,我们开发了云会计财务源码,为企业提供全面、高效的财务管理解决方案。云会计财务源码是一款基于云技术的财务管理系统,具备以下特点:智能化:通过人工智能技术,自......
  • SAAS版 财务系统 云会计财务源码
    现代企业的财务管理面临着越来越多的挑战,包括复杂的会计规范、繁琐的报表填写和高昂的人力成本。为了解决这些问题,我们开发了云会计财务源码,为企业提供全面、高效的财务管理解决方案。云会计财务源码是一款基于云技术的财务管理系统,具备以下特点:智能化:通过人工智能技术,自动......
  • 基于Java健身房管理系统设计与实现 毕业设计源码15390
    摘 要随着人们生活水平的日益提高,健身已经成为了很多人生活中不可或缺的一部分。为了满足人们对健身的需求,各种健身房也应运而生。然而,传统的健身房管理方式存在诸多问题,如信息管理混乱、客户体验差等。为了解决这些问题,提高健身房的管理效率和服务质量,我们设计并实现了一套......