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.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 # 或者使用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 性能优化建议

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

12.4 安全性建议

  1. 使用JWT:无状态认证,适合分布式环境
  2. **密码加密

Read more

C++11新特性(下)----《Hello C++ Wrold!》(26)--(C/C++)

C++11新特性(下)----《Hello C++ Wrold!》(26)--(C/C++)

文章目录 * 前言 * lambda表达式 * 可变参数模板 * 展开参数包的方法 * 应用 * 包装器 * fiction包装器 * bind函数 * 作业部分 前言 在 C++11 标准带来的诸多革命性特性中,“简化代码编写” 与 “统一可调用对象管理” 是两大核心目标。lambda 表达式解决了传统仿函数 “定义繁琐、复用性低” 的痛点,让局部场景下的自定义逻辑(如排序规则、回调函数)能以更简洁的匿名函数形式实现;可变参数模板则打破了模板参数数量固定的限制,为 STL 容器(如emplace_back)和通用函数设计提供了灵活的参数处理能力;而 function 包装器与 bind 函数,则进一步整合了函数指针、仿函数、lambda 等不同类型的可调用对象,实现了统一管理与参数适配,甚至让可调用对象存储到容器中成为可能。 这些特性并非孤立存在 ——lambda 的底层依赖仿函数实现,可变参数模板为emplace系列接口提供了技术支撑,

By Ne0inhk
C++日新月异的未来代码:C++11(上)

C++日新月异的未来代码:C++11(上)

文章目录 * 1.统一的列表初始化 * 1.1 普通{ }初始化 * 1.2 initializer_list * 2.声明 * 2.1 auto、nullptr * 2.2 decltype * 3.左值右值 * 3.1 概念 * 3.2 左值引用与右值引用比较 * 3.3 左值引用与右值引用的应用 * 3.4 完美转发 * 希望读者们多多三连支持 * 小编会继续更新 * 你们的鼓励就是我前进的动力! C++11 能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习 1.统一的列表初始化 1.1

By Ne0inhk
Re:从零开始的 C++ 入門篇(十一):全站最全面的C/C++内存管理的底层剖析与硬核指南

Re:从零开始的 C++ 入門篇(十一):全站最全面的C/C++内存管理的底层剖析与硬核指南

◆ 博主名称: 晓此方-ZEEKLOG博客 大家好,欢迎来到晓此方的博客。 ⭐️C++系列个人专栏: Re:从零开始的C++_晓此方的博客-ZEEKLOG博客  ⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰 目录 0.1概要&序論 一,布局模型与常见误区解析 1.1C/C++内存布局 1.2内存布局易误解点 二,复习C语言的内存管理方法 2.1malloc 2.2calloc 2.3relloc 2.4free 2.5罗列常见的内存管理错误 三,C++内存管理方法 3.1new/delete管理体系 3.1.1开辟单个空间与释放 3.1.2开辟多个连续的空间与释放

By Ne0inhk
华为OD技术面八股文真题_C++_3

华为OD技术面八股文真题_C++_3

文章目录 * 变量的声明和定义的区别 * 内存泄露是什么意思?怎么避免内存泄露 * 怎么排查内存泄漏,遇到内存泄漏情况,一般怎么解决 * 说一下define和const的区别 * define和typedef的区别 * 宏函数和内联函数的区别 * 类和结构体的区别 * 结构体(struct)和联合体(union)差别 * 静态库和动态库区别 * 介绍一下C++的编译过程 变量的声明和定义的区别 * 变量的声明是告诉编译器变量的名称和类型,不分配存储空间; * 变量的定义会为变量分配存储空间并建立实体。 * 一个变量可以在多个地方声明,但只能在一个地方定义。 使用 extern 修饰的变量通常是声明,表示该变量在其它文件中定义,但 如果 extern 变量带初始化,则该语句仍然属于定义。 内存泄露是什么意思?怎么避免内存泄露 内存泄漏是指程序在动态申请内存后,后续失去对该内存的控制,导致这块内存无法被释放,从而造成内存资源浪费的现象。内存被申请了,却释放不了。 内存泄漏的危害如下: 1. 程序内存占用不断增大,导致系统可用内存减少,性能下

By Ne0inhk