从 NestJS 到 Spring Boot:一次完整的 Node.js → Java 后端迁移实战

本文将带你把一个已运行的 NestJS + TypeORM + MySQL 后端项目,完整迁移到 Spring Boot 3 + Spring Data JPA + MySQL。不是玩具 Demo,是真实 API 验证通过的可运行项目,附源码对照、踩坑记录和一张速查映射表。


0. 为什么要迁移?

先亮明态度:没有对错,只有取舍

维度 NestJS (TypeScript) Spring Boot (Java)
学习曲线 低(前端转 Node 友好) 中(Java 生态厚重)
运行性能 单线程 Event Loop 多线程 JVM,吞吐更高
类型安全 编译时 TS 类型 + class-validator 编译时 + 运行时字节码校验
企业级生态 快速成长中,但不如 Spring 成熟 20 年积累,稳如老狗
部署体积 node_modules 动辄几百 MB fat jar 几十 MB,自带 GC
适合场景 快速原型、全栈同构、小中型服务 金融/政企/大型分布式系统

本文的迁移背景是一个教学用全栈 Demo:登录注册 + 商品 CRUD + Bug 演示模块。原 NestJS 后端已经跑在生产 MySQL 上,我们要让 Java 版本零改动对接原前端。


1. 最终目录结构对照

迁移前 vs 迁移后,整体模块划分几乎一致 ------ 这正是 NestJS 和 Spring Boot 架构理念同源的体现。

scss 复制代码
NestJS (server/)                          Spring Boot (server-java/)
├── src/                                  ├── src/main/java/com/fullstack/demo/
│   ├── main.ts            ← 启动入口      │   ├── FullstackDemoApplication.java
│   ├── app.module.ts      ← 根模块        │   │   (注解自动扫描,不需要 AppModule)
│   ├── auth/              ← 认证模块      │   ├── auth/
│   │   ├── auth.controller.ts            │   │   ├── AuthController.java
│   │   ├── auth.service.ts               │   │   ├── AuthService.java
│   │   ├── auth.module.ts                │   │   ├── User.java          ← JPA Entity
│   │   ├── user.entity.ts                │   │   ├── UserRepository.java  ← 替代 Repository<User>
│   │   └── dto/                          │   │   ├── RegisterDto.java (jakarta.validation)
│   ├── products/          ← 商品模块      │   │   ├── LoginDto.java
│   │   ├── products.controller.ts        │   │   └── JwtUtil.java        ← 替代 @nestjs/jwt
│   │   ├── products.service.ts           │   ├── products/
│   │   ├── product.entity.ts             │   │   ├── Product.java
│   │   └── dto/                          │   │   ├── ProductsController.java
│   └── bug/               ← Bug演示      │   │   └── ProductsService.java
│       └── bug.controller.ts              │   ├── bug/
├── package.json                          │   │   └── BugController.java
├── tsconfig.json                         │   └── common/                ← 新增:通用模块
│                                         │       ├── ApiResponse.java
│                                         │       ├── GlobalExceptionHandler.java
│                                         │       └── SecurityConfig.java
│                                         ├── pom.xml
│                                         ├── mvnw / mvnw.cmd         ← Maven Wrapper
│                                         └── src/main/resources/application.yml

核心观察:

  • NestJS 的 *.module.ts(模块装饰器)消失了 ------ Spring Boot 用 @SpringBootApplication + 包扫描自动装配
  • 新增了 common/ 包放横切关注点(统一响应、全局异常、安全配置),这在 NestJS 里通常由 Guard/Interceptor/Filter 实现
  • package.jsonpom.xml.envapplication.yml

2. 实体映射:TypeORM Decorator vs JPA Annotation

这是迁移中最直接的一步 ------ TypeORM 装饰器和 JPA 注解几乎一一对应,但有几个细节差异值得注意。

NestJS 原版

typescript 复制代码
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 50, unique: true })
  username: string;

  @Column({ length: 100, unique: true })
  email: string;

  @Column({ length: 255 })
  password: string;

  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
  updatedAt: Date;
}

Java 版本

java 复制代码
// User.java
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // ← 对应 @PrimaryGeneratedColumn
    private Long id;                                     // ← 用 Long(NestJS 用 number)

    @Column(length = 50, unique = true, nullable = false)
    private String username;

    @Column(length = 100, unique = true, nullable = false)
    private String email;

    @Column(length = 255, nullable = false)
    private String password;

    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;                     // ← 用 LocalDateTime(NestJS 用 Date)

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @PrePersist                                          // ← 对应 @CreateDateColumn
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate                                           // ← 对应 @UpdateDateColumn
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    // ... getters & setters
}

关键差异点

项目 NestJS / TypeORM Spring Boot / JPA
主键自增 @PrimaryGeneratedColumn() @GeneratedValue(strategy = GenerationType.IDENTITY)
时间类型 Date LocalDateTime(推荐,无 timezone 坑)
自动填充时间 @CreateDateColumn / @UpdateDateColumn @PrePersist / @PreUpdate 生命周期回调
数值精度 @Column({ type: 'decimal', precision: 10, scale: 2 }) + transformer @Column(precision = 10, scale = 2) + BigDecimal
类型包装 number Long / Integer(对象类型,null 安全)

踩坑记录 #1: Product 实体中 TypeORM 用了 transformer: { to: (v) => v, from: (v) => Number(v) } 来处理 MySQL DECIMAL 返回字符串的问题。Java 侧 不需要 transformer ,直接用 BigDecimal,JDBC 驱动原生处理类型转换。


3. DTO 校验:class-validator vs jakarta.validation

NestJS 靠 class-validator 装饰器 + 全局 ValidationPipe 实现参数校验;Spring Boot 用 jakarta.validation 注解 + @Valid 触发校验。注解写法几乎一样,但触发机制不同。

NestJS 原版

typescript 复制代码
// register.dto.ts
import { IsString, MinLength, IsEmail } from 'class-validator';

export class RegisterDto {
  @IsString()
  @MinLength(3, { message: '用户名至少 3 位' })
  username: string;

  @IsEmail({}, { message: '邮箱格式不正确' })
  email: string;

  @IsString()
  @MinLength(6, { message: '密码至少 6 位' })
  password: string;
}
typescript 复制代码
// main.ts 全局开启 ValidationPipe
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,   // 剔除 DTO 未定义字段
  transform: true,   // 自动类型转换
}));

Java 版本

java 复制代码
// RegisterDto.java
public class RegisterDto {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, message = "用户名至少 3 位")
    private String username;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码至少 6 位")
    private String password;

    // ... getters & setters
}
java 复制代码
// AuthController.java ------ 在方法参数上加 @Valid
@PostMapping("/register")
public ApiResponse<AuthService.SafeUser> register(
        @Valid @RequestBody RegisterDto dto) {   // ← @Valid 触发校验
    // ...
}

关键差异点

项目 NestJS Spring Boot
必填校验 @IsString() + 空值检查 @NotBlank / @NotNull(JSR-380)
最小长度 @MinLength(n) @Size(min = n)
数字最小值 @Min(n) @Min(n)(注解同名)
可选字段 @IsOptional() 直接不加注解即可(null 默认跳过校验)
校验触发 全局 ValidationPipe 自动对所有 Body 生效 必须在 Controller 方法参数上加 @Valid

踩坑记录 #2: 别忘记在 Controller 的 @RequestBody 参数前加 @Valid!不加的话 Spring Boot 完全不会执行任何校验,所有脏数据会直接进 Service 层。


4. 认证链路:bcrypt + JWT 全对照

这是迁移中最有技术含量的部分 ------ 密码加密 + Token 生成 + 登录验证。我们用的是业界标准方案,但两侧的依赖和实现方式不同。

NestJS 原版 AuthService

typescript 复制代码
// auth.service.ts
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
import { UnauthorizedException, ConflictException } from '@nestjs/common';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User) private readonly userRepo: Repository<User>,
    private readonly jwtService: JwtService,
  ) {}

  async login(dto: LoginDto) {
    const user = await this.userRepo.findOne({ where: { username: dto.username } });
    if (!user) throw new UnauthorizedException('用户名或密码错误');

    const passwordMatch = await bcrypt.compare(dto.password, user.password);
    if (!passwordMatch) throw new UnauthorizedException('用户名或密码错误');

    const payload = { sub: user.id, username: user.username };
    const token = this.jwtService.sign(payload);

    const { password, ...safeUser } = user;
    return { token, user: safeUser };
  }
}

Java 版本 AuthService

java 复制代码
// AuthService.java
@Service
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;  // ← Spring Security BCrypt
    private final JwtUtil jwtUtil;                   // ← 我们自己封装的 jjwt

    public AuthService(UserRepository userRepository,
                       PasswordEncoder passwordEncoder,
                       JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtUtil = jwtUtil;
    }

    public LoginResult login(LoginDto dto) {
        User user = userRepository.findByUsername(dto.getUsername())
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.UNAUTHORIZED, "用户名或密码错误"));

        if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
            throw new ResponseStatusException(
                    HttpStatus.UNAUTHORIZED, "用户名或密码错误");
        }

        String token = jwtUtil.generateToken(user.getId(), user.getUsername());

        SafeUser safeUser = toSafeUser(user);  // ← 手动剔除 password
        return new LoginResult(token, safeUser);
    }
}

依赖对照

功能 NestJS npm 包 Spring Boot Maven 依赖
密码加密 bcrypt spring-boot-starter-security(内置 BCryptPasswordEncoder)
JWT 生成/解析 @nestjs/jwt(封装了 jsonwebtoken) io.jsonwebtoken:jjwt-api + jjwt-impl + jjwt-jackson
数据库访问 @nestjs/typeorm + typeorm spring-boot-starter-data-jpa
异常抛出 UnauthorizedException ResponseStatusException(HttpStatus.UNAUTHORIZED)

BCrypt 配置一致性

两边都用 salt rounds = 10,这一点很重要 ------ 如果 rounds 不一致,同密码加密结果不同,用户就登录不上了。

typescript 复制代码
// NestJS ------ auth.service.ts
const hashedPassword = await bcrypt.hash(dto.password, 10);
java 复制代码
// Spring Boot ------ SecurityConfig.java
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(10);  // ← 同样的 10
}

JwtUtil 实现(自研替代 @nestjs/jwt)

@nestjs/jwt 是 NestJS 生态的封装,Spring Boot 没有直接对应物。我们用 jjwt 自己写一个 40 行的工具类:

java 复制代码
// JwtUtil.java ------ 完整实现
@Component
public class JwtUtil {

    private final SecretKey secretKey;
    private final long expiration;

    public JwtUtil(@Value("${jwt.secret}") String secret,
                   @Value("${jwt.expiration}") long expiration) {
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        if (keyBytes.length < 32) {
            byte[] padded = new byte[32];
            System.arraycopy(keyBytes, 0, padded, 0, Math.min(keyBytes.length, 32));
            keyBytes = padded;
        }
        this.secretKey = Keys.hmacShaKeyFor(keyBytes);
        this.expiration = expiration;
    }

    public String generateToken(Long userId, String username) {
        long now = System.currentTimeMillis();
        return Jwts.builder()
                .subject(String.valueOf(userId))
                .claim("username", username)
                .issuedAt(new Date(now))
                .expiration(new Date(now + expiration))
                .signWith(secretKey)
                .compact();
    }
}

踩坑记录 #3: jjwt 0.12.x 要求 HMAC 密钥至少 32 字节 。NestJS 的 JWT_SECRET 经常写个短字符串(比如 'dev-secret'),直接拿过来会导致 WeakKeyException。解决办法是像上面那样 padding 到 32 字节,或者换个够长的 secret。


5. 统一响应与异常处理架构

NestJS 有一套非常优雅的架构来处理「所有接口返回统一的 { code, data, message } 格式」和「所有异常转成统一格式」,Spring Boot 也能做到,但方式不同。

5.1 统一响应体

NestJSInterceptor 在响应离开 Controller 之前做一层包装:

typescript 复制代码
// 伪代码 ------ NestJS 统一响应
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context, next) {
    return next.handle().pipe(
      map(data => ({ code: 0, data, message: 'success' })),
    );
  }
}

Spring Boot 更简单 ------ 直接让 Controller 返回 ApiResponse<T> 泛型类即可:

java 复制代码
// ApiResponse.java ------ 一个泛型 POJO
public class ApiResponse<T> {
    private int code;
    private T data;
    private String message;

    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(0, data, "success");
    }
    public static <T> ApiResponse<T> success(T data, String message) {
        return new ApiResponse<>(0, data, message);
    }
    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, null, message);
    }
}

用静态工厂方法 + 泛型,Controller 代码变得极其干净:

java 复制代码
@GetMapping("/products")
public ApiResponse<List<Product>> findAll() {
    return ApiResponse.success(productsService.findAll());
}

5.2 全局异常处理

NestJSExceptionFilter

typescript 复制代码
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception, host) {
    const response = host.switchToHttp().getResponse();
    response.status(status).json({
      code: exception.status || 500,
      data: null,
      message: exception.message,
    });
  }
}

Spring Boot@ControllerAdvice ------ 类名本身就在说"我是全 Controller 的 Advice":

java 复制代码
// GlobalExceptionHandler.java ------ 一个类覆盖所有场景
@ControllerAdvice
public class GlobalExceptionHandler {

    // 业务异常:409/404/400
    @ExceptionHandler(ResponseStatusException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusiness(ResponseStatusException ex) {
        HttpStatus status = HttpStatus.valueOf(ex.getStatusCode().value());
        return ResponseEntity.status(status)
                .body(new ApiResponse<>(status.value(), null, ex.getReason()));
    }

    // 参数校验异常:400
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException ex) {
        String msg = ex.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining("; "));
        return ResponseEntity.badRequest().body(new ApiResponse<>(400, null, msg));
    }

    // 兜底:500
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleAll(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ApiResponse<>(500, null, "服务器内部错误: " + ex.getMessage()));
    }
}

异常抛出对照

NestJS 提供了一整套语义化异常类,Spring Boot 没有对应物,我们用 ResponseStatusException 作为统一替代:

NestJS 异常 HTTP 状态码 Spring Boot 等价写法
ConflictException 409 new ResponseStatusException(HttpStatus.CONFLICT, "xxx")
UnauthorizedException 401 new ResponseStatusException(HttpStatus.UNAUTHORIZED, "xxx")
NotFoundException 404 new ResponseStatusException(HttpStatus.NOT_FOUND, "xxx")
BadRequestException 400 new ResponseStatusException(HttpStatus.BAD_REQUEST, "xxx")

6. CORS 配置与安全

NestJS 原版

typescript 复制代码
// main.ts
app.enableCors({
  origin: 'http://localhost:5173',
  credentials: true,
});

Spring Boot 版本

java 复制代码
// SecurityConfig.java ------ 放在 @Configuration 类中
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("http://localhost:5173"));
    config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
        .csrf(csrf -> csrf.disable())                    // 无状态 JWT 不需要 CSRF
        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth.anyRequest().permitAll());  // 所有接口放行
    return http.build();
}

为什么 Spring Security 默认就加进来了? 因为我们要用 BCryptPasswordEncoder。一旦引入了 spring-boot-starter-security,Spring Boot 就会自动开启一整套安全过滤链,必须显式配置才能让接口正常放行。这是 NestJS 没有的「副作用」。


7. 踩坑全记录(Windows + Spring Boot 迁移特辑)

坑 #1:MySQL JDBC characterEncoding 参数

yaml 复制代码
# ❌ 错误!characterEncoding 接受 Java charset 名,不是 MySQL charset 名
url: jdbc:mysql://localhost:3306/db?characterEncoding=utf8mb4

# ✅ 正确
url: jdbc:mysql://localhost:3306/db?characterEncoding=UTF-8

NestJS 用 mysql2 驱动,它直接理解 utf8mb4;但 Spring Boot 用的是 MySQL Connector J,它要的是 Java 标准 charset 名

坑 #2:PowerShell 命令分隔符

powershell 复制代码
# ❌ PowerShell 不支持 &&
npm install && npm run build

# ✅ PowerShell 用分号
npm install; npm run build

# 或者直接用 Maven Wrapper 一条命令搞定
.\mvnw.cmd spring-boot:run

坑 #3:Maven 没装?用 Maven Wrapper!

powershell 复制代码
# 不装 Maven,项目里自带 mvnw / mvnw.cmd
.\mvnw.cmd spring-boot:run    # Windows
./mvnw spring-boot:run        # macOS / Linux

Maven Wrapper 会自动下载指定版本的 Maven 到 ~/.m2/wrapper/dists/团队每个人用的 Maven 版本完全一致,CI/CD 也零配置。

坑 #4:端口被上一个 Java 进程占用

Spring Boot 对 ServerPortAlreadyInUseException硬退出,不会像 NestJS 那样等几秒重试。快速定位:

powershell 复制代码
netstat -ano | findstr :3000     # 找到占用的 PID
taskkill /PID <PID> /F            # 干掉

坑 #5:jjwt 密钥长度

jjwt 0.12.x 强制要求 HMAC 密钥至少 32 字节。短 secret 直接触发 WeakKeyException。解决方案见上文 JwtUtil 的 padding 代码。


8. 最终运行验证

启动日志干净利落:

csharp 复制代码
Tomcat started on port 3000 (http) with context path ''
Started FullstackDemoApplication in 4.645 seconds

API 测试(用 PowerShell 的 Invoke-RestMethod 或浏览器直接访问):

bash 复制代码
GET  http://localhost:3000/products
{
  "code": 0,
  "data": [
    { "id": 1, "name": "iPhone 15 Pro", "price": 7999.00, "description": "新款旗舰手机", "createdAt": "2026-09-12T21:08:47" },
    ...
  ],
  "message": "success"
}
json 复制代码
GET  http://localhost:3000/auth/users
{
  "code": 0,
  "data": [
    { "id": 1, "username": "testuser", "email": "test@demo.com", "createdAt": "...", "updatedAt": "..." }
    // ⚠️ password 字段已被剔除!与 NestJS 行为完全一致
  ],
  "message": "success"
}

前端零改动,直接把 npm run dev 跑起来就能对接 Java 后端 ------ 路由、端口、响应格式全部对齐。


9. 技术栈映射速查表

最后给一张一页纸速查表,下次从 Node.js 后端迁到 Spring Boot 时可以直接对照:

领域 NestJS / TypeScript Spring Boot / Java
构建工具 npm / pnpm Maven / Gradle
启动入口 NestFactory.create(AppModule) @SpringBootApplication + SpringApplication.run()
模块组织 @Module 装饰器 包扫描自动装配,不需要 Module 类
依赖注入 构造函数注入(@Injectable 构造函数注入(@Service / @Component
路由定义 @Controller() + @Get/@Post @RestController + @GetMapping/@PostMapping
DTO 校验 class-validator + 全局 ValidationPipe jakarta.validation + 方法参数 @Valid
数据库 ORM TypeORM(装饰器驱动) Spring Data JPA(注解驱动)
时间字段 @CreateDateColumn @PrePersist 生命周期回调
DTO 自动剔除字段 TypeScript Omit<User, 'password'> 手动拷贝到 SafeUser DTO
密码加密 bcrypt npm 包 BCryptPasswordEncoder(Spring Security)
JWT @nestjs/jwt jjwt(或自研 JwtUtil)
异常处理 ExceptionFilter @ControllerAdvice
统一响应 Interceptor 包装 Controller 返回泛型 ApiResponse<T>
CORS app.enableCors() SecurityConfigCorsConfigurationSource
环境配置 .env + dotenv application.yml / application.properties
数据库迁移 synchronize: true(开发) ddl-auto: update
运行端口 app.listen(port) server.port 配置

10. 结语

这次迁移最大的感悟是:NestJS 和 Spring Boot 看起来像两套东西,但骨子里是一套设计哲学

  • 都用装饰器/注解做声明式编程
  • 都有清晰的 Controller → Service → Repository 分层
  • 都强调依赖注入和面向接口编程
  • 都把"横切关注点"(校验、异常、CORS)从业务代码中抽离出来

如果你是前端出身想入坑后端,从 NestJS 开始再转 Spring Boot 是一条非常平滑的路径 ------ 你会发现自己之前写的"装饰器、DTO、Guard、Module"这些概念,在 Java 里全都找到了对应物,只是换了个注解名而已。

本文所有源码在 GitHub 上,见 server-java/ 目录。原 NestJS 代码在 server/ 目录。可以直接 git clone 下来本地跑,不依赖任何额外的 Maven 安装(项目自带 mvnw)。

相关推荐
Shinomiya1 小时前
我最近在学 Linux 进程控制:fork、退出码与进程等待
后端
企业数字化笔记1 小时前
固定资产Excel批量导入怎么防重复?资产编码、重复检查和错误回执
java·后端·excel
SamDeepThinking1 小时前
Java 17内存屏障:为什么需要,怎么用
java·后端·面试
azhou的代码园2 小时前
景区游船预约服务系统
人工智能·spring boot·后端
小灰灰搞电子2 小时前
Rust Attribute(属性标记)完整整理
开发语言·后端·rust
tedcloud1233 小时前
God‘s Eye View 怎么搭建?在云服务器上部署一个实时 3D 地球可视化平台
linux·服务器·开发语言·后端·rust
深入云栈3 小时前
Netty 4.2.x 源码深度解析 (十三):Epoll 传输 —— Linux 高性能 IO 的 Netty 实现
java·后端
Lyra_Infra3 小时前
从一段错误 JSON 说起:Policy、Role 与 IAM
后端·json·aigc
Charlie_Byte3 小时前
在用户目录安装多版本 JDK,并用环境变量切换
后端