解决 Spring Boot 3.5.0 后 Jasypt 无法解析环境变量中的加密字符串问题
当 Spring Boot 项目升级到 3.5.0 及更高版本后,使用 jasypt-spring-boot-starter
会遇到一个问题:应用程序无法正确解析环境变量中配置的加密字符串。具体表现为,在应用程序运行期间,读取到的配置值仍然是加密后的字符串(例如
ENC(加密字符串)),而非解密后的原始值。
当 Spring Boot 项目升级到 3.5.0 及更高版本后,使用 jasypt-spring-boot-starter
会遇到一个问题:应用程序无法正确解析环境变量中配置的加密字符串。具体表现为,在应用程序运行期间,读取到的配置值仍然是加密后的字符串(例如
ENC(加密字符串)),而非解密后的原始值。
此问题是升级apache-httpclient5过程中遇到的问题.
项目是多租户场景, 每个租户都有自己的服务, 所有服务部署在kubernetes
上.
每个租户的服务在独立的namespace
中 namespace
是租户的ID
(例如1663783236729442304
)
今天遇到一个问题是当测试模拟ldap
的服务端主节点挂掉的时候并在页面点击登录, 后端一直未作出响应.
这个问题的原因是因为代码中的ldap-client
的failover
未生效, 通过排查发现ldap-client
一直在连接ldap
的主节点,
并且tcp
连接一直处于SYN_SENT
状态.
由于ldap-client
没有默认情况下没有控制超时, 导致代码一直堵塞, 从而导致failover
不工作.
下面将开始介绍具体细节以及解决方案.
有的时候需要将jar
包中的class
文件反编译为java
源码文件并对其行为进行分析.
IntelliJ IDEA
中的java-decompiler
插件可以将jar
包中的class
文件反编译为java
源码文件.
下面介绍如何使用IntelliJ IDEA
中的java-decompiler
插件将jar
包反编译为java
源码.
数据验证是一个非常常见的需求, 对于java
项目来说, 目前jakarta
的bean validation
已经成为了java中的标准.
其自带了一些常见的数据验证注解, 例如@NotNull
, @NotEmpty
, @Size
等.
这些注解如果遇到复杂的数据验证需求时, 就会显得力不从心. 所以需要一种更加灵活的数据验证方式.
为了满足这种需求, 我们可以通过clojure
表达式来实现数据验证.
同时我们需要和现有的bean validation
一起使用, 以便于满足现有的业务需求.
以下列表是截至到目前Open JDK
中部分进行的对JDK
的改进项目
这些改进项目的主要目的是为了改进Java
的性能和开发体验.
从而使Java
能够更好地适应目前的软件开发需求.
以下是这些项目的简单介绍.
前端请求服务相应接口报错, 日志如下
2023-01-30 12:10:14.822 WARN 1 --- [nio-4396-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 25006
2023-01-30 12:10:14.822 ERROR 1 --- [nio-4396-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: cannot execute UPDATE in a read-only transaction
at org.springframework.security.web.authentication.AnonymousAuthen
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81)
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:126)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:121)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:327)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:684)
at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:920)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at jdk.internal.reflect.GeneratedMethodAccessor355.invoke(Unknown Source)
at com.fastonetech.computecloud.api.regional.software.controller.LaunchableAppController.lastAccessAt(LaunchableAppController.kt:56)
at com.fastonetech.computecloud.api.regional.software.service.UserSoftwareUsageServiceImpl$$EnhancerBySpringCGLIB$$1caff315.lastAccessAt(<generated>)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:743)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:562)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:281)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:40)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:183)
at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:448)
at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2380)
at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3212)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:453)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1362)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:99)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40)
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475)
at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604)
at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:201)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3769)
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3355)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3493)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:130)
at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:164)
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:401)
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:481)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:322)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2297)
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2565)
org.postgresql.util.PSQLException: ERROR: cannot execute UPDATE in a read-only transaction
数据库采用主从架构, 由于主库宕机,
Load balancer
将请求转发到了从库, 从库的事务为read only
导致更新失败。
后端相关代码如下
@javax.transaction.Transactional
fun lastAccessAt(id: Long, type: LaunchableAppType): UserSoftwareUsageDTO {
val userId = authService.currentAsPortal().bind().userId
val userSoftwareUsage =
getOrElseCreate(userId, id, type).copy(lastAccessAt = ZonedDateTime.now()).let(repository::save)
return UserSoftwareUsageDTO(
userSoftwareUsage.appId,
userSoftwareUsage.appType,
userSoftwareUsage.lastAccessAt,
userSoftwareUsage.collected
)
}
其采用的javax
的@Transactional
进行事务控制.
该注解没有提供显式指定数据库事务的读写行为相关属性, 是否是只读或者写入只能由由数据库的默认行为决定.
在PostgreSQL
中, 可以通过以下命令来查看默认的读写行为
show default_transaction_read_only;
-- return true if default transaction is read only, false otherwise
readonly
的框架, 如spring的@Transactional
注解, 该注解提供了readOnly
属性,
可以显式控制事务的读写行为。这样可以不隐式依赖数据库的默认行为, 从而在创建事务时提前发现问题。基于graalvm
的spring boot
项目打包好后调用/scheduling/api/v1/taskDefinitions
接口返回如下数据, 其中triggerStrategy
字段是空json
对象
[
{
"id": "BILLING_SYNC_FOR_DEPLOYMENT_127",
"triggerStrategy": {},
"tags": {
"EXECUTOR": "com.fastonetech.billing.sync.scheduling.BillingSyncTaskExecutor"
},
"variables": {
"SOURCE_RCLONE_CONFIG": "TENCENT_ap-beijing",
"SOURCE_BUCKET": "cheng1201-1310454728",
"SOURCE_PATH": "",
"TARGET_RCLONE_CONFIG": "aggregation",
"TARGET_BUCKET": "billing-aggregation",
"TARGET_PATH": "TENCENT/ap-beijing/cheng1201-1310454728"
}
}
]
在consul
中修改相关服务的配置时引发ConcurrentModificationException
并导致协程任务异常退出.
相关报错如下:
2022-11-24 10:08:27.954 INFO 1 --- [TaskScheduler-1] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-config/mgmt-scheduler/'}, BootstrapPropertySource {name='bootstrapProperties-config/application/'}]
2022-11-24 10:08:27.968 INFO 1 --- [TaskScheduler-1] o.s.boot.SpringApplication : No active profile set, falling back to 1 default profile: "default"
2022-11-24 10:08:27.979 INFO 1 --- [TaskScheduler-1] o.s.boot.SpringApplication : Started application in 0.244 seconds (JVM running for 7930.38)
Exception in thread "DefaultDispatcher-worker-6" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
at com.fastonetech.scheduling.core.ResourceDispatcher$Companion$of$1$1.invokeSuspend(ResourceDispatcher.kt:40)
at com.fastonetech.scheduling.core.ResourceDispatcher$Companion$of$1$1.invoke(ResourceDispatcher.kt)
at com.fastonetech.scheduling.core.ResourceDispatcher$Companion$of$1$1.invoke(ResourceDispatcher.kt)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
at com.fastonetech.scheduling.core.ResourceDispatcher$Companion$of$1.dispatch(ResourceDispatcher.kt:16)
at com.fastonetech.scheduling.core.ResourceDispatcher$Companion$of$3.schedule(ResourceDispatcher.kt:32)
at com.fastonetech.scheduling.core.ResourceDispatcher$Companion$of$3.schedule(ResourceDispatcher.kt:27)
at com.fastonetech.scheduling.core.ResourceDispatcher$Companion$of$1$1$2$1.invokeSuspend(ResourceDispatcher.kt:19)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@19b8b4b6, Dispatchers.IO]
这个问题的原因是被修改的配置映射到了代码中被@ConfigurationProperties
注解的类中的一个List
类型的属性.
该属性被修改时恰好协程任务正在遍历该属性, 从而导致ConcurrentModificationException
异常.
每次获取该属性时都进行一次防御性复制, 从而避免ConcurrentModificationException
异常
新增quota_usage.proto文件后编译失败, 输出以下错误
[ERROR] PROTOC FAILED: com/fastonetech/contract/computecloud/deploy/v2/QuotaUsage.java: Tried to write the same file twice.
[libprotobuf WARNING ../../../../../src/google/protobuf/compiler/java/java_file.cc:232] cmdb/v1/cmdb.proto: The file's outer class name, "Cmdb", matches the name of one of the types declared inside it when case is ignored. This can cause compilation issues on Windows / MacOS. Please either rename the type or use the java_outer_classname option to specify a different outer class name for the .proto file to be safe.