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版本要求:
# 检查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 # 或者使用cURLcurl 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:8080servlet: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=trueusername: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari:minimum-idle:5maximum-pool-size:20idle-timeout:30000pool-name: DemoHikariCP max-lifetime:1800000connection-timeout:30000# JPA配置jpa:hibernate:ddl-auto: update show-sql:trueproperties:hibernate:dialect: org.hibernate.dialect.MySQLDialect format_sql:trueopen-in-view:false# Redis配置data:redis:host: localhost port:6379password:lettuce:pool:max-active:8max-idle:8min-idle:2# MyBatis配置mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.example.demo.entity configuration:map-underscore-to-camel-case:truelog-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:
packagecom.example.demo;importorg.mybatis.spring.annotation.MapperScan;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.cache.annotation.EnableCaching;importorg.springframework.scheduling.annotation.EnableAsync;importorg.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication@MapperScan("com.example.demo.mapper")@EnableCaching// 开启缓存@EnableAsync// 开启异步@EnableScheduling// 开启定时任务publicclassDemoApplication{publicstaticvoidmain(String[] args){SpringApplication.run(DemoApplication.class, args);System.out.println("🚀 Demo Application Started Successfully!");}}4.2 实体类设计
User.java:
packagecom.example.demo.entity;importjakarta.persistence.*;importlombok.Data;importjava.io.Serializable;importjava.time.LocalDateTime;/** * 用户实体类 */@Entity@Table(name ="users")@DatapublicclassUserimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@Id@GeneratedValue(strategy =GenerationType.IDENTITY)privateLong id;@Column(nullable =false, unique =true, length =50)privateString username;@Column(nullable =false)privateString password;@Column(length =100)privateString email;@Column(length =20)privateString phone;@Enumerated(EnumType.STRING)@Column(length =20)privateUserStatus status =UserStatus.ACTIVE;@Column(name ="created_at", updatable =false)privateLocalDateTime createdAt;@Column(name ="updated_at")privateLocalDateTime updatedAt;@PrePersistprotectedvoidonCreate(){ createdAt =LocalDateTime.now(); updatedAt =LocalDateTime.now();}@PreUpdateprotectedvoidonUpdate(){ updatedAt =LocalDateTime.now();}publicenumUserStatus{ ACTIVE,// 活跃 INACTIVE,// 非活跃 LOCKED // 锁定}}4.3 Repository层
UserRepository.java:
packagecom.example.demo.repository;importcom.example.demo.entity.User;importcom.example.demo.entity.User.UserStatus;importorg.springframework.data.domain.Page;importorg.springframework.data.domain.Pageable;importorg.springframework.data.jpa.repository.JpaRepository;importorg.springframework.data.jpa.repository.JpaSpecificationExecutor;importorg.springframework.data.jpa.repository.Query;importorg.springframework.data.repository.query.Param;importorg.springframework.stereotype.Repository;importjava.time.LocalDateTime;importjava.util.List;importjava.util.Optional;@RepositorypublicinterfaceUserRepositoryextendsJpaRepository<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 );// 统计用户数量longcountByStatus(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);// 批量删除voiddeleteByStatus(UserStatus status);}4.4 Service层
UserService.java:
packagecom.example.demo.service;importcom.example.demo.dto.UserCreateDTO;importcom.example.demo.dto.UserUpdateDTO;importcom.example.demo.entity.User;importcom.example.demo.entity.User.UserStatus;importcom.example.demo.exception.BusinessException;importcom.example.demo.repository.UserRepository;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.cache.annotation.CacheEvict;importorg.springframework.cache.annotation.Cacheable;importorg.springframework.data.domain.Page;importorg.springframework.data.domain.Pageable;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importjava.util.List;@Service@RequiredArgsConstructor@Slf4jpublicclassUserService{privatefinalUserRepository userRepository;privatefinalPasswordEncoder passwordEncoder;/** * 查询所有用户(带缓存) */@Cacheable(value ="users", key ="'all'")publicList<User>findAll(){ log.info("查询所有用户");return userRepository.findAll();}/** * 分页查询用户 */publicPage<User>findByPage(UserStatus status,Pageable pageable){return userRepository.findByStatus(status, pageable);}/** * 根据ID查询用户 */@Cacheable(value ="users", key ="#id")publicUserfindById(Long id){ log.info("查询用户: {}", id);return userRepository.findById(id).orElseThrow(()->newBusinessException("用户不存在"));}/** * 根据用户名查询 */publicUserfindByUsername(String username){return userRepository.findByUsername(username).orElseThrow(()->newBusinessException("用户不存在"));}/** * 创建用户 */@Transactional@CacheEvict(value ="users", allEntries =true)publicUsercreate(UserCreateDTO createDTO){ log.info("创建用户: {}", createDTO.getUsername());// 检查用户名是否已存在if(userRepository.findByUsername(createDTO.getUsername()).isPresent()){thrownewBusinessException("用户名已存在");}// 检查邮箱是否已存在if(createDTO.getEmail()!=null&& userRepository.findByEmail(createDTO.getEmail()).isPresent()){thrownewBusinessException("邮箱已被注册");}// 创建用户User user =newUser(); 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")publicUserupdate(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)publicvoiddelete(Long id){ log.info("删除用户: {}", id);if(!userRepository.existsById(id)){thrownewBusinessException("用户不存在");} userRepository.deleteById(id);}/** * 批量删除 */@Transactional@CacheEvict(value ="users", allEntries =true)publicvoidbatchDelete(List<Long> ids){ log.info("批量删除用户: {}", ids); userRepository.deleteAllById(ids);}}4.5 Controller层
UserController.java:
packagecom.example.demo.controller;importcom.example.demo.dto.*;importcom.example.demo.entity.User;importcom.example.demo.entity.User.UserStatus;importcom.example.demo.service.UserService;importjakarta.validation.Valid;importlombok.RequiredArgsConstructor;importorg.springframework.data.domain.Page;importorg.springframework.data.domain.Pageable;importorg.springframework.data.web.PageableDefault;importorg.springframework.http.HttpStatus;importorg.springframework.http.ResponseEntity;importorg.springframework.security.access.prepost.PreAuthorize;importorg.springframework.web.bind.annotation.*;importjava.util.List;@RestController@RequestMapping("/users")@RequiredArgsConstructorpublicclassUserController{privatefinalUserService userService;/** * 获取所有用户 */@GetMappingpublicResponseEntity<List<User>>getAllUsers(){returnResponseEntity.ok(userService.findAll());}/** * 分页查询用户 */@GetMapping("/page")publicResponseEntity<Page<User>>getUsersByPage(@RequestParam(required =false)UserStatus status,@PageableDefault(size =10, sort ="createdAt")Pageable pageable){returnResponseEntity.ok(userService.findByPage(status, pageable));}/** * 根据ID查询用户 */@GetMapping("/{id}")publicResponseEntity<User>getUser(@PathVariableLong id){returnResponseEntity.ok(userService.findById(id));}/** * 创建用户 */@PostMappingpublicResponseEntity<User>createUser(@Valid@RequestBodyUserCreateDTO createDTO){User user = userService.create(createDTO);returnResponseEntity.status(HttpStatus.CREATED).body(user);}/** * 更新用户 */@PutMapping("/{id}")publicResponseEntity<User>updateUser(@PathVariableLong id,@Valid@RequestBodyUserUpdateDTO updateDTO){returnResponseEntity.ok(userService.update(id, updateDTO));}/** * 删除用户 */@DeleteMapping("/{id}")@PreAuthorize("hasRole('ADMIN')")publicResponseEntity<Void>deleteUser(@PathVariableLong id){ userService.delete(id);returnResponseEntity.noContent().build();}/** * 批量删除用户 */@DeleteMapping("/batch")@PreAuthorize("hasRole('ADMIN')")publicResponseEntity<Void>batchDeleteUsers(@RequestBodyList<Long> ids){ userService.batchDelete(ids);returnResponseEntity.noContent().build();}}5. 数据访问层
5.1 JPA动态查询
UserSpecification.java:
packagecom.example.demo.specification;importcom.example.demo.entity.User;importcom.example.demo.entity.User.UserStatus;importjakarta.persistence.criteria.Predicate;importorg.springframework.data.jpa.domain.Specification;importjava.time.LocalDateTime;importjava.util.ArrayList;importjava.util.List;publicclassUserSpecification{publicstaticSpecification<User>withSearch(String username,UserStatus status,LocalDateTime startDate,LocalDateTime endDate){return(root, query, criteriaBuilder)->{List<Predicate> predicates =newArrayList<>();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(newPredicate[0]));};}}6. 安全性配置
6.1 Spring Security配置
SecurityConfig.java:
packagecom.example.demo.config;importcom.example.demo.security.JwtAuthenticationFilter;importcom.example.demo.security.JwtTokenProvider;importlombok.RequiredArgsConstructor;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;importorg.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;importorg.springframework.security.config.http.SessionCreationPolicy;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.web.SecurityFilterChain;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration@EnableWebSecurity@EnableMethodSecurity@RequiredArgsConstructorpublicclassSecurityConfig{privatefinalJwtTokenProvider jwtTokenProvider;@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@BeanpublicAuthenticationManagerauthenticationManager(AuthenticationConfiguration authConfig)throwsException{return authConfig.getAuthenticationManager();}@BeanpublicSecurityFilterChainfilterChain(HttpSecurity http)throwsException{ 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(newJwtAuthenticationFilter(jwtTokenProvider),UsernamePasswordAuthenticationFilter.class);return http.build();}}7. 缓存配置
7.1 Redis缓存配置
RedisConfig.java:
packagecom.example.demo.config;importorg.springframework.cache.CacheManager;importorg.springframework.cache.annotation.EnableCaching;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.cache.RedisCacheConfiguration;importorg.springframework.data.redis.cache.RedisCacheManager;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;importorg.springframework.data.redis.serializer.RedisSerializationContext;importorg.springframework.data.redis.serializer.StringRedisSerializer;importjava.time.Duration;@Configuration@EnableCachingpublicclassRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<String,Object> template =newRedisTemplate<>(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(newStringRedisSerializer()); template.setValueSerializer(newGenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(newStringRedisSerializer()); template.setHashValueSerializer(newGenericJackson2JsonRedisSerializer()); template.afterPropertiesSet();return template;}@BeanpublicCacheManagercacheManager(RedisConnectionFactory connectionFactory){RedisCacheConfiguration config =RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(newStringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(newGenericJackson2JsonRedisSerializer())).disableCachingNullValues();returnRedisCacheManager.builder(connectionFactory).cacheDefaults(config).transactionAware().build();}}8. 定时任务
8.1 定时任务示例
packagecom.example.demo.task;importcom.example.demo.entity.User;importcom.example.demo.entity.User.UserStatus;importcom.example.demo.repository.UserRepository;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.scheduling.annotation.Scheduled;importorg.springframework.stereotype.Component;importjava.time.LocalDateTime;importjava.util.List;@Component@RequiredArgsConstructor@Slf4jpublicclassScheduledTasks{privatefinalUserRepository userRepository;/** * 每天凌晨1点执行:清理不活跃用户 */@Scheduled(cron ="0 0 1 * * ?")publicvoidcleanupInactiveUsers(){ log.info("开始清理不活跃用户: {}",LocalDateTime.now());List<User> inactiveUsers = userRepository .findByStatus(UserStatus.INACTIVE); log.info("清理完成,共清理 {} 个用户", inactiveUsers.size());}/** * 每小时执行:发送统计报告 */@Scheduled(fixedRate =3600000)// 1小时publicvoidsendHourlyReport(){ log.info("生成每小时统计报告: {}",LocalDateTime.now());long userCount = userRepository.count(); log.info("当前用户总数: {}", userCount);}/** * 每天零点:数据同步 */@Scheduled(cron ="0 0 0 * * ?")publicvoiddailyDataSync(){ log.info("开始每日数据同步: {}",LocalDateTime.now());}}9. 异常处理
9.1 全局异常处理器
GlobalExceptionHandler.java:
packagecom.example.demo.exception;importlombok.extern.slf4j.Slf4j;importorg.springframework.http.HttpStatus;importorg.springframework.http.ResponseEntity;importorg.springframework.security.access.AccessDeniedException;importorg.springframework.security.authentication.BadCredentialsException;importorg.springframework.validation.FieldError;importorg.springframework.web.bind.MethodArgumentNotValidException;importorg.springframework.web.bind.annotation.ExceptionHandler;importorg.springframework.web.bind.annotation.RestControllerAdvice;importjava.time.LocalDateTime;importjava.util.HashMap;importjava.util.Map;@RestControllerAdvice@Slf4jpublicclassGlobalExceptionHandler{/** * 业务异常 */@ExceptionHandler(BusinessException.class)publicResponseEntity<ErrorResponse>handleBusinessException(BusinessException ex){ log.error("业务异常: {}", ex.getMessage());ErrorResponse error =newErrorResponse(HttpStatus.BAD_REQUEST.value(), ex.getMessage(),LocalDateTime.now());returnResponseEntity.badRequest().body(error);}/** * 参数校验异常 */@ExceptionHandler(MethodArgumentNotValidException.class)publicResponseEntity<ValidationErrorResponse>handleValidationException(MethodArgumentNotValidException ex){Map<String,String> errors =newHashMap<>(); ex.getBindingResult().getAllErrors().forEach(error ->{String fieldName =((FieldError) error).getField();String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage);});ValidationErrorResponse response =newValidationErrorResponse(HttpStatus.BAD_REQUEST.value(),"参数校验失败", errors,LocalDateTime.now());returnResponseEntity.badRequest().body(response);}/** * 认证异常 */@ExceptionHandler(BadCredentialsException.class)publicResponseEntity<ErrorResponse>handleBadCredentialsException(BadCredentialsException ex){ log.error("认证失败: {}", ex.getMessage());ErrorResponse error =newErrorResponse(HttpStatus.UNAUTHORIZED.value(),"用户名或密码错误",LocalDateTime.now());returnResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);}/** * 权限不足异常 */@ExceptionHandler(AccessDeniedException.class)publicResponseEntity<ErrorResponse>handleAccessDeniedException(AccessDeniedException ex){ log.error("权限不足: {}", ex.getMessage());ErrorResponse error =newErrorResponse(HttpStatus.FORBIDDEN.value(),"权限不足,无法访问此资源",LocalDateTime.now());returnResponseEntity.status(HttpStatus.FORBIDDEN).body(error);}/** * 其他异常 */@ExceptionHandler(Exception.class)publicResponseEntity<ErrorResponse>handleGenericException(Exception ex){ log.error("系统异常", ex);ErrorResponse error =newErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(),"系统繁忙,请稍后重试",LocalDateTime.now());returnResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);}}10. 测试
10.1 单元测试
UserServiceTest.java:
packagecom.example.demo.service;importcom.example.demo.dto.UserCreateDTO;importcom.example.demo.entity.User;importcom.example.demo.entity.User.UserStatus;importcom.example.demo.exception.BusinessException;importcom.example.demo.repository.UserRepository;importorg.junit.jupiter.api.BeforeEach;importorg.junit.jupiter.api.Test;importorg.junit.jupiter.api.extension.ExtendWith;importorg.mockito.InjectMocks;importorg.mockito.Mock;importorg.mockito.junit.jupiter.MockitoExtension;importorg.springframework.security.crypto.password.PasswordEncoder;importjava.time.LocalDateTime;importjava.util.Optional;importstaticorg.junit.jupiter.api.Assertions.*;importstaticorg.mockito.ArgumentMatchers.any;importstaticorg.mockito.Mockito.*;@ExtendWith(MockitoExtension.class)classUserServiceTest{@MockprivateUserRepository userRepository;@MockprivatePasswordEncoder passwordEncoder;@InjectMocksprivateUserService userService;privateUser testUser;privateUserCreateDTO createDTO;@BeforeEachvoidsetUp(){ testUser =newUser(); testUser.setId(1L); testUser.setUsername("testuser"); testUser.setPassword("encodedPassword"); testUser.setEmail("[email protected]"); testUser.setStatus(UserStatus.ACTIVE); testUser.setCreatedAt(LocalDateTime.now()); createDTO =newUserCreateDTO(); createDTO.setUsername("newuser"); createDTO.setPassword("password123"); createDTO.setEmail("[email protected]");}@TestvoidfindById_WhenUserExists_ReturnsUser(){// Givenwhen(userRepository.findById(1L)).thenReturn(Optional.of(testUser));// WhenUser result = userService.findById(1L);// ThenassertNotNull(result);assertEquals("testuser", result.getUsername());verify(userRepository).findById(1L);}@TestvoidfindById_WhenUserNotExists_ThrowsException(){// Givenwhen(userRepository.findById(999L)).thenReturn(Optional.empty());// When & ThenassertThrows(BusinessException.class,()->{ userService.findById(999L);});verify(userRepository).findById(999L);}@Testvoidcreate_WhenUsernameExists_ThrowsException(){// Givenwhen(userRepository.findByUsername("newuser")).thenReturn(Optional.of(testUser));// When & ThenassertThrows(BusinessException.class,()->{ userService.create(createDTO);});verify(userRepository).findByUsername("newuser");verify(userRepository,never()).save(any());}@Testvoidcreate_WhenValidData_ReturnsUser(){// Givenwhen(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;});// WhenUser result = userService.create(createDTO);// ThenassertNotNull(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.0ports:-"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:3selector:matchLabels:app: demo-app template:metadata:labels:app: demo-app spec:containers:-name: demo-app image: demo:latest ports:-containerPort:8080env:-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:8080initialDelaySeconds:60periodSeconds:10readinessProbe:httpGet:path: /actuator/health/readiness port:8080initialDelaySeconds:30periodSeconds:5---apiVersion: v1 kind: Service metadata:name: demo-service spec:selector:app: demo-app ports:-port:80targetPort:8080type: LoadBalancer 12. 最佳实践总结
12.1 项目结构最佳实践
分层清晰: - controller/ # 控制器层(处理HTTP请求) - service/ # 服务层(业务逻辑) - repository/ # 数据访问层(JPA/MyBatis) - entity/ # 实体类 - dto/ # 数据传输对象 - mapper/ # MyBatis映射器 - config/ # 配置类 - security/ # 安全配置 - exception/ # 异常处理 - utils/ # 工具类 12.2 开发最佳实践
// ✅ 好的实践@Service@RequiredArgsConstructor@Slf4jpublicclassUserService{privatefinalUserRepository userRepository;@Cacheable(value ="users", key ="#id")publicUserfindById(Long id){return userRepository.findById(id).orElseThrow(()->newBusinessException("用户不存在"));}}// ❌ 避免的实践@ServicepublicclassBadUserService{privateUserRepository userRepository;publicBadUserService(UserRepository userRepository){this.userRepository = userRepository;// 应该用Lombok @RequiredArgsConstructor}publicUserfindById(Long id){User user = userRepository.findById(id).get();// 可能抛出NoSuchElementExceptionreturn user;}}12.3 性能优化建议
- 使用连接池:HikariCP是高性能连接池
- 开启缓存:减少数据库访问
- 批量操作:减少数据库交互次数
- 异步处理:提高响应速度
- 懒加载:按需加载数据
- 索引优化:合理设计数据库索引
12.4 安全性建议
- 使用JWT:无状态认证,适合分布式环境
- **密码加密