当前位置:   article > 正文

【HikariCP】【HikariConfig】源码学习_maxpoolsize cannot be less than 1

maxpoolsize cannot be less than 1

Hikari目前已经是springboot的默认数据库连接池,并且以高效和轻量著称,因为代码量比较少,所以可以阅读一下,学习一下,github地址:HikariCP

HikariConfig

常量

常量基本都是一些参数的默认值

//随机生成线程池名称时使用
private static final char[] ID_CHARACTERS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
//连接超时默认30秒
private static final long CONNECTION_TIMEOUT = SECONDS.toMillis(30);
//验证超时默认5秒
private static final long VALIDATION_TIMEOUT = SECONDS.toMillis(5);
//空闲超时默认10分钟
private static final long IDLE_TIMEOUT = MINUTES.toMillis(10);
//最大生命周期默认30分钟
private static final long MAX_LIFETIME = MINUTES.toMillis(30);
//默认连接池大小10
private static final int DEFAULT_POOL_SIZE = 10;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

还有一个是否是单元测试的标识符

private static boolean unitTest = false;
  • 1

运行时可变参数

注释中的描述是运行过程中可以通过HikariConfigMXBean进行修改,HikariConfigMXBean是接口,所以应该是指用它的实现类来进行值的修改。它的修饰符都是volatile,表示对所有线程可见

private volatile String catalog;
private volatile long connectionTimeout;
private volatile long validationTimeout;
private volatile long idleTimeout;
private volatile long leakDetectionThreshold;
private volatile long maxLifetime;
private volatile int maxPoolSize;
private volatile int minIdle;
private volatile String username;
private volatile String password;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行时不可变参数

这些参数在运行前已经设置好,运行以后不能修改

private long initializationFailTimeout;
private String connectionInitSql;
private String connectionTestQuery;
private String dataSourceClassName;
private String dataSourceJndiName;
private String driverClassName;
private String jdbcUrl;
private String poolName;
private String schema;
private String transactionIsolationName;
private boolean isAutoCommit;
private boolean isReadOnly;
private boolean isIsolateInternalQueries;
private boolean isRegisterMbeans;
private boolean isAllowPoolSuspension;
private DataSource dataSource;
private Properties dataSourceProperties;
private ThreadFactory threadFactory;
private ScheduledExecutorService scheduledExecutor;
private MetricsTrackerFactory metricsTrackerFactory;
private Object metricRegistry;
private Object healthCheckRegistry;
private Properties healthCheckProperties;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

还有一个参数设置是否可以在运行时不通过HikariConfigMXBean进行参数的修改,如果是true,那么一定要通过HikariConfigMXBean进行参数修改

private volatile boolean sealed;
  • 1

默认构造器

public HikariConfig()
{
  dataSourceProperties = new Properties();
  healthCheckProperties = new Properties();

  minIdle = -1;
  maxPoolSize = -1;
  maxLifetime = MAX_LIFETIME;
  connectionTimeout = CONNECTION_TIMEOUT;
  validationTimeout = VALIDATION_TIMEOUT;
  idleTimeout = IDLE_TIMEOUT;
  initializationFailTimeout = 1;
  isAutoCommit = true;

  String systemProp = System.getProperty("hikaricp.configurationFile");
  if (systemProp != null) {
     loadProperties(systemProp);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

代码中就是对一些参数进行默认值的设置,如果有指定配置文件的路径,那么会加载配置文件中的配置。

带参构造器

public HikariConfig(Properties properties)
{
  this();
  PropertyElf.setTargetFromProperties(this, properties);
}

  public HikariConfig(String propertyFileName)
{
  this();

  loadProperties(propertyFileName);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

带参构造器就是先调用默认构造器后再将传入的配置或者配置文件设置到参数

getter/setter

接下来都是成员变量的getter和setter,没有特殊逻辑的直接跳过

  • public void setConnectionTimeout(long connectionTimeoutMs)
@Override
public void setConnectionTimeout(long connectionTimeoutMs)
{
  if (connectionTimeoutMs == 0) {
     this.connectionTimeout = Integer.MAX_VALUE;
  }
  else if (connectionTimeoutMs < 250) {
     throw new IllegalArgumentException("connectionTimeout cannot be less than 250ms");
  }
  else {
     this.connectionTimeout = connectionTimeoutMs;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

源码中显示,连接超时的时间时不能设置小于250ms,如果设置成0,那么实际的时间将被设置成Integer.MAX_VALUE,相当于不会超时。

  • public void setIdleTimeout(long idleTimeoutMs)
@Override
public void setIdleTimeout(long idleTimeoutMs)
{
  if (idleTimeoutMs < 0) {
     throw new IllegalArgumentException("idleTimeout cannot be negative");
  }
  this.idleTimeout = idleTimeoutMs;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

空闲超时的配置只检查一个,不能是负数。

  • public void setMaximumPoolSize(int maxPoolSize)
@Override
public void setMaximumPoolSize(int maxPoolSize)
{
  if (maxPoolSize < 1) {
     throw new IllegalArgumentException("maxPoolSize cannot be less than 1");
  }
  this.maxPoolSize = maxPoolSize;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

数据库连接池最大连接数只检查不能小于1

  • public void setMinimumIdle(int minIdle)
@Override
public void setMinimumIdle(int minIdle)
{
  if (minIdle < 0) {
     throw new IllegalArgumentException("minimumIdle cannot be negative");
  }
  this.minIdle = minIdle;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

数据库连接池最小连接数只检查不能小于0

  • public void setValidationTimeout(long validationTimeoutMs)
@Override
public void setValidationTimeout(long validationTimeoutMs)
{
  if (validationTimeoutMs < 250) {
     throw new IllegalArgumentException("validationTimeout cannot be less than 250ms");
  }

  this.validationTimeout = validationTimeoutMs;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

验证超时时间的参数不能小于250ms

  • public void setDriverClassName(String driverClassName)
public void setDriverClassName(String driverClassName)
{
  checkIfSealed();

  Class<?> driverClass = attemptFromContextLoader(driverClassName);
  try {
     if (driverClass == null) {
        driverClass = this.getClass().getClassLoader().loadClass(driverClassName);
        LOGGER.debug("Driver class {} found in the HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader());
     }
  } catch (ClassNotFoundException e) {
     LOGGER.error("Failed to load driver class {} from HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader());
  }

  if (driverClass == null) {
     throw new RuntimeException("Failed to load driver class " + driverClassName + " in either of HikariConfig class loader or Thread context classloader");
  }

  try {
     driverClass.getConstructor().newInstance();
     this.driverClassName = driverClassName;
  }
  catch (Exception e) {
     throw new RuntimeException("Failed to instantiate class " + driverClassName, e);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

这个方法是设置数据库的驱动类,数据库驱动类需要在启动数据库连接池时加载到内存中。这边的主要逻辑就是先用Thread.currentThread().getContextClassLoader()的classLoader来加载类,如果没有加载到,那么用this.getClass().getClassLoader()来加载驱动,如果都加载失败,那么抛出ClassNotFoundException。然后尝试创建驱动的实例,成功的话就将驱动的名称设置到参数中。至于中间的两个classLoader的区别是什么,这个是另一个问题了。简而言之就是通过当前线程去加载比较安全,避免双亲委派加载中可能出现的错误。

  • public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory)
public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory)
{
  if (metricRegistry != null) {
     throw new IllegalStateException("cannot use setMetricsTrackerFactory() and setMetricRegistry() together");
  }

  this.metricsTrackerFactory = metricsTrackerFactory;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

设置度量标准跟踪器工厂的唯一检查就是不能和度量标准注册表共同出现

  • public void setMetricRegistry(Object metricRegistry)
public void setMetricRegistry(Object metricRegistry)
{
  if (metricsTrackerFactory != null) {
     throw new IllegalStateException("cannot use setMetricRegistry() and setMetricsTrackerFactory() together");
  }

  if (metricRegistry != null) {
     metricRegistry = getObjectOrPerformJndiLookup(metricRegistry);

     if (!safeIsAssignableFrom(metricRegistry, "com.codahale.metrics.MetricRegistry")
         && !(safeIsAssignableFrom(metricRegistry, "io.micrometer.core.instrument.MeterRegistry"))) {
        throw new IllegalArgumentException("Class must be instance of com.codahale.metrics.MetricRegistry or io.micrometer.core.instrument.MeterRegistry");
     }
  }

  this.metricRegistry = metricRegistry;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

设置度量标准注册表时,不能已经设置了度量标准跟踪器工厂。然后通过JNDI查找该类是否存在,同时检查这个类是否是com.codahale.metrics.MetricRegistry或者io.micrometer.core.instrument.MeterRegistry的子类或者子接口,如果不是就抛出异常,如果是的,将度量标准注册表赋给变量

  • public void setHealthCheckRegistry(Object healthCheckRegistry)
public void setHealthCheckRegistry(Object healthCheckRegistry)
{
  checkIfSealed();

  if (healthCheckRegistry != null) {
     healthCheckRegistry = getObjectOrPerformJndiLookup(healthCheckRegistry);

     if (!(healthCheckRegistry instanceof HealthCheckRegistry)) {
        throw new IllegalArgumentException("Class must be an instance of com.codahale.metrics.health.HealthCheckRegistry");
     }
  }

  this.healthCheckRegistry = healthCheckRegistry;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

健康检查注册表的设置和度量标准注册表的方式差不多,先检查指定的类在不在,如果在的话检查是不是HealthCheckRegistry的实例,如果不是抛出异常,是的话进行参数设置。

  • public void copyStateTo(HikariConfig other)
public void copyStateTo(HikariConfig other)
{
  for (Field field : HikariConfig.class.getDeclaredFields()) {
     if (!Modifier.isFinal(field.getModifiers())) {
        field.setAccessible(true);
        try {
           field.set(other, field.get(this));
        }
        catch (Exception e) {
           throw new RuntimeException("Failed to copy HikariConfig state: " + e.getMessage(), e);
        }
     }
  }

  other.sealed = false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

将本Hikari数据库连接池的配置复制给另一个,主要就是通过字段的反射赋值将值一对一赋给新的配置对象

私有方法

  • private Class<?> attemptFromContextLoader(final String driverClassName)
private Class<?> attemptFromContextLoader(final String driverClassName) {
  final ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader();
  if (threadContextClassLoader != null) {
     try {
        final Class<?> driverClass = threadContextClassLoader.loadClass(driverClassName);
        LOGGER.debug("Driver class {} found in Thread context class loader {}", driverClassName, threadContextClassLoader);
        return driverClass;
     } catch (ClassNotFoundException e) {
        LOGGER.debug("Driver class {} not found in Thread context class loader {}, trying classloader {}",
           driverClassName, threadContextClassLoader, this.getClass().getClassLoader());
     }
  }

  return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

加载驱动类的方法

  • public void validate()
public void validate()
{
  if (poolName == null) {
     poolName = generatePoolName();
  }
  else if (isRegisterMbeans && poolName.contains(":")) {
     throw new IllegalArgumentException("poolName cannot contain ':' when used with JMX");
  }

  // treat empty property as null
  //noinspection NonAtomicOperationOnVolatileField
  catalog = getNullIfEmpty(catalog);
  connectionInitSql = getNullIfEmpty(connectionInitSql);
  connectionTestQuery = getNullIfEmpty(connectionTestQuery);
  transactionIsolationName = getNullIfEmpty(transactionIsolationName);
  dataSourceClassName = getNullIfEmpty(dataSourceClassName);
  dataSourceJndiName = getNullIfEmpty(dataSourceJndiName);
  driverClassName = getNullIfEmpty(driverClassName);
  jdbcUrl = getNullIfEmpty(jdbcUrl);

  // Check Data Source Options
  if (dataSource != null) {
     if (dataSourceClassName != null) {
        LOGGER.warn("{} - using dataSource and ignoring dataSourceClassName.", poolName);
     }
  }
  else if (dataSourceClassName != null) {
     if (driverClassName != null) {
        LOGGER.error("{} - cannot use driverClassName and dataSourceClassName together.", poolName);
        // NOTE: This exception text is referenced by a Spring Boot FailureAnalyzer, it should not be
        // changed without first notifying the Spring Boot developers.
        throw new IllegalStateException("cannot use driverClassName and dataSourceClassName together.");
     }
     else if (jdbcUrl != null) {
        LOGGER.warn("{} - using dataSourceClassName and ignoring jdbcUrl.", poolName);
     }
  }
  else if (jdbcUrl != null || dataSourceJndiName != null) {
     // ok
  }
  else if (driverClassName != null) {
     LOGGER.error("{} - jdbcUrl is required with driverClassName.", poolName);
     throw new IllegalArgumentException("jdbcUrl is required with driverClassName.");
  }
  else {
     LOGGER.error("{} - dataSource or dataSourceClassName or jdbcUrl is required.", poolName);
     throw new IllegalArgumentException("dataSource or dataSourceClassName or jdbcUrl is required.");
  }

  validateNumerics();

  if (LOGGER.isDebugEnabled() || unitTest) {
     logConfiguration();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

参数验证的方法,在创建连接池和获取连接的时候调用。主要是验证必要的字符串不能为空,并且数字形式的参数需要值在限定的范围内。

  • private void validateNumerics()
private void validateNumerics()
{
  if (maxLifetime != 0 && maxLifetime < SECONDS.toMillis(30)) {
     LOGGER.warn("{} - maxLifetime is less than 30000ms, setting to default {}ms.", poolName, MAX_LIFETIME);
     maxLifetime = MAX_LIFETIME;
  }

  if (leakDetectionThreshold > 0 && !unitTest) {
     if (leakDetectionThreshold < SECONDS.toMillis(2) || (leakDetectionThreshold > maxLifetime && maxLifetime > 0)) {
        LOGGER.warn("{} - leakDetectionThreshold is less than 2000ms or more than maxLifetime, disabling it.", poolName);
        leakDetectionThreshold = 0;
     }
  }

  if (connectionTimeout < 250) {
     LOGGER.warn("{} - connectionTimeout is less than 250ms, setting to {}ms.", poolName, CONNECTION_TIMEOUT);
     connectionTimeout = CONNECTION_TIMEOUT;
  }

  if (validationTimeout < 250) {
     LOGGER.warn("{} - validationTimeout is less than 250ms, setting to {}ms.", poolName, VALIDATION_TIMEOUT);
     validationTimeout = VALIDATION_TIMEOUT;
  }

  if (maxPoolSize < 1) {
     maxPoolSize = DEFAULT_POOL_SIZE;
  }

  if (minIdle < 0 || minIdle > maxPoolSize) {
     minIdle = maxPoolSize;
  }

  if (idleTimeout + SECONDS.toMillis(1) > maxLifetime && maxLifetime > 0 && minIdle < maxPoolSize) {
     LOGGER.warn("{} - idleTimeout is close to or more than maxLifetime, disabling it.", poolName);
     idleTimeout = 0;
  }
  else if (idleTimeout != 0 && idleTimeout < SECONDS.toMillis(10) && minIdle < maxPoolSize) {
     LOGGER.warn("{} - idleTimeout is less than 10000ms, setting to default {}ms.", poolName, IDLE_TIMEOUT);
     idleTimeout = IDLE_TIMEOUT;
  }
  else  if (idleTimeout != IDLE_TIMEOUT && idleTimeout != 0 && minIdle == maxPoolSize) {
     LOGGER.warn("{} - idleTimeout has been set but has no effect because the pool is operating as a fixed size pool.", poolName);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

验证数字参数的合法性

  • private void checkIfSealed()
private void checkIfSealed()
{
  if (sealed) throw new IllegalStateException("The configuration of the pool is sealed once started. Use HikariConfigMXBean for runtime changes.");
}
  • 1
  • 2
  • 3
  • 4

在很多的setter方法中最先执行的就是这个检查关闭的方法,因为有一些参数是运行过程中不能修改的,所以需要加一个这样的限制。这个成员变量是一个volatile类型

  • private void logConfiguration()
private void logConfiguration()
{
  LOGGER.debug("{} - configuration:", poolName);
  final Set<String> propertyNames = new TreeSet<>(PropertyElf.getPropertyNames(HikariConfig.class));
  for (String prop : propertyNames) {
     try {
        Object value = PropertyElf.getProperty(prop, this);
        if ("dataSourceProperties".equals(prop)) {
           Properties dsProps = PropertyElf.copyProperties(dataSourceProperties);
           dsProps.setProperty("password", "<masked>");
           value = dsProps;
        }

        if ("initializationFailTimeout".equals(prop) && initializationFailTimeout == Long.MAX_VALUE) {
           value = "infinite";
        }
        else if ("transactionIsolation".equals(prop) && transactionIsolationName == null) {
           value = "default";
        }
        else if (prop.matches("scheduledExecutorService|threadFactory") && value == null) {
           value = "internal";
        }
        else if (prop.contains("jdbcUrl") && value instanceof String) {
           value = ((String)value).replaceAll("([?&;]password=)[^&#;]*(.*)", "$1<masked>$2");
        }
        else if (prop.contains("password")) {
           value = "<masked>";
        }
        else if (value instanceof String) {
           value = "\"" + value + "\""; // quote to see lead/trailing spaces is any
        }
        else if (value == null) {
           value = "none";
        }
        LOGGER.debug((prop + "................................................").substring(0, 32) + value);
     }
     catch (Exception e) {
        // continue
     }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

这个方法是在验证参数的时候,验证通过后如果是单元测试或者是debug时候可以打印出连接池参数

  • private void loadProperties(String propertyFileName)
private void loadProperties(String propertyFileName)
{
  final File propFile = new File(propertyFileName);
  try (final InputStream is = propFile.isFile() ? new FileInputStream(propFile) : this.getClass().getResourceAsStream(propertyFileName)) {
     if (is != null) {
        Properties props = new Properties();
        props.load(is);
        PropertyElf.setTargetFromProperties(this, props);
     }
     else {
        throw new IllegalArgumentException("Cannot find property file: " + propertyFileName);
     }
  }
  catch (IOException io) {
     throw new RuntimeException("Failed to read property file", io);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

加载配置文件中的参数到成员变量中

  • private String generatePoolName()
private String generatePoolName()
{
  final String prefix = "HikariPool-";
  try {
     // Pool number is global to the VM to avoid overlapping pool numbers in classloader scoped environments
     synchronized (System.getProperties()) {
        final String next = String.valueOf(Integer.getInteger("com.zaxxer.hikari.pool_number", 0) + 1);
        System.setProperty("com.zaxxer.hikari.pool_number", next);
        return prefix + next;
     }
  } catch (AccessControlException e) {
     // The SecurityManager didn't allow us to read/write system properties
     // so just generate a random pool number instead
     final ThreadLocalRandom random = ThreadLocalRandom.current();
     final StringBuilder buf = new StringBuilder(prefix);

     for (int i = 0; i < 4; i++) {
        buf.append(ID_CHARACTERS[random.nextInt(62)]);
     }

     LOGGER.info("assigned random pool name '{}' (security manager prevented access to system properties)", buf);

     return buf.toString();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

自动生成线程池名称的方法,检查是否有设定的线程池数量值,有的话+1,否则就是通过随机数产生名称,前缀都是HikaraPool-

  • private Object getObjectOrPerformJndiLookup(Object object)
private Object getObjectOrPerformJndiLookup(Object object)
{
  if (object instanceof String) {
     try {
        InitialContext initCtx = new InitialContext();
        return initCtx.lookup((String) object);
     }
     catch (NamingException e) {
        throw new IllegalArgumentException(e);
     }
  }
  return object;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

通过JNDI查找资源的方法

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/160661
推荐阅读
相关标签
  

闽ICP备14008679号