引言 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程。它基于Spring框架,通过约定优于配置的理念,大大简化了Spring应用的配置工作,让开发者能够快速构建独立的、生产级别的Spring应用程序。
一、Spring Boot核心概念 (一)设计理念 1. 约定优于配置(Convention over Configuration) 默认配置 :提供合理的默认配置,减少配置工作标准化结构 :统一的项目结构和命名规范自动装配 :基于类路径自动配置Bean2. 开箱即用(Out of the Box) 内嵌服务器 :无需外部容器,直接运行起步依赖 :简化依赖管理生产就绪 :内置监控、健康检查等功能3. 微服务友好 轻量级 :快速启动,资源占用少独立部署 :每个服务独立运行云原生 :支持容器化和云部署(二)核心特性 1. 自动配置(Auto Configuration) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } } @Configuration @ConditionalOnClass(DataSource.class) @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { @Bean @ConditionalOnMissingBean public DataSource dataSource (DataSourceProperties properties) { return DataSourceBuilder.create() .driverClassName(properties.getDriverClassName()) .url(properties.getUrl()) .username(properties.getUsername()) .password(properties.getPassword()) .build(); } }
2. 起步依赖(Starter Dependencies) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <dependencies > <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 > </dependencies >
3. 内嵌服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Configuration public class ServerConfiguration { @Bean public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer () { return factory -> { factory.setPort(8080 ); factory.setContextPath("/api" ); factory.addConnectorCustomizers(connector -> { connector.setMaxPostSize(10485760 ); }); }; } } server: port: 8080 servlet: context-path: /api tomcat: max-post-size: 10MB threads: max: 200 min-spare: 10
二、项目结构与配置 (一)标准项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── demo/ │ │ ├── DemoApplication.java # 主启动类 │ │ ├── controller/ # 控制器层 │ │ │ └── UserController.java │ │ ├── service/ # 服务层 │ │ │ ├── UserService.java │ │ │ └── impl/ │ │ │ └── UserServiceImpl.java │ │ ├── repository/ # 数据访问层 │ │ │ └── UserRepository.java │ │ ├── entity/ # 实体类 │ │ │ └── User.java │ │ ├── dto/ # 数据传输对象 │ │ │ ├── UserDTO.java │ │ │ └── CreateUserRequest.java │ │ ├── config/ # 配置类 │ │ │ ├── DatabaseConfig.java │ │ │ └── SecurityConfig.java │ │ └── exception/ # 异常处理 │ │ ├── GlobalExceptionHandler.java │ │ └── BusinessException.java │ └── resources/ │ ├── application.yml # 主配置文件 │ ├── application-dev.yml # 开发环境配置 │ ├── application-prod.yml # 生产环境配置 │ ├── static/ # 静态资源 │ └── templates/ # 模板文件 └── test/ └── java/ └── com/ └── example/ └── demo/ ├── DemoApplicationTests.java # 集成测试 ├── controller/ # 控制器测试 │ └── UserControllerTest.java └── service/ # 服务测试 └── UserServiceTest.java
(二)配置文件详解 1. application.yml 主配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 spring: application: name: demo-application profiles: active: dev datasource: url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC username: ${DB_USERNAME:root} password: ${DB_PASSWORD:password} driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect format_sql: true redis: host: localhost port: 6379 password: ${REDIS_PASSWORD:} timeout: 2000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms cache: type: redis redis: time-to-live: 600000 server: port: 8080 servlet: context-path: /api compression: enabled: true mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json min-response-size: 1024 logging: level: com.example.demo: DEBUG org.springframework.web: DEBUG org.hibernate.SQL: DEBUG org.hibernate.type.descriptor.sql.BasicBinder: TRACE pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" file: name: logs/application.log max-size: 10MB max-history: 30 management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: export: prometheus: enabled: true app: jwt: secret: ${JWT_SECRET:mySecretKey} expiration: 86400000 upload: path: ${UPLOAD_PATH:/tmp/uploads} max-file-size: 10MB
2. 环境特定配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 spring: datasource: url: jdbc:h2:mem:testdb driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true jpa: hibernate: ddl-auto: create-drop logging: level: root: INFO com.example.demo: DEBUG --- spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME} username: ${DB_USERNAME} password: ${DB_PASSWORD} hikari: maximum-pool-size: 50 jpa: hibernate: ddl-auto: validate show-sql: false logging: level: root: WARN com.example.demo: INFO file: name: /var/log/application.log server: port: 80
三、Web开发 (一)RESTful API开发 1. 实体类设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package com.example.demo.entity;import lombok.Data;import lombok.NoArgsConstructor;import lombok.AllArgsConstructor;import lombok.Builder;import javax.persistence.*;import javax.validation.constraints.*;import java.time.LocalDateTime;import java.util.List;@Entity @Table(name = "users") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false, length = 50) @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 50, message = "用户名长度必须在3-50之间") private String username; @Column(nullable = false, length = 100) @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; @Column(name = "phone_number", length = 20) @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phoneNumber; @Column(name = "full_name", length = 100) private String fullName; @Column(name = "is_active") @Builder .Default private Boolean isActive = true ; @Column(name = "created_at") @Builder .Default private LocalDateTime createdAt = LocalDateTime.now(); @Column(name = "updated_at") private LocalDateTime updatedAt; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Order> orders; @PreUpdate public void preUpdate () { this .updatedAt = LocalDateTime.now(); } }
2. 数据传输对象(DTO) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package com.example.demo.dto;import lombok.Data;import lombok.Builder;import com.fasterxml.jackson.annotation.JsonFormat;import java.time.LocalDateTime;@Data @Builder public class UserDTO { private Long id; private String username; private String email; private String phoneNumber; private String fullName; private Boolean isActive; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createdAt; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updatedAt; } package com.example.demo.dto;import lombok.Data;import javax.validation.constraints.*;@Data public class CreateUserRequest { @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 50, message = "用户名长度必须在3-50之间") private String username; @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phoneNumber; private String fullName; } package com.example.demo.dto;import lombok.Data;import javax.validation.constraints.*;@Data public class UpdateUserRequest { @Size(min = 3, max = 50, message = "用户名长度必须在3-50之间") private String username; @Email(message = "邮箱格式不正确") private String email; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phoneNumber; private String fullName; private Boolean isActive; }
3. 数据访问层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.example.demo.repository;import com.example.demo.entity.User;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.Query;import org.springframework.data.repository.query.Param;import org.springframework.stereotype.Repository;import java.time.LocalDateTime;import java.util.List;import java.util.Optional;@Repository public interface UserRepository extends JpaRepository <User, Long> { Optional<User> findByUsername (String username) ; Optional<User> findByEmail (String email) ; List<User> findByIsActiveTrue () ; Page<User> findByIsActiveTrue (Pageable pageable) ; @Query("SELECT u FROM User u WHERE u.username LIKE %:username%") List<User> findByUsernameContaining (@Param("username") String username) ; @Query("SELECT u FROM User u WHERE u.createdAt BETWEEN :startDate AND :endDate") List<User> findByCreatedAtBetween ( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate ) ; @Query("SELECT COUNT(u) FROM User u WHERE u.isActive = true") long countActiveUsers () ; boolean existsByUsername (String username) ; boolean existsByEmail (String email) ; }
4. 服务层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 package com.example.demo.service;import com.example.demo.dto.*;import com.example.demo.entity.User;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import java.util.List;public interface UserService { UserDTO createUser (CreateUserRequest request) ; UserDTO getUserById (Long id) ; UserDTO getUserByUsername (String username) ; List<UserDTO> getAllUsers () ; Page<UserDTO> getUsers (Pageable pageable) ; UserDTO updateUser (Long id, UpdateUserRequest request) ; void deleteUser (Long id) ; UserDTO toggleUserStatus (Long id) ; List<UserDTO> searchUsers (String keyword) ; } package com.example.demo.service.impl;import com.example.demo.dto.*;import com.example.demo.entity.User;import com.example.demo.exception.BusinessException;import com.example.demo.repository.UserRepository;import com.example.demo.service.UserService;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.Cacheable;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.List;import java.util.stream.Collectors;@Service @RequiredArgsConstructor @Slf4j @Transactional(readOnly = true) public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Override @Transactional public UserDTO createUser (CreateUserRequest request) { log.info("Creating user with username: {}" , request.getUsername()); if (userRepository.existsByUsername(request.getUsername())) { throw new BusinessException ("用户名已存在: " + request.getUsername()); } if (userRepository.existsByEmail(request.getEmail())) { throw new BusinessException ("邮箱已存在: " + request.getEmail()); } User user = User.builder() .username(request.getUsername()) .email(request.getEmail()) .phoneNumber(request.getPhoneNumber()) .fullName(request.getFullName()) .build(); User savedUser = userRepository.save(user); log.info("User created successfully with ID: {}" , savedUser.getId()); return convertToDTO(savedUser); } @Override @Cacheable(value = "users", key = "#id") public UserDTO getUserById (Long id) { log.debug("Getting user by ID: {}" , id); User user = userRepository.findById(id) .orElseThrow(() -> new BusinessException ("用户不存在: " + id)); return convertToDTO(user); } @Override @Cacheable(value = "users", key = "#username") public UserDTO getUserByUsername (String username) { log.debug("Getting user by username: {}" , username); User user = userRepository.findByUsername(username) .orElseThrow(() -> new BusinessException ("用户不存在: " + username)); return convertToDTO(user); } @Override public List<UserDTO> getAllUsers () { log.debug("Getting all users" ); return userRepository.findAll().stream() .map(this ::convertToDTO) .collect(Collectors.toList()); } @Override public Page<UserDTO> getUsers (Pageable pageable) { log.debug("Getting users with pagination: {}" , pageable); return userRepository.findAll(pageable) .map(this ::convertToDTO); } @Override @Transactional @CacheEvict(value = "users", key = "#id") public UserDTO updateUser (Long id, UpdateUserRequest request) { log.info("Updating user with ID: {}" , id); User user = userRepository.findById(id) .orElseThrow(() -> new BusinessException ("用户不存在: " + id)); if (request.getUsername() != null && !request.getUsername().equals(user.getUsername()) && userRepository.existsByUsername(request.getUsername())) { throw new BusinessException ("用户名已存在: " + request.getUsername()); } if (request.getEmail() != null && !request.getEmail().equals(user.getEmail()) && userRepository.existsByEmail(request.getEmail())) { throw new BusinessException ("邮箱已存在: " + request.getEmail()); } if (request.getUsername() != null ) { user.setUsername(request.getUsername()); } if (request.getEmail() != null ) { user.setEmail(request.getEmail()); } if (request.getPhoneNumber() != null ) { user.setPhoneNumber(request.getPhoneNumber()); } if (request.getFullName() != null ) { user.setFullName(request.getFullName()); } if (request.getIsActive() != null ) { user.setIsActive(request.getIsActive()); } User updatedUser = userRepository.save(user); log.info("User updated successfully: {}" , updatedUser.getId()); return convertToDTO(updatedUser); } @Override @Transactional @CacheEvict(value = "users", key = "#id") public void deleteUser (Long id) { log.info("Deleting user with ID: {}" , id); if (!userRepository.existsById(id)) { throw new BusinessException ("用户不存在: " + id); } userRepository.deleteById(id); log.info("User deleted successfully: {}" , id); } @Override @Transactional @CacheEvict(value = "users", key = "#id") public UserDTO toggleUserStatus (Long id) { log.info("Toggling user status for ID: {}" , id); User user = userRepository.findById(id) .orElseThrow(() -> new BusinessException ("用户不存在: " + id)); user.setIsActive(!user.getIsActive()); User updatedUser = userRepository.save(user); log.info("User status toggled: {} -> {}" , id, updatedUser.getIsActive()); return convertToDTO(updatedUser); } @Override public List<UserDTO> searchUsers (String keyword) { log.debug("Searching users with keyword: {}" , keyword); return userRepository.findByUsernameContaining(keyword).stream() .map(this ::convertToDTO) .collect(Collectors.toList()); } private UserDTO convertToDTO (User user) { return UserDTO.builder() .id(user.getId()) .username(user.getUsername()) .email(user.getEmail()) .phoneNumber(user.getPhoneNumber()) .fullName(user.getFullName()) .isActive(user.getIsActive()) .createdAt(user.getCreatedAt()) .updatedAt(user.getUpdatedAt()) .build(); } }
5. 控制器层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 package com.example.demo.controller;import com.example.demo.dto.*;import com.example.demo.service.UserService;import io.swagger.v3.oas.annotations.Operation;import io.swagger.v3.oas.annotations.Parameter;import io.swagger.v3.oas.annotations.tags.Tag;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Pageable;import org.springframework.data.domain.Sort;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import javax.validation.Valid;import javax.validation.constraints.Min;import java.util.List;@RestController @RequestMapping("/api/v1/users") @RequiredArgsConstructor @Slf4j @Validated @Tag(name = "用户管理", description = "用户相关的API接口") public class UserController { private final UserService userService; @PostMapping @Operation(summary = "创建用户", description = "创建新的用户") public ResponseEntity<ApiResponse<UserDTO>> createUser ( @Valid @RequestBody CreateUserRequest request) { log.info("Creating user: {}" , request.getUsername()); UserDTO user = userService.createUser(request); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(user, "用户创建成功" )); } @GetMapping("/{id}") @Operation(summary = "获取用户", description = "根据ID获取用户信息") public ResponseEntity<ApiResponse<UserDTO>> getUserById ( @Parameter(description = "用户ID") @PathVariable @Min(1) Long id) { log.debug("Getting user by ID: {}" , id); UserDTO user = userService.getUserById(id); return ResponseEntity.ok(ApiResponse.success(user)); } @GetMapping @Operation(summary = "获取用户列表", description = "分页获取用户列表") public ResponseEntity<ApiResponse<Page<UserDTO>>> getUsers ( @Parameter(description = "页码,从0开始") @RequestParam(defaultValue = "0") @Min(0) int page, @Parameter(description = "每页大小") @RequestParam(defaultValue = "10") @Min(1) int size, @Parameter(description = "排序字段") @RequestParam(defaultValue = "id") String sortBy, @Parameter(description = "排序方向") @RequestParam(defaultValue = "asc") String sortDir) { log.debug("Getting users - page: {}, size: {}, sortBy: {}, sortDir: {}" , page, size, sortBy, sortDir); Sort sort = sortDir.equalsIgnoreCase("desc" ) ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); Pageable pageable = PageRequest.of(page, size, sort); Page<UserDTO> users = userService.getUsers(pageable); return ResponseEntity.ok(ApiResponse.success(users)); } @PutMapping("/{id}") @Operation(summary = "更新用户", description = "更新用户信息") public ResponseEntity<ApiResponse<UserDTO>> updateUser ( @Parameter(description = "用户ID") @PathVariable @Min(1) Long id, @Valid @RequestBody UpdateUserRequest request) { log.info("Updating user: {}" , id); UserDTO user = userService.updateUser(id, request); return ResponseEntity.ok(ApiResponse.success(user, "用户更新成功" )); } @DeleteMapping("/{id}") @Operation(summary = "删除用户", description = "删除指定用户") public ResponseEntity<ApiResponse<Void>> deleteUser ( @Parameter(description = "用户ID") @PathVariable @Min(1) Long id) { log.info("Deleting user: {}" , id); userService.deleteUser(id); return ResponseEntity.ok(ApiResponse.success(null , "用户删除成功" )); } @PatchMapping("/{id}/toggle-status") @Operation(summary = "切换用户状态", description = "激活或禁用用户") public ResponseEntity<ApiResponse<UserDTO>> toggleUserStatus ( @Parameter(description = "用户ID") @PathVariable @Min(1) Long id) { log.info("Toggling user status: {}" , id); UserDTO user = userService.toggleUserStatus(id); return ResponseEntity.ok(ApiResponse.success(user, "用户状态更新成功" )); } @GetMapping("/search") @Operation(summary = "搜索用户", description = "根据关键词搜索用户") public ResponseEntity<ApiResponse<List<UserDTO>>> searchUsers ( @Parameter(description = "搜索关键词") @RequestParam String keyword) { log.debug("Searching users with keyword: {}" , keyword); List<UserDTO> users = userService.searchUsers(keyword); return ResponseEntity.ok(ApiResponse.success(users)); } }
6. 统一响应格式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.example.demo.dto;import lombok.Data;import lombok.Builder;import lombok.AllArgsConstructor;import lombok.NoArgsConstructor;import com.fasterxml.jackson.annotation.JsonFormat;import java.time.LocalDateTime;@Data @Builder @AllArgsConstructor @NoArgsConstructor public class ApiResponse <T> { private boolean success; private String message; private T data; private String errorCode; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Builder .Default private LocalDateTime timestamp = LocalDateTime.now(); public static <T> ApiResponse<T> success (T data) { return ApiResponse.<T>builder() .success(true ) .data(data) .message("操作成功" ) .build(); } public static <T> ApiResponse<T> success (T data, String message) { return ApiResponse.<T>builder() .success(true ) .data(data) .message(message) .build(); } public static <T> ApiResponse<T> error (String message) { return ApiResponse.<T>builder() .success(false ) .message(message) .build(); } public static <T> ApiResponse<T> error (String message, String errorCode) { return ApiResponse.<T>builder() .success(false ) .message(message) .errorCode(errorCode) .build(); } }
四、数据访问与持久化 (一)Spring Data JPA 1. 高级查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package com.example.demo.repository;import com.example.demo.entity.User;import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.JpaSpecificationExecutor;import org.springframework.data.jpa.repository.Modifying;import org.springframework.data.jpa.repository.Query;import org.springframework.data.repository.query.Param;import java.time.LocalDateTime;import java.util.List;public interface UserRepository extends JpaRepository <User, Long>, JpaSpecificationExecutor<User> { @Query(value = "SELECT * FROM users u WHERE u.created_at >= :startDate", nativeQuery = true) List<User> findUsersCreatedAfter (@Param("startDate") LocalDateTime startDate) ; @Modifying @Query("UPDATE User u SET u.isActive = :status WHERE u.id IN :ids") int updateUserStatus (@Param("ids") List<Long> ids, @Param("status") Boolean status) ; @Query("SELECT u FROM User u WHERE " + "(:username IS NULL OR u.username LIKE %:username%) AND " + "(:email IS NULL OR u.email LIKE %:email%) AND " + "(:isActive IS NULL OR u.isActive = :isActive)") Page<User> findUsersWithFilters ( @Param("username") String username, @Param("email") String email, @Param("isActive") Boolean isActive, Pageable pageable ) ;} package com.example.demo.specification;import com.example.demo.entity.User;import org.springframework.data.jpa.domain.Specification;import javax.persistence.criteria.Predicate;import java.time.LocalDateTime;import java.util.ArrayList;import java.util.List;public class UserSpecification { public static Specification<User> withFilters ( String username, String email, Boolean isActive, LocalDateTime startDate, LocalDateTime endDate) { return (root, query, criteriaBuilder) -> { List<Predicate> predicates = new ArrayList <>(); if (username != null && !username.trim().isEmpty()) { predicates.add(criteriaBuilder.like( criteriaBuilder.lower(root.get("username" )), "%" + username.toLowerCase() + "%" )); } if (email != null && !email.trim().isEmpty()) { predicates.add(criteriaBuilder.like( criteriaBuilder.lower(root.get("email" )), "%" + email.toLowerCase() + "%" )); } if (isActive != null ) { predicates.add(criteriaBuilder.equal(root.get("isActive" ), isActive)); } if (startDate != null ) { predicates.add(criteriaBuilder.greaterThanOrEqualTo( root.get("createdAt" ), startDate )); } if (endDate != null ) { predicates.add(criteriaBuilder.lessThanOrEqualTo( root.get("createdAt" ), endDate )); } return criteriaBuilder.and(predicates.toArray(new Predicate [0 ])); }; } }
2. 事务管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 package com.example.demo.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.orm.jpa.JpaTransactionManager;import org.springframework.transaction.PlatformTransactionManager;import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.persistence.EntityManagerFactory;@Configuration @EnableTransactionManagement public class TransactionConfig { @Bean public PlatformTransactionManager transactionManager ( EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager (entityManagerFactory); } } package com.example.demo.service.impl;import com.example.demo.entity.User;import com.example.demo.entity.Order;import com.example.demo.repository.UserRepository;import com.example.demo.repository.OrderRepository;import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Isolation;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;@Service @RequiredArgsConstructor public class OrderService { private final UserRepository userRepository; private final OrderRepository orderRepository; @Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, timeout = 30 ) public Order createOrder (Long userId, OrderRequest request) { User user = userRepository.findById(userId) .orElseThrow(() -> new BusinessException ("用户不存在" )); Order order = Order.builder() .user(user) .amount(request.getAmount()) .status(OrderStatus.PENDING) .build(); Order savedOrder = orderRepository.save(order); notificationService.sendOrderNotification(savedOrder); return savedOrder; } @Transactional(propagation = Propagation.REQUIRES_NEW) public void logOrderActivity (Long orderId, String activity) { OrderLog log = OrderLog.builder() .orderId(orderId) .activity(activity) .timestamp(LocalDateTime.now()) .build(); orderLogRepository.save(log); } }
(二)多数据源配置 1. 多数据源配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 package com.example.demo.config;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.jdbc.DataSourceBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.data.jpa.repository.config.EnableJpaRepositories;import org.springframework.orm.jpa.JpaTransactionManager;import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;import org.springframework.transaction.PlatformTransactionManager;import javax.persistence.EntityManagerFactory;import javax.sql.DataSource;import java.util.Properties;@Configuration public class DatabaseConfig { @Primary @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource () { return DataSourceBuilder.create().build(); } @Bean(name = "secondaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.secondary") public DataSource secondaryDataSource () { return DataSourceBuilder.create().build(); } @Primary @Bean(name = "primaryEntityManagerFactory") public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory ( @Qualifier("primaryDataSource") DataSource dataSource) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean (); em.setDataSource(dataSource); em.setPackagesToScan("com.example.demo.entity.primary" ); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter (); em.setJpaVendorAdapter(vendorAdapter); Properties properties = new Properties (); properties.setProperty("hibernate.hbm2ddl.auto" , "update" ); properties.setProperty("hibernate.dialect" , "org.hibernate.dialect.MySQL8Dialect" ); em.setJpaProperties(properties); return em; } @Bean(name = "secondaryEntityManagerFactory") public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory ( @Qualifier("secondaryDataSource") DataSource dataSource) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean (); em.setDataSource(dataSource); em.setPackagesToScan("com.example.demo.entity.secondary" ); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter (); em.setJpaVendorAdapter(vendorAdapter); Properties properties = new Properties (); properties.setProperty("hibernate.hbm2ddl.auto" , "update" ); properties.setProperty("hibernate.dialect" , "org.hibernate.dialect.MySQL8Dialect" ); em.setJpaProperties(properties); return em; } @Primary @Bean(name = "primaryTransactionManager") public PlatformTransactionManager primaryTransactionManager ( @Qualifier("primaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager (entityManagerFactory); } @Bean(name = "secondaryTransactionManager") public PlatformTransactionManager secondaryTransactionManager ( @Qualifier("secondaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager (entityManagerFactory); } } @Configuration @EnableJpaRepositories( basePackages = "com.example.demo.repository.primary", entityManagerFactoryRef = "primaryEntityManagerFactory", transactionManagerRef = "primaryTransactionManager" ) public class PrimaryDataSourceConfig {} @Configuration @EnableJpaRepositories( basePackages = "com.example.demo.repository.secondary", entityManagerFactoryRef = "secondaryEntityManagerFactory", transactionManagerRef = "secondaryTransactionManager" ) public class SecondaryDataSourceConfig {}
2. 读写分离配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 spring: datasource: primary: url: jdbc:mysql://master-db:3306/demo username: ${DB_USERNAME} password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 secondary: url: jdbc:mysql://slave-db:3306/demo username: ${DB_USERNAME} password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2
五、安全与认证 (一)Spring Security配置 1. 基础安全配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package com.example.demo.config;import com.example.demo.security.JwtAuthenticationEntryPoint;import com.example.demo.security.JwtAuthenticationFilter;import lombok.RequiredArgsConstructor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.SecurityFilterChain;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.CorsConfigurationSource;import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import java.util.Arrays;@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } @Bean public AuthenticationManager authenticationManager ( AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain (HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(authz -> authz .antMatchers("/api/v1/auth/**" ).permitAll() .antMatchers("/api/v1/public/**" ).permitAll() .antMatchers("/swagger-ui/**" , "/v3/api-docs/**" ).permitAll() .antMatchers("/actuator/health" , "/actuator/info" ).permitAll() .antMatchers("/api/v1/admin/**" ).hasRole("ADMIN" ) .antMatchers("/api/v1/users/**" ).hasAnyRole("USER" , "ADMIN" ) .anyRequest().authenticated() ); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource () { CorsConfiguration configuration = new CorsConfiguration (); configuration.setAllowedOriginPatterns(Arrays.asList("*" )); configuration.setAllowedMethods(Arrays.asList("GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" )); configuration.setAllowedHeaders(Arrays.asList("*" )); configuration.setAllowCredentials(true ); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (); source.registerCorsConfiguration("/**" , configuration); return source; } }
2. JWT认证实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 package com.example.demo.security;import io.jsonwebtoken.*;import io.jsonwebtoken.security.Keys;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.security.core.Authentication;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.stereotype.Component;import javax.crypto.SecretKey;import java.util.Date;@Component @Slf4j public class JwtUtil { @Value("${app.jwt.secret}") private String jwtSecret; @Value("${app.jwt.expiration}") private int jwtExpirationMs; private SecretKey getSigningKey () { return Keys.hmacShaKeyFor(jwtSecret.getBytes()); } public String generateJwtToken (Authentication authentication) { UserDetails userPrincipal = (UserDetails) authentication.getPrincipal(); return Jwts.builder() .setSubject(userPrincipal.getUsername()) .setIssuedAt(new Date ()) .setExpiration(new Date (System.currentTimeMillis() + jwtExpirationMs)) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public String generateJwtToken (String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date ()) .setExpiration(new Date (System.currentTimeMillis() + jwtExpirationMs)) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public String getUsernameFromJwtToken (String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody() .getSubject(); } public boolean validateJwtToken (String authToken) { try { Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(authToken); return true ; } catch (SecurityException e) { log.error("Invalid JWT signature: {}" , e.getMessage()); } catch (MalformedJwtException e) { log.error("Invalid JWT token: {}" , e.getMessage()); } catch (ExpiredJwtException e) { log.error("JWT token is expired: {}" , e.getMessage()); } catch (UnsupportedJwtException e) { log.error("JWT token is unsupported: {}" , e.getMessage()); } catch (IllegalArgumentException e) { log.error("JWT claims string is empty: {}" , e.getMessage()); } return false ; } public Date getExpirationDateFromJwtToken (String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody() .getExpiration(); } } package com.example.demo.security;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Component @RequiredArgsConstructor @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = parseJwt(request); if (jwt != null && jwtUtil.validateJwtToken(jwt)) { String username = jwtUtil.getUsernameFromJwtToken(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) { log.error("Cannot set user authentication: {}" , e.getMessage()); } filterChain.doFilter(request, response); } private String parseJwt (HttpServletRequest request) { String headerAuth = request.getHeader("Authorization" ); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer " )) { return headerAuth.substring(7 ); } return null ; } }
3. 认证控制器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 package com.example.demo.controller;import com.example.demo.dto.*;import com.example.demo.security.JwtUtil;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.http.ResponseEntity;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.web.bind.annotation.*;import javax.validation.Valid;@RestController @RequestMapping("/api/v1/auth") @RequiredArgsConstructor @Slf4j public class AuthController { private final AuthenticationManager authenticationManager; private final JwtUtil jwtUtil; private final UserService userService; @PostMapping("/login") public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { try { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken ( loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtUtil.generateJwtToken(authentication); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); List<String> roles = userDetails.getAuthorities().stream() .map(item -> item.getAuthority()) .collect(Collectors.toList()); return ResponseEntity.ok(new JwtResponse (jwt, userDetails.getUsername(), roles)); } catch (BadCredentialsException e) { return ResponseEntity.badRequest() .body(new MessageResponse ("用户名或密码错误" )); } } @PostMapping("/register") public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) { if (userService.existsByUsername(signUpRequest.getUsername())) { return ResponseEntity.badRequest() .body(new MessageResponse ("用户名已存在" )); } if (userService.existsByEmail(signUpRequest.getEmail())) { return ResponseEntity.badRequest() .body(new MessageResponse ("邮箱已被使用" )); } User user = new User (signUpRequest.getUsername(), signUpRequest.getEmail(), passwordEncoder.encode(signUpRequest.getPassword())); userService.save(user); return ResponseEntity.ok(new MessageResponse ("用户注册成功" )); } @PostMapping("/refresh") public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) { String requestRefreshToken = request.getRefreshToken(); return refreshTokenService.findByToken(requestRefreshToken) .map(refreshTokenService::verifyExpiration) .map(RefreshToken::getUser) .map(user -> { String token = jwtUtil.generateJwtToken(user.getUsername()); return ResponseEntity.ok(new TokenRefreshResponse (token, requestRefreshToken)); }) .orElseThrow(() -> new TokenRefreshException (requestRefreshToken, "刷新令牌不在数据库中" )); } @PostMapping("/logout") public ResponseEntity<?> logoutUser() { UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext() .getAuthentication().getPrincipal(); refreshTokenService.deleteByUserId(userDetails.getUsername()); return ResponseEntity.ok(new MessageResponse ("注销成功" )); } } @Data @NoArgsConstructor @AllArgsConstructor class LoginRequest { @NotBlank private String username; @NotBlank private String password; } @Data @NoArgsConstructor @AllArgsConstructor class SignupRequest { @NotBlank @Size(min = 3, max = 20) private String username; @NotBlank @Size(max = 50) @Email private String email; @NotBlank @Size(min = 6, max = 40) private String password; } @Data @NoArgsConstructor @AllArgsConstructor class JwtResponse { private String token; private String type = "Bearer" ; private String username; private List<String> roles; public JwtResponse (String accessToken, String username, List<String> roles) { this .token = accessToken; this .username = username; this .roles = roles; } } @Data @NoArgsConstructor @AllArgsConstructor class MessageResponse { private String message; } @Data @NoArgsConstructor @AllArgsConstructor class TokenRefreshRequest { @NotBlank private String refreshToken; } @Data @NoArgsConstructor @AllArgsConstructor class TokenRefreshResponse { private String accessToken; private String refreshToken; private String tokenType = "Bearer" ; public TokenRefreshResponse (String accessToken, String refreshToken) { this .accessToken = accessToken; this .refreshToken = refreshToken; } }
(四)用户详情服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 package com.example.demo.security;import com.example.demo.entity.User;import com.example.demo.repository.UserRepository;import lombok.RequiredArgsConstructor;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository userRepository; @Override @Transactional public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException ( "用户不存在: " + username)); return UserPrincipal.create(user); } } package com.example.demo.security;import com.example.demo.entity.User;import com.fasterxml.jackson.annotation.JsonIgnore;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;import java.util.List;import java.util.Objects;import java.util.stream.Collectors;@Data @NoArgsConstructor @AllArgsConstructor public class UserPrincipal implements UserDetails { private Long id; private String username; private String email; @JsonIgnore private String password; private Collection<? extends GrantedAuthority > authorities; public static UserPrincipal create (User user) { List<GrantedAuthority> authorities = user.getRoles().stream() .map(role -> new SimpleGrantedAuthority ("ROLE_" + role.getName().name())) .collect(Collectors.toList()); return new UserPrincipal ( user.getId(), user.getUsername(), user.getEmail(), user.getPassword(), authorities ); } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return true ; } @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; UserPrincipal that = (UserPrincipal) o; return Objects.equals(id, that.id); } @Override public int hashCode () { return Objects.hash(id); } }
十、微服务架构 (一)Spring Cloud集成 1. 服务注册与发现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> @SpringBootApplication @EnableEurekaClient public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } } spring: application: name: user-service eureka: client: service-url: defaultZone: http: instance: prefer-ip-address: true
2. 服务间通信 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @FeignClient(name = "order-service") public interface OrderServiceClient { @GetMapping("/api/orders/user/{userId}") List<Order> getOrdersByUserId (@PathVariable("userId") Long userId) ; @PostMapping("/api/orders") Order createOrder (@RequestBody CreateOrderRequest request) ; } @Service @RequiredArgsConstructor public class UserOrderService { private final OrderServiceClient orderServiceClient; public List<Order> getUserOrders (Long userId) { return orderServiceClient.getOrdersByUserId(userId); } }
(二)配置中心 1 2 3 4 5 6 7 8 9 spring: application: name: user-service cloud: config: uri: http://localhost:8888 profile: dev label: master
(三)API网关 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class GatewayConfig { @Bean public RouteLocator customRouteLocator (RouteLocatorBuilder builder) { return builder.routes() .route("user-service" , r -> r.path("/api/users/**" ) .uri("lb://user-service" )) .route("order-service" , r -> r.path("/api/orders/**" ) .uri("lb://order-service" )) .build(); } }
十一、部署与运维 (一)Docker容器化 1. Dockerfile 1 2 3 4 5 6 7 8 9 10 11 12 FROM maven:3.8 .4 -openjdk-17 AS buildWORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:17 -jre-slimWORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java" , "-jar" , "app.jar" ]
2. Docker Compose 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 version: '3.8' services: app: build: . ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=docker - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/demo depends_on: - db - redis db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: demo ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql redis: image: redis:7-alpine ports: - "6379:6379" volumes: mysql_data:
(二)Kubernetes部署 1. Deployment配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 apiVersion: apps/v1 kind: Deployment metadata: name: spring-boot-app spec: replicas: 3 selector: matchLabels: app: spring-boot-app template: metadata: labels: app: spring-boot-app spec: containers: - name: app image: your-registry/spring-boot-app:latest ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "k8s" resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 5 periodSeconds: 5
2. Service配置 1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Service metadata: name: spring-boot-service spec: selector: app: spring-boot-app ports: - protocol: TCP port: 80 targetPort: 8080 type: LoadBalancer
(三)CI/CD流水线 1. GitHub Actions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 name: CI/CD Pipeline on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Run tests run: mvn test build-and-deploy: needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Build with Maven run: mvn clean package - name: Build Docker image run: docker build -t ${{ secrets.DOCKER_REGISTRY }}/spring-boot-app:${{ github.sha }} . - name: Push to registry run: | echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin docker push ${{ secrets.DOCKER_REGISTRY }}/spring-boot-app:${{ github.sha }}
十二、性能优化 (一)JVM调优 1. JVM参数配置 1 2 3 4 5 6 7 8 java -Xms2g -Xmx4g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/logs/heapdump.hprof \ -Dspring.profiles.active=prod \ -jar app.jar
2. GC监控 1 2 3 4 5 6 7 8 9 10 11 12 @Component @Slf4j public class GCMonitor { @EventListener public void handleGCEvent (GarbageCollectionNotificationInfo info) { GcInfo gcInfo = info.getGcInfo(); log.info("GC occurred: {} - Duration: {}ms" , info.getGcName(), gcInfo.getDuration()); } }
(二)数据库优化 1. 连接池配置 1 2 3 4 5 6 7 8 9 spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 leak-detection-threshold: 60000
2. 查询优化 1 2 3 4 5 6 7 8 9 10 11 12 @Repository public interface UserRepository extends JpaRepository <User, Long> { @Query(value = "SELECT u FROM User u WHERE u.status = :status", countQuery = "SELECT count(u) FROM User u WHERE u.status = :status") Page<User> findByStatus (@Param("status") String status, Pageable pageable) ; @Query("SELECT u FROM User u WHERE u.email = :email") Optional<User> findByEmailOptimized (@Param("email") String email) ; }
(三)缓存策略 1. 多级缓存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Service public class UserCacheService { private final Cache<String, User> localCache = Caffeine.newBuilder() .maximumSize(1000 ) .expireAfterWrite(Duration.ofMinutes(10 )) .build(); @Cacheable(value = "users", key = "#id") public User getUserById (Long id) { return userRepository.findById(id).orElse(null ); } }
十三、监控与日志 (一)应用监控 1. Micrometer集成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Component public class CustomMetrics { private final Counter userRegistrationCounter; private final Timer userLoginTimer; public CustomMetrics (MeterRegistry meterRegistry) { this .userRegistrationCounter = Counter.builder("user.registration" ) .description("用户注册计数" ) .register(meterRegistry); this .userLoginTimer = Timer.builder("user.login.duration" ) .description("用户登录耗时" ) .register(meterRegistry); } public void incrementUserRegistration () { userRegistrationCounter.increment(); } public Timer.Sample startLoginTimer () { return Timer.start(); } }
2. 健康检查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Component public class DatabaseHealthIndicator implements HealthIndicator { @Autowired private DataSource dataSource; @Override public Health health () { try (Connection connection = dataSource.getConnection()) { if (connection.isValid(1 )) { return Health.up() .withDetail("database" , "Available" ) .build(); } } catch (Exception e) { return Health.down() .withDetail("database" , "Unavailable" ) .withException(e) .build(); } return Health.down().build(); } }
(二)日志管理 1. 结构化日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j @RestController public class UserController { @PostMapping("/users") public ResponseEntity<User> createUser (@RequestBody User user) { MDC.put("operation" , "createUser" ); MDC.put("userId" , user.getId().toString()); try { User savedUser = userService.save(user); log.info("User created successfully" ); return ResponseEntity.ok(savedUser); } catch (Exception e) { log.error("Failed to create user" , e); throw e; } finally { MDC.clear(); } } }
2. 日志配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <configuration > <springProfile name ="!prod" > <appender name ="CONSOLE" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern > </encoder > </appender > <root level ="INFO" > <appender-ref ref ="CONSOLE" /> </root > </springProfile > <springProfile name ="prod" > <appender name ="FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <file > logs/application.log</file > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <fileNamePattern > logs/application.%d{yyyy-MM-dd}.%i.gz</fileNamePattern > <maxFileSize > 100MB</maxFileSize > <maxHistory > 30</maxHistory > <totalSizeCap > 3GB</totalSizeCap > </rollingPolicy > <encoder class ="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder" > <providers > <timestamp /> <logLevel /> <loggerName /> <message /> <mdc /> <stackTrace /> </providers > </encoder > </appender > <root level ="INFO" > <appender-ref ref ="FILE" /> </root > </springProfile > </configuration >
总结 Spring Boot作为现代Java企业级应用开发的首选框架,具有以下核心优势:
核心优势 开箱即用 :提供大量自动配置,减少样板代码生产就绪 :内置监控、健康检查、指标收集等功能微服务友好 :与Spring Cloud无缝集成社区活跃 :丰富的第三方集成和社区支持测试友好 :提供完善的测试支持和工具适用场景 企业级Web应用 :复杂的业务逻辑和数据处理微服务架构 :分布式系统和服务治理RESTful API :后端服务和接口开发数据处理应用 :批处理和流处理云原生应用 :容器化和Kubernetes部署最佳实践 遵循约定优于配置 :使用Spring Boot的默认配置合理使用自动配置 :理解自动配置原理,必要时自定义分层架构设计 :Controller、Service、Repository分离异常处理统一 :使用全局异常处理器安全第一 :实施适当的安全措施性能监控 :集成监控和日志系统测试驱动 :编写全面的单元测试和集成测试Spring Boot为Java开发者提供了一个强大、灵活且易于使用的开发平台,是构建现代企业级应用的理想选择。通过合理运用其特性和最佳实践,可以显著提高开发效率和应用质量。