MyBatis03-一个完整的bank transfer MVC框架的代码
一、代码示例
1-1、前端代码
index.html页面(转账页面)
<body> <form action="/bank/transfer" method="post"> 转出账户:<input type="text" name="fromActno"><br> 转入账户:<input type="text" name="toActno"><br> 转账金额:<input type="text" name="money"><br> <input type="submit" value="转账"> </form> </body>一个success.html和三个error.html
<body> <h1>转账成功!!!!</h1> </body><body> <h1>未知异常</h1> </body><body> <h1>转账失败!!!!</h1> </body><body> <h1>余额不足!!!!</h1> </body>1-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 ThreadLocal<SqlSession> 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配置文件中获取sqlSessionFactory
2、再从sqlSessionFactory中获取 sqlSession:打开一个新的会话(可以理解为开启一次数据库访问)
类本身:单例工具类
为什么是单例
构造器是
private SqlSessionUtils(),外部无法new;内部
sqlSessionFactory是static,整个 JVM 只有一份;方法都是
static,不需要实例化,直接SqlSessionUtils.openSession()调用。
意味着:
这个类在 JVM 里就只有一份,不管有多少线程,它们调用的都是同一个SqlSessionUtils。
内部的 ThreadLocal:让每个线程有独立 SqlSession
private static ThreadLocal<SqlSession> 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 | /myapp |
http://localhost:8080/(应用部署在根目录) | ""(空字符串) |
http://localhost:8080/shop/cart | /shop |
response.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 ThreadLocal<String> 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.多线程中的数据库连接(每个线程一份 Connection)
private static final ThreadLocal<Connection> 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 ThreadLocal<SimpleDateFormat> 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.ThreadLocal<T> |
| 作用 | 为每个线程提供独立变量副本 |
| 常用方法 | .set(T value)、.get()、.remove() |
| 常见用途 | 登录用户上下文、SimpleDateFormat、数据库连接等线程隔离变量 |
| 必须清除? | 是,线程池中尤其要注意调用remove() |
3-4、与new对象的核心区别:
问题:我每次
new一个对象不也能实现隔离吗?那为什么还要用ThreadLocal呢?
| 方式 | 变量作用域 | 线程隔离性 | 使用便捷性 | 生命周期绑定 |
|---|---|---|---|---|
new普通对象 | 每次创建新对象,变量归属对象 | 是(对象不共享) | 需要自己传来传去 | 和对象绑定 |
ThreadLocal | 同一个对象内部,按线程维度自动隔离变量副本 | 是(每个线程自动独立) | 全局共享对象,线程自动隔离 | 和线程绑定 |
打个比方来解释:
你可以想象你在写一个网站,用户访问一个页面时,会在后台起一个线程来处理。
你用:
private static ThreadLocal<String> 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 ThreadLocal<String> 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 = { ThreadLocal@1a2b3c → "用户A的会话", ThreadLocal@4d5e6f → "数据库连接A" } 线程 B 的内存: threadLocals = { ThreadLocal@1a2b3c → "用户B的会话", // 同一个 ThreadLocal 对象! ThreadLocal@4d5e6f → "数据库连接B" }关键点:同一个ThreadLocal实例,在不同线程的 map 中对应不同的 value。
【注意】:这里的 key 不是线程的 key,而是
ThreadLocalMap里的 key。
1、拆解结构
// 假设有两个 ThreadLocal 静态变量 // 对象 @1a2b3c private static final ThreadLocal<User> USER_CONTEXT = new ThreadLocal<>(); // 对象 @4d5e6f private static final ThreadLocal<Connection> CONN_HOLDER = new ThreadLocal<>();线程 A 的内存:
Thread-A { threadLocals: ThreadLocalMap { 条目1: { key: ThreadLocal@1a2b3c(弱引用), value: User对象("用户A") }, 条目2: { key: ThreadLocal@4d5e6f(弱引用), value: Connection对象("连接A") } } }线程 B 的内存:
Thread-B { threadLocals: ThreadLocalMap { 条目1: { key: ThreadLocal@1a2b3c(弱引用), value: User对象("用户B") }, 条目2: { key: ThreadLocal@4d5e6f(弱引用), value: Connection对象("连接B") } } }2、关键理解
| 层级 | 说明 |
|---|---|
ThreadLocal@1a2b3c | 全局唯一的静态对象,存在堆中,所有线程共享这个引用 |
Thread-A.threadLocals | 线程 A私有的 Map |
Thread-B.threadLocals | 线程 B私有的 Map |
同一个
ThreadLocal对象,作为 key 出现在不同线程的私有 Map 中,对应不同的 value。
3-9、内存泄漏问题
这是ThreadLocal最容易踩的坑:
1、为什么会泄漏?
ThreadLocalMap的 key 是弱引用(WeakReference)的ThreadLocal对象:
// ThreadLocalMap 中的 Entry 结构 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // 强引用! Entry(ThreadLocal<?> k, Object v) { super(k); // key 是弱引用 value = v; // value 是强引用 } }问题场景:
线程 A 执行完任务,但线程池复用了该线程(线程不会销毁)
ThreadLocal变量在外部不再被强引用GC 回收了弱引用的 key(
ThreadLocal对象)但
ThreadLocalMap中的 Entry 还在,value 是强引用,无法回收结果:value 永远滞留在内存中,造成泄漏
2、解决方案:用完必须 remove()
try { threadLocal.set(value); // 业务逻辑... } finally { threadLocal.remove(); // 必须清理! }3、现代替代方案:InheritableThreadLocal & TransmittableThreadLocal
(1)InheritableThreadLocal— 子线程继承父线程的值
// 子线程可以获取父线程设置的值 InheritableThreadLocal<String> inheritable = new InheritableThreadLocal<>(); inheritable.set("父线程的值"); new Thread(() -> { System.out.println(inheritable.get()); // 输出:"父线程的值" }).start();局限:只适用于直接创建
new Thread(),对线程池无效(线程池的线程不是新建的)。
(2)TransmittableThreadLocal(阿里开源)
线程池场景下的完美解决方案,需要引入transmittable-thread-local依赖:
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); // 包装线程池 ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); context.set("主线程的值"); ttlExecutor.submit(() -> { System.out.println(context.get()); // 能正确获取:"主线程的值" });总结对比
| 特性 | ThreadLocal | InheritableThreadLocal | TransmittableThreadLocal |
|---|---|---|---|
| 线程隔离 | ✅ | ✅ | ✅ |
| 子线程继承 | ❌ | ✅(仅new Thread) | ✅(支持线程池) |
| 线程池支持 | ✅ | ❌ | ✅ |
| 需手动remove | ✅ | ✅ | ✅ |
3-10、ThreadLocal的标准定义方式
private static final是ThreadLocal的标准定义方式。
标准写法
public class UserContext { private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>(); }为什么要static?
核心原因:确保所有线程共享同一个
ThreadLocal实例
ThreadLocal的隔离机制是以实例为维度的。如果不用static,每个对象都有自己的ThreadLocal实例,就会破坏设计意图:
// ❌ 错误:非 static public class UserContext { private final ThreadLocal<User> currentUser = new ThreadLocal<>(); // 每次 new UserContext() 都会创建新的 ThreadLocal 实例 // 线程 A 在实例1里 set,在实例2里 get → 拿到 null }// ✅ 正确:static public class UserContext { private static final ThreadLocal<User> currentUser = new ThreadLocal<>(); // 全局唯一实例,所有线程都用它作为 key // 线程 A 的 currentUser.set() 和 currentUser.get() 操作的是同一个 key }原理回顾:ThreadLocalMap是以ThreadLocal对象本身作为 key 的。如果 key 不唯一,就找不到对应的 value。
为什么要private?
防止外部直接操作,通过封装的方法控制访问:
public class UserContext { private static final ThreadLocal<User> 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 ThreadLocal<Connection> 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 ThreadLocal<T> NAME = new ThreadLocal<>();四、作用域(Scope)和生命周期
4-1、SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。
因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
4-2、SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。
即:一个数据库一个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)
生成字节码文件(.class)
mybatis底层也是用的Javassist!
5-2、基本使用步骤(以动态创建类为例):
假设我们要生成一个名为com.example.Person的类,包含一个字段name和一个sayHello()方法。
1、Step 1:引入依赖(以 Maven 为例)
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.29.2-GA</version> </dependency>Step 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 } }执行后你会得到一个类Person:
public 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接口的一个代理对象(实现类)。
