`
skoyou
  • 浏览: 5811 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

Mybatis 代码流程及实现原理解析(三)

阅读更多

接上篇, 这篇继续分析XMLMapperBuilder.parse()里的configurationElement() 这个方法。

private void configurationElement(XNode context) {
    try {
      //mapper可以加namespace来避免重复情况
      String namespace = context.getStringAttribute("namespace");
      builderAssistant.setCurrentNamespace(namespace);
      //该节点表示从其他名目空间引用缓存配置 
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));//解析cache子节点.
      //解析parameterMap 子节点,因为有多个并列的parameterMap节点,所以要加上路径,解析返回的列表
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //解析resultMap子节点,因为有多个并列的resultMap节点,所以要加上路径,解析返回的列表
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      //解析sql子节点,因为有多个并列的sql节点,所以要加上路径,解析返回的列表
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析select,insert,update,delete子节点
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

此方法逐步解析<mapper>的子节点。

子节点<cache-ref>

xml配置片段:

<cache-ref namespace="com.Book"/>

java代码解析:

private void cacheRefElement(XNode context) {
    if (context != null) {
     //首先用该节点的namespace替换cacherefmap中mapper文件的。
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
       //创建resolver,判断该namespace是否可用。
    	  cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        //如果不可用,加入到为完成列表,在所有节点解析完成后,再统一做处理
    	  configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
  }
CacheRefResolver.java的resolve方法

public Cache resolveCacheRef() {
  return assistant.useCacheRef(cacheRefNamespace);
}
MapperBuilderAssistant.java的useCacheRef方法:

public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;
      Cache cache = configuration.getCache(namespace);
      //如果当前configuration对象没有持有该namespace的Cache, 则抛出异常。
      if (cache == null) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      }
      currentCache = cache;
      unresolvedCacheRef = false;
      return cache;
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
  }

子节点<cache>

xml配置如下:

<mapper namespace="com.skoyou.domain.UserDAO">
<cache type="com.domain.something.MyCustomCache"
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="tr">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
type:配置自定义缓存或为其他第三方缓存方案 创建适配器来完全覆盖缓存行为。如果未指定的话, 使用“PERPETFUL”类型(PerpetualCache.java)。
eviction:缓存回收策略,默认值是“LRU”, 有如下几种策略可以指定:
LRU:最近最少使用的:移除最长时间不被使用的对象。
FIFO:先进先出:按对象进入缓存的顺序来移除它们。
SOFT:软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK:弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象.
flushInterval:刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size:(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readyOnly:(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
cache可以有个子节点<property> 来加载.

java方法解析:

private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      //获取cache type的值,没有的话,使用默认PERPETUAL
      String type = context.getStringAttribute("type", "PERPETUAL");
      // 得到type类, 默认是PERPETUAL 对应的PerpetualCache
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      //获取eviction的值,没有的话,使用默认LRU得到eviction类, 即LruCache.class
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size"); 
      boolean readWrite = !context.getBooleanAttribute("readOnly", false); 
     Properties props = context.getChildrenAsProperties();
     //根据提供的参数, 建一个cache对象, 并将这个对象放入到Configuration 对象中. 
     builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props); 
    } 
}



子节点<parameterMap>

在Mybatis中,这个节点已经被废弃了!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除。其实解析还是比较简单的。取这个节点的所有属性,build出一个ParameterMapping对象,方法parametermappings列表里, 然后将这个列表更新到configuration对象中的ParameterMap中去。

    for (XNode parameterMapNode : list) {
      String id = parameterMapNode.getStringAttribute("id");
      String type = parameterMapNode.getStringAttribute("type");
      Class<?> parameterClass = resolveClass(type);
      List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
      List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
      for (XNode parameterNode : parameterNodes) {
        String property = parameterNode.getStringAttribute("property");
        String javaType = parameterNode.getStringAttribute("javaType");
        String jdbcType = parameterNode.getStringAttribute("jdbcType");
        String resultMap = parameterNode.getStringAttribute("resultMap");
        String mode = parameterNode.getStringAttribute("mode");
        String typeHandler = parameterNode.getStringAttribute("typeHandler");
        Integer numericScale = parameterNode.getIntAttribute("numericScale");
        ParameterMode modeEnum = resolveParameterMode(mode);
        Class<?> javaTypeClass = resolveClass(javaType);
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
        @SuppressWarnings("unchecked")
        Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
        ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
        parameterMappings.add(parameterMapping);
      }
      builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
    }
  }

子节点<resultMap>

这个节点的解析较复杂,会在下一遍中单独讲解。

子节点<sql>

<sql id="userColumns"> id,username,password </sql>
这个元素可以被用来定义可重用的 SQL 代码段,可以包含在其他语句中。
Java 代码

private void sqlElement(List<XNode> list) throws Exception {
    if (configuration.getDatabaseId() != null) {  //传人configuration现有的database id。
      sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
  }
sqlElement方法:

  private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
    for (XNode context : list) { //由于有多个并列的sql节点,需求逐个解析。
     //sql 也可以有自己的database id,但实际上这个id要与全局配置的一致
      String databaseId = context.getStringAttribute("databaseId");  
      String id = context.getStringAttribute("id");
     //判断id是否包含有当前的namespace,包含的话直接返回当前id。 没有的话,则以当前mapper的namespace.id值返回
      id = builderAssistant.applyCurrentNamespace(id, false);
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) 
       sqlFragments.put(id, context);
    }
  }

databaseIdMatchesCurrent 方法:

sql节点自己指定的database id要与configuration中的一致。

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    if (requiredDatabaseId != null) {
      if (!requiredDatabaseId.equals(databaseId)) {
        return false;
      }
    } else {
      if (databaseId != null) {
        return false;
      }
      // skip this fragment if there is a previous one with a not null databaseId
      if (this.sqlFragments.containsKey(id)) {
        XNode context = this.sqlFragments.get(id);
        if (context.getStringAttribute("databaseId") != null) {
          return false;
        }
      }
    }
    return true;
  }
实际上这个sqlFragments map是在XMLMappBuilder初始化的时候,有configuration传入的, 所以configuration能得到最新的对这个map的修改。这个map会在build select等节点的时候被用到。

子节点<select>,<insert>,<update>,<delete>
节点配置,以select为例:

<select
  id="selectPerson"
  databaseId="test"
  fetchSize="256"
  timeout="10000"
  parameterMap="deprecated"
  parameterType="int"
  resultMap="personResultMap"
  resultType="hashmap"
  lang="XML"
  resultSetType="FORWARD_ONLY"
  statementType="PREPARED"
  flushCache="false"
  useCache="true"
  resultOrdered="false"
  >

节点属性描述:

属性 描述
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
databaseId 如果配置了databaseProvider,databaseId要与配置的一致。如果不匹配的话,后续代码不会继续解析
fetchSize 这是暗示驱动程序每次批量返回的结果行数。默认不设置(驱动 自行处理)。
timeout 这个设置驱动程序等待数据库返回请求结果,并抛出异常时间的 最大等待值。默认不设置(驱动自行处理)
parameterMap 这是引用外部 parameterMap 的已经被废弃的方法。使用内联参数 映射和 parameterType 属性。
parameterType 将会传入这条语句的参数类的完全限定名或别名。
resultMap 命名引用外部的 resultMap。 返回 map 是 MyBatis 最具力量的特性, 对其有一个很好的理解的话, 许多复杂映射的情形就 能被解决了。 使用 resultMap 或 resultType,但不能同时使用。
resultType 从这条语句中返回的期望类型的类的完全限定名或别名。注意集 合情形,那应该是集合可以包含的类型,而不能是集合本身。使 用 resultType 或 resultMap,但不能同时使用
lang 提供”XML“或”RAW“,不同的lang,可能会影响最终的sql语句的生成,默认是”XML“
resultSetType FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种。默认是不设置(驱动自行处理)。
statementType STA TEMENT,PREPARED 或 CALLABLE 的一种。 这会让 MyBatis 使用选择使用 Statement,PreparedStatement 或 CallableStatement。 默认值:PREPARED
fhushCache 将其设置为 true,不论语句什么时候被调用,都会导致缓存被 清空。默认值:false
userCache 将其设置为 true, 将会导致本条语句的结果被缓存。 默认值: true
resultOrdered 这个属性值适用于有嵌套结果的select 语句。如果是true, 那么就认为包含了嵌套的结果, 或者嵌套的结果被分在一组。这样的话, 当一个新的主结果返回时, 就不会出现上一个结果行。 这能够更加内存友好地填充嵌套结果。默认是false。

另外还有3个是只对insert有效的属性:

useGeneratedKeys 这会告诉 MyBatis使用JDBC的getGeneratedKeys 方法来取出由数据(比如:像 MySQL 和 SQL Server 这样的数据库管理系统的自动递增字段)内部生成的主键。 默认值:false
keyProperty 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值。 默认: 不设置
keyColumn 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值。 默认: 不设置

sql语句的解析实际上是在 XMLStatementBuider的parseStatementNode方法中解析的。

  private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      //构造statementBuilder。这几个sql语句相关的节点在这个类里解析
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

XMLStatementBuilder.java的 parseStatementNode方法,如下。这个方法比较长,但主要是读取属性值,然后包装生成一个MappedStatement,并放入configuration对象中。

public void parseStatementNode() {
    //读取id 属性
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    //读取databaseId 属性,但是一定要与现有存在的匹配, 否则不继续解析剩余节点 
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;

    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    //statementType 默认是PreparedStatement
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //如果是select语句,则默认值是false。否则默认值是true。
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //如果是select语句,则默认值是true。否则默认值是false。
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // 解析<include>子节点,其实是用引用替换<sql>节点的实际内容。
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 解析<selectKey>子节点,仅对<insert>有用。后面会讲解parseSelectKeyNodes方法
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    if (configuration.getDatabaseId() != null) {
      parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);

    //默认是XMLLanguageDriver,parameterTypeClass用不上
    //生成持有当前sql语句的sqlSource对象,后面会具体看这个sqlSource是怎么生成的
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //keyProperty和keyColumn使用于<insert>语句
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }
    //通过解析出来的属性值,构造MappedStatement对象。并放入configuration对象中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver);
  }

<insert>节点的子节点<selectKey>的解析

<selectKey>配置例子:

<selectKey
  databaseId="mysql“
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>

keyProperty:selectKey 语句结果应该被设置的目标属性。就是给哪个字段生成主键值。

resultType:结果的类型。MyBatis 通常可以算出来,但是写上也没有问题。 MyBatis 允许任何简单类型用作主键的类型,包括字符串

statementType:MyBatis 支持 STA TEMENT ,PREPARED 和 CALLABLE 语句的映射类型,分别代表 PreparedStatement 和 CallableStatement 类型。

order:这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那 么它会首先选择主键, 设置 keyProperty 然后执行插入语句。 如果 设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素- 这和如 Oracle 数据库相似,可以在插入语句中嵌入序列调用.

java代码:

public void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
   //可以有多个<selectKey>节点。
   for (XNode nodeToHandle : list) {
      String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      String databaseId = nodeToHandle.getStringAttribute("databaseId");
      if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
        parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
      }
    }
  }
  //
  public void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
    String resultType = nodeToHandle.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    //defaults
    boolean useCache = false;
    boolean resultOrdered = false;
    KeyGenerator keyGenerator = new NoKeyGenerator();
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;
    //解析生成主键的sql语句
    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;
    //生成MappedStatement 对象,放入configuration对象的mappedStatements map中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, null, databaseId, langDriver);

    id = builderAssistant.applyCurrentNamespace(id, false);

    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    //同时将这个MappedStatement 对象放入到configuration的 keyGenerator map中
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
    nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
  }

下篇会主要看看<resultMap>的解析和sqlSource的生成。


分享到:
评论

相关推荐

    基于Javaweb实现的校园疫情防控管理系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    基于ssm+vue高校专业信息管理系统设计与实现+(源码+部署说明+演示视频+源码介绍).zip

    源码介绍则对项目的源码进行了详细的解释和说明,包括代码结构、模块划分、关键代码解析等。通过阅读源码介绍,可以深入理解项目的设计和实现原理,对于学习和使用该项目有很大帮助。 总的来说,这个资源包提供了从...

    校园订餐系统,基于javaweb+SSM+maven实现.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    基于JavaWeb的医院挂号管理系统,使用SSM框架+MySQL实现.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    一个运用了MVC模式及JavaWeb三大框架的注册和登录系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    J2EE项目系列(一)--运用MVC模式及JavaWeb三层框架的学生管理系统。.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    基于javaweb的仿照百度网盘的小型云盘系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    javaweb项目:校园宿舍管理系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    基于JavaWeb开发的图书管理系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    JavaWeb图书管理系统(MVC框架).zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    停车场管理系统 javaweb 源码+文档.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    javaweb在线考试系统 学生+教师+管理员.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    javaweb学生成绩管理系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    B站【遇见狂神说】JavaWeb项目【超市订单管理系统】.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    基于JavaWeb的个人博客系统 (毕设),本系统基于JavaWeb进行开发

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    简陋的JavaWeb学生公寓管理系统 Servlet+JSP+MySql+Layui.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    JavaWeb,图书管理系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    javaweb管理系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    教材订购系统,JavaWeb大作业.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

    javaweb学生信息管理系统.zip

    代码与实现细节:提供了完整的项目源代码,并针对关键部分进行了详细的注释和解析。 文档与笔记:整理了开发过程中的关键决策、技术难题以及学习心得,有助于深入理解项目背后的思考过程。 二、适用人群 这个项目...

Global site tag (gtag.js) - Google Analytics