Skip to content

Java Spring Boot 快速上手指南

作者:Atom
字数统计:7k 字
阅读时长:31 分钟

本文面向有多年前端开发经验、熟悉 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 BootNode.js / 前端
Spring BootEggJS / NestJS(约定式框架)
Maven / Gradlenpm / pnpm(包管理 + 构建)
application.yml.env + config/ 配置文件
@RestControllerExpress 的 router / Koa 的 controller
@ServiceEggJS 的 service
@Repository (JPA)Sequelize / TypeORM / Prisma
@Autowired (依赖注入)NestJS 的 @Inject()
Spring AOPExpress 中间件 / NestJS 拦截器
pom.xml / build.gradlepackage.json

Spring Boot 的核心优势

  1. 自动配置 (Auto Configuration): 引入一个 starter 依赖, 相关配置自动生效, 无需手动 XML 配置
  2. 内嵌服务器: 自带 Tomcat, 不需要单独部署 WAR 包, 直接 java -jar 启动
  3. Starter 依赖: 一个 starter 打包了一组常用依赖, 类似于 create-react-app 帮你预装了 webpack + babel + eslint
  4. 生产就绪: 内置健康检查、指标监控、外部化配置等运维能力

二、环境准备与项目创建

2.1 环境清单

工具版本要求说明
JDK17+ (推荐 21)Java 开发工具包, 类似 Node.js 运行时
Maven 或 GradleMaven 3.8+ / Gradle 7+构建工具, 类似 npm / pnpm
IDEIntelliJ IDEA (推荐)Java 开发首选, 类似前端的 VS Code

macOS 快速安装

sh
# 使用 SDKMAN 管理 Java 版本 (类似 nvm)
curl -s "https://get.sdkman.io" | bash
sdk install java 21-tem

# 验证
java -version

2.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 类型系统对比

java
// 基本类型 (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>
typescript
// 基本类型
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
// 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;
    }
}
java
// 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;
}
typescript
// TS 的 class, 简洁很多
class User {
    constructor(
        public id: number,
        public name: string,
        public email: string
    ) {}
}

Lombok 必知注解

注解作用TS 类比
@Data生成 getter/setter/toString/equals/hashCodeclass 的 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 几乎是同一个概念:

java
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    public User createUser(@RequestBody @Valid CreateUserDTO dto) {
        // ...
    }
}
typescript
@Controller('/api/users')
export class UserController {

    @Get('/:id')
    getUser(@Param('id') id: number) {
        // ...
    }

    @Post()
    createUser(@Body() dto: CreateUserDTO) {
        // ...
    }
}

四、第一个 REST API

4.1 入口文件

java
@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 中, 你会这样写路由:

js
router.get('/api/hello', (req, res) => {
  res.json({ message: 'Hello World' })
})

在 Spring Boot 中, 用注解来声明路由:

java
@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 对应
@GetMappingGETrouter.get()
@PostMappingPOSTrouter.post()
@PutMappingPUTrouter.put()
@DeleteMappingDELETErouter.delete()
@PatchMappingPATCHrouter.patch()
@PathVariable路径参数req.params.id
@RequestParam查询参数req.query.page
@RequestBody请求体req.body
@RequestHeader请求头req.headers['x-token']

4.3 启动与验证

sh
# 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/

yaml
# 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 多环境配置

yaml
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb_dev
logging:
  level:
    root: DEBUG
yaml
server:
  port: 80
spring:
  datasource:
    url: jdbc:mysql://prod-db:3306/mydb_prod
logging:
  level:
    root: WARN

5.3 自定义配置绑定

java
// 类似 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
    }
}
yaml
# application.yml
app:
  name: my-app
  version: 1.0.0
  jwt:
    secret: my-secret-key
    expiration: 86400000
java
// 在任何地方注入使用
@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 借鉴而来:

typescript
// NestJS - 和 Spring 几乎一模一样
@Injectable()
export class UserService {
    constructor(private readonly userRepo: UserRepository) {}
}
java
// 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 注入方式

java
@Service
@RequiredArgsConstructor  // Lombok 自动生成构造器
public class UserService {
    // final 字段 = 必须注入, 类似 NestJS 的 private readonly
    private final UserRepository userRepository;
    private final RedisTemplate<String, String> redisTemplate;
}
java
@Service
public class UserService {
    // 不推荐: 无法在测试中轻松 mock
    @Autowired
    private UserRepository userRepository;
}

为什么推荐构造器注入

  1. 不可变性: final 字段保证注入后不会被修改
  2. 可测试性: 测试时直接通过构造器传入 mock 对象
  3. 明确依赖: 一眼看出这个类依赖了什么
  4. NullSafe: 编译期就能发现缺失的依赖, 而不是运行时 NPE

七、分层架构实战

一个完整的 Spring Boot 应用采用经典的三层架构, 与 EggJS / NestJS 如出一辙:

7.1 Entity 实体类

java
@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 的对比

typescript
// 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 + 校验规则:

java
// 创建用户请求 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:

java
@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:

方法名关键字生成的 SQLPrisma 对照
findByWHEREfindMany({ where: {} })
AndANDAND 条件
OrOROR 条件
OrderByORDER BYorderBy
BetweenBETWEENgte + lte
Like / ContainingLIKE %?%contains
InIN (?)in
IsNullIS NULLequals: null
countSELECT COUNT(*)count()
existsBySELECT EXISTS手动查

7.4 Service 业务逻辑层

java
@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 的:

js
await sequelize.transaction(async (t) => {
  await User.create(data, { transaction: t })
})

Spring 只需要加一个注解, 不用手动管理事务的开始和提交

7.5 Controller 控制器层

java
@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 响应结构

java
@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 捕获全局异常:

java
@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 对比

javascript
// 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 对应执行时机
Filterapp.use(cors()) / app.use(bodyParser())所有请求, 最外层
Interceptorapp.use(authMiddleware)进入 Controller 前后
@Aspect (AOP)NestJS 的 @UseInterceptors()方法执行前后
@ControllerAdviceapp.use(errorHandler)异常发生时

9.2 自定义拦截器

java
@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 切面

实现一个方法执行耗时统计的切面:

java
@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 子句:

java
@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 对比

typescript
// 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 复杂关联查询

java
// 一对多关系
@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 一样)。解决方案:

  1. JPQL fetch join: SELECT u FROM User u JOIN FETCH u.department
  2. EntityGraph: @EntityGraph(attributePaths = {"department"})
  3. 直接用 DTO 投影: 不查关联实体, 只查需要的字段

十一、Redis 缓存集成

11.1 基础配置

yaml
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: ""
      database: 0

11.2 使用 RedisTemplate

java
@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 提供了更优雅的声明式缓存, 只需加注解:

java
@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 工具类

java
@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 过滤器链配置

java
@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 过滤器

java
@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:

javascript
// Node.js EventEmitter
emitter.emit('user:registered', { userId: 1 })

// 多个消费者监听
emitter.on('user:registered', sendWelcomeEmail)
emitter.on('user:registered', initUserProfile)

区别在于消息队列是跨进程、可持久化的, 即使消费者宕机, 消息也不会丢失

13.2 使用 RocketMQ

java
// 生产者: 发送消息
@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 内置定时任务

适合简单的单机定时任务:

java
@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 这类调度平台:

java
@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              # 基础设施

十六、常用命令速查

构建与运行

sh
# 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 端点

sh
# 健康检查 (类似 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 的思维转换

写给前端开发者的建议

  1. 拥抱 IDE: Java 开发离不开 IntelliJ IDEA, 它的代码补全、重构、调试能力远超 VS Code 写 Java 的体验
  2. 不要怕样板代码: Java 确实比 JS/TS 啰嗦, 但 Lombok + IDEA 快捷键可以大幅缓解
  3. 理解编译期 vs 运行时: Java 的很多错误在编译期就能发现, 这是优势而非束缚
  4. 善用 Spring 文档: Spring 官方文档 质量极高, 是最好的学习资源
  5. AI 辅助开发: 借助 AI 工具快速理解 Java 语法和 Spring 惯用写法, 可以大幅降低学习曲线