通用的 Spring Boot 工程 skill,不绑定特定机器或项目,换台电脑直接用。 覆盖 Controller-Service-Repository 分层架构、JPA/MyBatis 双数据方案、RESTful API 规范、 Redis 缓存、AOP 日志、全局异常处理、定时任务、事务排查、性能调优。 含环境检测脚本和零硬编码路径设计。 触发关键词:Spring Boot、Java 后端、JPA、MyBatis、Spring Cloud、微服务、 Maven、@Transactional、@Res
Spring Boot 开发专家是一项面向实际任务的技能,主要用于通用的、可迁移的 Spring Boot 开发 skill;不绑定特定机器、特定项目——任何 Spring Boot 工程师在任何电脑上都能直接用;
默认8080,可配 server.port ,不做假设;用 spring.dat。它将相关步骤、工具调用和结果整理方式集中到统一流程中,帮助使用者更快完成目标并减少重复操作。从功能定位来看,该技能强调把分散的操作要求整理成清晰、可复用的处理流程,使用户能够围绕既定目标快速准备输入、选择执行方式并获得结构化结果。实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;
若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。该技能适合用于一次性任务,也可以接入自动化工作流,与其他技能或上层代理配合完成更完整的业务链路;在组合使用时,应明确每一步的输入输出关系,并避免不同步骤之间出现参数冲突。
通用的、可迁移的 Spring Boot 开发 skill。不绑定特定机器、特定项目——任何 Spring Boot 工程师在任何电脑上都能直接用。
本 skill 遵循零硬编码原则:
| 原则 | 做法 |
|---|---|
| 路径 | 只用项目相对路径 src/main/java/...,绝不用绝对路径 |
| 端口 | 默认8080,可配 server.port,不做假设 |
| 数据库 | 用 spring.datasource.url 自动检测,不硬编码连接字符串 |
| 环境检测 | 用 java -version、mvn -v 检测工具链 |
| 包名 | 用占位符 ${basePackage} 表示,根据项目实际替换 |
# 直接 curl 下载模板项目(任何机器都能跑) curl -s https://start.spring.io/starter.zip -d type=maven-project -d language=java -d bootVersion=3.4.5 -d baseDir=my-project -d groupId=com.example -d artifactId=my-project -d name=MyProject -d description="Spring Boot project" -d packageName=com.example.demo -d packaging=jar -d javaVersion=17 -d dependencies=web,data-jpa,mysql,validation,lombok,devtools -o my-project.zip
org.springframework.boot spring-boot-starter-parent 3.4.5 org.springframework.boot spring-boot-starter-web
# 判断是否 Spring Boot 项目 grep -rl "spring-boot-starter" pom.xml build.gradle 2>/dev/null # 获取 Spring Boot 版本 mvn dependency:tree -Dincludes=org.springframework.boot 2>/dev/null | grep "spring-boot-starter"
src/main/java/${basePackage}/
├── controller/ ← 接口层:@RestController, @RequestMapping
├── service/ ← 业务层:@Service, @Transactional
│ └── impl/ ← 实现类(接口-实现分离时)
├── repository/ ← 数据层:JpaRepository / MyBatis Mapper
├── entity/ ← 实体:@Entity, @Table
├── dto/ ← 数据传输对象(API 入参/出参)
├── vo/ ← 视图对象(返回给前端的)
├── config/ ← 配置类:@Configuration, @Bean
├── interceptor/ ← 拦截器:HandlerInterceptor
├── aspect/ ← 切面:@Aspect, @Around
├── exception/ ← 全局异常处理:@ControllerAdvice
├── util/ ← 工具类
└── constant/ ← 常量/枚举
包名约定: ${basePackage} = groupId + artifactId 的组合,如 com.example.demo
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
@GetMapping
public Result> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String keyword) {
return Result.ok(userService.listUsers(page, size, keyword));
}
@GetMapping("/{id}")
public Result getById(@PathVariable Long id) {
return Result.ok(userService.getById(id));
}
@PostMapping
public Result create(@Valid @RequestBody UserCreateDTO dto) {
return Result.ok(userService.create(dto));
}
@PutMapping("/{id}")
public Result update(@PathVariable Long id, @Valid @RequestBody UserUpdateDTO dto) {
userService.update(id, dto);
return Result.ok();
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Long id) {
userService.delete(id);
return Result.ok();
}
}
@Data @NoArgsConstructor @AllArgsConstructor public class Result{ private int code; private String message; private T data; public static Result ok() { return new Result<>(200, "success", null); } public static Result ok(T data) { return new Result<>(200, "success", data); } public static Result fail(int code, String msg) { return new Result<>(code, msg, null); } } @Data public class PageResult { private long total; private int page; private int size; private List records; }
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.fail(400, msg);
}
@ExceptionHandler(BusinessException.class)
public Result handleBusiness(BusinessException e) {
return Result.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result handleUnknown(Exception e) {
log.error("Unhandled exception", e);
return Result.fail(500, "Internal server error");
}
}
public interface UserService {
PageResult listUsers(int page, int size, String keyword);
UserVO getById(Long id);
Long create(UserCreateDTO dto);
void update(Long id, UserUpdateDTO dto);
void delete(Long id);
}
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public PageResult listUsers(int page, int size, String keyword) {
Page userPage = userRepository.findByKeyword(keyword, PageRequest.of(page - 1, size));
return new PageResult<>(
userPage.getTotalElements(), page, size,
userPage.getContent().stream().map(UserVO::from).toList()
);
}
@Override
@Transactional
public Long create(UserCreateDTO dto) {
if (userRepository.existsByEmail(dto.getEmail())) {
throw new BusinessException(400, "邮箱已被注册");
}
User user = User.from(dto);
return userRepository.save(user).getId();
}
// ... 其余方法
}
@Repository public interface UserRepository extends JpaRepository{ boolean existsByEmail(String email); @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%") Page findByKeyword(@Param("keyword") String keyword, Pageable pageable); @Modifying @Query("UPDATE User u SET u.status = :status WHERE u.id IN :ids") int batchUpdateStatus(@Param("status") int status, @Param("ids") List ids); }
@Mapper public interface UserMapper extends BaseMapper{ @Select("SELECT * FROM user WHERE email = #{email}") User selectByEmail(@Param("email") String email); List selectPageWithOrders(@Param("keyword") String keyword, Page page); }
@Entity
@Table(name = "user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false)
private Integer status = 1; // 1=正常 0=禁用
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
public static User from(UserCreateDTO dto) {
User user = new User();
BeanUtils.copyProperties(dto, user);
return user;
}
}
server:
port: ${SERVER_PORT:8080}
spring:
application:
name: ${spring.application.name}
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai}
username: ${SPRING_DATASOURCE_USERNAME:root}
password: ${SPRING_DATASOURCE_PASSWORD:}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate # none/validate/update/create/create-drop
show-sql: false
open-in-view: false
redis:
host: ${SPRING_REDIS_HOST:localhost}
port: ${SPRING_REDIS_PORT:6379}
logging:
level:
root: INFO
${basePackage}: DEBUG
可迁移关键: 用环境变量覆盖默认值,不硬编码真实密码。
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())))
.build();
}
}
// 使用
@Service
public class UserService {
@Cacheable(value = "user", key = "#id")
public UserVO getById(Long id) { ... }
@CacheEvict(value = "user", key = "#id")
public void update(Long id, UserUpdateDTO dto) { ... }
}
@Configuration
@EnableScheduling
public class ScheduleConfig {}
@Component
@Slf4j
public class ScheduledTasks {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void cleanExpiredData() {
log.info("Cleaning expired data...");
}
@Scheduled(fixedRate = 60000) // 每60秒
public void heartbeat() {
log.debug("Heartbeat check");
}
}
@Aspect
@Component
@Slf4j
public class LogAspect {
@Around("execution(* ${basePackage}.controller..*(..))")
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
log.info("{}.{} -> {}ms",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), elapsed);
return result;
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUserList() throws Exception {
when(userService.listUsers(anyInt(), anyInt(), any()))
.thenReturn(new PageResult<>(1, 1, 20, List.of()));
mockMvc.perform(get("/api/v1/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
}
# 启动(自动检测 profile) mvn spring-boot:run # 指定 profile mvn spring-boot:run -Dspring-boot.run.profiles=dev # 打包 mvn clean package -DskipTests # 运行 jar java -jar target/*.jar --server.port=8081 # 测试 mvn test mvn test -Dtest=UserControllerTest # 查看依赖树(定位版本冲突) mvn dependency:tree | grep -E "conflict|omitted" # 生成 OpenAPI 文档(springdoc) # 启动后访问: http://localhost:8080/swagger-ui.html
| 项目 | 配置 | 说明 |
|---|---|---|
| 连接池 | spring.datasource.hikari.maximum-pool-size=20 |
默认10,生产可调20-50 |
| JPA | spring.jpa.open-in-view=false |
避免长连接持有数据库连接 |
| Tomcat 线程 | server.tomcat.threads.max=200 |
默认200 |
| 日志 | 生产关闭 show-sql |
避免 IO 瓶颈 |
| 缓存 | 热点数据加 Redis 缓存 | TTL 按业务设 |
| 异步 | @EnableAsync + @Async |
非核心路径异步化 |
| 压缩 | server.compression.enabled=true |
减少带宽 |
# 看完整堆栈(不要只看最后几行) mvn spring-boot:run 2>&1 | tee startup.log grep "Caused by:" startup.log
90% 的启动失败原因:
spring.datasource.urllsof -i :8080 或 netstat -ano | findstr 8080@Primary 或 @Qualifier 解决@Lazy 注入或用 setter 注入mvn dependency:tree 查冲突@RequestMapping 路径拼写@SpringBootApplication 是否在上层包context-path:server.servlet.context-path 配置// ❌ 同一个类内部调用不走代理
public void methodA() {
this.methodB(); // @Transactional 不生效
}
// ✅ 注入自己或移到另一个 Service
@Service
public class OtherService {
@Transactional
public void methodB() { ... }
}
#!/bin/bash # check-env.sh — 在任何机器上检测 Spring Boot 开发环境 echo "Java: $(java -version 2>&1 | head -1)" echo "Maven: $(mvn -v 2>&1 | head -1)" echo "Gradle: $(gradle -v 2>&1 | head -1)" echo "Spring Boot project: $(grep -l 'spring-boot-starter' pom.xml build.gradle 2>/dev/null || echo 'NOT FOUND')" echo "Boot version: $(grep '' pom.xml 2>/dev/null | head -1 | sed 's/.* //;s/ //')" echo "Database: $(grep 'spring.datasource.url' src/main/resources/application*.yml 2>/dev/null | head -1)" echo "Port: $(grep 'server.port' src/main/resources/application*.yml 2>/dev/null | head -1)"
${basePackage} 是占位符,使用时替换为项目实际根包名