首页
壁纸
统计
友链
Search
1
Vue2详细笔记
68 阅读
2
ExpressAPI
34 阅读
3
微信小程序
31 阅读
4
Spring MVC 请求生命周期
30 阅读
5
Nestjs概述-中文
29 阅读
Nodejs
Vue
Java
Msql
登录
Search
Wasnl
累计撰写
40
篇文章
累计收到
1
条评论
首页
栏目
Nodejs
Vue
Java
Msql
页面
壁纸
统计
友链
搜索到
9
篇与
的结果
2026-06-03
反射 + AOP + 注解
反射 + 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,然后 读取自定义注解,从而执行相应逻辑——这。
2026年06月03日
15 阅读
0 评论
1 点赞
2026-06-03
Spring JDBC + JdbcTemplate
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 操作特殊需求、存储过程---。
2026年06月03日
12 阅读
0 评论
1 点赞
2026-06-02
未命名文档
注意: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 名称,注意避免重名(同一类型不同方法名)。
2026年06月02日
5 阅读
0 评论
1 点赞
2026-04-03
SpringBoot
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 配置(版本管理)<!-- sky-take-out/pom.xml --> <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 子模块依赖配置<!-- sky-server/pom.xml --> <dependencies> <!-- Spring Boot Web Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus Starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Redis --> <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 + 苍穹外卖实战项目整理,覆盖了从项目搭建到部署上线的完整流程,适合日常开发查阅和面试前背诵。建议结合源码实践加深理解。
2026年04月03日
23 阅读
0 评论
0 点赞
1
2