您的位置:首页 > 编程语言 > Java开发

ThreadLocal实践

2014-02-23 21:49 537 查看
无聊的话

不管是用过Hibernate还是Spring的程序员,又或者作为一名面试官,经常会提及Hibernate等框架在操作数据库时是如何做到多线程不混乱,

并且能自动提交、回滚,不需要程序员做这些麻烦的操作,极大的解放了程序员的工作量,且保持代码简洁优雅一般大家都会说,

用ThreadLocal来保存当前线程使用的SqlSession就OK了。确实是这样,但很少看到有人仔细去分析,或者自己尝试去实现这一过程

这几天一直在想实现一个简单版本的这种组件,纯粹为了实践下ThreadLocal的巧妙之处,于是有了这篇文章

 

关于设计的一些想法

(1)一个应用程序允许存在多个数据源,即一个或多个DataSource

(2)每个DataSource由一个数据连接工厂来处理,数据连接工厂即SqlSessionFactory

(3)正在运行的每个线程,可以操作多个数据源,那就要求实现多个数据源的一起提交与回滚

(4)针对该线程里面的每个数据源,只能分配一个SqlSession

(5)针对每个SqlSession,只能分配一个数据库连接,即一个Connection

这里面关键点就是通过【同一个线程】串联起来的,如果做到每个线程里面的每个数据源,都只有一个Sqlsession,下面看一个典型应用

 

package com.yli.sql.connection;

import java.util.HashMap;
import java.util.Map;

import com.yli.sql.session.SqlSession;

public class Test {

public static void main(String[] args) {
// 创建Dao和User的实例,通常这由Spring的IOC来完成
// 并且Dao和User在整个应用里面是单例,即singleton模式的
UserDao userDao = new UserDao();
UserService userService = new UserService();
userService.setUserDao(userDao);

// 调用service的业务方法:通常在service层加事务控制
// 使用注解或者xml配置的方式:匹配粒度可以到方法名,也可以到类名等等
// 但是不需要程序员手动打开、关闭连接,也不需要手动控制事务
Map<String, Object> params = new HashMap<String, Object>();
params.put("userId", 1001);

userService.modifyUser(params);
}
}

class UserService {
private UserDao userDao;

public void modifyUser(Map<String, Object> params){

// ===>DB1
userDao.insertUser(params);

// ===>DB2
userDao.updateUser(params);

// another service method
this.updateUser(params);
}

public void updateUser(Map<String, Object> params){
userDao.updateUser(params);
}

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}

class UserDao {

// 操作数据库1
private SqlSession session1;

// 操作数据库2
private SqlSession session2;

public void insertUser(Map<String, Object> params){
String sql = "insert ...";
session1.insert(sql, params);
}

public void updateUser(Map<String, Object> params){
String sql = "update ...";
session2.insert(sql, params);
}
}


 

那么怎么实现自动打开关闭连接、自动提交回滚事务呢,并且是涉及到多个数据源的

我的想法比较简单,在业务方法执行前,使用 Connection.setAutoCommit(false) ;成功执行后,使用 Connection.commit() 提交;执行过程中遇到异常时,使用 Connection.rollback() 回滚

那么这样一说,大家很可能就想到了使用Spring的AOP来实现,比如凡是调用 save* , update* , insert* , delete* , execute* 这种方法的

就在业务方法执行前、执行中、执行后 这3个阶段来包裹一段代码,实现自动提交与回滚

确实是这样的,不过既然是自己实现,那就不用Spring的AOP了,JDK原生的动态代理  invocationhandler 就能很好的做到

不过JDK的动态代理要求业务类必须定义接口,这不太方便,那就使用 cglib 来实现,它可以为每一个类生成动态代理,不强制要求有接口(原理就是为每个类生成一个子类)

 

但是我们看看上面的代码,即 userService.modifyUser(params),其实每一个业务方法里面

肯定会调用其他业务方法,或者调用其他业务类的方法,比如 employeeService.save() 方法。

如果就简单粗暴的【为每个业务类生成代理类,并为每个匹配切点的方法包裹一段代码】,那就是这种结构的代码

class ProxyService implements MethodInterceptor {

@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {

// 暂且不说这个 session 是如何得到的,但是前文已经提过
// 由session来获取连接 Connection

// 设置要程序提交事务
session.getConnection().setAutoCommit(false);

try{
Object result = proxy.invokeSuper(obj, args);
}catch(SQLException e) {
e.printStackTrace();
session.getConnection().rollback();
}

session.getConnection().commit();

return null;
}

}


分析下前面提到的典型应用 userService.modifyUser(params),由于该方法又调用了 userService.updateUser()方法

那么按照上述代理类的执行过程就是这种:

===>modifyUser()

===>setAutoCommit()

===>proxy.invokeSuper(obj, args):进入了 真正的 modifyUser  方法

      ===>updateUser()  :执行 userDao.insertUser(params)  和  userDao.updateUser(params)



      ===>setAutoCommit()

      ===>proxy.invokeSuper(obj, args)

      ===>commit()

===>commit()

很明显这是有问题的,出现了多次setAutoCommit和多次 commit,还能想到在异常那段也有多次rollback

特别是多次 commit 就导致整个方法调用不是事务同步的。。。因为内层方法已经提交了,外层基本出错

还怎么回滚掉内层的(不要多想Spring事务管理的传播属性,我没那么强大)

 

那么怎么处理呢,我的想法是把当前线程的每次方法调用,用一个栈记录下来

一进入某个方法,就push当前方法名;一调用完毕,就pop当前方法名。因为整个线程是把所有内层方法串联起来的,所以很好办了。

那么只要判断当前方法栈里面有多少个方法就行了:在第一个入口方法处调用 setAutoCommit,在第一个入口方法调用结束处调用 commit 就行了

那这个方法栈,是当前线程独有的局部变量,怎么得到呢,就是ThreadLocal来保存了

那就来看整个实现过程好了,不想啰嗦了。。。一张图+代码说明



 

 

1.创建connection包,来定义一些类,处理Connection

比如创建包:com.yli.sql.connection

 

package com.yli.sql.connection;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

import javax.sql.DataSource;

/**
* 每一个Connection都应该有DataSource来提供
* DataSource充当连接池,比如C3P0或者DBCP都是非常优秀的连接池
* 此处不想这么费事了。。。我就每次new一个Connection好了
* 所以我这就实现getConnection方法
* @author yli
*
*/
public class MyDataSource implements DataSource {

private String dirver = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://localhost:3306/test";
private String user = "root";
private String password = "gzu_imis";

public MyDataSource(String driver, String url, String user, String password) {
this.dirver = driver;
this.url = url;
this.user = user;
this.password = password;
}

@Override
public Connection getConnection() throws SQLException {
try {
Class.forName(this.dirver);
return DriverManager.getConnection(this.url, this.user, this.password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}

@Override
public Connection getConnection(String username, String password)
throws SQLException {
// TODO Auto-generated method stub
return null;
}

@Override
public PrintWriter getLogWriter() throws SQLException {
// TODO Auto-generated method stub
return null;
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {
// TODO Auto-generated method stub

}

@Override
public void setLoginTimeout(int seconds) throws SQLException {
// TODO Auto-generated method stub

}

@Override
public int getLoginTimeout() throws SQLException {
// TODO Auto-generated method stub
return 0;
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
// TODO Auto-generated method stub
return null;
}

@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
// TODO Auto-generated method stub
return null;
}

@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
// TODO Auto-generated method stub
return false;
}
}


 

2.创建session包,来定义一些类,处理Session

比如创建包:com.yli.sql.session

 

package com.yli.sql.session;

import java.sql.Connection;
import java.util.List;
import java.util.Map;

/**
* 定义SqlSession接口
* 暴露给程序员的方法,就是一些基本的query、update等等
* 当然要提供获取Connection的方法
* @author yli
*
*/
public interface SqlSession {

Connection getConnection();

boolean execute(String sql, Map<String, Object> params);

int insert(String sql, Map<String, Object> params);

int update(String sql, Map<String, Object> params);

int delete(String sql, Map<String, Object> params);

Map<String, Object> selectForOne(String sql, Map<String, Object> params);

List<Map<String, Object>> selectForList(String sql, Map<String, Object> params);

}

 

 

package com.yli.sql.session;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* 定义一个简单的sql参数处理工具类
* 比如动态配置sql语句:select * from user where userId=:userId
* 就是使用 :userId 来处理动态参数
* 这个工具类很简单,不能用于生产系统
* 看过Spring处理Sql动态参数的童鞋都明白,其实蛮复杂的一个过程
*
* @author yli
*/
public class SqlParam {

/**
* 将 select * from user where userId=:userId 处理成
*   select * from user where userId=?
* 因为 ? 才是PreParedStatement能处理的
* 使用 :userId 完全是为了方便开发者,并且方便程序动态处理所有参数
*/
private String       prePareSql;

/**
* 动态sql语句要传递进来的参数
*/
private List<Object> paramList;

public SqlParam(String prePareSql, List<Object> paramList) {
this.prePareSql = prePareSql;
this.paramList = paramList;
}

public void setPreParams(PreparedStatement ps){
if(null == ps || null == paramList) {
return;
}
for (int i = 0; i < paramList.size(); i++) {
try {
ps.setObject(i + 1, paramList.get(i));
} catch (SQLException e) {
e.printStackTrace();
}
}
}

/**
* 用正则表达式匹配 :userId 这种结构的动态sql语句
* 比较简单的一个处理,有很多bug的,别在意。。。
* @param sql
* @param params
* @return
*/
public static SqlParam prePareSqlParam(String sql, Map<String, Object> params) {
String regx = ":[a-zA-Z0-9_]+";
Pattern pattern = Pattern.compile(regx);
Matcher matcher = pattern.matcher(sql);
List<Object> paramList = new ArrayList<Object>();
String sqlParam;
while (matcher.find()) {
sqlParam = sql.substring(matcher.start(), matcher.end());
paramList.add(params.get(sqlParam.substring(1)));
}
if (paramList.isEmpty()) {
System.out.println("==========>没有任何递参数进来:" + sql);
} else {
sql = matcher.replaceAll("?");
}
return new SqlParam(sql, paramList);
}

public String getPrePareSql() {
return prePareSql;
}

public List<Object> getParamList() {
return paramList;
}
}


package com.yli.sql.session;

import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

/**
* 定义一个SqlSessionFactory工厂类
* 该工厂提供openSession方法:从当前线程获取Session
* 更重要的是定义一个localMap,用来保存当心线程使用到的session
* 前文说过:每个线程可以操作多个数据源的,每个数据源要用一个SqlSession
* 所有用了一个Map来保存
*
* @author yli
*
*/
public class SqlSessionFactory {

private DataSource dataSource;

private static ThreadLocal<Map<SqlSessionFactory, SqlSession>> localFactory = new ThreadLocal<Map<SqlSessionFactory, SqlSession>>();

public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}

public DataSource getDataSource() {
return this.dataSource;
}

/**
* 根据当前线程使用的SqlSessionFactory来判断应该使用哪个数据源
* 在创建对应的SqlSession即可
* 每次创建的SqlSession保存到Map中:可以就是当前要使用的SqlSessionFactory
* @return
*/
public SqlSession openSession() {
Map<SqlSessionFactory, SqlSession> factory = localFactory.get();
SqlSession session = null;
if (null == factory) {
try {
factory = new HashMap<SqlSessionFactory, SqlSession>();
session = new DefaultSqlSession(this);
factory.put(this, session);
localFactory.set(factory);
} catch (SQLException e) {
e.printStackTrace();
}
} else {
if(null == factory.get(this)) {
try {
session = new DefaultSqlSession(this);
factory.put(this, session);
localFactory.set(factory);
} catch (SQLException e) {
e.printStackTrace();
}
}
}

return factory.get(this);
}

// 如果使用这个方式来保存SqlSession,那么当前线程只能使用一个SqlSession了
// 对应要操作多个数据源的应用程序来说,就不适用了
// private static ThreadLocal<SqlSession> localSession = new ThreadLocal<SqlSession>();

/* 我想这段代码是很多人在分析Hibernate或者Spring时会提到的
* 但有没有像过同一线程操作对个数据源的情况了,你要亲自试试才知道
* 当然Hibernate没我说的这么简单。。。是有很多人想简单了
public SqlSession openSession1() {
SqlSession session = localSession.get();
if(null == session) {
try {
session = new DefaultSqlSession(this);
} catch (SQLException e) {
e.printStackTrace();
}
localSession.set(session);
}
return session;
}
*/
}
package com.yli.sql.session;

/**
* 在定义个工具类,一般来说由程序员自己实现或者扩展它
* 因为要使用多个SqlSessionFactory对应多个数据源
* 所以此处定义两个来测试,如果你有更多的数据源,就再定义了
*
* 所以这个类是客户端自己定义或者扩展的
* 客户端的Dao继承自它,就可以使用SqlSession了
* @author yli
*
*/
public class SessionDaoSupport {

private SqlSessionFactory factory1;

private SqlSessionFactory factory2;

public void setFactory1(SqlSessionFactory factory1) {
this.factory1 = factory1;
}

public void setFactory2(SqlSessionFactory factory2) {
this.factory2 = factory2;
}

public SqlSession getSession1(){
return this.factory1.openSession();
}

public SqlSession getSession2(){
return this.factory2.openSession();
}
}


 

3.创建动态代理的包,来自动处理事务

比如创建包:com.yli.sql.transaction

package com.yli.sql.transaction;

/**
* 简单点,我们定义一个匹配方法的切点
*
* @author liyu
*
*/
public class MethodAdvice {

private String[] matches;

public MethodAdvice(String[] matches) {
this.matches = matches;
}

public String[] getMatches(){
return this.matches;
}
}


 

package com.yli.sql.transaction;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import com.yli.sql.session.SqlSessionFactory;

/**
* 定义一个事务管理类
* 该类关联到应用程序中所有SqlSessionFactory
* 那么就可以获取到当前线程里面的 SqlSession
* 获取到SqlSession就能获取Connectin
* 获取到Connection就能自动提交、回滚了。。。
* 不过这很简单也很粗暴的实现啊
*
* @author yli
*
*/
public class TransactionManager {

private MethodAdvice advice;

private List<SqlSessionFactory> factorys = new ArrayList<SqlSessionFactory>();

private static ThreadLocal<Stack<String>> methodStack = new ThreadLocal<Stack<String>>();

public void setFactorys(List<SqlSessionFactory> factorys,
MethodAdvice advice) {
this.factorys = factorys;
this.advice = advice;
}

/**
* 开始事务处理
* @param methodName
*/
public void beginTransaction(String methodName) {
// 获取当前执行线程的方法栈
Stack<String> methodStack = this.getMethodStack();

// 如果该方法匹配拦截点,则压入当前方法栈
if (isMatchMethod(methodName)) {
methodStack.push(methodName);

// 如果当前栈只有1个方法,说明在第一个方法入口处
if (methodStack.size() == 1) {
System.out.println(methodName
+ "【开始】设置:conn.setAutoCommit(false)");

try {
for (SqlSessionFactory factory : factorys) {
factory.openSession().getConnection()
.setAutoCommit(false);
}
} catch (SQLException e) {
e.printStackTrace();
// 回滚
rollBack();
}
}
}
}

/**
* 自定提交事务。。。也不是很好
* @param methodName
*/
public void endTransaction(String methodName) {
// 获取当前执行线程的方法栈
Stack<String> methodStack = this.getMethodStack();

// 如果该方法匹配拦截点,则从栈中移除当前方法
if (isMatchMethod(methodName)) {
methodStack.pop();
// 如果栈中没有方法了:说明方法链已经全部执行完
if (methodStack.isEmpty()) {
try {
for (SqlSessionFactory factory : factorys) {
factory.openSession().getConnection().commit();
}
} catch (SQLException e) {
e.printStackTrace();
// 回滚
rollBack();
}
System.out.println(methodName + "【结束】设置:conn.commit()");
}
}

}

// 自动回滚:将当前所有工程获取到的Sqlsession全部回滚掉
// 不是很好,有些方法可能只使用了一个factory来创建SqlSession
public void rollBack() {
for (SqlSessionFactory factory : factorys) {
try {
factory.openSession().getConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

// 检查当前方法是否被匹配到了
private boolean isMatchMethod(String methodName) {
for (String match : advice.getMatches()) {
if (methodName.toLowerCase().startsWith(match)) {
return true;
}
}
return false;
}

// 获取当前线程对应的方法栈
private Stack<String> getMethodStack() {
Stack<String> currentStack = methodStack.get();
if (null == currentStack) {
currentStack = new Stack<String>();
methodStack.set(currentStack);
}
return currentStack;
}
}
package com.yli.sql.transaction;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
* 前文提到使用JDK的动态代理来搞自动提交和回滚
* 这里用了 cglib 为每个类生成动态代理
* @author yli
*
*/
public class MethodTransaction implements MethodInterceptor {

// 使用事务管理器来管理实务
private TransactionManager manager;

public MethodTransaction(TransactionManager manager) {
this.manager = manager;
}

@Override
public Object intercept(Object object, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
String methodName = method.getName();
System.out.println("=============>当前准备执行方法:" + methodName);

// 开启事务
manager.beginTransaction(methodName);

// 执行目标方法
Object result = null;
try {
result = proxy.invokeSuper(object, args);
// 提交事务
manager.endTransaction(methodName);
} catch (Throwable e) {
System.out.println("=============>当前方法:" + methodName + "全部回滚");
manager.rollBack();
}

// 返回方法执行结果
return result;
}

}


 

好了,截止到这里,我模拟的简单自动提交、回滚就这样了,当然还是有很多bug哈

那么来测试下,我没有用Spring 的IOC,就自己模拟一个IOC好了,真的是最简单的模拟。。。

4.测试

package com.yli.sql.test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import net.sf.cglib.proxy.Enhancer;

import com.yli.sql.connection.MyDataSource;
import com.yli.sql.session.SqlSessionFactory;
import com.yli.sql.transaction.MethodAdvice;
import com.yli.sql.transaction.MethodTransaction;
import com.yli.sql.transaction.TransactionManager;

/**
* 定义一个Map
* 把所有Bean创建好,并创建好依赖关系
* 当然DataSource要创建多个
* 那么SqlSessionFactory也是多个
* 并定义一个事务管理器、一个方法匹配器
* 然后为业务类定义代理类。。。
* 真正的Spring IOC 大家都搞过啊,就是这么干的
*
* @author yli
*
*/
public class BeanFactory {

public static Map<String, Object> beanMap = new HashMap<String, Object>();

public static void initBean() {
// 数据源-1:test数据库
String dirver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "123456";
DataSource da1 = new MyDataSource(dirver, url, user, password);

// 数据源-2:test1数据库
url = "jdbc:mysql://localhost:3306/test1";
DataSource da2 = new MyDataSource(dirver, url, user, password);

// Session工厂-1
SqlSessionFactory factory1 = new SqlSessionFactory();
factory1.setDataSource(da1);

// Session工厂-2
SqlSessionFactory factory2 = new SqlSessionFactory();
factory2.setDataSource(da2);

// DaoSupport
UserDao userDao = new UserDao();
userDao.setFactory1(factory1);
userDao.setFactory2(factory2);

// 定义哪些方法需要处理事务
String[] methods = new String[] { "save", "update", "insert", "delete",
"execute", "modify" };
MethodAdvice advice = new MethodAdvice(methods);

// 事务管理类:将所有session工厂注入
TransactionManager manager = new TransactionManager();
List<SqlSessionFactory> factorys = new ArrayList<SqlSessionFactory>();
factorys.add(factory1);
factorys.add(factory2);
manager.setFactorys(factorys, advice);

// 业务Bean:主要针对Service层,哪些方法需要自动处理事务
UserService userService = new UserService();
userService.setUserDao(userDao);

// 对于要求开启事务的[类==>方法列表]:生成代理类
UserService userServiceProxy = (UserService) getProxyBean(UserService.class, manager);
userServiceProxy.setUserDao(userDao);

// 将所有Bean加载到BeanMap
beanMap.put("da1", da1);
beanMap.put("da2", da2);
beanMap.put("factory1", factory1);
beanMap.put("factory2", factory2);
beanMap.put("userService", userServiceProxy); // 注意此处的UserService设置为代理类
beanMap.put("userService$", userService); // 原生的类则使用[相同名称+$]定位:Spring 也这么干
}

public static Object getProxyBean(Class<?> classz, TransactionManager manager) {
Enhancer enhancer = new Enhancer();
// 如果不能传递class进来,那就传className进来: Class.forName(className)
// 也能根据类名获取class对象 ,比如Spring的bean配置文件,肯定是要配置类的全路径的
enhancer.setSuperclass(classz);
enhancer.setCallback(new MethodTransaction(manager));
return enhancer.create();
}
}


再定义一个Dao和Servei吧

package com.yli.sql.test;

import java.util.Map;

import com.yli.sql.session.SessionDaoSupport;

public class UserDao extends SessionDaoSupport{

public boolean inserUserToDB1(Map<String, Object> params){
String sql = "insert into user(name,sex) values(:name,:sex)";
return getSession1().insert(sql, params) > 0;
}

public boolean inserUserToDB2(Map<String, Object> params){
String sql = "insert into user(name,sex) values(:name,:sex)";
return getSession2().insert(sql, params) > 0;
}

public boolean updateUserToDB1(Map<String, Object> params){
String sql = "update user set name=:name where id=:Id";
return getSession1().update(sql, params) > 0;
}

public boolean updateUserToDB2(Map<String, Object> params){
String sql = "update user set name=:name where id=:Id";
return getSession2().update(sql, params) > 0;
}

public boolean deleteUserToDB1(Map<String, Object> params){
String sql = "delete from user where id=:Id";
return getSession1().delete(sql, params) > 0;
}

public boolean deleteUserToDB2(Map<String, Object> params){
String sql = "delete from user where id=:Id";
return getSession2().delete(sql, params) > 0;
}

}


 

package com.yli.sql.test;

import java.util.HashMap;
import java.util.Map;

public class UserService {

private UserDao userDao;

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void modifyUser() {
Map<String, Object> params = new HashMap<String, Object>();
// params.put("Id", 1);
params.put("name", "hello");
params.put("sex", "0");

// insert => DB1
this.userDao.inserUserToDB1(params);

// 测试程序出错了回滚
// System.out.println(1/0);

// insert => DB2
this.userDao.inserUserToDB2(params);

// 有调用其他业务方法,也可以是其他业务类的方法
insertUser();
}

public void insertUser() {
Map<String, Object> params = new HashMap<String, Object>();
params.put("name", "yli");
params.put("sex", "1");
// insert => DB1
this.userDao.inserUserToDB1(params);

// 测试程序出错了回滚
// System.out.println(1 / 0);
}
}


最后来一个Main方法测试了

package com.yli.sql.test;

public class MainTest {

public static void main(String[] args) {
BeanFactory.initBean();

UserService user = (UserService)BeanFactory.beanMap.get("userService");

user.modifyUser();

}
}


测试表明一起提交或者某个出错就一起回滚了。。。

好吧,只是简单地应用,有人浏览到这里,看看就行了

 对了我是用的mysql,在公司用db2也试了,哎,肯定是相同的结果

这个例子就依赖了 mysql5.5 和 cglib2.2的 jar包

 

 

 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息