Spring Boot
Spring Boot 是一个开源的 Java 基础框架,旨在简化 Spring 应用的创建和开发过程。它由 Pivotal 团队(现为 VMware)开发,是 Spring 平台和第三方库的集成,提供了一个快速且广泛接受的方式来构建 Spring 应用。Spring Boot 使得设置和配置 Spring 应用变得简单,主要通过约定优于配置的原则,减少了项目的样板代码。

查看更多相关内容
Spring Boot 的启动流程是怎样的?## Spring Boot 启动流程详解
### 入口方法
```java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
### 启动流程总览
```
SpringApplication.run()
├── 1. 创建 SpringApplication 实例
│ ├── 推断应用类型 (Servlet/Reactive/None)
│ ├── 加载 BootstrapRegistryInitializer
│ ├── 加载 ApplicationContextInitializer
│ └── 加载 ApplicationListener
│
└── 2. 执行 run() 方法
├── 2.1 启动计时器
├── 2.2 创建 DefaultBootstrapContext
├── 2.3 配置 headless 模式
├── 2.4 发布 Starting 事件
├── 2.5 准备 Environment
├── 2.6 打印 Banner
├── 2.7 创建 ApplicationContext
├── 2.8 准备 ApplicationContext
├── 2.9 刷新 ApplicationContext
├── 2.10 执行 Runner
└── 2.11 发布 Started 事件
```
### 详细启动步骤
#### 第一步:创建 SpringApplication 实例
```java
public SpringApplication(Class<?>... primarySources) {
// 资源加载器
this.resourceLoader = null;
// 主配置类
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 推断应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 加载 BootstrapRegistryInitializer
this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
// 加载 ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 加载 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 推断主类
this.mainApplicationClass = deduceMainApplicationClass();
}
```
**应用类型推断逻辑:**
```java
static WebApplicationType deduceFromClasspath() {
// 存在 reactor.netty.http.server.HttpServer 且不存在
// org.springframework.web.servlet.DispatcherServlet
if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
&& !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
// 存在 javax.servlet.Servlet 或 jakarta.servlet.Servlet
for (String className : SERVLET_INDICATOR_CLASSES) {
if (ClassUtils.isPresent(className, null)) {
return WebApplicationType.SERVLET;
}
}
return WebApplicationType.NONE;
}
```
#### 第二步:执行 run() 方法核心流程
```java
public ConfigurableApplicationContext run(String... args) {
// 1. 启动计时器
StartupStep startupStep = this.applicationStartup.start("spring.boot.application.starting");
// 2. 创建 BootstrapContext
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
// 3. 配置 headless 模式
configureHeadlessProperty();
// 4. 获取 SpringApplicationRunListeners
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 5. 准备 ApplicationArguments
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 6. 准备 Environment
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
// 7. 打印 Banner
Banner printedBanner = printBanner(environment);
// 8. 创建 ApplicationContext
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 9. 准备 ApplicationContext
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 10. 刷新 ApplicationContext(核心)
refreshContext(context);
// 11. 刷新后处理
afterRefresh(context, applicationArguments);
// 12. 启动完成
listeners.started(context);
// 13. 执行 Runner
callRunners(context, applicationArguments);
} catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
} catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
```
#### 第三步:准备 Environment
```java
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext,
ApplicationArguments applicationArguments) {
// 创建或获取 Environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置 PropertySources 和 Profiles
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 发布 EnvironmentPrepared 事件
listeners.environmentPrepared(bootstrapContext, environment);
// 绑定到 SpringApplication
ConfigurationPropertySources.attach(environment);
return environment;
}
```
**配置文件加载顺序(优先级从高到低):**
1. 命令行参数
2. `java:comp/env` 的 JNDI 属性
3. Java 系统属性(System.getProperties())
4. 操作系统环境变量
5. `RandomValuePropertySource`
6. jar 包外部的 `application-{profile}.properties`
7. jar 包内部的 `application-{profile}.properties`
8. jar 包外部的 `application.properties`
9. jar 包内部的 `application.properties`
10. @PropertySource 注解定义的属性
11. 默认属性(SpringApplication.setDefaultProperties)
#### 第四步:创建 ApplicationContext
```java
protected ConfigurableApplicationContext createApplicationContext() {
return this.applicationContextFactory.apply(this.webApplicationType);
}
// 根据应用类型创建不同的上下文
// Servlet -> AnnotationConfigServletWebServerApplicationContext
// Reactive -> AnnotationConfigReactiveWebServerApplicationContext
// None -> AnnotationConfigApplicationContext
```
#### 第五步:准备 ApplicationContext
```java
private void prepareContext(DefaultBootstrapContext bootstrapContext,
ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置 Environment
context.setEnvironment(environment);
// 应用后处理器
postProcessApplicationContext(context);
// 执行 ApplicationContextInitializer
applyInitializers(context);
// 发布 ContextPrepared 事件
listeners.contextPrepared(context);
// 注册 Spring Boot 特殊 Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 加载主配置类
Set<Object> sources = getAllSources();
load(context, sources.toArray(new Object[0]));
// 发布 ContextLoaded 事件
listeners.contextLoaded(context);
}
```
#### 第六步:刷新 ApplicationContext(核心)
```java
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
// 注册 ShutdownHook
if (this.registerShutdownHook) {
shutdownHook.registerApplicationContext(context);
}
}
// 调用 AbstractApplicationContext.refresh()
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 1. 准备刷新
prepareRefresh();
// 2. 获取 BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3. 准备 BeanFactory
prepareBeanFactory(beanFactory);
try {
// 4. 子类扩展
postProcessBeanFactory(beanFactory);
// 5. 执行 BeanFactoryPostProcessor
invokeBeanFactoryPostProcessors(beanFactory);
// 6. 注册 BeanPostProcessor
registerBeanPostProcessors(beanFactory);
// 7. 初始化 MessageSource
initMessageSource();
// 8. 初始化事件广播器
initApplicationEventMulticaster();
// 9. 子类扩展(WebServer 在此初始化)
onRefresh();
// 10. 注册监听器
registerListeners();
// 11. 初始化所有非懒加载单例 Bean
finishBeanFactoryInitialization(beanFactory);
// 12. 完成刷新
finishRefresh();
} catch (BeansException ex) {
destroyBeans();
closeBeanFactory();
throw ex;
}
}
}
```
**WebServer 启动时机:**
```java
// ServletWebServerApplicationContext.onRefresh()
@Override
protected void onRefresh() {
super.onRefresh();
try {
// 创建并启动 WebServer(Tomcat/Jetty/Undertow)
createWebServer();
} catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
```
#### 第七步:执行 Runner
```java
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
// 获取所有 ApplicationRunner
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
// 获取所有 CommandLineRunner
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
// 排序并执行
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
```
### 启动事件监听
```java
// 可以通过实现 ApplicationListener 监听启动过程
@Component
public class MyApplicationListener implements ApplicationListener<ApplicationStartedEvent> {
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
System.out.println("Application started!");
}
}
```
**主要事件类型:**
| 事件 | 触发时机 |
|------|---------|
| `ApplicationStartingEvent` | run 方法开始执行时 |
| `ApplicationEnvironmentPreparedEvent` | Environment 准备完成时 |
| `ApplicationContextInitializedEvent` | Context 初始化完成时 |
| `ApplicationPreparedEvent` | Context 准备完成时 |
| `ApplicationStartedEvent` | Context 刷新完成时 |
| `ApplicationReadyEvent` | 所有 Runner 执行完成时 |
| `ApplicationFailedEvent` | 启动失败时 |
### 总结
Spring Boot 启动流程可以概括为:
1. **实例化阶段**:推断应用类型,加载初始化器和监听器
2. **环境准备阶段**:准备 Environment,加载配置文件
3. **上下文创建阶段**:创建并配置 ApplicationContext
4. **刷新阶段**:加载 Bean 定义,初始化 Bean,启动 WebServer
5. **启动完成阶段**:执行 Runner,发布启动完成事件
理解启动流程有助于排查启动问题、自定义启动逻辑和优化启动性能。
服务端 · 3月7日 12:04
Spring Boot 中如何实现安全认证(Spring Security)?## Spring Boot + Spring Security 安全认证详解
### Spring Security 核心功能
- **认证(Authentication)**:验证用户身份
- **授权(Authorization)**:控制用户访问权限
- **防护(Protection)**:CSRF、会话固定等攻击防护
- **加密(Encryption)**:密码加密存储
### 基础集成
#### 1. 添加依赖
```xml
<dependencies>
<!-- Spring Security Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 支持(可选) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.3</version>
</dependency>
<!-- OAuth2 支持(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
</dependencies>
```
#### 2. 基础安全配置
```java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(仅用于 API 开发,Web 应用需启用)
.csrf(csrf -> csrf.disable())
// 配置授权规则
.authorizeHttpRequests(auth -> auth
// 公开路径
.requestMatchers("/", "/login", "/register", "/public/**").permitAll()
// 静态资源
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
// API 文档
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// 管理员路径
.requestMatchers("/admin/**").hasRole("ADMIN")
// 用户路径
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 其他需要认证
.anyRequest().authenticated()
)
// 表单登录配置
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/home", false)
.failureUrl("/login?error=true")
.permitAll()
)
// 注销配置
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
)
// 会话管理
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
// 使用 BCrypt 加密
return new BCryptPasswordEncoder();
}
}
```
### 基于内存的用户认证
```java
@Configuration
public class InMemoryUserConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(new BCryptPasswordEncoder().encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(new BCryptPasswordEncoder().encode("admin"))
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
```
### 基于数据库的用户认证
#### 1. 用户实体类
```java
@Entity
@Table(name = "users")
@Data
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(unique = true, nullable = false)
private String email;
private boolean enabled = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
@Entity
@Table(name = "roles")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
}
```
#### 2. 自定义 UserDetailsService
```java
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().stream()
.map(Role::getName)
.toArray(String[]::new))
.disabled(!user.isEnabled())
.build();
}
}
```
#### 3. 更新 Security 配置
```java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout.permitAll())
.userDetailsService(userDetailsService);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
```
### JWT 认证实现
#### 1. JWT 工具类
```java
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
private final SecretKey key;
public JwtTokenProvider() {
this.key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
}
/**
* 生成 JWT Token
*/
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key)
.compact();
}
/**
* 从 Token 获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
/**
* 验证 Token
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
```
#### 2. JWT 认证过滤器
```java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
```
#### 3. JWT Security 配置
```java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class JwtSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
```
#### 4. 认证 Controller
```java
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider tokenProvider;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthResponse(jwt));
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(@RequestBody RegisterRequest request) {
if (userRepository.existsByUsername(request.getUsername())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "Username already taken"));
}
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPassword(passwordEncoder.encode(request.getPassword()));
userRepository.save(user);
return ResponseEntity.ok(new ApiResponse(true, "User registered successfully"));
}
}
```
### 方法级安全控制
```java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/me")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<User> getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok(userService.findByUsername(userDetails.getUsername()));
}
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @userSecurity.hasUserId(authentication, #id)")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok().build();
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
}
// 自定义权限判断
@Component("userSecurity")
public class UserSecurity {
public boolean hasUserId(Authentication authentication, Long userId) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
User user = userRepository.findByUsername(userDetails.getUsername())
.orElse(null);
return user != null && user.getId().equals(userId);
}
}
```
### OAuth2 / OIDC 集成
```java
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home")
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(userAuthoritiesMapper())
)
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
```
### 安全配置
```yaml
spring:
security:
user:
name: admin
password: admin
roles: ADMIN
oauth2:
client:
registration:
google:
client-id: your-client-id
client-secret: your-client-secret
scope:
- email
- profile
jwt:
secret: your-secret-key-here-must-be-at-least-256-bits-long
expiration: 86400000 # 24 hours
```
### 测试安全配置
```java
@SpringBootTest
@AutoConfigureMockMvc
public class SecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(username = "user", roles = "USER")
public void testUserAccess() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
public void testAdminAccess() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "user", roles = "USER")
public void testUserCannotAccessAdmin() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
public void testPublicAccess() throws Exception {
mockMvc.perform(get("/public/info"))
.andExpect(status().isOk());
}
@Test
public void testProtectedRequiresAuth() throws Exception {
mockMvc.perform(get("/api/protected"))
.andExpect(status().isUnauthorized());
}
}
```
### 总结
| 认证方式 | 适用场景 | 优点 | 缺点 |
|---------|---------|------|------|
| Session | 传统 Web 应用 | 简单、成熟 | 不适合分布式 |
| JWT | REST API、移动端 | 无状态、可扩展 | Token 无法撤销 |
| OAuth2 | 第三方登录 | 标准化、安全 | 实现复杂 |
| LDAP/AD | 企业环境 | 集中管理 | 需要 LDAP 服务器 |
安全建议:
1. 始终使用 HTTPS
2. 密码使用 BCrypt 加密
3. 启用 CSRF 防护(Web 应用)
4. 设置合理的会话超时
5. 实现密码强度校验
6. 添加登录失败锁定机制
服务端 · 3月6日 21:59
Spring Boot Starter 依赖的作用和原理是什么?## Spring Boot Starter 详解
### 什么是 Starter
Spring Boot Starter 是一组**预定义的依赖描述符**,它整合了某个功能所需的全部依赖,开发者只需引入一个 starter,即可获得完整的功能支持。
### Starter 的命名规范
| 类型 | 命名规则 | 示例 |
|------|---------|------|
| 官方 Starter | `spring-boot-starter-*` | spring-boot-starter-web |
| 第三方 Starter | `*-spring-boot-starter` | mybatis-spring-boot-starter |
### 常见官方 Starter 列表
```xml
<!-- Web 应用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据访问 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 安全框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
```
### Starter 的工作原理
#### 1. 依赖传递机制
Starter 本质上是一个 Maven/Gradle 项目,通过 `pom.xml` 定义了功能所需的全部依赖:
```xml
<!-- spring-boot-starter-web 的 pom.xml -->
<dependencies>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<!-- Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
```
#### 2. 版本管理
Spring Boot 通过 **Parent POM** 统一管理依赖版本:
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
```
在 `spring-boot-dependencies` 中定义了所有依赖的版本号:
```xml
<properties>
<spring-framework.version>6.1.1</spring-framework.version>
<tomcat.version>10.1.16</tomcat.version>
<jackson.version>2.16.0</jackson.version>
</properties>
```
#### 3. 自动配置激活
Starter 通常配合自动配置使用,在 `META-INF/spring.factories` 中注册:
```properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
```
### 自定义 Starter 开发
#### 步骤一:创建 Maven 项目
```xml
<artifactId>my-spring-boot-starter</artifactId>
<dependencies>
<!-- 自动配置依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 配置处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
```
#### 步骤二:创建属性配置类
```java
@ConfigurationProperties(prefix = "my.service")
public class MyServiceProperties {
private String host = "localhost";
private int port = 8080;
private boolean enabled = true;
// getters and setters
}
```
#### 步骤三:创建服务类
```java
public class MyService {
private final MyServiceProperties properties;
public MyService(MyServiceProperties properties) {
this.properties = properties;
}
public String connect() {
return "Connecting to " + properties.getHost() + ":" + properties.getPort();
}
}
```
#### 步骤四:创建自动配置类
```java
@Configuration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyServiceProperties.class)
@ConditionalOnProperty(prefix = "my.service", value = "enabled", matchIfMissing = true)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyServiceProperties properties) {
return new MyService(properties);
}
}
```
#### 步骤五:注册自动配置
在 `src/main/resources/META-INF/spring.factories` 中添加:
```properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.mystarter.MyServiceAutoConfiguration
```
Spring Boot 2.7+ 推荐使用 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`:
```
com.example.mystarter.MyServiceAutoConfiguration
```
#### 步骤六:打包发布
```bash
mvn clean install
```
### Starter 使用示例
```xml
<dependency>
<groupId>com.example</groupId>
<artifactId>my-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
```
```yaml
my:
service:
host: 192.168.1.100
port: 9090
```
```java
@Service
public class MyApplicationService {
@Autowired
private MyService myService;
public void doSomething() {
String result = myService.connect();
System.out.println(result);
}
}
```
### Starter 的优势
1. **简化依赖管理**:一个依赖引入全部所需
2. **版本兼容性**:Spring Boot 统一管理版本
3. **开箱即用**:配合自动配置,零配置启动
4. **可扩展性**:易于自定义和扩展
### 总结
Spring Boot Starter 是**依赖聚合**和**自动配置**的结合体,它通过 Maven/Gradle 的依赖传递机制简化依赖引入,配合自动配置实现功能的即插即用,是 Spring Boot 简化开发的核心机制之一。
服务端 · 3月6日 21:58
Spring Boot 中如何实现全局异常处理?## Spring Boot 全局异常处理详解
### 为什么需要全局异常处理
在 Web 应用中,异常处理是必不可少的:
- **用户体验**:友好的错误提示而非堆栈信息
- **系统安全**:隐藏内部实现细节
- **代码整洁**:避免每个方法都写 try-catch
- **统一规范**:统一的错误响应格式
### 实现方式一:@ControllerAdvice + @ExceptionHandler(推荐)
#### 1. 基础异常处理类
```java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Result.error(e.getCode(), e.getMessage()));
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<Void>> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
log.warn("参数校验失败: {}", message);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Result.error(400, message));
}
/**
* 处理参数绑定异常
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<Result<Void>> handleBindException(BindException e) {
String message = e.getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Result.error(400, message));
}
/**
* 处理缺少请求参数异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Result<Void>> handleMissingParam(MissingServletRequestParameterException e) {
String message = "缺少必要参数: " + e.getParameterName();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Result.error(400, message));
}
/**
* 处理请求方法不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Result<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
String message = "请求方法不支持: " + e.getMethod();
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(Result.error(405, message));
}
/**
* 处理资源未找到异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<Result<Void>> handleNoHandlerFound(NoHandlerFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Result.error(404, "请求路径不存在"));
}
/**
* 处理空指针异常
*/
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<Result<Void>> handleNullPointer(NullPointerException e) {
log.error("空指针异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error(500, "系统内部错误"));
}
/**
* 处理其他所有异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error(500, "系统繁忙,请稍后重试"));
}
}
```
#### 2. 统一响应结果类
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public Result() {
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
public static <T> Result<T> error(String message) {
return error(500, message);
}
}
```
#### 3. 自定义业务异常
```java
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(String message) {
super(message);
this.code = 500;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
}
```
#### 4. 错误码枚举
```java
@Getter
@AllArgsConstructor
public enum ErrorCode {
SUCCESS(200, "操作成功"),
PARAM_ERROR(400, "参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
INTERNAL_ERROR(500, "系统内部错误"),
// 业务错误码
USER_NOT_FOUND(1001, "用户不存在"),
USER_ALREADY_EXISTS(1002, "用户已存在"),
PASSWORD_ERROR(1003, "密码错误"),
ACCOUNT_LOCKED(1004, "账户已锁定"),
ORDER_NOT_FOUND(2001, "订单不存在"),
ORDER_STATUS_ERROR(2002, "订单状态错误"),
INSUFFICIENT_STOCK(2003, "库存不足");
private final Integer code;
private final String message;
}
```
### 实现方式二:实现 HandlerExceptionResolver 接口
```java
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
PrintWriter writer = response.getWriter();
Result<Void> result;
if (ex instanceof BusinessException) {
BusinessException be = (BusinessException) ex;
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
result = Result.error(be.getCode(), be.getMessage());
} else {
result = Result.error(500, "系统错误");
}
writer.write(JSON.toJSONString(result));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
```
### 实现方式三:@ResponseStatus 注解
```java
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidParameterException extends RuntimeException {
public InvalidParameterException(String message) {
super(message);
}
}
```
### 参数校验异常处理(详细)
#### 1. 实体类添加校验注解
```java
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$",
message = "密码必须包含大小写字母和数字,且至少8位")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
```
#### 2. 分组校验
```java
public interface CreateGroup {}
public interface UpdateGroup {}
@Data
public class UserDTO {
@NotNull(groups = UpdateGroup.class, message = "ID不能为空")
private Long id;
@NotBlank(groups = CreateGroup.class, message = "用户名不能为空")
private String username;
@NotBlank(groups = CreateGroup.class, message = "密码不能为空")
private String password;
}
```
#### 3. Controller 使用
```java
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public Result<Void> create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) {
// 创建用户
return Result.success();
}
@PutMapping
public Result<Void> update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) {
// 更新用户
return Result.success();
}
}
```
#### 4. 详细的校验异常处理
```java
@RestControllerAdvice
@Slf4j
public class ValidationExceptionHandler {
/**
* 处理 @Valid 校验失败
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<Void>> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(error -> {
String fieldName = error.getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
log.warn("参数校验失败: {}", errors);
return ResponseEntity.badRequest()
.body(Result.error(400, "参数校验失败: " + errors));
}
/**
* 处理 @Validated 校验失败(@RequestParam)
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Result<Void>> handleConstraintViolation(ConstraintViolationException e) {
String message = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest()
.body(Result.error(400, message));
}
/**
* 处理参数类型不匹配
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Result<Void>> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
String message = String.format("参数 '%s' 类型不匹配,期望类型: %s",
e.getName(), e.getRequiredType().getSimpleName());
return ResponseEntity.badRequest()
.body(Result.error(400, message));
}
/**
* 处理缺少请求体
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Result<Void>> handleMissingBody(HttpMessageNotReadableException e) {
return ResponseEntity.badRequest()
.body(Result.error(400, "请求体不能为空或格式错误"));
}
}
```
### 404 异常处理配置
Spring Boot 默认不会抛出 404 异常,需要手动开启:
```yaml
spring:
web:
resources:
add-mappings: false
mvc:
throw-exception-if-no-handler-found: true
```
```java
@RestControllerAdvice
public class NotFoundExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<Void> handleNoHandlerFound(NoHandlerFoundException e) {
return Result.error(404, "请求路径不存在: " + e.getRequestURL());
}
}
```
### 日志记录最佳实践
```java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e, WebRequest request) {
// 记录警告日志
log.warn("业务异常 [{}] - URL: {}, Message: {}",
e.getCode(),
request.getDescription(false),
e.getMessage());
return ResponseEntity.badRequest()
.body(Result.error(e.getCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleException(Exception e, WebRequest request) {
// 生成错误追踪ID
String traceId = UUID.randomUUID().toString();
// 记录错误日志,包含堆栈信息
log.error("系统异常 [TraceId: {}] - URL: {}",
traceId,
request.getDescription(false),
e);
// 返回给客户端时隐藏详细错误信息
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error(500, "系统繁忙,请稍后重试 [TraceId: " + traceId + "]"));
}
}
```
### 国际化错误消息
```java
@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
@Bean
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
}
```
```properties
# messages.properties
error.user.notfound=User not found
error.user.exists=User already exists
# messages_zh_CN.properties
error.user.notfound=用户不存在
error.user.exists=用户已存在
```
```java
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e, Locale locale) {
String message = messageSource.getMessage(e.getMessageKey(), null, locale);
return ResponseEntity.badRequest()
.body(Result.error(e.getCode(), message));
}
```
### 测试验证
```java
@SpringBootTest
@AutoConfigureMockMvc
public class GlobalExceptionHandlerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testBusinessException() throws Exception {
mockMvc.perform(get("/test/business-error"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(400))
.andExpect(jsonPath("$.message").value("业务错误"));
}
@Test
public void testValidationException() throws Exception {
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\": \"ab\", \"email\": \"invalid\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(400));
}
@Test
public void testNotFound() throws Exception {
mockMvc.perform(get("/non-existent-path"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(404));
}
}
```
### 总结
| 方式 | 适用场景 | 优点 | 缺点 |
|------|---------|------|------|
| @ControllerAdvice | 大多数场景 | 集中管理、灵活 | 无 |
| HandlerExceptionResolver | 需要完全控制 | 最高优先级 | 较复杂 |
| @ResponseStatus | 简单异常 | 简洁 | 不够灵活 |
全局异常处理是构建健壮 REST API 的基础,建议:
1. 统一错误响应格式
2. 定义清晰的错误码体系
3. 合理记录日志(区分 warn/error)
4. 生产环境隐藏敏感信息
服务端 · 3月6日 21:58
Spring Boot 中如何实现多环境配置?## Spring Boot 多环境配置详解
### 为什么需要多环境配置
在实际项目开发中,通常需要部署到不同环境:
- **开发环境 (dev)**:本地开发调试
- **测试环境 (test)**:QA 测试验证
- **预发布环境 (staging)**:上线前最终验证
- **生产环境 (prod)**:正式对外服务
不同环境需要不同的配置:数据库连接、日志级别、服务端点等。
### 实现方式一:Profile-specific 配置文件
#### 1. 配置文件命名规则
```
application.yml # 默认配置(所有环境共享)
application-dev.yml # 开发环境配置
application-test.yml # 测试环境配置
application-staging.yml # 预发布环境配置
application-prod.yml # 生产环境配置
```
#### 2. 配置文件示例
**application.yml(默认配置)**
```yaml
spring:
application:
name: my-service
profiles:
active: dev # 默认激活 dev 环境
server:
port: 8080
# 公共配置
logging:
level:
root: INFO
```
**application-dev.yml(开发环境)**
```yaml
# 开发环境特定配置
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/dev_db
username: dev_user
password: dev_pass
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
logging:
level:
com.example: DEBUG
org.springframework.jdbc: DEBUG
```
**application-prod.yml(生产环境)**
```yaml
# 生产环境特定配置
server:
port: 80
spring:
datasource:
url: jdbc:mysql://prod-db.example.com:3306/prod_db
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 50
minimum-idle: 10
jpa:
hibernate:
ddl-auto: validate
show-sql: false
logging:
level:
root: WARN
com.example: INFO
file:
name: /var/log/my-service/app.log
```
### 实现方式二:单文件多文档块
Spring Boot 2.4+ 支持在一个 YAML 文件中定义多个 Profile:
```yaml
# application.yml
spring:
application:
name: my-service
---
spring:
config:
activate:
on-profile: dev
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/dev_db
username: dev_user
password: dev_pass
---
spring:
config:
activate:
on-profile: prod
server:
port: 80
spring:
datasource:
url: jdbc:mysql://prod-db.example.com:3306/prod_db
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
```
### 激活 Profile 的方式
#### 方式1:配置文件指定(application.yml)
```yaml
spring:
profiles:
active: dev
```
#### 方式2:命令行参数
```bash
# 启动时指定环境
java -jar myapp.jar --spring.profiles.active=prod
# 或者
java -jar myapp.jar -Dspring.profiles.active=prod
```
#### 方式3:环境变量
```bash
# Linux/Mac
export SPRING_PROFILES_ACTIVE=prod
java -jar myapp.jar
# Windows
set SPRING_PROFILES_ACTIVE=prod
java -jar myapp.jar
```
#### 方式4:JVM 系统属性
```bash
java -Dspring.profiles.active=prod -jar myapp.jar
```
#### 方式5:IDE 配置
**IntelliJ IDEA:**
- Run → Edit Configurations → Active profiles: `dev`
### Profile 分组(Spring Boot 2.4+)
可以将多个 Profile 组合成一个组:
```yaml
spring:
profiles:
group:
local:
- dev
- local-db
- local-cache
production:
- prod
- prod-db
- prod-cache
- prod-mq
```
使用:
```bash
java -jar myapp.jar --spring.profiles.active=local
# 实际激活: dev, local-db, local-cache
```
### 条件化 Bean 配置
使用 `@Profile` 注解根据环境创建不同的 Bean:
```java
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/dev_db");
config.setUsername("dev_user");
config.setPassword("dev_pass");
return new HikariDataSource(config);
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://prod-db.example.com:3306/prod_db");
config.setUsername(System.getenv("DB_USERNAME"));
config.setPassword(System.getenv("DB_PASSWORD"));
config.setMaximumPoolSize(50);
return new HikariDataSource(config);
}
@Bean
@Profile("!prod") // 非生产环境
public MockService mockService() {
return new MockService();
}
}
```
### 在代码中获取当前 Profile
```java
@Component
public class EnvironmentChecker implements CommandLineRunner {
@Autowired
private Environment environment;
@Override
public void run(String... args) {
// 获取所有激活的 profiles
String[] activeProfiles = environment.getActiveProfiles();
System.out.println("Active profiles: " + String.join(", ", activeProfiles));
// 判断是否包含某个 profile
if (environment.acceptsProfiles(Profiles.of("dev"))) {
System.out.println("Development mode");
}
// 判断是否生产环境
boolean isProd = Arrays.asList(activeProfiles).contains("prod");
}
}
```
### 配置文件加载顺序与优先级
Spring Boot 配置文件加载优先级(从高到低):
1. 命令行参数
2. `java:comp/env` JNDI 属性
3. Java 系统属性 (`System.getProperties()`)
4. 操作系统环境变量
5. `RandomValuePropertySource`
6. jar 包外部的 `application-{profile}.properties`
7. jar 包内部的 `application-{profile}.properties`
8. jar 包外部的 `application.properties`
9. jar 包内部的 `application.properties`
10. @PropertySource 注解
11. SpringApplication 默认属性
**重要规则**:
- `application-{profile}.yml` 会覆盖 `application.yml` 中的同名配置
- 高优先级的配置会覆盖低优先级的同名配置
### 敏感信息加密
生产环境密码等敏感信息不应明文存储:
**方案1:环境变量**
```yaml
spring:
datasource:
password: ${DB_PASSWORD}
```
**方案2:Jasypt 加密**
```xml
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
```
```yaml
spring:
datasource:
password: ENC(加密后的密文)
jasypt:
encryptor:
password: ${JASYPT_ENCRYPTOR_PASSWORD}
```
### 最佳实践
#### 1. 配置分层结构
```yaml
# application.yml - 只放真正公共的配置
spring:
application:
name: my-service
jackson:
date-format: yyyy-MM-dd HH:mm:ss
---
# 各环境特定配置放在单独文件
```
#### 2. 使用配置属性类
```java
@ConfigurationProperties(prefix = "app")
@Component
public class AppProperties {
private String name;
private String version;
private Map<String, String> metadata;
// getters and setters
}
```
#### 3. 生产环境配置检查清单
- [ ] 关闭调试模式:`debug: false`
- [ ] 关闭 SQL 打印:`show-sql: false`
- [ ] 设置合适的日志级别
- [ ] 配置连接池参数
- [ ] 使用环境变量存储敏感信息
- [ ] 配置健康检查端点
- [ ] 设置合理的超时参数
#### 4. 本地开发便利配置
```yaml
# application-local.yml
spring:
devtools:
restart:
enabled: true
livereload:
enabled: true
h2:
console:
enabled: true
path: /h2-console
```
### 常见问题
**Q1: Profile 不生效?**
- 检查配置文件命名是否正确:`application-{profile}.yml`
- 确认 Profile 已正确激活
- 检查是否有拼写错误
**Q2: 多个 Profile 如何同时激活?**
```bash
--spring.profiles.active=dev,local-db
```
**Q3: 如何设置默认 Profile?**
```yaml
spring:
profiles:
default: dev # 当没有指定时默认使用
```
### 总结
| 特性 | 说明 |
|------|------|
| 配置文件分离 | `application-{profile}.yml` |
| 单文件多文档 | 使用 `---` 分隔 |
| 激活方式 | 配置、命令行、环境变量、JVM 参数 |
| 条件 Bean | `@Profile` 注解 |
| Profile 分组 | `spring.profiles.group` |
| 优先级 | 命令行 > 环境变量 > 配置文件 |
服务端 · 3月6日 21:58
Spring Boot 中如何实现异步编程?## Spring Boot 异步编程详解
### 为什么需要异步编程
- **提高吞吐量**:不阻塞主线程,处理更多请求
- **改善响应时间**:耗时操作后台执行,快速响应用户
- **资源利用**:充分利用 CPU 多核特性
- **解耦操作**:发送消息、记录日志等非核心操作异步化
### 实现方式一:@Async 注解(最常用)
#### 1. 开启异步支持
```java
@Configuration
@EnableAsync
public class AsyncConfig {
/**
* 自定义线程池
*/
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(5);
// 最大线程数
executor.setMaxPoolSize(20);
// 队列容量
executor.setQueueCapacity(100);
// 线程名称前缀
executor.setThreadNamePrefix("async-task-");
// 拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务完成后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
/**
* 邮件发送专用线程池
*/
@Bean("mailExecutor")
public Executor mailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("mail-task-");
executor.initialize();
return executor;
}
}
```
#### 2. 使用 @Async 注解
```java
@Service
@Slf4j
public class AsyncService {
/**
* 默认线程池执行
*/
@Async
public void sendNotification(String userId, String message) {
log.info("发送通知给用户: {}, 线程: {}", userId, Thread.currentThread().getName());
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("通知发送完成: {}", userId);
}
/**
* 指定线程池
*/
@Async("mailExecutor")
public void sendEmail(String to, String subject, String content) {
log.info("发送邮件到: {}, 线程: {}", to, Thread.currentThread().getName());
// 邮件发送逻辑
}
/**
* 带返回值的异步方法
*/
@Async
public CompletableFuture<String> processDataAsync(String data) {
log.info("异步处理数据: {}, 线程: {}", data, Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return CompletableFuture.completedFuture("处理结果: " + data);
}
/**
* 批量异步处理
*/
@Async
public CompletableFuture<Integer> calculateAsync(int number) {
int result = number * number;
return CompletableFuture.completedFuture(result);
}
}
```
#### 3. 调用异步方法
```java
@RestController
@RequestMapping("/async")
@RequiredArgsConstructor
public class AsyncController {
private final AsyncService asyncService;
@GetMapping("/notify")
public Result<Void> sendNotification(@RequestParam String userId) {
// 立即返回,不等待异步操作完成
asyncService.sendNotification(userId, "您有一条新消息");
return Result.success("通知发送中");
}
@GetMapping("/process")
public Result<String> processData(@RequestParam String data) throws Exception {
// 调用异步方法并等待结果
CompletableFuture<String> future = asyncService.processDataAsync(data);
// 设置超时
String result = future.get(5, TimeUnit.SECONDS);
return Result.success(result);
}
@GetMapping("/batch")
public Result<List<Integer>> batchProcess() throws Exception {
// 批量异步处理
List<CompletableFuture<Integer>> futures = IntStream.range(1, 11)
.mapToObj(asyncService::calculateAsync)
.collect(Collectors.toList());
// 等待所有任务完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 获取所有结果
allFutures.join();
List<Integer> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
return Result.success(results);
}
}
```
### 实现方式二:CompletableFuture(Java 8+)
```java
@Service
@Slf4j
public class CompletableFutureService {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
/**
* 异步执行并组合结果
*/
public CompletableFuture<String> combineAsyncTasks() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
log.info("执行任务1");
sleep(1000);
return "结果1";
}, executor);
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
log.info("执行任务2");
sleep(1500);
return "结果2";
}, executor);
// 组合两个任务的结果
return task1.thenCombine(task2, (result1, result2) -> {
return result1 + " + " + result2;
});
}
/**
* 异步任务链
*/
public CompletableFuture<String> taskChain(String input) {
return CompletableFuture.supplyAsync(() -> {
log.info("步骤1: 处理输入");
return input.toUpperCase();
}, executor)
.thenApplyAsync(result -> {
log.info("步骤2: 添加前缀");
return "PREFIX_" + result;
}, executor)
.thenApplyAsync(result -> {
log.info("步骤3: 添加后缀");
return result + "_SUFFIX";
}, executor)
.exceptionally(ex -> {
log.error("任务链异常: {}", ex.getMessage());
return "DEFAULT_VALUE";
});
}
/**
* 并行处理多个任务
*/
public List<String> parallelProcess(List<String> inputs) {
List<CompletableFuture<String>> futures = inputs.stream()
.map(input -> CompletableFuture.supplyAsync(() -> processSingle(input), executor))
.collect(Collectors.toList());
// 等待所有完成并收集结果
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
private String processSingle(String input) {
sleep(500);
return "Processed: " + input;
}
/**
* 带超时的异步操作
*/
public String asyncWithTimeout() {
try {
return CompletableFuture.supplyAsync(() -> {
sleep(5000); // 模拟耗时操作
return "结果";
}, executor)
.orTimeout(3, TimeUnit.SECONDS) // Java 9+
.exceptionally(ex -> "超时默认值")
.get();
} catch (Exception e) {
return "异常: " + e.getMessage();
}
}
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
```
### 实现方式三:Spring WebFlux(响应式编程)
```java
@Configuration
@EnableWebFlux
public class WebFluxConfig {
}
@RestController
@RequestMapping("/reactive")
public class ReactiveController {
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamData() {
return Flux.interval(Duration.ofSeconds(1))
.map(seq -> "Data " + seq + " at " + LocalTime.now())
.take(10);
}
@GetMapping("/mono/{id}")
public Mono<ResponseEntity<User>> getUser(@PathVariable Long id) {
return Mono.fromCallable(() -> findUserById(id))
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping("/users")
public Mono<User> createUser(@RequestBody Mono<User> userMono) {
return userMono.flatMap(this::saveUser);
}
@GetMapping("/users")
public Flux<User> getAllUsers() {
return Flux.fromIterable(findAllUsers());
}
// 模拟数据库操作
private User findUserById(Long id) {
// 模拟异步数据库查询
return new User(id, "User" + id);
}
private Mono<User> saveUser(User user) {
return Mono.just(user)
.map(u -> {
u.setId(System.currentTimeMillis());
return u;
});
}
private List<User> findAllUsers() {
return Arrays.asList(
new User(1L, "User1"),
new User(2L, "User2"),
new User(3L, "User3")
);
}
}
@Data
@AllArgsConstructor
class User {
private Long id;
private String name;
}
```
### 实现方式四:DeferredResult(Servlet 异步)
```java
@RestController
@RequestMapping("/deferred")
@RequiredArgsConstructor
public class DeferredResultController {
private final AsyncService asyncService;
@GetMapping("/task")
public DeferredResult<String> asyncTask() {
DeferredResult<String> deferredResult = new DeferredResult<>(10000L, "超时");
// 异步处理
new Thread(() -> {
try {
Thread.sleep(3000);
deferredResult.setResult("任务完成");
} catch (InterruptedException e) {
deferredResult.setErrorResult("任务中断");
}
}).start();
// 超时回调
deferredResult.onTimeout(() -> {
System.out.println("任务超时");
});
// 完成回调
deferredResult.onCompletion(() -> {
System.out.println("任务完成回调");
});
return deferredResult;
}
@GetMapping("/callable")
public Callable<String> callableTask() {
return () -> {
Thread.sleep(3000);
return "Callable 任务完成";
};
}
}
```
### 异步事务处理
```java
@Service
@Slf4j
public class AsyncTransactionalService {
/**
* 异步方法中的事务
* 注意:@Async 和 @Transactional 可以一起使用
*/
@Async
@Transactional
public CompletableFuture<Void> asyncTransactionalOperation() {
// 事务操作
log.info("异步事务操作,线程: {}", Thread.currentThread().getName());
return CompletableFuture.completedFuture(null);
}
/**
* 在异步方法中调用事务方法
*/
@Async
public CompletableFuture<Void> callTransactionalMethod() {
// 调用另一个类的 @Transactional 方法
transactionalService.doTransaction();
return CompletableFuture.completedFuture(null);
}
}
```
### 异步异常处理
```java
@Configuration
@Slf4j
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("异步方法异常 - 方法: {}, 参数: {}", method.getName(), params, ex);
// 可以在这里发送告警、记录日志等
sendAlert(method.getName(), ex.getMessage());
}
private void sendAlert(String methodName, String errorMessage) {
// 发送告警通知
log.warn("发送告警: 方法 {} 执行失败,错误: {}", methodName, errorMessage);
}
}
// 在配置类中注册
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}
```
### 线程池监控
```java
@Component
@Slf4j
public class ThreadPoolMonitor {
@Autowired
@Qualifier("taskExecutor")
private ThreadPoolTaskExecutor taskExecutor;
@Scheduled(fixedRate = 60000) // 每分钟执行
public void monitor() {
ThreadPoolExecutor executor = taskExecutor.getThreadPoolExecutor();
log.info("线程池状态 - 核心线程数: {}, 活跃线程数: {}, 完成任务数: {}, " +
"队列任务数: {}, 总任务数: {}",
executor.getCorePoolSize(),
executor.getActiveCount(),
executor.getCompletedTaskCount(),
executor.getQueue().size(),
executor.getTaskCount());
}
}
```
### 最佳实践
#### 1. 线程池配置建议
```java
@Bean("ioIntensiveExecutor")
public Executor ioIntensiveExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// IO 密集型:核心线程数可以设置较大
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("io-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean("cpuIntensiveExecutor")
public Executor cpuIntensiveExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// CPU 密集型:核心线程数 = CPU 核心数 + 1
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1);
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() + 1);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("cpu-task-");
executor.initialize();
return executor;
}
```
#### 2. 避免异步陷阱
```java
@Service
@Slf4j
public class AsyncPitfallService {
/**
* ❌ 错误:同类中调用 @Async 方法不会异步执行
*/
public void wrongAsyncCall() {
// 这样调用不会走代理,不会异步执行
this.asyncMethod();
}
@Async
public void asyncMethod() {
// 异步逻辑
}
/**
* ✅ 正确:通过注入的代理对象调用
*/
@Autowired
private AsyncPitfallService self;
public void correctAsyncCall() {
// 通过代理对象调用
self.asyncMethod();
}
}
```
#### 3. 异步方法返回值
```java
@Service
public class AsyncReturnService {
/**
* ❌ 不推荐:返回 void,无法知道执行结果
*/
@Async
public void fireAndForget() {
// 执行操作
}
/**
* ✅ 推荐:返回 CompletableFuture,可以获取结果和异常
*/
@Async
public CompletableFuture<String> asyncWithResult() {
try {
// 执行操作
return CompletableFuture.completedFuture("success");
} catch (Exception e) {
CompletableFuture<String> future = new CompletableFuture<>();
future.completeExceptionally(e);
return future;
}
}
}
```
### 总结
| 方式 | 适用场景 | 优点 | 缺点 |
|------|---------|------|------|
| @Async | 简单异步任务 | 简单易用 | 功能有限 |
| CompletableFuture | 复杂异步编排 | 功能强大、可组合 | 学习曲线 |
| WebFlux | 高并发响应式 | 非阻塞、高性能 | 编程模型不同 |
| DeferredResult | Servlet 异步 | 兼容传统 MVC | 较底层 |
选择建议:
- 简单后台任务:@Async
- 复杂异步流程:CompletableFuture
- 高并发 API:WebFlux
- 传统项目升级:DeferredResult
服务端 · 3月6日 21:58
Spring Boot 中如何实现微服务架构的服务注册与发现?## Spring Boot 微服务注册与发现详解
### 为什么需要服务注册与发现
在微服务架构中,服务实例动态变化:
- **服务扩容/缩容**:实例数量不断变化
- **故障转移**:故障实例需要自动剔除
- **动态路由**:客户端需要知道可用服务地址
- **负载均衡**:在多个实例间分发请求
### 主流注册中心对比
| 特性 | Eureka | Nacos | Consul | Zookeeper |
|------|--------|-------|--------|-----------|
| 开发公司 | Netflix | 阿里巴巴 | HashiCorp | Apache |
| 一致性协议 | AP | AP/CP | CP | CP |
| 健康检查 | 客户端心跳 | TCP/HTTP/MySQL | TCP/HTTP/gRPC | 临时节点 |
| 多数据中心 | 支持 | 支持 | 支持 | 不支持 |
| Spring Cloud | 原生支持 | 支持 | 支持 | 支持 |
| 配置中心 | 不支持 | 支持 | 支持 | 不支持 |
### 实现方式一:Eureka
#### 1. Eureka Server 配置
**添加依赖**
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
```
**启动类**
```java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
```
**配置文件**
```yaml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
# 不向注册中心注册自己
register-with-eureka: false
# 不需要检索服务
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
# 关闭自我保护机制
enable-self-preservation: false
# 清理间隔(毫秒)
eviction-interval-timer-in-ms: 5000
```
#### 2. Eureka Client 配置
**添加依赖**
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
```
**配置文件**
```yaml
server:
port: 8081
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
# 使用 IP 地址注册
prefer-ip-address: true
# 实例 ID
instance-id: ${spring.application.name}:${server.port}
# 心跳间隔
lease-renewal-interval-in-seconds: 5
# 服务过期时间
lease-expiration-duration-in-seconds: 10
```
#### 3. Eureka 高可用集群
```yaml
# eureka-server-1.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: eureka1
client:
service-url:
defaultZone: http://eureka2:8762/eureka/,http://eureka3:8763/eureka/
# eureka-server-2.yml
server:
port: 8762
eureka:
instance:
hostname: eureka2
client:
service-url:
defaultZone: http://eureka1:8761/eureka/,http://eureka3:8763/eureka/
# eureka-server-3.yml
server:
port: 8763
eureka:
instance:
hostname: eureka3
client:
service-url:
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/
```
### 实现方式二:Nacos(推荐)
#### 1. Nacos Server 部署
```bash
# 下载并启动 Nacos
curl -O https://github.com/alibaba/nacos/releases/download/2.2.3/nacos-server-2.2.3.tar.gz
tar -xzf nacos-server-2.2.3.tar.gz
cd nacos/bin
# 单机模式启动
sh startup.sh -m standalone
# 访问控制台
# http://localhost:8848/nacos
# 默认账号密码:nacos/nacos
```
#### 2. Nacos Client 配置
**添加依赖**
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
```
**配置文件**
```yaml
server:
port: 8081
spring:
application:
name: user-service
cloud:
nacos:
discovery:
# Nacos 服务器地址
server-addr: localhost:8848
# 命名空间(环境隔离)
namespace: dev
# 分组
group: DEFAULT_GROUP
# 元数据
metadata:
version: v1
region: beijing
# 权重
weight: 1
# 临时实例(false 为持久实例)
ephemeral: true
# 心跳间隔
heart-beat-interval: 5000
# 心跳超时
heart-beat-timeout: 15000
```
#### 3. Nacos 服务发现
```java
@RestController
@RequestMapping("/discovery")
@RequiredArgsConstructor
public class DiscoveryController {
private final DiscoveryClient discoveryClient;
private final LoadBalancerClient loadBalancerClient;
/**
* 获取所有服务实例
*/
@GetMapping("/services")
public List<String> getServices() {
return discoveryClient.getServices();
}
/**
* 获取指定服务的实例
*/
@GetMapping("/instances/{serviceName}")
public List<ServiceInstance> getInstances(@PathVariable String serviceName) {
return discoveryClient.getInstances(serviceName);
}
/**
* 使用负载均衡选择实例
*/
@GetMapping("/choose/{serviceName}")
public ServiceInstance choose(@PathVariable String serviceName) {
return loadBalancerClient.choose(serviceName);
}
}
```
### 实现方式三:Consul
#### 1. Consul Server 部署
```bash
# 下载 Consul
# https://www.consul.io/downloads
# 开发模式启动
consul agent -dev
# 生产模式(集群)
consul agent -server -bootstrap-expect=3 -data-dir=/var/consul \
-bind=192.168.1.1 -client=0.0.0.0 -ui -retry-join="provider=aws ..."
```
#### 2. Consul Client 配置
**添加依赖**
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
```
**配置文件**
```yaml
server:
port: 8081
spring:
application:
name: user-service
cloud:
consul:
host: localhost
port: 8500
discovery:
# 注册服务
register: true
# 服务名称
service-name: ${spring.application.name}
# 健康检查路径
health-check-path: /actuator/health
# 健康检查间隔
health-check-interval: 10s
# 实例 ID
instance-id: ${spring.application.name}:${random.value}
# 标签
tags: version=1.0,profile=dev
# 元数据
metadata:
version: v1
region: beijing
```
### 服务间调用
#### 1. 使用 RestTemplate + @LoadBalanced
```java
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 开启负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final RestTemplate restTemplate;
public Order getOrderByUserId(Long userId) {
// 使用服务名调用,而非具体 IP
String url = "http://order-service/orders/user/" + userId;
return restTemplate.getForObject(url, Order.class);
}
}
```
#### 2. 使用 OpenFeign(推荐)
**添加依赖**
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
```
**启用 Feign**
```java
@SpringBootApplication
@EnableFeignClients
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
```
**定义 Feign 客户端**
```java
@FeignClient(
name = "order-service",
fallback = OrderClientFallback.class,
configuration = FeignConfig.class
)
public interface OrderClient {
@GetMapping("/orders/{id}")
Order getOrderById(@PathVariable("id") Long id);
@GetMapping("/orders/user/{userId}")
List<Order> getOrdersByUserId(@PathVariable("userId") Long userId);
@PostMapping("/orders")
Order createOrder(@RequestBody Order order);
}
@Component
@Slf4j
public class OrderClientFallback implements OrderClient {
@Override
public Order getOrderById(Long id) {
log.warn("Order service is down, returning fallback for order: {}", id);
Order order = new Order();
order.setId(id);
order.setStatus("UNKNOWN");
return order;
}
@Override
public List<Order> getOrdersByUserId(Long userId) {
return Collections.emptyList();
}
@Override
public Order createOrder(Order order) {
throw new RuntimeException("Order service is unavailable");
}
}
```
**Feign 配置**
```java
@Configuration
public class FeignConfig {
@Bean
public Retryer feignRetryer() {
// 重试策略:初始间隔 100ms,最大间隔 1s,最多重试 3 次
return new Retryer.Default(100, 1000, 3);
}
@Bean
public ErrorDecoder feignErrorDecoder() {
return new CustomErrorDecoder();
}
@Bean
public RequestInterceptor feignRequestInterceptor() {
return requestTemplate -> {
// 添加请求头
requestTemplate.header("X-Request-Id", UUID.randomUUID().toString());
// 传递认证信息
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token)) {
requestTemplate.header("Authorization", token);
}
}
};
}
}
```
### 负载均衡
#### 1. Ribbon(已弃用,Spring Cloud 2020+ 使用 Spring Cloud LoadBalancer)
```yaml
# 自定义负载均衡策略
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# 其他策略:RoundRobinRule, WeightedResponseTimeRule, BestAvailableRule
```
#### 2. Spring Cloud LoadBalancer
```java
@Configuration
public class LoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new ReactorServiceInstanceLoadBalancer() {
private Random random = new Random();
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = loadBalancerClientFactory
.getLazyProvider(name, ServiceInstanceListSupplier.class)
.getIfAvailable();
return supplier.get().next().map(instances -> {
if (instances.isEmpty()) {
return new EmptyResponse();
}
// 随机选择
int index = random.nextInt(instances.size());
return new DefaultResponse(instances.get(index));
});
}
};
}
}
```
### 服务网关(Gateway)
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
```
```yaml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 自动从注册中心发现服务
lower-case-service-id: true
routes:
# 用户服务路由
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
# 订单服务路由
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: orderServiceCircuitBreaker
fallbackUri: forward:/fallback/order
# 全局过滤器
default-filters:
- AddRequestHeader=X-Request-From, Gateway
- AddResponseHeader=X-Response-From, Gateway
```
### 配置中心集成(Nacos Config)
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
```
```yaml
# bootstrap.yml
spring:
application:
name: user-service
profiles:
active: dev
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml
namespace: dev
group: DEFAULT_GROUP
# 共享配置
shared-configs:
- data-id: common.yaml
group: DEFAULT_GROUP
refresh: true
# 扩展配置
extension-configs:
- data-id: redis.yaml
group: DEFAULT_GROUP
refresh: true
```
```java
@RestController
@RefreshScope // 配置自动刷新
public class ConfigController {
@Value("${user.name:default}")
private String userName;
@Value("${user.age:0}")
private Integer userAge;
@GetMapping("/config")
public Map<String, Object> getConfig() {
Map<String, Object> config = new HashMap<>();
config.put("userName", userName);
config.put("userAge", userAge);
return config;
}
}
```
### 健康检查与监控
```yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
```
```java
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check();
if (errorCode != 0) {
return Health.down()
.withDetail("Error Code", errorCode)
.build();
}
return Health.up().build();
}
private int check() {
// 自定义健康检查逻辑
return 0;
}
}
```
### 最佳实践
#### 1. 服务命名规范
```yaml
spring:
application:
# 使用小写,用横线分隔
name: order-service
```
#### 2. 版本管理
```yaml
# 通过元数据区分版本
spring:
cloud:
nacos:
discovery:
metadata:
version: v2
region: beijing
```
#### 3. 优雅下线
```java
@Component
public class GracefulShutdown implements ApplicationListener<ContextClosedEvent> {
@Autowired
private NacosRegistration registration;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 从注册中心注销
registration.deregister();
// 等待正在处理的请求完成
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
```
### 总结
| 注册中心 | 推荐场景 | 优势 | 劣势 |
|---------|---------|------|------|
| Eureka | Spring Cloud 传统项目 | 成熟稳定 | 停止维护 |
| Nacos | 新项目的首选 | 功能全面、国产支持 | 学习成本 |
| Consul | 多语言环境 | 云原生、功能丰富 | 资源占用 |
| Zookeeper | 已有 ZK 集群 | 成熟可靠 | 功能单一 |
选型建议:
- 新项目:Nacos(功能全面,社区活跃)
- 云原生:Consul
- 遗留项目:Eureka
服务端 · 3月6日 21:58
Spring Boot 中如何整合 MyBatis 进行数据库操作?## Spring Boot 整合 MyBatis 详解
### 为什么要用 MyBatis
- **SQL 可控**:手写 SQL,便于优化复杂查询
- **轻量级**:相比 JPA,学习成本低
- **灵活**:支持动态 SQL、存储过程
- **与 Spring Boot 无缝集成**:mybatis-spring-boot-starter 提供自动配置
### 环境准备
#### 1. 添加依赖
```xml
<dependencies>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 连接池(可选,Spring Boot 默认使用 HikariCP) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- 分页插件(可选) -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
</dependencies>
```
#### 2. 配置文件
```yaml
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 20000
# MyBatis 配置
mybatis:
# Mapper XML 文件位置
mapper-locations: classpath:mapper/*.xml
# 实体类包路径(配置别名)
type-aliases-package: com.example.entity
# 驼峰命名自动映射
configuration:
map-underscore-to-camel-case: true
# 打印 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 全局配置文件(可选,与 configuration 互斥)
# config-location: classpath:mybatis-config.xml
```
### 基础使用方式
#### 方式一:注解方式(推荐简单场景)
**实体类**
```java
@Data
public class User {
private Long id;
private String username;
private String email;
private Integer age;
private LocalDateTime createTime;
}
```
**Mapper 接口**
```java
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
@Select("SELECT * FROM user")
List<User> selectAll();
@Insert("INSERT INTO user(username, email, age) VALUES(#{username}, #{email}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE user SET username=#{username}, email=#{email}, age=#{age} WHERE id=#{id}")
int update(User user);
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteById(Long id);
// 动态 SQL 示例
@Select("<script>" +
"SELECT * FROM user " +
"<where>" +
" <if test='username != null'>AND username LIKE CONCAT('%', #{username}, '%')</if>" +
" <if test='age != null'>AND age = #{age}</if>" +
"</where>" +
"</script>")
List<User> selectByCondition(@Param("username") String username, @Param("age") Integer age);
}
```
#### 方式二:XML 方式(推荐复杂场景)
**Mapper 接口**
```java
@Mapper
public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
int insert(User user);
int update(User user);
int deleteById(Long id);
List<User> selectByCondition(UserQueryDTO query);
}
```
**XML 映射文件(resources/mapper/UserMapper.xml)**
```xml
<?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.example.mapper.UserMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="age" property="age"/>
<result column="create_time" property="createTime"/>
</resultMap>
<!-- 查询单条 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 查询全部 -->
<select id="selectAll" resultMap="BaseResultMap">
SELECT * FROM user
</select>
<!-- 插入 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(username, email, age, create_time)
VALUES(#{username}, #{email}, #{age}, NOW())
</insert>
<!-- 更新 -->
<update id="update">
UPDATE user
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
<if test="age != null">age = #{age}</if>
</set>
WHERE id = #{id}
</update>
<!-- 删除 -->
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
<!-- 动态条件查询 -->
<select id="selectByCondition" resultMap="BaseResultMap">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</where>
ORDER BY create_time DESC
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user(username, email, age, create_time) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.email}, #{user.age}, NOW())
</foreach>
</insert>
<!-- 批量删除 -->
<delete id="batchDelete">
DELETE FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
```
### 高级特性
#### 1. 一对一关联查询
```xml
<resultMap id="UserWithOrderMap" type="com.example.entity.User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<association property="order" javaType="com.example.entity.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</association>
</resultMap>
<select id="selectUserWithOrder" resultMap="UserWithOrderMap">
SELECT
u.id as user_id,
u.username,
o.id as order_id,
o.order_no,
o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
```
#### 2. 一对多关联查询
```xml
<resultMap id="UserWithOrdersMap" type="com.example.entity.User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<collection property="orders" ofType="com.example.entity.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="UserWithOrdersMap">
SELECT
u.id as user_id,
u.username,
o.id as order_id,
o.order_no,
o.amount
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
```
#### 3. 分页查询
使用 PageHelper:
```java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public PageInfo<User> getUserPage(int pageNum, int pageSize) {
// 开启分页
PageHelper.startPage(pageNum, pageSize);
// 执行查询
List<User> list = userMapper.selectAll();
// 封装分页信息
return new PageInfo<>(list);
}
public PageInfo<User> searchUsers(UserQueryDTO query, int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> list = userMapper.selectByCondition(query);
return new PageInfo<>(list);
}
}
```
#### 4. 多数据源配置
```yaml
spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/db1
username: root
password: pass1
secondary:
jdbc-url: jdbc:mysql://localhost:3306/db2
username: root
password: pass2
# MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/**/*.xml
```
```java
@Configuration
@MapperScan(basePackages = "com.example.mapper.primary",
sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig {
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "primarySqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(
@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/primary/*.xml"));
return bean.getObject();
}
@Bean(name = "primarySqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
```
### 事务管理
```java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
@Transactional
public void createUserWithOrder(User user, Order order) {
// 插入用户
userMapper.insert(user);
// 设置订单用户ID
order.setUserId(user.getId());
// 插入订单
orderMapper.insert(order);
// 模拟异常,测试事务回滚
if (user.getAge() < 0) {
throw new IllegalArgumentException("Invalid age");
}
}
@Transactional(readOnly = true)
public User getUserById(Long id) {
return userMapper.selectById(id);
}
@Transactional(rollbackFor = Exception.class)
public void updateUser(User user) {
userMapper.update(user);
}
}
```
### 代码生成器
使用 MyBatis Generator 自动生成代码:
```xml
<!-- pom.xml -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
</plugin>
```
```xml
<!-- generatorConfig.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="default" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mydb"
userId="root"
password="password"/>
<javaModelGenerator targetPackage="com.example.entity"
targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="mapper"
targetProject="src/main/resources"/>
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.example.mapper"
targetProject="src/main/java"/>
<table tableName="user" domainObjectName="User"/>
</context>
</generatorConfiguration>
```
### 最佳实践
#### 1. 项目结构
```
src/main/java/com/example/
├── entity/ # 实体类
├── mapper/ # Mapper 接口
├── service/ # 业务层
├── controller/ # 控制层
└── dto/ # 数据传输对象
src/main/resources/
├── mapper/ # XML 映射文件
├── application.yml # 配置文件
└── mybatis-config.xml # MyBatis 全局配置(可选)
```
#### 2. Mapper 扫描方式
方式1:每个 Mapper 加 `@Mapper` 注解
方式2:启动类统一扫描(推荐)
```java
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
#### 3. SQL 编写规范
- 复杂 SQL 使用 XML,简单 SQL 使用注解
- 使用 `#{}` 防止 SQL 注入,避免使用 `${}`
- 大字段使用延迟加载
- 分页查询必须加 ORDER BY
#### 4. 性能优化
```yaml
mybatis:
configuration:
# 开启二级缓存
cache-enabled: true
# 延迟加载
lazy-loading-enabled: true
# 积极加载
aggressive-lazy-loading: false
# 默认执行器
default-executor-type: reuse
```
### 常见问题
**Q1: Invalid bound statement (not found)**
- 检查 Mapper 接口和 XML 的 namespace 是否一致
- 检查方法名是否匹配
- 确认 XML 文件在编译后的 target 目录中
**Q2: 如何打印 SQL 日志?**
```yaml
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 或使用日志框架
logging:
level:
com.example.mapper: debug
```
**Q3: 如何处理枚举类型?**
```java
public enum Status {
ACTIVE, INACTIVE;
}
// 自定义 TypeHandler
@MappedTypes(Status.class)
public class StatusTypeHandler extends BaseTypeHandler<Status> {
// 实现方法
}
```
### 总结
| 特性 | 注解方式 | XML 方式 |
|------|---------|---------|
| 简单 SQL | ✅ 推荐 | 可用 |
| 复杂 SQL | 繁琐 | ✅ 推荐 |
| 动态 SQL | 支持(@SelectProvider) | ✅ 更强大 |
| 维护性 | 一般 | ✅ 好 |
Spring Boot + MyBatis 的组合提供了灵活的数据访问能力,适合需要精细控制 SQL 的场景。
服务端 · 3月6日 21:58
Spring Boot 中如何实现缓存?## Spring Boot 缓存详解
### 为什么需要缓存
- **减少数据库压力**:热点数据直接从缓存读取
- **提升响应速度**:内存访问比磁盘快几个数量级
- **降低系统负载**:减少重复计算
- **改善用户体验**:页面加载更快
### Spring Boot 缓存抽象
Spring 提供了一套缓存抽象,支持多种缓存实现:
- **ConcurrentMapCache**:基于内存,单机使用
- **Caffeine**:高性能本地缓存
- **EhCache**:老牌缓存框架
- **Redis**:分布式缓存
- **Hazelcast**:分布式内存数据网格
### 实现方式一:基于内存的 ConcurrentMapCache
#### 1. 开启缓存支持
```java
@Configuration
@EnableCaching
public class CacheConfig {
/**
* 配置缓存管理器
*/
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
// 创建多个缓存区域
List<Cache> caches = new ArrayList<>();
caches.add(new ConcurrentMapCache("users"));
caches.add(new ConcurrentMapCache("products"));
caches.add(new ConcurrentMapCache("orders"));
cacheManager.setCaches(caches);
return cacheManager;
}
}
```
#### 2. 使用缓存注解
```java
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* @Cacheable:先查缓存,没有则执行方法并缓存结果
* value:缓存名称
* key:缓存键,支持 SpEL
* unless:条件判断,为 true 时不缓存
*/
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
log.info("从数据库查询用户: {}", id);
return userRepository.findById(id).orElse(null);
}
/**
* 使用多个 key 组合
*/
@Cacheable(value = "users", key = "#username + ':' + #age")
public User getUserByUsernameAndAge(String username, Integer age) {
log.info("从数据库查询用户: {}, {}", username, age);
return userRepository.findByUsernameAndAge(username, age);
}
/**
* @CachePut:先执行方法,再更新缓存
* 适用于更新操作
*/
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
log.info("更新用户: {}", user.getId());
return userRepository.save(user);
}
/**
* @CacheEvict:删除缓存
* allEntries = true:清空整个缓存区域
* beforeInvocation = true:方法执行前删除缓存
*/
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
log.info("删除用户: {}", id);
userRepository.deleteById(id);
}
/**
* 清空整个 users 缓存
*/
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
log.info("清空用户缓存");
}
/**
* @Caching:组合多个缓存操作
*/
@Caching(
put = {
@CachePut(value = "users", key = "#user.id"),
@CachePut(value = "users", key = "#user.username")
},
evict = {
@CacheEvict(value = "users", key = "'list'")
}
)
public User saveUser(User user) {
log.info("保存用户: {}", user.getId());
return userRepository.save(user);
}
}
```
### 实现方式二:Caffeine 高性能本地缓存
#### 1. 添加依赖
```xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
```
#### 2. 配置 Caffeine
```java
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
/**
* Caffeine 缓存配置
*/
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 配置默认的 Caffeine 规格
cacheManager.setCaffeine(Caffeine.newBuilder()
// 初始容量
.initialCapacity(100)
// 最大容量
.maximumSize(1000)
// 写入后 10 分钟过期
.expireAfterWrite(10, TimeUnit.MINUTES)
// 访问后 5 分钟过期
.expireAfterAccess(5, TimeUnit.MINUTES)
// 记录缓存统计
.recordStats()
);
// 指定缓存名称
cacheManager.setCacheNames(Arrays.asList("users", "products", "orders"));
return cacheManager;
}
/**
* 为不同缓存配置不同策略
*/
@Bean("customCacheManager")
public CacheManager customCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 用户缓存:高频访问,长期有效
cacheManager.registerCustomCache("users",
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.recordStats()
.build()
);
// 产品缓存:中等频率,短期有效
cacheManager.registerCustomCache("products",
Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build()
);
// 订单缓存:低频访问,快速过期
cacheManager.registerCustomCache("orders",
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterAccess(5, TimeUnit.MINUTES)
.build()
);
return cacheManager;
}
}
```
#### 3. YAML 配置方式(Spring Boot 2.7+)
```yaml
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
```
### 实现方式三:Redis 分布式缓存
#### 1. 添加依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
```
#### 2. 配置 Redis
```yaml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
cache:
type: redis
redis:
time-to-live: 600000 # 10分钟
cache-null-values: true
use-key-prefix: true
key-prefix: "myapp:"
```
#### 3. 配置 RedisCacheManager
```java
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默认配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
n .fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// 为不同缓存配置不同过期时间
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("users", defaultConfig.entryTtl(Duration.ofMinutes(30)));
configMap.put("products", defaultConfig.entryTtl(Duration.ofMinutes(10)));
configMap.put("orders", defaultConfig.entryTtl(Duration.ofMinutes(5)));
configMap.put("shortTerm", defaultConfig.entryTtl(Duration.ofSeconds(60)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configMap)
.transactionAware()
.build();
}
/**
* 配置 RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用 Jackson 序列化
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
```
### 缓存注解详解
#### @Cacheable
```java
/**
* 缓存查询结果
*/
@Cacheable(
value = "users", // 缓存名称
key = "#id", // 缓存键
condition = "#id > 0", // 条件:id>0 时才缓存
unless = "#result == null" // 结果不为 null 时才缓存
)
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
/**
* 使用 SpEL 表达式
*/
@Cacheable(value = "users", key = "T(com.example.util.CacheKeyUtil).generateKey('user', #id)")
public User getUserWithCustomKey(Long id) {
return userRepository.findById(id).orElse(null);
}
/**
* 多条件查询缓存
*/
@Cacheable(value = "users", key = "#root.methodName + ':' + #username + ':' + #status")
public List<User> findByUsernameAndStatus(String username, Integer status) {
return userRepository.findByUsernameAndStatus(username, status);
}
```
#### @CachePut
```java
/**
* 更新缓存(方法执行后缓存)
*/
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
/**
* 根据结果动态设置 key
*/
@CachePut(value = "users", key = "#result.id")
public User createUser(User user) {
return userRepository.save(user);
}
```
#### @CacheEvict
```java
/**
* 删除指定缓存
*/
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
/**
* 方法执行前删除缓存
*/
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUserBefore(Long id) {
userRepository.deleteById(id);
}
/**
* 清空整个缓存区域
*/
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() {
// 清空所有用户缓存
}
/**
* 同时清除多个缓存
*/
@Caching(evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "users", key = "#user.username"),
@CacheEvict(value = "userList", allEntries = true)
})
public void deleteUserAndClearCache(User user) {
userRepository.delete(user);
}
```
### 自定义缓存 Key 生成器
```java
@Component
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName()).append(":");
sb.append(method.getName()).append(":");
for (Object param : params) {
if (param != null) {
sb.append(param.toString()).append(":");
}
}
return sb.toString();
}
}
// 使用
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
```
### 缓存条件判断
```java
@Service
public class ProductService {
/**
* 只有 VIP 用户的查询结果才缓存
*/
@Cacheable(
value = "products",
key = "#userId",
condition = "@userService.isVip(#userId)"
)
public List<Product> getRecommendedProducts(Long userId) {
// 获取推荐商品
return productRepository.findRecommendedByUserId(userId);
}
/**
* 结果为空时不缓存
*/
@Cacheable(
value = "products",
key = "#id",
unless = "#result == null or #result.stock <= 0"
)
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
}
```
### 缓存与事务
```java
@Service
public class OrderService {
/**
* 缓存与事务结合
* 注意:@Cacheable 在事务之外执行
*/
@Transactional
@Cacheable(value = "orders", key = "#orderId")
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId).orElse(null);
}
/**
* 更新操作:先更新数据库,再更新缓存
*/
@Transactional
@CachePut(value = "orders", key = "#order.id")
public Order updateOrder(Order order) {
// 业务校验
validateOrder(order);
// 更新数据库
Order saved = orderRepository.save(order);
// 发送消息
eventPublisher.publishEvent(new OrderUpdatedEvent(saved));
return saved;
}
}
```
### 缓存监控
```java
@Component
@Slf4j
public class CacheMonitor {
@Autowired
private CacheManager cacheManager;
@Scheduled(fixedRate = 60000) // 每分钟执行
public void logCacheStats() {
if (cacheManager instanceof CaffeineCacheManager) {
Collection<String> cacheNames = cacheManager.getCacheNames();
for (String cacheName : cacheNames) {
Cache cache = cacheManager.getCache(cacheName);
if (cache instanceof CaffeineCache) {
com.github.benmanes.caffeine.cache.Cache nativeCache =
(com.github.benmanes.caffeine.cache.Cache) cache.getNativeCache();
log.info("Cache [{}] Stats: {}", cacheName, nativeCache.stats());
}
}
}
}
}
```
### 缓存穿透、击穿、雪崩解决方案
```java
@Service
public class CacheSolutionService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 解决缓存穿透:缓存空值
*/
@Cacheable(
value = "users",
key = "#id",
unless = "#result == null"
)
public User getUserWithNullCache(Long id) {
User user = userRepository.findById(id).orElse(null);
// 数据库不存在也缓存空值(短时间)
if (user == null) {
redisTemplate.opsForValue().set(
"users::" + id,
"null",
5,
TimeUnit.MINUTES
);
}
return user;
}
/**
* 解决缓存击穿:互斥锁
*/
public User getUserWithLock(Long id) {
String cacheKey = "users::" + id;
String lockKey = "lock:users:" + id;
// 先查缓存
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 获取分布式锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待 10 秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
try {
// 双重检查
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 查询数据库
User user = userRepository.findById(id).orElse(null);
// 写入缓存
if (user != null) {
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(user),
30,
TimeUnit.MINUTES
);
}
return user;
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
/**
* 解决缓存雪崩:随机过期时间
*/
@Cacheable(value = "products", key = "#categoryId")
public List<Product> getProductsByCategory(Long categoryId) {
List<Product> products = productRepository.findByCategoryId(categoryId);
// 设置随机过期时间,防止同时过期
int randomExpire = 600 + (int)(Math.random() * 300); // 10-15 分钟
redisTemplate.opsForValue().set(
"products::" + categoryId,
JSON.toJSONString(products),
randomExpire,
TimeUnit.SECONDS
);
return products;
}
}
```
### 总结
| 缓存类型 | 适用场景 | 优点 | 缺点 |
|---------|---------|------|------|
| ConcurrentMapCache | 单机、开发测试 | 简单、无需额外依赖 | 不支持分布式 |
| Caffeine | 单机高性能 | 性能极高、功能丰富 | 不支持分布式 |
| Redis | 分布式系统 | 支持分布式、持久化 | 网络开销 |
| EhCache | 企业级应用 | 功能全面、支持集群 | 配置复杂 |
选择建议:
- 单机应用:Caffeine
- 分布式应用:Redis
- 简单场景:ConcurrentMapCache
服务端 · 3月6日 21:58
什么是 Spring Boot 的自动配置原理?## Spring Boot 自动配置原理详解
### 核心概念
Spring Boot 的自动配置(Auto-Configuration)是其最核心的特性之一,它通过**约定优于配置**的理念,大大简化了 Spring 应用的开发和配置工作。
### 自动配置的工作原理
#### 1. @SpringBootApplication 注解
这是自动配置的入口,它组合了三个重要注解:
```java
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
}
```
其中 `@EnableAutoConfiguration` 是开启自动配置的关键。
#### 2. @EnableAutoConfiguration 机制
```java
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
```
`AutoConfigurationImportSelector` 是实现自动配置的核心类。
#### 3. 自动配置的核心流程
**步骤一:读取 META-INF/spring.factories**
Spring Boot 启动时会扫描所有 jar 包中的 `META-INF/spring.factories` 文件,查找 `EnableAutoConfiguration` 对应的配置类:
```properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
```
**步骤二:条件注解过滤**
Spring Boot 使用一系列条件注解(@Conditional)来决定是否加载某个配置类:
| 条件注解 | 作用 |
|---------|------|
| `@ConditionalOnClass` | 类路径存在指定类时才生效 |
| `@ConditionalOnMissingClass` | 类路径不存在指定类时才生效 |
| `@ConditionalOnBean` | 容器中存在指定 Bean 时才生效 |
| `@ConditionalOnMissingBean` | 容器中不存在指定 Bean 时才生效 |
| `@ConditionalOnProperty` | 指定属性满足条件时才生效 |
| `@ConditionalOnWebApplication` | 是 Web 应用时才生效 |
**步骤三:配置属性绑定**
通过 `@EnableConfigurationProperties` 将配置文件中的属性绑定到配置类:
```java
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return DataSourceBuilder.create()
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
}
```
### 自动配置的执行时机
```
SpringApplication.run()
└── refreshContext()
└── invokeBeanFactoryPostProcessors()
└── ConfigurationClassPostProcessor
└── AutoConfigurationImportSelector.selectImports()
└── 加载 spring.factories 中的配置类
```
### 如何自定义自动配置
#### 1. 创建自动配置类
```java
@Configuration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyProperties properties) {
return new MyService(properties);
}
}
```
#### 2. 创建属性配置类
```java
@ConfigurationProperties(prefix = "my.service")
public class MyProperties {
private String name;
private boolean enabled = true;
// getters and setters
}
```
#### 3. 注册到 spring.factories
在 `META-INF/spring.factories` 中添加:
```properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration
```
### 自动配置的排除与定制
#### 排除特定自动配置
```java
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
public class Application {
}
```
或在配置文件中:
```yaml
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
```
### 总结
Spring Boot 自动配置的核心机制:
1. **SPI 机制**:通过 `spring.factories` 文件发现配置类
2. **条件装配**:使用 `@Conditional` 系列注解智能判断是否加载配置
3. **属性绑定**:通过 `@ConfigurationProperties` 实现外部化配置
4. **约定优于配置**:提供合理的默认值,减少显式配置
这种设计使得 Spring Boot 应用可以快速启动,同时保持高度的可定制性。
服务端 · 3月6日 21:58