JavaEE中分層解耦合與事物控制-方案

JavaEE中分層解耦合與事物控制-方案


轉載請註明出處:
http://blog.csdn.net/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,那我們爲什麼需要對代碼結構進行分層呢?主要有以下幾個方面的考慮:

  1. 解耦和:我們將一個業務流轉和數據庫操作分離開來,既符合面向對象的編程原則,又將代碼邏輯整理的很清楚,在調試過程中也能方便改正BUG,體現了高內聚,低耦合的思想;
  2. 業務拆分:有時候我們一個接口需要處理的業務很複雜,通過分層能夠將業務分爲具體的某一類業務,然後對這一類業務的具體處理邏輯拆分爲多次調用dao進行完成,讓複雜的邏輯簡單化了;
  3. 便於接口升級:各層之間通過接口進行解耦操作,也就是將接口和具體實現進行了分離,這樣可以方便的實現或替換掉具體實現;
  4. 提高代碼服用率:作爲一個程序猿,我們不應該造重複的輪子,在一個系統中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中進行一組數據庫操作,如果需要進行事物控制怎麼辦?

通過上面對事物的分析,我們也有兩種解決方案:

  1. 讓這一組數據庫操作拿到的是同一個JDBC Connection;
  2. 記錄運行的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.csdn.net/u010825468/article/details/49434803

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章