跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
Javajava

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

综述由AI生成Spring Boot 3.x 版本的实战开发流程。内容涵盖环境搭建(JDK 17/Maven)、项目结构规范、核心功能实现(JPA、Security、Redis、定时任务、异常处理)及单元测试。重点讲解了基于 Docker 和 Kubernetes 的容器化部署方案,并提供了性能优化与安全加固的最佳实践建议,适用于 Java 企业级应用开发。

心动瞬间发布于 2026/3/23更新于 2026/5/316 浏览

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.xJava 17+、Spring 6新项目推荐

本文基于 Spring Boot 3.x 版本

2. 环境搭建

2.1 开发工具配置

JDK 版本要求:

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

Maven 配置(settings.xml):

<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <mirrors>
    <mirror>
      <id>aliyun</id>
      <mirrorOf>central</mirrorOf>
      <name>Aliyun Maven</name>
      https://maven.aliyun.com/repository/central
    
  
  
    
      jdk-17
      
        true
      
      
        17
        17
        UTF-8
      
    
  

<url>
</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>
</id>
<activation>
<activeByDefault>
</activeByDefault>
</activation>
<properties>
<maven.compiler.source>
</maven.compiler.source>
<maven.compiler.target>
</maven.compiler.target>
<project.build.sourceEncoding>
</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+

方式二:命令行创建

# 使用 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:

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:

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:

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:

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:

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:

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:

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:

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:

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 定时任务示例

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:

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:

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("[email protected]");
        testUser.setStatus(UserStatus.ACTIVE);
        testUser.setCreatedAt(LocalDateTime.now());

        createDTO = new UserCreateDTO();
        createDTO.setUsername("newuser");
        createDTO.setPassword("password123");
        createDTO.setEmail("[email protected]");
    }

    @Test
    void findById_WhenUserExists_ReturnsUser() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
        User result = userService.findById(1L);
        assertNotNull(result);
        assertEquals("testuser", result.getUsername());
        verify(userRepository).findById(1L);
    }

    @Test
    void findById_WhenUserNotExists_ThrowsException() {
        when(userRepository.findById(999L)).thenReturn(Optional.empty());
        assertThrows(BusinessException.class, () -> {
            userService.findById(999L);
        });
        verify(userRepository).findById(999L);
    }

    @Test
    void create_WhenUsernameExists_ThrowsException() {
        when(userRepository.findByUsername("newuser")).thenReturn(Optional.of(testUser));
        assertThrows(BusinessException.class, () -> {
            userService.create(createDTO);
        });
        verify(userRepository).findByUsername("newuser");
        verify(userRepository, never()).save(any());
    }

    @Test
    void create_WhenValidData_ReturnsUser() {
        when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty());
        when(userRepository.findByEmail("[email protected]")).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;
        });

        User result = userService.create(createDTO);
        assertNotNull(result);
        assertEquals("newuser", result.getUsername());
        assertEquals("[email protected]", result.getEmail());
        assertEquals(UserStatus.ACTIVE, result.getStatus());
        verify(passwordEncoder).encode("password123");
        verify(userRepository).save(any(User.class));
    }
}

11. 项目部署

11.1 Maven 构建

# 清理构建
./mvnw clean
# 编译项目
./mvnw compile
# 运行测试
./mvnw test
# 打包
./mvnw package -DskipTests
# 生成 Docker 镜像
./mvnw spring-boot:build-image

11.2 Docker 部署

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:

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:

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 开发最佳实践

// ✅ 好的实践
@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. 密码加密

目录

  1. Spring Boot 实战:从入门到项目部署
  2. 1. Spring Boot 概述
  3. 1.1 什么是 Spring Boot?
  4. 1.2 Spring Boot 版本选择
  5. 2. 环境搭建
  6. 2.1 开发工具配置
  7. 检查 Java 版本
  8. 需要 Java 17 或更高版本
  9. 2.2 Spring Initializr 创建项目
  10. 使用 Spring Boot CLI
  11. 或者使用 cURL
  12. 3. 项目结构详解
  13. 3.1 标准项目结构
  14. 3.2 核心配置文件
  15. 数据源配置
  16. JPA 配置
  17. Redis 配置
  18. MyBatis 配置
  19. 日志配置
  20. Actuator 配置
  21. 4. 核心功能实现
  22. 4.1 启动类配置
  23. 4.2 实体类设计
  24. 4.3 Repository 层
  25. 4.4 Service 层
  26. 4.5 Controller 层
  27. 5. 数据访问层
  28. 5.1 JPA 动态查询
  29. 6. 安全性配置
  30. 6.1 Spring Security 配置
  31. 7. 缓存配置
  32. 7.1 Redis 缓存配置
  33. 8. 定时任务
  34. 8.1 定时任务示例
  35. 9. 异常处理
  36. 9.1 全局异常处理器
  37. 10. 测试
  38. 10.1 单元测试
  39. 11. 项目部署
  40. 11.1 Maven 构建
  41. 清理构建
  42. 编译项目
  43. 运行测试
  44. 打包
  45. 生成 Docker 镜像
  46. 11.2 Docker 部署
  47. 构建阶段
  48. 运行阶段
  49. 设置时区
  50. 暴露端口
  51. 健康检查
  52. 启动应用
  53. 11.3 Kubernetes 部署
  54. 12. 最佳实践总结
  55. 12.1 项目结构最佳实践
  56. 12.2 开发最佳实践
  57. 12.3 性能优化建议
  58. 12.4 安全性建议
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Linux 下 Java JNI 调用 .so 文件的方法
  • Neo4j 图数据库整合 MGeo:构建智能地理知识网络
  • Windows 桌面金价监控工具 AnyGold 更新:走势图、AI 研判与声音提醒
  • Python ASGI 服务器 uvicorn 极简实战指南
  • AI Agent 框架选型指南:OpenClaw、LangChain、AutoGPT、CrewAI 深度对比
  • Neat Download Manager (NDM) 安装配置教程(适用于 Windows 和 macOS)
  • 2023 数据分析发展前景及高薪行业分析
  • 零基础调用 OpenAI API 实战指南
  • 文心大模型 4.5 开源实测:快速部署与多模态能力解析
  • OpenClaw AI 全能助手服务器安装与配置指南
  • 基于 SSM 框架的高校宿舍报修与换宿管理系统设计
  • 数据结构详解:KMP 算法、Trie 树与并查集
  • 深入解析 AI 中的 Skills:机制、对比与应用场景
  • React Native 鸿蒙化集成 react-native-webview 实战
  • BigBanana AI Director:从剧本到成片的 AI 短剧自动化工作流
  • 转行程序员:开发语言选择与岗位薪资解析
  • Llama-Factory 在金融舆情分析中的实际应用案例
  • 飞算 JavaAI 专业版实战:从零构建 Spring Boot 项目
  • 阿里开源 Page-Agent:一行 JS 实现大模型前端 DOM 自动化
  • MicroPE 基于 NVMe 启动运行 GLM-4.6V-Flash-WEB 的本地 AI 部署实践

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online