1. 程式人生 > >「框架」菜鳥簡單模仿一下spring的ioc和aop思想,歡迎大家進來閱讀指教

「框架」菜鳥簡單模仿一下spring的ioc和aop思想,歡迎大家進來閱讀指教

col buffere all 防止 換行 root getc rop proxy

*博客搬家:初版發布於 2015/12/04 16:41 原博客地址:https://my.oschina.net/sunqinwen/blog/539397

spring最核心的部分莫過於ioc和aop了,博主菜逼一枚,如果有哪裏理解的不對或者代碼上有瑕疵的地方歡迎大家指正,大家互相學習,還有就是這只是模仿一下spring思想,只是把事務管理和bean管理簡單模仿一下,完全不代表spring,如果想深入理解請看spring源碼,下面就開始我們簡單的模仿,純手打,覺得還行就贊一下吧~

這個項目不是web項目,只是一個簡單的java項目,測試用junit,廢話不多說了,下面上代碼:

項目的目錄結構:

技術分享圖片

說明:圖中劃紅線的部分都是核心部分

紅線部分說明:

① BeanFactory:所有bean的核心生成器(spring容器)

② ConnBean:jdbc連接生成器(沒用連接池哦~)

③Transaction:事務管理的代理類

④ beans.properties:配置文件

其余的沒劃線的就是domain、dao、service、controller這些web基本層次結構,待會會說

--------------------------------------------------------------------------------------------------------------

主要幾個類的代碼:

① BeanFactory:


package sun.juwin.factory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;
/**
 * 本類用來讀取配置文件中的信息對每個接口對象生成具體的實現
 * 主要是將接口作為key,實例作為value存儲進去,這是個單例,
 * spring默認為每個層次生成實現也是單例,但可以通過@Scope
 * 來指定,我們簡單模仿一下,只是單例
 */
public class BeanFactory {
   private static HashMap<String, Object> mapResult;
   public static HashMap<String, Object> getBean() {
      if (mapResult == null) {
         synchronized (BeanFactory.class) {//雙重檢查的單例,防止多線程訪問時多次new對象
            if (mapResult == null) {
               BufferedReader bf = null;
               String line = null;
               try {
                 /**
                  *下面這句代碼通過流來讀取資源包下面的配置文件,為了省去不必要的麻煩,
                  * 我們沒有用xml,而是用了properties
                  */
                  InputStreamReader inputStreamReader = new InputStreamReader(BeanFactory.class.getClassLoader().getResourceAsStream("beans.properties"));
                  bf = new BufferedReader(inputStreamReader);
                  mapResult = new HashMap<>();
                  while ((line = bf.readLine()) != null) {//每次僅讀一行
                     if ("".equals(line)){//有可能讀到換行時隔了一行(即只有一個換行符)
                        continue;
                     }
                     String[] point = line.trim().split("=");//按照等號拼接
                     if (point.length > 2) {
                        throw new Exception("beans文件格式不對!");
                     }
                     Object obj = Class.forName(point[1].trim()).newInstance();//反射實例化出目標對象
                     mapResult.put(point[0].trim(), obj);//然後以鍵值對的形式存入
                  }
               } catch (Exception e) {
                  e.printStackTrace();
               }
            }
         }
      }
      return mapResult;
   }
}

上面的類可以通過配置文件來實例化不同的對象,符合ioc最基本的思想,下面讓我們來看看配置文件beans.properties的內容吧:


userDao = sun.juwin.dao.impl.UserDaoImpl
userDetailDao = sun.juwin.dao.impl.UserDetailDaoImpl

這裏面只有兩句話,指定dao層接口對象的實現類的路徑,其實已經很接近spring的xml裏對bean的配置了,只不過這裏是properties文件,簡化了許多

② TransactionProxy代理類:


package sun.juwin.proxy.transctional;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
/**
 * 事務代理類,通過這個類可以為要執行的方法加上事務管理
 */
public class TransactionProxy implements InvocationHandler {
    private Object targetObj;
    public Object getTargetObj(Object targetObj){
        this.targetObj = targetObj;
        return Proxy.newProxyInstance(this.targetObj.getClass().getClassLoader(),
                this.targetObj.getClass().getInterfaces(), this);
    }
    /*下面這個方法會在被代理類執行方法時調用,拿到被代理類的要執行的method對象*/
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        Connection connection = (Connection)args[0];//要求第一個參數必須是conn
        try{
            connection.setAutoCommit(false);//開啟事務
            result = method.invoke(this.targetObj, args);//執行目標方法
            connection.commit();//事務提交
            System.out.print("commit success!");
        }catch (Exception e){
            connection.rollback();//事務回滾
            System.err.println("rollback!");
            e.printStackTrace();
        }finally {
            connection.close();//關閉連接
            System.out.println("connection closed!");
        }
        return result;
    }
}

說明:java在1.3版本的時候就為我們提供了一個用作代理類實現的接口InvacationHandler,通過實現這個接口可以很隨意的寫一個耦合度特別低的動態代理類(即這一個代理類可以代理任何類)

③ ConnBean,用來生成一個數據庫連接對象,在不用連接池的情況下,我們用ThreadLocal進行封裝,代碼如下:


package sun.juwin.db;
import java.sql.Connection;
import java.sql.DriverManager;
/*原始產生數據庫連接的類*/
public class ConnBean {
    private static ThreadLocal conn = new ThreadLocal<>();
    private ConnBean(){}
    public static Connection getConn(){
        Connection connection = conn.get();
        if(connection == null){
            synchronized (ConnBean.class){//由於用到了ThreadLocal,因此該單例僅僅相對於當前線程是單例的
                if(connection == null){
                    try{
                        Connection realConn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_useradd", "root", "");
                        conn.set(realConn);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
        return conn.get();//返回給當前線程一個Connection對象
    }
}

以上就是核心的一些實現代碼,下面讓我們來看一下我們的業務吧:

實體類:User,UserDetail,要求添加一個User的同時要添加一個UserDetail

User:


private Long id;
private String userName;
private String address;
private int money;

UserDetail:


private Long id;
private int age;
private String realname;

dao層的接口和實現:

UserDao:


public interface UserDao {
    public void save(User user, Connection conn)throws Exception;
}

UserDaoImpl:


public class UserDaoImpl implements UserDao{
    @Override
    public void save(User user, Connection conn) throws Exception {
        Statement statement = conn.createStatement();//為了省去不必要的麻煩,我們不用預編譯語句
        String sql = "insert into tb_user (userName, address, money) values (‘"
                + user.getUserName() + "‘, ‘"
                + user.getAddress() + "‘, "
                + user.getMoney() + ")";
        statement.executeUpdate(sql);
        statement.close();
    }
}

UserDetailDao:


public interface UserDetailDao {
    public void save(UserDetail userDetail, Connection connection) throws Exception;
}

UserDetailDaoImpl:


public class UserDetailDaoImpl implements UserDetailDao {
    @Override
    public void save(UserDetail userDetail, Connection connection) throws Exception {
        Statement statement = connection.createStatement();
        String sql = "insert into user_detail (age, realname) values ("
                +userDetail.getAge()+", ‘"
                +userDetail.getRealname()+"‘)";
        statement.executeUpdate(sql);
    }
}

UserService:


public interface UserService {
    public void saveService(Connection connection, User user) throws Exception;
}

UserServiceImpl:


/**
 * 業務層
 * juwin
 * 2015-12-04
 */
public class UserServiceImpl implements UserService {
    //下面的dao層實例由BeanFactory通過properties配置文件幫我們生成對應的實例對象
    private UserDao userDao = (UserDao) BeanFactory.getBean().get("userDao");
    private UserDetailDao userDetailDao = (UserDetailDao) BeanFactory.getBean().get("userDetailDao");
    @Override
    public void saveService( Connection connection, User user)throws Exception {
        /**
         * 這個業務層方法執行了兩個dao層方法,可以看做一個事務,
         * 任意一個dao層調用過程中如果發生異常,整個業務方法進行的所有dao層操作就會回滾
         */
        userDao.save(user, connection);
        /*要求在添加user的同時生產一個對應的detail,這裏偷個懶,就自己new一個UserDetail對象吧*/
        UserDetail userDetail = new UserDetail();
        userDetail.setAge(22);
        userDetail.setRealname("juwin");
        userDetailDao.save(userDetail, connection);
        throw new Exception("攔-路-虎");//這個異常是用來測試事務會不會回滾的,正常情況下不加這個
    }
}

UserController:


/**
 * 程序入口,類似於controller層
 */
public class UserController {
    public void SaveUser(User user)throws Exception{
        /**
         * 這一步很關鍵,為每一個執行這個操作的線程分配一個connection連接對象
         * 說明:在實際web開發中客戶端通過發送http請求到業務後臺,這時候tomcat會為這次請求分配一個線程
         * 因此就出現了並發甚至並行的現象,假象一下,我們如果只是利用單例寫一個生成connection對象的方法,
         * 那麽多線程並發訪問的時候就有可能出現:線程1利用完connection對象將其狀態修改為close,而此時線程2
         * 也要用connection,這時候就會報“connection已經關閉”的異常
         * 因此我們采用ThreadLocal,為單獨一個線程生成一個單例的connection對象
         */
        Connection connection = ConnBean.getConn();
        /**
         * 下面這個實例要加一層事務代理,就是讓TransactionProxy這個代理類攪合一下,
         * 這樣我們再利用service層對象調用任何方法時,都會加上事務管理了
         */
        UserService userService = (UserService) new TransactionProxy().getTargetObj(new UserServiceImpl());
        userService.saveService(connection,user);
    }
}

測試類:


public class UserAddTest {
    @Test
    public void Test1() throws Exception{
        User user = new User();
        user.setUserName("weixiaojie1993");
        user.setAddress("beijing");
        user.setMoney(1);
        UserController userController = new UserController();
        userController.SaveUser(user);
        System.out.print("Done !");
    }
}

ok,大功告成了,現在讓我們用junit來測試一下吧:

service層不加:


throw new Exception("攔-路-虎");

執行結果:

技術分享圖片

可以看出來事務已經提交了,我們來看看數據庫裏面的變化:

tb_user表:

技術分享圖片

user_detail表:

技術分享圖片

然後在業務層加上:


throw new Exception("攔-路-虎");

運行結果:

技術分享圖片

仔細觀察劃綠色線的部分就能發現,事務已經回滾了,看數據庫表也是沒有記錄的

我們主鍵id由於是遞增的,因此我們還要確定一下事務是不是真的回滾了,我們把異常代碼去掉,然後再往裏面插入成功一次數據,運行後的數據庫表記錄如下:

tb_user:

技術分享圖片

user_detail:

技術分享圖片

大家仔細看id,已經是3了,說明原來事務成功回滾了

說明:其實connection對象不必每次都作為參數傳遞給方法,這裏只是為了更清楚的展示connection的流向,其實我們用ThreadLocal封裝成一個單例的時候就已經註定了本次訪問(即當前線程從controller層調用到dao層)所有get到的connection對象都是同一個;

最後,個人感覺這個程序有個非常要命的地方,就是我要給service層加事務代理,這樣就導致了sevice層的對象不能通過配置文件來實例化,正在糾結中。。以後還會優化,這只是簡單實現以下,真正的spring要復雜的多得多,第一次發表博客,以後也會多發一些,大家互相學習~

「框架」菜鳥簡單模仿一下spring的ioc和aop思想,歡迎大家進來閱讀指教