Spring/Spring Boot实战:从入门到项目部署

Spring Boot是目前最流行的Java企业级应用开发框架,本文将通过一个完整的项目实例,从环境搭建到项目部署,全面讲解Spring Boot的核心特性和实战应用。

1. Spring Boot概述

1.1 什么是Spring Boot?

Spring Boot是由Pivotal团队提供的框架,其设计目的是简化Spring应用的创建、配置和部署过程。

Spring Boot的核心优势

  • 快速开发:开箱即用,零配置
  • 内嵌服务器:无需部署到外部Tomcat
  • 自动配置:根据类路径自动配置
  • 健康检查:内置Actuator监控
  • 微服务友好:天然支持微服务架构

1.2 Spring Boot版本选择

版本 特性 适用场景
2.7.x 稳定版本 生产环境推荐
3.x Java 17+、Spring 6 新项目推荐

本文基于Spring Boot 3.x版本

2. 环境搭建

2.1 开发工具配置

JDK版本要求

bash 复制代码
# 检查Java版本
java -version

# 需要Java 17或更高版本
openjdk version "17.0.8" 2023-07-18

Maven配置(settings.xml):

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<settings>
    <mirrors>
        <mirror>
            <id>aliyun</id>
            <mirrorOf>central</mirrorOf>
            <name>Aliyun Maven</name>
            <url>https://maven.aliyun.com/repository/central</url>
        </mirror>
    </mirrors>
    
    <profiles>
        <profile>
            <id>jdk-17</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <maven.compiler.source>17</maven.compiler.source>
                <maven.compiler.target>17</maven.compiler.target>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            </properties>
        </profile>
    </profiles>
</settings>

2.2 Spring Initializr创建项目

方式一:在线创建

访问 https://start.spring.io/,选择:

  • Project: Maven
  • Language: Java
  • Spring Boot: 3.x
  • Packaging: Jar
  • Java: 17+

方式二:命令行创建

bash 复制代码
# 使用Spring Boot CLI
spring init demo-project \
  --dependencies=web,data-jpa,mysql,security \
  --groupId=com.example \
  --artifactId=demo \
  --package-name=com.example.demo \
  --version=1.0.0

# 或者使用cURL
curl https://start.spring.io/starter.zip \
  -d dependencies=web,data-jpa,mysql \
  -d groupId=com.example \
  -d artifactId=demo \
  -d name=demo \
  -d baseDir=demo \
  -o demo.zip

3. 项目结构详解

3.1 标准项目结构

复制代码
demo/
├── pom.xml                          # Maven配置
├── mvnw                             # Maven Wrapper脚本
├── mvnw.cmd
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── demo/
│   │   │               ├── DemoApplication.java    # 启动类
│   │   │               ├── config/                   # 配置类
│   │   │               ├── controller/              # 控制器层
│   │   │               ├── service/                 # 服务层
│   │   │               ├── repository/             # 数据访问层
│   │   │               ├── entity/                  # 实体类
│   │   │               ├── dto/                     # 数据传输对象
│   │   │               ├── mapper/                  # MyBatis映射器
│   │   │               ├── security/                # 安全配置
│   │   │               └── exception/               # 异常处理
│   │   └── resources/
│   │       ├── application.yml                     # 配置文件
│   │       ├── application-dev.yml                 # 开发环境配置
│   │       ├── application-prod.yml                # 生产环境配置
│   │       ├── static/                             # 静态资源
│   │       └── templates/                          # 模板文件
│   └── test/
│       └── java/
│           └── com/
│               └── example/
│                   └── demo/
│                       └── DemoApplicationTests.java  # 单元测试
└── target/                           # 编译输出目录

3.2 核心配置文件

application.yml

yaml 复制代码
server:
  port: 8080
  servlet:
    context-path: /api

spring:
  application:
    name: demo
  
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      minimum-idle: 5
      maximum-pool-size: 20
      idle-timeout: 30000
      pool-name: DemoHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
  
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect
        format_sql: true
    open-in-view: false

  # Redis配置
  data:
    redis:
      host: localhost
      port: 6379
      password: 
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 2

# MyBatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.demo.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 日志配置
logging:
  level:
    root: INFO
    com.example.demo: DEBUG
    org.hibernate.SQL: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

# Actuator配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: always

4. 核心功能实现

4.1 启动类配置

DemoApplication.java

java 复制代码
package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@MapperScan("com.example.demo.mapper")
@EnableCaching          // 开启缓存
@EnableAsync             // 开启异步
@EnableScheduling        // 开启定时任务
public class DemoApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        System.out.println("🚀 Demo Application Started Successfully!");
    }
}

4.2 实体类设计

User.java

java 复制代码
package com.example.demo.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 用户实体类
 */
@Entity
@Table(name = "users")
@Data
public class User implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    @Column(length = 100)
    private String email;
    
    @Column(length = 20)
    private String phone;
    
    @Enumerated(EnumType.STRING)
    @Column(length = 20)
    private UserStatus status = UserStatus.ACTIVE;
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
    
    public enum UserStatus {
        ACTIVE,    // 活跃
        INACTIVE,  // 非活跃
        LOCKED     // 锁定
    }
}

4.3 Repository层

UserRepository.java

java 复制代码
package com.example.demo.repository;

import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
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.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>, JpaSpecificationExecutor<User> {
    
    // 根据用户名查询
    Optional<User> findByUsername(String username);
    
    // 根据邮箱查询
    Optional<User> findByEmail(String email);
    
    // 根据状态查询
    List<User> findByStatus(UserStatus status);
    
    // 分页查询
    Page<User> findByStatus(UserStatus status, Pageable pageable);
    
    // 自定义查询
    @Query("SELECT u FROM User u WHERE u.username = :username AND u.status = :status")
    Optional<User> findByUsernameAndStatus(
        @Param("username") String username,
        @Param("status") UserStatus status
    );
    
    // 统计用户数量
    long countByStatus(UserStatus status);
    
    // 模糊查询
    List<User> findByUsernameContainingIgnoreCase(String username);
    
    // 原生查询
    @Query(value = "SELECT * FROM users WHERE created_at > :startDate ORDER BY created_at DESC", 
           nativeQuery = true)
    List<User> findRecentUsers(@Param("startDate") LocalDateTime startDate);
    
    // 批量删除
    void deleteByStatus(UserStatus status);
}

4.4 Service层

UserService.java

java 复制代码
package com.example.demo.service;

import com.example.demo.dto.UserCreateDTO;
import com.example.demo.dto.UserUpdateDTO;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import com.example.demo.exception.BusinessException;
import com.example.demo.repository.UserRepository;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
    
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    
    /**
     * 查询所有用户(带缓存)
     */
    @Cacheable(value = "users", key = "'all'")
    public List<User> findAll() {
        log.info("查询所有用户");
        return userRepository.findAll();
    }
    
    /**
     * 分页查询用户
     */
    public Page<User> findByPage(UserStatus status, Pageable pageable) {
        return userRepository.findByStatus(status, pageable);
    }
    
    /**
     * 根据ID查询用户
     */
    @Cacheable(value = "users", key = "#id")
    public User findById(Long id) {
        log.info("查询用户: {}", id);
        return userRepository.findById(id)
            .orElseThrow(() -> new BusinessException("用户不存在"));
    }
    
    /**
     * 根据用户名查询
     */
    public User findByUsername(String username) {
        return userRepository.findByUsername(username)
            .orElseThrow(() -> new BusinessException("用户不存在"));
    }
    
    /**
     * 创建用户
     */
    @Transactional
    @CacheEvict(value = "users", allEntries = true)
    public User create(UserCreateDTO createDTO) {
        log.info("创建用户: {}", createDTO.getUsername());
        
        // 检查用户名是否已存在
        if (userRepository.findByUsername(createDTO.getUsername()).isPresent()) {
            throw new BusinessException("用户名已存在");
        }
        
        // 检查邮箱是否已存在
        if (createDTO.getEmail() != null && 
            userRepository.findByEmail(createDTO.getEmail()).isPresent()) {
            throw new BusinessException("邮箱已被注册");
        }
        
        // 创建用户
        User user = new User();
        user.setUsername(createDTO.getUsername());
        user.setPassword(passwordEncoder.encode(createDTO.getPassword()));
        user.setEmail(createDTO.getEmail());
        user.setPhone(createDTO.getPhone());
        user.setStatus(UserStatus.ACTIVE);
        
        return userRepository.save(user);
    }
    
    /**
     * 更新用户
     */
    @Transactional
    @CacheEvict(value = "users", key = "#id")
    public User update(Long id, UserUpdateDTO updateDTO) {
        log.info("更新用户: {}", id);
        
        User user = findById(id);
        
        if (updateDTO.getEmail() != null) {
            user.setEmail(updateDTO.getEmail());
        }
        if (updateDTO.getPhone() != null) {
            user.setPhone(updateDTO.getPhone());
        }
        if (updateDTO.getStatus() != null) {
            user.setStatus(updateDTO.getStatus());
        }
        
        return userRepository.save(user);
    }
    
    /**
     * 删除用户
     */
    @Transactional
    @CacheEvict(value = "users", allEntries = true)
    public void delete(Long id) {
        log.info("删除用户: {}", id);
        
        if (!userRepository.existsById(id)) {
            throw new BusinessException("用户不存在");
        }
        
        userRepository.deleteById(id);
    }
    
    /**
     * 批量删除
     */
    @Transactional
    @CacheEvict(value = "users", allEntries = true)
    public void batchDelete(List<Long> ids) {
        log.info("批量删除用户: {}", ids);
        userRepository.deleteAllById(ids);
    }
}

4.5 Controller层

UserController.java

java 复制代码
package com.example.demo.controller;

import com.example.demo.dto.*;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    
    private final UserService userService;
    
    /**
     * 获取所有用户
     */
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        return ResponseEntity.ok(userService.findAll());
    }
    
    /**
     * 分页查询用户
     */
    @GetMapping("/page")
    public ResponseEntity<Page<User>> getUsersByPage(
            @RequestParam(required = false) UserStatus status,
            @PageableDefault(size = 10, sort = "createdAt") Pageable pageable) {
        return ResponseEntity.ok(userService.findByPage(status, pageable));
    }
    
    /**
     * 根据ID查询用户
     */
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }
    
    /**
     * 创建用户
     */
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateDTO createDTO) {
        User user = userService.create(createDTO);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
    
    /**
     * 更新用户
     */
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UserUpdateDTO updateDTO) {
        return ResponseEntity.ok(userService.update(id, updateDTO));
    }
    
    /**
     * 删除用户
     */
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
    
    /**
     * 批量删除用户
     */
    @DeleteMapping("/batch")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> batchDeleteUsers(@RequestBody List<Long> ids) {
        userService.batchDelete(ids);
        return ResponseEntity.noContent().build();
    }
}

5. 数据访问层

5.1 JPA动态查询

UserSpecification.java

java 复制代码
package com.example.demo.specification;

import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import jakarta.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class UserSpecification {
    
    public static Specification<User> withSearch(String username, 
                                               UserStatus status,
                                               LocalDateTime startDate,
                                               LocalDateTime endDate) {
        return (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            if (username != null && !username.isEmpty()) {
                predicates.add(criteriaBuilder.like(
                    criteriaBuilder.lower(root.get("username")),
                    "%" + username.toLowerCase() + "%"
                ));
            }
            
            if (status != null) {
                predicates.add(criteriaBuilder.equal(root.get("status"), status));
            }
            
            if (startDate != null) {
                predicates.add(criteriaBuilder.greaterThanOrEqualTo(
                    root.get("createdAt"), startDate
                ));
            }
            
            if (endDate != null) {
                predicates.add(criteriaBuilder.lessThanOrEqualTo(
                    root.get("createdAt"), endDate
                ));
            }
            
            query.orderBy(criteriaBuilder.desc(root.get("createdAt")));
            
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
}

6. 安全性配置

6.1 Spring Security配置

SecurityConfig.java

java 复制代码
package com.example.demo.config;

import com.example.demo.security.JwtAuthenticationFilter;
import com.example.demo.security.JwtTokenProvider;
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.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    
    private final JwtTokenProvider jwtTokenProvider;
    
    @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
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .requestMatchers("/actuator/**").permitAll()
                .requestMatchers("/users/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(
                new JwtAuthenticationFilter(jwtTokenProvider),
                UsernamePasswordAuthenticationFilter.class
            );
        
        return http.build();
    }
}

7. 缓存配置

7.1 Redis缓存配置

RedisConfig.java

java 复制代码
package com.example.demo.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new StringRedisSerializer()))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new GenericJackson2JsonRedisSerializer()))
            .disableCachingNullValues();
        
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .transactionAware()
            .build();
    }
}

8. 定时任务

8.1 定时任务示例

java 复制代码
package com.example.demo.task;

import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import com.example.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

@Component
@RequiredArgsConstructor
@Slf4j
public class ScheduledTasks {
    
    private final UserRepository userRepository;
    
    /**
     * 每天凌晨1点执行:清理不活跃用户
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void cleanupInactiveUsers() {
        log.info("开始清理不活跃用户: {}", LocalDateTime.now());
        
        List<User> inactiveUsers = userRepository
            .findByStatus(UserStatus.INACTIVE);
        
        log.info("清理完成,共清理 {} 个用户", inactiveUsers.size());
    }
    
    /**
     * 每小时执行:发送统计报告
     */
    @Scheduled(fixedRate = 3600000)  // 1小时
    public void sendHourlyReport() {
        log.info("生成每小时统计报告: {}", LocalDateTime.now());
        
        long userCount = userRepository.count();
        log.info("当前用户总数: {}", userCount);
    }
    
    /**
     * 每天零点:数据同步
     */
    @Scheduled(cron = "0 0 0 * * ?")
    public void dailyDataSync() {
        log.info("开始每日数据同步: {}", LocalDateTime.now());
    }
}

9. 异常处理

9.1 全局异常处理器

GlobalExceptionHandler.java

java 复制代码
package com.example.demo.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    /**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
        log.error("业务异常: {}", ex.getMessage());
        
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        
        return ResponseEntity.badRequest().body(error);
    }
    
    /**
     * 参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        
        ValidationErrorResponse response = new ValidationErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "参数校验失败",
            errors,
            LocalDateTime.now()
        );
        
        return ResponseEntity.badRequest().body(response);
    }
    
    /**
     * 认证异常
     */
    @ExceptionHandler(BadCredentialsException.class)
    public ResponseEntity<ErrorResponse> handleBadCredentialsException(
            BadCredentialsException ex) {
        log.error("认证失败: {}", ex.getMessage());
        
        ErrorResponse error = new ErrorResponse(
            HttpStatus.UNAUTHORIZED.value(),
            "用户名或密码错误",
            LocalDateTime.now()
        );
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
    }
    
    /**
     * 权限不足异常
     */
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDeniedException(
            AccessDeniedException ex) {
        log.error("权限不足: {}", ex.getMessage());
        
        ErrorResponse error = new ErrorResponse(
            HttpStatus.FORBIDDEN.value(),
            "权限不足,无法访问此资源",
            LocalDateTime.now()
        );
        
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
    }
    
    /**
     * 其他异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
        log.error("系统异常", ex);
        
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "系统繁忙,请稍后重试",
            LocalDateTime.now()
        );
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

10. 测试

10.1 单元测试

UserServiceTest.java

java 复制代码
package com.example.demo.service;

import com.example.demo.dto.UserCreateDTO;
import com.example.demo.entity.User;
import com.example.demo.entity.User.UserStatus;
import com.example.demo.exception.BusinessException;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.time.LocalDateTime;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private PasswordEncoder passwordEncoder;
    
    @InjectMocks
    private UserService userService;
    
    private User testUser;
    private UserCreateDTO createDTO;
    
    @BeforeEach
    void setUp() {
        testUser = new User();
        testUser.setId(1L);
        testUser.setUsername("testuser");
        testUser.setPassword("encodedPassword");
        testUser.setEmail("test@example.com");
        testUser.setStatus(UserStatus.ACTIVE);
        testUser.setCreatedAt(LocalDateTime.now());
        
        createDTO = new UserCreateDTO();
        createDTO.setUsername("newuser");
        createDTO.setPassword("password123");
        createDTO.setEmail("new@example.com");
    }
    
    @Test
    void findById_WhenUserExists_ReturnsUser() {
        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
        
        // When
        User result = userService.findById(1L);
        
        // Then
        assertNotNull(result);
        assertEquals("testuser", result.getUsername());
        verify(userRepository).findById(1L);
    }
    
    @Test
    void findById_WhenUserNotExists_ThrowsException() {
        // Given
        when(userRepository.findById(999L)).thenReturn(Optional.empty());
        
        // When & Then
        assertThrows(BusinessException.class, () -> {
            userService.findById(999L);
        });
        verify(userRepository).findById(999L);
    }
    
    @Test
    void create_WhenUsernameExists_ThrowsException() {
        // Given
        when(userRepository.findByUsername("newuser")).thenReturn(Optional.of(testUser));
        
        // When & Then
        assertThrows(BusinessException.class, () -> {
            userService.create(createDTO);
        });
        verify(userRepository).findByUsername("newuser");
        verify(userRepository, never()).save(any());
    }
    
    @Test
    void create_WhenValidData_ReturnsUser() {
        // Given
        when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty());
        when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty());
        when(passwordEncoder.encode("password123")).thenReturn("encodedPassword");
        when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
            User user = invocation.getArgument(0);
            user.setId(2L);
            return user;
        });
        
        // When
        User result = userService.create(createDTO);
        
        // Then
        assertNotNull(result);
        assertEquals("newuser", result.getUsername());
        assertEquals("new@example.com", result.getEmail());
        assertEquals(UserStatus.ACTIVE, result.getStatus());
        verify(passwordEncoder).encode("password123");
        verify(userRepository).save(any(User.class));
    }
}

11. 项目部署

11.1 Maven构建

bash 复制代码
# 清理构建
./mvnw clean

# 编译项目
./mvnw compile

# 运行测试
./mvnw test

# 打包
./mvnw package -DskipTests

# 生成Docker镜像
./mvnw spring-boot:build-image

11.2 Docker部署

Dockerfile

dockerfile 复制代码
# 构建阶段
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

# 运行阶段
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar

# 设置时区
RUN apk add --no-cache tzdata \
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo "Asia/Shanghai" > /etc/timezone

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]

docker-compose.yml

yaml 复制代码
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/demo
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=root_password
      - SPRING_REDIS_HOST=redis
    depends_on:
      - db
      - redis
    networks:
      - demo-network

  db:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=root_password
      - MYSQL_DATABASE=demo
    volumes:
      - mysql_data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - demo-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - demo-network

networks:
  demo-network:

volumes:
  mysql_data:
  redis_data:

11.3 Kubernetes部署

deployment.yaml

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
      - name: demo-app
        image: demo:latest
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        - name: MYSQL_HOST
          valueFrom:
            configMapKeyRef:
              name: demo-config
              key: mysql-host
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: demo-secrets
              key: mysql-password
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: demo-service
spec:
  selector:
    app: demo-app
  ports:
  - port: 80
    targetPort: 8080
  type: LoadBalancer

12. 最佳实践总结

12.1 项目结构最佳实践

复制代码
分层清晰:
- controller/     # 控制器层(处理HTTP请求)
- service/       # 服务层(业务逻辑)
- repository/     # 数据访问层(JPA/MyBatis)
- entity/        # 实体类
- dto/           # 数据传输对象
- mapper/        # MyBatis映射器
- config/        # 配置类
- security/     # 安全配置
- exception/     # 异常处理
- utils/        # 工具类

12.2 开发最佳实践

java 复制代码
// ✅ 好的实践
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
    private final UserRepository userRepository;
    
    @Cacheable(value = "users", key = "#id")
    public User findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new BusinessException("用户不存在"));
    }
}

// ❌ 避免的实践
@Service
public class BadUserService {
    private UserRepository userRepository;
    
    public BadUserService(UserRepository userRepository) {
        this.userRepository = userRepository;  // 应该用Lombok @RequiredArgsConstructor
    }
    
    public User findById(Long id) {
        User user = userRepository.findById(id).get();  // 可能抛出NoSuchElementException
        return user;
    }
}

12.3 性能优化建议

  1. 使用连接池:HikariCP是高性能连接池
  2. 开启缓存:减少数据库访问
  3. 批量操作:减少数据库交互次数
  4. 异步处理:提高响应速度
  5. 懒加载:按需加载数据
  6. 索引优化:合理设计数据库索引

12.4 安全性建议

  1. 使用JWT:无状态认证,适合分布式环境
  2. **密码加密
相关推荐
山岚的运维笔记2 小时前
SQL Server笔记 -- 第16章:MERGE
java·笔记·sql·microsoft·sqlserver
Andy Dennis2 小时前
一文漫谈设计模式之创建型模式(一)
java·开发语言·设计模式
belldeep2 小时前
Java:Tomcat 9 和 mermaid.min.js 10.9 上传.csv文件实现 Markdown 中 Mermaid 图表的渲染
java·tomcat·mermaid·去除flexmark
AutumnorLiuu2 小时前
C++并发编程学习(二)—— 线程所有权和管控
java·c++·学习
Demon_Hao2 小时前
JAVA缓存的使用RedisCache、LocalCache、复合缓存
java·开发语言·缓存
踏雪羽翼2 小时前
android 解决混淆导致AGPBI: {“kind“:“error“,“text“:“Type a.a is defined multiple times
android·java·开发语言·混淆·混淆打包出现a.a
lang201509282 小时前
Tomcat Maven插件:部署与卸载的架构设计
java·tomcat·maven
serve the people3 小时前
python环境搭建 (六) Makefile 简单使用方法
java·服务器·python
重生之后端学习3 小时前
146. LRU 缓存
java·数据结构·算法·leetcode·职场和发展