乐闻世界logo
搜索文章和话题

Spring Boot

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