手把手帶你實現 Spring 中的 IoC 和 AOP
一、核心思想
IoC
和AOP
不是spring
提出來的,在spring
之前就已經存在,只不過更偏向理論化,spring
在技術層面把這兩個思想做了非常好的實現。在手寫spring
中的IoC
和AOP
之前,我們先來了解IoC
和AOP
的思想。
1、IoC
1.1 什麼是 IoC?
IoC
Inversion of Control
(控制反轉、反轉控制),注意它是一個技術思想,不是技術實現。
描述的事情:Java
開發領域對象的創建、管理的問題。
傳統開發模式:比如類 A 依賴類 B,往往會在類 A 中new
一個 B 的對象。
IoC
思想下開發模式:我們不再自己去new
對象了,而是由IoC
容器 (Spring
框架) 去幫助我們實例化對象並且管理它,我們需要哪個對象,去問IoC
容器要即可。
我們喪失了一個權力 (創建、管理對象的權力),得到一個福利 (不用考慮對象的創建、管理等一系列事情)。
爲什麼叫控制反轉?
控制:指的是對象創建 (實例化、管理) 的權力。
反轉:控制權交給外部環境了 (Spring
框架、IoC
容器)。
1.2 IoC 解決了什麼問題
IoC 解決對象之家的耦合問題
1.3 IoC 和 DI 的區別
DI
:Dependency Injection
(依賴注入)
如何理解:
IoC 和 DI 描述的是同一件事情,只不過角度不一樣罷了。
2、AOP
2.1 什麼是 AOP?
AOP
:Aspect Oriented Programming
面向切面編程 / 面向方面編程
AOP
是OOP
的延續,我們從OOP
說起
OOP
三大特徵:封裝、繼承、多態
OOP
是一種垂直繼承體系:
OOP
編程思想可以解決大多數的代碼重複問題,但是有一些情況是處理不了的,比如下面的在頂級父類Animal
中的多個方法中相同位置出現了重複代碼,OOP
就解決不了了。
橫切邏輯代碼:
橫切邏輯代碼存在什麼問題:
-
橫切代碼重複問題
-
橫切邏輯代碼和業務代碼混雜在一起,代碼臃腫,維護不方便。
AOP
出場,AOP
獨闢蹊徑提出橫向抽取機制,將橫切邏輯代碼和業務邏輯代碼分析:
代碼拆分容易,那麼如何在不改變原有業務邏輯的情況下,悄無聲息的把橫切邏輯代碼應用到原有的業 務邏輯中,達到和原來一樣的效果,這個是比較難的。
2.2 AOP 解決的什麼問題
在不改變原有業務邏輯情況下,增強橫切邏輯代碼,根本上解耦合,避免橫切邏輯代碼重複。
2.3 爲什麼叫面向切面編程
「切」
: 指的是橫切邏輯,原有業務邏輯代碼我們不能動,只能操作橫切邏輯代碼,所以面向橫切邏輯。
「面」
: 橫切邏輯代碼往往要影響的是很多個方法,每一個方法都如同一個點,多個點構成面,有一個 面的概念在裏面。
二、手寫實現 IoC 和 AOP
上面我們理解了IoC
和AOP
的思想。我們先不考慮 Spring
是如何實現這兩個思想的,我這裏準備了一 個『銀行轉賬』的案例,** 請分析該案例在代碼層次有什麼問題?** 分析之後使用我們已有知識解決這些問題 (痛點)。其實這個過程我們就是在一步步分析並手寫實現 IoC 和 AOP。
1、銀行轉賬案例界面
2、銀行轉賬案例表結構
3、銀行轉賬案例代碼調用關係
4、銀行轉賬案例關鍵代碼
TransferServlet
@WebServlet()
public class TransferServlet extends HttpServlet {
// 1. 實例化service層對象
private TransferService transferService = new TransferServiceImpl();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 設置請求體的字符編碼
req.setCharacterEncoding("UTF-8");
String fromCardNo = req.getParameter("fromCardNo");
String toCardNo = req.getParameter("toCardNo");
String moneyStr = req.getParameter("money");
int money = Integer.parseInt(moneyStr);
Result result = new Result();
try {
// 2. 調用service層方法
transferService.transfer(fromCardNo,toCardNo,money);
result.setStatus("200");
} catch (Exception e) {
e.printStackTrace();
result.setStatus("201");
result.setMessage(e.toString());
}
// 響應
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().print(JsonUtil.object2Json(result));
}
}
TransferService 接口及實現類
public interface TransferService {
void transfer(String fromCardNo, String toCardNo, int money) throws Exception;
}
public class TransferServiceImpl implements TransferService {
private AccountDao accountDao = new JdbcAccountDaoImpl();
@Override
public void transfer(String fromCardNo, String toCardNo, int money) throws Exception {
Account from = accountDao.queryAccountByCardNo(fromCardNo);
Account to = accountDao.queryAccountByCardNo(toCardNo);
from.setMoney(from.getMoney() - money);
to.setMoney(to.getMoney() + money);
accountDao.updateAccountByCardNo(to);
accountDao.updateAccountByCardNo(from);
}
}
AccountDao 層接口及基於 Jdbc 的實現類
public interface AccountDao {
Account queryAccountByCardNo(String cardNo) throws Exception;
int updateAccountByCardNo(Account account) throws Exception;
}
public class JdbcAccountDaoImpl implements AccountDao {
@Override
public Account queryAccountByCardNo(String cardNo) throws Exception {
// 從連接池獲取連接
Connection con = DruidUtils.getInstance().getConnection();
String sql = "select * from account where cardNo = ?";
PreparedStatement preparedStatement = con.prepareStatement(sql);
preparedStatement.setString(1, cardNo);
ResultSet resultSet = preparedStatement.executeQuery();
Account account = new Account();
while(resultSet.next()) {
account.setCardNo(resultSet.getString("cardNo"));
account.setName(resultSet.getString("name"));
account.setMoney(resultSet.getInt("money"));
}
resultSet.close();
preparedStatement.close();
// con.close();
return account;
}
@Override
public int updateAccountByCardNo(Account account) throws Exception {
// 從連接池獲取連接
Connection con = DruidUtils.getInstance().getConnection();
String sql = "update account set money = ? where cardNo = ?";
PreparedStatement preparedStatement = con.prepareStatement(sql);
preparedStatement.setInt(1, account.getMoney());
preparedStatement.setString(2, account.getCardNo());
int i = preparedStatement.executeUpdate();
preparedStatement.close();
con.close();
return i;
}
}
5、銀行轉賬案例代碼問題分析
(1)、問題一: 在上述案例實現中,service
層實現類在使用 dao
層對象時,直接在 TransferServiceImpl
中通過 AccountDao accountDao = new JdbcAccountDaoImpl()
獲得了 dao
層對 象,然而一個 new
關鍵字卻將 TransferServiceImpl
和 dao
層具體的一個實現類 JdbcAccountDaoImpl
耦合在了一起,如果說技術架構發生一些變動,dao
層的實現要使用其它技術, 比如 Mybatis
,思考切換起來的成本? 每一個 new
的地方都需要修改源代碼,重新編譯,面向接口開發的意義將大打折扣?
(2)、問題二: service
層代碼沒有竟然還沒有進行事務控制?! 如果轉賬過程中出現異常,將可能導致數據庫數據錯亂,後果可能會很嚴重,尤其在金融業務。
6、 問題解決思路
針對問題一思考:
-
實例化對象的方式除了
new
之外,還有什麼技術?反射
(需要把類的全限定類名配置在xml
中) -
考慮使用設計模式中的工廠模式解耦合,另外項目中往往有很多對象需要實例化,那就在工廠中使 用反射技術實例化對象,工廠模式很合適。
-
更進一步,代碼中能否只聲明所需實例的接口類型,不出現
new
也不出現工廠類的字眼,如下圖? 能! 聲明一個變量並提供set
方法,在反射的時候將所需要的對象注入進去。
針對問題二思考:
service
層沒有添加事務控制,怎麼辦? 沒有事務就添加上事務控制,手動控制JDBC
的Connection
事務,但要注意將Connection
和當前線程綁定 (即保證一個線程只有一個Connection
,這樣操作才針對的是同一個Connection
,進而控制的是同一個事務)
7、案例代碼改造
(1)、針對問題一的代碼改造
-
beans.xml
<!--id標識對象,class是類的全限定類名--> <bean> <property /> </bean> <bean> <!--set+ name 之後鎖定到傳值的set方法了,通過反射技術可以調用該方法傳入對應的值--> <property ></property> </bean>
-
增加 BeanFactory.java
/** * 工廠類,生產對象(使用反射技術) */ public class BeanFactory { /** * 任務一:讀取解析xml,通過反射技術實例化對象並且存儲待用(map集合) * 任務二:對外提供獲取實例對象的接口(根據id獲取) */ private static Map<String, Object> map = Maps.newHashMap(); // 存儲對象 static { // 任務一:讀取解析xml,通過反射技術實例化對象並且存儲待用(map集合) // 加載xml InputStream resourceAsStream = BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml"); // 解析xml SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(resourceAsStream); Element rootElement = document.getRootElement(); List<Element> beanList = rootElement.selectNodes("//bean"); for (int i = 0; i < beanList.size(); i++) { Element element = beanList.get(i); // 處理每個bean元素,獲取到該元素的id 和 class 屬性 String id = element.attributeValue("id"); // accountDao String clazz = element.attributeValue("class"); // com.riemann.dao.impl.JdbcAccountDaoImpl // 通過反射技術實例化對象 Class<?> aClass = Class.forName(clazz); Object o = aClass.newInstance(); // 實例化之後的對象 // 存儲到map中待用 map.put(id, o); } // 實例化完成之後維護對象的依賴關係,檢查哪些對象需要傳值進入,根據它的配置,我們傳入相應的值 // 有property子元素的bean就有傳值需求 List<Element> propertyList = rootElement.selectNodes("//property"); // 解析property,獲取父元素 for (int i = 0; i < propertyList.size(); i++) { Element element = propertyList.get(i); //<property ></property> String name = element.attributeValue("name"); String ref = element.attributeValue("ref"); // 找到當前需要被處理依賴關係的bean Element parent = element.getParent(); // 調用父元素對象的反射功能 String parentId = parent.attributeValue("id"); Object parentObject = map.get(parentId); // 遍歷父對象中的所有方法,找到"set" + name Method[] methods = parentObject.getClass().getMethods(); for (int j = 0; j < methods.length; j++) { Method method = methods[j]; if(method.getName().equalsIgnoreCase("set" + name)) { // 該方法就是 setAccountDao(AccountDao accountDao) method.invoke(parentObject, map.get(ref)); } } // 把處理之後的parentObject重新放到map中 map.put(parentId,parentObject); } } catch (DocumentException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } // 任務二:對外提供獲取實例對象的接口(根據id獲取) public static Object getBean(String id) { return map.get(id); } }
-
修改 TransferServlet
@WebServlet() public class TransferServlet extends HttpServlet { private TransferService transferService = (TransferService) BeanFactory.getBean("transferService"); ... }
-
修改 TransferServiceImpl
public class TransferServiceImpl implements TransferService {
private AccountDao accountDao;
// 構造函數傳值/set方法傳值
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
...
}
(2)、針對問題二的代碼改造
-
增加 ConnectionUtil
public class ConnectionUtil { private ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); // 存儲當前線程的連接 /** * 從當前線程獲取連接 */ public Connection getCurrentThreadConn() throws SQLException { /** * 判斷當前線程中是否已經綁定連接,如果沒有綁定,需要從連接池獲取一個連接綁定到當前線程 */ Connection connection = threadLocal.get(); if (connection == null) { // 從連接池拿連接並綁定到線程 connection = DruidUtil.getInstance().getConnection(); // 綁定到當前線程 threadLocal.set(connection); } return connection; } }
-
增加 TransactionManager 事務管理器類
/** * 事務管理器類:負責手動事務的開啓、提交、回滾 */ public class TransactionManager { private ConnectionUtil connectionUtil; public void setConnectionUtil(ConnectionUtil connectionUtil) { this.connectionUtil = connectionUtil; } // 開啓手動事務控制 public void beginTransaction() throws SQLException { connectionUtil.getCurrentThreadConn().setAutoCommit(false); } // 提交事務 public void commit() throws SQLException { connectionUtil.getCurrentThreadConn().commit(); } // 回滾事務 public void rollback() throws SQLException { connectionUtil.getCurrentThreadConn().rollback(); } }
-
增加 ProxyFactory 代理工廠類
/** * 代理對象工廠:生成代理對象的 */ public class ProxyFactory { private TransactionManager transactionManager; public void setTransactionManager(TransactionManager transactionManager) { this.transactionManager = transactionManager; } /** * Jdk動態代理 * @param obj 委託對象 * @return 代理對象 */ public Object getJdkProxy(Object obj) { // 獲取代理對象 return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try{ // 開啓事務(關閉事務的自動提交) transactionManager.beginTransaction(); result = method.invoke(obj, args); // 提交事務 transactionManager.commit(); } catch (Exception e) { e.printStackTrace(); // 回滾事務 transactionManager.rollback(); // 拋出異常便於上層servlet捕獲 throw e; } return result; } }); } /** * 使用cglib動態代理生成代理對象 * @param obj 委託對象 * @return */ public Object getCglibProxy(Object obj) { return Enhancer.create(obj.getClass(), new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { Object result = null; try{ // 開啓事務(關閉事務的自動提交) transactionManager.beginTransaction(); result = method.invoke(obj, objects); // 提交事務 transactionManager.commit(); } catch (Exception e) { e.printStackTrace(); // 回滾事務 transactionManager.rollback(); // 拋出異常便於上層servlet捕獲 throw e; } return result; } }); } }
-
修改 beans.xml
<?xml version="1.0" encoding="UTF-8" ?> <!--跟標籤beans,裏面配置一個又一個的bean子標籤,每一個bean子標籤都代表一個類的配置--> <beans> <!--id標識對象,class是類的全限定類名--> <bean> <property /> </bean> <bean> <!--set+ name 之後鎖定到傳值的set方法了,通過反射技術可以調用該方法傳入對應的值--> <property ></property> </bean> <!--配置新增的三個Bean--> <bean></bean> <!--事務管理器--> <bean> <property /> </bean> <!--代理對象工廠--> <bean> <property /> </bean> </beans>
-
修改 JdbcAccountDaoImpl
public class JdbcAccountDaoImpl implements AccountDao { private ConnectionUtil connectionUtil; public void setConnectionUtil(ConnectionUtil connectionUtil) { this.connectionUtil = connectionUtil; } @Override public Account queryAccountByCardNo(String cardNo) throws Exception { // 從連接池獲取連接 // Connection con = DruidUtils.getInstance().getConnection(); Connection con = connectionUtil.getCurrentThreadConn(); String sql = "select * from account where cardNo = ?"; PreparedStatement preparedStatement = con.prepareStatement(sql); preparedStatement.setString(1, cardNo); ResultSet resultSet = preparedStatement.executeQuery(); Account account = new Account(); while(resultSet.next()) { account.setCardNo(resultSet.getString("cardNo")); account.setName(resultSet.getString("name")); account.setMoney(resultSet.getInt("money")); } resultSet.close(); preparedStatement.close(); // con.close(); return account; } @Override public int updateAccountByCardNo(Account account) throws Exception { // 從連接池獲取連接 // 改造爲:當前線程當中獲取綁定的從connection連接 //Connection con = DruidUtils.getInstance().getConnection(); Connection con = connectionUtil.getCurrentThreadConn(); String sql = "update account set money=? where cardNo=?"; PreparedStatement preparedStatement = con.prepareStatement(sql); preparedStatement.setInt(1, account.getMoney()); preparedStatement.setString(2, account.getCardNo()); int i = preparedStatement.executeUpdate(); preparedStatement.close(); //con.close(); return i; } }
-
修改 TransferServlet
@WebServlet() public class TransferServlet extends HttpServlet { // 原本寫法 // 1. 實例化service層對象 // private TransferService transferService = new TransferServiceImpl(); // private TransferService transferService = (TransferService) BeanFactory.getBean("transferService"); // 從工廠獲取委託對象(委託對象是增強了事務控制的功能) // 改造寫法 // 首先從BeanFactory獲取到proxyFactory代理工廠的實例化對象 private ProxyFactory proxyFactory = (ProxyFactory) BeanFactory.getBean("proxyFactory"); private TransferService transferService = (TransferService) proxyFactory.getJdkProxy(BeanFactory.getBean("transferService")) ; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 設置請求體的字符編碼 req.setCharacterEncoding("UTF-8"); String fromCardNo = req.getParameter("fromCardNo"); String toCardNo = req.getParameter("toCardNo"); String moneyStr = req.getParameter("money"); int money = Integer.parseInt(moneyStr); Result result = new Result(); try { // 2. 調用service層方法 transferService.transfer(fromCardNo,toCardNo,money); result.setStatus("200"); } catch (Exception e) { e.printStackTrace(); result.setStatus("201"); result.setMessage(e.toString()); } // 響應 resp.setContentType("application/json;charset=utf-8"); resp.getWriter().print(JsonUtil.object2Json(result)); } }
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/AU_tbAso4HGyw5ASvWvGvw