Dawn's Blogs

分享技术 记录成长

0%

Bean 的加载

Bean 的加载方式

Bean 的加载方式包括:

  • XML 配置文件方式导入。

    • 使用 bean 标签导入。
    • 使用 @ImportResource 导入配置文件。
  • @Component 注解和 @Bean 注解。

    • @Component 注解。
    • @Configuration + @Bean 注解。
    • 类实现 FactoryBean 接口,并使用 @Bean 修饰方法返回实现 FactoryBean 的实现类。
  • @Import 导入。

    • @Import 直接导入 Bean 类、配置类。
    • @Import 导入 ImportSelector 接口。
    • @Import 导入 ImportBeanDefinitionRegistrar 接口。
    • @Import 导入 BeanDefinitionRegistryPostProcessor 接口。
  • 在 IOC 容器初始化完成后再注册 Bean,即 AnnotationConfigApplicationContext 调用 register 方法注册 bean。

下面说明 ImportSelector 接口、ImportBeanDefinitionRegitrar 接口、BeanDefinitionRegistryPostProcessor 接口。

ImportSelector 接口可以根据注解元数据 AnnotationMetadata 进行一系列判断,返回的是需要被导入的全路径名。

1
2
3
4
5
6
7
8
9
10
11
12
public MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata metadata) {
// 。。。
// 可以根据 AnnotationMetadata 做出一系列判断,如:
boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Import");
if flag {
return []String{/* ... */};
}

return []String{/* ... */};
}
}

ImportBeanDefinitionRegistrar 接口,不仅可以通过元数据 AnnotationMetadata 进行判断,而且可以通过 BeanDefinition 注册器来手动注册 Bean,控制 Bean 的注册过程。

1
2
3
4
5
6
7
8
9
10
public MyImportBeanDefinitionRegistrar implements InportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 。。。
// 使用 AnnotationMetadata 做出判断

// 注册 bean
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(xxx.class).getBeanDefinition();
registry.registerBeanDefinition("beanName", beanDefinition);
}
}

BeanDefinitionRegistryPostProcessor 接口,通过 BeanDefinition 注册器注册 bean,实现对 bean 的最终裁定(可以覆盖前面 bean 的注册)。

Bean 的加载控制

Bean 的加载控制就是根据特定情况进行选择性的加载,控制 Bean 的加载过程。可以分为两种方式,编程式和注解式。

  • 编程式:
    • @Import 导入 ImportSelector 接口。
    • @Import 导入 ImportBeanDefinitionRegistrar 接口。
    • @Import 导入 BeanDefinitionRegistryPostProcessor 接口。
    • AnnotationConfigApplicationContext 调用 register 方法注册 bean。
  • 注解式:
    • 使用 @Conditional 注解及其衍生注解 @ConditionalOnXxx 设置 Bean 的加载条件。

自动配置

SpringBoot 在启动时会扫描外部引用 jar 包中的 META-INF/spring.factories 文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。

从 SpringBoot 3.0 开始,自动配置包的路径从 META-INF/spring.factories 改为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。

引入 starter 之后,可以通过少量注解和一些简单的配置就能使用第三方组件提供的功能了,通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。

原理

SpringBoot 的核心注解 @SpringBootApplication ,主要包含三个注解 @SpringBootConfiguration、@ComponentScan、@EnableAutoConfiguration。

  • @SpringBootConfiguration:可以看作是 @Configuration,允许在上下文中注册额外的 bean 或导入其他配置类。
  • @ComponentScan:扫描被 @Component 注解的 Bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。同时,容器中将排除 TypeExcludeFilter AutoConfigurationExcludeFilter
  • @EnableAutoConfiguration:开启自动配置,是自动配置的关键注解。
1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@ComponentScan
@EnableAutoConfiguration
public @interface SpringBootApplication {

}

@EnableAutoConfiguration

@EnableAutoConfiguration 其中包含了两个主要注解:

  • @AutoConfigurationPackage:将 @SpringBootApplication 路径下的所有 Bean 注册到容器中。
  • @Import({AutoConfigurationImportSelector.class}):加载自动装配类。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage //作用:将main包下的所有组件注册到容器中
@Import({AutoConfigurationImportSelector.class}) //加载自动装配类 xxxAutoconfiguration
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

AutoConfigurationImportSelector 加载自动装配类

AutoConfigurationImportSelector 的声明如下:

1
2
3
4
5
6
7
8
9
10
11
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

}

public interface DeferredImportSelector extends ImportSelector {

}

public interface ImportSelector {
String[] selectImports(AnnotationMetadata var1);
}

AutoConfigurationImportSelector 实现了 ImportSelector 类,作用是获取所有符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中。通过 getAutoConfigurationEntry 方法来实现获取所有需要自动配置的全限定类名。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final String[] NO_IMPORTS = new String[0];

public String[] selectImports(AnnotationMetadata annotationMetadata) {
// <1>.判断自动装配开关是否打开
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
//<2>.获取所有需要装配的bean
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}

getAutoConfigurationEntry 方法

getAutoConfigurationEntry 方法如下,主要有流程:

  1. 判断自动装配开关是否打开,默认为 true。
  2. 获取 @EnableAutoConfiguration 注解中的 exclude 和 excludeName 属性。
  3. 获取所有自动装配的配置类,读取所有引入 jar 包下的 META-INF/spring.factories 文件。
  4. 自动配装配的配置类通过 @ConditionalOnXxx 进行选择性的加载控制,在 getAutoConfigurationEntry 中剔除不需要被加载的配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
//<1>.
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
//<2>.
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//<3>.
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//<4>.
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}

自实现 starter

了解了自动配置的原理后,自实现 starter 的流程如下:

  1. 引入 spring boot 相关依赖
  2. 创建自动配置类,用 @ConditionalOnXxx 修饰
  3. 将自动配置类填写到 META-INF/spring.factories 文件下
  4. 新建工程引入 xxx-spring-boot-starter

缓存

Spring 框架提供了透明添加缓存到应用程序的支持。 只要通过使用 @EnableCaching 注解启用缓存支持,Spring Boot就会自动配置缓存基础设施。

Spring Cache

Spring Boot 提供的缓存包括:JCache、Redis、Simple 等。通过 spring.cache.type 配置参数来指定缓存类型,当该值为 None 时为完全禁用缓存。

在方法上使用 @Cachable、@CacheEvict、@CachePut 表示缓存的行为。

Simple

Simple 作为默认的缓存实现,使用 ConcurrentHashMap 作为缓存存储。

Redis

如果已经引入了 spring-boot-starter-redis,缓存的配置信息可以通过 spring.cache.redis.* 来设置。

如果需要对配置信息进行更多的控制,可以考虑注册一个 RedisCacheManagerBuilderCustomizer bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.time.Duration;

import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;

@Configuration(proxyBeanMethods = false)
public class MyRedisCacheManagerConfiguration {

@Bean
public RedisCacheManagerBuilderCustomizer myRedisCacheManagerBuilderCustomizer() {
return (builder) -> builder
.withCacheConfiguration("cache1", RedisCacheConfiguration
.defaultCacheConfig().entryTtl(Duration.ofSeconds(10)))
.withCacheConfiguration("cache2", RedisCacheConfiguration
.defaultCacheConfig().entryTtl(Duration.ofMinutes(1)));

}

}

@Configuration 注解:

  • @Configuration 是 Spring 框架提供的一个元注解,用于表示一个类是一个配置类。
  • 配置类通常包含 @Bean 注解,定义了创建和配置 Bean 的方法。
  • Spring 会扫描 @Configuration 类,并将其中的 @Bean 方法注册为 Spring 容器中的 Bean(name 默认为方法名)。

@Configuration 注解的 proxyBeanMethods 属性:用于控制@Bean方法的代理行为,默认为 true。

  • 当 proxyBeanMethods 属性设置为 true 时,Spring 会对 @Configuration 类进行 CGLIB 代理。调用 @Bean 方法时,Spring 会检查是否已经存在该 Bean,如果存在,则直接返回已存在的 Bean,否则调用方法创建新的 Bean 并缓存起来。
  • 当 proxyBeanMethods 属性设置为 false 时,会禁用 CGLIB 代理。这种情况下,每一次调用 @Bean 都会执行一次方法体,不会缓存 Bean 对象,适用于那些需要每次返回新实例的场景

JetCache

JetCache 是一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。 JetCache 提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了 Cache 接口用于手工缓存操作,支持 Spring Boot、支持统计信息。 目前有四个实现:

  • 远程缓存:
    • redis
    • tair
  • 本地缓存:
    • caffeine
    • linked hash map

@EnableMethodCache、@EnableCreateCacheAnnotation 这两个注解分别激活 @Cached 和 @CreateCache 注解。也就是说,可以直接使用 @Cached 实现方法缓存,或者使用创建一个 Cache 实例两种方式,来使用 JetCache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.company.mypackage;

import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableMethodCache(basePackages = "com.company.mypackage")
@EnableCreateCacheAnnotation
public class MySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApp.class);
}
}

配置信息参见:https://github.com/alibaba/jetcache/blob/master/docs/CN/Config.md

创建 Cache 实例

有两种方法创建 Cache 实例,分别是 CacheManager 和 @CacheCreate 注解(在jetcache 2.7 版本CreateCache注解已经废弃)。

  • 使用 CacheManager 可以创建 Cache 实例,area 和 name 相同的情况下,它和 Cached 注解使用同一个 Cache 实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private CacheManager cacheManager;
private Cache<String, UserDO> userCache;

@PostConstruct
public void init() {
QuickConfig qc = QuickConfig.newBuilder("userCache")
.expire(Duration.ofSeconds(100))
.cacheType(CacheType.BOTH) // two level cache
.syncLocal(true) // invalidate local cache in all jvm process after update
.build();
userCache = cacheManager.getOrCreateCache(qc);
}
  • 使用 @CreateCache 注解创建一个Cache实例,在jetcache 2.7 版本CreateCache注解已经废弃
1
2
@CreateCache(expire = 100)
private Cache<Long, UserDO> userCache;

方法缓存

在 spring 环境下,使用 @Cached 注解可以为一个方法添加缓存,@CacheUpdate 用于更新缓存,@CacheInvalidate 用于移除缓存元素。

1
2
3
4
5
6
7
8
9
10
public interface UserService {
@Cached(name="userCache.", key="#userId", expire = 3600)
User getUserById(long userId);

@CacheUpdate(name="userCache.", key="#user.userId", value="#user")
void updateUser(User user);

@CacheInvalidate(name="userCache.", key="#userId")
void deleteUser(long userId);
}

定时任务

Quartz

Spring Boot 提供了 Quartz 的 starter,spring-boot-starter-quartz。如果 Quartz 可用,就会自动配置一个 Scheduler。以下类型的 Bean 会被自动装配并与 Scheduler 关联:

  • JobDetail:定义了一个特定的 Job。 JobDetail 实例可以通过 JobBuilder API建立。
  • Calendar
  • Trigger:定义了一个特定的 Job 何时被触发。

定义任务

通过继承 QuartzJobBean 来定义自己的任务,重写 executeInternal 方法,并且可以通过 setter 注入属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import org.springframework.scheduling.quartz.QuartzJobBean;

public class MySampleJob extends QuartzJobBean {

private MyService myService;

private String name;

// Inject "MyService" bean
public void setMyService(MyService myService) {
this.myService = myService;
}

// Inject the "name" job data property
public void setName(String name) {
this.name = name;
}

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
this.myService.someMethod(context.getFireTime(), this.name);
}

}

持久化任务

默认情况下,使用内存中的 JobStore。 然而,如果你的应用程序中有一个 DataSource Bean,并且相应地配置了 spring.quartz.job-store-type 属性,也可以配置一个基于 JDBC 的 store。

要让 Quartz 使用应用程序主 DataSource 以外的 DataSource,可以声明一个 DataSource bean,用 @QuartzDataSource 注释其 @Bean 方法。这样做可以确保Quartz特定的数据源被 SchedulerFactoryBean 和schema初始化所使用。

同样地,为了让 Quartz 使用应用程序的主 TransactionManager 以外的 TransactionManager,需要声明一个 TransactionManager Bean,用 @QuartzTransactionManager 注释其 @Bean 方法。

默认情况下,通过配置创建的Job不会覆盖已经从持久性 JobStore 中读取的已注册Job。 要启用覆盖现有作业定义,需要设置 spring.quartz.overwrite-existing-jobs 属性。

Spring Task

Spring 原生的支持定时任务功能,使用 @EnableScheduling 注解开启定时任务功能,使用 @Scheduled 注解指定定时任务。

1
2
3
4
5
6
7
@Component
class MyBean {
@Scheduled(cron = "1 * * * * ?")
public void myTask() {
// ...
}
}

Redis

通过引入 spring-boot-start-data-redis,就可以在 Spring Boot 中集成 Redis;同时也可以引入 spring-boot-starter-data-redis-reactive,支持与 Redis 的响应式异步交互。

使用

通过引入 starter,就可以注入一个自动配置的 RedisConnectionFactory、StringRedisTemplate、RedisTemplate。通过 StringRedisTemplate 就可以与 Redis 进行交互。

客户端

Spring Boot 支持两种 Redis 客户端,分别是 lettuce 和 jedis,lettuce 是默认的 Redis 客户端。

区别

lettuce 与 jedis 区别:

  • jedis 连接 Redis 服务器是直连模式,当多线程模式下使用 jedis 会存在线程安全问题,解决方案可以通过配置连接池使每个连接专用,但是会产生性能问题。
  • lettcus 基于 Netty 框架进行与 Redis 服务器连接,底层设计中采用 StatefulRedisConnection。StatefulRedisConnection 自身是线程安全的,可以保障并发访问安全问题,一个连接可以被多线程复用

MongoDB

通过引入 spring-boot-start-data-mongodb,就可以在 Spring Boot 中集成 MongoDB;同时也可以引入 spring-boot-starter-data-mongodb-reactive,支持与 MongoDB 的响应式异步交互。

可以注入 MongoTemplate 类来操作 MongoDB。

ElasticSearch

通过引入 spring-boot-start-data-elasticsearch,就可以在 Spring Boot 中集成 ES;同时也可以引入 spring-boot-starter-data-elasticsearch-reactive,支持与 ES 的响应式异步交互。

Spring Boot支持多个客户端。

  • 官方低级别的 REST 客户端(low-level REST client)
  • 官方的 Java API 客户端
  • Spring Data Elasticsearch 提供的 ReactiveElasticsearchClient

通过 ElasticsearchTemplate 来操作 ES。

日志

记录日志

在程序开发过程中,需要定义一个 Logger 类型的属性 log,用于记录日志:

1
private static final Logger log = LoggerFactory.getLogger(Xxx.class);

@Slf4j 注解

lombok 提供了 @Slf4j 注解,可以简化开发。在类上声明 @Slf4j 注解,可以自动生成一个 Logger 类型的属性 log。

日志格式

在配置文件中,使用 logging.pattern.console 设置日志格式。

输出日志

日志输出级别

日志级别分为六个等级:

  • trace:运行时的堆栈信息。
  • debug:测试的 Debug 信息。
  • info:记录运行过程中的数据。
  • warn:运行过程中的告警数据。
  • error:错误信息。
  • fatal:灾难信息,与 error 合并。

可以使用 Spring Boot 的配置文件,设置日志的输出级别。可以为根、包、日志组设置日志输出级别。

1
2
3
4
5
6
7
8
9
10
logging:
group: # 设置日志组
group1: com.group1.xxx,com.group1.yyy
level:
# 根日志级别
root: info
# 为包设置日志
com.dawnzzz: debug
# 为日志组设置级别
group1: debug

日志文件

使用配置文件中的 logging.file 属性,可以定义日志文件的配置信息。

如果使用 logback 作为日志工具,使用 logging.logback.rollingpolicy 属性,可以设置滚动滚动属性。

阅读全文 »

Spring Boot 介绍

Spring Boot 目标是减少 Spring 应用程序的配置。

自动依赖管理

parent

Spring Boot 所做的自动依赖管理,是由于在 Maven pom 配置文件中继承了 Spring Boot 的配置文件 spring-boot-parent,spring-boot-parent 有继承了 spring-boot-dependencies(本质上来说,就是在本地 pom 文件中引入了 spring-boot-dependencies,进行依赖管理避免依赖冲突)。

spring-boot-dependencies 中定义了依赖信息和版本信息。具体来说,是利用 properties 定义了版本信息,利用 dependencyManagement 定义依赖管理。

利用阿里云场景 Spring Boot 工程,使用的是引入 spring-boot-dependencies 的方式。

image-20240502163253884

image-20240502163312630

starter

starter 定义了当前项目所使用的依赖坐标,减少依赖配置。starter 在 Spring Boot 中的命名规范为 spring-boot-starter-xxx 或者 xxx-spring-boot-starter(第三方技术)。

在本地 pom 配置文件中导入 starter 依赖,就可以直接使用了。

image-20240502164630343

在引入 spring-boot-starter-web 时,运行程序会自动启动 tomcat 服务器(内嵌 tomcat 服务器),这是因为 spring-boot-starter-web 引用了 spring-boot-starter-tomcat。

内嵌 tomcat 服务器的原理就是,将 tomcat 服务器作为对象,在 Spring IOC 容器中管理起来

配置文件

Spring Boot 的默认配置文件为 application.properties,同时 Boot 支持三种配置文件格式:

  • properties
  • yml
  • yaml

优先级 properties > yml > yaml

可以在命令行参数中指定以下参数,来修改配置文件名:

  • –spring.config.name 参数,指定配置文件名(不包括扩展名)。
  • –spring.config.location 参数,指定配置文件全路径名。

配置文件位置

Spring boot 按照以下顺序读取配置文件,后面的配置会覆盖前面的配置。

  1. classpath(开发时)
    1. classpath 根路径。
    2. classpath 下的 /config 包。
  2. jar 当前目录(运行时):
    1. 当前目录下。
    2. 当前目录下的 config/ 子目录。
    3. config/ 子目录的直接子目录。

外部化配置

Spring Boot 支持外部化配置,可以使用各种外部配置源,包括Java properties 文件、YAML文件、环境变量和命令行参数。

读取配置信息

配置的属性值可以通过以下方式读取值

  • @Value 注入到 Bean 中。
  • 可以通过 Spring 的 Environment 访问(定义一个 Environment 类型的属性,使用 @Autowired 自动装配)。
  • 通过 @ConfigurationProperties 绑定对象(首先得将对象定义为 Spring Bean),或者 @EnableConfigurationProperties 注解。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@ConfigurationProperties( prefix="myPrefix" )
class MyDataSource {
private String url;

public String getUrl() {
return url;
}

public setUrl(String url) {
this.url = url;
}
}

使用 @ConfigurationProperties 注解读取配置时,支持配置属性的宽松绑定(支持中划线、下划线、不区分大小写),前缀 prefix 命名规范仅仅支持小写字母、中划线、数字。

在 Maven 中引入 JSR303 及其实现类(如 Hibernate),使用 @Validated 注解开启数据校验功能,在字段上使用 @MAX、@MIN、@Email 等注解进行数据校验。

外部配置源顺序

Spring Boot 使用以下外部配置源的顺序,后面的配置会覆盖前面的配置:

  1. 默认属性(通过 SpringApplication.setDefaultProperties 指定)。
  2. @Configuration 类上的 @PropertySource 注解(这样的属性源直到 spring 容器启动时才会被加载,但是对于配置某些属性来说已经太晚了,如 logging 配置)。
  3. 配置文件,如 application.properties。
  4. RandomValuePropertySource
  5. 环境变量。
  6. Java System properties,使用 System.setProperties() 指定。
  7. java:comp/env 中的 JNDI 属性。
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. 来自 SPRING_APPLICATION_JSON 的属性(嵌入环境变量或系统属性中的内联 JSON)。
  11. 命令行参数,使用 --属性名=值 指定。
  12. 在测试中的 properties 和 args 属性, @SpringBootTest 注解指定 @SpringBootTest(properties = {"server.port=8080", ...}, args={"--server.port=8081", ...}),其中 args 属性比 properties 属性优先级高。
  13. 在测试中的 @DynamicPropertySource 注解。
  14. 测试中的 @TestPropertySource 注解.
  15. 当 devtools 处于活动状态时,$HOME/.config/spring-boot 目录下的Devtools全局设置属性。

多环境配置

Spring Boot 的多环境配置,可以在单个配置文件中指定,也可以在多个配置文件中指定,每一个环境对应一个配置文件(推荐)。

单文件配置

同一个配置文件中可以指定多个环境的配置,环境之间使用 --- 分隔,环境名称使用 spring.profiles 或者 spring.config.activate.on-profile 指定,使用的环境使用 spring.profiles.active 指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 指定环境名称
spring:
profiles:
active: dev

---
server:
port: 8080

# 旧版格式
spring:
profiles: dev

---
server:
port: 8081

# 新版格式
spring:
config:
activate:
on-profile: test

多文件配置

可以使用 application-环境名称.yml 指定多环境的配置信息。在主配置文件中,使用 spring.profiles.active 指定当前使用的环境。

可以根据功能对配置文件中的信息进行拆分,,如 application-devDB.yml、application-devRedis 等,之后在主配置文件中引入配置信息,方式有两种:

  • 使用 spring.profiles.include 引入多个配置,优先级为 spring.profiles.active 大于 spring.profiles.include
  • 使用 spring.profiles.group 指定配置组,配置组优先级大于 spring.profiles.active
1
2
3
4
5
6
spring:
profiles:
active: dev
group: # 指定配置组
"dev": devDB,devRedis,devMVC
"test": testDB,testRedis,testMVC

为了开发效率,可以将映射文件替换为注解开发。常用的 MyBatis 注解:

  • @Insert
  • @Delete
  • @Update
  • @Select
  • @Results:用于替换 ResultMapper 标签,可以在其中定义多个 @Result 集合。
    • @Result:替换了 ResultMapper 中标签,其中 one 属性配合 @One 注解代替了 association 标签的 select 属性;many 属性配合 @Many 注解代替了 collection 标签的 select 属性。
  • @Mapper:添加在 Mapper 接口中,目的是让 Spring 容器可以识别到。

多表查询

假设有两张表,分别是员工表和部门表。

多对一

查询员工以及员工所在的部门(需要在 User 实体类中定义一个 Dept 类型的属性 dept),可以有三种方法:

  • 级联方式
  • 使用 association
  • 分步查询

级联

使用级联方式,只需要在 resultMap 中定义 result 标签即可,dept 部门属性表示为 dept.xxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resultMap id="empDeptMap" type="Emp">
<id column="eid" property="eid"></id>
<result column="ename" property="ename"></result>
<result column="age" property="age"></result>
<result column="sex" property="sex"></result>
<result column="did" property="dept.did"></result>
<result column="dname" property="dept.dname"></result>
</resultMap>

<!--Emp getEmpAndDeptByEid(@Param("eid") int eid);-->
<select id="getEmpAndDeptByEid" resultMap="empDeptMap">
select emp.*,dept.* from t_emp emp left join t_dept dept on emp.did =
dept.did where emp.eid = #{eid}
</select>

association

使用 association 标签,可以指明属性 dept 和属性类型 Dept,在 association 标签中定义部门的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<resultMap id="empDeptMap" type="Emp">
<id column="eid" property="eid"></id>
<result column="ename" property="ename"></result>
<result column="age" property="age"></result>
<result column="sex" property="sex"></result>
<association property="dept" javaType="Dept">
<id column="did" property="did"></id>
<result column="dname" property="dname"></result>
</association>
</resultMap>

<!--Emp getEmpAndDeptByEid(@Param("eid") int eid);-->
<select id="getEmpAndDeptByEid" resultMap="empDeptMap">
select emp.*,dept.* from t_emp emp left join t_dept dept on emp.did =
dept.did where emp.eid = #{eid}
</select>

分步查询

分步查询就是首先查询员工信息,然后根据员工信息查询部门信息。

具体就是使用 association 的 select 属性和 column 属性

  • select 属性:设置分步查询,查询某个属性的值的 sql 的标识。
  • column 属性:将第一步中的查询结果中的字段,设置为下一步查询的输入值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<resultMap id="empDeptStepMap" type="Emp">
<id column="eid" property="eid"></id>
<result column="ename" property="ename"></result>
<result column="age" property="age"></result>
<result column="sex" property="sex"></result>
<!--
select:设置分步查询,查询某个属性的值的sql的标识(namespace.sqlId)
column:将sql以及查询结果中的某个字段设置为分步查询的条件
-->
<association property="dept"
select="com.atguigu.MyBatis.mapper.DeptMapper.getEmpDeptByStep" column="did">
</association>
</resultMap>

<!--Emp getEmpByStep(@Param("eid") int eid);-->
<select id="getEmpByStep" resultMap="empDeptStepMap">
select * from t_emp where eid = #{eid}
</select>

<!--Dept getEmpDeptByStep(@Param("did") int did);-->
<select id="getEmpDeptByStep" resultType="Dept">
select * from t_dept where did = #{did}
</select>

一对多

查询部门下的所有员工(需要在 Dept 实体类中定义 User List),可以有以下几种方法:

  • collection
  • 分步查询

collection

使用 collection 标签可以表示一对多的关系,ofType 属性指明集合中存储的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<resultMap id="deptEmpMap" type="Dept">
<id property="did" column="did"></id>
<result property="dname" column="dname"></result>
<!--
ofType:设置collection标签所处理的集合属性中存储数据的类型
-->
<collection property="emps" ofType="Emp">
<id property="eid" column="eid"></id>
<result property="ename" column="ename"></result>
<result property="age" column="age"></result>
<result property="sex" column="sex"></result>
</collection>
</resultMap>

<!--Dept getDeptEmpByDid(@Param("did") int did);-->
<select id="getDeptEmpByDid" resultMap="deptEmpMap">
select dept.*,emp.* from t_dept dept left join t_emp emp on dept.did =
emp.did where dept.did = #{did}
</select>

分步查询

即先查询部门,再查询部门下的所有员工。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<resultMap id="deptEmpStep" type="Dept">
<id property="did" column="did"></id>
<result property="dname" column="dname"></result>
<collection property="emps" fetchType="eager"
select="com.atguigu.MyBatis.mapper.EmpMapper.getEmpListByDid" column="did">
</collection>
</resultMap>

<!--Dept getDeptByStep(@Param("did") int did);-->
<select id="getDeptByStep" resultMap="deptEmpStep">
select * from t_dept where did = #{did}
</select>

<!--List<Emp> getEmpListByDid(@Param("did") int did);-->
<select id="getEmpListByDid" resultType="Emp">
select * from t_emp where did = #{did}
</select>

延迟加载

分步查询的好处就是可以实现延迟加载

MyBatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载。在 MyBatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false,然后通过设置 association 和 collection 中的 fetchType 属性设置当前分步查询是否延迟加载(lazy 延迟加载,eager 立即加载)。

原理

MyBatis 延迟加载实现的原理是,使用 CGLIB 创建目标对象的代理对象

当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName() ,拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会执行查询关联 B 对象的 sql 语句去查询,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。

缓存

MyBatis 有缓存的功能,将 select 语句缓存到内存中,下⼀次还是这条相同 select 语句的话,直接从缓存中取,不再查数据库。MyBatis 的缓存包括:

  • 一级缓存:将查询到的结果存储到 SqlSession 中。
  • 二级缓存:将查询到的数据存储到 SqlSessionFactory 中。

还有一些第三方缓存,如 EHCache、Memcache 等。

一级缓存

一级缓存是 SqlSession 级别的,同一个 SqlSession 下查询的数据会被缓存。一级缓存失效的原因:

  • 使用不同的 SqlSession。
  • 查询语句发生变化。
  • 查询之间有增删改操作。
  • 手动调用了 clearCache() 方法,手动清空一级缓存。

二级缓存

二级缓存是 SqlSessionFactory 级别的,二级缓存需要手动开启:

  1. 在配置文件中设置 <setting name="cacheEnabled" value="true"> 开启缓存,默认是 true。
  2. 在需要使⽤二级缓存的映射文件中添加一个标签:<catche />
  3. 使用二级缓存的实体类必须是可序列化的,也就是实现 java.io.Serializable 接口。
  4. SqlSession 对象关闭或者提交后,一级缓存中的数据才会被写入到二级缓存中。

配置

二级缓存在 <cache /> 标签中配置,可选的配置项:

  • eviction:淘汰算法。
    • LRU
    • FIFO
    • SOFT:淘汰软引用指向的对象。
    • WEEK:淘汰弱引用指向的对象。
  • flushInterval:刷新间隔,单位毫秒。默认是不刷新。
  • readOnly:只读属性。
    • true:缓存返回相同的实例,可能存在并发问题。
    • false:通过序列化返回对象的拷贝,线程安全,默认为 false。
  • size:缓存的对象数,代表缓存最多可以存储多少个对象,太大容易导致内存溢出。

Mybatis 映射文件由 mapper 标签作为顶层标签,拥有属性 namespace。一个基本的映射文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<mapper namespace="ns">
<!-- 插入操作 -->
<insert id="insertID" parameterType="Xxx">
insert into xxx values(#{field1}, #{field2}, #{field3})
</insert>
<!-- 删除操作 -->
<delete id="deleteID", parameterType="Xxx">
delete frrom xxx where ...
</delete>
<!-- 更新操作 -->
<update id="updateID", parameterType="Xxx">
update xxx ser ... where ...
</update>
<!-- 查询操作 -->
<select id="selectID", parameterType="Xxx", resultType="Xxx">
select * from xxx where ...
</select>
</mapper>

${} 和 #{} 的区别:${} 本质上就是字符串的拼接,需要手动加上单引号;#{} 是占位符的方式。

阅读全文 »

配置文件用于配置 MyBatis,由 configuration 标签包裹。

MyBatis 配置文件中标签的顺序:

properties、settings、typeAliases、typeHandlers、objectFactory、objectWrapperFactory、reflectorFactory、plugins、environments、databaseIdProvider、mappers

基本配置

properties

properties 标签用于引入 properties 文件。

1
<properties resource="jdbc.properties" />

typeAliases

typeAliases 用于设置多个类型的别名,可以包含 typeAlias 标签和 package 标签。

  • typeAlias 标签用于设置某个类型的别名,有两个属性 type 和 alias,用于指定类型和别名(不设置别名,则使用默认的别名,即类名且不区分大小写)。
  • package 标签,以包为单位,将包下所有的类型设置默认的类型别名,即类名且不区分大小写。
1
2
3
4
<typeAliases>
<!--<typeAlias type="com.xxx.User"></typeAlias>-->
<package name="com.xxx"/>
</typeAliases>

MyBatis 中已经为常用类型设置了类型别名:

  • java.lang.Integer –> int|integer
  • int –> _int|_integer
  • Map –> map
  • List –> list
  • 。。。。。。

environments

environments 用于设置多个数据库连接配置,可以指定 default 环境 id。

environment 标签用于配置某个具体环境,属性 id 表示连接数据库的环境的唯一标识,不能重复。包含以下标签:

  • transactionManager 标签,设置事务管理方式。有属性 type,可选 JDBC 或者 MANAGED。
    • JDBC 表示当前环境中,执行 SQL 时,使用的是 JDBC 中原生的事务管理方式,事务的提交或回滚需要手动处理。
    • MANAGED 表示被管理,例如 Spring。
  • dataSource 标签,表示配置数据源。有属性 type 表示数据源类型,可选值为 POOLED(使用连接池)/UNPOOLED(不使用连接池)/JNDI(使用上下文中的数据源)。可以包含 property 标签用于配置数据库驱动、连接地址、用户名、密码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<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>
</environment>
</environments>

mappers

mappers 用于引入映射文件,可以包含 mapper 或者 package 标签。

  • mapper 标签,可以指定 resource 属性(或者 url、class 属性),用于引入 mapper 映射文件。
  • package 标签,以包为单位引入映射文件,但是有两个要求:(1)mapper 接口所在的包要和映射文件所在的包一致(2)mapper 接口要和映射文件的名字一致。
1
2
3
4
<mappers>
<!--<mapper resource="mappers/UserMapper.xml"/>-->
<package name="com.xxx.mapper"/>
</mappers>
阅读全文 »

介绍

MyBatis 是 Java 的持久层框架,内部封装了 jdbc,使开发者只需要关注 SQL 语句本身,不需要精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。

MyBatis 通过 XML 或者注解的方式将要执行的各个 statement 配置,通过 Java 对象和 statement 中的动态参数进行映射,并生成最终执行的 SQL 语句。最后 MyBatis 执行 SQL 语句并将结果映射为 Java 对象并返回(ORM)。

开发流程

MyBatis 的开发流程:

  1. 编写核心配置文件。
  2. 定义实体类。
  3. 定义 Mapper 接口,定义数据库操作。
  4. 编写映射文件。