一、代码示例1-1、前端代码index.html页面转账页面body form action/bank/transfer methodpost 转出账户input typetext namefromActnobr 转入账户input typetext nametoActnobr 转账金额input typetext namemoneybr input typesubmit value转账 /form /body一个success.html和三个error.htmlbody h1转账成功!!!!/h1 /bodybody h1未知异常/h1 /bodybody h1转账失败!!!!/h1 /bodybody h1余额不足!!!!/h1 /body1-2、后台代码0、工具类/** * mybatis工具类 */ public class SqlSessionUtils { private static SqlSessionFactory sqlSessionFactory; // 工具类中的构造方法都是private目的防止创造对象 private SqlSessionUtils(){ } // 类加载时执行 // SqlSessionUtils工具类在进行第一次加载的时候解析mybatis-config.xml文件创建sqlSessionFactory对象 static { try { sqlSessionFactory new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(mybatis-config.xml)); } catch (IOException e) { System.err.println(读取 mybatis-config.xml 出错了); e.printStackTrace(); } } // 全局的服务器级别的一个服务器当中定义一个即可 private static ThreadLocalSqlSession local new ThreadLocal(); // 工具类中所有的方法都是静态的目的直接采用类名即可调用不用new public static SqlSession openSession(){ SqlSession sqlSession local.get(); if (sqlSession null){ sqlSession sqlSessionFactory.openSession(); // 将sqlsession对象绑定到当前线程上 local.set(sqlSession); } return sqlSession; } /** * 关闭SqlSession对象从当前线程中移除SqlSession对象 */ public static void close(SqlSession sqlSession){ if (sqlSession ! null){ sqlSession.close(); // 注意移除SqlSession对象和当前线程的绑定关系 // 因为Tomcat服务器支持线程池也就是说用过的线程对象t1可能下一次还会使用这个t1线程 local.remove(); } } }1、从mybatis-config.xml配置文件中获取sqlSessionFactory2、再从sqlSessionFactory中获取 sqlSession打开一个新的会话可以理解为开启一次数据库访问类本身单例工具类为什么是单例构造器是private SqlSessionUtils()外部无法new内部sqlSessionFactory是static整个 JVM 只有一份方法都是static不需要实例化直接SqlSessionUtils.openSession()调用。意味着这个类在 JVM 里就只有一份不管有多少线程它们调用的都是同一个SqlSessionUtils。内部的 ThreadLocal让每个线程有独立 SqlSessionprivate static ThreadLocalSqlSession local new ThreadLocal();ThreadLocal是静态的也只有一份但每个线程在这个 ThreadLocal 上存的值互相独立线程 A 调用openSession()时先get()为null→ 创建一个SqlSession→ 存到local线程 B 调用时也会得到自己的SqlSession线程 A 和线程 B 的SqlSession不会互相干扰。这就是常见的「单例 ThreadLocal」组合类本身是单例全局工具类内部用 ThreadLocal 让每个线程隔离各自的状态。为什么这样设计Web/服务器场景在 Tomcat 这类容器中请求会被分配到线程池里的线程处理每个线程处理自己的数据库会话SqlSession如果直接用一个全局唯一的SqlSession多线程访问会产生线程安全问题所以用 ThreadLocal 保证同一个请求/线程共享同一个SqlSession线程之间互不干扰同时提供close()方法做local.remove()防止线程池复用时数据泄漏。【回顾】SqlSession类SqlSessionFactory是一个线程安全的单例对象用来创建 SqlSession 的工厂就像数据库的连接池工厂。SqlSession 是什么是 MyBatis 操作数据库的核心接口相当于你和数据库沟通的“会话对象”。你可以用sqlSession做这些事操作方法执行查询selectOne(),selectList()执行插入insert()执行更新update()执行删除delete()获取 Mapper 接口代理对象sqlSession.getMapper(YourMapper.class)控制事务如果不是自动提交commit(),rollback()【注意点】如果你用的是手动事务默认是要记得手动commit()或rollback()。最后一定要sqlSession.close()否则会占用数据库连接资源。1、controller处理类Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String fromActno request.getParameter(fromActno); String toActno request.getParameter(toActno); Double money Double.parseDouble(request.getParameter(money)); // 调用service方法完成转账 try { accountService.transfer(fromActno, toActno, money); // 程序走到这里说明转账成功 // 调用view完成展示结果 response.sendRedirect(request.getContextPath() /success.html); } catch (MoneyNotEnoughException e) { e.printStackTrace(); response.sendRedirect(request.getContextPath() /error2.html); } catch (TransferException e) { e.printStackTrace(); response.sendRedirect(request.getContextPath() /error1.html); }catch (Exception e){ response.sendRedirect(request.getContextPath() /error.html); } }【注意点】request.getContextPath()讲解这是 Servlet/JSP 里一个很常见的用法。返回当前 Web 应用的上下文路径Context Path。通俗理解就是你部署这个 Web 应用时的「根路径」。访问地址getContextPath()返回http://localhost:8080/myapp/index.jsp/myapphttp://localhost:8080/应用部署在根目录空字符串http://localhost:8080/shop/cart/shopresponse.sendRedirect(...)告诉浏览器请重新去请求一个新的 URL返回 302重定向响应。浏览器接到后会自动跳转到指定的地址。2、业务层service类public class AccountServiceImpl implements AccountService { private AccountDao accountDao new AccountDaoImpl(); Override public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException { // 添加事务控制代码 // 确保不同的dao拿到的是同一个sqlSession SqlSession sqlSession SqlSessionUtils.openSession(); // 1、判断装出账户的余额是否充足 Account fromAct accountDao.selectByActno(fromActno); Double balance fromAct.getBalance(); if(money balance){ // 2、若是余额不足提示用户 throw new MoneyNotEnoughException(对不起余额不足); } // 3、若是转出账户余额充足更新转出账户余额 Account toAct accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() money); int count accountDao.updateByActno(fromAct); // 模拟异常判断事务 String s null; s.toString(); // 4、更新转入账户的余额 count accountDao.updateByActno(toAct); if (count ! 2) { // 转账失败抛出异常 throw new TransferException(转账失败); } // 提交事务 sqlSession.commit(); // 提交和关闭事务 SqlSessionUtils.close(sqlSession); } }【注意点1】【注意点2】在service层实现事务的控制因为dao层只是单纯的数据库操作代码尽可能的少一个dao层的方法对应一个sql操作。3、dao层数据库处理public class AccountDaoImpl implements AccountDao { Override public Account selectByActno(String actno) { SqlSession sqlSession SqlSessionUtils.openSession(); Account selectByActno (Account) sqlSession.selectOne(selectByActno, actno); return selectByActno; } Override public int updateByActno(Account act) { SqlSession sqlSession SqlSessionUtils.openSession(); int count sqlSession.update(updateByActno, act); return count; } }【注意】一般用mybatis的话包名不叫dao而是mapper在 MyBatis 的语境下DAO 层和 Mapper 层是同一个东西只是叫法不同。dao层的接口也叫xxxMapper二、注意点说明1、controller层service层和dao层之间对象的注入使用接口传递2、要确保同一个事物之间多个不同的dao使用的是一个SqlSession这样才能回滚成功使用ThreadLocal实现3、事务的控制要在service层三、ThreadLocal讲解ThreadLocal是 Java 中的一个类主要作用是为每个线程提供独立的变量副本。也就是说多个线程访问同一个ThreadLocal对象时彼此之间是隔离的互不干扰。ThreadLocal就像是一个线程的私有小抽屉每个线程只能访问自己抽屉里的数据。一句话记忆ThreadLocal 线程级别的全局变量同一线程内处处可访问线程之间互不干扰。用完后一定要remove()否则线程池环境下会内存泄漏。3-1、使用场景举例1. 每个线程都要保存自己的用户信息public class UserContext { private static ThreadLocalString currentUser new ThreadLocal(); public static void setUser(String user) { currentUser.set(user); } public static String getUser() { return currentUser.get(); } }如果两个线程分别执行UserContext.setUser(Alice); UserContext.setUser(Bob);各自线程中调用UserContext.getUser()返回的将是各自的Alice或Bob不会互相影响。2.多线程中的数据库连接每个线程一份 Connectionprivate static final ThreadLocalConnection connHolder new ThreadLocal(); public Connection getConnection() { Connection conn connHolder.get(); if (conn null) { conn DriverManager.getConnection(url, user, pwd); connHolder.set(conn); } return conn; // 保证同一线程内获取的是同一个连接 }3. SimpleDateFormat 线程安全// SimpleDateFormat 不是线程安全的但用 ThreadLocal 可以安全复用 private static final ThreadLocalSimpleDateFormat dateFormat ThreadLocal.withInitial(() - new SimpleDateFormat(yyyy-MM-dd)); public String format(Date date) { return dateFormat.get().format(date); // 每个线程有自己的实例 }核心点SimpleDateFormat不是只读地查一下时间它在 format/parse 的过程中会反复修改自己内部的 Calendar 对象。这个修改过程不是瞬间完成的中间会被其他线程插入。现代 Java 更推荐的做法如果你用的是Java 8 或更高版本现在更推荐用DateTimeFormatter它是线程安全的不需要 ThreadLocal// Java 8 推荐写法 private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd); public String format(LocalDate date) { return date.format(FORMATTER); // 直接共享完全线程安全 }但如果是维护老项目或者必须用SimpleDateFormat你图中的ThreadLocal写法就是标准做法。3-2、注意事项ThreadLocal 使用完要 remove()否则会发生内存泄漏尤其是在使用线程池的场景下。try { threadLocal.set(some value); // 使用 threadLocal 的值 } finally { threadLocal.remove(); // 清理 }ThreadLocal 是基于当前线程Thread对象内部的一个ThreadLocalMap存储的。3-3、总结要点内容类名java.lang.ThreadLocalT作用为每个线程提供独立变量副本常用方法.set(T value)、.get()、.remove()常见用途登录用户上下文、SimpleDateFormat、数据库连接等线程隔离变量必须清除是线程池中尤其要注意调用remove()3-4、与new对象的核心区别问题我每次new一个对象不也能实现隔离吗那为什么还要用ThreadLocal呢方式变量作用域线程隔离性使用便捷性生命周期绑定new普通对象每次创建新对象变量归属对象是对象不共享需要自己传来传去和对象绑定ThreadLocal同一个对象内部按线程维度自动隔离变量副本是每个线程自动独立全局共享对象线程自动隔离和线程绑定打个比方来解释你可以想象你在写一个网站用户访问一个页面时会在后台起一个线程来处理。你用private static ThreadLocalString currentUser new ThreadLocal();这个currentUser是一个全局的工具变量但它背后是为每个线程“自动”分配了一个独立的副本你不需要手动传来传去。而如果你new User(Alice)你确实可以做到隔离但你得自己把这个User对象一层层传入你程序的每个方法比如Controller → Service → DAO很麻烦。而ThreadLocal可以让你在任何地方用静态方法就获取到当前线程自己的“用户”就像你随时从自己口袋掏出东西一样其他人拿不到。3-5、对比代码来看效果❌ 普通对象传递你得传来传去public void controller(User user) { service(user); } public void service(User user) { dao(user); } public void dao(User user) { System.out.println(user.getName()); }✅ ThreadLocal不需要一层层传public class UserContext { private static ThreadLocalString currentUser new ThreadLocal(); public static void set(String user) { currentUser.set(user); } public static String get() { return currentUser.get(); } public static void clear() { currentUser.remove(); } }使用UserContext.set(Alice); // 后续在任何方法里都能获取当前线程自己的用户 String user UserContext.get();3-6、ThreadLocal 的最大价值你不需要手动传值框架/工具类能全局访问不会线程共享线程自动隔离适合做“与当前线程相关的上下文”3-7、经典应用场景当前登录用户信息Spring Security 就是这么做的每线程一个SimpleDateFormat每线程一个数据库连接在某些 ORM 框架中3-8、底层实现原理Java 的每个Thread对象内部都有一个ThreadLocalMap// Thread 类中的源码简化版 class Thread { ThreadLocal.ThreadLocalMap threadLocals null; }当你调用threadLocal.set(value)时实际上是以ThreadLocal对象为key把值存进了当前线程自己的ThreadLocalMap中。线程 A 的内存: threadLocals { ThreadLocal1a2b3c → 用户A的会话, ThreadLocal4d5e6f → 数据库连接A } 线程 B 的内存: threadLocals { ThreadLocal1a2b3c → 用户B的会话, // 同一个 ThreadLocal 对象 ThreadLocal4d5e6f → 数据库连接B }关键点同一个ThreadLocal实例在不同线程的 map 中对应不同的 value。【注意】这里的 key 不是线程的 key而是ThreadLocalMap里的 key。1、拆解结构// 假设有两个 ThreadLocal 静态变量 // 对象 1a2b3c private static final ThreadLocalUser USER_CONTEXT new ThreadLocal(); // 对象 4d5e6f private static final ThreadLocalConnection CONN_HOLDER new ThreadLocal();线程 A 的内存Thread-A { threadLocals: ThreadLocalMap { 条目1: { key: ThreadLocal1a2b3c(弱引用), value: User对象(用户A) }, 条目2: { key: ThreadLocal4d5e6f(弱引用), value: Connection对象(连接A) } } }线程 B 的内存Thread-B { threadLocals: ThreadLocalMap { 条目1: { key: ThreadLocal1a2b3c(弱引用), value: User对象(用户B) }, 条目2: { key: ThreadLocal4d5e6f(弱引用), value: Connection对象(连接B) } } }2、关键理解层级说明ThreadLocal1a2b3c全局唯一的静态对象存在堆中所有线程共享这个引用Thread-A.threadLocals线程 A私有的 MapThread-B.threadLocals线程 B私有的 Map同一个ThreadLocal对象作为 key 出现在不同线程的私有 Map 中对应不同的 value。3-9、内存泄漏问题这是ThreadLocal最容易踩的坑1、为什么会泄漏ThreadLocalMap的 key 是弱引用WeakReference的ThreadLocal对象// ThreadLocalMap 中的 Entry 结构 static class Entry extends WeakReferenceThreadLocal? { Object value; // 强引用 Entry(ThreadLocal? k, Object v) { super(k); // key 是弱引用 value v; // value 是强引用 } }问题场景线程 A 执行完任务但线程池复用了该线程线程不会销毁ThreadLocal变量在外部不再被强引用GC 回收了弱引用的 keyThreadLocal对象但ThreadLocalMap中的 Entry 还在value 是强引用无法回收结果value 永远滞留在内存中造成泄漏2、解决方案用完必须 remove()try { threadLocal.set(value); // 业务逻辑... } finally { threadLocal.remove(); // 必须清理 }3、现代替代方案InheritableThreadLocal TransmittableThreadLocal1InheritableThreadLocal— 子线程继承父线程的值// 子线程可以获取父线程设置的值 InheritableThreadLocalString inheritable new InheritableThreadLocal(); inheritable.set(父线程的值); new Thread(() - { System.out.println(inheritable.get()); // 输出父线程的值 }).start();局限只适用于直接创建new Thread()对线程池无效线程池的线程不是新建的。2TransmittableThreadLocal阿里开源线程池场景下的完美解决方案需要引入transmittable-thread-local依赖TransmittableThreadLocalString context new TransmittableThreadLocal(); // 包装线程池 ExecutorService ttlExecutor TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); context.set(主线程的值); ttlExecutor.submit(() - { System.out.println(context.get()); // 能正确获取主线程的值 });总结对比特性ThreadLocalInheritableThreadLocalTransmittableThreadLocal线程隔离✅✅✅子线程继承❌✅仅new Thread✅支持线程池线程池支持✅❌✅需手动remove✅✅✅3-10、ThreadLocal的标准定义方式private static final是ThreadLocal的标准定义方式。标准写法public class UserContext { private static final ThreadLocalUser CURRENT_USER new ThreadLocal(); }为什么要static核心原因确保所有线程共享同一个ThreadLocal实例ThreadLocal的隔离机制是以实例为维度的。如果不用static每个对象都有自己的ThreadLocal实例就会破坏设计意图// ❌ 错误非 static public class UserContext { private final ThreadLocalUser currentUser new ThreadLocal(); // 每次 new UserContext() 都会创建新的 ThreadLocal 实例 // 线程 A 在实例1里 set在实例2里 get → 拿到 null }// ✅ 正确static public class UserContext { private static final ThreadLocalUser currentUser new ThreadLocal(); // 全局唯一实例所有线程都用它作为 key // 线程 A 的 currentUser.set() 和 currentUser.get() 操作的是同一个 key }原理回顾ThreadLocalMap是以ThreadLocal对象本身作为 key 的。如果 key 不唯一就找不到对应的 value。为什么要private防止外部直接操作通过封装的方法控制访问public class UserContext { private static final ThreadLocalUser CURRENT_USER new ThreadLocal(); public static void set(User user) { CURRENT_USER.set(user); } public static User get() { return CURRENT_USER.get(); } public static void remove() { CURRENT_USER.remove(); } }这样外部只能UserContext.get()不能直接UserContext.CURRENT_USER.set(...)避免误用。为什么要final防止引用被重新赋值CURRENT_USER new ThreadLocal()保证 key 的稳定性一旦赋值不可更改语义明确这是一个常量引用特殊情况不用 static 的场景只有一种情况不需要static当ThreadLocal本身就是实例的一部分且每个实例需要独立的线程隔离空间。// 例如每个数据库连接池实例有自己的线程隔离 public class ConnectionPool { // 每个 ConnectionPool 实例有独立的线程隔离 private final ThreadLocalConnection connectionHolder new ThreadLocal(); public Connection getConnection() { Connection conn connectionHolder.get(); if (conn null) { conn createConnection(); connectionHolder.set(conn); } return conn; } }但这种情况很少见绝大多数场景用户上下文、事务管理、日志追踪等都是全局唯一的ThreadLocal。总结修饰符作用是否必须private封装防止外部直接操作强烈建议static确保全局唯一 key线程隔离正确工作必须final防止引用被篡改语义明确强烈建议所以标准就是private static final ThreadLocalT NAME new ThreadLocal();四、作用域Scope和生命周期4-1、SqlSessionFactoryBuilder这个类可以被实例化、使用和丢弃一旦创建了 SqlSessionFactory就不再需要它了。因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域也就是局部方法变量。4-2、SqlSessionFactorySqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在没有任何理由丢弃它或重新创建另一个实例。即一个数据库一个SqlSessionFactory使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到最简单的就是使用单例模式或者静态单例模式。4-3、SqlSession每个线程都应该有它自己的SqlSession 实例。SqlSession 的实例不是线程安全的因此是不能被共享的所以它的最佳的作用域是请求或方法作用域。绝对不能将 SqlSession 实例的引用放在一个类的静态域甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中比如 Servlet 框架中的 HttpSession。如果你现在正在使用一种 Web 框架考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说每次收到 HTTP 请求就可以打开一个 SqlSession返回一个响应后就关闭它。这个关闭操作很重要为了确保每次都能执行关闭操作你应该把这个关闭操作放到 finally 块中。将SqlSession与ThreadLocal绑定确保一个线程一个SqlSession实例。五、使用javassist生成类※观察DAO的实现类发现没有业务逻辑代码比较少而且比较固定就是简单的增删改查所以可以使用 javassist动态生成类不用自己手动写Javassist是 Java 中一个非常强大的字节码操作库可以用来在运行时动态生成类、修改已有类的方法、字段等。名字是 Java Assist辅助的合成词。5-1、Javassist 能做什么动态生成一个类Class给类添加字段、方法、构造器修改已有类的方法体比如用于 AOP生成字节码文件.classmybatis底层也是用的Javassist5-2、基本使用步骤以动态创建类为例假设我们要生成一个名为com.example.Person的类包含一个字段name和一个sayHello()方法。1、Step 1引入依赖以 Maven 为例dependency groupIdorg.javassist/groupId artifactIdjavassist/artifactId version3.29.2-GA/version /dependencyStep 2示例代码 — 动态生成类import javassist.*; public class JavassistDemo { public static void main(String[] args) throws Exception { ClassPool pool ClassPool.getDefault(); // 1. 获取类池 CtClass ctClass pool.makeClass(com.example.Person); // 2. 创建一个类名 // 3. 添加字段 private String name; CtField nameField new CtField(pool.get(java.lang.String), name, ctClass); nameField.setModifiers(Modifier.PRIVATE); ctClass.addField(nameField); // 4. 添加 getName 和 setName 方法 ctClass.addMethod(CtNewMethod.getter(getName, nameField)); ctClass.addMethod(CtNewMethod.setter(setName, nameField)); // 5. 添加一个 sayHello 方法 CtMethod sayHello CtNewMethod.make( public void sayHello() { System.out.println(\Hello, my name is \ this.name); }, ctClass ); ctClass.addMethod(sayHello); // 6. 写出class文件可选 ctClass.writeFile(output); // 输出目录 // 7. 加载到当前 JVM 并使用 Class? clazz ctClass.toClass(); Object obj clazz.newInstance(); // 使用反射调用方法 clazz.getMethod(setName, String.class).invoke(obj, Alice); clazz.getMethod(sayHello).invoke(obj); // 输出Hello, my name is Alice } }执行后你会得到一个类Personpublic class Person { private String name; public String getName() { return name; } public void setName(String name) { this.name name; } public void sayHello() { System.out.println(Hello, my name is this.name); } }代码说明步骤操作说明ClassPool.getDefault()获取类池管理要操作/创建的类makeClass(String name)创建类动态定义类名CtField创建字段包含类型、字段名、所属类等信息CtMethod创建方法可以自己写字符串或用辅助方法toClass()加载类返回 Java 的Class?对象writeFile()写文件可将生成的.class文件保存到硬盘5-3、注意事项Javassist 操作的是字节码层面运行时效率比反射高但调试较困难。不支持 lambda、泛型等高级语法可以手动拼接源码字符串来实现。虽然可以用 Javassist 自动生成简单的 CRUD 类但写起来比直接敲几行 Java 代码还麻烦因此一般不推荐自己去造轮子。Javassist 适合框架开发者做动态增强业务层 CRUD 直接写类或用现成代码生成器更简单。5-4、mybatis动态生成dao接口的实现类MyBatis 的一个核心功能就是可以自动、动态地为你生成DAO 接口的实现类代理类dao接口的代理原理就是Javassist。使用mybatis代理机制的前提1、XXXMapper.xml文件中的namespace必须是dao接口的全限定名称2、id必须是dao接口的方法名。使用方法你只需要写Mapper 接口DAO 接口和对应的XML或注解MyBatis 会在运行时自动帮你生成接口的实现类。本质就是sqlSession.getMapper(Dao接口.class);它就动态生成了Dao接口的一个代理对象实现类。