SpringBoot
创建简单视图
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 适用于前后不分离项目,创建简单的视图控制器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 将 url / 映射到 /home
registry.addViewController("/").setViewName("home");
registry.addViewController("/login");
}
}Spring 注入方式
建议始终在 bean 中使用基于构造函数的依赖项注入,始终对强制性依赖项使用断言; 构造器和 setter 注入,在类被调用时可以保证它们都完全准备好了,也可以保证这些变量的值不会是 null
// 基于 Lombok 提供的注解注入
@RequiredArgsConstructor
public class UserServiceImpl implents UserService{
private final UserDao userDao;
}
// 基于构造函数注入
public class UserServiceImpl implents UserService{
private final UserDao userDao;
@Autowired
public UserServiceImpl(UserDao userDao){
Assert.notNull(userDao, "userDao must not be null");
this.userDao = userDao;
}
}
// 基于 Setter 注入
public class UserServiceImpl implents UserService{
private final UserDao userDao;
// Spring 4.3 及以后的版本中,@Autowired 可不写
@Autowired
public setUserDao(UserDao userDao){
Assert.notNull(userDao, "userDao must not be null");
this.userDao = userDao;
}
}
// 基于字段入
public class UserServiceImpl implents UserService{
@Autowired
private UserDao userDao;
}属性配置
使用@ConfigurationProperties 注解示例
# 配置示例
alipay:
appId: 666
aliAppPublicKey: 555@Data
@Component
@ConfigurationProperties(prefix = "alipay")
public class AlipayConfig {
private String appId;
private String aliAppPublicKey;
}提示
在 IDE 中可能看到 yml 文件存在“unknown property”警告,这是因为 IDE 会尝试寻找它所需要的元数据,以便了解这些属性的含义; 在 pom.xml 中添加如下依赖即可消除警告
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>应用启动时执行
Spring Boot 提供了两个非常有用的接口,用于在应用启动的时候执行一定的逻辑,即 CommandLineRunner 和 ApplicationRunner
因为 CommandLineRunner 和 ApplicationRunner 都是函数式接口,都需要实现一个 run() 方法,所以在配置类中可以很容易地将其声明为 bean,只需在一个返回 lambda 表达式的方法上使用@Bean 注解
CommandLineRunner 和 ApplicationRunner 的区别在于各自 run() 方法的参数; CommandLineRunner 的 run() 方法接受一个 String 类型的可变长度参数; ApplicationRunner 的 run() 方法接受一个 ApplicationArguments 参数;
@Bean
public CommandLineRunner dataLoader(MyService service) {
return args -> {
// 这里的 args 代表命令行参数;如 java -jar xxx.jar --version 1.0 运行
// args 就为'version 1.0'的字符串;
// CommandLineRunner 的 run 方法参数可变,若有多个命令行参数,则 (arg1,arg2,...) -> {}
service.doSomething();
};
}@Bean
public ApplicationRunner dataLoader(MyService service) {
return args -> {
// 相对于 CommandLineRunner,ApplicationRunner 获取命令行参数较简洁,无需转换
List<String> version = args.getOptionValues("version");
service.doSomething();
};
}加载外部 jar 包
在项目 resources 目录下创建一个文件夹,例如名称为 lib ,将需要加载的 jar 包放入新建的 lib 目录中,并修改 pom.xml,配置如下:
<!-- 外部引入的 jar 包,g、a、v 可随意定义 -->
<dependency>
<groupId>com.zjx</groupId>
<artifactId>zjx-test-a</artifactId>
<version>1.0</version>
<systemPath>${project.basedir}/src/main/resources/lib/a.jar</systemPath>
</dependency>
<dependency>
<groupId>com.zjx</groupId>
<artifactId>zjx-test-b</artifactId>
<version>1.0</version>
<systemPath>${project.basedir}/src/main/resources/lib/b.jar</systemPath>
</dependency>日志配置
SLF4J 是日志的接口标准,Logback 和 Log4j2 都是 SLF4J 的具体实现,Spring Boot 2.x 选择 Logback 作为默认的日志框架,Spring Boot 3.x 的默认日志实现改为了 Log4j 2。
为了完全控制日志的配置,可以在类路径的根目录下(在“src/main/resources”中)创建一个 logback.xml 文件
借助 Spring Boot 的配置属性功能,不用创建 logback.xml 文件也能完成配置
# 配置示例
logging:
file:
path: /var/logs/
file: TacoCloud.log
level:
root: WARN
org.springframework.security: DEBUG比较完整的 logback.xml 示例
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="">
<!--默认配置-->
<!--<include resource="org/springframework/boot/logging/logback/defaults.xml"/>-->
<!--配置控制台 (Console)-->
<!--<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>-->
<springProperty scope = "context" name = "applicationName" source = "spring.application.name" />
<springProperty scope = "context" name = "env" source = "spring.profiles.active" />
<springProperty scope = "context" name = "console_log_level" source = "console.log.level" />
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志 appender 是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${console_log_level}</level>
</filter>
<encoder>
<Pattern>[%date{yyyy-MM-dd HH:mm:ss}] [ %-5level] [%X{traceId}] [%X{requestId}] %logger{96} [%line] - %msg%n</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name = "debug_log" class = "ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/debug/application.log</file>
<rollingPolicy class = "ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/debug/application-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>5MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>[%date{yyyy-MM-dd HH:mm:ss}] [ %-5level] [%X{traceId}] [%X{requestId}] %logger{96} [%line] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 此日志文档记录 debug 以上级别的 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debug</level>
</filter>
</appender>
<appender name = "info_log" class = "ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/info/application.log</file>
<rollingPolicy class = "ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/info/application-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>5MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>[%date{yyyy-MM-dd HH:mm:ss}] [ %-5level] [%X{traceId}] [%X{requestId}] %logger{96} [%line] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 此日志文档记录 info 以上级别的 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>info</level>
<!--<onMatch>ACCEPT</onMatch>-->
<!--<onMismatch>DENY</onMismatch>-->
</filter>
</appender>
<appender name = "error_log" class = "ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/error/application.log</file>
<rollingPolicy class = "ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/error/application-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>5MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>[%date{yyyy-MM-dd HH:mm:ss}] [ %-5level] [%X{traceId}] [%X{requestId}] %logger{96} [%line] - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 此日志文档记录 info 以上级别的 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>error</level>
<!--<onMatch>ACCEPT</onMatch>-->
<!--<onMismatch>DENY</onMismatch>-->
</filter>
</appender>
<!-- 为 logstash 输出的 JSON 格式的 Appender -->
<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>47.108.88.119:4560</destination>
<!-- 日志输出编码 -->
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<pattern>
<pattern>
{
"severity": "%level",
"service": "${applicationName:-}",
"trace": "%X{traceId:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"message": "%message",
"env": "${env}",
"stack_trace": "%exception"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<logger name="com.zjx.com.mapper" level="DEBUG">
<appender-ref ref="CONSOLE" />
<appender-ref ref="info_log" />
</logger>
<root level = "INFO">
<appender-ref ref="logstash"/>
<appender-ref ref="CONSOLE"/>
<!--<appender-ref ref = "debug_log" />-->
<appender-ref ref = "info_log" />
<appender-ref ref = "error_log" />
</root>
</configuration>Spring Boot 中加载 xml 配置
@Configuration
@ImportResource("classpath:/xxx.xml")
public class MyConfig { ... }缓存
以下都为注解式缓存示例,他们可以同时存在,也可以只使用其中一个,根据项目需求添加即可。
Caffeine
Caffeine 是一个高性能、功能强大的本地缓存库。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>cache:
caffeine:
# 默认最大缓存数为 2000,写入后固定 600s 过期
default-spec: maximumSize=2000,expireAfterWrite=600s@Slf4j
@Configuration
@EnableCaching
public class MultiCacheConfig {
@Value("${cache.caffeine.default-spec:maximumSize=1000,expireAfterWrite=600s}")
private String caffeineDefaultSpec;
// 缓存名称常量(统一在全局引用)
public static final String USER_CACHE = "userCache";
public static final String PRODUCT_CACHE = "productCache";
public static final String SESSION_CACHE = "sessionCache";
/**
* Caffeine 缓存管理器 - 本地内存缓存
* 支持根据 cacheName 设置不同策略,未命中使用默认配置
*/
@Bean("caffeineCacheManager")
@Primary // 设置为主要缓存管理器
public CacheManager caffeineCacheManager() {
log.info("初始化 Caffeine Cache Manager...");
// 提前构建默认配置,避免多次解析
Caffeine<Object, Object> defaultBuilder = Caffeine.from(caffeineDefaultSpec);
return new CaffeineCacheManager() {
@Override
@SuppressWarnings("NullableProblems") // 解决 Spring 6 @NonNullApi 警告
protected Cache<Object, Object> createNativeCaffeineCache(String name) {
return switch (name) {
case USER_CACHE -> Caffeine.newBuilder()
.maximumSize(500) // 最大缓存数为 500
.expireAfterWrite(60, TimeUnit.SECONDS) // 写入后固定时间过期
.removalListener((key, value, cause) ->
log.debug("清理缓存 key: {} - {}, 原因为:{}", key, value, cause))
.build();
case PRODUCT_CACHE -> Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterAccess(300, TimeUnit.SECONDS) // 最后一次访问后固定时间过期
.build();
case SESSION_CACHE -> Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
default -> defaultBuilder.build(); // 默认配置兜底
};
}
};
}
}@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/cache")
public class CacheController {
/**
* 测试 Caffeine 缓存
*/
@Cacheable(
value = MultiCacheConfig.USER_CACHE, // 缓存名称,和 cacheNames 属性是等效的
key = "'userId::'+ #userId",
cacheManager = "caffeineCacheManager" // 指定使用哪种缓存(到容器中找对应的 Bean)
)
@GetMapping("/test/caffeine")
public String testCaffeine(Long userId) {
log.info(" caffeine --- 猜猜我会打印几次:{}", userId);
return "caffeine:" + userId;
}
}Redis
<!-- 这里说明一下,如果项目中已经有 Redisson 作为 Redis 客户端,则此处依赖都可以删掉 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>spring:
data:
# 注意这个 Redis 配置主要影响 RedisTemplate 和 RedisCacheManager。
# 如果项目中使用了Redisson,它会被 redisson.yaml 中的 Redis 配置覆盖,即使删掉此处配置,RedisTemplate 依然能用
# 但是无法实现比如使用 RedisTemplate 操作 0 号数据库,Redisson 操作 1 号库这种垃圾需求
# 建议项目中 RedisTemplate 和 Redisson 二选一,不要混着用,以免配置冲突
redis:
host: 127.0.0.1
port: 6379
database: 1
cache:
redis:
default-ttl: 3600@Slf4j
@Configuration
@EnableCaching
public class MultiCacheConfig {
@Value("${cache.caffeine.default-spec:maximumSize=1000,expireAfterWrite=600s}")
private String caffeineDefaultSpec;
// 缓存名称常量(统一在全局引用)
public static final String USER_CACHE = "userCache";
public static final String PRODUCT_CACHE = "productCache";
public static final String SESSION_CACHE = "sessionCache";
/**
* Redis 缓存管理器 - 分布式缓存
* 适用于分布式环境下的数据共享
*/
@Bean("redisCacheManager")
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
log.info("初始化 Redis Cache Manager...");
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
// 设置缓存项的存活时间,每次写入缓存时都会刷新 TTL
.entryTtl(Duration.ofSeconds(redisDefaultTtl))
.disableCachingNullValues()
// 配置序列化器,这里的序列化和容器中的 RedisTemple 无关;RedisCacheManager 专注于方法级缓存,和 RedisTemple 的手动操作不同
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put(USER_CACHE, defaultConfig.entryTtl(Duration.ofMinutes(30)));
cacheConfigurations.put(PRODUCT_CACHE, defaultConfig.entryTtl(Duration.ofHours(2)));
cacheConfigurations.put(SESSION_CACHE, defaultConfig.entryTtl(Duration.ofDays(1)));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}
}@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/cache")
public class CacheController {
/**
* 测试 Redis 缓存
* 观察 Redis 中 key 为 userCache::userId:xxx ,不要纠结为什么有 :: 符号,这是 Spring 默认分隔符,用于区分缓存名和 key
*/
@Cacheable(
value = MultiCacheConfig.USER_CACHE,
key = "'userId:'+ #userId",
cacheManager = "redisCacheManager"
)
@GetMapping("/test/redis")
public String testRedis(Long userId) {
log.info(" redis --- 猜猜我会打印几次:{}", userId);
return "redis:" + userId;
}
}Redission
关于 Redission 的依赖和配置项,请参考此篇文档,保证 Spring 容器中有 RedissonClient 这个 Bean 即可。以下是使用 Redisson 实现注解式缓存的示例。
@Slf4j
@Configuration
@EnableCaching
public class MultiCacheConfig {
@Value("${cache.caffeine.default-spec:maximumSize=1000,expireAfterWrite=600s}")
private String caffeineDefaultSpec;
// 缓存名称常量(统一在全局引用)
public static final String USER_CACHE = "userCache";
public static final String PRODUCT_CACHE = "productCache";
public static final String SESSION_CACHE = "sessionCache";
/**
* Redisson 缓存管理器 - 高级分布式缓存
* 适用于需要 TTL + 最大空闲时间控制的场景
*/
@Bean("redissonCacheManager")
public CacheManager redissonCacheManager(RedissonClient redissonClient) {
log.info("初始化 Redisson Cache Manager...");
Map<String, CacheConfig> config = new HashMap<>();
config.put(USER_CACHE, new CacheConfig(
TimeUnit.MINUTES.toMillis(30), // 30分钟TTL(对应的毫秒数)
TimeUnit.MINUTES.toMillis(15) // 15分钟最大空闲时间
));
config.put(PRODUCT_CACHE, new CacheConfig(
TimeUnit.HOURS.toMillis(2), // 2小时TTL
TimeUnit.MINUTES.toMillis(30) // 30分钟最大空闲时间
));
config.put(SESSION_CACHE, new CacheConfig(
TimeUnit.HOURS.toMillis(24), // 24小时TTL
TimeUnit.HOURS.toMillis(1) // 1小时最大空闲时间
));
// RedissonSpringCacheManager 使用 RMapCache 实现(支持 TTL + Idle)
return new RedissonSpringCacheManager(redissonClient, config);
}
}@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/cache")
public class CacheController {
/**
* 测试 Redisson 缓存
*/
@Cacheable(
value = MultiCacheConfig.USER_CACHE,
key = "'userId:'+ #userId",
cacheManager = "redissonCacheManager"
)
@GetMapping("/test/redisson")
public String testRedisson(Long userId) {
log.info(" Redisson --- 猜猜我会打印几次:{}", userId);
return "Redisson:" + userId;
}
}单元测试
单元测试案例参考
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.TimeUnit;
/**
* 单元测试案例
*/
@Slf4j
@ComponentScan(basePackages = "com.zjx")
@SpringBootTest // 此注解只能在 springboot 主包下使用
public class DemoUnitTest {
private final IMyTestService service;
@Autowired
public DemoUnitTest(IMyTestService service) {
this.service = service;
}
@Test
public void testTest() {
Assertions.assertNotNull(service);
log.info(service.toString());
}
}import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.TimeUnit;
/**
* 单元测试案例
*/
@Slf4j
@ComponentScan(basePackages = "com.zjx")
@SpringBootTest // 此注解只能在 springboot 主包下使用
@DisplayName("单元测试案例")
public class DemoUnitTest {
private final IMyTestService service;
@Autowired
public DemoUnitTest(IMyTestService service) {
this.service = service;
}
@DisplayName("测试 @SpringBootTest @Test @DisplayName 注解")
@Test
public void testTest() {
Assertions.assertNotNull(service);
log.info(service.toString());
}
@Disabled
@DisplayName("测试 @Disabled 注解")
@Test
public void testDisabled() {
log.info("@BeforeAll =====");
}
@Timeout(value = 2L, unit = TimeUnit.SECONDS)
@DisplayName("测试 @Timeout 注解")
@Test
public void testTimeout() throws InterruptedException {
Thread.sleep(3000);
log.info("@Timeout =====");
}
@DisplayName("测试 @RepeatedTest 注解")
@RepeatedTest(3)
public void testRepeatedTest() {
log.info("@RepeatedTest =====");
}
@BeforeAll
public static void testBeforeAll() {
log.info("@BeforeAll =====");
}
@BeforeEach
public void testBeforeEach() {
log.info("@BeforeEach =====");
}
@AfterEach
public void testAfterEach() {
log.info("@AfterEach =====");
}
@AfterAll
public static void testAfterAll() {
log.info("@AfterAll =====");
}
}import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* 断言单元测试案例
*/
@DisplayName("断言单元测试案例")
public class AssertUnitTest {
@DisplayName("测试 assertEquals 方法")
@Test
public void testAssertEquals() {
Assertions.assertEquals("666", new String("666"));
Assertions.assertNotEquals("666", new String("666"));
}
@DisplayName("测试 assertSame 方法")
@Test
public void testAssertSame() {
Object obj = new Object();
Object obj1 = obj;
Assertions.assertSame(obj, obj1);
Assertions.assertNotSame(obj, obj1);
}
@DisplayName("测试 assertTrue 方法")
@Test
public void testAssertTrue() {
Assertions.assertTrue(true);
Assertions.assertFalse(true);
}
@DisplayName("测试 assertNull 方法")
@Test
public void testAssertNull() {
Assertions.assertNull(null);
Assertions.assertNotNull(null);
}
}import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
/**
* 带参数单元测试案例
*/
@Slf4j
@DisplayName("带参数单元测试案例")
public class ParamUnitTest {
@DisplayName("测试 @ValueSource 注解")
@ParameterizedTest
@ValueSource(strings = {"t1", "t2", "t3"})
public void testValueSource(String str) {
log.info(str);
}
@DisplayName("测试 @NullSource 注解")
@ParameterizedTest
@NullSource
public void testNullSource(String str) {
log.info(str);
}
@DisplayName("测试 @EnumSource 注解")
@ParameterizedTest
@EnumSource(MyUserType.class)
public void testEnumSource(MyUserType type) {
log.info(type.getUserType());
}
@DisplayName("测试 @MethodSource 注解")
@ParameterizedTest
@MethodSource("getParam")
public void testMethodSource(String str) {
log.info(str);
}
public static Stream<String> getParam() {
List<String> list = new ArrayList<>();
list.add("t1");
list.add("t2");
list.add("t3");
return list.stream();
}
@BeforeEach
public void testBeforeEach() {
log.info("@BeforeEach =====");
}
@AfterEach
public void testAfterEach() {
log.info("@AfterEach =====");
}
}import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;
/**
* 标签单元测试案例
*/
@SpringBootTest
@DisplayName("标签单元测试案例")
public class TagUnitTest {
@Tag("dev")
@DisplayName("测试 @Tag dev")
@Test
public void testTagDev() {
log.info("dev");
}
@Tag("prod")
@DisplayName("测试 @Tag prod")
@Test
public void testTagProd() {
log.info("prod");
}
@Tag("local")
@DisplayName("测试 @Tag local")
@Test
public void testTagLocal() {
log.info("local");
}
@Tag("exclude")
@DisplayName("测试 @Tag exclude")
@Test
public void testTagExclude() {
log.info("exclude");
}
@BeforeEach
public void testBeforeEach() {
log.info("@BeforeEach =====");
}
@AfterEach
public void testAfterEach() {
log.info("@AfterEach =====");
}
}启用 WebSocket
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>实现方式
方式一(推荐)@Component @ServerEndpoint("/first-endpoint") public class FirstWebSocketEndpoint { @OnOpen public void onOpen(Session session) { // 连接建立时执行 } @OnMessage public void onMessage(String message, Session session) { // 收到消息时执行 } @OnClose public void onClose(Session session) { // 连接关闭时执行 } @OnError public void onError(Session session, Throwable error) { // 发生错误时执行 } }方式一配置类@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }方式二// 启用 Spring WebSocket 支持 @Configuration @EnableWebSocket @RequiredArgsConstructor public class WebSocketConfig implements WebSocketConfigurer { private final FirstWebSocketHandler myFirstHandler; private final SecondWebSocketHandler mySecondHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myFirstHandler, "/first-websocket") .setAllowedOrigins("*"); registry.addHandler(mySecondHandler, "/second-websocket") .setAllowedOrigins("*"); } }方式二配置类@Slf4j @Component public class FirstWebSocketHandler extends TextWebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 连接建立时的逻辑 session.sendMessage(new TextMessage("已连接到第一个 WebSocket 服务")); log.info("新连接建立 sessionId={}, url 参数:{}", session.getId(), session.getAttributes().toString()); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 处理收到的文本消息 String payload = message.getPayload(); session.sendMessage(new TextMessage("收到你的消息:" + payload)); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { // 连接关闭时的逻辑 System.out.println("第一个 WebSocket 连接已关闭:" + session.getId()); } }
解决跨越问题
使用 @CrossOrigin 注解(不推荐)
在控制器方法或类上添加 @CrossOrigin 注解,适用于小型项目或单个接口的跨域配置
@RestController @GetMapping("/api") @CrossOrigin(origins = "https://example.com", maxAge = 3600) public class MyController { @GetMapping("/resource") public ResponseEntity<?> getResource() { ... } }全局配置(推荐)
通过 WebMvcConfigurer 实现统一配置,适合管理所有端点的 CORS 规则。适用于未接入其他安全框架(如Spring Security)。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 所有接口 .allowedOrigins("https://trusted-domain.com", "https://another-domain.com") // 严格指定可信域名,建议生产环境使用 // .allowedOriginPatterns("https://*.trusted-domain.com", "https://another-domain.com") // 通配符指定可信域名,生产环境需注意是否存在安全风险 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的 HTTP 方法 .allowedHeaders("*") // 允许所有请求头 .allowCredentials(true) // 允许携带 Cookie .maxAge(3600); // 1 小时内不需要预检请求 } }Spring Security 集成配置
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) // ... 其他安全配置 return http.build(); } // CORS 配置源 CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(Arrays.asList("https://trusted-domain.com")); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }

