`

MyBatis原理(1)-启动流程2

 
阅读更多
1.XMLConfigBuilder.parse

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

2.调用XPathParser.evalNode返回代码configuration根节点的XNode对象,并将之前设置的dom对象传入
 public XNode evalNode(String expression) {
    return evalNode(document, expression);
  }

3.XPathParser调用xpath对象的evaluate方法构造返回org.w3c.dom.Node对象,evalNode的重载方法构造返回XNode对象,XNode是mybatis对Node节点的封装:
  public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
      return null;
    }
    return new XNode(this, node, variables);
  }

  private Object evaluate(String expression, Object root, QName returnType) {
    try {
      return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
      throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
  }

Xnode在构造方法会对Node中的属性调用parseAttributes再调用PropertyParser进行解析,对“${}”表达式进行解析,使用XpathParser中的variables对当前节点的attributes进行设值,此处的node是Configuration ,attributeNodes.getLength()长度为0直接跳出。
parseAttributes后再对parseBody进行对当前节点的所有的CDDATA节点或者文本节点中的表达式进行赋值,设置为当前节点的body。到此configuration作为根节点XNode返回。
4.parseConfiguration
 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);
    }
  }

4.1      propertiesElement(root.evalNode("properties"));
和上面的流程一样,不同的是根节点自身持有对XPathParser的引用,随后XPathParser调用evalNode,最后由XPathParser,xpath对象调用xpath.evaluate(expression, root, returnType)方法返回原生dom对象,再对XNode进行初始化
4.1.1 获取resource或者url属性对应的外部属性文件并加加载到configuration的variables和当前XpathParser的variables中。准备给后续的表达式进行赋值,这一步需要最先进行解析,值得注意的是url和resource属性只能设置一个。
 private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      Properties defaults = context.getChildrenAsProperties();
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      if (resource != null && url != null) {
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
  }


4.2 解析 <setting>节点
流程和解析properties一样,可以看出来 evalNode的作用就是在构造XNode,在构造XNode的过程中,调用parseAttributes对节点attribute进行解析并放入Node的attribute属性备用,接着调用parseBody对Node节点body进行解析,如果第一次getBodyData返回不为空即是文本或者CDATA节点,如果其中包含表达式使用PropertyParser.pase解析并赋值。
解析节点之后调用settingsAsProperties方法将settings中的setting节点,name和value解析为properties对象,然后调用MetaClass对Configuration进行属性检查,如果在Configuration中没有的属性,则抛出异常。在解析的过程中第一步调用getChildren()和getChildrenAsProperties方法将所有子节点返回。然后调用MetaClass.forClass工厂方法构造指定Configuration类型的Reflector和reflectorFactory,reflectorFactory是Reflector的缓存,Reflector则包含了对getter setter field 的封装,初始化这两个对象后作为MetaClass的私有属性,MetaClass则对reflector进行进一步的封装。下面在Metaclass中对setting节点进行检查是否有对应的 setter方法
 public boolean hasSetter(String name) {
    PropertyTokenizer prop = new PropertyTokenizer(name);
    if (prop.hasNext()) {
      if (reflector.hasSetter(prop.getName())) {
        MetaClass metaProp = metaClassForProperty(prop.getName());
        return metaProp.hasSetter(prop.getChildren());
      } else {
        return false;
      }
    } else {
      return reflector.hasSetter(prop.getName());
    }
  }

4.3加载定义VFS
loadCustomVfs(settings);

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);
        }
      }
    }
  }

//TODO 等后面用到VFS在进行添加
4.4 加载 类型别名typeAliases
   typeAliasesElement(root.evalNode("typeAliases"));

扫描该节点下的子节点如果子节点名称是package则扫描这个包下面所有的类,每一个在包 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 domain.blog.Author 的别名为 author;若有注解,则别名为其注解值。如果不是package则默认对每个
  <typeAlias alias="Author" type="domain.blog.Author"/>
进行添加。
 private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          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);
          }
        }
      }
    }
  }

在进行扫描包的过程中
  public void registerAliases(String packageName){
    registerAliases(packageName, Object.class);
  }

  public void registerAliases(String packageName, Class<?> superType){
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> 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() && !type.isInterface() && !type.isMemberClass()) {
        registerAlias(type);
      }
    }
  }

ResolverUtil能根据相应的条件(某个类的超类,子类,注解等)在指定路径下查找对应满足条件的所有类加载到matches这个set中。
public class ResolverUtil<T> {
  /*
   * An instance of Log to use for logging in this class.
   */
  private static final Log log = LogFactory.getLog(ResolverUtil.class);

  /**
   * A simple interface that specifies how to test classes to determine if they
   * are to be included in the results produced by the ResolverUtil.
   */
  public interface Test {
    /**
     * Will be called repeatedly with candidate classes. Must return True if a class
     * is to be included in the results, false otherwise.
     */
    boolean matches(Class<?> type);
  }

  /**
   * A Test that checks to see if each class is assignable to the provided class. Note
   * that this test will match the parent type itself if it is presented for matching.
   */
  public static class IsA implements Test {
    private Class<?> parent;

    /** Constructs an IsA test using the supplied Class as the parent class/interface. */
    public IsA(Class<?> parentType) {
      this.parent = parentType;
    }

    /** Returns true if type is assignable to the parent type supplied in the constructor. */
    @Override
    public boolean matches(Class<?> type) {
      return type != null && parent.isAssignableFrom(type);
    }

    @Override
    public String toString() {
      return "is assignable to " + parent.getSimpleName();
    }
  }

  /**
   * A Test that checks to see if each class is annotated with a specific annotation. If it
   * is, then the test returns true, otherwise false.
   */
  public static class AnnotatedWith implements Test {
    private Class<? extends Annotation> annotation;

    /** Constructs an AnnotatedWith test for the specified annotation type. */
    public AnnotatedWith(Class<? extends Annotation> annotation) {
      this.annotation = annotation;
    }

    /** Returns true if the type is annotated with the class provided to the constructor. */
    @Override
    public boolean matches(Class<?> type) {
      return type != null && type.isAnnotationPresent(annotation);
    }

    @Override
    public String toString() {
      return "annotated with @" + annotation.getSimpleName();
    }
  }

  /** The set of matches being accumulated. */
  private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();

  /**
   * The ClassLoader to use when looking for classes. If null then the ClassLoader returned
   * by Thread.currentThread().getContextClassLoader() will be used.
   */
  private ClassLoader classloader;

  /**
   * Provides access to the classes discovered so far. If no calls have been made to
   * any of the {@code find()} methods, this set will be empty.
   *
   * @return the set of classes that have been discovered.
   */
  public Set<Class<? extends T>> getClasses() {
    return matches;
  }

  /**
   * Returns the classloader that will be used for scanning for classes. If no explicit
   * ClassLoader has been set by the calling, the context class loader will be used.
   *
   * @return the ClassLoader that will be used to scan for classes
   */
  public ClassLoader getClassLoader() {
    return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
  }

  /**
   * Sets an explicit ClassLoader that should be used when scanning for classes. If none
   * is set then the context classloader will be used.
   *
   * @param classloader a ClassLoader to use when scanning for classes
   */
  public void setClassLoader(ClassLoader classloader) {
    this.classloader = classloader;
  }

  /**
   * Attempts to discover classes that are assignable to the type provided. In the case
   * that an interface is provided this method will collect implementations. In the case
   * of a non-interface class, subclasses will be collected.  Accumulated classes can be
   * accessed by calling {@link #getClasses()}.
   *
   * @param parent the class of interface to find subclasses or implementations of
   * @param packageNames one or more package names to scan (including subpackages) for classes
   */
  public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) {
    if (packageNames == null) {
      return this;
    }

    Test test = new IsA(parent);
    for (String pkg : packageNames) {
      find(test, pkg);
    }

    return this;
  }

  /**
   * Attempts to discover classes that are annotated with the annotation. Accumulated
   * classes can be accessed by calling {@link #getClasses()}.
   *
   * @param annotation the annotation that should be present on matching classes
   * @param packageNames one or more package names to scan (including subpackages) for classes
   */
  public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
    if (packageNames == null) {
      return this;
    }

    Test test = new AnnotatedWith(annotation);
    for (String pkg : packageNames) {
      find(test, pkg);
    }

    return this;
  }

  /**
   * Scans for classes starting at the package provided and descending into subpackages.
   * Each class is offered up to the Test as it is discovered, and if the Test returns
   * true the class is retained.  Accumulated classes can be fetched by calling
   * {@link #getClasses()}.
   *
   * @param test an instance of {@link Test} that will be used to filter classes
   * @param packageName the name of the package from which to start scanning for
   *        classes, e.g. {@code net.sourceforge.stripes}
   */
  public ResolverUtil<T> find(Test test, String packageName) {
    String path = getPackagePath(packageName);

    try {
      List<String> children = VFS.getInstance().list(path);
      for (String child : children) {
        if (child.endsWith(".class")) {
          addIfMatching(test, child);
        }
      }
    } catch (IOException ioe) {
      log.error("Could not read package: " + packageName, ioe);
    }

    return this;
  }

  /**
   * Converts a Java package name to a path that can be looked up with a call to
   * {@link ClassLoader#getResources(String)}.
   * 
   * @param packageName The Java package name to convert to a path
   */
  protected String getPackagePath(String packageName) {
    return packageName == null ? null : packageName.replace('.', '/');
  }

  /**
   * Add the class designated by the fully qualified class name provided to the set of
   * resolved classes if and only if it is approved by the Test supplied.
   *
   * @param test the test used to determine if the class matches
   * @param fqn the fully qualified name of a class
   */
  @SuppressWarnings("unchecked")
  protected void addIfMatching(Test test, String fqn) {
    try {
      String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
      ClassLoader loader = getClassLoader();
      if (log.isDebugEnabled()) {
        log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
      }

      Class<?> type = loader.loadClass(externalName);
      if (test.matches(type)) {
        matches.add((Class<T>) type);
      }
    } catch (Throwable t) {
      log.warn("Could not examine class '" + fqn + "'" + " due to a " +
          t.getClass().getName() + " with message: " + t.getMessage());
    }
  }
}

ResolverUtil有一个接口Test 以及IsA、AnnotatedWith两个实现类,IsA的matches表示当前类是传入类的父亲,AnnotatedWith的matches表示当前是否标注指定注解,classloader属性是一个单独的属性,用来加载查找到后的类,没有设置则默认为
Thread.currentThread().getContextClassLoader()

 
public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) 
  public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) 
  public ResolverUtil<T> find(Test test, String packageName) 



findImplementations和findAnnotated都会调用find进行类的加载和查找,其中find方法调用VFS查询指定包下面的所有子路径,找出class结尾的路径后调用addIfMatching方法,addIfMatching负责对类进行加载校验并添加到matches集合后,供getClasses方法返回。
4.5 加载插件
      pluginElement(root.evalNode("plugins"));

注:从4.3开始不在对evalNode方法进行跟踪
首先遍历子节点,获取interceptor属性,即包名resolveClass方法最终会调用BaseBuilder的typeAliasRegistry的resolveAlias方法,这个方法会对先检查是否已经存在此别名,不存在则使用Resources进行加载。继续读取interceptor节点的property子节点,并设置到Inteceptor中,在最后添加到Configuration中的拦截器链中。
 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 interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

4.6 加载objectFactory
      objectFactoryElement(root.evalNode("objectFactory"));

这部分逻辑和加载plugin类似

  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);
    }
  }
值得注意的是如果配置了对象工厂会覆盖Configuration中默认配置的DefaultObjectFactory
4.7 加载objectWrapperFactory和上面类似
      objectWrapperFactoryElement(root.evalNode("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);
    }
  }

4.8 加载reflectorFactory 反射工厂,和上面类似
      reflectorFactoryElement(root.evalNode("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);
    }
  }

4.9 加载settings
  settingsElement(settings);

这部分就是对settings下面的子节点进行解析写入到Configuration中
4.10 加载environments,事务工厂和数据源的初始化

 
environmentsElement(root.evalNode("environments"));

 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");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

4.11 解析databaseIdProvider,根据不同的数据库标识使用不同的映射语句
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));

如果配置了 type=VENDOR(英文释义:厂商)当前只有这一个值,首先获取节点的properties,再构造DatabaseProvider的实例,在
    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
Configuration初始化的时候就已经默认注册了此别名,前面也说道resolveClass最终会调用typeAliasRegistry的resolveAlias方法查找和创建实例,VendorDatabaseIdProvider是DatabaseIdProvider接口的唯一实例,可自定义覆盖掉configuration中的配置,VendorDatabaseIdProvider的实现很简单,传入数据源获取Connection获取connection元数据信息,如果getDatabaseProductName返回的数据和在<property>子节点定义的name一致则返回property对应的value,例如:如果配置 <property name="Oracle" value="oracle" /> ,如果 getDatabaseProductName() 返回“Oracle (DataDirect)”,databaseId 将被设置为“oracle”。
最后将configuration中的databaseId设置为这个时候获取到的"oracle"或是mysql 就可以在update,delete 等语句中进行指定。
 private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
      String type = context.getStringAttribute("type");
      // awful patch to keep backward compatibility
      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 && databaseIdProvider != null) {
      String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
      configuration.setDatabaseId(databaseId);
    }
  }

public class VendorDatabaseIdProvider implements DatabaseIdProvider {
  
  private static final Log log = LogFactory.getLog(VendorDatabaseIdProvider.class);

  private Properties properties;

  @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;
  }

  @Override
  public void setProperties(Properties p) {
    this.properties = p;
  }

  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
        }
      }
    }
  }
  
}

  <select id="SelectTime"   resultType="String" databaseId="mysql">
   SELECT  NOW() FROM dual 
  </select>

  <select id="SelectTime"   resultType="String" databaseId="oracle">
   SELECT  'oralce'||to_char(sysdate,'yyyy-mm-dd hh24:mi:ss')  FROM dual 
  </select>


4.12  加载typeHandlers
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);
          }
        }
      }
    }
  }


如果typehandlers子节点有package获取名称属性调用
 public void register(String packageName) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    for (Class<?> type : handlerSet) {
      //Ignore inner classes and interfaces (including package-info.java) and abstract classes
      if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
        register(type);
      }
    }
  }

进行扫描resolverUtil上面有讲到,否则挨个的进行注册。
分享到:
评论

相关推荐

    单点登录源码

    Spring+SpringMVC+Mybatis框架集成公共模块,包括公共配置、MybatisGenerator扩展插件、通用BaseService、工具类等。 &gt; zheng-admin 基于bootstrap实现的响应式Material Design风格的通用后台管理系统,`zheng`...

    springboot知识点整理

    7.1 启动流程(Springboot 1.50版本) 128 7.1.1 创建SpringApplication对象 129 7.1.2 运行run方法 130 7.1.3 编写事件监听机制 132 8 Spring Boot自定义starters 136 8.1 概述 136 8.2 步骤 137 9 更多Springboot...

    最新Java面试题视频网盘,Java面试题84集、java面试专属及面试必问课程

    │ Java面试题48.struts2的执行流程或者struts2的原理.mp4 │ Java面试题49.Struts2的拦截器是什么?你都用它干什么?.mp4 │ Java面试题50.Spring MVC的执行流程.mp4 │ Java面试题51.SpringMVC和Struts2的不同.mp4...

    基于jbpm与activiti的工作流平台技术架构介绍

    ◦定时任务管理 通过配置以实现某时刻重复执行的系统任务,如配置每月最后一天进行库存清算任务,并且启动库存清算审批流程。 ◦系统日志管理 记录进入系统中的每个用户访问的每个功能 ◦数据源管理 可以设置多种...

    基于SSM的个人所得税服务系统(源码+部署说明+演示视频+源码介绍).zip

    部署说明:本资源提供了详细的部署说明,包括如何配置数据库连接、如何启动项目等。此外,还介绍了如何在不同操作系统上进行部署,以满足不同开发者的需求。演示视频:本资源附带了一个演示视频,展示了系统的运行...

    基于SSM+vue小学生课外知识学习网站+vue(源码+部署说明+演示视频+源码介绍).zip

    这是一个基于SSM(Spring、SpringMVC、MyBatis)框架和Vue前端技术栈的小学生课外知识学习网站的源码资源包。该资源包包含了完整的项目源码、部署说明、演示视频以及源码介绍,旨在帮助用户快速搭建和运行一个功能...

    Spring3.x企业应用开发实战(完整版) part1

     《Spring3.x企业应用开发实战》是在《精通Spring2.x——企业应用开发详解》的基础上,经过历时一年的重大调整改版而成的,本书延续了上一版本追求深度,注重原理,不停留在技术表面的写作风格,力求使读者在熟练...

    Spring.3.x企业应用开发实战(完整版).part2

     《Spring3.x企业应用开发实战》是在《精通Spring2.x——企业应用开发详解》的基础上,经过历时一年的重大调整改版而成的,本书延续了上一版本追求深度,注重原理,不停留在技术表面的写作风格,力求使读者在熟练...

    idea使用教程2017-06-01.pdf

    启动配置..................................................................................................................................... 9 配置空间...................................................

    网络架构师148讲视频课程

    │ 第48节:VCL的子程序和Request流程.avi │ 第49节:VCL的变量和常见的应用片断.avi │ 第50节:使用CLI来管理Varnish.avi │ 第51节:Varnishd命令和运行期参数.avi │ 第52节:Varnish的日志操作.avi │ 第53节...

    JAVA核心知识点整理(有效)

    1. 目录 1. 2. 目录 .........................................................................................................................................................1 JVM ........................

Global site tag (gtag.js) - Google Analytics