一、背景介绍
某应用在压测过程机器 cpu 使用率超过 80%,通过在线诊断工具进行 CPU 采样生成的火焰图,看到程序中频繁调用 environment.getProperty()获取属性值,而其内部调用了 JndiPropertySource.getProperty()
通过在线诊断工具进行 CPU 采样生成的火焰图
问题解决
属性进行缓存,这里通过 @Value+set 方法注入到静态变量。后使用Forcebot平台进行单机压测(结果):cpu 在 70%左右,改造前 qps550,改造后 qps900,性能提升 63%
两个疑问
1)为什么从 properties 文件获取属性值会使用到 JNDI 服务?
2)如何解决,避免使用 JNDI 服务获取属性值?
二、为什么会使用到 JNDI
1、什么是 JNDI
JNDI(Java Naming and Directory Interface)是 Java 的一种命名和目录服务接口,提供了查找和访问由名称命名的对象的方法。这些对象可以是任何类型的 Java 对象,例如数据源、消息队列、邮件会话等。
JNDI服务的主要用途有:
1. 资源管理:在Java EE环境中,JNDI通常用于管理和配置资源,如数据库连接池、JMS队列/主题、邮件会话等。这些资源会被配置在服务器级别,并在JNDI环境中注册。然后,应用程序可以通过查找这些资源的JNDI名称来使用它们,而不需要自己管理这些资源的生命周期。
2. 远程对象查找:在分布式系统中,JNDI可以用于查找远程对象,如EJB(Enterprise JavaBeans)对象。这些远程对象会在JNDI环境中注册,然后客户端可以通过查找这些对象的JNDI名称来获取对它们的引用。
3. 目录服务:JNDI可以用于访问各种目录服务,如LDAP(Lightweight Directory Access Protocol)和DNS(Domain Name System)。你可以使用JNDI来查询、更新和删除目录中的条目。
总的来说,JNDI是一种灵活的服务,它可以帮助Java应用程序与各种环境和资源交互,而无需知道这些资源的具体实现细节。
然而对于一些现代的、轻量级的、微服务架构的应用,人们可能会倾向于不使用JNDI,原因主要有以下几点:
1. 复杂性:JNDI是一个强大且灵活的服务,但它也相当复杂。使用JNDI通常需要对Java EE、命名服务和目录服务有深入的了解。对于一些简单的应用,使用JNDI可能会引入不必要的复杂性。
2. 环境依赖:JNDI通常需要运行在Java EE服务器上。这意味着如果你的应用使用了JNDI,那么它可能就无法在没有Java EE服务器的环境(如简单的Java SE环境或轻量级的容器环境)中运行。
3. 测试难度:由于JNDI依赖于运行环境,所以在单元测试或集成测试中模拟JNDI环境可能会很困难。
4. 现代替代方案:许多现代的Java框架,如Spring和Micronaut,提供了更简单、更灵活的方式来管理和配置资源。例如,你可以使用Spring的依赖注入和外部配置功能来配置数据库连接池,而无需使用JNDI。
因此,虽然JNDI仍然在某些场合有其用处,但在许多现代Java应用中,人们可能会选择使用更简单、更灵活的替代方案。
复制代码
2、JNDI 属性源如何被添加的
调用过程:SpringApplication#run(java.lang.String...) -> SpringApplication#prepareEnvironment -> SpringApplication#getOrCreateEnvironment
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}
复制代码
webApplicationType 如果是 SERVLET,则创建一个 StandardServletEnvironment 对象,它继承类 StandardEnvironment,而类 StandardEnvironment 又继承类 AbstractEnvironment,构造方法中调用方法 customizePropertySources
public AbstractEnvironment() {
customizePropertySources(this.propertySources);
if (logger.isDebugEnabled()) {
logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
}
}
复制代码
StandardServletEnvironment 重写了方法 customizePropertySources,此方法中判断如果 JNDI 服务可用,则会添加 JndiPropertySource
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
super.customizePropertySources(propertySources);
}
复制代码
判断 JNDI 服务是否可用的方法 JndiLocatorDelegate#isDefaultJndiEnvironmentAvailable
public static boolean isDefaultJndiEnvironmentAvailable() {
if (shouldIgnoreDefaultJndiEnvironment) {
return false;
}
try {
new InitialContext().getEnvironment();
return true;
}
catch (Throwable ex) {
return false;
}
}
private static final boolean shouldIgnoreDefaultJndiEnvironment = SpringProperties.getFlag(IGNORE_JNDI_PROPERTY_NAME);
public static final String IGNORE_JNDI_PROPERTY_NAME = "spring.jndi.ignore";
复制代码
spring.jndi.ignore 默认为 null,变量 shouldIgnoreDefaultJndiEnvironment 则为 false。
主要看 new InitialContext().getEnvironment()是否会抛异常,对于使用 Spring Boot 构建的 web 应用来讲
•打包方式如果是 jar 包,因嵌入式 Servlet 容器通常不支持 JNDI,则会抛异常,返回 false
•打包方式如果是 war 包,部署到外部 Servlet 容器(如 Tomcat)默认支持 JNDI,则会成功,返回 true
3、为什么会使用到 JNDI 属性源
通过 environment.getProperty(key)获取属性值,首先会进入 AbstractEnvironment#getProperty(String key),解析器是 PropertySourcesPropertyResolver,调用方法 PropertySourcesPropertyResolver#getProperty(java.lang.String, java.lang.Class<T>, boolean)
ConfigurationPropertySourcesPropertySource 会添加到首位(具体在 org.springframework.boot.context.properties.source.ConfigurationPropertySources#attach),它其实是一种特殊的属性源,将 Environment 中所有其他属性源转化为 ConfigurationPropertySource 并作为自己的属性源。具体在 ConfigurationPropertySourcesPropertySource#findConfigurationProperty()方法中获取属性值
依次循环去获取,取到则返回。这里可以看出在 PropertySourceList 中 JndiPropertySource 比 OriginTrackedMapPropertySource(application.properties)靠前,由于是顺序读取,所以会先从 JndiPropertySource 中取值,取不到后才会从 OriginTrackedMapPropertySource 取值。
而 JndiPropertySource 需要在 JNDI 服务器查询属性,可能会进行网络通信。如果你的应用没有相关的 JNDI 配置,那主要在于初始化 JNDI 上下文以及进行无效的查询操作,这个耗时也会高于从 OriginTrackedMapPropertySource 的 Map 内存数据结构中获取。
PropertySource 的优先级
Spring Boot中的`PropertySource`的优先级从高到低如下:
1. Devtools全局设置属性(`spring.devtools.*`)(只有在开发者工具存在的情况下)
2. `@TestPropertySource`注解在测试中的属性。
3. `@SpringBootTest#properties`注解在测试中的属性。
4. 命令行参数。
5. `SPRING_APPLICATION_JSON`中的属性(内联JSON嵌入在环境变量中)。
6. `ServletConfig`初始化参数。
7. `ServletContext`初始化参数。
8. `JNDI`属性从`java:comp/env`。
9. Java系统属性(`System.getProperties()`)。
10. 操作系统环境变量。
11. `RandomValuePropertySource`属性只有`random.*`的属性存在。
12. JAR包外部的应用程序配置文件(`application-{profile}.properties`和`YAML`变体)。
13. JAR包内部的应用程序配置文件(`application-{profile}.properties`和`YAML`变体)。
14. 在配置类上的`@PropertySource`注解。
15. 默认属性(使用`SpringApplication.setDefaultProperties`指定)。
复制代码
详情见官方文档: https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config
三、如何避免使用 JNDI
通过上述的分析,可以得到以下三种方式避免使用 JNDI
方式一:通过 jar 包部署(推荐)
方式二:war 包部署,关闭 JNDI 服务
java_opts 环境变量中添加配置:-Dspring.jndi.ignore=true
方式三:war 包部署,自定义调整 PropertySource 顺序(不推荐)
四、经验总结
强烈建议大家使用Forcebot平台压测任务配合在线诊断工具,可以方便的检查出工程中不合理的地方,进行性能优化,降本增效。
作者:京东零售 郭宏宇
来源:京东云开发者社区 转载请注明来源
评论