Spring Boot数据库开发
通常SpringBoot数据库开发,会引入spring-boot-starter-jdbc
,而如果引入了spring-boot-starter-jdbc
,但没有可用的数据源或者没有配置,那么在运行Spring Boot时会出现异常,因为spring-boot-starter-jdbc
内部会自动配置一个数据源,如果不能连接就会引发异常
假如系统用的数据源来自MySQL,但还没有配置,可以在POM里引入一个内存数据库(也可以配置成持久化存储在硬盘上),轻量级的关系型数据库管理系统,如下所示
<!-- 引入H2数据库的依赖 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
然后在启动SpringBoot,访问如下地址就能看到H2的访问页面,但实际上这个数据库并没有单独安装
在启动日志里也能看到这个数据库的启动
也可以取消SpringBoot默认数据源的创建,如下代码所示
package com.sbdev;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
//exclude = DataSourceAutoConfiguration.class 禁止自动配置数据源
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SbDevApplication {
@GetMapping("/test")
public Map<String, String> test() {
Map<String, String> map = new HashMap<>();
map.put("success", "true");
map.put("message", "我的第一个Spring Boot程序");
return map;
}
public static void main(String[] args) {
SpringApplication.run(SbDevApplication.class, args);
}
}
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
表示启动SpringBoot,不再使用DataSourceAutoConfiguration
这个配置类,如果没有配置数据源,运行时就不再发生异常,这也变相的说明SpringBoot的配置理念,使用配置类来生成对应的Bean,而Bean的属性可以通过配置文件自定义
正常情况在开发应用的时候都会提前配置好,需要确认自己的环境,如下列情况不同的数据库版本配置对应的数据库驱动及参数
服务启动过程中经常会遇到的一个问题是无法创建连接,其中有一种原因是数据库的版本和连接驱动类的版本不匹配,如果使用的是MySQL8.0以下的版本,那在POM中添加的依赖应该是
<!-- 引入MySQL数据库连接驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.29</version>
</dependency>
配置数据源的时候,driverClassName配置应该是props.setProperty("driverClassName", "com.mysql.jdbc.Driver");
而如果MySQL用的是8.0以上的版本,那么POM中应该添加的依赖是
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version> <!-- 使用实际的版本号 -->
</dependency>
或者
<!-- 引入MySQL连接器,用于在运行时连接MySQL数据库 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
配置数据源的时候,driverClassName配置应该是props.setProperty("driverClassName", "com.mysql.cj.jdbc.Driver");
配置好这些之后,在SpringBoot的配置文件application.yml
中,写入数据源配置,如下所示
spring.application.name:
- SBDev
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: Ms123!@#
server:
servlet:
context-path: /SBDev
port: 8001
这样POM中既不需要引入h2database
也不需要在启动类上添加exclude = DataSourceAutoConfiguration.class
,如果上述数据源配置无误且能正常链接,则启动SpringBoot的时候就可以正常启动
这里的配置最终会被读取到DataSourceAutoConfiguration
中,然后再去创建数据源,看一下这个类的源码如下
/**
* 自动配置数据源的类。根据应用程序的条件自动配置嵌入式数据库或连接池。
*
* @AutoConfiguration before = {SqlInitializationAutoConfiguration.class} 指定此配置类在SqlInitializationAutoConfiguration之前加载
* @ConditionalOnClass {DataSource.class, EmbeddedDatabaseType.class} 检查类路径中是否存在DataSource和EmbeddedDatabaseType类,以决定是否应该加载此配置
* @ConditionalOnMissingBean type = {"io.r2dbc.spi.ConnectionFactory"} 如果应用程序中不存在ConnectionFactory bean,则加载此配置
* @EnableConfigurationProperties {DataSourceProperties.class} 启用对DataSourceProperties属性的配置
* @Import {DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class} 导入额外的配置类
*/
@AutoConfiguration(
before = {SqlInitializationAutoConfiguration.class}
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(
type = {"io.r2dbc.spi.ConnectionFactory"}
)
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class})
public class DataSourceAutoConfiguration {
/**
* 判断是否应该配置嵌入式数据库的条件类。
* 它检查是否设置了spring.datasource.url属性,或者是否存在支持的连接池数据源。
*/
static class EmbeddedDatabaseCondition extends SpringBootCondition {
private static final String DATASOURCE_URL_PROPERTY = "spring.datasource.url";
private final SpringBootCondition pooledCondition = new PooledDataSourceCondition();
EmbeddedDatabaseCondition() {
}
/**
* 根据当前环境和条件判断是否应该配置嵌入式数据库。
*
* @param context 条件上下文,提供环境和类加载器等信息
* @param metadata 注解类型元数据,用于获取注解参数等信息
* @return 条件匹配的结果
*/
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("EmbeddedDataSource", new Object[0]);
if (this.hasDataSourceUrlProperty(context)) {
return ConditionOutcome.noMatch(message.because("spring.datasource.url is set"));
} else if (this.anyMatches(context, metadata, new Condition[]{this.pooledCondition})) {
return ConditionOutcome.noMatch(message.foundExactly("supported pooled data source"));
} else {
EmbeddedDatabaseType type = EmbeddedDatabaseConnection.get(context.getClassLoader()).getType();
return type == null ? ConditionOutcome.noMatch(message.didNotFind("embedded database").atAll()) : ConditionOutcome.match(message.found("embedded database").items(new Object[]{type}));
}
}
/**
* 检查是否设置了spring.datasource.url属性并且该属性有文本值。
*
* @param context 条件上下文
* @return 如果设置了有效的spring.datasource.url属性,则返回true;否则返回false
*/
private boolean hasDataSourceUrlProperty(ConditionContext context) {
Environment environment = context.getEnvironment();
if (environment.containsProperty("spring.datasource.url")) {
try {
return StringUtils.hasText(environment.getProperty("spring.datasource.url"));
} catch (IllegalArgumentException var4) {
}
}
return false;
}
}
/**
* 判断是否应该配置连接池数据源的条件类。
*/
static class PooledDataSourceAvailableCondition extends SpringBootCondition {
PooledDataSourceAvailableCondition() {
}
/**
* 根据当前环境判断是否应该配置连接池数据源。
*
* @param context 条件上下文
* @param metadata 注解类型元数据
* @return 条件匹配的结果
*/
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("PooledDataSource", new Object[0]);
return DataSourceBuilder.findType(context.getClassLoader()) != null ? ConditionOutcome.match(message.foundExactly("supported DataSource")) : ConditionOutcome.noMatch(message.didNotFind("supported DataSource").atAll());
}
}
/**
* 管理连接池数据源配置的条件类。
* 根据是否存在特定的DataSource类型或属性来决定是否应用配置。
*/
static class PooledDataSourceCondition extends AnyNestedCondition {
PooledDataSourceCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
/**
* 当存在支持的连接池数据源类型时触发的条件。
*/
@Conditional({PooledDataSourceAvailableCondition.class})
static class PooledDataSourceAvailable {
PooledDataSourceAvailable() {
}
}
/**
* 当spring.datasource.type属性被设置时触发的条件。
*/
@ConditionalOnProperty(
prefix = "spring.datasource",
name = {"type"}
)
static class ExplicitType {
ExplicitType() {
}
}
}
/**
* 配置连接池数据源的配置类。
* 在不存在DataSource和XADataSource bean且满足其他条件的情况下激活。
*/
@Configuration(
proxyBeanMethods = false
)
@Conditional({PooledDataSourceCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration() {
}
/**
* 提供基于DataSourceProperties配置的JdbcConnectionDetails bean。
*
* @param properties DataSource的属性
* @return JdbcConnectionDetails的实例
*/
@Bean
@ConditionalOnMissingBean({JdbcConnectionDetails.class})
PropertiesJdbcConnectionDetails jdbcConnectionDetails(DataSourceProperties properties) {
return new PropertiesJdbcConnectionDetails(properties);
}
}
/**
* 配置嵌入式数据库的配置类。
* 在不存在DataSource和XADataSource bean且满足其他条件的情况下激活。
*/
@Configuration(
proxyBeanMethods = false
)
@Conditional({EmbeddedDatabaseCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({EmbeddedDataSourceConfiguration.class})
protected static class EmbeddedDatabaseConfiguration {
protected EmbeddedDatabaseConfiguration() {
}
}
}
代码中不难看出加载了配置类@EnableConfigurationProperties({DataSourceProperties.class})
, 这是Spring暴露的配置,在看一下这个配置类的源码如下
/**
* 数据源属性配置类,用于封装数据源相关的配置信息。
* 通过@ConfigurationProperties注解,绑定到配置文件中以spring.datasource为前缀的配置项。
*/
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private boolean generateUniqueName = true;
private String name;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
private String jndiName;
private EmbeddedDatabaseConnection embeddedDatabaseConnection;
private Xa xa = new Xa();
private String uniqueName;
/**
* 默认构造函数
*/
public DataSourceProperties() {
}
/**
* 实现BeanClassLoaderAware接口,设置类加载器
* @param classLoader 类加载器
*/
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* 实现InitializingBean接口,初始化数据源构建器
* @throws Exception 初始化异常
*/
public void afterPropertiesSet() throws Exception {
if (this.embeddedDatabaseConnection == null) {
this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader);
}
}
/**
* 初始化DataSourceBuilder,用于构建DataSource实例。
* @return DataSourceBuilder实例
*/
public DataSourceBuilder<?> initializeDataSourceBuilder() {
return DataSourceBuilder.create(this.getClassLoader()).type(this.getType()).driverClassName(this.determineDriverClassName()).url(this.determineUrl()).username(this.determineUsername()).password(this.determinePassword());
}
/**
* 获取是否生成唯一名称的属性。
* @return true表示生成唯一名称,false表示不生成
*/
public boolean isGenerateUniqueName() {
return this.generateUniqueName;
}
/**
* 设置是否生成唯一名称的属性。
* @param generateUniqueName true表示生成唯一名称,false表示不生成
*/
public void setGenerateUniqueName(boolean generateUniqueName) {
this.generateUniqueName = generateUniqueName;
}
/**
* 获取数据源名称。
* @return 数据源名称
*/
public String getName() {
return this.name;
}
/**
* 设置数据源名称。
* @param name 数据源名称
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取数据源类型。
* @return 数据源类型
*/
public Class<? extends DataSource> getType() {
return this.type;
}
/**
* 设置数据源类型。
* @param type 数据源类型
*/
public void setType(Class<? extends DataSource> type) {
this.type = type;
}
/**
* 获取驱动类名称。
* @return 驱动类名称
*/
public String getDriverClassName() {
return this.driverClassName;
}
/**
* 设置驱动类名称。
* @param driverClassName 驱动类名称
*/
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
/**
* 确定驱动类名称。
* 如果已设置驱动类名称,则直接返回;否则根据URL确定驱动类名称。
* @return 驱动类名称
* @throws DataSourceBeanCreationException 如果无法确定驱动类名称,则抛出异常
*/
public String determineDriverClassName() {
if (StringUtils.hasText(this.driverClassName)) {
Assert.state(this.driverClassIsLoadable(), () -> {
return "Cannot load driver class: " + this.driverClassName;
});
return this.driverClassName;
} else {
String driverClassName = null;
if (StringUtils.hasText(this.url)) {
driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
}
if (!StringUtils.hasText(driverClassName)) {
driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
}
if (!StringUtils.hasText(driverClassName)) {
throw new DataSourceBeanCreationException("Failed to determine a suitable driver class", this, this.embeddedDatabaseConnection);
} else {
return driverClassName;
}
}
}
/**
* 检查驱动类是否可加载。
* @return true表示驱动类可加载,false表示不可加载
*/
private boolean driverClassIsLoadable() {
try {
ClassUtils.forName(this.driverClassName, (ClassLoader)null);
return true;
} catch (UnsupportedClassVersionError var2) {
throw var2;
} catch (Throwable var3) {
var3.printStackTrace();
return false;
}
}
/**
* 获取URL。
* @return JDBC URL
*/
public String getUrl() {
return this.url;
}
/**
* 设置URL。
* @param url JDBC URL
*/
public void setUrl(String url) {
this.url = url;
}
/**
* 确定URL。
* 如果已设置URL,则直接返回;否则根据数据库名称确定URL。
* @return JDBC URL
* @throws DataSourceBeanCreationException 如果无法确定URL,则抛出异常
*/
public String determineUrl() {
if (StringUtils.hasText(this.url)) {
return this.url;
} else {
String databaseName = this.determineDatabaseName();
String url = databaseName != null ? this.embeddedDatabaseConnection.getUrl(databaseName) : null;
if (!StringUtils.hasText(url)) {
throw new DataSourceBeanCreationException("Failed to determine suitable jdbc url", this, this.embeddedDatabaseConnection);
} else {
return url;
}
}
}
/**
* 确定数据库名称。
* 如果需要生成唯一名称,则生成并返回唯一名称;否则根据name属性返回名称。
* @return 数据库名称
*/
public String determineDatabaseName() {
if (this.generateUniqueName) {
if (this.uniqueName == null) {
this.uniqueName = UUID.randomUUID().toString();
}
return this.uniqueName;
} else if (StringUtils.hasLength(this.name)) {
return this.name;
} else {
return this.embeddedDatabaseConnection != EmbeddedDatabaseConnection.NONE ? "testdb" : null;
}
}
/**
* 获取用户名。
* @return 用户名
*/
public String getUsername() {
return this.username;
}
/**
* 设置用户名。
* @param username 用户名
*/
public void setUsername(String username) {
this.username = username;
}
/**
* 确定用户名。
* 如果已设置用户名,则直接返回;否则对于嵌入式数据库,返回"sa"。
* @return 用户名
*/
public String determineUsername() {
if (StringUtils.hasText(this.username)) {
return this.username;
} else {
return EmbeddedDatabaseConnection.isEmbedded(this.determineDriverClassName(), this.determineUrl()) ? "sa" : null;
}
}
/**
* 获取密码。
* @return 密码
*/
public String getPassword() {
return this.password;
}
/**
* 设置密码。
* @param password 密码
*/
public void setPassword(String password) {
this.password = password;
}
/**
* 确定密码。
* 如果已设置密码,则直接返回;否则对于嵌入式数据库,返回空字符串。
* @return 密码
*/
public String determinePassword() {
if (StringUtils.hasText(this.password)) {
return this.password;
} else {
return EmbeddedDatabaseConnection.isEmbedded(this.determineDriverClassName(), this.determineUrl()) ? "" : null;
}
}
/**
* 获取JNDI名称。
* @return JNDI名称
*/
public String getJndiName() {
return this.jndiName;
}
/**
* 设置JNDI名称。
* @param jndiName JNDI名称
*/
public void setJndiName(String jndiName) {
this.jndiName = jndiName;
}
/**
* 获取嵌入式数据库连接类型。
* @return 嵌入式数据库连接类型
*/
public EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() {
return this.embeddedDatabaseConnection;
}
/**
* 设置嵌入式数据库连接类型。
* @param embeddedDatabaseConnection 嵌入式数据库连接类型
*/
public void setEmbeddedDatabaseConnection(EmbeddedDatabaseConnection embeddedDatabaseConnection) {
this.embeddedDatabaseConnection = embeddedDatabaseConnection;
}
/**
* 获取类加载器。
* @return 类加载器
*/
public ClassLoader getClassLoader() {
return this.classLoader;
}
/**
* 获取Xa配置。
* @return Xa配置
*/
public Xa getXa() {
return this.xa;
}
/**
* 设置Xa配置。
* @param xa Xa配置
*/
public void setXa(Xa xa) {
this.xa = xa;
}
/**
* Xa配置类,用于存储XA数据源的相关配置。
*/
public static class Xa {
private String dataSourceClassName;
private Map<String, String> properties = new LinkedHashMap();
/**
* 获取数据源类名称。
* @return 数据源类名称
*/
public String getDataSourceClassName() {
return this.dataSourceClassName;
}
/**
* 设置数据源类名称。
* @param dataSourceClassName 数据源类名称
*/
public void setDataSourceClassName(String dataSourceClassName) {
this.dataSourceClassName = dataSourceClassName;
}
/**
* 获取属性配置。
* @return 属性配置
*/
public Map<String, String> getProperties() {
return this.properties;
}
/**
* 设置属性配置。
* @param properties 属性配置
*/
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
}
/**
* 数据源创建异常类,用于处理数据源创建过程中的异常。
*/
static class DataSourceBeanCreationException extends BeanCreationException {
private final DataSourceProperties properties;
private final EmbeddedDatabaseConnection connection;
/**
* 构造函数。
* @param message 异常信息
* @param properties 数据源属性
* @param connection 嵌入式数据库连接类型
*/
DataSourceBeanCreationException(String message, DataSourceProperties properties, EmbeddedDatabaseConnection connection) {
super(message);
this.properties = properties;
this.connection = connection;
}
/**
* 获取数据源属性。
* @return 数据源属性
*/
DataSourceProperties getProperties() {
return this.properties;
}
/**
* 获取嵌入式数据库连接类型。
* @return 嵌入式数据库连接类型
*/
EmbeddedDatabaseConnection getConnection() {
return this.connection;
}
}
}
代码中不难看出,要求对应的配置项以spring.datasource
开头,上一段源码中有一段加入各种数据源配置的地方
@Import({DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class})
再深入看一下源码,以第一个数据源配置类为例
/**
* 数据源配置类,用于根据不同的数据源类型创建对应的数据源实例。
*/
abstract class DataSourceConfiguration {
/**
* 根据连接详情和数据源类型创建数据源实例。
*
* @param connectionDetails 连接详情,包含 JDBC URL、驱动类名、用户名和密码等信息。
* @param type 数据源的类类型。
* @param classLoader 类加载器。
* @param <T> 数据源类型。
* @return 创建的数据源实例。
*/
private static <T> T createDataSource(JdbcConnectionDetails connectionDetails, Class<? extends DataSource> type, ClassLoader classLoader) {
// 使用 DataSourceBuilder 创建并配置数据源实例,然后返回。
return DataSourceBuilder.create(classLoader).type(type).driverClassName(connectionDetails.getDriverClassName()).url(connectionDetails.getJdbcUrl()).username(connectionDetails.getUsername()).password(connectionDetails.getPassword()).build();
}
/**
* 配置通用的数据源 bean,当不存在自定义数据源 bean 且配置了 spring.datasource.type 时使用。
*/
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
name = {"spring.datasource.type"}
)
static class Generic {
/**
* 根据连接属性和连接详情创建并配置数据源 bean。
*
* @param properties 数据源属性。
* @param connectionDetails 连接详情。
* @return 配置后的数据源实例。
*/
@Bean
DataSource dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) {
// 调用 createDataSource 方法创建并返回数据源实例。
return (DataSource)DataSourceConfiguration.createDataSource(connectionDetails, properties.getType(), properties.getClassLoader());
}
}
/**
* 配置 Oracle UCP 数据源 bean,当类路径下存在 PoolDataSourceImpl 类且未定义 DataSource bean 时使用。
*/
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({PoolDataSourceImpl.class, OracleConnection.class})
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
name = {"spring.datasource.type"},
havingValue = "oracle.ucp.jdbc.PoolDataSource",
matchIfMissing = true
)
static class OracleUcp {
/**
* 创建并配置 Oracle UCP 数据源 bean。
*
* @param properties 数据源属性。
* @param connectionDetails 连接详情。
* @return 配置后的 Oracle UCP 数据源实例。
* @throws SQLException 如果配置数据源时发生错误。
*/
@Bean
@ConfigurationProperties(
prefix = "spring.datasource.oracleucp"
)
PoolDataSourceImpl dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) throws SQLException {
PoolDataSourceImpl dataSource = (PoolDataSourceImpl)DataSourceConfiguration.createDataSource(connectionDetails, PoolDataSourceImpl.class, properties.getClassLoader());
// 如果设置了数据源名称,则应用到 Oracle UCP 数据源。
if (StringUtils.hasText(properties.getName())) {
dataSource.setConnectionPoolName(properties.getName());
}
return dataSource;
}
}
/**
* 配置 DBCP2 数据源 bean,当类路径下存在 BasicDataSource 类且未定义 DataSource bean 时使用。
*/
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({BasicDataSource.class})
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
name = {"spring.datasource.type"},
havingValue = "org.apache.commons.dbcp2.BasicDataSource",
matchIfMissing = true
)
static class Dbcp2 {
/**
* 创建并配置 DBCP2 数据源 bean。
*
* @param properties 数据源属性。
* @param connectionDetails 连接详情。
* @return 配置后的 DBCP2 数据源实例。
*/
@Bean
@ConfigurationProperties(
prefix = "spring.datasource.dbcp2"
)
BasicDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) {
Class<? extends DataSource> dataSourceType = BasicDataSource.class;
return (BasicDataSource)DataSourceConfiguration.createDataSource(connectionDetails, dataSourceType, properties.getClassLoader());
}
}
/**
* 配置 HikariCP 数据源 bean,当类路径下存在 HikariDataSource 类且未定义 DataSource bean 时使用。
*/
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({HikariDataSource.class})
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
name = {"spring.datasource.type"},
havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true
)
static class Hikari {
/**
* 创建并配置 HikariCP 数据源 bean。
*
* @param properties 数据源属性。
* @param connectionDetails 连接详情。
* @return 配置后的 HikariCP 数据源实例。
*/
@Bean
@ConfigurationProperties(
prefix = "spring.datasource.hikari"
)
HikariDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) {
HikariDataSource dataSource = (HikariDataSource)DataSourceConfiguration.createDataSource(connectionDetails, HikariDataSource.class, properties.getClassLoader());
// 如果设置了数据源名称,则应用到 HikariCP 数据源。
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
/**
* 配置 Tomcat 数据源 bean,当类路径下存在 org.apache.tomcat.jdbc.pool.DataSource 类且未定义 DataSource bean 时使用。
*/
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({org.apache.tomcat.jdbc.pool.DataSource.class})
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
name = {"spring.datasource.type"},
havingValue = "org.apache.tomcat.jdbc.pool.DataSource",
matchIfMissing = true
)
static class Tomcat {
/**
* 创建并配置 Tomcat 数据源 bean。
*
* @param properties 数据源属性。
* @param connectionDetails 连接详情。
* @return 配置后的 Tomcat 数据源实例。
*/
@Bean
@ConfigurationProperties(
prefix = "spring.datasource.tomcat"
)
org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) {
Class<? extends DataSource> dataSourceType = org.apache.tomcat.jdbc.pool.DataSource.class;
org.apache.tomcat.jdbc.pool.DataSource dataSource = (org.apache.tomcat.jdbc.pool.DataSource)DataSourceConfiguration.createDataSource(connectionDetails, dataSourceType, properties.getClassLoader());
DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(connectionDetails.getJdbcUrl());
String validationQuery = databaseDriver.getValidationQuery();
// 如果存在验证查询语句,则启用borrow时的验证并设置验证查询语句。
if (validationQuery != null) {
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery(validationQuery);
}
return dataSource;
}
}
}
如此Spring Boot就可以通过我们的配置,将数据源创建出来了,其他的也是同理,比如Spring MVC、Redis等等,近一步测试一下,先写一个控制器, 代码如下
package com.sbdev.controller.db;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/role")
public class DataSourceController {
// 注入JdbcTemplate,它由Spring Boot自动创建不需要我们干预
@Autowired
private JdbcTemplate jdbcTemplate = null;
/**
* 获取角色
* @param id 角色编号
* @return 角色信息
*/
@GetMapping("/info/{id}")
public Map<String, Object> getRole(@PathVariable("id") Long id) {
System.out.println("DataSource类型:" + jdbcTemplate.getDataSource().getClass().getName());
Map<String, Object> roleMap = null;
String sql = "select id, role_name, note from t_role where id = ?";
roleMap = jdbcTemplate.queryForMap(sql, id);
return roleMap;
}
}
然后访问地址http://localhost:8001/SBDev/role/info/2
即可
Spring Boot默认使用Hikari数据源,有时候我们想切换不同的数据源,Spring Boot也给予了支持,例如另一个数据源是DBCP2,先在POM添加该依赖
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.12.0</version>
</dependency>
接着修改application.yml文件数据库的配置,如下所示
# 应用程序名称,用于标识和区分不同的Spring Boot应用
spring.application.name:
- SBDev
spring:
# 数据源配置,用于连接和管理数据库
datasource:
# 数据库驱动类名,指定连接MySQL数据库所需的驱动类
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接URL,指定连接的数据库地址、端口、数据库名称以及连接参数
url: jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=false
# 数据库用户名,用于身份验证和授权
username: root
# 数据库密码,与用户名配合用于身份验证和授权
password: Ms123!@#
# 数据源类型,使用Apache Commons DBCP2提供的基本数据源
type: org.apache.commons.dbcp2.BasicDataSource
# DBCP2数据源特定配置,用于管理连接池的大小和行为
dbcp2:
# 最大空闲连接数,超过该数目的空闲连接将被关闭
max-idle: 20
# 等待连接池分配连接的最大时间,超过该时间仍未获取到连接将抛出异常
max-wait-millis: 5000
# 连接池允许的最大连接数,超过该数目将无法获取新的连接
max-total: 50
# 最小空闲连接数,连接池会维护至少这么多的空闲连接
# 服务器配置,用于设置服务器端口和上下文路径
server:
servlet:
# 应用程序上下文路径,用于区分不同的Spring Boot应用或服务
context-path: /SBDev
# 服务器端口,指定应用监听的端口号
port: 8001
Spring Boot整合MyBatis
在Spring Boot中整合MyBatis和传统方式差不多,只是部分MyBatis组件可以在SpringBoot配置文件中进行配置,且常见的类都会自动初始化,不需要编写,
添加依赖
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
建表
use ssm;
Create Table t_user(
id int(12) not null auto_increment comment '主键',
username varchar(255) not null comment '用户名',
password varchar(255) not null comment '密码',
sex int(3) not null default 0 comment '性别',
note varchar(255) comment '备注',
primary key(id),
CHECK (sex in (0,1))
);
insert into t_user(username,password,sex,note) values('admin','123456',0,'管理员');
insert into t_user(username,password,sex,note) values('user','123456',1,'普通用户');
insert into t_user(username,password,sex,note) values('user2','123456',1,'普通用户');
insert into t_user(username,password,sex,note) values('user3','123456',1,'普通用户');
insert into t_user(username,password,sex,note) values('user4','123456',1,'普通用户');
写POJO
package com.sbdev.dic;
public enum SexEnum {
MALE(0, "男"),
FEMALE(1, "女");
private Integer id;
private String value;
SexEnum(Integer id, String value) {
this.id = id;
this.value = value;
}
/**
* 获取根据编号获取性别枚举
* @param id 编号
* @return 枚举
*/
public static SexEnum getSexEnum(Integer id) {
for (SexEnum sex : SexEnum.values()) {
if (sex.getId().equals(id)) {
return sex;
}
}
return null;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
package com.sbdev.pojo;
import com.sbdev.dic.SexEnum;
import org.apache.ibatis.type.Alias;
import java.io.Serializable;
@Alias("user")
public class User implements Serializable {
private static final long serialVersionUID = 2386785787854557L;
private Long id;
private String userName;
private SexEnum sex;
private String note;
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 SexEnum getSex() {
return sex;
}
public void setSex(SexEnum sex) {
this.sex = sex;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
写映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.ssm.chapter25.dao.UserDao">
<select id="getUser" parameterType="long" resultType="user">
select id, user_name as userName, sex, note from t_user where id = #{id}
</select>
<insert id="insertUser" parameterType="user" useGeneratedKeys="true" keyProperty="id">
insert into t_user(user_name, sex, note) values (#{userName}, #{sex}, #{note})
</insert>
</mapper>
定义DAO接口层
package com.sbdev.dao;
import com.sbdev.pojo.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserDao {
public User getUser(Long id);
public Integer insertUser(User user);
}
编写TypeHandler
因为用户的性别是个枚举,为了更方便的使用它,需要编写一个TypeHandler
package com.sbdev.type.handler;
import com.sbdev.dic.SexEnum;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 性别枚举类型处理器,用于MyBatis中将数据库中的整型数据与SexEnum枚举类型相互转换。
* 使用@MappedJdbcTypes和@MappedTypes注解分别指定了对应的JDBC类型和Java类型。
*/
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes({SexEnum.class})
public class SexTypeHandler extends BaseTypeHandler<SexEnum> {
/**
* 设置非空参数。
* 将枚举类型SexEnum的id值设置到PreparedStatement中,对应数据库中的整型字段。
* @param ps PreparedStatement对象
* @param idx 参数索引
* @param sex 性别枚举值
* @param jdbcType JDBC类型
* @throws SQLException 如果设置参数时发生错误
*/
@Override
public void setNonNullParameter(
PreparedStatement ps, int idx, SexEnum sex, JdbcType jdbcType) throws SQLException {
ps.setInt(idx, sex.getId());
}
/**
* 从ResultSet中获取非空结果。
* 根据列名获取整型值,并将其转换为SexEnum枚举类型。
* @param rs ResultSet对象
* @param columnName 列名
* @return 性别枚举值
* @throws SQLException 如果获取结果时发生错误
*/
@Override
public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
Integer id = rs.getInt(columnName);
return SexEnum.getSexEnum(id);
}
/**
* 从ResultSet中获取非空结果。
* 根据索引获取整型值,并将其转换为SexEnum枚举类型。
* @param rs ResultSet对象
* @param idx 列索引
* @return 性别枚举值
* @throws SQLException 如果获取结果时发生错误
*/
@Override
public SexEnum getNullableResult(ResultSet rs, int idx) throws SQLException {
Integer id = rs.getInt(idx);
return SexEnum.getSexEnum(id);
}
/**
* 从CallableStatement中获取非空结果。
* 根据索引获取整型值,并将其转换为SexEnum枚举类型。
* @param cs CallableStatement对象
* @param idx 列索引
* @return 性别枚举值
* @throws SQLException 如果获取结果时发生错误
*/
@Override
public SexEnum getNullableResult(CallableStatement cs, int idx) throws SQLException {
Integer id = cs.getInt(idx);
return SexEnum.getSexEnum(id);
}
}
配置MyBatis
在传统的MyBatis使用中,我们还需要XML配置文件或者使用Java代码继承配置类进行配置,在SpringBoot中,通过application.yml进行配置即可
# 应用程序名称,用于标识和区分不同的Spring Boot应用
spring.application.name: SBDev
spring:
# 数据源配置,用于连接和管理数据库
datasource:
# 数据库驱动类名,指定连接MySQL数据库所需的驱动类
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接URL,指定连接的数据库地址、端口、数据库名称以及连接参数
url: jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=false
# 数据库用户名,用于身份验证和授权
username: root
# 数据库密码,与用户名配合用于身份验证
password: Ms123!@#
# 数据源类型,使用Apache Commons DBCP2提供的基本数据源
type: org.apache.commons.dbcp2.BasicDataSource
# DBCP2特定的配置,用于管理连接池的大小和行为
dbcp2:
# 最大空闲连接数,超过该数目的空闲连接将被关闭
max-idle: 20
# 等待连接的最大时间,超过该时间仍未获取到连接将抛出异常
max-wait-millis: 5000
# 连接池允许的最大连接数,超过该数目将拒绝新的连接请求
max-total: 50
# 服务器配置,用于设置应用的访问路径和端口
server:
# 应用程序的上下文路径,用于区分不同的服务或模块
servlet:
context-path: /SBDev
# 服务器端口,指定应用监听的网络端口号
port: 8001
# mybatis配置项
mybatis:
# 映射文件路径
mapper-locations: classpath:mapper/*.xml
# TypeHandler扫描包
type-handlers-package: com.sbdev.type.handler
# 扫描别名
type-aliases-package: com.sbdev.pojo
logging:
level:
# 日志级别
root: DEBUG
开发业务层
package com.sbdev.service;
import com.sbdev.pojo.User;
public interface UserService {
public User getUser(Long id);
public Integer insertUser(User user);
}
package com.sbdev.service.impl;
import com.sbdev.dao.UserDao;
import com.sbdev.pojo.User;
import com.sbdev.service.UserService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
@Override
@Cacheable(value="redisCache", key="'redis_user_'+#id")
public User getUser(Long id) {
return userDao.getUser(id);
}
@Override
// 事务管理器由Spring Boot自动装配,无需自己配置
@Transactional(isolation = Isolation.READ_COMMITTED)
public Integer insertUser(User user) {
return userDao.insertUser(user);
}
}
开发控制器
package com.sbdev.controller;
import com.sbdev.dic.SexEnum;
import com.sbdev.pojo.User;
import com.sbdev.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // REST风格网站
@RequestMapping("/user")
public class UserController {
// 用户服务接口
@Autowired
private UserService userService = null;
/**
* 获取用户信息
* @param id 用户编号
* @return 用户信息
*/
@GetMapping("/info/{id}")
public User getUser(@PathVariable("id") Long id) {
User user = userService.getUser(id);
return user;
}
@GetMapping("/addition/{id}")
public User insertUser(@PathVariable("id") Long id) {
User user = new User();
user.setSex(SexEnum.getSexEnum(id.intValue()%2));
user.setUserName("user_name_" + id);
user.setNote("note_" + id);
userService.insertUser(user);
return user;
}
@GetMapping("/print/{user}")
public User print(User user) {
return user;
}
}
配置SpringBoot启动
/**
* 应用程序的入口点。
* 使用@SpringBootApplication注解标记这个类作为Spring Boot应用程序的起点。
* scanBasePackages属性指定了Spring应用程序上下文应该扫描的包,即在这个包及其子包下寻找组件(如控制器、服务、配置类等)。
* 这里指定的包是"com.sbdev",意味着所有在这个包下面的组件都会被Spring Boot自动识别和管理。
*/
@SpringBootApplication(
scanBasePackages = "com.sbdev"
)
/**
* 配置MyBatis的Mapper扫描器。
* 通过此注解,Spring Boot将自动扫描指定包下的所有Mapper接口,并将其注册到MyBatis的SqlSessionFactory或SqlSessionTemplate中。
* @param basePackages 指定需要扫描的Mapper接口所在的包。Spring Boot会递归扫描指定包及其子包下的所有接口。
* @param annotationClass 指定需要扫描的接口上应存在的注解。这里指定为@Mapper,意味着只有标注了@Mapper注解的接口才会被扫描和处理。
* @param sqlSessionFactoryRef 指定SqlSessionFactory的Bean名称。扫描到的Mapper接口将使用这个SqlSessionFactory来创建SqlSession。
* @param sqlSessionTemplateRef 指定SqlSessionTemplate的Bean名称。扫描到的Mapper接口将使用这个SqlSessionTemplate来执行SQL操作。
*/
@MapperScan(
basePackages = "com.sbdev",
annotationClass = Mapper.class,
sqlSessionFactoryRef = "sqlSessionFactory",
sqlSessionTemplateRef = "sqlSessionTemplate"
)
@EnableCaching
public class SbDevApplication {
public static void main(String[] args) {
SpringApplication.run(SbDevApplication.class, args);
}
SpringBoot启动类配置,注释写的比较清楚
注意这个配置
sqlSessionFactoryRef = "sqlSessionFactory",sqlSessionTemplateRef = "sqlSessionTemplate"
,在这里可以删除它们,因为在项目中不存在多个SqlSessionFactory
和SqlSessionTemplate
,但是从头到尾都没有sqlSessionFactory
和sqlSessionTemplate
相关的配置或定义,那在这里却可以直接配置的原因便是当我们配置了数据源javax.sql.DataSource
后,Spring Boot就会自动创建数据源的Bean,并且将它装配到Spring IoC容器中,而这个过程无需开发,也是Spring Boot的特色
插件
package com.sbdev.plugin;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
/**
* 消费时间插件,用于拦截MyBatis的Executor执行查询操作,计算查询操作的消耗时间。
* 该插件通过实现Interceptor接口,利用MyBatis的插件机制,在查询执行前后插入计时逻辑。
*/
@Intercepts(
@Signature(
type = Executor.class,
method = "query",
args = {
MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
}))
public class ConsumptionTimePlugin implements Interceptor {
/**
* 对Executor的query方法进行拦截。
* 在执行查询操作前后记录时间,以计算查询操作的耗时,并输出到控制台。
*
* @param invocation 查询操作的调用信息,包含执行查询所需的所有参数。
* @return 查询操作的结果对象。
* @throws Throwable 查询操作中可能抛出的任何异常。
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 记录查询开始时间
long start = System.currentTimeMillis();
// 执行查询操作
Object returnObj = invocation.proceed();
// 记录查询结束时间
long end = System.currentTimeMillis();
// 计算并输出查询耗时
System.out.println("耗时【" + (end - start)+"】毫秒");
return returnObj;
}
/**
* 为指定的目标对象创建一个插件代理。
* 该方法用于实现插件的包装逻辑,将当前插件应用于目标对象,以拦截目标对象的方法调用。
*
* @param target 被插件化的对象,即MyBatis的Executor实例。
* @return 包装了当前插件的目标对象的代理实例。
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置插件的属性。
* 该方法用于接收从配置文件中读取的插件属性,当前插件未使用任何属性,因此该方法为空实现。
*
* @param properties 插件的属性配置。
*/
@Override
public void setProperties(Properties properties) {
// 该插件不使用任何属性,因此该方法为空实现
}
}
这是一个MyBatis插件,可以用来监控执行查询SQL消耗的时间,因为插件相对独立,可以将其配置到MyBatis自身的配置文件中,在resources
目录下创建mybatis-config.xml
,来配置插件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 配置文件的根元素,用于定义MyBatis的全局属性、类型别名、映射器等 -->
<configuration>
<!-- 插件配置,用于注册自定义的插件 -->
<plugins>
<!-- 注册一个插件,该插件实现了com.sbdev.plugin.ConsumptionTimePlugin接口 -->
<!-- 该插件用于记录方法的执行时间,实现性能监控 -->
<plugin interceptor="com.sbdev.plugin.ConsumptionTimePlugin"/>
</plugins>
</configuration>
将MyBatis配置文件加到SpringBoot的配置里,修改application.yml如下
# 应用程序名称,用于标识和区分不同的Spring Boot应用
spring.application.name: SBDev
spring:
# 数据源配置,用于连接和管理数据库
datasource:
# 数据库驱动类名,指定连接MySQL数据库所需的驱动类
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接URL,指定连接的数据库地址、端口、数据库名称以及连接参数
url: jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=false
# 数据库用户名,用于身份验证和授权
username: root
# 数据库密码,与用户名配合用于身份验证
password: Ms123!@#
# 数据源类型,使用Apache Commons DBCP2提供的基本数据源
type: org.apache.commons.dbcp2.BasicDataSource
# DBCP2特定的配置,用于管理连接池的大小和行为
dbcp2:
# 最大空闲连接数,超过该数目的空闲连接将被关闭
max-idle: 20
# 等待连接的最大时间,超过该时间仍未获取到连接将抛出异常
max-wait-millis: 5000
# 连接池允许的最大连接数,超过该数目将拒绝新的连接请求
max-total: 50
# 服务器配置,用于设置应用的访问路径和端口
server:
# 应用程序的上下文路径,用于区分不同的服务或模块
servlet:
context-path: /SBDev
# 服务器端口,指定应用监听的网络端口号
port: 8001
# mybatis配置项
mybatis:
# 映射文件路径
mapper-locations: classpath:mapper/*.xml
# TypeHandler扫描包
type-handlers-package: com.sbdev.type.handler
# 扫描别名
type-aliases-package: com.sbdev.pojo
# 配置MyBatis配置文件的位置
# 使用classpath指定配置文件位于类路径下,便于资源的统一管理和访问
config-location: classpath:mybatis-config.xml
logging:
level:
# 日志级别
root: DEBUG
实际上还有很多配置项,可以自行研究,这样配置完,Spring Boot就会启用这个插件了
数据库事务
在Spring Boot初始化数据源的时候,会同时初始化对应的数据库事务管理器,因此不需要配置任何数据库事务的内容可以直接使用,例如如下代码
package com.sbdev.service.impl;
import com.sbdev.dao.UserDao;
import com.sbdev.pojo.User;
import com.sbdev.service.UserService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
@Override
@Cacheable(value="redisCache", key="'redis_user_'+#id")
public User getUser(Long id) {
return userDao.getUser(id);
}
@Override
// 事务管理器由Spring Boot自动装配,无需自己配置
@Transactional(isolation = Isolation.READ_COMMITTED)
public Integer insertUser(User user) {
return userDao.insertUser(user);
}
}
为了方便使用Spring Boot还提供了默认隔离级别的配置,如下所示
spring:
# 数据源配置,用于连接和管理数据库
datasource:
# 配置Hikari连接池的事务隔离级别为读未提交
# 这一设置影响使用Hikari连接池的数据库连接的默认事务隔离行为
hikari:
transaction-isolation: 2
# -1:默认隔离级别
# 1:读未提交
# 2:读已提交
# 4:可重复读
# 8:串行化
# 配置Tomcat连接池的默认事务隔离级别为读未提交
# 此设置适用于所有通过Tomcat连接池获取的数据库连接
tomcat:
default-transaction-isolation: 2
# 数据库驱动类名,指定连接MySQL数据库所需的驱动类
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接URL,指定连接的数据库地址、端口、数据库名称以及连接参数
url: jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=false
# 数据库用户名,用于身份验证和授权
username: root
# 数据库密码,与用户名配合用于身份验证
password: Ms123!@#
# 数据源类型,使用Apache Commons DBCP2提供的基本数据源
type: org.apache.commons.dbcp2.BasicDataSource
# DBCP2特定的配置,用于管理连接池的大小和行为
dbcp2:
# 最大空闲连接数,超过该数目的空闲连接将被关闭
max-idle: 20
# 等待连接的最大时间,超过该时间仍未获取到连接将抛出异常
max-wait-millis: 5000
# 连接池允许的最大连接数,超过该数目将拒绝新的连接请求
max-total: 50
# 设置默认事务隔离级别为二级隔离
default-transaction-isolation: 2
Spring Boot和Spring MVC
在大部分情况下,在Spring Boot中使用Spring MVC的方法和传统Spring MVC并无太大的不同,但又一些特殊用法
使用WebMvcConfigurer接口
通常我们可以通过实现WebMvcConfiguration接口来自定义Spring MVC的组件,例如需要开发一个用户拦截器,如下代码所示
/**
* 用户拦截器类,用于在请求处理的不同阶段执行自定义逻辑。
* 实现了Spring MVC的HandlerInterceptor接口。
*/
package com.sbdev.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class UserInterceptor implements HandlerInterceptor {
/**
* 在请求处理之前执行的逻辑。
* 该方法用于进行权限检查或初始化一些资源。
* @param request 当前请求的HttpServletRequest对象
* @param response 当前请求的HttpServletResponse对象
* @param handler 将要处理请求的目标对象
* @return true表示继续处理请求,false表示中断请求处理
* @throws Exception 如果在预处理阶段发生异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
/**
* 在请求处理完成后,但在视图渲染之前执行的逻辑。
* 该方法可用于进行一些数据处理或清理工作。
* @param request 当前请求的HttpServletRequest对象
* @param response 当前请求的HttpServletResponse对象
* @param handler 处理请求的目标对象
* @param modelAndView 视图模型对象,可能为null,表示不返回视图
* @throws Exception 如果在后处理阶段发生异常
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
/**
* 在整个请求处理完成后,包括视图渲染,执行的逻辑。
* 该方法可用于进行一些资源的释放或日志记录等清理工作。
* @param request 当前请求的HttpServletRequest对象
* @param response 当前请求的HttpServletResponse对象
* @param handler 处理请求的目标对象
* @param ex 在请求处理过程中抛出的异常,可能为null
* @throws Exception 如果在完成处理阶段发生异常
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
package com.sbdev.config;
import com.sbdev.interceptor.UserInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类,用于自定义Spring MVC的配置。
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加拦截器配置。
* @param registry 拦截器注册表,用于注册和管理拦截器。
* 这里通过添加UserInterceptor拦截器并指定拦截路径为/user/**,
* 实现对用户相关请求的拦截处理。
*/
public void addInterceptors(InterceptorRegistry registry) {
// 添加UserInterceptor拦截器
registry.addInterceptor(new UserInterceptor())
// 指定拦截器拦截的URL路径
.addPathPatterns("/user/**");
}
}
这样就可以配置各类Spring MVC组件了
使用SpringBoot的Spring MVC配置
# 应用程序名称,用于标识和区分不同的Spring Boot应用
spring.application.name: SBDev
spring:
# 数据源配置,用于连接和管理数据库
datasource:
# 配置Hikari连接池的事务隔离级别为读未提交
# 这一设置影响使用Hikari连接池的数据库连接的默认事务隔离行为
hikari:
transaction-isolation: 2
# -1:默认隔离级别
# 1:读未提交
# 2:读已提交
# 4:可重复读
# 8:串行化
# 配置Tomcat连接池的默认事务隔离级别为读未提交
# 此设置适用于所有通过Tomcat连接池获取的数据库连接
tomcat:
default-transaction-isolation: 2
# 数据库驱动类名,指定连接MySQL数据库所需的驱动类
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接URL,指定连接的数据库地址、端口、数据库名称以及连接参数
url: jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=false
# 数据库用户名,用于身份验证和授权
username: root
# 数据库密码,与用户名配合用于身份验证
password: Ms123!@#
# 数据源类型,使用Apache Commons DBCP2提供的基本数据源
type: org.apache.commons.dbcp2.BasicDataSource
# DBCP2特定的配置,用于管理连接池的大小和行为
dbcp2:
# 最大空闲连接数,超过该数目的空闲连接将被关闭
max-idle: 20
# 等待连接的最大时间,超过该时间仍未获取到连接将抛出异常
max-wait-millis: 5000
# 连接池允许的最大连接数,超过该数目将拒绝新的连接请求
max-total: 50
# 设置默认事务隔离级别为二级隔离
default-transaction-isolation: 2
# 配置 MVC 框架的相关设置
mvc:
# 配置用户服务的 Servlet 信息
servlet:
# 定义 Servlet 的访问路径
path: /user
# 设置 Servlet 在应用启动时加载的顺序
load-on-startup: 1
# 配置视图解析的相关设置
view:
# 定义视图文件的后缀名
suffix: .jsp
# 定义视图文件的目录前缀
prefix: /WEB-INF/jsp/
# 配置 Web 应用的国际化设置
web:
# 设置固定的地域解析器
locale-resolver: fixed
# 指定应用的默认地域为中文(中国)
locale: zh_CN
# 配置 Jackson 的相关设置,用于序列化和反序列化日期时间
jackson:
# 定义日期时间的格式化样式
date-format: yyyy-MM-dd HH:mm:ss
# 服务器配置,用于设置应用的访问路径和端口
server:
# 应用程序的上下文路径,用于区分不同的服务或模块
servlet:
context-path: /SBDev
# 服务器端口,指定应用监听的网络端口号
port: 8001
# mybatis配置项
mybatis:
# 映射文件路径
mapper-locations: classpath:mapper/*.xml
# TypeHandler扫描包
type-handlers-package: com.sbdev.type.handler
# 扫描别名
type-aliases-package: com.sbdev.pojo
# 配置MyBatis配置文件的位置
# 使用classpath指定配置文件位于类路径下,便于资源的统一管理和访问
config-location: classpath:mybatis-config.xml
logging:
level:
# 日志级别
root: DEBUG
在IDEA里双击Shift键,搜MybatisProperties或者搜WebMvcProperties能看到如下内容,都是Spring Boot配置文件中的配置项
使用转换器
定义转换器,例如用户POJO,可以约定浏览器提交的格式为{id}-{userName}-{sex}-{note}
可以使用Converter机制处理
package com.sbdev.comverter;
import com.sbdev.dic.SexEnum;
import com.sbdev.pojo.User;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
public class UserConverter implements Converter<String, User> {
/**
* 约定提交格式“{id}-{userName}-{sex}-{note}”
* @param source 源字符串
* @return 用户对象
*/
@Override
public User convert(String source) {
if (source == null) {
return null;
}
String []arr = source.split("-");
if (arr.length != 4) { // 不符合格式
return null;
}
User user = new User();
user.setId(Long.parseLong(arr[0]));
user.setUserName(arr[1]);
user.setSex(SexEnum.getSexEnum(Integer.parseInt(arr[2])));
user.setNote(arr[3]);
return user;
}
}
这里标注了@Component
,这意味着它将被扫描装配到Spring IoC容器中,然后可以通过控制器来进行测试
@GetMapping("/print/{user}")
public User print(User user) {
return user;
}
然后在浏览器中输入输入地址user/print/1-username-0-note
测试转换器是否生效即可,这里我们并没有给Spring MVC注册UserConverter,但是Spring MVC还是使用了UserConverter去转换参数,这事因为Spring Boot已经为我们做了注册,但前提是需要将它们装配到Spring IoC容器中去
在Spring MVC的配置类WebMvcAutoConfiguration中调用了ApplicationConversionService的静态addBeans方法,源码如下
/**
* 将豆类工厂中的转换器、格式化器、解析器注册到格式化器注册表中。
* 此方法旨在支持自动注册由Spring Bean工厂提供的各种转换器和格式化器,以增强系统的数据处理能力。
* @param registry 格式化器注册表,用于接收和管理转换器、格式化器、解析器。
* @param beanFactory Spring的Bean工厂,用于获取所有实现了转换器、格式化器、解析器接口的Bean实例。
*/
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
// 使用LinkedHashSet来保存转换器、格式化器、解析器实例,以保持注册顺序。
Set<Object> beans = new LinkedHashSet<>();
// 分别获取并合并GenericConverter、Converter、Printer、Parser类型的Bean实例。
beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values());
beans.addAll(beanFactory.getBeansOfType(Converter.class).values());
beans.addAll(beanFactory.getBeansOfType(Printer.class).values());
beans.addAll(beanFactory.getBeansOfType(Parser.class).values());
// 遍历所有获取到的Bean实例。
Iterator var3 = beans.iterator();
while(var3.hasNext()) {
Object bean = var3.next();
// 根据实例类型,分别注册到对应的格式化器注册表中。
// 这里使用了Java的实例类型检查来决定注册哪种类型的实例。
if (bean instanceof GenericConverter genericConverter) {
registry.addConverter(genericConverter);
} else if (bean instanceof Converter<?, ?> converter) {
registry.addConverter(converter);
} else if (bean instanceof Formatter<?> formatter) {
registry.addFormatter(formatter);
} else if (bean instanceof Printer<?> printer) {
registry.addPrinter(printer);
} else if (bean instanceof Parser<?> parser) {
registry.addParser(parser);
}
}
}
SpringBoot会将所有在Spring IoC容器中的Converter、GenericConverter、Formatter都注册到Spring MVC的转换机制中,所以即使我们没有显示的注册,Spring MVC也能发现我们自定义的转换器
Spring Boot 使用Redis
使用Redis需要添加相关依赖
<!-- 添加Tomcat-JDBC依赖 -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- 引入Spring Boot的Redis支持,但排除Lettuce客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!--不依赖Redis的异步客户端lettuce-->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入Jedis作为Redis的同步客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
Spring Boot默认依赖Lettuce客户端,但这里排除了它,因为Lettuce是一个可伸缩线程安全的Redis客户端,多个线程可以共享一个Redis连接,所以会牺牲一部分性能,但一般来说缓存对线程安全的需求并不高,更注重性能,而Jedis是一种多线程非安全的客户端,性能更好
配置Redis
data:
redis:
host: 127.0.0.1
port: 6379
jedis:
pool:
# 最大连接数
max-active: 20
# 最大等待时间
max-wait: 2000ms
# 最大空闲连接数
max-idle: 10
# 最小空闲等待连接数
min-idle: 5
# # 哨兵模式配置
# sentinel:
# # 主服务器
# master: 192.168.80.130:26379
# # 节点
# nodes: 192.168.80.131:26379, 192.168.80.132:26379
# # 集群配置
# cluster:
# # 集群节点
# nodes: 192.168.80.133:7001, 192.168.80.133:7002, 192.168.80.133:7003, 192.168.80.133:7004, 192.168.80.133:7005, 192.168.80.133:7006
# # 单一连接最大转向次数
# max-redirects: 10
在Spring Boot中自动配置类为RedisAutoConfiguration属性类为RedisProperties,更多的配置可以在这里边找到配置好后便可以使用Redis了
package com.sbdev.controller;
import com.sbdev.dic.SexEnum;
import com.sbdev.pojo.User;
import com.sbdev.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/redis")
public class RedisController {
// StringRedisTemplate是RedisTemplate的子类,但是该属性名“redisTemplate”和Bean名称一致,
// 因此可以使用@Autowired注入,如果属性名称不为“redisTemplate”,则注入失败,抛出异常
@Autowired
// private RedisTemplate<Object, Object> redisTemplate = null;
private RedisTemplate<String, Object> redisTemplate = null;
// 注入StringRedisTemplate
@Autowired
private StringRedisTemplate stringRedisTemplate = null;
// 测试字符串
@GetMapping("/string/{key}/{value}")
public Map<String, String> string(@PathVariable("key") String key,@PathVariable("value") String value) {
stringRedisTemplate.opsForValue().set(key, value);
Map<String, String> result = new HashMap<>();
result.put(key, value);
return result;
}
// 测试对象
@GetMapping("/object/{id}")
public User object(@PathVariable("id") Long id){
User user = new User();
user.setId(id);
user.setUserName("user_name" + id);
user.setNote("note_" + id);
user.setSex(SexEnum.getSexEnum(id.intValue() % 2));
redisTemplate.opsForValue().set("user_" + id, user);
return user;
}
@Autowired
private UserService userService = null;
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Long id) {
return userService.getUser(id);
}
}
然后根据控制器中的地址请求即可完成测试即可
Redis缓存管理器
首先在Spring Boot的配置文件中添加缓存配置
data:
redis:
host: 127.0.0.1
port: 6379
jedis:
pool:
# 最大连接数
max-active: 20
# 最大等待时间
max-wait: 2000ms
# 最大空闲连接数
max-idle: 10
# 最小空闲等待连接数
min-idle: 5
# # 哨兵模式配置
# sentinel:
# # 主服务器
# master: 192.168.80.130:26379
# # 节点
# nodes: 192.168.80.131:26379, 192.168.80.132:26379
# # 集群配置
# cluster:
# # 集群节点
# nodes: 192.168.80.133:7001, 192.168.80.133:7002, 192.168.80.133:7003, 192.168.80.133:7004, 192.168.80.133:7005, 192.168.80.133:7006
# # 单一连接最大转向次数
# max-redirects: 10
# 缓存管理器配置
cache:
# 缓存管理器名称
cache-names: redisCache
# 缓存管理器类型,可不配,Spring Boot会自动发现
# type: redis
redis:
# 十分钟超时时间,配置为0 则永不超时
time-to-live: 600000ms
# 缓存空值
cache-null-values: true
# 缓存key前缀
key-prefix: 'sbdev::'
# key是否使用前缀
use-key-prefix: true
然后通过缓存注解的方式来使用Redis
package com.sbdev.service.impl;
import com.sbdev.dao.UserDao;
import com.sbdev.pojo.User;
import com.sbdev.service.UserService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
/**
* 从Redis缓存中获取用户信息。
* 使用了@Cacheable注解,使得该方法的结果可以被缓存。如果缓存中已存在对应键的用户信息,
* 则直接从缓存中返回,避免了再次查询数据库。缓存的键由'redis_user_'和用户ID拼接而成,
* 保证了键的唯一性。
* @param id 用户的ID,作为查询和缓存键的一部分。
* @return 用户对象,包含用户信息。
*/
@Override
@Cacheable(value = "redisCache", key = "'redis_user_'+#id")
public User getUser(Long id) {
// 通过UserDao查询Redis中的用户信息。
return userDao.getUser(id);
}
@Override
// 事务管理器由Spring Boot自动装配,无需自己配置
@Transactional(isolation = Isolation.READ_COMMITTED)
public Integer insertUser(User user) {
return userDao.insertUser(user);
}
}
然后通过控制器对应的方法进行测试
@Autowired
private UserService userService = null;
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Long id) {
return userService.getUser(id);
}