跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

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

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

心动瞬间发布于 2026/3/23更新于 2026/6/2033 浏览

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>
      <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+

方式二:命令行创建

# 使用 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 安全性建议
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • LINUX DO 社区无需邀请码注册流程说明
  • 大模型开发入门指南:从零掌握核心技术与应用
  • Claude Code 核心执行模式与辅助功能解析
  • 统一 OpenAI API 格式调用 20+ 主流大模型方案
  • 2026 年 5 个最佳 React UI 库
  • OpenClaw 技能扩展实战:Tavily 联网与多维表格自动化
  • Qt 开源项目 VNote 源码解读 (一):核心类与主流程
  • Linux 下 libwebkit2gtk-4.1-0 安装与配置指南
  • 小型语言模型(SLMs)综述:架构、训练与压缩技术详解
  • GitHub Copilot 安装配置与高效使用指南
  • 医疗 AI 场景下的算法编程深度解析(一)
  • Java 方法调用绑定:前期绑定与后期绑定详解
  • KaiwuDB 3.1.0 在 Ubuntu 22.04 部署实战:TLS 配置与性能基线
  • Obsidian 插件安装指南:本地存储与 GitHub 源码获取
  • 基于FPGA的五级积分梳状CIC滤波器Verilog设计
  • 从 Copilot 到 Agent:构建私有化开发助手指南
  • 堆排序算法详解与实现
  • AI 大模型助力专利翻译:功能与应用解析
  • 工程管理系统功能设计与技术架构解析
  • 基于 Java SpringBoot+Vue 的海洋生物管理系统设计与实现

相关免费在线工具

  • 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