JavaEE中分层解耦合与事物控制-方案
转载请注明出处:
http://blog.youkuaiyun.com/u010825468/article/details/49434803
0、写在前面
很多年前,依靠李兴华的《Java WEB开发实战经典》一书,开启了Java WEB之旅,个人认为这本书作为JavaEE入门非常不错,李兴华的视频讲的也很不错,或许大家现在都在看《*疯狂讲义》或是培训机构的教学视频,但经典始终是经典。
我在学习完JavaEE后没有真正从事过JavaEE开发的正式工作,大多只是平常自己写点东西,或是帮别人写点小应用之类的,两年后大学毕业,我也正式的转入了Android开发阵营,我对Android开发的理解大致为:如果我不在Android上做深入了解与学习,其实我就是个写前台的。但是好在有WEB的基础与WEB锻炼的Java技能,开发Android其实非常简单。
1、为什么要分层
学习过JavaEE基础的人都知道MVC,这是传统的JavaEE的设计模式,MVC即将工程或者说代码分成Model、View、Controller三层。Model即模型层,一般包括Java Bean以及这些bean对应的数据库的原子性操作方法(一般的CRUD、getAll、findByXXX、delByXXX),而这些数据库操作就是我们熟悉的DAO。我们一般将bean包命名为VO或者domain,dao操作包命名为dao。View即显示层,在JavaEE中可以将其看作Jsp,用于和用户产生动态交互的界面,是通过服务器编译才能进行显示的,关于Jsp的更多我们在后面详解。Controller即控制层,用来控制业务的流转,比如一个servlet,能够接受用户通过View提交的数据,然后根据自己的业务规则去调用不同的service(service属于控制层,是对每一个具体业务操作事物的封装,通过service调用dao),因此Controller是介于Model和View之间的桥梁,也是业务流转的核心。
解释了MVC,那我们为什么需要对代码结构进行分层呢?主要有以下几个方面的考虑:
- 解耦和:我们将一个业务流转和数据库操作分离开来,既符合面向对象的编程原则,又将代码逻辑整理的很清楚,在调试过程中也能方便改正BUG,体现了高内聚,低耦合的思想;
- 业务拆分:有时候我们一个接口需要处理的业务很复杂,通过分层能够将业务分为具体的某一类业务,然后对这一类业务的具体处理逻辑拆分为多次调用dao进行完成,让复杂的逻辑简单化了;
- 便于接口升级:各层之间通过接口进行解耦操作,也就是将接口和具体实现进行了分离,这样可以方便的实现或替换掉具体实现;
- 提高代码服用率:作为一个程序猿,我们不应该造重复的轮子,在一个系统中Model层是对数据库的原子性操作,复用性最高。假如我们更换了数据库,只要保证访问数据的接口方法不发生变化,我们也能够很快的实现对不同数据库的操作。
2、我的分层模式
个人一般将代码结构分为DAO、Service、Controller、View四层,dao和service都使用接口和真是实现类的方式,DAO、Service、Controller之间通过工厂类和配置文件进行解耦和。
3、Demo
在这个demo中我使用了struts2.3、c3p0、dbutils等常用的工具。包结构如下:
1.domain包里都是javabean,这里就不给代码了;
2.dao包里是对数据库的基础操作方法,其中有一个名为Dao的接口,只作为一个标识(后面在工厂类用到),没有定义方法和变量,以UserDao为例,UserDao接口继承自Dao(所有的dao类接口都需要继承自Dao),UserDaoImpl为真实实现类,继承自UserDaoDao。
public interface UserDao extends Dao {
/**
* 根据用户名查找用户
* @param username 用户名
* @return 找到的用户bean,如果没找到返回null
*/
User findUserByUName(String username);
/**
* 增加用户
* @param user 封装了用户信息的bean
*/
void addUser(User user);
/**
* 根据用户名 密码 查找用户
* @param username 用户名
* @param password 密码
* @return 找到的用户 ,如果找不到返回null
*/
User findUserByUNameAndPsw(String username, String password);
----------
}
public class UserDaoImpl implements UserDao {
@Override
public User findUserByUName(String username) {
String sql = "select * from users where userName = ?";
try {
QueryRunner runner = new QueryRunner(DaoUtils.getSource());
return runner.query(sql, new BeanHandler<User>(User.class), username);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
@Override
public void addUser(User user) {
String sql = "insert into users values (null,?,?)";
try {
QueryRunner runner = new QueryRunner(DaoUtils.getSource());
runner.update(sql, user.getUserName(), user.getPassword());
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
----------
}
3.service包里是对应的原子性的业务的操作。同样,有一个不定义方法的service接口,UserService继承自Service接口,UserServiceImpl继承自UserService。
public interface UserService extends Service {
/**
* 注册用户
* @param user 封装了用户信息的bean
*/
void registUser(User user);
/**
* 激活用户
* @param activecode 激活码
*/
void activeUser(String activecode);
/**
* 根据用户名 密码 查找用户
* @param username 用户名
* @param password 密码
* @return 找到的用户 ,如果找不到返回null
*/
User findUserByUNameAndPsw(String username, String password);
}
public class UserServiceImpl implements UserService {
private UserDao dao = BasicFactory.getFactory().getInstance(UserDao.class);
public void registUser(User user) {
----------
}
public void activeUser(String activecode) {
----------
}
public User findUserByUNameAndPsw(String username, String password) {
return dao.findUserByUNameAndPsw(username,password);
}
}
4.action包是用户通过Jsp直接访问的控制类。定义一个UserAction,用户在进行注册操作时,该方法再调用service方法进行数据存储的具体操作:
public class UserAction {
UserService service = BasicFactory.getFactory().getInstance(UserService.class);
private User user = new User();
private String errorMsg;
private User result_user;
public User getResult_user() {
service.registUser(user);
----------
return xxx;
}
----------
}
以上操作就完成了一个简单的MVC模式(Jsp方面省略不说了)。上面的代码分层也是较为明确的了,但是设想,我们一个操作例如用户注册,由于数据库设计的原因,需要在多张数据库表中进行数据写入,但是其中有一次数据库操作失败了,导致注册失败。此时我们应该将事物回滚。
4、事物回滚
事物回滚是数据库操作中比较常见的一种操作方式,我们将一组业务的整体处理叫做一个事物,并且我们这一组业务在业务层面上具有原子性,既要么都成功,要么都失败。一旦在操作中途出现失败,必须将之前的数据操作进行回滚,将数据恢复到原始状态。
我们对事物的管理一般是基于JDBC Connection,在得到一个connection后,我们设置其AutoCommit为false:
conn.setAutoCommit(false);
那么我们在这个Connection关闭之前的所有数据库操作都不会即时执行,知道指令:
conn.commit();
才能将Connection执行的数据库操作真是操作在数据库中。
但是在hibernate中,实现了基于JTA的事物管理,也就是跨session即跨Connection生命周期的事物管理。通过JTA容器可以横跨多个JDBC Connection并忽略其生命周期的影响(JTA事物管理与JDBC事物管理不应该同时使用)。
我们在这个项目中没有用到hibernate而是使用c3p0作为数据源、dbutils作为数据库操作的工具类,并且在dao中使用QueryRunner进行数据库操作时,传入的是DataSource即一个数据源,那么问题来了:在service中进行一组数据库操作,如果需要进行事物控制怎么办?
通过上面对事物的分析,我们也有两种解决方案:
- 让这一组数据库操作拿到的是同一个JDBC Connection;
- 记录运行的sql,出现异常后通过补偿的sql进行回滚。
这两个方法都需要在service层进行处理。
我这里采用第一种方法进行事物控制:先贴代码
自定义一个注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Tran {
}
BasicFactory类,用于解耦、数据库事物管理:
public class BasicFactory {
private static BasicFactory factory = new BasicFactory();
private BasicFactory() {
}
public static BasicFactory getFactory() {
return factory;
}
@SuppressWarnings("unchecked")
public <T> T getInstance(Class<T> clz) {
try {
if (Service.class.isAssignableFrom(clz)) {
//--当前获取的是Service
String infClzName = clz.getSimpleName();
String impClzName = ConfigUtils.getProp(infClzName);
//--生成真正的Service对象
final T t = (T) Class.forName(impClzName).newInstance();
//--生成service对象的代理,在service方法执行之前和之后做事务的控制
T proxy = (T) Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces()
, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isAnnotationPresent(Tran.class)) {
//--当前被调用的方法上有@Tran,需要事务
try {
TransactionManager.startTran();
Object obj = method.invoke(t, args);
TransactionManager.commit();
return obj;
} catch (InvocationTargetException e) {
TransactionManager.rollback();
e.getTargetException().printStackTrace();
throw new RuntimeException(e.getTargetException());
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
throw new RuntimeException(e);
} finally {
TransactionManager.release();
}
} else {
//--当前被调用的方法上没有@Tran,不需要事务
return method.invoke(t, args);
}
}
});
return proxy;
} else if (Dao.class.isAssignableFrom(clz)) {
//--当前获取的是Dao
String infClzName = clz.getSimpleName();
String impClzName = ConfigUtils.getProp(infClzName);
return (T) Class.forName(impClzName).newInstance();
} else {
//既不是Service也不是Dao
throw new RuntimeException("参数错误");
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
TransactionManager类,管理事务的管理类:
ThreadLocal可参照:关于ThreadLocal的理解
public class TransactionManager {
private static ThreadLocal<Connection> conn_local = new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
return DaoUtils.getConn();
}
};
private static ThreadLocal<Boolean> hasTran_Local = new ThreadLocal<Boolean>() {
protected Boolean initialValue() {
return false;
};
};
private TransactionManager() {
}
public static void startTran() {
try {
hasTran_Local.set(true);
conn_local.get().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static void commit() {
try {
conn_local.get().commit();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static void rollback() {
try {
conn_local.get().rollback();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static Connection getConnection() {
return conn_local.get();
}
public static boolean hasTran() {
return hasTran_Local.get();
}
public static void release() {
try {
conn_local.get().close();
conn_local.remove();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
DaoUtils类,用于获取数据库连接池以及连接的:
public class DaoUtils {
private static DataSource source = new ComboPooledDataSource();
/*
判断,
如果当前线程没有开启过事务,返回普通的数据源
如果开启过事务,返回改造过getConnetion方法的数据源,让getConnection方法每次调用都返回线程内部的开启过事务的那个Connection
*/
public static DataSource getSource() {
if (TransactionManager.hasTran()) {
//--开过事务,返回改造过getConnection方法的数据源
DataSource proxy = (DataSource) Proxy.newProxyInstance(source.getClass().getClassLoader(), source.getClass().getInterfaces()
, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getConnection".equals(method.getName())) {
final Connection conn = TransactionManager.getConnection();
Connection conn_proxy = (Connection) Proxy.newProxyInstance(conn.getClass().getClassLoader(), conn.getClass().getInterfaces()
, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("close".equals(method.getName())) {
return null;
} else {
return method.invoke(conn, args);
}
}
});
return conn_proxy;
} else {
return method.invoke(source, args);
}
}
});
return proxy;
} else {
//--没事务,返回普通数据源
return source;
}
}
public static Connection getConn() {
try {
return source.getConnection();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
工厂类所用到的配置文件:
UserDao=xxxxxx.dao.UserDaoImpl
UserService=xxxxxx.service.UserServiceImpl
从以上代码可以看出,我们定义的注解应该增加在service层的接口上。
在通过工厂类得到service的真实实现类的时候,如果判断service的方法被设置了tran这个注解,那么就会进入到我们的反射方法,我们在反射方法中真正执行方法method.invoke前后加上事物的控制就完成了整个service的事物控制。这里有个类非常重要ThreadLocal,如果不明白可以参见关于ThreadLocal的理解,我们在dao的真实实现类中使用
QueryRunner runner = new QueryRunner(DaoUtils.getSource());
其中DaoUtils.getSource()就是我们最为重要的,需要在一个线程中保持同一个的DataSource。
BasicFactory类在执行TransactionManager.startTran();的时候,实际执行的是:
private static ThreadLocal<Connection> conn_local = new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
return DaoUtils.getConn();
}
};
private static ThreadLocal<Boolean> hasTran_Local = new ThreadLocal<Boolean>() {
protected Boolean initialValue() {
return false;
};
};
public static void startTran() {
try {
hasTran_Local.set(true);
conn_local.get().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
即此时已经给这个线程标记了一个hasTran_Local的布尔型的属性,true为执行事物。变量conn_local默认值是DaoUtils.getConn();即得到一个JDBC的连接,而我们在dao真是实现类里面通过DaoUtils.getSource()得到数据源时,我们通过反射得到数据源的代理,并且设置如果访问getConnection()方法是调用Connection conn = TransactionManager.getConnection();方法,而在TransactionManager.getConnection();方法中实际执行如下:
public static Connection getConnection() {
return conn_local.get();
}
这里只是通过conn_local.get()得到一个JDBC的Connection对象,根据ThreadLocal的特性,我们没有调用set()方法,所以只有在这次调用get()方法时就会调用initialValue()方法,去DaoUtils里得到一个JDBC的connection。因此,这是DaoUtils的getSource()方法中的代码:
Connection conn = TransactionManager.getConnection();
得到的实际是当前source的connection。这样就完成了在一个线程中存储一个JDBC的Connection的操作。
那么在DaoUtils.getSource()方法中,为什么我们又要对在线程中得到的connection进行反射呢?
Connection conn_proxy = (Connection) Proxy.newProxyInstance(conn.getClass().getClassLoader(), conn.getClass().getInterfaces()
, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("close".equals(method.getName())) {
return null;
} else {
return method.invoke(conn, args);
}
}
});
由于我们现在使用的是c3p0数据源,它在使用完数据源后会将这个数据源的connection给close掉,从而在下一次取出一个数据源中的DataSource后才能正常使用。
因此,我们并不希望c3p0帮我们关闭了连接,因此我们在返回的DataSource的connection中做反射,当调用其close方法时,不真正的执行close方法。而是通过TransactionManager.release()方法真正的关闭一个JDBC的connection。
转载请注明出处:
http://blog.youkuaiyun.com/u010825468/article/details/49434803