Java 框架技术栈笔记
1. Spring 框架
1.1 Spring 框架概述
Spring 是一个轻量级的 Java 企业级应用框架,提供了全面的基础设施支持,用于开发 Java 应用程序。
Spring 核心特性:
- 依赖注入(DI)
- 控制反转(IoC)
- 面向切面编程(AOP)
- 事务管理
- 数据访问
- Web 开发
- 消息传递
- 测试支持
1.2 Spring Framework 6
Spring Framework 6 是 Spring 框架的最新版本,于 2022 年 11 月发布。
新特性:
- 基于 Java 17+ 构建
- 支持 Jakarta EE 10
- 响应式编程增强
- 原生镜像支持(GraalVM)
- 改进的 AOT 处理
- 简化的配置
核心模块:
spring-core:核心容器spring-beans:Bean 管理spring-context:应用上下文spring-aop:面向切面编程spring-tx:事务管理spring-jdbc:JDBC 支持spring-orm:ORM 支持spring-web:Web 支持spring-webmvc:MVC 框架
示例:
// 配置类
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public UserService userService(UserRepository userRepository) {
return new UserServiceImpl(userRepository);
}
}
// 组件类
@Component
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User getUserById(Long id) {
return userRepository.findById(id);
}
}
// 启动类
public class Application {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
User user = userService.getUserById(1L);
System.out.println(user);
}
}// 配置类
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public UserService userService(UserRepository userRepository) {
return new UserServiceImpl(userRepository);
}
}
// 组件类
@Component
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User getUserById(Long id) {
return userRepository.findById(id);
}
}
// 启动类
public class Application {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
User user = userService.getUserById(1L);
System.out.println(user);
}
}2. Spring Boot 3
2.1 Spring Boot 概述
Spring Boot 是 Spring 框架的扩展,用于快速创建基于 Spring 的应用程序,简化了 Spring 应用的配置和部署。
核心特性:
- 自动配置
- 起步依赖(Starter Dependencies)
- 嵌入式服务器
- 生产就绪特性
- 无代码生成和 XML 配置
2.2 Spring Boot 3
Spring Boot 3 是 Spring Boot 的最新版本,于 2022 年 11 月发布,基于 Spring Framework 6 构建。
新特性:
- 基于 Java 17+ 构建
- 支持 Jakarta EE 10
- 原生镜像支持(GraalVM)
- 改进的 AOT 处理
- 响应式编程增强
- 简化的配置
- 新的 actuator 端点
示例:
1. 创建 Spring Boot 项目
使用 Spring Initializr 创建项目,添加以下依赖:
- Spring Web
- Spring Data JPA
- H2 Database
- Spring Boot Actuator
2. 应用配置
application.properties:
# 应用名称
spring.application.name=myapp
# 服务器端口
server.port=8080
# H2 数据库配置
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
# H2 控制台
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Actuator 配置
management.endpoints.web.exposure.include=*# 应用名称
spring.application.name=myapp
# 服务器端口
server.port=8080
# H2 数据库配置
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
# H2 控制台
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Actuator 配置
management.endpoints.web.exposure.include=*3. 实体类
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name", nullable = false)
private String firstName;
@Column(name = "last_name", nullable = false)
private String lastName;
@Column(nullable = false, unique = true)
private String email;
// 构造函数、getter、setter
}@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name", nullable = false)
private String firstName;
@Column(name = "last_name", nullable = false)
private String lastName;
@Column(nullable = false, unique = true)
private String email;
// 构造函数、getter、setter
}4. 数据访问层
public interface UserRepository extends JpaRepository<User, Long> {
// 自定义查询方法
List<User> findByLastName(String lastName);
@Query("SELECT u FROM User u WHERE u.email LIKE %:email%")
List<User> findByEmailContaining(@Param("email") String email);
}public interface UserRepository extends JpaRepository<User, Long> {
// 自定义查询方法
List<User> findByLastName(String lastName);
@Query("SELECT u FROM User u WHERE u.email LIKE %:email%")
List<User> findByEmailContaining(@Param("email") String email);
}5. 服务层
@Service
@Transactional
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
@Override
public List<User> getAllUsers() {
return userRepository.findAll();
}
@Override
public User createUser(User user) {
return userRepository.save(user);
}
@Override
public User updateUser(Long id, User userDetails) {
User user = getUserById(id);
user.setFirstName(userDetails.getFirstName());
user.setLastName(userDetails.getLastName());
user.setEmail(userDetails.getEmail());
return userRepository.save(user);
}
@Override
public void deleteUser(Long id) {
User user = getUserById(id);
userRepository.delete(user);
}
}@Service
@Transactional
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
@Override
public List<User> getAllUsers() {
return userRepository.findAll();
}
@Override
public User createUser(User user) {
return userRepository.save(user);
}
@Override
public User updateUser(Long id, User userDetails) {
User user = getUserById(id);
user.setFirstName(userDetails.getFirstName());
user.setLastName(userDetails.getLastName());
user.setEmail(userDetails.getEmail());
return userRepository.save(user);
}
@Override
public void deleteUser(Long id) {
User user = getUserById(id);
userRepository.delete(user);
}
}6. 控制器层
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
User updatedUser = userService.updateUser(id, userDetails);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
User updatedUser = userService.updateUser(id, userDetails);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}7. 启动类
@SpringBootApplication
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}@SpringBootApplication
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}8. 测试
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
User user = new User();
user.setFirstName("John");
user.setLastName("Doe");
user.setEmail("john.doe@example.com");
userRepository.save(user);
}
@Test
void testGetUserById() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/api/users/1"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.firstName").value("John"))
.andExpect(MockMvcResultMatchers.jsonPath("$.lastName").value("Doe"));
}
@Test
void testCreateUser() throws Exception {
String userJson = "{\"firstName\":\"Jane\",\"lastName\":\"Smith\",\"email\":\"jane.smith@example.com\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(userJson))
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber())
.andExpect(MockMvcResultMatchers.jsonPath("$.email").value("jane.smith@example.com"));
}
}@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
User user = new User();
user.setFirstName("John");
user.setLastName("Doe");
user.setEmail("john.doe@example.com");
userRepository.save(user);
}
@Test
void testGetUserById() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/api/users/1"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.firstName").value("John"))
.andExpect(MockMvcResultMatchers.jsonPath("$.lastName").value("Doe"));
}
@Test
void testCreateUser() throws Exception {
String userJson = "{\"firstName\":\"Jane\",\"lastName\":\"Smith\",\"email\":\"jane.smith@example.com\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(userJson))
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber())
.andExpect(MockMvcResultMatchers.jsonPath("$.email").value("jane.smith@example.com"));
}
}2.3 SpringMVC 请求流程(面试高频)
DispatcherServlet 完整处理链路:
用户请求 → DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → ViewResolver → 响应用户请求 → DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → ViewResolver → 响应┌────────────────────────────────────────────────────────────────┐
│ DispatcherServlet │
│ ┌─────────────────┐ │
│ │ 1. HandlerMapping│ → 找到处理请求的 Controller 方法 │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 2. HandlerAdapter│ → 调用 Controller 方法(参数绑定、类型转换) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 3. Controller │ → 执行业务逻辑,返回 ModelAndView/数据 │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 4. ViewResolver │ → 解析视图名 → 渲染 HTML │
│ └────────┬────────┘ (前后端分离项目中,返回 JSON 跳过此步) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 5. 拦截器后处理 │ → afterCompletion 等 │
│ └─────────────────┘ │
└────────────────────────────────────────────────────────────────┘┌────────────────────────────────────────────────────────────────┐
│ DispatcherServlet │
│ ┌─────────────────┐ │
│ │ 1. HandlerMapping│ → 找到处理请求的 Controller 方法 │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 2. HandlerAdapter│ → 调用 Controller 方法(参数绑定、类型转换) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 3. Controller │ → 执行业务逻辑,返回 ModelAndView/数据 │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 4. ViewResolver │ → 解析视图名 → 渲染 HTML │
│ └────────┬────────┘ (前后端分离项目中,返回 JSON 跳过此步) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 5. 拦截器后处理 │ → afterCompletion 等 │
│ └─────────────────┘ │
└────────────────────────────────────────────────────────────────┘关键组件:
- HandlerMapping:根据请求 URL 找到对应的 Handler(Controller 方法)
- HandlerAdapter:执行 Handler,处理参数绑定、类型转换、数据校验
- HandlerInterceptor:拦截器,前置/后置处理(登录校验、日志等)
- ViewResolver:解析视图名到具体视图(前后端分离项目中通常不涉及)
- HttpMessageConverter:将 Controller 返回值转换为 JSON/XML(
@ResponseBody)
面试回答模板:
请求先到达 DispatcherServlet,它通过 HandlerMapping 找到对应的 Controller 方法,HandlerAdapter 负责调用并处理参数绑定,执行完后如果有 @ResponseBody 则通过 HttpMessageConverter 把返回值转成 JSON 写回客户端。
2.4 Bean 完整生命周期(面试高频)
┌─────────────────────────────────────────────────────────────────┐
│ Spring Bean 生命周期 │
│ │
│ 1. 实例化(Instantiation) │
│ └→ 通过构造方法或工厂方法创建 Bean 实例 │
│ │
│ 2. 属性填充(Populate Properties) │
│ └→ 注入 @Autowired 依赖、@Value 值 │
│ │
│ 3. Aware 回调 │
│ └→ BeanNameAware.setBeanName() │
│ └→ BeanFactoryAware.setBeanFactory() │
│ └→ ApplicationContextAware.setApplicationContext() │
│ │
│ 4. BeanPostProcessor 前置处理 │
│ └→ postProcessBeforeInitialization() │
│ │
│ 5. 初始化(Initialization) │
│ └→ @PostConstruct 方法 │
│ └→ InitializingBean.afterPropertiesSet() │
│ └→ init-method(XML 配置) │
│ │
│ 6. BeanPostProcessor 后置处理 │
│ └→ postProcessAfterInitialization() ← AOP 代理在此生成! │
│ │
│ 7. Bean 就绪(Ready) │
│ └→ 可以被使用 │
│ │
│ 8. 销毁(Destruction) │
│ └→ @PreDestroy 方法 │
│ └→ DisposableBean.destroy() │
│ └→ destroy-method(XML 配置) │
└─────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────┐
│ Spring Bean 生命周期 │
│ │
│ 1. 实例化(Instantiation) │
│ └→ 通过构造方法或工厂方法创建 Bean 实例 │
│ │
│ 2. 属性填充(Populate Properties) │
│ └→ 注入 @Autowired 依赖、@Value 值 │
│ │
│ 3. Aware 回调 │
│ └→ BeanNameAware.setBeanName() │
│ └→ BeanFactoryAware.setBeanFactory() │
│ └→ ApplicationContextAware.setApplicationContext() │
│ │
│ 4. BeanPostProcessor 前置处理 │
│ └→ postProcessBeforeInitialization() │
│ │
│ 5. 初始化(Initialization) │
│ └→ @PostConstruct 方法 │
│ └→ InitializingBean.afterPropertiesSet() │
│ └→ init-method(XML 配置) │
│ │
│ 6. BeanPostProcessor 后置处理 │
│ └→ postProcessAfterInitialization() ← AOP 代理在此生成! │
│ │
│ 7. Bean 就绪(Ready) │
│ └→ 可以被使用 │
│ │
│ 8. 销毁(Destruction) │
│ └→ @PreDestroy 方法 │
│ └→ DisposableBean.destroy() │
│ └→ destroy-method(XML 配置) │
└─────────────────────────────────────────────────────────────────┘关键面试点:
- AOP 代理对象是在
postProcessAfterInitialization阶段通过BeanPostProcessor生成的 - 先执行
@PostConstruct,再执行InitializingBean.afterPropertiesSet() - 依赖注入(@Autowired)发生在属性填充阶段,在初始化之前
2.5 SpringBoot 自动配置原理(面试高频)
核心注解链:
@SpringBootApplication
└─ @EnableAutoConfiguration
└─ @Import(AutoConfigurationImportSelector.class)
└─ 读取 spring-boot-autoconfigure.jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)
或 spring.factories(Spring Boot 2.x)
└─ 加载所有自动配置类@SpringBootApplication
└─ @EnableAutoConfiguration
└─ @Import(AutoConfigurationImportSelector.class)
└─ 读取 spring-boot-autoconfigure.jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)
或 spring.factories(Spring Boot 2.x)
└─ 加载所有自动配置类条件注解过滤: 加载后会根据 @ConditionalOnXxx 决定是否生效:
| 条件注解 | 生效条件 |
|---|---|
@ConditionalOnClass | classpath 中存在指定类 |
@ConditionalOnMissingBean | 容器中没有指定 Bean |
@ConditionalOnProperty | 配置文件中有指定属性 |
@ConditionalOnBean | 容器中存在指定 Bean |
@ConditionalOnMissingClass | classpath 中不存在指定类 |
@ConditionalOnWebApplication | 当前是 Web 应用 |
以 DataSource 自动配置为例:
@AutoConfiguration
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 用户自己配了就不创建
public DataSource dataSource(DataSourceProperties properties) {
// 创建 HikariCP 连接池
return properties.initializeDataSourceBuilder().build();
}
}@AutoConfiguration
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 用户自己配了就不创建
public DataSource dataSource(DataSourceProperties properties) {
// 创建 HikariCP 连接池
return properties.initializeDataSourceBuilder().build();
}
}面试回答模板:
@SpringBootApplication 包含 @EnableAutoConfiguration,它通过 AutoConfigurationImportSelector 读取 spring-boot-autoconfigure 包中的自动配置类列表,然后根据 @ConditionalOnXxx 条件注解判断哪些配置生效。用户自定义的 Bean 优先于自动配置(@ConditionalOnMissingBean)。
3. Spring Security 6
3.1 Spring Security 概述
Spring Security 是 Spring 框架的安全模块,用于提供身份验证、授权和其他安全功能。
3.2 Spring Security 6
Spring Security 6 是 Spring Security 的最新版本,于 2022 年 11 月发布。
新特性:
- 基于 Java 17+ 构建
- 支持 Jakarta EE 10
- 简化的配置
- 改进的 OAuth 2.0 支持
- 原生镜像支持
- 改进的测试支持
示例:
1. 添加依赖
在 pom.xml 中添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>2. 安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 开发环境禁用 CSRF
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/h2-console/**").permitAll() // 允许访问 H2 控制台
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
)
);
// 允许 H2 控制台使用 iframe
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()));
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 配置 JWT 解码器
return NimbusJwtDecoder.withPublicKey(rsaPublicKey()).build();
}
@Bean
public RSAPublicKey rsaPublicKey() {
// 加载公钥
// 这里简化处理,实际应从配置或密钥库加载
return (RSAPublicKey) keyPair().getPublic();
}
@Bean
public KeyPair keyPair() {
// 生成密钥对(实际应用中应使用已有的密钥)
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}
@Bean
public UserDetailsService userDetailsService() {
// 内存用户存储
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin123"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 开发环境禁用 CSRF
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/h2-console/**").permitAll() // 允许访问 H2 控制台
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
)
);
// 允许 H2 控制台使用 iframe
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()));
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 配置 JWT 解码器
return NimbusJwtDecoder.withPublicKey(rsaPublicKey()).build();
}
@Bean
public RSAPublicKey rsaPublicKey() {
// 加载公钥
// 这里简化处理,实际应从配置或密钥库加载
return (RSAPublicKey) keyPair().getPublic();
}
@Bean
public KeyPair keyPair() {
// 生成密钥对(实际应用中应使用已有的密钥)
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}
@Bean
public UserDetailsService userDetailsService() {
// 内存用户存储
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin123"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}3. 控制器
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/public/hello")
public String publicHello() {
return "Hello from public endpoint!";
}
@GetMapping("/user/hello")
public String userHello() {
return "Hello from user endpoint!";
}
@GetMapping("/admin/hello")
public String adminHello() {
return "Hello from admin endpoint!";
}
}@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/public/hello")
public String publicHello() {
return "Hello from public endpoint!";
}
@GetMapping("/user/hello")
public String userHello() {
return "Hello from user endpoint!";
}
@GetMapping("/admin/hello")
public String adminHello() {
return "Hello from admin endpoint!";
}
}4. Jakarta EE 10
4.1 Jakarta EE 概述
Jakarta EE 是 Java EE 的继任者,是一个用于构建企业级应用的开源平台。
4.2 Jakarta EE 10
Jakarta EE 10 是 Jakarta EE 的最新版本,于 2022 年 9 月发布。
核心规范:
- Jakarta Servlet 6.0
- Jakarta JSP 3.1
- Jakarta EL 5.0
- Jakarta CDI 4.0
- Jakarta EJB 4.0
- Jakarta JPA 3.1
- Jakarta RESTful Web Services 3.1
- Jakarta Security 3.0
- Jakarta Bean Validation 3.0
- Jakarta Persistence 3.1
- Jakarta WebSocket 2.1
示例:
1. 创建 Jakarta EE 项目
使用 Maven 创建 Jakarta EE 10 项目:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jakartaee-example</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<jakartaee.version>10.0.0</jakartaee.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>${jakartaee.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jakartaee-example</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<jakartaee.version>10.0.0</jakartaee.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>${jakartaee.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>2. REST 资源
@Path("/api/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
@Inject
private UserService userService;
@GET
@Path("/{id}")
public Response getUserById(@PathParam("id") Long id) {
User user = userService.getUserById(id);
return Response.ok(user).build();
}
@GET
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@POST
public Response createUser(User user) {
User createdUser = userService.createUser(user);
return Response.status(Response.Status.CREATED)
.header("Location", "/api/users/" + createdUser.getId())
.entity(createdUser)
.build();
}
@PUT
@Path("/{id}")
public Response updateUser(@PathParam("id") Long id, User user) {
User updatedUser = userService.updateUser(id, user);
return Response.ok(updatedUser).build();
}
@DELETE
@Path("/{id}")
public Response deleteUser(@PathParam("id") Long id) {
userService.deleteUser(id);
return Response.noContent().build();
}
}@Path("/api/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
@Inject
private UserService userService;
@GET
@Path("/{id}")
public Response getUserById(@PathParam("id") Long id) {
User user = userService.getUserById(id);
return Response.ok(user).build();
}
@GET
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@POST
public Response createUser(User user) {
User createdUser = userService.createUser(user);
return Response.status(Response.Status.CREATED)
.header("Location", "/api/users/" + createdUser.getId())
.entity(createdUser)
.build();
}
@PUT
@Path("/{id}")
public Response updateUser(@PathParam("id") Long id, User user) {
User updatedUser = userService.updateUser(id, user);
return Response.ok(updatedUser).build();
}
@DELETE
@Path("/{id}")
public Response deleteUser(@PathParam("id") Long id) {
userService.deleteUser(id);
return Response.noContent().build();
}
}3. CDI 服务
@ApplicationScoped
public class UserServiceImpl implements UserService {
@PersistenceContext
private EntityManager entityManager;
@Override
public User getUserById(Long id) {
User user = entityManager.find(User.class, id);
if (user == null) {
throw new WebApplicationException("User not found with id: " + id, Response.Status.NOT_FOUND);
}
return user;
}
@Override
public List<User> getAllUsers() {
TypedQuery<User> query = entityManager.createQuery("SELECT u FROM User u", User.class);
return query.getResultList();
}
@Override
@Transactional
public User createUser(User user) {
entityManager.persist(user);
return user;
}
@Override
@Transactional
public User updateUser(Long id, User user) {
User existingUser = getUserById(id);
existingUser.setFirstName(user.getFirstName());
existingUser.setLastName(user.getLastName());
existingUser.setEmail(user.getEmail());
return existingUser;
}
@Override
@Transactional
public void deleteUser(Long id) {
User user = getUserById(id);
entityManager.remove(user);
}
}@ApplicationScoped
public class UserServiceImpl implements UserService {
@PersistenceContext
private EntityManager entityManager;
@Override
public User getUserById(Long id) {
User user = entityManager.find(User.class, id);
if (user == null) {
throw new WebApplicationException("User not found with id: " + id, Response.Status.NOT_FOUND);
}
return user;
}
@Override
public List<User> getAllUsers() {
TypedQuery<User> query = entityManager.createQuery("SELECT u FROM User u", User.class);
return query.getResultList();
}
@Override
@Transactional
public User createUser(User user) {
entityManager.persist(user);
return user;
}
@Override
@Transactional
public User updateUser(Long id, User user) {
User existingUser = getUserById(id);
existingUser.setFirstName(user.getFirstName());
existingUser.setLastName(user.getLastName());
existingUser.setEmail(user.getEmail());
return existingUser;
}
@Override
@Transactional
public void deleteUser(Long id) {
User user = getUserById(id);
entityManager.remove(user);
}
}4. 实体类
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name", nullable = false)
private String firstName;
@Column(name = "last_name", nullable = false)
private String lastName;
@Column(nullable = false, unique = true)
private String email;
// 构造函数、getter、setter
}@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name", nullable = false)
private String firstName;
@Column(name = "last_name", nullable = false)
private String lastName;
@Column(nullable = false, unique = true)
private String email;
// 构造函数、getter、setter
}5. Spring Data
5.1 Spring Data 概述
Spring Data 是 Spring 框架的一个子项目,用于简化数据访问层的开发。
核心模块:
- Spring Data JPA
- Spring Data MongoDB
- Spring Data Redis
- Spring Data Elasticsearch
- Spring Data Cassandra
- Spring Data REST
5.2 Spring Data JPA
Spring Data JPA 简化了 JPA 的使用,提供了自动生成查询方法的功能。
示例:
1. 自定义查询方法
public interface UserRepository extends JpaRepository<User, Long> {
// 自动生成查询:SELECT u FROM User u WHERE u.lastName = ?1
List<User> findByLastName(String lastName);
// 自动生成查询:SELECT u FROM User u WHERE u.firstName = ?1 AND u.lastName = ?2
List<User> findByFirstNameAndLastName(String firstName, String lastName);
// 自动生成查询:SELECT u FROM User u WHERE u.email LIKE %?1%
List<User> findByEmailContaining(String email);
// 自动生成查询:SELECT u FROM User u WHERE u.age > ?1
List<User> findByAgeGreaterThan(int age);
// 排序
List<User> findByLastNameOrderByFirstNameAsc(String lastName);
// 分页
Page<User> findByLastName(String lastName, Pageable pageable);
// 限制结果数量
List<User> findTop5ByOrderByAgeDesc();
// 使用 @Query 注解
@Query("SELECT u FROM User u WHERE u.email LIKE %:email%")
List<User> findByEmailLike(@Param("email") String email);
// 使用原生 SQL
@Query(value = "SELECT * FROM users WHERE email LIKE %:email%", nativeQuery = true)
List<User> findByEmailNative(@Param("email") String email);
// 更新操作
@Modifying
@Query("UPDATE User u SET u.email = :email WHERE u.id = :id")
int updateEmail(@Param("id") Long id, @Param("email") String email);
}public interface UserRepository extends JpaRepository<User, Long> {
// 自动生成查询:SELECT u FROM User u WHERE u.lastName = ?1
List<User> findByLastName(String lastName);
// 自动生成查询:SELECT u FROM User u WHERE u.firstName = ?1 AND u.lastName = ?2
List<User> findByFirstNameAndLastName(String firstName, String lastName);
// 自动生成查询:SELECT u FROM User u WHERE u.email LIKE %?1%
List<User> findByEmailContaining(String email);
// 自动生成查询:SELECT u FROM User u WHERE u.age > ?1
List<User> findByAgeGreaterThan(int age);
// 排序
List<User> findByLastNameOrderByFirstNameAsc(String lastName);
// 分页
Page<User> findByLastName(String lastName, Pageable pageable);
// 限制结果数量
List<User> findTop5ByOrderByAgeDesc();
// 使用 @Query 注解
@Query("SELECT u FROM User u WHERE u.email LIKE %:email%")
List<User> findByEmailLike(@Param("email") String email);
// 使用原生 SQL
@Query(value = "SELECT * FROM users WHERE email LIKE %:email%", nativeQuery = true)
List<User> findByEmailNative(@Param("email") String email);
// 更新操作
@Modifying
@Query("UPDATE User u SET u.email = :email WHERE u.id = :id")
int updateEmail(@Param("id") Long id, @Param("email") String email);
}2. 分页和排序
// 分页和排序
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<User> users = userRepository.findAll(pageable);
// 获取分页信息
int totalPages = users.getTotalPages();
long totalElements = users.getTotalElements();
List<User> content = users.getContent();
boolean hasNext = users.hasNext();
boolean hasPrevious = users.hasPrevious();// 分页和排序
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<User> users = userRepository.findAll(pageable);
// 获取分页信息
int totalPages = users.getTotalPages();
long totalElements = users.getTotalElements();
List<User> content = users.getContent();
boolean hasNext = users.hasNext();
boolean hasPrevious = users.hasPrevious();3. 条件查询
// 使用 Example 进行条件查询
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnoreCase()
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING);
User example = new User();
example.setLastName("Smith");
Example<User> userExample = Example.of(example, matcher);
List<User> users = userRepository.findAll(userExample);// 使用 Example 进行条件查询
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnoreCase()
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING);
User example = new User();
example.setLastName("Smith");
Example<User> userExample = Example.of(example, matcher);
List<User> users = userRepository.findAll(userExample);6. 响应式编程
6.1 什么是响应式编程?
响应式编程是一种面向数据流和变化传播的编程范式,用于处理异步数据流。
6.2 Spring WebFlux
Spring WebFlux 是 Spring 框架的响应式 Web 框架,基于 Reactor 库构建。
示例:
1. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
</dependency><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
</dependency>2. 配置
application.properties:
spring.r2dbc.url=r2dbc:h2:mem:///testdb?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.r2dbc.username=sa
spring.r2dbc.password=
spring.sql.init.mode=alwaysspring.r2dbc.url=r2dbc:h2:mem:///testdb?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.r2dbc.username=sa
spring.r2dbc.password=
spring.sql.init.mode=always3. 实体类
@Table("users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
// 构造函数、getter、setter
}@Table("users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
// 构造函数、getter、setter
}4. 仓库接口
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
Flux<User> findByLastName(String lastName);
}public interface UserRepository extends ReactiveCrudRepository<User, Long> {
Flux<User> findByLastName(String lastName);
}5. 控制器
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable Long id) {
return userRepository.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@GetMapping
public Flux<User> getAllUsers() {
return userRepository.findAll();
}
@PostMapping
public Mono<ResponseEntity<User>> createUser(@RequestBody User user) {
return userRepository.save(user)
.map(u -> ResponseEntity.status(HttpStatus.CREATED).body(u));
}
@PutMapping("/{id}")
public Mono<ResponseEntity<User>> updateUser(@PathVariable Long id, @RequestBody User user) {
return userRepository.findById(id)
.flatMap(existingUser -> {
existingUser.setFirstName(user.getFirstName());
existingUser.setLastName(user.getLastName());
existingUser.setEmail(user.getEmail());
return userRepository.save(existingUser);
})
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteUser(@PathVariable Long id) {
return userRepository.findById(id)
.flatMap(existingUser -> userRepository.delete(existingUser)
.then(Mono.just(ResponseEntity.noContent().build())))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable Long id) {
return userRepository.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@GetMapping
public Flux<User> getAllUsers() {
return userRepository.findAll();
}
@PostMapping
public Mono<ResponseEntity<User>> createUser(@RequestBody User user) {
return userRepository.save(user)
.map(u -> ResponseEntity.status(HttpStatus.CREATED).body(u));
}
@PutMapping("/{id}")
public Mono<ResponseEntity<User>> updateUser(@PathVariable Long id, @RequestBody User user) {
return userRepository.findById(id)
.flatMap(existingUser -> {
existingUser.setFirstName(user.getFirstName());
existingUser.setLastName(user.getLastName());
existingUser.setEmail(user.getEmail());
return userRepository.save(existingUser);
})
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteUser(@PathVariable Long id) {
return userRepository.findById(id)
.flatMap(existingUser -> userRepository.delete(existingUser)
.then(Mono.just(ResponseEntity.noContent().build())))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}7. 工程实践专题(参数校验 / 鉴权 / 缓存 / 日志 / SSE)
7.1 参数校验 @Valid / @Validated(面试高频)
Spring Boot 集成 Bean Validation(JSR-380),配合 Hibernate Validator 实现参数校验。
基础用法:
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 18, message = "年龄不能小于18")
@Max(value = 120, message = "年龄不能大于120")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
@RestController
public class UserController {
@PostMapping("/users")
public Result createUser(@Valid @RequestBody UserDTO dto) {
// @Valid 校验失败会抛出 MethodArgumentNotValidException
return Result.success(userService.create(dto));
}
}@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 18, message = "年龄不能小于18")
@Max(value = 120, message = "年龄不能大于120")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
@RestController
public class UserController {
@PostMapping("/users")
public Result createUser(@Valid @RequestBody UserDTO dto) {
// @Valid 校验失败会抛出 MethodArgumentNotValidException
return Result.success(userService.create(dto));
}
}分组校验:
// 定义分组接口
public interface Create {}
public interface Update {}
@Data
public class UserDTO {
@NotNull(groups = Update.class) // 只在更新时校验
private Long id;
@NotBlank(groups = {Create.class, Update.class})
private String username;
}
// 使用 @Validated 指定分组
@PostMapping("/users")
public Result create(@Validated(Create.class) @RequestBody UserDTO dto) { }
@PutMapping("/users")
public Result update(@Validated(Update.class) @RequestBody UserDTO dto) { }// 定义分组接口
public interface Create {}
public interface Update {}
@Data
public class UserDTO {
@NotNull(groups = Update.class) // 只在更新时校验
private Long id;
@NotBlank(groups = {Create.class, Update.class})
private String username;
}
// 使用 @Validated 指定分组
@PostMapping("/users")
public Result create(@Validated(Create.class) @RequestBody UserDTO dto) { }
@PutMapping("/users")
public Result update(@Validated(Update.class) @RequestBody UserDTO dto) { }全局异常处理:
@RestControllerAdvice
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.error(msg);
}
}@RestControllerAdvice
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.error(msg);
}
}@Valid 与 @Validated 区别:
| 对比 | @Valid | @Validated |
|---|---|---|
| 来源 | JSR-380(javax/jakarta.validation) | Spring 框架 |
| 分组校验 | 不支持 | 支持 |
| 嵌套校验 | 支持 | 不支持 |
| 使用场景 | Controller 层 | Service 层方法参数 |
常用校验注解:
| 注解 | 说明 |
|---|---|
@NotNull | 不为 null |
@NotBlank | 不为 null 且不为空字符串 |
@NotEmpty | 不为 null 且 size > 0(集合/字符串) |
@Size(min, max) | 字符串/集合/数组长度范围 |
@Min / @Max | 数值最小值/最大值 |
@Email | 邮箱格式 |
@Pattern | 正则匹配 |
7.2 Cookie、Session、JWT 对比(面试高频)
| 维度 | Cookie | Session | JWT |
|---|---|---|---|
| 存储位置 | 浏览器 | 服务端(内存/Redis) | 客户端 |
| 安全性 | 可被篡改,需配合 HttpOnly | 相对安全(服务端存) | 无状态,签名防篡改 |
| 跨域 | 受同源策略限制 | 可配合 Cookie | 天然支持跨域 |
| 扩展性 | 无状态 | 需要 Session 共享(Redis) | 无状态,天然分布式 |
| 性能 | 不占用服务端 | 服务端占用内存 | 不占用服务端 |
| 注销 | 清除 Cookie | 删除 Session | 无法主动失效(需配合黑名单) |
| 适用场景 | 传统 Web 应用 | 单体应用 | 前后端分离、微服务、移动端 |
JWT 结构: Header.Payload.Signature
// 生成 JWT
String token = Jwts.builder()
.setSubject(userId.toString())
.claim("role", "admin")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24h
.signWith(secretKey)
.compact();
// 验证 JWT
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();// 生成 JWT
String token = Jwts.builder()
.setSubject(userId.toString())
.claim("role", "admin")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24h
.signWith(secretKey)
.compact();
// 验证 JWT
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();JWT 缺点:
- token 签发后无法主动注销(需配合 Redis 黑名单)
- payload 只 Base64 编码,不加密(不要放敏感信息)
- token 越长,每次请求的带宽开销越大
面试回答模板:
Cookie 存在浏览器,Session 存在服务端,JWT 是无状态的 Token。前后端分离项目推荐 JWT(无状态、跨域方便),但要注意 JWT 无法主动注销,需要配合 Redis 黑名单或短过期时间 + refresh token 方案。
7.3 Spring Cache 抽象(@Cacheable / @CacheEvict)
Spring 提供声明式缓存抽象,支持 Redis、Caffeine、EhCache 等多种缓存后端。
@Service
public class UserService {
// 查询缓存:缓存中有就直接返回,没有就执行方法并存入缓存
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getById(Long id) {
return userMapper.selectById(id);
}
// 更新后清除缓存
@CacheEvict(value = "user", key = "#user.id")
public User update(User user) {
userMapper.updateById(user);
return user;
}
// 每次方法执行前清除缓存(与 @Cacheable 区别:始终执行方法)
@CachePut(value = "user", key = "#result.id")
public User create(User user) {
userMapper.insert(user);
return user;
}
// 清除所有 user 缓存
@CacheEvict(value = "user", allEntries = true)
public void delete(Long id) {
userMapper.deleteById(id);
}
}@Service
public class UserService {
// 查询缓存:缓存中有就直接返回,没有就执行方法并存入缓存
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getById(Long id) {
return userMapper.selectById(id);
}
// 更新后清除缓存
@CacheEvict(value = "user", key = "#user.id")
public User update(User user) {
userMapper.updateById(user);
return user;
}
// 每次方法执行前清除缓存(与 @Cacheable 区别:始终执行方法)
@CachePut(value = "user", key = "#result.id")
public User create(User user) {
userMapper.insert(user);
return user;
}
// 清除所有 user 缓存
@CacheEvict(value = "user", allEntries = true)
public void delete(Long id) {
userMapper.deleteById(id);
}
}常用注解:
| 注解 | 说明 |
|---|---|
@Cacheable | 先查缓存,有则返回,无则执行方法并缓存结果 |
@CacheEvict | 清除缓存 |
@CachePut | 始终执行方法,结果存入缓存 |
@Caching | 组合多个缓存操作 |
常用 SpEL 表达式:
| 表达式 | 说明 |
|---|---|
#id | 方法参数 id |
#user.id | 参数 user 对象的 id 属性 |
#result | 方法返回值 |
#root.methodName | 方法名 |
配置 Redis 作为缓存后端:
spring:
cache:
type: redis
redis:
time-to-live: 3600000 # 默认过期时间(毫秒)
cache-null-values: false # 是否缓存 null 值(防止缓存穿透建议开启)spring:
cache:
type: redis
redis:
time-to-live: 3600000 # 默认过期时间(毫秒)
cache-null-values: false # 是否缓存 null 值(防止缓存穿透建议开启)面试回答模板:
@Cacheable 先查缓存,缓存命中直接返回;不命中执行方法,结果自动缓存。@CacheEvict 用于更新/删除时清除缓存。配合 Redis 作为缓存后端,还可以设置过期时间兜底,防止缓存穿透可以配置 cache-null-values=true。
7.4 日志体系 SLF4J + Logback(面试高频)
SLF4J 是门面(Facade),Logback 是默认实现。Spring Boot 内置了 spring-boot-starter-logging。
日志级别(从低到高): TRACE < DEBUG < INFO < WARN < ERROR
配置示例(application.yml):
logging:
level:
root: INFO
com.example: DEBUG # 自己的包打 DEBUG
com.example.mapper: WARN # MyBatis Mapper 只打 WARN
file:
name: logs/app.log # 日志文件路径
logback:
rollingpolicy:
max-file-size: 10MB # 单个文件最大 10MB
max-history: 30 # 保留 30 天logging:
level:
root: INFO
com.example: DEBUG # 自己的包打 DEBUG
com.example.mapper: WARN # MyBatis Mapper 只打 WARN
file:
name: logs/app.log # 日志文件路径
logback:
rollingpolicy:
max-file-size: 10MB # 单个文件最大 10MB
max-history: 30 # 保留 30 天代码中使用(推荐用 Lombok @Slf4j):
@Slf4j
@Service
public class UserService {
public User getById(Long id) {
log.info("查询用户: id={}", id); // 占位符方式,避免字符串拼接开销
User user = userMapper.selectById(id);
if (user == null) {
log.warn("用户不存在: id={}", id);
}
return user;
}
}@Slf4j
@Service
public class UserService {
public User getById(Long id) {
log.info("查询用户: id={}", id); // 占位符方式,避免字符串拼接开销
User user = userMapper.selectById(id);
if (user == null) {
log.warn("用户不存在: id={}", id);
}
return user;
}
}为什么不直接拼接字符串?
// 反例:即使 debug 不输出,也会执行字符串拼接
log.debug("查询用户: " + id + ", 姓名: " + name);
// 正例:占位符方式,debug 不输出时不会拼接
log.debug("查询用户: id={}, 姓名={}", id, name);// 反例:即使 debug 不输出,也会执行字符串拼接
log.debug("查询用户: " + id + ", 姓名: " + name);
// 正例:占位符方式,debug 不输出时不会拼接
log.debug("查询用户: id={}, 姓名={}", id, name);MDC 链路追踪(StackTrace 排查利器):
// 拦截器中设置 traceId
MDC.put("traceId", UUID.randomUUID().toString().replace("-", ""));
// logback-spring.xml 中配置 pattern 包含 traceId 和请求耗时
// %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - [%X{traceId}] - %msg%n// 拦截器中设置 traceId
MDC.put("traceId", UUID.randomUUID().toString().replace("-", ""));
// logback-spring.xml 中配置 pattern 包含 traceId 和请求耗时
// %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - [%X{traceId}] - %msg%n面试回答模板:
SLF4J 是门面,Logback 是默认实现。用占位符
{}代替字符串拼接,避免不必要的性能开销。排查问题时通过 MDC 设置 traceId 贯穿整个请求链路,在日志中直接搜索 traceId 就能看到完整调用链。
7.5 SSE 原理(Server-Sent Events)
SSE 是 HTTP 长连接技术,服务端向客户端单向推送数据。你的心理健康 AI 助手使用 SSE 实现流式对话。
核心原理:
客户端 服务端
│ │
│ GET /api/chat/stream │
│ Accept: text/event-stream │
│ ──────────────────────────>│
│ │
│ Content-Type: text/event-stream
│ Cache-Control: no-cache
│ Connection: keep-alive
│ <────────────────────────── │
│ │
│ data: {"chunk":"你"} │
│ <────────────────────────── │
│ │
│ data: {"chunk":"好"} │
│ <────────────────────────── │
│ │
│ data: [DONE] │
│ <────────────────────────── │客户端 服务端
│ │
│ GET /api/chat/stream │
│ Accept: text/event-stream │
│ ──────────────────────────>│
│ │
│ Content-Type: text/event-stream
│ Cache-Control: no-cache
│ Connection: keep-alive
│ <────────────────────────── │
│ │
│ data: {"chunk":"你"} │
│ <────────────────────────── │
│ │
│ data: {"chunk":"好"} │
│ <────────────────────────── │
│ │
│ data: [DONE] │
│ <────────────────────────── │SSE 消息格式:
data: 消息内容
id: 消息ID(可选,断线重连时从 Last-Event-ID 续传)
event: 事件类型(可选,默认 message)
retry: 重连间隔(毫秒,可选)
data: {"chunk": "你好"}
data: {"chunk": ",我是AI助手"}
data: [DONE]data: 消息内容
id: 消息ID(可选,断线重连时从 Last-Event-ID 续传)
event: 事件类型(可选,默认 message)
retry: 重连间隔(毫秒,可选)
data: {"chunk": "你好"}
data: {"chunk": ",我是AI助手"}
data: [DONE]Spring Boot 实现(SseEmitter):
@RestController
public class ChatController {
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chat(@RequestParam String message) {
SseEmitter emitter = new SseEmitter(60000L); // 60 秒超时
// 异步执行,避免阻塞主线程
CompletableFuture.runAsync(() -> {
try {
for (String chunk : aiService.chat(message)) {
emitter.send(SseEmitter.event().data(chunk));
}
emitter.send(SseEmitter.event().data("[DONE]"));
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}@RestController
public class ChatController {
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chat(@RequestParam String message) {
SseEmitter emitter = new SseEmitter(60000L); // 60 秒超时
// 异步执行,避免阻塞主线程
CompletableFuture.runAsync(() -> {
try {
for (String chunk : aiService.chat(message)) {
emitter.send(SseEmitter.event().data(chunk));
}
emitter.send(SseEmitter.event().data("[DONE]"));
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}Nginx 配置要点(SSE 必须配置):
location /chat/stream {
proxy_pass http://backend;
proxy_buffering off; # 关闭缓冲,否则数据会积压
proxy_cache off; # 关闭缓存
proxy_read_timeout 3600s; # 延长超时,防止长连接被断开
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
}location /chat/stream {
proxy_pass http://backend;
proxy_buffering off; # 关闭缓冲,否则数据会积压
proxy_cache off; # 关闭缓存
proxy_read_timeout 3600s; # 延长超时,防止长连接被断开
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
}SSE vs WebSocket 对比:
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务端 → 客户端) | 双向 |
| 协议 | HTTP | 独立协议 ws:// |
| 断线重连 | 浏览器内置 | 需手动实现 |
| 复杂度 | 低 | 高 |
| 适用场景 | 流式推送、通知、AI 对话 | 即时通讯、游戏 |
面试回答模板:
SSE 基于 HTTP 长连接,客户端发起 GET 请求后服务端持续推送数据。我用 SseEmitter 实现流式 AI 对话,重点注意 Nginx 要关闭 proxy_buffering 否则数据会积压,延长 proxy_read_timeout 防止长连接超时断开。
8. Spring AI(大模型接入)
8.1 为什么后端要自己接模型
前端直连模型有三个硬伤:API Key 泄露、无法管控成本、业务数据出不了内网。所以标准做法是后端做一层「模型网关」:
浏览器 ──SSE──> 自己的服务 ──HTTP──> 模型服务(OpenAI / 通义 / 硅基流动 / 本地 vLLM)
│
├── 鉴权 & 配额(谁能用、用多少)
├── Prompt 组装(系统提示词、RAG 片段、历史消息)
├── 工具调用(查库、查订单、调内部 API)
├── 流式转发(SSE 逐 token 推给前端)
└── 兜底(超时、限流、内容安全、降级话术)浏览器 ──SSE──> 自己的服务 ──HTTP──> 模型服务(OpenAI / 通义 / 硅基流动 / 本地 vLLM)
│
├── 鉴权 & 配额(谁能用、用多少)
├── Prompt 组装(系统提示词、RAG 片段、历史消息)
├── 工具调用(查库、查订单、调内部 API)
├── 流式转发(SSE 逐 token 推给前端)
└── 兜底(超时、限流、内容安全、降级话术)8.2 依赖与配置
<!-- 1.0 之后的命名;1.0 之前是 spring-ai-openai-spring-boot-starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency><!-- 1.0 之后的命名;1.0 之前是 spring-ai-openai-spring-boot-starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>spring:
ai:
openai:
base-url: https://api.siliconflow.cn # 换成任意 OpenAI 兼容网关
api-key: ${AI_API_KEY} # 绝不写进代码和仓库
chat:
options:
model: Qwen/Qwen2.5-72B-Instruct
temperature: 0.7
max-tokens: 2048spring:
ai:
openai:
base-url: https://api.siliconflow.cn # 换成任意 OpenAI 兼容网关
api-key: ${AI_API_KEY} # 绝不写进代码和仓库
chat:
options:
model: Qwen/Qwen2.5-72B-Instruct
temperature: 0.7
max-tokens: 20488.3 核心抽象
| 抽象 | 作用 |
|---|---|
ChatModel | 最底层,一次对话调用 |
ChatClient | 门面(类似 RestTemplate 之于 HttpClient),链式 API,日常只用它 |
Prompt / Message | System / User / Assistant / Tool 四种消息 |
Advisor | 切面式拦截器:记忆、RAG、日志、内容审核都挂在这里 |
ChatMemory | 多轮上下文,默认 MessageWindowChatMemory(保留最近 N 条) |
VectorStore | 向量库抽象:SimpleVectorStore(内存)、PgVectorStore、MilvusVectorStore |
@Tool | 把 Java 方法暴露给模型当函数调用 |
@Bean
ChatClient chatClient(ChatModel model) {
return ChatClient.builder(model)
.defaultSystem("你是心理健康助手,回答不超过 150 字,遇到自伤倾向要引导求助热线。")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(new MessageWindowChatMemory(20)).build(),
new SimpleLoggerAdvisor())
.build();
}@Bean
ChatClient chatClient(ChatModel model) {
return ChatClient.builder(model)
.defaultSystem("你是心理健康助手,回答不超过 150 字,遇到自伤倾向要引导求助热线。")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(new MessageWindowChatMemory(20)).build(),
new SimpleLoggerAdvisor())
.build();
}8.4 流式输出(SSE)
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam String q) {
return chatClient.prompt()
.user(q)
.stream() // 关键:stream 而不是 call
.content(); // Flux<String>,逐 token 推给前端
}
// 需要拿完整文本做后处理时:
String full = chatClient.prompt().user(q).call().content();@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam String q) {
return chatClient.prompt()
.user(q)
.stream() // 关键:stream 而不是 call
.content(); // Flux<String>,逐 token 推给前端
}
// 需要拿完整文本做后处理时:
String full = chatClient.prompt().user(q).call().content();前端用 EventSource 接收即可。SSE 是单向的(服务端 → 浏览器),要双向就上 WebSocket。
8.5 RAG 最小实现
// 1. 灌库:文档 → 切块 → 向量化
vectorStore.add(new TokenTextSplitter().apply(new Document(kbText)));
// 2. 检索增强:QuestionAnswerAdvisor 自动完成「检索 + 拼进 Prompt」
ChatResponse resp = chatClient.prompt()
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.7).build())
.build())
.user("年假怎么申请?")
.call();// 1. 灌库:文档 → 切块 → 向量化
vectorStore.add(new TokenTextSplitter().apply(new Document(kbText)));
// 2. 检索增强:QuestionAnswerAdvisor 自动完成「检索 + 拼进 Prompt」
ChatResponse resp = chatClient.prompt()
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.7).build())
.build())
.user("年假怎么申请?")
.call();完整链路是:切块 → 向量化 → 存库 → 检索 topK → 重排 → 拼 Prompt → 生成 → 引用标注。工程上真正决定效果的是切块策略和检索召回,模型反而是最不重要的那环。
8.6 工具调用
public class OrderTools {
@Tool(description = "根据订单号查询订单状态,订单号形如 SO20240101001")
public String queryOrder(String orderNo) {
return orderService.getStatus(orderNo); // 模型自己决定什么时候调
}
}
chatClient.prompt().user("帮我查下 SO20240101001 到哪了").tools(new OrderTools()).call();public class OrderTools {
@Tool(description = "根据订单号查询订单状态,订单号形如 SO20240101001")
public String queryOrder(String orderNo) {
return orderService.getStatus(orderNo); // 模型自己决定什么时候调
}
}
chatClient.prompt().user("帮我查下 SO20240101001 到哪了").tools(new OrderTools()).call();Spring AI 负责把 @Tool 转成 JSON Schema 发给模型,模型返回 tool_calls 后由框架反射调用、再把结果回传。注意:工具方法要自己做参数校验和权限校验,模型给出的参数不可信。
8.7 生产踩坑清单
| 坑 | 现象 | 处理 |
|---|---|---|
| Nginx 缓冲 | SSE 一次吐出全部内容 | proxy_buffering off; proxy_read_timeout 300s; |
| 网关超时 | 长回答被截断 | 服务端心跳 + 前端断线重连(带 lastEventId) |
| 全局异常处理器 | 异常被 @RestControllerAdvice 吞掉,SSE 流挂住不结束 | 对 Flux 返回类型单独处理,直接 onErrorResume 发 error 事件 |
| 上下文超限 | 长对话报 token 超限 | 滑动窗口 + 摘要压缩;限制单条输入长度 |
| 成本失控 | 账单暴涨 | 按用户配额、max-tokens 上限、便宜模型兜底、缓存高频问题 |
| 线程池 | 流式占线程 | WebFlux / SSE 用 Netty 事件循环,别用 @Async 默认池 |
| 内容安全 | 违规输出 | 输入输出双向过滤,命中走固定话术 |
8.8 和其他方案对比
| 方案 | 定位 |
|---|---|
| Spring AI | Spring 官方,抽象统一、Advisor 机制好扩展,适合已有 Spring 体系 |
| Spring AI Alibaba | 在 Spring AI 之上补了通义/DashScope、Graph 编排,国内落地更顺 |
| LangChain4j | 社区活跃、AI Service 注解式声明很省事,但生态耦合度低于 Spring |
| 自己封装 HTTP | 只调一两个接口、不打算做 RAG/工具调用时,反而最简单 |
面试口径:别说「用了 Spring AI」就完了,要说清「为什么是后端接而不是前端直连」(Key 安全 + 成本管控 + 数据不出内网)、SSE 怎么保证不中断、RAG 的召回率怎么评、token 成本怎么控。
9. 其他流行框架
9.1 Quarkus
Quarkus 是一个 Kubernetes 原生 Java 框架,用于构建轻量级、快速启动的应用程序。
特点:
- 快速启动时间
- 低内存占用
- 原生镜像支持(GraalVM)
- 响应式和命令式编程模型
- 与 Spring API 兼容
- 内置 Kubernetes 支持
示例:
@Path("/hello")
public class HelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello from Quarkus!";
}
@GET
@Path("/{name}")
@Produces(MediaType.TEXT_PLAIN)
public String hello(@PathParam("name") String name) {
return "Hello, " + name + "!";
}
}@Path("/hello")
public class HelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello from Quarkus!";
}
@GET
@Path("/{name}")
@Produces(MediaType.TEXT_PLAIN)
public String hello(@PathParam("name") String name) {
return "Hello, " + name + "!";
}
}9.2 Micronaut
Micronaut 是一个现代、轻量级的 Java 框架,用于构建模块化、易于测试的微服务应用程序。
特点:
- 快速启动时间
- 低内存占用
- 编译时依赖注入
- 原生镜像支持(GraalVM)
- 响应式编程支持
- 内置服务发现和配置管理
示例:
@Controller("/api/users")
public class UserController {
@Inject
private UserService userService;
@Get("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
@Get
@Produces(MediaType.APPLICATION_JSON)
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@Post
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public User createUser(@Body User user) {
return userService.createUser(user);
}
}@Controller("/api/users")
public class UserController {
@Inject
private UserService userService;
@Get("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
@Get
@Produces(MediaType.APPLICATION_JSON)
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@Post
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public User createUser(@Body User user) {
return userService.createUser(user);
}
}10. 总结
Java 框架技术栈是 Java 企业级应用开发的重要组成部分,包括:
- Spring Framework 6:提供了全面的基础设施支持
- Spring Boot 3:简化了 Spring 应用的开发和部署
- Spring Security 6:提供了强大的安全功能
- Jakarta EE 10:企业级应用开发平台
- Spring Data:简化了数据访问层的开发
- Spring WebFlux:响应式 Web 框架
- Quarkus:Kubernetes 原生 Java 框架
- Micronaut:现代、轻量级 Java 框架
这些框架和技术栈各有特点,开发者可以根据项目需求选择合适的框架。Spring 生态系统是 Java 企业级开发的主流选择,而 Quarkus 和 Micronaut 则适合构建云原生应用。
随着 Java 技术的不断发展,这些框架也在不断演进,支持最新的 Java 特性和云原生技术,为开发者提供了更高效、更便捷的开发体验。
Cao Note