去年年底做安全审计的时候,手里那套datax-web图形化调度平台被连番点名:Spring Boot 2.3.7已经停止社区维护,JDK8在扫描报告里挂了一串高危,前端和权限体系又急着接OAuth2。DataX本身是阿里开源的数据同步工具,单独用要手写JSON、敲命令行,团队协作时太容易出错,所以才有datax-web这类图形化界面,把Reader/Writer、同步任务、调度记录都管起来。datax-web在开源社区里算比较常用的DataX管理端,我们内部也用了好几年。当时决定把它从Spring Boot 2.x + JDK8整体升到Spring Boot 3.2 + JDK17,本以为是个体力活,真正动起手来才发现,框架升级本身就是一次小型的架构重构。DataX这个同步引擎没怎么变,变的全是它外面的壳——任务管理、用户权限、调度日志、执行器调用,每一层都要在Boot3和JDK17的规则里重新过一遍。这篇文章把整段升级过程的关键节点、依赖版本矩阵和排查思路原样写出来,给同样维护datax-web二次开发版或者类似老管理端的同学做个参考。
1. 升级前先摸清datax-web的家底:版本矩阵与依赖边界
1.1 老平台的技术栈底子
datax-web是什么?它是阿里开源同步工具DataX的图形化管理端,使用者不需要手写DataX的作业JSON,而是通过Web界面配置Reader和Writer,由后端帮忙生成JSON并调度执行。项目本身是Spring Boot单体后台加Vue管理界面,用户、菜单、数据源、任务管理都在一个库里。
我们维护的分支基于Spring Boot 2.3.7.RELEASE、JDK8,安全这块早期版本用的是Spring Security + JWT,数据库MySQL,连接池Druid,ORM用的MyBatis。这套组合在当年是标准答案,但站在现在往回看,问题很刺眼:Spring Boot 2.x的社区维护已经停止,JDK8在容器环境和麒麟这类Linux环境上的安全配置越来越麻烦,Security 5.x的配置方式跟新生态也严重脱节。升级前的第一件事不是改pom.xml,而是先把这个技术栈的家底盘清楚,哪个组件在哪个版本、哪个库还停在被废弃的写法上,全都要有个清单。
1.2 升级目标与边界划定
这次升级的目标很明确:JDK从8升到17 LTS,选17.0.10以上的patch版本;Spring Boot从2.3.7升到3.2.x,优先选已经出了一堆patch的稳定小版本;Spring Security跟着升到6.2+,把登录、鉴权、JWT链路重写;数据访问层替换到兼容Boot3的MyBatis、PageHelper、Druid版本。还有一个关键决定是:DataX核心引擎暂时不动,仍然用已验证过的稳定版本。
这个边界非常重要。datax-web这个工程里,最不稳定、最需要谨慎的是调度执行链路,如果顺手把DataX引擎也升级了,出了问题就要在框架升级和引擎升级两层里排查,难度翻倍。我的原则是:一次只动一件事。框架层动了,引擎层就锁版本。很多人在升级时喜欢把能升的全部升一遍,最后连到底是哪一步炸的都不知道,这种教训实在太多。
1.3 先写验证清单,再动手改依赖
升级前先定死一条回归路径:管理员和普通用户登录是否正常、创建数据源测试连通是否通过、配置同步任务生成JSON是否成功、手动触发任务执行器是否跑得通、调度记录和历史任务是否完整、数据源权限隔离是否还在。后面每改一步依赖,都拿这条路径跑一遍。只跑一个“启动不报错”远远不够,很多问题要等真正登录、真正跑一个迁移任务才会暴露出来。先有验证清单,再动手改代码,这是整个升级过程里性价比最高的一件事。
2. javax到jakarta这一步,决定后面所有麻烦的大小
2.1 为什么改名会引发连锁反应
Spring Boot 3最大的硬性变化,是所有原本挂在javax.命名空间下的Java EE API整体迁到了jakarta.。这不是Spring自己改的,是Java EE移交给Eclipse基金会后,Jakarta EE 9起的包名规则。Tomcat 10只实现jakarta.servlet,不再认javax.servlet。所以Boot3项目里如果还留着javax.servlet.http.HttpServletRequest这样的import,编译能过(因为你可能还带着老servlet-api依赖),一旦运行,类直接冲突或者找不到。
我在升级datax-web时第一步动作就是全局搜索import javax.,把所有命中点拉出来梳理。Controller层、过滤器、拦截器、工具类,全都要改。这里有一个特别容易忽略的坑:javax.annotation.PostConstruct、javax.annotation.Resource在JDK9之后被从Java SE里移除了,所以哪怕你只升JDK不升Spring Boot,也可能踩到。Boot3环境下统一换成jakarta.annotation.*。数据校验注解也一样,javax.validation.要整体换成jakarta.validation.,不然Controller层的@Valid @RequestBody直接失效。
2.2 数据访问层的依赖换血
datax-web的持久层是MyBatis配PageHelper分页,连接池Druid,这三个组件在Boot3下全部要换坐标版本。下面是我们最终锁定的版本对照表:
| 组件 | 升级前 | 升级后 | 补充说明 |
|---|---|---|---|
| JDK | 1.8 | 17.0.10+ | 选LTS,别用17.0.1这种早期版 |
| Spring Boot | 2.3.7.RELEASE | 3.2.x | 生产环境图稳,不追最新大版本 |
| mybatis-spring-boot-starter | 2.2.0 | 3.0.3 | 注意mybatis-spring 3.0的API变化 |
| pagehelper-spring-boot-starter | 1.4.6 | 2.1.0 | Boot3有专门的starter坐标 |
| druid-spring-boot-starter | 1.2.6 | druid-spring-boot-3-starter 1.2.20+ | 或者手动注入DruidDataSource |
| Lombok | 1.18.22 | 1.18.30+ | 低版本编译期就挂 |
| spring-security | 5.x | 6.2.x | 配置类基本要重写 |
| servlet-api | javax.servlet-api 4.0.1 | jakarta.servlet-api 6.0 | 依赖由Boot传递管理 |
MyBatis这里有个细节:mybatis-spring-boot-starter 3.0.x对应的mybatis-spring版本是3.0.x,不再是2.x。如果你原来在代码里直接new SqlSessionFactoryBean或者依赖tk.mybatis这类老封装,要确认它对Boot3的自动装配机制是否兼容。datax-web的DAO层主要用注解,影响不大,但有个别插件依赖老mapper-starter,这些必须清掉。PageHelper换到2.1.0之后,原来的PageHelperAutoConfiguration自动装配逻辑有变化,分页参数和插件顺序要重新验证一下,否则列表页可能分页失效。
2.3 spring.factories文件消失,自动装配改名
Spring Boot 2.x时代,自定义自动配置类写在META-INF/spring.factories里,键是org.springframework.boot.autoconfigure.EnableAutoConfiguration。Boot3把这个机制改成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,里面的内容是一行一个自动配置类的全类名,同时要求类上加@AutoConfiguration注解。
这个改动的坑在于:如果不改,Spring Boot不会报错,只是你的自动配置静默失效。datax-web里如果有自定义数据源初始化类、任务执行器初始化类,这种失效会在运行时变成奇奇怪怪的Bean找不到或任务仓库未初始化。我当时排查一个执行器初始化失败的问题,花了半天,最后才发现是自动配置文件路径不对。如果你是从IDEA新建Spring Boot 3工程,用Spring Initializr生成的项目里已经自动带了正确的AutoConfiguration.imports文件,直接改就行。
2.4 创建新工程时别在IDEA里选错版本
如果你打算用IDEA从零搭一个Boot3工程来对照,Spring Initializr里选Spring Boot 3.2.x,Java版本选17,Maven工程。这里要注意:生成出来的pom里Java版本已经是17,但如果你本地没装JDK17,IDEA会提示invalid source release。先把JDK17装好,再在Project Structure里把Project SDK和Modules的Language Level都切到17。千万不要出现Project SDK是17、Modules Language Level还是1.8的割裂状态,这种配置下编译不报错,但运行起来全是奇怪的不兼容。
3. Spring Security 6重写登录鉴权链路:旧写法全废
3.1 WebSecurityConfigurerAdapter被删了
datax-web原本的Security配置类大概率是继承WebSecurityConfigurerAdapter,重写configure(HttpSecurity http)和configure(AuthenticationManagerBuilder auth)这两组方法。这套写法在Spring Security 5.7开始标记废弃,到6.x彻底删干净。升级Boot3后编译期就会报错,不是运行期问题,所以很好发现,但正确的新写法需要重新理解。
新写法是直接声明SecurityFilterChain的@Bean:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .cors(Customizer.withDefaults()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/captcha", "/swagger-ui/**", "/doc.html", "/favicon.ico").permitAll() .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }这段代码有几个点值得展开说。首先csrf直接禁用,因为datax-web是无状态JWT接口,不需要CSRF防护;如果保留默认开启,登录后的POST请求会一直403。其次session管理设为STATELESS,否则Security会在内存里维护Session,跟JWT无状态设计冲突。最后authorizeHttpRequests里的requestMatchers,就是原来antMatchers的替代品。
3.2 antMatchers到requestMatchers,不只是改个名
很多人以为antMatchers换成requestMatchers就是关键字替换,实际匹配器的底层实现从AntPathRequestMatcher切到了PathPatternRequestMatcher,两者的通配规则略有差异。对大多数/xx/**这种写法没有影响,但如果你原来用了一些比较刁钻的Ant表达式,例如*只匹配一级路径这种特性,需要重新验证。还要注意,如果配了多个SecurityFilterChain,每个filter chain的匹配顺序也要理清。
认证这部分,原来习惯直接@Autowired一个AuthenticationManager进登录Controller,在Security6里如果直接在配置类里把AuthenticationManager定义为@Bean,容易在依赖注入时踩循环依赖,Security官方推荐从AuthenticationConfiguration获取:
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); }然后在登录接口里正常调authenticationManager.authenticate(...)即可。这里有一个常见的坑:如果自定义UserDetailsService和PasswordEncoder的@Bean没配对,会报无法找到用户服务,或者密码校验永远失败。我用的是BCryptPasswordEncoder,而旧库里如果存的是明文或者MD5,老账号会全线登录失败,需要做密码编码兼容。datax-web这种老平台,很多时候用户表里的密码是历史原因留下的,升级前先看看备份库里的密码哈希前缀是什么,再决定PasswordEncoder的兼容策略。
3.3 JWT过滤器与SecurityContextHolder的适配
JWT校验的逻辑可以用一个OncePerRequestFilter实现,除了放行的登录接口之外,其他请求都走一遍:
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = resolveToken(request); if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 解析token,从缓存加载用户信息,组装UsernamePasswordAuthenticationToken SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); } }这里有几个容易被忽略的点。一是过滤器里加载用户信息时不要每次都查库,高并发下datax-web的任务查询接口很吃鉴权性能,Redis或者本地缓存兜一层很必要。二是SecurityContextHolder默认策略是ThreadLocal,如果异步任务里要用到用户信息,需要显式设置策略为MODE_INHERITABLETHREADLOCAL。三是异常处理:filter里token过期或非法时,不要直接抛异常丢给Spring Security默认入口,否则前端拿到的不是JSON错误信息,而是默认的403/500页面,这些要统一处理成接口返回结构。
3.4 跨域配置和OAuth2扩展位
Boot3里跨域推荐声明CorsConfigurationSource的@Bean,然后在Security的cors(Customizer.withDefaults())里生效。datax-web前端如果和后台不同源,这个必须配好。原来Boot2时代很多项目是在WebMvcConfigurer的addCorsMappings里写的,Boot3下Security6会先于CORS过滤器工作,如果只配了MVC层,授权之前跨域请求就直接被拦截了,前端登录都发不出去。
热词里反复出现springboot3 security6 oauth2,这里多说一句。升级到Boot3 + Security6最大的红利之一,就是OAuth2客户端的接入变得很规整。引入spring-boot-starter-oauth2-client,配置ClientRegistrationRepository,就能对接第三方登录。datax-web这种管理平台后续如果要接企业微信、钉钉或者内部统一身份认证,这个扩展位在升级后已经是通的。我们这次升级把oauth2-client依赖加进去了,但暂时没启用,只留了配置位,避免一次动太多功能面。
4. JDK17模块化反射限制:运行期各种NoSuchFieldError的元凶
4.1 JDK9之后反射不再是原来的反射
JDK17带来的第一大冲击,不是语法,而是模块化之后反射访问被收紧。JDK8里写个反射直接访问java.lang包的私有字段,虽然警告但能过;JDK17下会直接抛InaccessibleObjectException,本质是模块系统不允许非法访问java.base模块内部。Spring自身靠ASM和MethodHandles的Lookup机制做了大量适配,但datax-web工程里还有DataX引擎这一层,这层是独立进程,不是Spring容器管理的。
升级后我第一次跑同步任务,DataX引擎启动阶段直接报:
java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(...) accessible: module java.base does not "opens java.lang" to unnamed module看到这个报错就明白了,DataX的插件类加载器在执行defineClass时访问了java.lang包,但JDK17默认不开这个口子。这个过程很像你住在一栋有门禁的大楼里,老式门卡在旧楼里随便刷,换了新楼必须先到物业登记才能开门。
4.2 --add-opens清单和两处生效位置
解决方案是给JVM加--add-opens参数。简要解释一下这个参数的作用:它告诉JVM,允许某个模块的包向另一个模块开放反射访问。对应用来说,平时访问不到java.base里的内部类,加了之后就能用反射访问了。
实际用到datax-web里的完整清单:
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.sql/java.sql=ALL-UNNAMED这串参数实际上要加在两个地方。datax-web主进程一份,放在bin/server.sh的JAVA_OPTS里;DataX执行器子进程一份,放在DataX安装目录下bin/datax.py的JVM参数模板里。因为datax-web调用DataX任务时,是通过ProcessBuilder拉起一个新的Java进程来跑datax.py脚本的,这个子进程和主进程是两套JVM。如果你只在主进程加了--add-opens,子进程里DataX引擎启动照样炸。我当时漏了第二处,主进程启动很干净,一触发任务就报错,而且报错信息在日志文件末尾,非常不好找。
4.3 还有一类问题是代理类和Lombok引起的
JDK17下CGLIB动态代理有个历史问题:生成代理类时通过ClassLoader.defineClass反射访问,如果缺上面那行java.lang的add-opens,Spring AOP、MyBatis拦截器、PageHelper代理都可能挂。补上之后基本就顺了。
另外Lombok也必须升到1.18.30+,否则编译期就报illegal reflective access。Maven的maven-compiler-plugin的release参数也要同步改成17,别只改pom里的java.version,编译插件不跟上,打出来的字节码版本还是52(JDK8),跑在JDK17上虽然能兼容,但你新代码用到的JDK17语法和API会直接编译失败。用IDEA的annotationProcessorPaths配置时,把Lombok版本也钉死,避免多个模块版本不一致。
5. 执行器改造与调度链路验证:从datax.py到DS联动
5.1 一个同步任务在前后台是怎么跑起来的
要理解升级后该查哪里,得先把datax-web执行同步任务的链路梳理清楚。datax-web本身不执行数据抽取,它是给DataX做编排和托管的。具体流程是:用户配置Reader/Writer,后台生成DataX作业JSON;JSON落到本地临时目录,datax-web记录任务实例;后台通过ProcessBuilder执行python脚本(通常是datax.py),脚本参数是JSON路径;datax.py内部检查JAVA_HOME,用java命令启动DataX核心进程;DataX核心加载Reader/Writer插件做同步,实时输出日志;datax-web按行读取子进程输出,回传到前端。
代码层大致是这样:
ProcessBuilder processBuilder = new ProcessBuilder("python3", dataXPyPath, jobJsonPath); processBuilder.redirectErrorStream(true); processBuilder.directory(new File(dataXHome)); Process process = processBuilder.start();升级Boot3/JDK17对前两步和后两步影响不大,问题集中在中间几步的环境匹配上。最典型的就是子进程拿到的JAVA_HOME还是JDK8的路径,datax.py就按JDK8去启动DataX了,这不会立刻报错,但会绕过前面配的所有add-opens,等到插件反射直接崩。而且datax.py通过java -version判断版本时,如果输出里有两套版本,它会优先用PATH里找到的那一套,这个排查起来非常迷惑。
5.2 在麒麟V10环境上翻车的三个细节
真实环境是麒麟V10 x86_64,部署时遇到的问题都很有代表性。
第一个是服务器上同时装了几套JDK。系统里既有JDK8又有JDK17,直接在终端执行java -version显示的是17,但datax-web的bin/server.sh里写死了老路径,或者用旧的JAVA_HOME环境变量,导致平台是17启动的,子进程是8启动的。解决方式统一:在bin/server.sh顶部显式export JAVA_HOME和PATH,不要在脚本里依赖全局环境变量的运气。用source /etc/profile后,执行which java确认指向新路径,如果/usr/bin/java里还有老JDK软链,优先级会覆盖PATH里的配置。
第二个是临时目录权限。datax-web把作业JSON写到java.io.tmpdir,如果这个目录没写权限,任务会一直卡在等待执行。排查这类问题时,先清理掉残留的python/java进程,再手动跑一次datax.py,看完整报错,往往比看平台日志快得多。如果日志里出现“Permission denied”或“No such file or directory”,基本就是临时目录或DataX安装目录的权限问题。
第三个是python2和python3的差异。老的datax.py脚本有些版本依赖python2的语法,而麒麟V10默认python是3。如果ProcessBuilder里写的是“python”,可能跑到python3解释器上,脚本跑不起来。建议在全局配置里明确python执行路径,用python3或者python2的绝对路径,不要裸写python。这个坑和JDK版本无关,但升级后重新部署环境时最容易一起爆发。
5.3 DolphinScheduler能不能调度DataX任务
很多人会问,dophin调度可以调datax任务吗?可以,而且和datax-web的升级完全不冲突。DolphinScheduler从3.x开始原生支持DataX任务类型,在任务定义里选择DataX节点,把DataX安装目录、作业JSON模板填进去即可。它的原理就是DS自己的worker去调用datax.py,跟datax-web的ProcessBuilder没有本质区别。所以DataX引擎本身只要能跑,DS侧不用做任何Boot3相关的适配。
这两种用法可以并存。数据源配置、JSON生成、任务试跑放在datax-web里做,生产环境的定时调度交给DolphinScheduler。升级datax-web只影响它自己的界面和管理功能,不影响DS对DataX的调度。如果DS侧也跑在同一台机器上,记得确认DS worker进程能读到JDK17的环境变量,否则DS触发DataX任务时同样会按旧JDK路径去启动,然后踩到反射问题。
6. 在麒麟V10+MySQL8上落地:安装、启动脚本与验证
6.1 Linux环境JDK17安装与JAVA_HOME切换
先确定架构,热词里提到的是x86_64,所以在镜像站选择对应x86_64架构的OpenJDK17 tar.gz包。不要下载带aarch64后缀的包,架构不匹配会报Exec format error。解压到统一目录后,编辑/etc/profile:
export JAVA_HOME=/usr/local/java/jdk-17.0.10 export PATH=$JAVA_HOME/bin:$PATH这里有个检查技巧:source /etc/profile后,执行which java,看是不是指向了刚解压的路径。如果系统里还有老JDK的软链在/usr/bin/java,优先级可能比PATH里的更高。最简单粗暴又可靠的办法,是把/usr/bin/java直接替换成软链指向新JDK,并且在datax-web和DataX的所有启动脚本里都显式带JAVA_HOME。Windows环境的JDK17安装更简单,解压或点安装包都行,但同样要注意把JAVA_HOME和Path系统变量配好,确保cmd里java -version输出版本是17。
6.2 MySQL8.0.36免安装部署和驱动参数
测试环境用MySQL8.0.36的免安装包,Windows下解压zip后,用mysqld --initialize-insecure初始化数据目录,再mysqld --install命令注册为服务。Linux下同样是解压tar包,初始化后首次启动。这些操作网上教程很多,不展开,重点说连接池和驱动参数。
datax-web的数据源URL建议写成:
jdbc:mysql://localhost:3306/datax_web?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true前面几个参数好理解,时区不对数据会差8小时,MySQL8的caching_sha2_password认证方式下不加allowPublicKeyRetrieval=true会报Public Key Retrieval is not allowed。最后一个nullCatalogMeansCurrent=true是关键中的关键:Druid连接池在MySQL多库场景下,元数据获取如果沿用老逻辑,可能把别的库的表也当成当前库的表,导致任务列表、数据源测试全乱。加上这个参数,DataX元数据只认当前连接的catalog。MySQL8的驱动类用com.mysql.cj.jdbc.Driver,不用再写老的com.mysql.jdbc.Driver,Boot3自动配置能识别。
6.3 回归验证的完整顺序
最后把升级后的回归路径完整跑一遍。这一步建议严格按照从外层到内层的顺序:启动datax-web主进程,观察启动日志无异常,端口正常监听;打开登录页,输入管理员账号,验证验证码、登录接口、JWT签发;创建MySQL数据源,测试连通性,确认Druid监控页有连接数据;配置一个最简单的源表到目标表的同步任务;手动触发,观察日志输出,任务从执行中到成功,DataX统计信息出现在日志里;查看运行历史、调度记录、用户权限菜单。
这个顺序的意义在于分层定位问题。如果第3步过了但第5步挂,问题范围就限定在执行器子进程环境(JAVA_HOME、python路径、add-opens),跟Security无关;如果第2步就挂,问题范围限定在Security6和JWT过滤器,先不要动数据源和DataX。这种分层排查的思路,比盯着一条报错瞎猜有效得多。尤其是升级这种牵一发动全身的任务,惰性排查只会让你把时间耗在无关的地方。
最后分享一点个人经验。升级这种老管理平台,最怕的就是想一次性把所有库都换到最新、顺手重构一把。Spring Boot 3 + JDK17不是简单的版本号跳动,而是整个Java生态在命名空间和模块化上的一次分水岭,老第三方库每一个都可能是雷。我这次能在一周多的时间内稳定跑完回归,靠的就是先锁定验证路径,再按“JDK → Spring Boot → 安全框架 → 数据访问层 → 执行器环境”的次序一层一层换,每一层换完都跑一遍最小闭环。DataX引擎本身没变,但整个管理平台的底座换了之后,后面接企业微信、接统一身份认证这类新需求就顺手很多。如果你的datax-web也卡在Boot2和JDK8上,照着这个思路走,应该能省下不少排查时间。