这是一份为你定制的Java接口（Interface）与抽象类（Abstract Class）终极对比笔记。这份笔记整合了从Java 7到Java 17的所有语法变迁，并深入到设计哲学和底层JVM机制，适合面试冲刺或作为案头速查手册。📌 第一部分：核心定义（先定调）抽象类（Abstract Class）：“不完整的实体”。它用来描述一类事物公共的“本质”（状态+行为）。它可以有构造器、有属性、有已实现的方法，但因为不够具体，所以不允许被new出来。接口（Interface）：“纯粹的契约/能力”。它用来定义一种“行为规范”。它不管实现者是谁（可以是鸟、飞机、超人），只要签署了这份契约（implements），就必须具备这个能力。📌 第二部分：终极语法速查表（Java 8+ 特别版）对比维度抽象类（Abstract Class）接口（Interface）关键字abstract classinterface实例化（new）不能（无法创建对象）不能（无法创建对象）构造方法可以有（用于子类初始化父类属性）绝对没有（因为无实例变量需初始化）成员变量可以是任何类型（private/protected/public 实例变量，可以有状态）默认且只能是 public static final（常量），变量必须显式赋初值普通具体方法可以有（直接写带{}的方法体）Java 8+ 通过 default 和 static 可以有；Java 9+ 通过 private 可以有抽象方法使用 abstract 关键字，子类必须实现（除非子类也是抽象类）默认隐含 public abstract，实现类必须实现（除非实现类是抽象类）继承/实现关系单继承（一个类只能 extends 一个父类）多继承/多实现（一个接口可以 extends 多个接口；一个类可以 implements 多个接口）访问修饰符限制方法可以是 public/protected/private/default接口中的抽象方法隐含且只能是 public（不能是private或protected，否则编译报错）静态方法可以有，子类可以继承调用可以有（Java 8+），但只能通过接口名调用，实现类无法继承final 修饰可以有 final 方法（禁止子类重写）接口中的方法不能声明为 final（因为抽象/默认方法天生需要被重写或覆盖）📌 第三部分：方法家族全览（最易混淆点详解）接口到底能写哪些方法？这是面试高频扣分点：方法类型是否允许（接口）是否允许（抽象类）备注抽象方法（无方法体）✅ 允许（隐含public abstract）✅ 允许接口只干这事（Java 7之前）实例具体方法（有方法体）❌ 不允许（直到Java 8）✅ 允许抽象类主打的功能default 方法（Java 8+）✅ 允许—用于接口版本升级，避免老实现类报错static 方法（Java 8+）✅ 允许✅ 允许接口的static不可继承；抽象类的static可被子类继承调用private 方法（Java 9+）✅ 允许✅ 允许接口内用于抽取default方法的公共逻辑，不对外暴露📌 第四部分：设计哲学（本质区别，面试必答）抽象类：本质是“是什么”（Is-A）。侧重于对象本身。子类和父类必须有血缘关系（例如：Dog extends Animal）。用于捕获公共的状态（属性）和通用的基础逻辑。例子：所有订单都有订单号、金额，创建时都要打印日志。这些状态和步骤必须由抽象类来定义，接口做不到。接口：本质是“能做什么”（Can-Do / Like-A）。侧重于对象拥有的能力。完全不相关的类可以通过实现同一个接口获得相同能力（例如：Bird、Plane、Superman 都能实现 Flyable）。用于定义行为契约，不关心实现者的内部状态。例子：Comparable（可比较）、Runnable（可运行）、Serializable（可序列化）。📌 第五部分：经典协作模式（源码级的黄金搭档）在顶级框架（如Spring、JDK集合框架）中，它们从不“单打独斗”，而是按照 “接口定义标准 + 抽象类提供骨架 + 具体类实现业务” 三层结构配合： [List 接口] ← 定义契约（能添加、能删除、能遍历） ↑ (implements) [AbstractList 抽象类] ← 提供骨架（把 indexOf、equals 等通用逻辑先写一半） ↑ (extends) ┌──────────┴──────────┐ [ArrayList] [LinkedList] ← 具体实现（各自实现最核心的差异化逻辑）结论：用接口应对多态和扩展，用抽象类应对代码复用和封装状态。📌 第六部分：面试高频陷阱题（查漏补缺）抽象类能实现接口吗？ ✅ 可以。且抽象类实现接口时可以不实现接口的方法，留给具体子类去实现（这就是适配器模式的底层原理）。接口能继承抽象类吗？ ❌ 绝对不可以。接口只能 extends 接口，不能 extends 类（语法上禁止）。什么时候用 default 方法重写？ 一个类实现了两个接口，两个接口都有同名的 default 方法，此时实现类必须显式重写该方法，否则编译冲突。抽象类里的 static 方法和接口里的 static 方法区别？ 抽象类的 static 方法可以通过子类名直接调用（继承）；接口的 static 方法只能通过接口名调用，子类/实现类无法继承。📌 第七部分：极简决策脑图（实用场景）如果你的需求是： ├── 共享给子类公共的 属性（成员变量）？ │ └── 是 → 必须用 抽象类（接口存不住状态） ├── 需要 非public 的访问权限（protected/private）？ │ └── 是 → 必须用 抽象类 ├── 想要为 完全无关的类 提供通用能力（如序列化）？ │ └── 是 → 必须用 接口 ├── 想要利用 多重继承 的好处？ │ └── 是 → 必须用 接口 └── 想要为已有系统做功能增强（不破坏旧代码）？ └── 是 → 用接口的 default 方法（Java 8+）这份笔记涵盖了从语法到架构的全部精髓。如果你想基于这份笔记，手写一个“接口+抽象类”配合的实际Demo（比如模拟Spring的BeanFactory），我可以立刻给你写一份，让你彻底吃透它们的配合使用。😊
在 Spring Boot 2.7 的企业实际开发中，数组操作主要依赖 Java 标准库（java.util.Arrays）和 Spring 框架内置工具类（org.springframework.util 包），两者结合基本能覆盖所有场景。⚙️ 数组操作核心方法速查操作类型核心方法所属类/库关键说明排序Arrays.sort(array)java.util.Arrays对数组进行原地升序排序。二分查找Arrays.binarySearch(array, key)java.util.Arrays必须在已排序数组中使用。数组拷贝Arrays.copyOf(original, newLength)Arrays.copyOfRange(original, from, to)java.util.Arrays灵活复制数组全部或部分内容。数组填充Arrays.fill(array, value)java.util.Arrays将数组所有或指定范围元素设为同一个值。数组转字符串Arrays.toString(array)Arrays.deepToString(array)java.util.Arrays方便打印一维或多维数组内容。数组比较Arrays.equals(array1, array2)Arrays.deepEquals(array1, array2)java.util.Arrays比较一维或多维数组内容是否相等。集合判空CollectionUtils.isEmpty(collection)CollectionUtils.isNotEmpty(collection)org.springframework.util.CollectionUtils安全地检查集合是否为 null 或空。数组转集合CollectionUtils.arrayToList(array)org.springframework.util.CollectionUtils支持基本类型数组转换。数组合并到集合CollectionUtils.mergeArrayIntoCollection(array, collection)org.springframework.util.CollectionUtils将数组元素合并到现有集合中。集合空值兜底CollectionUtils.emptyIfNull(collection)org.springframework.util.CollectionUtils如果集合为 null，返回一个空集合，避免 NPE。集合包含检查CollectionUtils.contains(collection.iterator(), element)org.springframework.util.CollectionUtils检查集合是否包含某元素。对象/数组判空ObjectUtils.isEmpty(obj)org.springframework.util.ObjectUtils可检查对象、数组、集合等是否为 null 或空。获取首个非空对象ObjectUtils.firstNonNull(obj1, obj2, ...)org.springframework.util.ObjectUtils从多个对象中返回第一个非 null 的。数组转分隔字符串StringUtils.arrayToCommaDelimitedString(arr)org.springframework.util.StringUtils将数组元素用逗号连接成字符串。🚀 数组与集合互转（高频场景）这是日常开发中最常见的操作之一，需要特别注意不同方法的区别。数组 转 ListArrays.asList(T... a)：最常用，但返回的 List 是固定长度的，不能进行 add 或 remove 操作。且不支持基本类型数组。// 引用类型数组 -> 正确 String[] strArray = {"a", "b"}; List<String> list = Arrays.asList(strArray); // 基本类型数组 -> 错误！ list 实际上是一个包含 int[] 对象的 List int[] intArray = {1, 2}; List<int[]> list = Arrays.asList(intArray); new ArrayList<>(Arrays.asList(array))：创建一个可变的 ArrayList，可以自由增删元素。String[] strArray = {"a", "b"}; List<String> list = new ArrayList<>(Arrays.asList(strArray)); list.add("c"); // 可以操作CollectionUtils.arrayToList(array)：Spring 提供的工具方法，优雅地解决了基本类型数组的转换问题。int[] intArray = {1, 2}; // 自动将 int[] 转为 List<Integer> List<?> list = CollectionUtils.arrayToList(intArray); Arrays.stream(array).collect(Collectors.toList())：Stream 方式，灵活强大，适用于需要进行过滤、映射等复杂操作后再收集的场景。List 转 数组list.toArray(T[] a)：标准推荐方式。List<String> list = Arrays.asList("a", "b"); // 传入一个长度相同的空数组，类型安全 String[] array = list.toArray(new String[0]);list.stream().toArray()：Java 8 引入的 Stream 方式。List<String> list = Arrays.asList("a", "b"); String[] array = list.stream().toArray(String[]::new);✨ 高阶技巧：Stream 流式处理对于复杂的数组操作（过滤、转换、聚合），Stream API 是首选。int[] numbers = {1, 2, 3, 4, 5}; // 1. 过滤出偶数并打印 Arrays.stream(numbers).filter(n -> n % 2 == 0).forEach(System.out::println); // 输出 2, 4 // 2. 所有元素乘以2并收集为新的List List<Integer> doubledList = Arrays.stream(numbers) .map(n -> n * 2) .boxed() // 将 IntStream 装箱为 Stream<Integer> .collect(Collectors.toList());对于二维数组，Arrays.stream(arr) 得到的是 Stream<int[]>，即每一行作为一个元素。🧰 企业常用第三方库虽然标准库和 Spring 工具类已很强大，但在实际项目中，为了进一步简化代码、减少样板代码，往往会引入一些优秀的第三方库：Hutool：国产明星工具库，提供 ArrayUtil 等大量实用的数组操作方法。Guava：Google 出品，其 Lists、Ints 等工具类在集合和数组操作上非常强大。Apache Commons Lang：老牌工具库，提供 ArrayUtils 类。这些库通常提供了更丰富、更便捷的API，例如判空、反转、合并等，可以作为补充。⚠️ 踩坑指南Arrays.asList() 的“坑”：返回的是 java.util.Arrays.ArrayList，不是我们常用的 java.util.ArrayList，不支持 add/remove 操作，否则会抛出 UnsupportedOperationException。基本类型数组的误用：如 Arrays.asList(new int[]{1,2})，得到的是一个 List<int[]>，而非 List<Integer>。空指针异常 (NullPointerException)：当数组或集合为 null 时，直接操作或遍历会抛出 NPE。建议使用 Spring 的 CollectionUtils.isEmpty()或 ObjectUtils.isEmpty()进行安全检查。数组越界 (ArrayIndexOutOfBoundsException)：访问数组时，索引必须在 [0, array.length - 1] 范围内。遍历时优先使用增强 for 循环或 Arrays.stream() 来避免手动管理索引。💎 总结Spring Boot 2.7 企业开发中的数组操作，核心是掌握 java.util.Arrays 和 org.springframework.util 包下的工具类。对于数组与集合的转换、判空等高频操作，优先使用 Spring 的 CollectionUtils，可以避免很多常见的坑。对于复杂的业务逻辑，善用 Stream API 能让代码更清晰、更优雅。
🌐 Spring MVC 知识地图一、核心架构与组件Spring MVC 核心 ├── DispatcherServlet（前端控制器） ├── HandlerMapping（处理器映射） │ ├── RequestMappingHandlerMapping （注解映射，最常用） │ ├── BeanNameUrlHandlerMapping │ └── SimpleUrlHandlerMapping ├── HandlerAdapter（处理器适配器） │ ├── RequestMappingHandlerAdapter │ ├── HttpRequestHandlerAdapter │ └── SimpleControllerHandlerAdapter ├── Handler / Controller（处理器） ├── ModelAndView（模型与视图封装） ├── ViewResolver（视图解析器） │ ├── InternalResourceViewResolver（JSP） │ ├── BeanNameViewResolver │ ├── XmlViewResolver │ ├── ContentNegotiatingViewResolver │ └── …… └── View（视图） ├── JSP / JSTL ├── Thymeleaf ├── FreeMarker └── ……二、请求处理完整流程客户端请求 → DispatcherServlet 接收 ├── 1. 通过 HandlerMapping 查找 Handler（返回 HandlerExecutionChain，包含拦截器） ├── 2. 通过 HandlerAdapter 执行 Handler（Controller） ├── 3. Handler 执行业务逻辑，返回 ModelAndView ├── 4. 通过 ViewResolver 解析 ModelAndView 得到 View 对象 └── 5. View 渲染，将模型数据填充到视图，生成响应三、控制器开发Controller 开发 ├── 注解 │ ├── @Controller / @RestController │ ├── @RequestMapping │ │ ├── @GetMapping │ │ ├── @PostMapping │ │ ├── @PutMapping │ │ ├── @DeleteMapping │ │ └── @PatchMapping │ └── @CrossOrigin（跨域） ├── 请求参数绑定 │ ├── @RequestParam │ ├── @PathVariable（路径变量） │ ├── @RequestHeader │ ├── @CookieValue │ ├── @MatrixVariable │ ├── @RequestBody（JSON/XML 等） │ ├── @ModelAttribute（表单/查询参数绑定到对象） │ ├── HttpServletRequest / HttpServletResponse │ └── MultipartFile（文件上传） ├── 返回类型 │ ├── String（视图名，配合 Model） │ ├── ModelAndView │ ├── void（直接操作 HttpServletResponse） │ ├── @ResponseBody / ResponseEntity（JSON/XML 响应） │ └── View、Model、Map 等 └── 异步处理 ├── Callable<T> ├── DeferredResult<T> └── ResponseBodyEmitter / SseEmitter / StreamingResponseBody四、数据绑定、类型转换与校验数据绑定与校验 ├── 类型转换 │ ├── PropertyEditor │ ├── Converter / ConverterFactory │ ├── Formatter（日期/数字等格式化） │ └── @InitBinder（自定义绑定规则，注册编辑器/校验器） ├── 数据校验 │ ├── JSR-380（Bean Validation）：@Valid / @Validated │ ├── BindingResult / Errors（校验结果） │ └── 自定义校验注解 └── WebDataBinder └── 控制绑定行为（允许/禁止字段等）五、RESTful Web 服务REST 支持 ├── @RestController = @Controller + @ResponseBody ├── HTTP 方法映射注解 ├── 内容协商 ContentNegotiationManager ├── HttpMessageConverter 消息转换器 │ ├── MappingJackson2HttpMessageConverter（JSON） │ ├── Jaxb2RootElementHttpMessageConverter（XML） │ ├── StringHttpMessageConverter │ └── FormHttpMessageConverter 等 ├── 请求/响应体处理 │ ├── @RequestBody │ └── @ResponseBody / ResponseEntity / HttpEntity ├── 跨域 @CrossOrigin / 全局 CORS 配置 └── HATEOAS 支持（Spring HATEOAS）六、视图解析与渲染视图技术 ├── JSP + JSTL │ └── InternalResourceViewResolver 配置 ├── Thymeleaf │ └── SpringTemplateEngine + ThymeleafViewResolver ├── FreeMarker │ └── FreeMarkerViewResolver ├── 重定向与转发 │ ├── "redirect:/path"（RedirectView） │ └── "forward:/path"（InternalResourceView） ├── 视图解析链（多个 ViewResolver 顺序解析） └── ContentNegotiatingViewResolver（根据请求类型选择视图）七、拦截器与过滤器拦截器 HandlerInterceptor（Spring MVC 层面） ├── 方法 │ ├── preHandle（控制器执行前） │ ├── postHandle（控制器执行后，视图渲染前） │ └── afterCompletion（视图渲染后） ├── 配置方式 │ ├── XML 配置 <mvc:interceptors> │ └── Java 配置实现 WebMvcConfigurer.addInterceptors() └── 与 Servlet Filter 的区别 ├── Filter 在 Servlet 容器层面，不依赖 Spring └── Interceptor 可访问 Handler 对象、ModelAndView，更贴合 MVC八、异常处理异常处理体系 ├── @ExceptionHandler（方法级，仅处理当前 Controller） ├── @ControllerAdvice / @RestControllerAdvice（全局异常处理） │ └── + @ExceptionHandler、@InitBinder、@ModelAttribute ├── HandlerExceptionResolver 接口 │ ├── SimpleMappingExceptionResolver（映射异常到错误视图） │ ├── DefaultHandlerExceptionResolver（处理标准 Spring 异常） │ └── ResponseStatusExceptionResolver ├── @ResponseStatus（标注异常，返回对应 HTTP 状态码） └── 自定义异常 + 全局处理九、文件上传与下载文件操作 ├── 上传 │ ├── MultipartResolver │ │ ├── CommonsMultipartResolver（需 Apache Commons FileUpload） │ │ └── StandardServletMultipartResolver（Servlet 3.0+ 标准） │ └── @RequestParam("file") MultipartFile ├── 下载 │ ├── ResponseEntity<byte[]> 配合 InputStreamResource │ ├── StreamingResponseBody（流式大文件下载） │ └── 设置 Content-Disposition 响应头 └── 多文件上传（MultipartFile[] 或 List<MultipartFile>）十、国际化与主题i18n & 主题 ├── MessageSource（国际化消息源） │ ├── ResourceBundleMessageSource │ └── ReloadableResourceBundleMessageSource（可热加载） ├── LocaleResolver（区域解析器） │ ├── AcceptHeaderLocaleResolver │ ├── SessionLocaleResolver │ ├── CookieLocaleResolver │ └── FixedLocaleResolver ├── LocaleChangeInterceptor（切换语言拦截器） ├── 主题 Theme │ ├── ThemeResolver │ └── ThemeSource（ResourceBundleThemeSource）十一、配置方式配置方式演进 ├── XML 时代 │ ├── web.xml 配置 DispatcherServlet │ ├── [name]-servlet.xml 配置 │ ├── <mvc:annotation-driven/> │ ├── <mvc:resources>（静态资源处理） │ └── <mvc:view-controller>（无逻辑视图跳转） ├── Servlet 3.0+ 纯 Java 配置 │ ├── WebApplicationInitializer / AbstractAnnotationConfigDispatcherServletInitializer │ ├── @Configuration + @EnableWebMvc │ ├── 实现 WebMvcConfigurer 接口进行定制 │ └── 静态资源 addResourceHandlers() └── Spring Boot 自动配置 ├── spring-boot-starter-web ├── 自动配置 DispatcherServlet、HttpMessageConverters、ViewResolver 等 ├── application.properties / yml 微调 └── 使用 @Configuration + WebMvcConfigurer 扩展十二、测试支持测试 ├── MockMvc（独立测试控制器，不启动完整服务器） │ ├── mockMvc.perform(get("/api")) │ ├── .andExpect(status().isOk()) │ └── .andExpect(jsonPath("$.name").value("test")) ├── @WebMvcTest（Spring Boot 切片测试，仅加载 MVC 层） ├── TestRestTemplate / WebTestClient（集成测试） └── Mock 服务依赖（@MockBean）十三、高级与周边高级特性 ├── CORS 全局配置（WebMvcConfigurer.addCorsMappings） ├── HTTP 缓存控制（CacheControl, ETag, Last-Modified） ├── Servlet 异步支持 / 响应式桥梁 ├── WebSocket 集成（@MessageMapping, SimpMessagingTemplate） ├── Swagger / OpenAPI 3 文档生成 ├── 多版本 API 管理 └── 自定义 HandlerMethodArgumentResolver / HandlerMethodReturnValueHandler如果需要某个分支更详细的逐层展开，或想看 Spring MVC 与 Spring Boot 整合的实例脑图，可以告诉我。
Spring MVC 请求生命周期SpringBoot 2.7 + Java8 企业级开发一、整体架构图 —— 一次请求的完整流水线HTTP Request -> http://你的项目/getUser?id=1 │ ▼ ┌─────────────────────┐ │ Tomcat │ 内嵌 Servlet 容器，最早接收请求 │ Servlet Container │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ Filter 过滤器链 │ Servlet 级别的“门卫”，工作在 Spring MVC 之前 └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ DispatcherServlet │ Spring MVC 的“总控”，分发请求的核心 └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ HandlerInterceptor │ Spring 自己的拦截器，更懂 Controller │ preHandle │ 请求到达 Controller 前执行 └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ Controller │ 你写的接口，@RestController / @GetMapping └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ Service │ 业务逻辑层 └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ DAO/JdbcTemplate │ 数据访问层，执行 SQL └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ MySQL │ 数据库 └─────────────────────┘ │ ▼ Controller返回结果 返回 Java 对象 │ ▼ Interceptor.postHandle 所有拦截器的 postHandle │ ▼ ViewResolver / JSON序列化 前后端分离时转成 JSON 字符串 │ ▼ Interceptor.afterCompletion 资源清理、日志记录 │ ▼ Response → 浏览器二、企业项目真实执行顺序（必背）虚假的顺序：Tomcat -> Interceptor -> Controller实际严格顺序：Tomcat -> Filter -> DispatcherServlet -> Interceptor -> Controller -> Service -> DAO -> MySQL为什么是这个顺序？Filter 是 Servlet 规范 定义的，工作在容器级别，比 Spring 更底层；Interceptor 是 Spring MVC 内部 的，只有在 DispatcherServlet 接收请求之后才生效。也就是说：Filter 可以拦截所有请求（包括静态资源），而 Interceptor 只能拦截进入 Spring MVC 的请求。三、Filter 和 Interceptor 区别 —— 两个不同级别的“保安”1. Filter（Servlet 级别）定义方式：实现 javax.servlet.Filter 接口执行时机：Tomcat → Filter → Spring MVC特点：工作在容器层，不依赖 Spring，拿不到 Controller、方法等 Spring 上下文信息。典型用途：JWT 登录校验（直接解析 token，失败直接返回 401）XSS 过滤（清洗 等危险字符）请求日志（记录每个请求的耗时、URL）跨域处理（CorsFilter 统一设置响应头）2. Interceptor（Spring MVC 级别）定义方式：实现 org.springframework.web.servlet.HandlerInterceptor 接口执行时机：DispatcherServlet → Interceptor → Controller特点：能获取到 Handler（Controller 方法）、参数、返回结果等，是 Spring 生态内的拦截手段。典型用途：用户信息注入（从 token 解析用户信息，放入 UserContext）权限校验（admin / user 角色判断）操作日志（记录“谁在何时做了什么”）API 访问统计（PV / UV 计数）面试标准答案Filter 先执行，Interceptor 后执行。 Filter 是 Servlet 规范，Intercetpor 是 Spring MVC 专属。四、DispatcherServlet 到底干了什么 —— 核心方法 doDispatch()设计模式：前端控制器模式（Front Controller Pattern）所有请求都会先被 DispatcherServlet 接管，它自己不干活，只负责指挥。源码简化流程：doDispatch() { // 1. 根据请求找到 Handler（Controller 方法）并包装成 HandlerExecutionChain HandlerExecutionChain chain = getHandler(request); // 2. 执行所有拦截器的 preHandle chain.applyPreHandle(request, response); // 3. 真正调用 Controller 方法 HandlerAdapter adapter = getHandlerAdapter(handler); ModelAndView mv = adapter.handle(request, response, handler); // 4. 执行所有拦截器的 postHandle chain.applyPostHandle(request, response, mv); // 5. 处理返回结果（JSON 序列化、视图渲染等） processDispatchResult(request, response, chain, mv, exception); // 6. 触发拦截器的 afterCompletion（总是执行） chain.triggerAfterCompletion(request, response, exception); }五、HandlerExecutionChain —— Controller + 拦截器的组合体请求到达 DispatcherServlet 后，getHandler() 方法会将你的 Controller 方法和所有适用的拦截器打包成一个执行链。结构示意：HandlerExecutionChain ├── Handler（你的 Controller 方法） ├── Interceptor A ├── Interceptor B └── Interceptor C执行顺序（链式调用，具有栈特性）：进入时：A.pre → B.pre → C.pre → Controller返回时：C.post → B.post → A.post最终完成：C.after → B.after → A.after（即使前面出异常也会执行）六、@RequestScope 的底层原理 —— 代理对象，而非每次都 new很多人以为 @RequestScope 每次请求都会创建一个全新的 Bean，实际上 Spring 注入的是一个代理对象。特别提醒：代理对象通过当前线程的 ThreadLocal 拿到绑定的请求上下文，从而找到属于当前请求的那个真实 @RequestScope 实例。如果在 Controller 里用 @Async 等开了新线程，原请求的 ThreadLocal 会丢失，导致代理找不到请求上下文而直接报错。@Autowired private UserContext userContext;实际注入的是 UserContext$$SpringProxy 代理类。该代理对象内部并不持有真实数据，当真正调用其方法时，它会从当前请求的 RequestAttributes 中动态获取属于当前请求的真实实例。流程：当前线程 → RequestAttributes → 真实的 UserContext好处：即便 Controller 是单例（Singleton），也能安全地使用 @RequestScope 的 Bean，因为每次调用都会通过代理去获取当前请求对应的真实对象。七、ThreadLocal 与 @RequestScope —— 如何安全地存放用户信息在企业项目中，经常需要在任何地方（Service、DAO）获取当前登录用户，常用两种方案。方案实现方式优点缺点最佳实践ThreadLocalUserHolder.set(user) / UserHolder.get()极快，线程隔离必须手动 remove()，否则线程池复用时会内存泄漏或数据串人在拦截器的 afterCompletion 中清理@RequestScope注解在 Spring 管理的 Bean 上Spring 自动管理生命周期，请求结束自动销毁，无泄漏风险性能比 ThreadLocal 多一点点反射/代理开销优先推荐，简单安全推荐：存放用户上下文信息时，优先选择 @RequestScope。只有在对性能要求极高、并发极大的场景下，才考虑 ThreadLocal，并确保在拦截器的 afterCompletion 中执行 remove()。八、Controller 参数解析过程 —— 为什么参数能自动绑定你写的 public Result get(Long id)，Spring 能自动从请求里拿到 id，靠的是 HandlerMethodArgumentResolver 接口。该解析器会遍历判断当前参数是否能被自己解析，若能则从 request 中取值并绑定。常见注解与对应解析器：注解解析器@RequestParamRequestParamMethodArgumentResolver@PathVariablePathVariableMethodArgumentResolver@RequestBodyRequestResponseBodyMethodProcessor@RequestHeaderRequestHeaderMethodArgumentResolver例如：@RequestHeader("token") String token 背后就是 RequestHeaderMethodArgumentResolver 在干活。你也可以实现该接口并注册，来支持自定义的参数注入（比如自动注入当前登录用户对象）。九、JSON 如何变成 Java 对象 —— HttpMessageConverter前端发送 JSON：POST { "name":"Tom" }到后端自动变成 UserDTO 对象，核心组件是 HttpMessageConverter。默认使用的实现是 MappingJackson2HttpMessageConverter，底层依赖 Jackson。请求方向：请求体 JSON → Jackson 反序列化 → Java 对象 → Controller 参数响应方向：Controller 返回的 Java 对象 → Jackson 序列化 → JSON 字符串 → HTTP 响应因此 Jackson 是 Spring Boot 中处理 JSON 的事实标准，排查问题时经常要关注它。十、统一异常处理 —— 别再到处 try-catch在企业项目中，应使用 @RestControllerAdvice 进行全局异常拦截，避免在每个 Controller 方法中写重复的 try-catch。@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result handleException(Exception e) { log.error("系统异常", e); return Result.fail(e.getMessage()); } @ExceptionHandler(BusinessException.class) public Result handleBusinessException(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); } }执行流程：Controller 抛出异常 ↓ HandlerExceptionResolver 捕获 ↓ 找到匹配的 @ExceptionHandler 方法 ↓ 返回统一的错误响应这样 Controller 只需关注正常业务逻辑，所有异常都会流入全局处理器，返回规范的接口格式。十一、完整认证流程 —— 不要只相信 Token真正的安全认证绝不能只解析 token，而必须结合 Redis 等存储做二次校验。标准流程：1. 请求携带 JWT token 2. Interceptor 解析 token 得到 userId 3. 去 Redis 查询该 userId 对应的完整用户信息（含权限、状态） 4. 如果 Redis 中不存在或 token 已失效 → 直接返回 401 5. 将用户信息放入 UserContext（@RequestScope 管理的 Bean） 6. Controller 直接从 UserContext 获取用户信息，无需再解析 token为什么要加 Redis？Token 可能被伪造（弱签名或密钥泄露）用户可能已被管理员禁用，但 token 还未过期用户执行了“退出登录”，token 理应立刻失效总结：Token 只负责提供身份标识（userId），真正的用户资料和权限应从 Redis（或数据库）获取，并在请求上下文中传递。十二、Spring MVC 八大核心组件（高级理解）DispatcherServlet 只是总控，真正复杂的工作由这些组件完成：组件作用重要性HandlerMapping根据请求 URL 找到对应的 Handler（Controller 方法）★★★HandlerAdapter真正执行 Handler，适配不同类型的处理器★★★HandlerExceptionResolver处理异常，分发给 @ExceptionHandler 或 @ControllerAdvice★★★ViewResolver解析视图名，找到具体视图（前后端分离时用得少）★MultipartResolver处理文件上传请求★LocaleResolver国际化，解析客户端的语言/地区★ThemeResolver主题切换（老旧项目中常见）★FlashMapManager用于重定向时保存临时数据★其中最核心的三个：HandlerMapping、HandlerAdapter、HandlerExceptionResolver，理解它们才能真正把握 Spring MVC 的执行原理。总结 —— 一条线全部串起来完整请求处理链路：Tomcat → Filter（登录校验、XSS、日志、跨域） → DispatcherServlet → HandlerMapping（找到 Controller 方法） → HandlerInterceptor.preHandle（权限、用户注入） → HandlerAdapter（执行方法） → Controller → Service → DAO / JdbcTemplate → MySQL → （若有异常）HandlerExceptionResolver → @RestControllerAdvice 统一处理 → HttpMessageConverter（Java 对象 ↔ JSON） → HandlerInterceptor.postHandle / afterCompletion → 返回 Response日常企业开发（SpringBoot 2.7 + JdbcTemplate + MySQL + Redis + JWT）中最核心的10个知识点：Filter — 容器层拦截，适合安全校验、跨域、日志Interceptor — Spring MVC 拦截，适合用户注入、权限、统计@RequestScope — 请求作用域 Bean，底层用代理实现ThreadLocal — 线程隔离的上下文存储，需手动清理HandlerMethodArgumentResolver — 参数解析器，实现自动注入HttpMessageConverter — JSON / 对象互转，底层 Jackson@RestControllerAdvice — 全局异常处理，不要到处 try-catchDispatcherServlet — 前端总控制器，调度所有请求HandlerExecutionChain — 拦截器与 Controller 的打包执行链Spring MVC 九大组件 — 理解项目架构离不开这些支撑组件
1. 策略模式 ⭐⭐⭐⭐⭐消灭 if-else 的核心武器，基于 Spring 容器自动选择策略。步骤：定义策略接口public interface Strategy { String getType(); void execute(); }实现多个策略并用 @Component 注册@Component public class StrategyA implements Strategy { @Override public String getType() { return "A"; } @Override public void execute() { /* ... */ } } @Component public class StrategyB implements Strategy { @Override public String getType() { return "B"; } @Override public void execute() { /* ... */ } }创建策略选择器@Component public class StrategySelector { private final Map<String, Strategy> strategyMap = new HashMap<>(); // 4. Spring自动注入所有Strategy实现到Map（详见文档末尾“注入条件”） public StrategySelector(Map<String, Strategy> strategyMap) { // 遍历注入的Map，按业务key重组 strategyMap.values().forEach(s -> this.strategyMap.put(s.getType(), s)); } // 5. 选择策略 public Strategy getStrategy(String type) { Strategy strategy = strategyMap.get(type); if (strategy == null) { throw new IllegalArgumentException("No strategy for: " + type); } return strategy; } }2. 工厂模式 ⭐⭐⭐⭐⭐创建对象的“万能工厂”，通过 Spring 容器自动装配产品族。步骤：定义产品接口public interface Product { String getType(); void doSomething(); }多个产品实现并注册@Component public class ProductA implements Product { @Override public String getType() { return "A"; } @Override public void doSomething() { /*...*/ } } @Component public class ProductB implements Product { @Override public String getType() { return "B"; } @Override public void doSomething() { /*...*/ } }创建工厂类（Spring自动注入所有产品）@Component public class ProductFactory { private final Map<String, Product> productMap; // 构造器注入：Spring会将所有Product的实现注入到Map中（key=bean名称，默认类名首字母小写） public ProductFactory(Map<String, Product> productMap) { this.productMap = productMap; } public Product create(String type) { Product product = productMap.get(type); // 或自定义getType()映射 if (product == null) throw new IllegalArgumentException("Unknown product type"); return product; } }与策略模式的区别：工厂侧重于创建/获取对象，策略侧重行为互换，但 Spring 集成写法相似。3. 责任链模式 ⭐⭐⭐⭐⭐多个处理器依次尝试，直到有人处理。步骤：定义请求上下文public class RequestContext { private boolean approved = false; private String rejectReason; private String approvedBy; // getters & setters... }编写 Handler 接口public interface Handler { boolean handle(RequestContext context); // true=处理完毕，不再传递 }具体处理器（@Component + @Order 控制顺序）@Component @Order(1) public class ManagerHandler implements Handler { public boolean handle(RequestContext context) { // 逻辑：通过则设置approved=true，返回true；否则返回false return true; } } @Component @Order(2) public class DirectorHandler implements Handler { /* ... */ }组装责任链（@PostConstruct 确保只排序一次）@Component public class HandlerChain { private final List<Handler> handlers; public HandlerChain(List<Handler> handlers) { this.handlers = handlers; } @PostConstruct public void init() { AnnotationAwareOrderComparator.sort(handlers); } public void process(RequestContext context) { for (Handler handler : handlers) { if (handler.handle(context)) break; } } }使用@Autowired HandlerChain chain; // ... RequestContext ctx = new RequestContext(); chain.process(ctx);4. 模板方法模式 ⭐⭐⭐⭐⭐父类定义骨架，子类填写细节。步骤：抽象模板类public abstract class AbstractExportService { public final void export() { prepareData(); writeFile(); cleanUp(); } protected abstract void prepareData(); protected abstract void writeFile(); protected void cleanUp() { /* 钩子：默认空实现 */ } }具体子类@Component public class ExcelExportService extends AbstractExportService { @Override protected void prepareData() { /*...*/ } @Override protected void writeFile() { /*...*/ } }Controller中使用（注入所有子类）@RestController public class ExportController { private final Map<String, AbstractExportService> exportServiceMap; // 注入时 key 为 bean 名称（类名首字母小写，或用@Service("excel")指定） public ExportController(Map<String, AbstractExportService> exportServiceMap) { this.exportServiceMap = exportServiceMap; } @GetMapping("/export/{type}") public void doExport(@PathVariable String type) { exportServiceMap.get(type).export(); } }5. 代理模式 ⭐⭐⭐⭐⭐控制对象访问，增强功能（Spring AOP 的基础）。Spring 推荐方式：声明式：@Transactional、@Cacheable 等本质是动态代理。手动示例：统计方法耗时。@Component public class SimpleService implements IService { @Override public void work() { /* ... */ } } @Component public class ServiceProxy implements IService { private final IService target; public ServiceProxy(@Qualifier("simpleService") IService target) { this.target = target; } @Override public void work() { long start = System.currentTimeMillis(); target.work(); System.out.println("cost: " + (System.currentTimeMillis() - start)); } }更常用的是直接使用 Spring AOP（@Aspect + @Around）实现动态代理。6. 状态模式 ⭐⭐⭐⭐⭐将状态逻辑拆分为独立的状态对象，避免大量 if-else。步骤（订单状态机）：状态接口public interface OrderState { void pay(OrderContext ctx); void ship(OrderContext ctx); }具体状态@Component public class PendingPaymentState implements OrderState { public void pay(OrderContext ctx) { // 支付成功后切换状态 ctx.setState(ctx.getPaidState()); } public void ship(OrderContext ctx) { throw new UnsupportedOperationException(); } } // PaidState, ShippedState... 同理上下文持有状态@Component public class OrderContext { private OrderState current; // 注入所有状态 @Autowired private PendingPaymentState pendingPaymentState; @Autowired private PaidState paidState; //... getters/setters，初始化为 pendingPaymentState }Spring 中可使用状态枚举配合 Map<OrderStatus, OrderState> 自动注入。7. 观察者模式 ⭐⭐⭐⭐一对多通知，Spring 事件机制天然支持。步骤：定义事件public class OrderCreatedEvent extends ApplicationEvent { private final Long orderId; public OrderCreatedEvent(Object source, Long orderId) { super(source); this.orderId = orderId; } // getter }发布事件@Component public class OrderService { @Autowired private ApplicationEventPublisher publisher; public void createOrder() { // 业务... publisher.publishEvent(new OrderCreatedEvent(this, orderId)); } }监听事件（可使用 @EventListener 或 @TransactionalEventListener）@Component public class SendSmsListener { @EventListener public void handleOrderCreated(OrderCreatedEvent event) { // 发短信 } }8. 门面模式 ⭐⭐⭐⭐为复杂子系统提供统一入口，Service 层常见模式。示例： 下单门面聚合库存、支付、物流三个服务。@Component public class OrderFacade { @Autowired private InventoryService inventoryService; @Autowired private PaymentService paymentService; @Autowired private LogisticsService logisticsService; public void placeOrder(OrderDTO dto) { inventoryService.reduceStock(dto.getSku()); paymentService.charge(dto.getAmount()); logisticsService.scheduleDelivery(dto.getOrderId()); } }9. 装饰器模式 ⭐⭐⭐⭐动态附加责任，比继承更灵活。示例： 对数据读写组件增加缓存装饰器。public interface DataService { String read(); } @Component("basicDataService") public class BasicDataService implements DataService { /*...*/ } @Component public class CacheDecorator implements DataService { private final DataService delegate; public CacheDecorator(@Qualifier("basicDataService") DataService delegate) { this.delegate = delegate; } @Override public String read() { // 先查缓存，没有则调用delegate并缓存 return delegate.read(); } }10. 适配器模式 ⭐⭐⭐⭐兼容两个不匹配的接口，常用于整合外部系统。// 目标接口 public interface Logger { void log(String msg); } // 被适配的第三方类 public class ExternalLogLib { public void writeLog(String text) { /*...*/ } } // 适配器 @Component public class ExternalLogAdapter implements Logger { @Autowired private ExternalLogLib externalLogLib; @Override public void log(String msg) { externalLogLib.writeLog(msg); } }11. DDD（领域驱动设计） ⭐⭐⭐⭐⭐⭐聚焦核心业务复杂度，分层架构与战略设计。核心步骤（简化分层）：用户接口层：Controller，接收请求并调用应用服务。应用层：@Service 应用服务，编排领域对象，不包含业务逻辑。领域层：实体（Entity）、值对象（Value Object）、聚合根（Aggregate Root）、领域服务（Domain Service）、仓储接口（Repository Interface）。基础设施层：仓储实现（@Repository），将聚合持久化到数据库。关键原则：领域层无任何 Spring 注解，纯 POJO。通过构造器注入将仓储接口注入领域服务或应用服务。聚合内通过根实体确保一致性。12. Event Driven（事件驱动架构） ⭐⭐⭐⭐⭐⭐通过事件解耦服务，提升伸缩性和最终一致性。Spring 实现：本地事件：ApplicationEventPublisher + @EventListener（见观察者模式）。分布式事件：集成 Kafka / RabbitMQ。// 发布领域事件到消息队列 @Autowired private KafkaTemplate<String, DomainEvent> kafkaTemplate; public void publish(DomainEvent event) { kafkaTemplate.send("topic-name", event); } // 消费 @KafkaListener(topics = "topic-name") public void handle(DomainEvent event) { /*...*/ }推荐配合 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 保证事务提交后再发消息。13. Saga ⭐⭐⭐⭐⭐⭐分布式事务的最终一致性方案，通过协调正向/补偿操作。编排型 Saga（借助事件驱动）：每个微服务监听前序事件，执行本地事务并发布新事件。若失败，发布补偿事件，上游监听并回滚。// 服务A发布订单创建事件，服务B监听并扣库存 // 若扣库存失败，服务B发布库存不足事件，服务A监听后进行补偿（取消订单）实现时可配合 Spring Cloud Stream / Axon Framework。14. CQRS（命令查询职责分离） ⭐⭐⭐⭐⭐⭐读写模型分离，最大化性能与扩展性。简单实现：命令侧：应用服务接收 Command，调用领域逻辑，写入主库。查询侧：@Service 直接从读库或缓存查询 DTO。// 命令 @PostMapping public void createOrder(@RequestBody CreateOrderCmd cmd) { commandService.handle(cmd); } // 查询（独立Controller或同一模块内分离） @GetMapping("/orders/{id}") public OrderView queryOrder(@PathVariable Long id) { return queryService.findById(id); }高级玩法：事件溯源，将所有状态变更存储为事件流，查询视图由事件投影构建。SpringBoot 2.7 注入条件（集合/Map注入）核心行为： Spring 会自动将所有实现了某个接口或父类的 Bean 注入到 List 或 Map 中。注入 List： @Autowired List<SomeInterface> list; 会按 Order 排序包含所有实现类的 Bean。注入 Map： @Autowired Map<String, SomeInterface> map; key 为 Bean 的名称（默认类名首字母小写，若用 @Service("myName") 则用指定名称），value 为实例。没有实现类时： Spring 默认会注入一个空集合（EmptyList/EmptyMap），不会报错。若希望强制存在才注入，可使用 @Autowired(required = true)，但注入集合时 required=true 仍允许空集合，除非你将它用在非集合字段上。推荐方式： 构造器注入，显式声明 final 字段，避免 @Autowired 注解（Spring 4.3+ 单构造器自动注入）。@Component public class StrategySelector { private final Map<String, Strategy> strategyMap = new HashMap<>(); // 构造器注入：Spring 自动将所有的 Strategy 实现类注入 Map public StrategySelector(Map<String, Strategy> allStrategies) { allStrategies.values().forEach(s -> strategyMap.put(s.getType(), s)); } }在 Spring Boot 2.7 (Spring Framework 5.3.x) 下此行为完全稳定，完美支持策略/工厂等模式的自动装配。
反射 + AOP + 注解 讲解目录注解（Annotation）什么是注解元注解与自定义注解如何在反射中读取注解反射（Reflection）反射的核心类：Class、Method、Field动态创建对象、调用方法、访问字段反射与注解的结合AOP（面向切面编程）AOP 的核心概念Spring AOP 的实现原理（代理）通知类型与切入点表达式如何在切面中使用反射和注解三剑合璧实战案例案例1：自定义 @Log 日志记录案例2：自定义 @RequirePermission 权限控制总结与关系图谱一、注解（Annotation）1.1 什么是注解？注解是 Java 5 引入的一种元数据，它本身不影响代码逻辑，但可以被编译、运行时工具或框架读取，从而赋予代码额外的语义或行为。常见注解如 @Override、@Deprecated、@Autowired 等。1.2 元注解定义注解时需要用到的注解，共有四个：元注解作用@Target指定注解能用在哪里（类、方法、字段等）@Retention指定注解的存活周期（源码、字节码、运行时）@Documented是否包含在 JavaDoc 中@Inherited是否允许子类继承父类的注解@Retention 的取值：RetentionPolicy.SOURCE：只在源码中存在，编译时丢弃（如 @Override）RetentionPolicy.CLASS：在字节码中存在，但运行时不可见（默认）RetentionPolicy.RUNTIME：运行时可通过反射读取（自定义注解必须选这个）1.3 自定义一个注解import java.lang.annotation.*; @Target(ElementType.METHOD) // 只能用在方法上 @Retention(RetentionPolicy.RUNTIME) // 运行时保留 public @interface Loggable { String value() default ""; // 注解属性 }使用：@Loggable("删除用户") public void deleteUser(Long id) { ... }1.4 在反射中读取注解无论是类、方法还是字段，反射 API 都提供了读取注解的方法：getAnnotation(Class<T> annotationClass) —— 获取指定类型的注解getAnnotations() —— 获取所有注解（含继承）getDeclaredAnnotations() —— 获取直接声明的注解（不含继承）isAnnotationPresent(Class<?> annotationClass) —— 判断是否存在某注解示例：读取方法上的 @LoggableMethod method = ...; // 通过反射拿到方法对象 Loggable log = method.getAnnotation(Loggable.class); if (log != null) { System.out.println("描述：" + log.value()); }二、反射（Reflection）2.1 反射的核心类类作用获取方式Class类的元信息对象.getClass()、类名.class、Class.forName("全类名")Method方法信息Class 对象的 getMethod(...)、getDeclaredMethods() 等Field字段信息Class 对象的 getField(...)、getDeclaredFields() 等Constructor构造器信息Class 对象的 getConstructor(...)、getDeclaredConstructors() 等2.2 获取 Class 对象的三种方式// 1. 通过类名.class Class<String> clz1 = String.class; // 2. 通过实例的 getClass() String s = "hello"; Class<?> clz2 = s.getClass(); // 3. 通过完全限定名字符串 Class<?> clz3 = Class.forName("java.lang.String");2.3 获取 Method 对象方法说明getMethod(name, paramTypes...)获取指定名称和参数的 public 方法（含继承）getMethods()获取所有 public 方法（含继承）getDeclaredMethod(name, paramTypes...)获取本类中声明的方法（不含继承，可访问私有）getDeclaredMethods()获取本类中声明的所有方法（不含继承）示例：获取 deleteUser(Long) 方法Method method = UserService.class.getMethod("deleteUser", Long.class);2.4 动态调用方法UserService target = new UserService(); Method method = UserService.class.getMethod("deleteUser", Long.class); Object result = method.invoke(target, 123L); // 相当于 target.deleteUser(123L)如果方法是私有的，需要先 setAccessible(true)。2.5 反射与注解的结合反射的 Method、Field、Class 等都实现了 AnnotatedElement 接口，因此可以直接读取注解。Method method = ...; if (method.isAnnotationPresent(Loggable.class)) { Loggable log = method.getAnnotation(Loggable.class); System.out.println("需要记录日志：" + log.value()); }应用场景：框架（Spring、Hibernate）通过反射扫描注解，完成依赖注入、ORM 映射等。自定义工具类，根据注解动态生成 SQL、校验参数等。三、AOP（面向切面编程）3.1 AOP 核心概念切面（Aspect）：封装横切逻辑的模块（比如日志、权限）。连接点（JoinPoint）：程序执行过程中的一个点（方法调用、字段访问等），Spring AOP 只支持方法连接点。切入点（Pointcut）：匹配连接点的表达式，决定哪些方法会被增强。通知（Advice）：在切入点处执行的动作（具体逻辑）。目标对象（Target）：被代理的原始对象。代理（Proxy）：包裹目标对象，加入切面逻辑后生成的对象。3.2 Spring AOP 的实现Spring AOP 基于动态代理：如果目标类实现了接口，默认使用 JDK 动态代理（java.lang.reflect.Proxy）。如果没有接口，则使用 CGLIB 生成子类作为代理。从 Spring Boot 2.0 开始，默认使用 CGLIB 代理（即使有接口也会用 CGLIB）。3.3 通知类型与切入点表达式通知类型注解说明@Before目标方法执行前@AfterReturning目标方法正常返回后@AfterThrowing目标方法抛出异常后@After目标方法结束后（类似 finally）@Around包围目标方法，可控制是否执行常用切入点表达式表达式含义execution(* com.example.service.*.*(..))匹配 service 包下的所有方法@annotation(com.example.Loggable)匹配带有 @Loggable 注解的方法execution(public * *(..))匹配所有 public 方法within(com.example.service.*)匹配 service 包下的所有类的所有方法3.4 切面中如何使用反射和注解在 @Around 通知中，通过 ProceedingJoinPoint 可以获取方法签名（MethodSignature），然后通过反射拿到 Method 对象，进而读取注解。@Around("@annotation(loggable)") public Object log(ProceedingJoinPoint pjp, Loggable loggable) throws Throwable { // 获得方法签名 MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); // 反射 Method 对象 // 可以读取更多注解 AnotherAnnotation anno = method.getAnnotation(AnotherAnnotation.class); // 执行原方法 return pjp.proceed(); }为什么要 MethodSignature？ 因为 pjp.getSignature() 返回的是通用的 Signature 接口，只有强转成 MethodSignature 才能获取反射 Method 以及参数名、返回类型等详细信息。3.5 JoinPoint 信息树（速查）JoinPoint (ProceedingJoinPoint 继承它) ├─ getTarget() → 目标对象（被代理的原始 Bean） ├─ getThis() → 当前代理对象 ├─ getArgs() → 方法参数值数组 ├─ getSignature() → Signature (实际是 MethodSignature) │ ├─ getName() → 方法名 │ ├─ getDeclaringTypeName() → 声明该方法的类全名 │ └─ (MethodSignature 特有) │ ├─ getMethod() → 反射 Method ★ │ │ └─ getAnnotation(..) → 获取注解 │ ├─ getParameterNames() → 参数名数组（需编译配置） │ ├─ getReturnType() → 返回类型 │ └─ getParameterTypes() → 参数类型数组 └─ getStaticPart() → 同 getSignature()四、三剑合璧实战案例4.1 案例 1：@Log 注解实现方法日志记录需求：在方法执行时自动打印入参、出参和耗时。步骤：自定义 @Log 注解编写切面 LogAspect，使用 @Around + @annotation在切面中通过 MethodSignature 获取方法名，通过 getArgs() 获取参数值// 注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Log { } // 切面 @Aspect @Component public class LogAspect { @Around("@annotation(com.example.Log)") public Object around(ProceedingJoinPoint pjp) throws Throwable { MethodSignature sig = (MethodSignature) pjp.getSignature(); String method = sig.getDeclaringTypeName() + "." + sig.getName(); Object[] args = pjp.getArgs(); System.out.println(">>> 调用 " + method + "，参数：" + Arrays.toString(args)); long start = System.currentTimeMillis(); Object result = pjp.proceed(); long time = System.currentTimeMillis() - start; System.out.println("<<< 返回：" + result + " 耗时：" + time + "ms"); return result; } } // 业务使用 @Service public class UserService { @Log public String getUser(Long id) { return "User#" + id; } }4.2 案例 2：@RequirePermission 权限控制需求：标记方法需要某权限，没有则拒绝访问。实现思路：定义注解 @RequirePermission("user:delete")在拦截器中把用户权限存入 ThreadLocal切面中读取注解所需的权限值，与用户权限对比，不满足抛异常// 注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String value(); } // 权限上下文 public class UserContext { private static final ThreadLocal<List<String>> PERMISSIONS = new ThreadLocal<>(); public static void set(List<String> p) { PERMISSIONS.set(p); } public static List<String> get() { return PERMISSIONS.get() != null ? PERMISSIONS.get() : Collections.emptyList(); } public static void clear() { PERMISSIONS.remove(); } } // 切面 @Aspect @Component public class PermissionAspect { @Around("@annotation(RequirePermission)") public Object check(ProceedingJoinPoint pjp) throws Throwable { MethodSignature sig = (MethodSignature) pjp.getSignature(); Method method = sig.getMethod(); RequirePermission permission = method.getAnnotation(RequirePermission.class); String required = permission.value(); if (!UserContext.get().contains(required)) { throw new RuntimeException("无权限：" + required); } return pjp.proceed(); } } // 拦截器（简化） public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, ...) { String user = request.getHeader("X-User"); List<String> permissions = loadPermissions(user); // 从数据库查 UserContext.set(permissions); return true; } @Override public void afterCompletion(...) { UserContext.clear(); } }五、总结与关系图谱三者关系：注解 是标记，贴在代码上描述元数据。反射 是读取这些标记和操作代码结构（类、方法）的能力。AOP 是利用反射机制，在运行时动态地将横切逻辑织入到带有特定注解（或符合表达式）的方法周围。常见协作模式：自定义一个注解（如 @Log）在 AOP 切面中，通过切入点表达式 @annotation(Log) 拦截所有标注了该注解的方法在切面通知方法里，利用 JoinPoint/ProceedingJoinPoint 获取方法签名（MethodSignature）通过 MethodSignature.getMethod() 拿到反射 Method 对象，再用 getAnnotation() 读取注解的具体值，实现差异化逻辑学习路径建议：先熟练 反射（Class、Method、注解读取）理解 代理模式（JDK 动态代理、CGLIB）掌握 Spring AOP 的基本用法（切入点表达式、通知类型）最后将三者结合，实现自定义注解驱动切面当你遇到如下代码时，大脑能快速解析：MethodSignature sig = (MethodSignature) joinPoint.getSignature(); Method method = sig.getMethod(); MyAnnotation ann = method.getAnnotation(MyAnnotation.class);它正在用 AOP 的连接点 获取 反射的 Method，然后 读取自定义注解，从而执行相应逻辑——这。
Spring JDBC + JdbcTemplate 学习笔记（方法驱动格式）本笔记聚焦于 JdbcTemplate 中常用的数据库操作方法。每个方法均按以下结构介绍：方法：方法签名/名称作用：该方法解决什么问题参数：主要参数的含义和类型方法案例：代码示例（可直接运行）实例说明：对案例的详细解释，包括注意事项前置环境：Spring 6 + HikariCP + HSQLDB，配置了 DataSource 和 JdbcTemplate Bean。第一章：准备工作与环境配置1.1 Maven 依赖<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>6.0.0</version> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.7.1</version> </dependency>1.2 Spring 配置类（AppConfig）@Configuration @ComponentScan @PropertySource("jdbc.properties") public class AppConfig { @Value("${jdbc.url}") String jdbcUrl; @Value("${jdbc.username}") String jdbcUsername; @Value("${jdbc.password}") String jdbcPassword; @Bean DataSource dataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl(jdbcUrl); config.setUsername(jdbcUsername); config.setPassword(jdbcPassword); return new HikariDataSource(config); } @Bean JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }1.3 数据库初始化（自动建表）@Component public class DatabaseInitializer { @Autowired JdbcTemplate jdbcTemplate; @PostConstruct void init() { jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" + "id BIGINT IDENTITY PRIMARY KEY, " + "email VARCHAR(100) NOT NULL UNIQUE, " + "password VARCHAR(100), " + "name VARCHAR(100))"); } }第二章：核心方法详解（按格式组织）方法一：queryForObject() – 查询单行记录并映射为对象作用：执行查询 SQL，期望返回一条记录，并将结果集的第一行通过 RowMapper 转换成 Java 对象。若查询结果为空或大于一行则抛出异常。参数：String sql – SQL 语句（可使用 ? 占位符）RowMapper<T> rowMapper – 将 ResultSet 当前行映射为 T 对象的函数式接口Object... args – 可变参数，按顺序替换 SQL 中的 ?方法案例：根据 email 查询用户public User getUserByEmail(String email) { String sql = "SELECT id, email, password, name FROM users WHERE email = ?"; return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User( rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name") ), email ); }实例说明：RowMapper 的 lambda 表达式接收 ResultSet 和当前行号（通常忽略行号），手动构建 User 对象。如果数据库中不存在该 email，会抛出 EmptyResultDataAccessException，需要捕获处理或改为返回 null。也适用于聚合查询，例如统计总数：public long countUsers() { String sql = "SELECT COUNT(*) FROM users"; return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> rs.getLong(1)); }方法二：query() – 查询多行记录，返回 List作用：执行查询 SQL，将结果集中的每一行都通过 RowMapper 转换为对象，最后返回 List<T>。结果集为空时返回空列表（不抛异常）。参数：String sql – SQL 语句RowMapper<T> rowMapper – 行映射器Object... args – 参数列表方法案例：分页查询用户public List<User> getUsersByPage(int pageIndex, int pageSize) { String sql = "SELECT * FROM users LIMIT ? OFFSET ?"; int offset = (pageIndex - 1) * pageSize; return jdbcTemplate.query(sql, (rs, rowNum) -> new User( rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name") ), pageSize, offset ); }实例说明：使用 BeanPropertyRowMapper 可自动映射（要求列名与属性名一致或下划线转驼峰）：public List<User> getAllUsers() { return jdbcTemplate.query("SELECT * FROM users", new BeanPropertyRowMapper<>(User.class)); }BeanPropertyRowMapper 适合字段较多的简单映射；复杂映射仍需手动 RowMapper。方法三：update() – 执行插入、更新、删除作用：执行 DML 语句（INSERT/UPDATE/DELETE），返回受影响的行数。参数：String sql – DML 语句Object... args – 参数列表返回值：int – 受影响的行数方法案例 1：更新用户名称public int updateUserName(Long id, String newName) { String sql = "UPDATE users SET name = ? WHERE id = ?"; return jdbcTemplate.update(sql, newName, id); }实例说明：调用方可根据返回值判断是否更新成功（例如返回值 == 0 表示用户不存在）。方法案例 2：删除用户public boolean deleteUser(Long id) { String sql = "DELETE FROM users WHERE id = ?"; int rows = jdbcTemplate.update(sql, id); return rows > 0; }方法案例 3：插入用户（不返回主键）public void addUserWithoutKey(String name, String email) { String sql = "INSERT INTO users (name, email) VALUES (?, ?)"; jdbcTemplate.update(sql, name, email); }方法四：update() + KeyHolder – 插入并返回自增主键作用：执行插入语句后，获取数据库自动生成的主键（如 IDENTITY 或 AUTO_INCREMENT）。参数：PreparedStatementCreator psc – 用于创建 PreparedStatement，必须指定 Statement.RETURN_GENERATED_KEYSKeyHolder keyHolder – 用于接收生成的主键方法案例：public User addUserAndGetId(String name, String email, String password) { String sql = "INSERT INTO users (name, email, password) VALUES (?, ?, ?)"; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, name); ps.setString(2, email); ps.setString(3, password); return ps; }, keyHolder); Long newId = keyHolder.getKey().longValue(); return new User(newId, email, password, name); }实例说明：PreparedStatementCreator 是一个函数式接口，需手动设置参数并返回带 RETURN_GENERATED_KEYS 的 PreparedStatement。执行后通过 keyHolder.getKey() 获取主键，通常为 Number 类型，需转换为 Long 或 Integer。适用于大多数关系型数据库（MySQL、HSQLDB、PostgreSQL、SQL Server 等）。方法五：batchUpdate() – 批量操作作用：一次性执行多条相同结构的 SQL（通常是批量插入或批量更新），性能远高于循环单条执行。参数：String sql – 带占位符的 SQL 模板List<Object[]> batchArgs – 每个元素是一个 Object[]，代表一次执行对应的参数返回值：int[] – 数组长度等于批处理次数，每个元素为对应操作影响的行数方法案例：批量插入多个用户public int[] batchAddUsers(List<User> users) { String sql = "INSERT INTO users (name, email, password) VALUES (?, ?, ?)"; List<Object[]> batch = new ArrayList<>(); for (User u : users) { batch.add(new Object[]{u.getName(), u.getEmail(), u.getPassword()}); } return jdbcTemplate.batchUpdate(sql, batch); }实例说明：批量操作会减少 JDBC 与数据库之间的网络往返次数，显著提升吞吐量。注意：某些数据库（如 HSQLDB）对单次批处理的大小有限制，可分批提交。方法六：execute() – 执行任意 SQL（DDL 或存储过程）作用：执行 DDL（如 CREATE TABLE）或任何不希望返回结果集的 SQL，也可以用于执行存储过程。参数：String sql – 要执行的 SQL 字符串返回值：无（或者可重载为 execute(Callback) 形式返回自定义结果）方法案例：创建日志表public void createLogTable() { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS sys_log (" + "id INT PRIMARY KEY, " + "message VARCHAR(255), " + "create_time TIMESTAMP)"); }实例说明：execute(String sql) 不关心返回结果，适合做数据库初始化、清空表、重建索引等管理操作。方法七：execute(ConnectionCallback) – 最底层扩展作用：获取原生 JDBC Connection 对象，执行任意复杂数据库操作（如设置超时、调用存储过程、使用原生 API 等）。参数：ConnectionCallback<T> action – 回调接口，接收 Connection 并返回 T返回值：由回调决定的 T方法案例：使用原生 PreparedStatement 和手动控制事务public User getUserWithNativeConnection(Long id) { return jdbcTemplate.execute((Connection conn) -> { try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) { ps.setLong(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return new User( rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name") ); } return null; } } }); }实例说明：JdbcTemplate 会自动释放 Connection，但回调内创建的 PreparedStatement 和 ResultSet 必须由开发者手动释放（建议使用 try-with-resources）。一般情况下优先使用更高级的 query()、update() 等方法，只有非常规需求时才使用 ConnectionCallback。方法八：execute(PreparedStatementCallback) – 灵活处理 PreparedStatement作用：由 JdbcTemplate 创建 PreparedStatement，回调中可自行执行查询、更新并处理结果集。参数：String sql – SQL 模板PreparedStatementCallback<T> action – 回调接收 PreparedStatement，返回 T方法案例：判断用户是否存在public boolean existsUserByName(String name) { return jdbcTemplate.execute("SELECT 1 FROM users WHERE name = ?", (PreparedStatement ps) -> { ps.setString(1, name); try (ResultSet rs = ps.executeQuery()) { return rs.next(); // 有记录返回 true } }); }实例说明：此方法比 ConnectionCallback 更高层，无需创建 PreparedStatement，但依然需要手动执行查询和处理 ResultSet。第三章：RowMapper 详解（两个常用实现）3.1 手动 RowMapper（Lambda）作用：将 ResultSet 当前行的数据提取并组装成业务对象。格式：(ResultSet rs, int rowNum) -> { ... }案例：已在 queryForObject 中展示。3.2 BeanPropertyRowMapper作用：自动将结果集的列名（支持下划线转驼峰）映射到 JavaBean 的同名属性（需有 setter）。参数：BeanPropertyRowMapper.newInstance(Class<T>) 或构造函数。案例：List<User> users = jdbcTemplate.query("SELECT id, email, name FROM users", BeanPropertyRowMapper.newInstance(User.class));实例说明：要求数据库列名 email 对应 Java 属性 email；若列名为 user_name，则对应 userName。非常便捷，适合大多数查询场景。第四章：声明式事务管理核心注解：@Transactional作用：将方法（或类中的所有方法）标记为事务性方法。Spring 会在方法执行前开启事务，执行成功则提交，抛出未捕获异常则回滚。参数（常用）：propagation – 传播行为（默认 REQUIRED）isolation – 隔离级别（默认数据库默认级别）rollbackFor – 指定哪些异常触发回滚（默认 RuntimeException 和 Error）方法案例：银行转账（两个更新操作必须在同一事务）@Service public class AccountService { @Autowired JdbcTemplate jdbcTemplate; @Transactional public void transfer(Long fromId, Long toId, BigDecimal amount) { jdbcTemplate.update("UPDATE account SET balance = balance - ? WHERE id = ?", amount, fromId); // 模拟异常：如果转账金额过大，回滚两个更新 if (amount.compareTo(BigDecimal.valueOf(10000)) > 0) { throw new RuntimeException("转账金额超过限额"); } jdbcTemplate.update("UPDATE account SET balance = balance + ? WHERE id = ?", amount, toId); } }实例说明：需要先在配置类上添加 @EnableTransactionManagement 并注册 DataSourceTransactionManager。默认情况下，只有 RuntimeException 会触发回滚；若希望检查型异常也回滚，需设置 @Transactional(rollbackFor = Exception.class)。事务边界默认基于 AOP 代理，内部方法调用（this.method()）不会触发事务。第五章：实战示例 – RESTful API 完整流程5.1 User 实体public class User { private Long id; private String name; private String email; private String password; // getter/setter/构造方法 省略 }5.2 UserService（使用上述方法）@Service public class UserService { @Autowired JdbcTemplate jdbcTemplate; public User addUser(String name, String email, String pwd) { KeyHolder kh = new GeneratedKeyHolder(); jdbcTemplate.update(conn -> { PreparedStatement ps = conn.prepareStatement( "INSERT INTO users (name, email, password) VALUES (?,?,?)", Statement.RETURN_GENERATED_KEYS); ps.setString(1, name); ps.setString(2, email); ps.setString(3, pwd); return ps; }, kh); return new User(kh.getKey().longValue(), email, pwd, name); } public User getUser(Long id) { try { return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", BeanPropertyRowMapper.newInstance(User.class), id); } catch (EmptyResultDataAccessException e) { return null; } } public List<User> listUsers() { return jdbcTemplate.query("SELECT * FROM users", BeanPropertyRowMapper.newInstance(User.class)); } public int updateUser(Long id, String newName) { return jdbcTemplate.update("UPDATE users SET name = ? WHERE id = ?", newName, id); } public int deleteUser(Long id) { return jdbcTemplate.update("DELETE FROM users WHERE id = ?", id); } }5.3 UserController@RestController @RequestMapping("/api/users") public class UserController { @Autowired UserService userService; @PostMapping public User add(@RequestParam String name, @RequestParam String email, @RequestParam String pwd) { return userService.addUser(name, email, pwd); } @GetMapping("/{id}") public User get(@PathVariable Long id) { return userService.getUser(id); } @GetMapping public List<User> list() { return userService.listUsers(); } @PutMapping("/{id}") public String update(@PathVariable Long id, @RequestParam String name) { int rows = userService.updateUser(id, name); return rows > 0 ? "success" : "user not found"; } @DeleteMapping("/{id}") public String delete(@PathVariable Long id) { int rows = userService.deleteUser(id); return rows > 0 ? "deleted" : "not exist"; } }5.4 测试命令# 新增 curl -X POST "http://localhost:8080/api/users?name=张三&email=zs@test.com&pwd=123" # 查询单个 curl "http://localhost:8080/api/users/1" # 查询全部 curl "http://localhost:8080/api/users" # 更新 curl -X PUT "http://localhost:8080/api/users/1?name=张三丰" # 删除 curl -X DELETE "http://localhost:8080/api/users/1"第六章：常见问题与注意事项queryForObject 空结果抛异常：需捕获 EmptyResultDataAccessException 或改用 query 方法加判断。BeanPropertyRowMapper 要求属性有 setter：否则映射会忽略该列。批量插入时数据库限制：例如 HSQLDB 默认批量最大 100 条，可分批调用 batchUpdate。事务失效场景：同一个类内非 @Transactional 方法调用 @Transactional 方法（无代理）方法被 private 修饰异常被 try-catch 吞掉且没有重新抛出使用 KeyHolder 时：必须通过 Statement.RETURN_GENERATED_KEYS 创建 PreparedStatement。附录：JdbcTemplate 方法速查表方法名作用适用场景queryForObject单行，返回对象根据主键查询、聚合函数query多行，返回 List列表查询、分页update单条 DML插入/更新/删除update + KeyHolder插入并返回主键获取自增 IDbatchUpdate批量 DML批量导入数据execute(String)DDL/存储过程建表、清空表execute(ConnectionCallback)原生 JDBC 操作特殊需求、存储过程---。
注意：Java 8 环境下，javax.annotation（@PostConstruct、@PreDestroy、@Resource）是 JDK 自带的，无需额外添加依赖。Spring Boot 2.7 使用 Spring Framework 5.x，完美支持 JSR-250 规范。一、Bean 的作用域（Scope）Spring 中的 Bean 默认是 单例（Singleton），即整个容器内只有一个实例。也可以通过 @Scope 指定为 原型（Prototype），每次获取都创建新实例。1.1 Singleton（单例）—— 默认创建时机：容器启动时（除非设置为 @Lazy）。销毁时机：容器正常关闭时，会调用 @PreDestroy 方法。获取结果：每次注入或 getBean() 返回同一个对象。@Component // 默认 singleton public class MailService { // ... }1.2 Prototype（原型）行为：每次获取（@Autowired 或 getBean）都会创建一个全新的实例。生命周期差异：容器负责创建 → 依赖注入 → 调用 @PostConstruct。容器不负责销毁：即使你在原型 Bean 中写了 @PreDestroy，Spring 不会自动调用它。需要你自己写代码清理（例如手动调用销毁方法）。声明方式：@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 或 @Scope("prototype") public class MailSession { @PostConstruct public void init() { System.out.println("原型 Bean 初始化，每次都会调用"); } @PreDestroy public void destroy() { System.out.println("原型 Bean 销毁 — 不会被 Spring 自动调用"); } }最佳实践：如果没有特殊需求，优先使用 singleton。prototype 适合那些有状态且不宜共享的对象，但要记得手动管理资源释放。二、注入 List —— 自动收集同一类型的所有 Bean当你需要在一个地方使用某个接口的 所有实现类 时，可以直接注入 List<T>，Spring 会自动把该接口的所有 Bean 放进列表。示例：多个 Validator 实现public interface Validator { void validate(String email, String password, String name); } @Component // 自动被收集 public class EmailValidator implements Validator { @Override public void validate(String email, String password, String name) { // email 校验逻辑 } } @Component public class PasswordValidator implements Validator { @Override public void validate(String email, String password, String name) { // password 校验逻辑 } } @Component public class NameValidator implements Validator { @Override public void validate(String email, String password, String name) { // name 校验逻辑 } }注入 List@Component public class Validators { @Autowired private List<Validator> validators; // Spring 自动注入所有 Validator 实现 public void validate(String email, String password, String name) { for (Validator v : validators) { v.validate(email, password, name); } } }控制顺序 —— @Order默认顺序不确定，可以用 @Order 注解指定（数字越小优先级越高）。@Component @Order(1) public class EmailValidator implements Validator { ... } @Component @Order(2) public class PasswordValidator implements Validator { ... } @Component @Order(3) public class NameValidator implements Validator { ... }注入的 List<Validator> 会按照 @Order 的值升序排列。三、可选注入 —— @Autowired(required = false)默认情况下，如果某个依赖的 Bean 不存在，Spring 会抛出 NoSuchBeanDefinitionException。如果你允许该依赖为 null，可以设置 required = false。@Component public class MailService { @Autowired(required = false) private ZoneId zoneId = ZoneId.systemDefault(); // 找不到 ZoneId 时使用默认值 }典型场景：某些配置是可选的（如时区、外部服务），若有定义则使用，否则退回到默认值。四、创建第三方 Bean —— @Bean 方法对于不在我们包内的类（例如 JDK 的 ZoneId、第三方库的 DataSource），无法用 @Component 扫描。此时需要在 @Configuration 类中编写一个 工厂方法，并标记 @Bean。@Configuration @ComponentScan public class AppConfig { @Bean public ZoneId zoneId() { return ZoneId.of("Z"); } @Bean("customRestTemplate") // 指定 Bean 名称 public RestTemplate myRestTemplate() { return new RestTemplate(); } }Spring 保证该方法 只被调用一次（默认单例）。方法名默认作为 Bean 的名称，也可用 @Bean("name") 显式命名。可以依赖其他 Bean：方法参数会自动注入。@Bean public DataSource dataSource() { return HikariDataSource(...); } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { // 自动注入上面创建的 DataSource return new JdbcTemplate(dataSource); }本质上，@Bean 方法就是一种 工厂方法，比实现 FactoryBean 接口更简洁。五、Bean 的初始化和销毁 —— @PostConstruct 和 @PreDestroy5.1 为什么需要初始化：在依赖注入完成后，执行一些启动逻辑（打开连接、启动后台线程、校验配置）。销毁：在容器关闭前，释放资源（关闭连接池、停止线程、清理临时文件）。5.2 使用方式（JSR-250，Java 8 原生支持）@Component public class MailService { @Autowired(required = false) private ZoneId zoneId = ZoneId.systemDefault(); @PostConstruct public void init() { System.out.println("初始化：依赖已注入，zoneId = " + zoneId); } @PreDestroy public void shutdown() { System.out.println("销毁前：释放资源（如关闭连接池）"); } }5.3 执行顺序初始化流程：调用构造方法创建实例依赖注入（@Autowired、@Resource 等）调用 @PostConstruct 方法（如果实现了 InitializingBean）调用 afterPropertiesSet()（如果配置了 init-method）调用自定义方法销毁流程（容器正常关闭时）：调用 @PreDestroy 方法（如果实现了 DisposableBean）调用 destroy()（如果配置了 destroy-method）调用自定义方法5.4 重要注意事项方法必须 无参数，返回值任意（通常 void）。方法名任意，不要求 init/destroy。prototype 作用域的 Bean，Spring 只调用 @PostConstruct，不会自动调用 @PreDestroy。如果原型 Bean 需要清理，你必须自己设计清理逻辑（例如提供 cleanup() 方法并手动调用）。5.5 “全自动”的条件在 Spring Boot 2.7（Java 8）中，只要满足以下条件，@PostConstruct 和 @PreDestroy 就是 全自动 的：Bean 是 singleton 作用域（默认）。Spring Boot 应用正常关闭（按 Ctrl+C、收到 SIGTERM、程序正常结束）。Spring Boot 会自动注册 JVM 关闭钩子，触发容器关闭，从而调用 @PreDestroy。不要使用 kill -9 强行杀死进程（那样 JVM 直接退出，不会执行关闭钩子）。@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); // 按 Ctrl+C 时，会看到 @PreDestroy 的输出 } }六、处理多个同类型 Bean —— 别名、限定符、主次6.1 问题描述在 @Configuration 中定义多个同类型的 @Bean：@Configuration public class AppConfig { @Bean ZoneId createZoneOfZ() { return ZoneId.of("Z"); } @Bean ZoneId createZoneOfUTC8() { return ZoneId.of("UTC+08:00"); } }直接注入 ZoneId 会报错：NoUniqueBeanDefinitionException。6.2 解决方案(1) 为每个 Bean 起一个名字（别名）@Bean("z") ZoneId createZoneOfZ() { ... } @Bean @Qualifier("utc8") ZoneId createZoneOfUTC8() { ... }(2) 注入时用 @Qualifier 指定名称@Component public class MailService { @Autowired @Qualifier("z") // 精确指定名称为 "z" 的那个 ZoneId private ZoneId zoneId; }或者使用 JSR-250 的 @Resource(name = "z")：@Component public class MailService { @Resource(name = "z") private ZoneId zoneId; }(3) 使用 @Primary 指定主要 Bean当没有使用 @Qualifier 时，Spring 会注入标记 @Primary 的那个 Bean。@Bean @Primary // 主要候选 @Qualifier("z") ZoneId createZoneOfZ() { ... } @Bean @Qualifier("utc8") ZoneId createZoneOfUTC8() { ... }注入时：@Component public class MailService { @Autowired // 无 @Qualifier，默认注入 @Primary 的那个（即 "z"） private ZoneId zoneId; }6.3 经典应用：多数据源@Configuration public class DataSourceConfig { @Bean @Primary DataSource masterDataSource() { ... } @Bean @Qualifier("slave") DataSource slaveDataSource() { ... } }默认注入主数据源（如 DAO 层无需特殊指定）。需要从库时：@Qualifier("slave") 精确注入。七、FactoryBean —— 工厂模式创建 Bean（了解即可）7.1 是什么FactoryBean 是 Spring 提供的工厂接口，允许你自定义 Bean 的创建逻辑。Spring 会先创建 FactoryBean 实例，再调用其 getObject() 方法返回真正的 Bean。@Component public class ZoneIdFactoryBean implements FactoryBean<ZoneId> { private String zone = "Z"; @Override public ZoneId getObject() throws Exception { return ZoneId.of(zone); } @Override public Class<?> getObjectType() { return ZoneId.class; } }注入/获取时拿到的是 getObject() 返回的 ZoneId。如果需要获取 FactoryBean 本身，可以在名称前加 &：context.getBean("&zoneIdFactoryBean")。7.2 现状在 Spring Boot 2.7 中，@Bean 方法已经足够简洁强大，完全可以替代 FactoryBean 的大部分场景。除非你要编写非常复杂的创建逻辑，或需要与旧框架集成（如 MyBatis 的 MapperFactoryBean），否则推荐直接使用 @Bean 方法。八、综合对比与最佳实践功能推荐方案关键点普通 Bean@Component默认 singleton原型 Bean@Scope("prototype")每次新建，容器不负责 @PreDestroy收集所有实现@Autowired List<Interface>配合 @Order 控制顺序可选依赖@Autowired(required=false)避免异常，可设默认值第三方 Bean@Bean 方法在 @Configuration 中定义初始化逻辑@PostConstruct依赖注入后自动执行销毁逻辑@PreDestroy容器关闭时自动执行（singleton 有效）同类型多个 Bean@Qualifier + 命名精确注入；@Primary 设置默认复杂工厂@Bean 方法一般不用 FactoryBean九、Java 8 + Spring Boot 2.7 特别提醒注解包：@PostConstruct、@PreDestroy、@Resource 使用 javax.annotation，无需添加任何额外依赖。Spring Boot 自动关闭：Spring Boot 应用默认注册 JVM 关闭钩子，只要你正常停止应用（Ctrl+C、kill 不带 -9、System.exit()），@PreDestroy 就会被调用。prototype 的陷阱：如果你的 Bean 是 prototype 且需要释放资源，请考虑改用 singleton，或自行设计清理方法（如实现 DisposableBean 并手动调用）。@Bean 方法的默认名称：方法名即 Bean 名称，注意避免重名（同一类型不同方法名）。
Spring Boot 2.7 入门到精通笔记（苍穹外卖实战版）一、Spring Boot 概述1.1 什么是 Spring BootSpring Boot 是 Spring 框架的“脚手架”，核心设计理念是 约定优于配置。与传统 Spring 的区别：对比项传统 SpringSpring Boot配置方式繁琐的 XML 配置文件注解 + 自动配置依赖管理手动管理版本、处理冲突起步依赖（Starter）自动引入部署方式打包成 WAR 部署到 Tomcat内嵌 Tomcat，直接 java -jar开发效率需要大量样板代码只需关心业务逻辑1.2 Spring Boot 核心特性自动配置：根据类路径中的依赖自动配置 Bean起步依赖：通过 Starter 机制简化依赖管理内嵌容器：默认集成 Tomcat/Jetty/Undertow生产就绪：Actuator 提供监控、健康检查等能力1.3 Spring Boot 2.7 新特性（重点）Spring Boot 2.7 是一个重要版本，2.5 已停止维护[reference:0]。自动配置变更：自动配置注册文件从 META-INF/spring.factories 迁移到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports新增 @AutoConfiguration 注解，用于标识自动配置类[reference:1]支持 after/before 属性进行配置排序其他新特性：支持 GraphQL（spring-boot-starter-graphql）支持 Podman 容器引擎Web Server SSL 增强[reference:2]1.4 苍穹外卖项目概述苍穹外卖是一个企业级 O2O 外卖平台，分为管理端和用户端（微信小程序）：角色功能管理端员工管理、菜品/套餐管理、订单管理、数据统计用户端登录授权、浏览点餐、购物车结算、订单追踪技术栈：Spring Boot 2.7 + Spring MVC + MyBatis-Plus + MySQL + Redis + OSS（阿里云）+ AOP + Swagger（Knife4j）+ WebSocket[reference:3]二、快速开始2.1 项目结构（苍穹外卖）苍穹外卖采用 Maven 多模块结构，各模块职责明确，便于维护和扩展：sky-take-out（父工程） ├── pom.xml # 统一依赖版本管理 ├── sky-common/ # 公共模块：工具类、常量、异常类 │ ├── constant/ # 常量定义（如JwtClaimsConstant） │ ├── context/ # ThreadLocal上下文 │ ├── exception/ # 自定义异常 │ ├── properties/ # 配置属性类 │ └── utils/ # 工具类（JWT、阿里OSS、微信支付等） ├── sky-pojo/ # 实体模块：Entity、DTO、VO │ ├── entity/ # 数据库实体类（与表一一对应） │ ├── dto/ # 数据传输对象（层间传递） │ └── vo/ # 视图对象（返回给前端） └── sky-server/ # 后端服务模块（核心业务代码） ├── config/ # 配置类（Redis、Swagger、WebMvc等） ├── controller/ # 控制器层 ├── service/ # 业务逻辑层 ├── mapper/ # 数据访问层 ├── interceptor/ # 拦截器（如JWT认证拦截器） ├── aspect/ # AOP切面（公共字段填充） └── SkyApplication.java # 启动类POJO 分类说明：Entity：实体类，与数据库表一一对应DTO（Data Transfer Object）：数据传输对象，用于层间传递数据VO（View Object）：视图对象，专门返回给前端展示[reference:4]2.2 父工程 POM 配置（版本管理） <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.0</version> <relativePath/> </parent> <groupId>com.sky</groupId> <artifactId>sky-take-out</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <java.version>1.8</java.version> <mybatis-plus.version>3.5.2</mybatis-plus.version> </properties> <dependencies> </dependencies>2.3 子模块依赖配置 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.sky</groupId> <artifactId>sky-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>2.4 启动类package com.sky; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.transaction.annotation.EnableTransactionManagement; @Slf4j @SpringBootApplication @EnableTransactionManagement // 开启事务管理 public class SkyApplication { public static void main(String[] args) { SpringApplication.run(SkyApplication.class, args); log.info("苍穹外卖后端服务启动成功！"); } }@SpringBootApplication 复合注解解析：注解作用@SpringBootConfiguration标识为配置类（@Configuration 的派生）@ComponentScan开启组件扫描，扫描启动类所在包及子包@EnableAutoConfiguration开启自动配置（核心）自动配置原理（5 步必背）：@SpringBootApplication → @EnableAutoConfiguration@Import(AutoConfigurationImportSelector.class) 导入选择器加载 META-INF/spring.factories（或 2.7 新路径 AutoConfiguration.imports）读取自动配置类的全限定类名通过 @ConditionalOnXxx 条件注解判断是否加载三、配置文件3.1 application.yml（苍穹外卖示例）server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 main: allow-circular-references: true # 允许循环依赖 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印 SQL 日志 global-config: db-config: id-type: auto # 主键自增 mapper-locations: classpath:mapper/*.xml sky: jwt: admin-secret-key: "itcast" admin-ttl: 7200000 admin-token-name: "token"3.2 多环境配置（必背）创建多个配置文件，通过 spring.profiles.active 激活：文件环境application.yml默认配置（公共）application-dev.yml开发环境application-prod.yml生产环境# application.yml（主配置） spring: profiles: active: dev # 激活开发环境 # application-dev.yml server: port: 8081 spring: config: activate: on-profile: dev # application-prod.yml server: port: 80 spring: config: activate: on-profile: prod激活方式：# 命令行激活 java -jar sky-server.jar --spring.profiles.active=prod # IDEA VM options -Dspring.profiles.active=prod四、分层架构（苍穹外卖实战）4.1 分层架构图解（外卖公司比喻）层级角色苍穹外卖示例Controller客服经理（接电话）接收 HTTP 请求，返回 JSON 响应Service业务主管（管流程）实现业务逻辑，调用 MapperMapper仓库管理员（管库存）执行 SQL，操作数据库Entity货物清单与数据库表一一对应的实体4.2 Entity 层（实体类）package com.sky.entity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.time.LocalDateTime; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Employee implements Serializable { private static final long serialVersionUID = 1L; private Long id; private String username; private String name; private String password; private String phone; private String sex; private String idNumber; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; private Long createUser; private Long updateUser; }4.3 Mapper 层（数据访问层）package com.sky.mapper; import com.sky.entity.Employee; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface EmployeeMapper { @Select("select * from employee where username = #{username}") Employee getByUsername(String username); // 分页查询员工 Page<Employee> pageQuery(EmployeePageQueryDTO pageQueryDTO); // 更新员工信息 void update(Employee employee); }MyBatis-Plus 增强版（推荐） ：// 继承 BaseMapper，自动拥有 CRUD 方法 @Mapper public interface DishMapper extends BaseMapper<Dish> { // 无需写任何基础 CRUD，BaseMapper 已提供： // insert(), deleteById(), updateById(), selectById(), selectList() 等 } // Service 层还可以继承 IService @Service public class DishService extends ServiceImpl<DishMapper, Dish> { // 自动拥有 saveBatch(), updateBatchById() 等批量方法 }4.4 Service 层（业务逻辑层）package com.sky.service.impl; import com.sky.context.BaseContext; import com.sky.dto.EmployeeDTO; import com.sky.entity.Employee; import com.sky.mapper.EmployeeMapper; import com.sky.service.EmployeeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.DigestUtils; @Slf4j @Service public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Override @Transactional // 开启事务 public void save(EmployeeDTO employeeDTO) { Employee employee = new Employee(); // 拷贝属性（DTO → Entity） BeanUtils.copyProperties(employeeDTO, employee); // 设置默认密码（MD5 加密） employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes())); employee.setStatus(1); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); // 从 ThreadLocal 获取当前操作用户 ID employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.insert(employee); } }4.5 Controller 层（控制层）package com.sky.controller.admin; import com.sky.dto.EmployeeDTO; import com.sky.dto.EmployeeLoginDTO; import com.sky.dto.EmployeePageQueryDTO; import com.sky.result.PageResult; import com.sky.result.Result; import com.sky.service.EmployeeService; import com.sky.vo.EmployeeLoginVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/admin/employee") @Api(tags = "员工管理接口") @Slf4j public class EmployeeController { @Autowired private EmployeeService employeeService; @PostMapping("/login") @ApiOperation("员工登录") public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO loginDTO) { log.info("员工登录：{}", loginDTO.getUsername()); EmployeeLoginVO employeeLoginVO = employeeService.login(loginDTO); return Result.success(employeeLoginVO); } @PostMapping @ApiOperation("新增员工") public Result save(@RequestBody EmployeeDTO employeeDTO) { log.info("新增员工：{}", employeeDTO); employeeService.save(employeeDTO); return Result.success(); } @GetMapping("/page") @ApiOperation("员工分页查询") public Result<PageResult> page(EmployeePageQueryDTO pageQueryDTO) { log.info("员工分页查询：{}", pageQueryDTO); PageResult pageResult = employeeService.pageQuery(pageQueryDTO); return Result.success(pageResult); } }4.6 分层调用链路总结HTTP 请求 ↓ Controller（接收参数，调用 Service） ↓ Service（业务逻辑，调用 Mapper） ↓ Mapper（执行 SQL，操作数据库） ↓ Entity（数据载体） ↓ 返回结果 → VO → JSON 响应⚠️ 常见错误：❌ Controller 直接调用 Mapper（越级）❌ Mapper 层写业务判断逻辑❌ Service 层写 SQL 拼接五、核心注解速查表5.1 组件注解注解用途层级@Component通用组件，交给 Spring 管理任意@ControllerWeb 控制器（配合 @ResponseBody 返回 JSON）表现层@RestController@Controller + @ResponseBody 的合并表现层@Service业务逻辑层组件业务层@Repository数据访问层组件，会转换持久层异常数据层@Repository 的特殊功能：将数据库操作抛出的异常自动转换为 Spring 统一的数据访问异常体系[reference:5]。5.2 请求映射注解注解作用@RequestMapping通用请求映射，可指定 method@GetMapping处理 GET 请求@PostMapping处理 POST 请求@PutMapping处理 PUT 请求@DeleteMapping处理 DELETE 请求5.3 参数绑定注解注解作用示例@PathVariable获取 URL 路径参数/user/{id} → @PathVariable Long id@RequestParam获取 URL 查询参数?name=张三 → @RequestParam String name@RequestBody获取请求体（JSON → 对象）@RequestBody EmployeeDTO dto@RequestHeader获取请求头@RequestHeader("token") String token5.4 依赖注入注解注解作用@Autowired按类型自动注入（Spring 原生）@Resource按名称自动注入（JSR-250）@Qualifier配合 @Autowired，指定 Bean 名称@Value注入配置文件中的值（如 ${server.port}）@ConfigurationProperties批量注入配置属性5.5 配置与条件注解注解作用@Configuration标识配置类@Bean在配置类中声明 Bean@ConditionalOnClass类路径存在指定类时才生效@ConditionalOnMissingBean容器中没有指定 Bean 时才生效@ConditionalOnProperty配置文件存在指定属性时才生效六、全局异常处理6.1 苍穹外卖全局异常处理器package com.sky.handler; import com.sky.constant.MessageConstant; import com.sky.exception.BaseException; import com.sky.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 捕获业务异常 @ExceptionHandler(BaseException.class) public Result exceptionHandler(BaseException ex) { log.error("业务异常：{}", ex.getMessage()); return Result.error(ex.getMessage()); } // 捕获 SQL 异常（如用户名重复） @ExceptionHandler(DuplicateKeyException.class) public Result handleDuplicateKeyException(DuplicateKeyException ex) { log.error("SQL 异常：{}", ex.getMessage()); return Result.error(MessageConstant.ALREADY_EXISTS); } // 兜底异常处理 @ExceptionHandler(Exception.class) public Result exceptionHandler(Exception ex) { log.error("系统异常：{}", ex.getMessage()); return Result.error(MessageConstant.UNKNOWN_ERROR); } }6.2 统一响应对象package com.sky.result; import lombok.Data; import java.io.Serializable; @Data public class Result<T> implements Serializable { private Integer code; // 1 成功，0 失败 private String msg; private T data; public static <T> Result<T> success() { Result<T> result = new Result<>(); result.code = 1; return result; } public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 1; result.data = data; return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.code = 0; result.msg = msg; return result; } }七、JWT 认证与拦截器7.1 JWT 工具类package com.sky.utils; import io.jsonwebtoken.*; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; import java.util.Map; @Data @Component @ConfigurationProperties(prefix = "sky.jwt") public class JwtUtil { private String adminSecretKey; private Long adminTtl; private String adminTokenName; // 生成 JWT 令牌 public String createJWT(Map<String, Object> claims, Long ttlMillis) { long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); JwtBuilder builder = Jwts.builder() .setClaims(claims) .setIssuedAt(now) .signWith(SignatureAlgorithm.HS256, adminSecretKey); if (ttlMillis > 0) { builder.setExpiration(new Date(nowMillis + ttlMillis)); } return builder.compact(); } // 解析 JWT 令牌 public Claims parseJWT(String token) { return Jwts.parser() .setSigningKey(adminSecretKey) .parseClaimsJws(token) .getBody(); } }7.2 JWT 认证拦截器package com.sky.interceptor; import com.sky.constant.JwtClaimsConstant; import com.sky.context.BaseContext; import com.sky.properties.JwtProperties; import com.sky.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 判断当前拦截的是否为 Controller 方法 if (!(handler instanceof HandlerMethod)) { return true; } // 从请求头中获取令牌 String token = request.getHeader(jwtUtil.getAdminTokenName()); // 解析令牌 try { Claims claims = jwtUtil.parseJWT(token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); // 将员工 ID 存入 ThreadLocal（供后续业务使用） BaseContext.setCurrentId(empId); log.info("当前员工 ID：{}", empId); return true; } catch (Exception ex) { log.error("JWT 解析失败：{}", ex.getMessage()); response.setStatus(401); return false; } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后清除 ThreadLocal，防止内存泄漏 BaseContext.removeCurrentId(); } }7.3 ThreadLocal 工具类（苍穹外卖）package com.sky.context; public class BaseContext { private static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } }ThreadLocal 原理：每个线程拥有自己的变量副本，线程隔离。用完必须 remove()，否则在线程池环境下会导致内存泄漏[reference:6]。7.4 注册拦截器package com.sky.config; import com.sky.interceptor.JwtTokenAdminInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**") // 拦截所有 /admin 开头的请求 .excludePathPatterns("/admin/employee/login") // 排除登录接口 .excludePathPatterns("/admin/employee/logout"); } }八、AOP 切面编程（公共字段自动填充）8.1 自定义注解package com.sky.annotation; import com.sky.enumeration.OperationType; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { OperationType value(); // UPDATE 或 INSERT }8.2 切面类实现package com.sky.aspect; import com.sky.annotation.AutoFill; import com.sky.constant.AutoFillConstant; import com.sky.context.BaseContext; import com.sky.enumeration.OperationType; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.time.LocalDateTime; @Aspect @Component @Slf4j public class AutoFillAspect { // 切点：标注了 @AutoFill 注解的方法 @Pointcut("@annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut() {} @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint) { // 获取当前被拦截的方法上的注解 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); OperationType operationType = autoFill.value(); // 获取方法的参数（实体对象） Object[] args = joinPoint.getArgs(); if (args == null || args.length == 0) return; Object entity = args[0]; // 准备要赋的值 LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); // 根据操作类型设置不同字段 if (operationType == OperationType.INSERT) { // 插入操作：设置 createTime, updateTime, createUser, updateUser try { Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setCreateTime.invoke(entity, now); setUpdateTime.invoke(entity, now); setCreateUser.invoke(entity, currentId); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { log.error("公共字段填充失败：{}", e.getMessage()); } } else if (operationType == OperationType.UPDATE) { // 更新操作：设置 updateTime, updateUser try { Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setUpdateTime.invoke(entity, now); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { log.error("公共字段填充失败：{}", e.getMessage()); } } } }8.3 使用示例@Mapper public interface EmployeeMapper { @AutoFill(OperationType.INSERT) void insert(Employee employee); @AutoFill(OperationType.UPDATE) void update(Employee employee); }九、缓存与 Redis9.1 Spring Cache 配置package com.sky.config; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @Configuration @EnableCaching public class RedisConfig { @Bean public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) // 缓存过期时间 30 分钟 .disableCachingNullValues() // 不缓存 null 值 .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())); } }9.2 缓存注解使用@Service @Slf4j public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; // 查询时缓存：key = "dish:categoryId:" + categoryId @Cacheable(value = "dish", key = "'dish:categoryId:' + #categoryId") public List<Dish> getByCategoryId(Long categoryId) { return dishMapper.getByCategoryId(categoryId); } // 更新时清空缓存 @CacheEvict(value = "dish", key = "'dish:categoryId:' + #dishDTO.categoryId") public void updateWithFlavor(DishDTO dishDTO) { // 更新业务逻辑 } // 批量清空缓存（key 模糊匹配） @CacheEvict(value = "dish", allEntries = true) public void deleteBatch(List<Long> ids) { // 删除业务逻辑 } }9.3 常用 Redis 操作（StringRedisTemplate）@Service public class ShopStatusService { @Autowired private StringRedisTemplate redisTemplate; private static final String SHOP_STATUS_KEY = "SHOP_STATUS"; public void setStatus(Integer status) { redisTemplate.opsForValue().set(SHOP_STATUS_KEY, String.valueOf(status)); } public Integer getStatus() { String status = redisTemplate.opsForValue().get(SHOP_STATUS_KEY); return status == null ? 0 : Integer.parseInt(status); } }十、Spring Task 定时任务10.1 开启定时任务@SpringBootApplication @EnableScheduling // 开启定时任务 public class SkyApplication { public static void main(String[] args) { SpringApplication.run(SkyApplication.class, args); } }10.2 订单状态定时处理package com.sky.task; import com.sky.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component @Slf4j public class OrderTask { @Autowired private OrderService orderService; // 每天凌晨 1 点处理超时未支付的订单 @Scheduled(cron = "0 0 1 * * ?") public void processTimeoutOrder() { log.info("定时任务：处理超时未支付订单"); orderService.processTimeoutOrders(); } // 每分钟执行一次，处理配送中的订单（送达后自动完成） @Scheduled(cron = "0 * * * * ?") public void processDeliveryOrder() { log.info("定时任务：处理配送中订单"); orderService.processDeliveryOrders(); } }10.3 Cron 表达式速查字段允许值特殊字符秒0-59, - * /分0-59, - * /时0-23, - * /日1-31, - * ? / L W月1-12, - * /周0-7（0 或 7 都代表周日）, - * ? / L #年（可选）空或 1970-2099, - * /常用表达式：0 0 1 * * ? → 每天凌晨 1 点0 */5 * * * ? → 每 5 分钟一次0 0 12 * * MON-FRI → 工作日中午 12 点十一、WebSocket 实时通信11.1 WebSocket 配置package com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }11.2 WebSocket 服务端package com.sky.websocket; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.concurrent.ConcurrentHashMap; @Component @ServerEndpoint("/ws/{sid}") @Slf4j public class WebSocketServer { // 存储所有在线会话（线程安全） private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { sessionMap.put(sid, session); log.info("WebSocket 连接建立：{}", sid); } @OnClose public void onClose(@PathParam("sid") String sid) { sessionMap.remove(sid); log.info("WebSocket 连接关闭：{}", sid); } @OnMessage public void onMessage(String message, Session session) { log.info("收到消息：{}", message); } @OnError public void onError(Session session, Throwable error) { log.error("WebSocket 错误：{}", error.getMessage()); } // 发送消息给指定用户 public void sendToClient(String sid, String message) { Session session = sessionMap.get(sid); if (session != null && session.isOpen()) { try { session.getBasicRemote().sendText(message); log.info("发送消息给 {}：{}", sid, message); } catch (Exception e) { log.error("发送失败：{}", e.getMessage()); } } } }11.3 来单提醒/催单场景@Service public class OrderServiceImpl implements OrderService { @Autowired private WebSocketServer webSocketServer; // 用户下单 → 来单提醒（推送给管理端） public void submit(OrdersSubmitDTO submitDTO) { // ... 下单业务逻辑 // 来单提醒 Map<String, Object> reminder = new HashMap<>(); reminder.put("type", 1); // 1=来单提醒 reminder.put("orderId", order.getId()); reminder.put("content", "您有一笔新订单"); webSocketServer.sendToClient("admin", JSON.toJSONString(reminder)); } // 用户催单 → 推送给管理端 public void reminder(Long orderId) { Map<String, Object> reminder = new HashMap<>(); reminder.put("type", 2); // 2=用户催单 reminder.put("orderId", orderId); reminder.put("content", "用户催单啦"); webSocketServer.sendToClient("admin", JSON.toJSONString(reminder)); } }十二、文件上传（阿里云 OSS）12.1 OSS 配置属性类package com.sky.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "sky.alioss") public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }12.2 OSS 工具类package com.sky.utils; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.sky.properties.AliOssProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.UUID; @Component @Slf4j public class AliOssUtil { @Autowired private AliOssProperties aliOssProperties; public String upload(MultipartFile file) { String endpoint = aliOssProperties.getEndpoint(); String accessKeyId = aliOssProperties.getAccessKeyId(); String accessKeySecret = aliOssProperties.getAccessKeySecret(); String bucketName = aliOssProperties.getBucketName(); // 生成唯一文件名（防止覆盖） String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); String objectName = UUID.randomUUID().toString() + extension; OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { ossClient.putObject(bucketName, objectName, file.getInputStream()); // 返回文件访问 URL return "https://" + bucketName + "." + endpoint + "/" + objectName; } catch (IOException e) { log.error("文件上传失败：{}", e.getMessage()); throw new RuntimeException(e); } finally { ossClient.shutdown(); } } }12.3 通用上传接口@RestController @RequestMapping("/admin/common") @Api(tags = "通用接口") @Slf4j public class CommonController { @Autowired private AliOssUtil aliOssUtil; @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file) { log.info("文件上传：{}", file.getOriginalFilename()); String url = aliOssUtil.upload(file); return Result.success(url); } }十三、Knife4j + Swagger 接口文档13.1 添加依赖<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>13.2 Swagger 配置类package com.sky.config; import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 @EnableKnife4j public class SwaggerConfig { @Bean public Docket adminApi() { return new Docket(DocumentationType.SWAGGER_2) .groupName("管理端接口") .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin")) .paths(PathSelectors.any()) .build(); } @Bean public Docket userApi() { return new Docket(DocumentationType.SWAGGER_2) .groupName("用户端接口") .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("苍穹外卖 API 文档") .version("1.0") .description("苍穹外卖后端接口文档") .build(); } }13.3 使用示例@Api(tags = "员工管理接口") @RestController @RequestMapping("/admin/employee") public class EmployeeController { @PostMapping("/login") @ApiOperation("员工登录") @ApiImplicitParams({ @ApiImplicitParam(name = "username", value = "用户名", required = true), @ApiImplicitParam(name = "password", value = "密码", required = true) }) public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO loginDTO) { // ... } }访问地址：http://localhost:8080/doc.html十四、常用工具类速查14.1 BeanUtils（属性拷贝）// DTO → Entity Employee employee = new Employee(); BeanUtils.copyProperties(employeeDTO, employee);14.2 DigestUtils（MD5 加密）// 密码 MD5 加密 String encryptedPassword = DigestUtils.md5DigestAsHex("123456".getBytes());14.3 Jackson（JSON 序列化）// 对象 → JSON 字符串 String jsonStr = JSON.toJSONString(object); // JSON 字符串 → 对象 User user = JSON.parseObject(jsonStr, User.class);14.4 LocalDateTime（时间处理）// 获取当前时间 LocalDateTime now = LocalDateTime.now(); // 时间格式化 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formatted = now.format(formatter);十五、项目部署与运行15.1 打包# 在项目根目录执行 mvn clean package # 跳过测试打包 mvn clean package -DskipTests15.2 运行# 运行 JAR 包 java -jar sky-server.jar # 指定环境运行 java -jar sky-server.jar --spring.profiles.active=prod # 后台运行 nohup java -jar sky-server.jar > app.log 2>&1 &15.3 Nginx 反向代理配置server { listen 80; server_name api.sky.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }十六、快速记忆口诀启动流程SpringApplication.run → 初始化上下文 → 加载自动配置 → 启动内嵌容器 → 注册 Controller → 应用就绪自动配置@SpringBootApplication → @EnableAutoConfiguration → @Import → spring.factories → @ConditionalOnXxx分层架构Controller 接请求，Service 写逻辑，Mapper 查数据库，Entity 存数据JWT 认证登录签发 Token → 请求携带 Header → 拦截器校验 → ThreadLocal 存用户 ID → 业务使用 → 请求结束清除缓存注解@Cacheable 查缓存，@CachePut 更新缓存，@CacheEvict 清缓存定时任务@EnableScheduling 开启，@Scheduled(cron) 标记，Cron 表达式定时间全局异常@RestControllerAdvice 全局处理，@ExceptionHandler 分类捕获本笔记基于 Spring Boot 2.7 + 苍穹外卖实战项目整理，覆盖了从项目搭建到部署上线的完整流程，适合日常开发查阅和面试前背诵。建议结合源码实践加深理解。