ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java项目启动报NullPointer,5个最佳实践救你命

Java项目启动报NullPointer,5个最佳实践救你命 Java项目启动报NullPointer,5个最佳实践救你命 刚接手的Java老项目,复制了同事的启动代码,结果控制台一片红,满屏NullPointerException。你盯着报错信息看了十分钟,连错在哪一行都不知道,更别提怎么改了。这种“代码能跑在我机器上,换个环境就崩”的噩梦,每个Java开发者都经历过。 别慌。今天不讲深奥理论,只聊那些让你抓狂的报错背后,到底藏着什么坑。我会把最常见的几个坑拆开揉碎,告诉你为什么错、怎么改、以后怎么防。这些经验,是从无数个通宵调试里攒出来的,希望能帮你少走点弯路。 坑一:依赖冲突导致类加载失败 现象:项目能编译通过,但一启动就报ClassNotFoundException或NoClassDefFoundError。明明pom.xml里写了依赖,为什么找不到类? 根本原因:Maven依赖传递机制。A项目依赖B,B依赖C,但A又直接依赖了C的另一个版本。Maven默认选择“最近优先”原则,可能导致加载了不兼容的版本。比如Spring Boot 2.x和3.x的包路径都变了,混用直接崩。 错误写法: dependencygroupIdorg.springframework/groupIdartifactIdspring-core/artifactIdversion5.3.20/version /dependency dependencygroupIdorg.springframework/groupIdartifactIdspring-web/artifactIdversion6.0.1/version /dependency这段代码看似没问题,但Spring 6.0.1要求Java 17+,而Spring 5.3.20基于Java 8设计。混用会导致方法签名不匹配,运行时直接抛异常。 正确写法: dependencyManagementdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-dependencies/artifactIdversion3.1.0/versiontypepom/typescopeimport/scope/dependency/dependencies /dependencyManagement用Spring Boot的BOM统一管理版本。所有Spring组件版本由BOM锁定,避免手动指定版本造成冲突。这是Spring官方源码仓库里明确推荐的依赖管理方式,比手动对齐版本可靠得多。 复现与修复:运行mvn dependency:tree查看依赖树 找到冲突的库,用-Dverbose参数查看排除信息 在冲突依赖上添加exclusions标签排除旧版本 重新打包测试规避建议:新项目统一用Spring Boot Starter,老项目迁移时先跑dependency:tree检查。别相信IDE的自动解析,Maven的依赖仲裁规则比想象中复杂。 坑二:时区处理导致数据错位 现象:用户在北京下单,数据库里存的时间比实际晚8小时。或者反过来,比实际早8小时。客服打电话来投诉,你查日志发现时间对不上。 根本原因:JVM默认时区是服务器所在时区,而数据库连接可能用了不同时区。java.util.Date不存储时区信息,只存时间戳,转换时依赖JVM默认时区,跨环境部署必然出错。 错误写法: Date now = new Date(); String sql = INSERT INTO orders (created_at) VALUES ( + now + );直接拼接Date对象,JVM会调用toString()方法,格式不固定,且依赖默认时区。在UTC服务器上跑,存进去的就是UTC时间,北京时间用户看到的就是错乱数据。 正确写法: ZonedDateTime now = ZonedDateTime.now(ZoneId.of(Asia/Shanghai)); String sql = INSERT INTO orders (created_at) VALUES (?); preparedStatement.setObject(1, now.toLocalDateTime());明确指定时区,用LocalDateTime存入数据库(假设数据库列是DATETIME类型),彻底摆脱JVM默认时区的干扰。这是Java 8时间API的设计初衷,也是Spring官方文档里反复强调的最佳实践。 复现与修复:检查数据库列类型,确认是TIMESTAMP还是DATETIME TIMESTAMP会按时区转换,DATETIME不会,选错直接踩坑 在连接串里显式指定时区:jdbc:mysql://host:3306/db?serverTimezone=Asia/Shanghai 统一所有环境用同一时区配置,别靠JVM默认值规避建议:新项目一律用LocalDateTime+ZonedDateTime,别再用Date。如果必须用Date,永远手动指定时区。数据库设计时,DATETIME存业务时间,TIMESTAMP存系统时间,分开管理。 坑三:线程池参数配置不当导致OOM 现象:系统运行几天后内存飙升,最终OOM。看堆栈全是Thread对象,但业务逻辑里没看到哪里创建了线程。 根本原因:每次请求都new Thread(),或者用了Executors.newFixedThreadPool()这类便捷方法。这些方法创建的线程池队列是无界的(LinkedBlockingQueue默认容量Integer.MAX_VALUE),任务堆积直接撑爆内存。 错误写法: ExecutorService pool = Executors.newFixedThreadPool(10); pool.submit(() - {// 业务逻辑 });看起来简洁,但队列无界。如果任务处理速度跟不上提交速度,队列会无限增长,每个任务对象都占内存,最终OOM。 正确写法: ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 20,60L, TimeUnit.SECONDS,new LinkedBlockingQueue(100),new ThreadFactoryBuilder().setNameFormat(biz-pool-%d).build(),new ThreadPoolExecutor.CallerRunsPolicy() );明确指定核心线程数、最大线程数、队列容量、拒绝策略。队列有界,超过容量时触发拒绝策略,而不是无限堆积。这是阿里巴巴Java开发手册里强制要求的写法,也是生产环境避免OOM的基本功。 复现与修复:用JVisualVM或Arthas监控线程数变化 发现线程数持续增长,定位到创建线程的代码 替换为有界队列的ThreadPoolExecutor 设置合理的拒绝策略,CallerRunsPolicy让调用线程自己执行,起到背压作用规避建议:永远别用Executors的便捷方法创建线程池。每个业务场景单独配置线程池,别共用。线程池参数不是拍脑袋定的,要压测后调整。核心线程数参考CPU核数,队列容量参考业务峰值。 坑四:SQL注入与慢查询隐患 现象:系统突然变慢,数据库CPU 100%,连接池耗尽。查慢查询日志,发现一堆全表扫描的SQL。 根本原因:字符串拼接SQL,用户输入直接进SQL语句。要么被注入,要么走了全表扫描。LIKE '%keyword%'这种写法,索引直接失效。 错误写法: String sql = SELECT * FROM users WHERE name LIKE '% + keyword + %'; Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql);字符串拼接,keyword如果是'; DROP TABLE users; --,直接删库。就算没注入,LIKE '%keyword%'前缀模糊查询,B+树索引无法使用,全表扫描。 正确写法: String sql = SELECT * FROM users WHERE name LIKE ?; PreparedStatement ps = connection.prepareStatement(sql); ps.setString(1, % + keyword + %); ResultSet rs = ps.executeQuery();参数化查询防注入。但LIKE '%keyword%'还是慢,要加全文索引或用搜索引擎。如果必须前缀模糊,改成LIKE 'keyword%',索引就能用上。 复现与修复:开启MySQL慢查询日志,long_query_time=1 用EXPLAIN分析SQL执行计划,看type列是不是ALL 优化SQL,能走索引就走索引 高频模糊查询场景,考虑Elasticsearch规避建议:所有SQL用PreparedStatement,别用字符串拼接。LIKE查询谨慎使用,优先精确匹配或前缀匹配。复杂查询场景,评估是否需要搜索引擎。数据库索引不是越多越好,要结合实际查询模式设计。 坑五:异常吞掉导致问题难追踪 现象:系统报错日志里全是Exception in thread main java.lang.Exception,没堆栈,没上下文,不知道哪里出的问题。 根本原因:catch(Exception e) { e.printStackTrace(); }或者catch(Exception e) { }。前者打印到控制台,生产环境控制台没人看;后者直接吞掉,连日志都没有。异常没带上下文,排查时只能靠猜。 错误写法: try {processOrder(orderId); } catch (Exception e) {e.printStackTrace(); }printStackTrace输出到System.err,生产环境通常重定向到/dev/null,等于没打。就算能看到,也没说哪个订单、哪个环节出错,排查时抓瞎。 正确写法: try {processOrder(orderId); } catch (Exception e) {log.error(处理订单失败, orderId={}, 原因={}, orderId, e.getMessage(), e);throw new BusinessException(订单处理异常, e); }用SLF4J日志框架,记录关键业务参数(orderId),带完整堆栈(第三个参数传e)。业务层捕获后包装成业务异常,向上传递,由统一异常处理器记录并返回友好提示。这是Spring Boot异常处理的标准做法,也是生产环境可观测性的基础。 复现与修复:全局搜索e.printStackTrace()和空catch块 替换为SLF4J日志,带上业务关键参数 配置logback,生产环境INFO级别,异常ERROR级别 接入ELK或云日志服务,方便检索和分析规避建议:禁止空catch块,代码审查时重点检查。日志要带上下文,没上下文的日志等于没打。异常处理要分层,底层记详细,上层给友好提示。别怕日志量大,排查问题时,少一行日志可能多查两小时。 这几个坑,覆盖了依赖管理、时间处理、线程池、SQL、异常处理五个高频场景。每个坑背后,都是无数个生产事故的教训。Java生态成熟,但坑也不少,关键在于理解底层机制,别靠记忆和运气写代码。 你更常用哪种写法?评论区交流。
返回列表