"java.lang.ClassNotFoundException: Cannot find class: ${jdbc.driver}"这个报错是MyBatis初学者最常见的拦路虎之一。我第一次在本地环境跑测试时也栽在这个坑里——明明配置文件写得漂漂亮亮,数据库连接参数一个不差,偏偏运行时控制台突然抛出这一行红字,让人措手不及。
这个错误的本质是JVM在动态加载阶段找不到指定的JDBC驱动类。当MyBatis初始化数据源时,会根据配置的driver属性值去加载对应的数据库驱动,但如果类加载器在classpath路径下找不到这个类文件,就会抛出ClassNotFoundException。有趣的是,从报错信息中的"${jdbc.driver}"可以看出,这里明显发生了变量替换失败的情况,说明问题可能出在配置文件的解析环节。
在MyBatis的XML配置文件中,我们通常会这样定义数据源:
xml复制<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
当看到报错信息中直接显示"${jdbc.driver}"时,说明发生了以下两种情况之一:
Java类加载器查找驱动类的过程是这样的:
首先确保存在jdbc.properties文件,并且内容格式正确:
properties复制jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useSSL=false
jdbc.username=root
jdbc.password=123456
然后在MyBatis主配置文件中必须明确指定加载这个属性文件:
xml复制<configuration>
<properties resource="jdbc.properties"/>
<!-- 其他配置 -->
</configuration>
关键细节:properties文件的路径是相对于classpath根目录的。如果文件放在src/main/resources/config目录下,resource属性应写为"config/jdbc.properties"
对于Maven项目,pom.xml中必须包含对应数据库驱动依赖。以MySQL为例:
xml复制<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
验证依赖是否真正引入的方法:
mvn dependency:tree查看依赖树即使所有配置都正确,如果运行时classpath不完整也会导致此错误。可以通过以下代码打印当前classpath:
java复制System.out.println(System.getProperty("java.class.path"));
对于Web项目,特别要注意:
在实际项目中,我们通常会有多套环境配置(dev/test/prod)。一个常见的错误是在切换环境时忘记同步修改配置加载逻辑。推荐的做法是:
xml复制<profiles>
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
</profile>
</profiles>
xml复制<properties resource="jdbc-${env}.properties"/>
不同数据库版本的驱动类名可能有变化:
血泪教训:我曾经因为MySQL升级导致驱动类名变更而排查了2小时,建议在版本升级时首先检查驱动类名是否变化
在使用动态数据源(如Spring AbstractRoutingDataSource)时,配置方式有所不同:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
@Override
public void afterPropertiesSet() {
// 需要手动初始化各数据源的驱动类
Map<Object, DataSource> resolvedDataSources = new HashMap<>();
dataSources.forEach((key, value) -> {
// 这里会触发驱动加载
resolvedDataSources.put(key, value);
});
super.setResolvedDataSources(resolvedDataSources);
super.afterPropertiesSet();
}
}
为了避免这类问题在生产环境发生,我总结了几条最佳实践:
java复制@SpringBootApplication
public class MyApp implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
// 应用启动时验证驱动类是否存在
try {
Class.forName(env.getProperty("spring.datasource.driver-class-name"));
} catch (ClassNotFoundException e) {
log.error("JDBC驱动类加载失败,请检查配置", e);
System.exit(1);
}
}
}
yaml复制spring:
datasource:
hikari:
connection-test-query: SELECT 1
validation-timeout: 3000
java复制@Test
public void testDbConnection() throws SQLException {
try (Connection conn = dataSource.getConnection()) {
assertFalse(conn.isClosed());
}
}
根据我多年排查经验,以下是最常见的几种错误模式:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 报错显示$ | 属性文件未加载 | 检查properties标签的resource属性 |
| 驱动类名正确但依然报错 | 依赖版本冲突 | 执行mvn dependency:tree排查 |
| 测试环境正常但生产报错 | classpath不一致 | 对比两个环境的lib目录 |
| 间歇性出现类找不到 | 类加载器问题 | 检查是否有多线程并行加载 |
| 报错类名带$$Enhanced | AOP代理问题 | 检查代理配置和排除策略 |
bash复制java -verbose:class MyApplication
在复杂的应用系统中,可以考虑自定义类加载策略来避免这类问题:
java复制public class SafeDriverLoader {
private static final Map<String, Class<?>> DRIVER_CACHE = new ConcurrentHashMap<>();
public static Class<?> loadDriver(String className) throws ClassNotFoundException {
return DRIVER_CACHE.computeIfAbsent(className, k -> {
try {
return Class.forName(k, true,
Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException("驱动加载失败: " + k, e);
}
});
}
}
这种设计带来了三个好处: