1. 先复现一下两个报错的现场SpringBoot整合H2这个操作看起来特别简单pom里加一个依赖application.yml里配置一段连接信息顺手写一个测试接口一启动控制台干干净净好像万事大吉。结果等你满心欢喜地调用接口时数据库层直接给你泼一盆冷水要么是Wrong username or password要么是Table USER not found。这两个报错我前后见过的次数加起来比SpringBoot自身报过的错都多。尤其是第一次接触H2的人很容易被这两句话绕晕因为从表面看它们都像是环境的错实际上绝大多数情况是自己挖的坑。这篇继续SpringBoot系列的实操记录专门把连上H2却查不了数据这件事掰开揉碎。内容不绕弯子先解释两个报错背后的机制再给出一套可以直接抄的配置最后附上排查速查表。无论你是刚入门SpringBoot还是在项目里临时用H2做演示、做单元测试都应该能从这篇里找到对应的解法。先说明一下这两个报错的出场位置。Wrong username or password大多出现在应用启动阶段Spring容器创建DataSource和JPA EntityManager或者第一次触发数据库操作的时候Table USER not found则更多出现在运行时的查询阶段启动过程可能完全正常。两者的发生时机不同根因也完全不一样所以别把它们当成同一个问题的两种表现要分开排查。2. 先理解H2的账号体系再谈密码报错2.1 H2默认账号和你以为的不一样很多初学者第一次看到H2文档里写着default user is sa, password is empty就想当然地认为用户名写sa密码不填就行。但在实际连接时密码为空和不提供密码是两个概念。JDBC驱动在获取连接时如果拿到一个null密码而H2用户本身的密码是空字符串你就可能被告知用户名或密码错误。这句话是什么意思呢举个例子。假设你在application.yml里只写了spring: datasource: username: sa没有写password那一行。SpringBoot在自动配置DataSource的时候密码就是null。放在老版本H2里可能还能蒙混过关但H2升级到2.x之后认证逻辑收紧了很多经常直接就报Wrong username or password。正确做法非常简单显式声明空密码spring: datasource: username: sa password: 看似只是一行空字符串但它告诉驱动我确认密码就是空的而不是我没提供密码。这一个细节能排除掉一大半的登录问题。2.2 URL里藏着的参数可能会悄悄覆盖你的配置H2的JDBC URL后面可以跟一堆分号分隔的参数比如jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE。问题在于URL中也可以带USER和PASSWORD参数。一旦你写了它们优先级比DataSource配置里的username和password要更高。举个例子有人在application.yml里规规矩矩写着username: sa却在URL末尾追加了;USERsa;PASSWORD123可实际H2中sa的密码依然是空的。结果就是URL参数里的123成了最高优先级的密码来源连接时拿123去匹配空密码必然失败。这种问题在日志里非常隐蔽不仔细看根本发现不了。所以我的建议是URL里不要出现任何账号信息用户名和密码一律放到spring.datasource.username和spring.datasource.password里统一管理。2.3 H2 Console本身就是最好的登录探测器如果不想靠猜最简单的办法是把H2 Console打开。在SpringBoot项目里配置spring: h2: console: enabled: true path: /h2-console启动后浏览器访问http://localhost:8080/h2-console在登录页填上同样的JDBC URL、用户名、密码点击Connect。能连上就说明账号没问题连不上会返回具体的报错信息你可以立刻判断是密码问题还是URL问题。这一步是排查所有H2连接问题的起点强烈建议先做。注意H2 Console默认只允许本机访问。如果项目里还配置了Spring Security必须把/h2-console/**放行否则你会看到一个空白页甚至404。这个坑和密码错误无关但没留意的话会浪费更多时间。3. Wrong username or password 的完整排查思路3.1 按顺序检查这四个位置遇到这个报错我通常按顺序检查四处配置确认pom里H2依赖存在且没有因为传递依赖引入了多个冲突版本。查看application.yml里是否显式配置了username和password密码为空时要写。检查JDBC URL里有没有;USER...;PASSWORD...这种覆盖账号的参数。确认是否曾经通过H2 Console或脚本修改过sa密码导致默认空密码失效。第3点要特别提醒一句。如果你的项目里同时引入了MySQL和H2依赖而DataSource自动配置选了MySQL驱动那么连接H2时实际用的是MySQL的账号这时候可能同时出现No suitable driver和用户名密码相关的报错。排查时先翻一下启动日志看看真正被创建的DataSource是哪一个能少走很多弯路。3.2 显式配置的最终形态下面这份配置在SpringBoot 2.x和3.x上都适用H2版本取2.2.xspring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true path: /h2-console这里面的URL参数值得解释一下。DB_CLOSE_DELAY-1表示即使最后一个连接关闭也不销毁内存数据库直到JVM退出。如果不加这个参数一旦连接池里的连接被释放整个内存库就消失了下次再查询时你连表都看不到。DB_CLOSE_ON_EXITFALSE则是防止JVM退出钩子触发数据库关闭在有些场景下可以避免日志刷出奇怪的异常。这两个参数不是必须的单连接测试不写也能跑但加上之后行为更稳定。3.3 如果sa密码被改过怎么办如果你通过H2 Console执行过类似ALTER USER SA SET PASSWORD xxx的命令那sa的密码就不再是空的了。这种情况下需要把配置改成实际密码spring: datasource: username: sa password: xxx或者干脆新建一个专用账号避免动管理员账号。H2的账号粒度比较细可以单独为应用建一个用户只给某个Schema的读写权限。不过对于本地开发Demo来说没必要sa加空密码已经足够了。4. Table USER not found四个字背后其实有四种情况这个报错比密码错误更常见因为触发条件实在太多。我把它归纳成四类表没建成、表建成了但看不见、表存在但初始化脚本没跑、以及连接的根本不是同一个库。掌握这个分类排查思路会清晰很多。4.1 表可能真的没建出来USER是保留字第一个原因最直接也最容易被新手忽略USER是SQL标准保留字H2在2.x版本对保留字的处理变得更严格。如果你在schema.sql里写了类似这样的建表语句CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) );H2会直接语法报错建表这一步就失败了。有些旧版本可能不报错或者建了一个奇怪的表但查询时同样找不到。所以最稳妥的做法是避开所有SQL保留字给业务表起一个带前缀的名字。t_user、tb_user、sys_user都行看到USER这种表名趁早改掉。如果因为历史原因必须使用USER作为表名也不是完全没办法。可以给表名加双引号CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) );但代价是查询时也必须加双引号而且大小写要完全一致。H2默认会把不带引号的标识符转成大写一旦用了user它存储的名字就是小写user你用SELECT * FROM USER查会报错必须写SELECT * FROM user。这种写法能跑但非常容易埋坑我的建议还是改名。4.2 表建了但你看不见大小写和Schema在捣鬼第二种情况是表确实建出来了但查询时找不到。最常见的两个因素大小写不一致和Schema不一致。大小写问题继承自H2的标识符规则。H2默认有一个DATABASE_TO_UPPERtrue的选项意思就是所有不带引号的标识符都会自动转成大写保存。你写create table t_user实际存的表名是T_USER。查询时写select * from t_user因为也不带引号会被转成T_USER所以能匹配上。问题出在一旦混用了引号比如建表时用了t_user查询用了不带引号的t_user两边一个保留小写、一个转成大写对不上就报Table T_USER not found。Schema问题同样隐蔽。H2默认有一个PUBLIC Schema建表时不指定Schema表会进PUBLIC。连接用户是sa时默认也走PUBLIC通常没问题。但如果你在URL里加了;SCHEMAxxx或者用ALTER USER SA SET SCHEMA xxx改过连接Schema访问的Schema就变了自然看不到PUBLIC里的表。排查这种问题最直接的办法是在H2 Console里执行SHOW TABLES;它会列出当前连接可见的所有表。再进一步SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES;就能看到表具体在哪个Schema里叫什么名字。4.3 初始化脚本没执行SpringBoot的sql init机制比你想的细第三种情况也特别常见你明明写了schema.sql和data.sql但SpringBoot根本没执行它们。SpringBoot执行初始化SQL有一套自己的逻辑在Spring Boot 2.5之后配置项是spring.sql.init.mode默认值是embedded也就是只对嵌入式数据库自动执行。H2属于嵌入式理论上默认就会执行。但如果你把脚本放到了不对的路径或者项目里同时配置了其他数据源脚本就可能静默跳过。确认脚本是否执行主要看启动日志。正常执行时日志里会有一行类似Executing SQL script from class path resource [schema.sql]的信息。如果找不到这行日志就可以往初始化配置的方向排查。遇到不执行的情况可以显式设置spring: sql: init: mode: always schema-locations: classpath:schema.sql >dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency注意h2依赖的scope是runtime只在运行期需要这是个常见写法。application.yml按前文梳理的要点完整写一遍spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE driver-class-name: org.h2.Driver username: sa password: sql: init: mode: always schema-locations: classpath:schema.sql >DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, age INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );data.sql负责灌数据INSERT INTO t_user (username, age) VALUES (zhangsan, 25); INSERT INTO t_user (username, age) VALUES (lisi, 30);实体类取名为User用Table注解指向t_userEntity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String username; private Integer age; public Long getId() { return id; } public void setId(Long id) { this.id id; } public String getUsername() { return username; } public void setUsername(String username) { this.username username; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age age; } }Repository接口public interface UserRepository extends JpaRepositoryUser, Long { }ControllerRestController RequestMapping(/api/user) public class UserController { private final UserRepository userRepository; public UserController(UserRepository userRepository) { this.userRepository userRepository; } GetMapping(/list) public ListUser list() { return userRepository.findAll(); } }启动项目后浏览器访问http://localhost:8080/api/user/list如果能返回两条JSON数据说明H2已经彻底跑通。在此基础上再打开http://localhost:8080/h2-console用同样的连接信息连进去执行SHOW TABLES应该能看到T_USER表执行SELECT * FROM T_USER能看到刚才插入的数据。5.3 这套方案为什么能同时避开两个报错用一张表把这套方案的设计点说清楚报错来源对应措施原因Wrong username or password显式配置password: 、URL不写账号参数空密码显式声明避免驱动把null当错误密码Table USER not found保留字表名使用t_user而非user彻底避开SQL保留字建表不会失败Table not found大小写不使用引号包裹表名让H2统一转大写查询匹配一致Table not foundschema不额外设置SCHEMA参数保持默认的PUBLIC SchemaTable not found初始化未执行spring.sql.init.mode: always强制加载schema.sql和data.sqlTable not foundJPA冲突ddl-auto: none与defer-datasource-initialization: true避免JPA自动建表和schema.sql互相打架Table not found内存库销毁URL加DB_CLOSE_DELAY-1保持内存库存活连接关闭后不消失这几点不只是为了解决当前报错也是后续维护时少踩坑的保障。尤其表名和初始化配置这两项哪怕项目后面换成文件模式的H2或者换成MySQL思路依然适用。6. 排查报错的黄金路径与速查表6.1 一套固定的排查顺序如果你已经按上面的方案配置了但仍然有问题建议按这个顺序排查看启动日志。搜索schema.sql、data.sql、Initializing关键字确认初始化脚本有没有真正执行。如果SQL有异常日志会明确打出哪一行失败。打开H2 Console手动连接。用和application.yml完全相同的URL、用户名、密码。这一步可以直接复现报错把问题定位到连接还是表。连接成功后执行SHOW TABLES确认表是否存在表名大小写是否符合预期。执行SELECT * FROM T_USER表名按SHOW TABLES的结果来写确认数据是否存在。如果表在但数据为空检查data.sql是否执行成功。核对所有DataSource连接串确认完全一致包括参数部分。这五步走下来大部分问题都能定位。剩下的基本是保留字和大小写问题在H2 Console里一跑SQL就能看出端倪。6.2 常见问题速查表把平时被问得最多的几种情况整理成一张表方便对照排查。现象可能原因解法启动报Wrong username or password密码是null不是空字符串配置password: 启动报Wrong username or passwordURL里有PASSWORD参数与配置冲突删除URL中的USER/PASSWORD改用属性配置启动报Wrong username or passwordsa密码已被修改改成实际密码或重置为空密码查询报Table USER not founduser是保留字建表失败改表名为t_user查询报Table T_USER not found建表用了带引号的小写表名统一不加引号全部走大写查询报Table not found启动日志无schema执行记录初始化脚本未配置或未执行设置spring.sql.init.mode: always查询报Table not found第一次启动正常重启后异常内存库被销毁加DB_CLOSE_DELAY-1表存在但查询不到连接了不同的内存库核对所有URL的库名一致H2 Console登录后看不到表Console连接URL与应用不一致使用完全相同的JDBC URL这张表不算全面但覆盖了H2初学阶段的高频问题。6.3 再聊一个容易被忽略的细节还有一类问题不太起眼但很多人中过招就是H2的版本。H2 1.x和2.x的差异非常大2.x对保留字、认证方式、兼容模式都做了大量收紧。如果你在一个老项目上复用了一份老配置升级H2依赖之后突然冒出这两个报错十有八九是版本差异导致的。最简单的验证方法是看pom里的版本号。H2的groupId和artifactId没变你可能完全感知不到版本变化但行为已经完全不同。遇到这种诡异问题时优先检查依赖版本。7. 最后分享一点个人的使用体会在我自己的项目里H2用得最多的地方是单元测试和本地联调。这种场景下表的命名规范和初始化配置必须从一开始就打算好不要想着反正只是临时用用就把USER这种表名直接写上。数据库的坑往往就是在你急着往前冲的时候埋下的等三个月后再回来看这些代码基本已经想不起来当时为什么会这么写了。所以我个人有一条小原则凡是能通过统一约定绕开的问题绝不留到运行时报错里再去修。表名避开保留字密码显式配置初始化脚本固定放到classpath根目录URL参数统一维护这些都是零成本的习惯却能把H2相关的报错概率降到一个很低的水平。希望这篇记录能帮你少走一些我当年走过的弯路。