Java Spring Boot 快速上手指南

本文面向有多年前端开发经验、熟悉 Node.js 后端(Express / EggJS / NestJS)的开发者, 从零搭建一个 Spring Boot 项目, 逐步深入到分层架构、数据库操作、安全认证、缓存、消息队列等企业级实践。每个概念都会与前端 / Node.js 生态中的对应物做类比, 帮你快速建立 Java 世界的心智模型
一、为什么是 Spring Boot
在 Java 生态中, Spring 就像前端的 React / Vue 一样, 是事实上的标准框架。而 Spring Boot 则是对 Spring 的开箱即用封装, 类似于 create-react-app 之于 React, 或者 EggJS 之于 Koa
对前端开发者的类比
| Java / Spring Boot | Node.js / 前端 |
|---|---|
| Spring Boot | EggJS / NestJS(约定式框架) |
| Maven / Gradle | npm / pnpm(包管理 + 构建) |
application.yml | .env + config/ 配置文件 |
@RestController | Express 的 router / Koa 的 controller |
@Service | EggJS 的 service 层 |
@Repository (JPA) | Sequelize / TypeORM / Prisma |
@Autowired (依赖注入) | NestJS 的 @Inject() |
| Spring AOP | Express 中间件 / NestJS 拦截器 |
pom.xml / build.gradle | package.json |
Spring Boot 的核心优势
- 自动配置 (Auto Configuration): 引入一个 starter 依赖, 相关配置自动生效, 无需手动 XML 配置
- 内嵌服务器: 自带 Tomcat, 不需要单独部署 WAR 包, 直接
java -jar启动 - Starter 依赖: 一个 starter 打包了一组常用依赖, 类似于
create-react-app帮你预装了 webpack + babel + eslint - 生产就绪: 内置健康检查、指标监控、外部化配置等运维能力
二、环境准备与项目创建
2.1 环境清单
| 工具 | 版本要求 | 说明 |
|---|---|---|
| JDK | 17+ (推荐 21) | Java 开发工具包, 类似 Node.js 运行时 |
| Maven 或 Gradle | Maven 3.8+ / Gradle 7+ | 构建工具, 类似 npm / pnpm |
| IDE | IntelliJ IDEA (推荐) | Java 开发首选, 类似前端的 VS Code |
macOS 快速安装
# 使用 SDKMAN 管理 Java 版本 (类似 nvm)
curl -s "https://get.sdkman.io" | bash
sdk install java 21-tem
# 验证
java -version2.2 创建项目
访问 Spring Initializr 生成项目骨架, 这就像前端用 npm create vite@latest 创建项目一样
推荐初始依赖
- Spring Web: 构建 REST API 的核心
- Spring Data JPA: ORM 框架, 类似 TypeORM / Prisma
- MySQL Driver: 数据库驱动
- Lombok: 减少 Java 样板代码(getter / setter / constructor)
- Spring Boot DevTools: 热重载, 类似 nodemon
- Validation: 参数校验, 类似 class-validator
2.3 项目目录结构
生成后的项目结构如下, 与 EggJS 的约定式目录有异曲同工之妙:
my-project/
├── src/
│ ├── main/
│ │ ├── java/com/example/demo/ # Java 源码 (类似 src/)
│ │ │ ├── DemoApplication.java # 入口文件 (类似 app.js / main.ts)
│ │ │ ├── controller/ # 控制器 (类似 router + controller)
│ │ │ ├── service/ # 业务逻辑 (类似 service/)
│ │ │ ├── repository/ # 数据访问 (类似 model/)
│ │ │ ├── entity/ # 实体类 (类似 ORM 的 entity)
│ │ │ ├── dto/ # 数据传输对象 (类似 TS 的 interface/type)
│ │ │ └── config/ # 配置类 (类似 config/)
│ │ └── resources/
│ │ ├── application.yml # 主配置 (类似 .env + config)
│ │ ├── application-dev.yml # 开发环境配置
│ │ └── application-prod.yml # 生产环境配置
│ └── test/ # 测试 (类似 __tests__/)
├── pom.xml # Maven 依赖 (类似 package.json)
└── mvnw / gradlew # 构建工具 wrapper (类似 npx)三、Java 语言核心速览
写在前面
如果你熟悉 TypeScript, 那 Java 对你来说并不陌生。Java 是强类型语言, TypeScript 的类型系统正是受 Java / C# 启发而来
3.1 类型系统对比
// 基本类型 (Primitive Types)
int count = 10; // TS: number
long bigCount = 100000L; // TS: number (大整数)
double price = 99.9; // TS: number
boolean active = true; // TS: boolean
String name = "Atom"; // TS: string
// 包装类型 (可以为 null, 类似 TS 的 number | null)
Integer nullableCount = null;
Long nullableLong = null;
// 集合类型
List<String> names = List.of("a", "b"); // TS: string[]
Map<String, Object> map = Map.of("key", "val"); // TS: Record<string, any>
Set<String> uniqueNames = Set.of("a", "b"); // TS: Set<string>// 基本类型
let count: number = 10;
let bigCount: number = 100000;
let price: number = 99.9;
let active: boolean = true;
let name: string = "Atom";
// 可空类型
let nullableCount: number | null = null;
// 集合类型
let names: string[] = ["a", "b"];
let map: Record<string, any> = { key: "val" };
let uniqueNames: Set<string> = new Set(["a", "b"]);3.2 类与接口
// Java 的 class 比 TS 更严格, 一个文件通常只有一个 public class
public class User {
private Long id;
private String name;
private String email;
// 构造函数
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getter / Setter (Java 的传统, 类似 TS 的属性访问)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}// Lombok 通过注解自动生成 getter/setter/constructor
// 类似 TS 直接声明 public 属性
@Data // 自动生成 getter + setter + toString + equals + hashCode
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 全参构造
public class User {
private Long id;
private String name;
private String email;
}// TS 的 class, 简洁很多
class User {
constructor(
public id: number,
public name: string,
public email: string
) {}
}Lombok 必知注解
| 注解 | 作用 | TS 类比 |
|---|---|---|
@Data | 生成 getter/setter/toString/equals/hashCode | class 的 public 属性 |
@Builder | 生成 Builder 模式构造 | 对象字面量 { ...spread } |
@NoArgsConstructor | 生成无参构造 | constructor() |
@AllArgsConstructor | 生成全参构造 | constructor(all params) |
@RequiredArgsConstructor | 生成 final 字段构造 | NestJS constructor(private readonly svc) |
@Slf4j | 生成日志对象 | const logger = console |
3.3 注解 (Annotation) = 装饰器 (Decorator)
Java 的注解 @Xxx 和 TypeScript / NestJS 的装饰器 @Xxx 几乎是同一个概念:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// ...
}
@PostMapping
public User createUser(@RequestBody @Valid CreateUserDTO dto) {
// ...
}
}@Controller('/api/users')
export class UserController {
@Get('/:id')
getUser(@Param('id') id: number) {
// ...
}
@Post()
createUser(@Body() dto: CreateUserDTO) {
// ...
}
}四、第一个 REST API
4.1 入口文件
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
// 类似 Node.js 的 app.listen(3000)
SpringApplication.run(DemoApplication.class, args);
}
}@SpringBootApplication 是一个组合注解, 它等价于:
4.2 编写 Controller
与 Express 的对比
在 Express 中, 你会这样写路由:
router.get('/api/hello', (req, res) => {
res.json({ message: 'Hello World' })
})在 Spring Boot 中, 用注解来声明路由:
@RestController
@RequestMapping("/api/v1")
public class HelloController {
@GetMapping("/hello")
public Map<String, String> hello() {
return Map.of("message", "Hello Spring Boot!");
}
@GetMapping("/hello/{name}")
public Map<String, String> helloName(@PathVariable String name) {
return Map.of("message", "Hello, " + name + "!");
}
@GetMapping("/search")
public Map<String, Object> search(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return Map.of("page", page, "size", size);
}
}常用路由注解速查
| 注解 | HTTP 方法 | Express 对应 |
|---|---|---|
@GetMapping | GET | router.get() |
@PostMapping | POST | router.post() |
@PutMapping | PUT | router.put() |
@DeleteMapping | DELETE | router.delete() |
@PatchMapping | PATCH | router.patch() |
@PathVariable | 路径参数 | req.params.id |
@RequestParam | 查询参数 | req.query.page |
@RequestBody | 请求体 | req.body |
@RequestHeader | 请求头 | req.headers['x-token'] |
4.3 启动与验证
# Maven 项目
./mvnw spring-boot:run
# Gradle 项目
./gradlew bootRun
# 访问
curl http://localhost:8080/api/v1/hello
# {"message":"Hello Spring Boot!"}热重载
添加 spring-boot-devtools 依赖后, 修改代码会自动重启应用, 类似 nodemon。在 IDEA 中需要开启 Build project automatically 设置
五、配置管理
5.1 application.yml
Spring Boot 使用 application.yml(或 .properties)管理配置, 类似 Node.js 项目的 .env + config/
# application.yml - 主配置
server:
port: 8080 # 类似 Express 的 app.listen(8080)
spring:
application:
name: my-app # 应用名称
profiles:
active: dev # 激活的环境 (类似 NODE_ENV=development)
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 自动建表/更新表结构
show-sql: true # 打印 SQL (开发时开启)5.2 多环境配置
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb_dev
logging:
level:
root: DEBUGserver:
port: 80
spring:
datasource:
url: jdbc:mysql://prod-db:3306/mydb_prod
logging:
level:
root: WARN5.3 自定义配置绑定
// 类似 Node.js 从 config 读取自定义配置
@Data
@Component
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private String name;
private String version;
private Jwt jwt = new Jwt();
@Data
public static class Jwt {
private String secret;
private long expiration = 86400000; // 24h
}
}# application.yml
app:
name: my-app
version: 1.0.0
jwt:
secret: my-secret-key
expiration: 86400000// 在任何地方注入使用
@Service
@RequiredArgsConstructor
public class AuthService {
private final AppConfig appConfig;
public String getSecret() {
return appConfig.getJwt().getSecret();
}
}六、IoC 与依赖注入
6.1 什么是 IoC
IoC(Inversion of Control, 控制反转) 是 Spring 的灵魂。简单说就是: 你不需要自己 new 对象, Spring 容器帮你创建和管理
对前端的类比
如果你用过 NestJS, 它的依赖注入系统就是从 Spring 借鉴而来:
// NestJS - 和 Spring 几乎一模一样
@Injectable()
export class UserService {
constructor(private readonly userRepo: UserRepository) {}
}// Spring Boot
@Service
public class UserService {
private final UserRepository userRepo;
// 构造器注入 (推荐)
public UserService(UserRepository userRepo) {
this.userRepo = userRepo;
}
}6.2 Bean 的注册方式
Spring 中被容器管理的对象叫做 Bean, 类似 NestJS 中被 @Injectable() 标记的 Provider
| 注解 | 用途 | NestJS 对应 |
|---|---|---|
@Component | 通用组件 | @Injectable() |
@Service | 业务逻辑层 | @Injectable() (Service) |
@Repository | 数据访问层 | @Injectable() (Repository) |
@Controller / @RestController | 控制器 | @Controller() |
@Configuration + @Bean | 手动注册 | providers: [{ provide: ..., useFactory: ... }] |
6.3 注入方式
@Service
@RequiredArgsConstructor // Lombok 自动生成构造器
public class UserService {
// final 字段 = 必须注入, 类似 NestJS 的 private readonly
private final UserRepository userRepository;
private final RedisTemplate<String, String> redisTemplate;
}@Service
public class UserService {
// 不推荐: 无法在测试中轻松 mock
@Autowired
private UserRepository userRepository;
}为什么推荐构造器注入
- 不可变性:
final字段保证注入后不会被修改 - 可测试性: 测试时直接通过构造器传入 mock 对象
- 明确依赖: 一眼看出这个类依赖了什么
- NullSafe: 编译期就能发现缺失的依赖, 而不是运行时 NPE
七、分层架构实战
一个完整的 Spring Boot 应用采用经典的三层架构, 与 EggJS / NestJS 如出一辙:
7.1 Entity 实体类
@Data
@Entity
@Table(name = "t_user")
@DynamicUpdate
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String username;
@Column(nullable = false)
private String password;
@Column(unique = true, length = 100)
private String email;
@Column(name = "phone_number", length = 20)
private String phoneNumber;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "created_at", updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
@Column(name = "updated_at")
@UpdateTimestamp
private LocalDateTime updatedAt;
public enum UserStatus {
ACTIVE, INACTIVE, BANNED
}
}与 TypeORM 的对比
// TypeORM Entity - 几乎一模一样的概念
@Entity('t_user')
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ nullable: false, length: 50 })
username: string
@CreateDateColumn()
createdAt: Date
}7.2 DTO 数据传输对象
DTO 是 Controller 层和 Service 层之间的数据契约, 类似 TypeScript 的 interface + 校验规则:
// 创建用户请求 DTO
@Data
public class CreateUserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度 2-50 个字符")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 100, message = "密码长度 6-100 个字符")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phoneNumber;
}
// 用户响应 DTO (不暴露密码等敏感字段)
@Data
@Builder
public class UserVO {
private Long id;
private String username;
private String email;
private String phoneNumber;
private String status;
private LocalDateTime createdAt;
}DTO vs Entity 的关系
- Request DTO: 接收前端参数 + 校验, 类似
class-validator的 DTO - Entity: 映射数据库表, 内部使用
- Response VO: 返回给前端的数据, 隐藏敏感信息
7.3 Repository 数据访问层
Spring Data JPA 让你只需定义接口, 不用写实现, 框架自动生成 SQL:
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
// 方法名即查询! Spring 自动生成 SQL
// SELECT * FROM t_user WHERE username = ?
Optional<User> findByUsername(String username);
// SELECT * FROM t_user WHERE email = ?
Optional<User> findByEmail(String email);
// SELECT * FROM t_user WHERE status = ? AND created_at > ?
List<User> findByStatusAndCreatedAtAfter(User.UserStatus status, LocalDateTime time);
// 支持分页
Page<User> findByStatus(User.UserStatus status, Pageable pageable);
// 复杂查询可以手写 JPQL
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword%")
Page<User> searchByKeyword(@Param("keyword") String keyword, Pageable pageable);
// 判断是否存在
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}方法命名规则
Spring Data JPA 通过解析方法名自动生成查询, 类似 Prisma 的 findUnique / findMany:
| 方法名关键字 | 生成的 SQL | Prisma 对照 |
|---|---|---|
findBy | WHERE | findMany({ where: {} }) |
And | AND | AND 条件 |
Or | OR | OR 条件 |
OrderBy | ORDER BY | orderBy |
Between | BETWEEN | gte + lte |
Like / Containing | LIKE %?% | contains |
In | IN (?) | in |
IsNull | IS NULL | equals: null |
count | SELECT COUNT(*) | count() |
existsBy | SELECT EXISTS | 手动查 |
7.4 Service 业务逻辑层
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
/**
* 创建用户
*/
@Transactional
public UserVO createUser(CreateUserDTO dto) {
// 1. 业务校验
if (userRepository.existsByUsername(dto.getUsername())) {
throw new BusinessException("用户名已存在");
}
if (dto.getEmail() != null && userRepository.existsByEmail(dto.getEmail())) {
throw new BusinessException("邮箱已被注册");
}
// 2. DTO -> Entity
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setEmail(dto.getEmail());
user.setPhoneNumber(dto.getPhoneNumber());
// 3. 持久化
User saved = userRepository.save(user);
log.info("用户创建成功: id={}, username={}", saved.getId(), saved.getUsername());
// 4. Entity -> VO
return toVO(saved);
}
/**
* 分页查询用户
*/
public Page<UserVO> getUsers(int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size, Sort.by("createdAt").descending());
return userRepository.findAll(pageable).map(this::toVO);
}
/**
* 根据 ID 查询
*/
public UserVO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new BusinessException("用户不存在"));
return toVO(user);
}
/**
* Entity -> VO 转换
*/
private UserVO toVO(User user) {
return UserVO.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.phoneNumber(user.getPhoneNumber())
.status(user.getStatus().name())
.createdAt(user.getCreatedAt())
.build();
}
}@Transactional 事务管理
@Transactional 注解标记的方法, 会在一个数据库事务中执行, 任何异常都会自动回滚。类似 Sequelize 的:
await sequelize.transaction(async (t) => {
await User.create(data, { transaction: t })
})Spring 只需要加一个注解, 不用手动管理事务的开始和提交
7.5 Controller 控制器层
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserVO> createUser(@RequestBody @Valid CreateUserDTO dto) {
UserVO user = userService.createUser(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@GetMapping
public ResponseEntity<Page<UserVO>> getUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(userService.getUsers(page, size));
}
@GetMapping("/{id}")
public ResponseEntity<UserVO> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
}八、统一响应与异常处理
8.1 统一响应格式
前端最熟悉的后端约定: 统一的 JSON 响应结构
@Data
@Builder
public class R<T> {
private int code;
private String message;
private T data;
public static <T> R<T> ok(T data) {
return R.<T>builder()
.code(200)
.message("success")
.data(data)
.build();
}
public static <T> R<T> fail(int code, String message) {
return R.<T>builder()
.code(code)
.message(message)
.build();
}
}8.2 全局异常处理
类似 Express 的错误处理中间件, Spring Boot 用 @ControllerAdvice 捕获全局异常:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<R<Void>> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return ResponseEntity.badRequest().body(R.fail(400, e.getMessage()));
}
/**
* 参数校验异常 (DTO 的 @Valid 校验失败时触发)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<R<Void>> handleValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(R.fail(400, message));
}
/**
* 兜底: 未知异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<R<Void>> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.internalServerError().body(R.fail(500, "服务器内部错误"));
}
}Express 对比
// Express 的全局错误处理中间件
app.use((err, req, res, next) => {
if (err instanceof BusinessError) {
return res.status(400).json({ code: 400, message: err.message })
}
res.status(500).json({ code: 500, message: '服务器内部错误' })
})Spring Boot 的 @RestControllerAdvice 做的事情完全一样, 只是用注解代替了中间件
九、Spring AOP 与拦截器
9.1 AOP 面向切面编程
AOP 是 Spring 的核心特性之一。如果说依赖注入解决了"对象怎么创建和组装"的问题, 那 AOP 解决的就是"怎么在不修改业务代码的情况下, 统一添加日志、权限、事务等横切关注点"
对 Express / Koa 中间件的类比
| Spring 机制 | Express / Koa 对应 | 执行时机 |
|---|---|---|
Filter | app.use(cors()) / app.use(bodyParser()) | 所有请求, 最外层 |
Interceptor | app.use(authMiddleware) | 进入 Controller 前后 |
@Aspect (AOP) | NestJS 的 @UseInterceptors() | 方法执行前后 |
@ControllerAdvice | app.use(errorHandler) | 异常发生时 |
9.2 自定义拦截器
@Slf4j
@Component
public class RequestLogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
log.info("[请求开始] {} {}", request.getMethod(), request.getRequestURI());
return true; // true = 放行, false = 拦截
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
log.info("[请求结束] {} {} - {}ms", request.getMethod(), request.getRequestURI(), duration);
}
}
// 注册拦截器
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final RequestLogInterceptor requestLogInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLogInterceptor)
.addPathPatterns("/api/**") // 拦截 /api/ 下的所有请求
.excludePathPatterns("/api/v1/auth/**"); // 排除登录接口
}
}9.3 自定义 AOP 切面
实现一个方法执行耗时统计的切面:
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
// 切入点: 匹配所有 Service 层的 public 方法
@Around("execution(* com.example.demo.service.*.*(..))")
public Object logPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed(); // 执行目标方法
long duration = System.currentTimeMillis() - start;
if (duration > 500) {
log.warn("[慢方法] {} 耗时 {}ms", methodName, duration);
}
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - start;
log.error("[方法异常] {} 耗时 {}ms, 异常: {}", methodName, duration, e.getMessage());
throw e;
}
}
}十、数据库进阶
10.1 动态查询 (JPA Specification)
当查询条件不固定时(比如搜索页面的多个可选筛选条件), 使用 Specification 动态拼接 WHERE 子句:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public Page<UserVO> searchUsers(String keyword, User.UserStatus status,
int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
Specification<User> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 关键词搜索 (用户名或邮箱)
if (StringUtils.hasText(keyword)) {
Predicate nameLike = cb.like(root.get("username"), "%" + keyword + "%");
Predicate emailLike = cb.like(root.get("email"), "%" + keyword + "%");
predicates.add(cb.or(nameLike, emailLike));
}
// 状态筛选
if (status != null) {
predicates.add(cb.equal(root.get("status"), status));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
return userRepository.findAll(spec, pageable).map(this::toVO);
}
}Prisma 对比
// Prisma 的动态查询
const users = await prisma.user.findMany({
where: {
OR: keyword ? [
{ username: { contains: keyword } },
{ email: { contains: keyword } },
] : undefined,
status: status ?? undefined,
},
skip: (page - 1) * size,
take: size,
})两者的思路完全一致: 根据传入条件动态拼接查询
10.2 复杂关联查询
// 一对多关系
@Entity
@Table(name = "t_department")
@Data
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 一个部门有多个用户
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<User> users;
}
// 多对一关系
@Entity
@Table(name = "t_user")
@Data
public class User {
// ...其他字段
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
}N+1 查询问题
JPA 默认使用懒加载(LAZY), 循环访问关联对象时会产生 N+1 查询问题(和 TypeORM / Sequelize 一样)。解决方案:
- JPQL fetch join:
SELECT u FROM User u JOIN FETCH u.department - EntityGraph:
@EntityGraph(attributePaths = {"department"}) - 直接用 DTO 投影: 不查关联实体, 只查需要的字段
十一、Redis 缓存集成
11.1 基础配置
spring:
data:
redis:
host: localhost
port: 6379
password: ""
database: 011.2 使用 RedisTemplate
@Service
@RequiredArgsConstructor
public class CacheService {
private final RedisTemplate<String, Object> redisTemplate;
// 缓存用户信息 (类似 Node.js 的 redis.set)
public void cacheUser(Long userId, UserVO user) {
String key = "user:" + userId;
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
}
// 获取缓存
public UserVO getCachedUser(Long userId) {
String key = "user:" + userId;
return (UserVO) redisTemplate.opsForValue().get(key);
}
// 删除缓存
public void evictUser(Long userId) {
redisTemplate.delete("user:" + userId);
}
}11.3 注解式缓存
Spring Cache 提供了更优雅的声明式缓存, 只需加注解:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// 查询时自动缓存, key = "user::1"
@Cacheable(value = "user", key = "#id")
public UserVO getUserById(Long id) {
log.info("从数据库查询用户: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new BusinessException("用户不存在"));
return toVO(user);
}
// 更新时自动更新缓存
@CachePut(value = "user", key = "#id")
public UserVO updateUser(Long id, UpdateUserDTO dto) {
// ...更新逻辑
}
// 删除时自动清除缓存
@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}缓存注解速查
| 注解 | 作用 | 等效操作 |
|---|---|---|
@Cacheable | 先查缓存, 没有才执行方法 | cache.get(key) ?? (await fn()) |
@CachePut | 执行方法并更新缓存 | cache.set(key, fn()) |
@CacheEvict | 执行方法并删除缓存 | cache.del(key) |
十二、安全认证 (JWT)
12.1 认证流程
12.2 JWT 工具类
@Component
@RequiredArgsConstructor
public class JwtUtil {
private final AppConfig appConfig;
// 生成 Token
public String generateToken(Long userId, String username) {
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("username", username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()
+ appConfig.getJwt().getExpiration()))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 解析 Token
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 从 Token 中获取用户 ID
public Long getUserId(String token) {
return Long.parseLong(parseToken(token).getSubject());
}
private Key getSigningKey() {
byte[] keyBytes = appConfig.getJwt().getSecret().getBytes();
return Keys.hmacShaKeyFor(keyBytes);
}
}12.3 Security 过滤器链配置
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF (前后端分离不需要)
.csrf(csrf -> csrf.disable())
// 禁用 Session (用 JWT 无状态认证)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 路由权限配置
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll() // 登录注册公开
.requestMatchers("/actuator/**").permitAll() // 监控端点公开
.anyRequest().authenticated() // 其他都需要认证
)
// 在 UsernamePasswordAuthenticationFilter 之前插入 JWT 过滤器
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}12.4 JWT 过滤器
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Long userId = jwtUtil.getUserId(token);
User user = userRepository.findById(userId).orElse(null);
if (user != null) {
// 将用户信息放入 SecurityContext, 后续可通过 SecurityContextHolder 获取
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(user, null, List.of());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (Exception e) {
// Token 无效, 不设置认证信息, 后续会被 Spring Security 拦截
}
}
filterChain.doFilter(request, response);
}
}十三、消息队列集成
在大型项目中, 很多操作不需要同步完成(比如发送邮件、生成报告、写日志)。消息队列可以将这些耗时任务异步化
13.1 消息队列概念
与前端的类比
消息队列就像浏览器的 postMessage / BroadcastChannel, 或者 Node.js 的 EventEmitter:
// Node.js EventEmitter
emitter.emit('user:registered', { userId: 1 })
// 多个消费者监听
emitter.on('user:registered', sendWelcomeEmail)
emitter.on('user:registered', initUserProfile)区别在于消息队列是跨进程、可持久化的, 即使消费者宕机, 消息也不会丢失
13.2 使用 RocketMQ
// 生产者: 发送消息
@Service
@RequiredArgsConstructor
public class MessageProducer {
private final RocketMQTemplate rocketMQTemplate;
public void sendUserRegisteredEvent(Long userId, String username) {
UserRegisteredEvent event = new UserRegisteredEvent(userId, username);
rocketMQTemplate.convertAndSend("user-registered-topic", event);
}
// 延迟消息 (比如: 注册后 30 分钟发问卷)
public void sendDelayedMessage(Long userId) {
Message<Long> message = MessageBuilder.withPayload(userId).build();
// delayLevel: 1=1s, 2=5s, 3=10s, ..., 16=2h
rocketMQTemplate.syncSend("survey-topic", message, 3000, 14); // 延迟 10 分钟
}
}
// 消费者: 处理消息
@Slf4j
@Component
@RocketMQMessageListener(
topic = "user-registered-topic",
consumerGroup = "email-consumer-group"
)
public class WelcomeEmailConsumer implements RocketMQListener<UserRegisteredEvent> {
@Override
public void onMessage(UserRegisteredEvent event) {
log.info("发送欢迎邮件给用户: {}", event.getUsername());
// 发送邮件逻辑...
}
}十四、定时任务
14.1 Spring 内置定时任务
适合简单的单机定时任务:
@Slf4j
@Component
@EnableScheduling
public class ScheduledTasks {
// 每天凌晨 2 点执行 (Cron 表达式, 和 Node.js 的 node-cron 一样)
@Scheduled(cron = "0 0 2 * * ?")
public void cleanExpiredData() {
log.info("开始清理过期数据...");
// 清理逻辑
}
// 每 5 分钟执行一次
@Scheduled(fixedRate = 300000)
public void healthCheck() {
log.info("执行健康检查...");
}
}14.2 分布式定时任务 (XXL-Job)
当应用部署多个实例时, 内置的 @Scheduled 每个实例都会执行。分布式场景需要用 XXL-Job 这类调度平台:
@Slf4j
@Component
public class DataSyncJob {
@XxlJob("dataSyncJobHandler")
public void execute() {
log.info("XXL-Job 触发数据同步任务");
// 分布式环境下只有一个实例会执行
}
}十五、项目实战架构
综合以上所有知识, 一个生产级的 Spring Boot 项目架构如下:
多模块项目结构
企业级项目通常采用多模块结构, 用 Gradle 或 Maven 管理:
my-project/
├── build.gradle # 根构建文件
├── settings.gradle # 模块注册
├── my-common/ # 公共模块
│ ├── my-common-core/ # 核心工具类
│ └── my-common-pojo/ # 公共 POJO / DTO
├── my-platform/ # 平台模块
│ └── my-platform-auth/ # 认证授权
├── my-user/ # 用户模块
│ ├── my-user-common/ # 模块内公共
│ ├── my-user-service/ # 业务实现
│ └── my-user-api/ # Feign 接口 (微服务调用)
├── my-order/ # 订单模块
│ ├── my-order-common/
│ ├── my-order-service/
│ └── my-order-api/
└── docker-compose.yml # 基础设施十六、常用命令速查
构建与运行
# Maven
./mvnw clean install # 类似 npm install && npm run build
./mvnw spring-boot:run # 类似 npm run dev
./mvnw package -DskipTests # 打包跳过测试
# Gradle
./gradlew clean build # 构建
./gradlew bootRun # 开发运行
./gradlew bootJar # 打成可执行 JAR
# Docker 部署
docker build -t my-app .
docker run -p 8080:8080 my-app常用 Actuator 端点
# 健康检查 (类似 Node.js 的 /healthz)
curl http://localhost:8080/actuator/health
# 查看所有配置
curl http://localhost:8080/actuator/env
# Prometheus 指标
curl http://localhost:8080/actuator/prometheus十七、从 Node.js 到 Spring Boot 的思维转换
写给前端开发者的建议
- 拥抱 IDE: Java 开发离不开 IntelliJ IDEA, 它的代码补全、重构、调试能力远超 VS Code 写 Java 的体验
- 不要怕样板代码: Java 确实比 JS/TS 啰嗦, 但 Lombok + IDEA 快捷键可以大幅缓解
- 理解编译期 vs 运行时: Java 的很多错误在编译期就能发现, 这是优势而非束缚
- 善用 Spring 文档: Spring 官方文档 质量极高, 是最好的学习资源
- AI 辅助开发: 借助 AI 工具快速理解 Java 语法和 Spring 惯用写法, 可以大幅降低学习曲线