手写实现IoC和AOP
# 手写实现IoC和AOP
了解IoC和AOP思想后,先不考虑 Spring 是如何实现这两个思想的,此处以 拉钩-『银⾏转账』为案例,请分析该案例在代码层次有什么问题?分析之后使⽤我们已有知识解决这些问题。其实整个过程就是一步步分析并⼿写实现 IoC 和 AOP。
# 银⾏转账案例概述
通过一个Web界面实现,A账户转钱给B账户的功能。
# 界面
# 数据库表
create table account
(
name varchar(128) null comment '用户名',
money decimal(16, 4) null comment '账户金额',
cardNo varchar(255) not null comment '银行卡号' primary key
);
insert into transfer_bank.account (name, money, cardNo)
values ('李大雷', 200.0000, '6029621011000'),
('韩梅梅', 100.0000, '6029621011001');
# 代码调用关系
# 关键代码
# TransferServlet
@WebServlet(name="transferServlet",urlPatterns = "/transferServlet")
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());
}
// 3.响应
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().print(JsonUtils.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(from);
accountDao.updateAccountByCardNo(to);
}
}
# 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();
Connection con = connectionUtils.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 con = DruidUtils.getInstance().getConnection();
Connection con = connectionUtils.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;
}
}
# 问题分析
# 问题一:代码耦合
代码耦合,直接用new关键字将某个接口的实现直接实例化引用,不符合面向接口开发的原则。例如:
- TransferServiceImpl在使用dao层对象时也是直接通过
AccountDao accountDao = new JdbcAccountDaoImpl()
引用
假如说某一天技术架构发⽣⼀些变动,dao层的实现要使⽤其它技术,⽐如 Mybatis,我们的切换的成本及非常大:每⼀个 new 的地⽅都需要修改源代码,重新编译,同时⾯向接⼝开发的意义在哪里?
# 问题二:没有事务控制
Service层代码没有竟然还没有进⾏事务控制。如果转账过程中出现异常,将可能导致数据库数据异常,后果可能会很严重,尤其在⾦融业务。
# 解决思路
# 问题一解决思路
实例化对象的⽅式除了 new 之外,还有什么技术?
反射, Class.forName(“全限定类名”);我们可以把类的全限定类名配置在xml中
项⽬中往往有很多对象需要实例化,该怎么解耦合?
使⽤设计模式中的⼯⼚模式解耦合,另外,就在⼯⼚中使⽤反射技术实例化对象
更进一步:代码中能否只声明所需实例的接⼝类型,不出现 new 关键字、也不出现⼯⼚类?
能!声明⼀个变量并提供 set ⽅法,在反射的时候将所需要的对象注⼊进去。如下:
public class TransferServiceImpl implements TransferService { private AccountDao accountDao; public void setAccountDao(AccountDao accountDao) { this.accountDao = accountDao; } // ...... }
# 问题二解决思路
Service层没有添加事务控制,怎么办?
加上事务控制,通过TransactionManager管理起来,⼿动控制 JDBC 的Connection 事务。注意将Connection和当前线程绑定(即保证⼀个线程只有⼀个Connection,这样操作才针对的是同⼀个 Connection,进⽽控制的是同⼀个事务)。
随着Service层业务逻辑越来越多,重复的事务控制逻辑怎么办?
TransactionManager管理了事务,我们可以将事务的开始、提交、回滚看成一个个"切面",通过动态代理的方式织入每个代理Bean中。
# 案例代码改造
# 问题一改造
增加beans.xml,配置bean
<?xml version="1.0" encoding="UTF-8" ?> <beans> <!--id标识对象,class是类的全限定类名--> <bean id="accountDao" class="com.example.transfer.dao.impl.JdbcAccountDaoImpl"> </bean> <bean id="transferService" class="com.example.transfer.service.impl.TransferServiceImpl"> <!--set + name 锁定set方法,通过反射技术可以调用该方法传入对应的值--> <property name="AccountDao" ref="accountDao"/> </bean> </beans>
增加BeanFactory.java
/** * 工厂类,生产对象(使用反射技术) */ public class BeanFactory { /** * 任务一:读取解析xml,通过反射技术实例化对象并且存储待用(map集合) * 任务二:对外提供获取实例对象的接口(根据id获取) */ private static Map<String, Object> beanMap = new HashMap<>(); // 存储对象 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 (Element element : beanList) { // 处理每个bean元素,获取到该元素的id 和 class 属性 String id = element.attributeValue("id"); // accountDao String clazz = element.attributeValue("class"); // com.example.transfer.dao.impl.JdbcAccountDaoImpl // 通过反射技术实例化对象 Class<?> aClass = Class.forName(clazz); // 实例化之后的对象 Object o = aClass.newInstance(); // 存储到map中待用 beanMap.put(id, o); } // 实例化完成之后维护对象的依赖关系,检查哪些对象需要传值进入,根据它的配置,我们传入相应的值 // 有property子元素的bean就有传值需求 List<Element> propertyList = rootElement.selectNodes("//property"); // 解析property,获取父元素 //<property name="AccountDao" ref="accountDao"></property> for (Element element : propertyList) { String name = element.attributeValue("name"); String ref = element.attributeValue("ref"); // 找到当前需要被处理依赖关系的bean Element parent = element.getParent(); // 调用父元素对象的反射功能 String parentId = parent.attributeValue("id"); Object parentObject = beanMap.get(parentId); // 遍历父对象中的所有方法,找到 "set" + name Method[] methods = parentObject.getClass().getMethods(); for (Method method : methods) { // 该方法就是 setAccountDao(AccountDao accountDao) if (method.getName().equalsIgnoreCase("set" + name)) { method.invoke(parentObject, beanMap.get(ref)); } } // 把处理之后的parentObject重新放到map中 beanMap.put(parentId, parentObject); } } catch (DocumentException | ClassNotFoundException | IllegalAccessException | InstantiationException | InvocationTargetException e) { e.printStackTrace(); } } // 任务二:对外提供获取实例对象的接口(根据id获取) public static Object getBean(String id) { return beanMap.get(id); } }
修改 TransferServlet
@WebServlet(name="transferServlet",urlPatterns = "/transferServlet") public class TransferServlet extends HttpServlet { private TransferService transferService = (TransferService) BeanFactory.getBean("transferService"); // ...... }
修改 TransferServiceImpl
public class TransferServiceImpl implements TransferService { private AccountDao accountDao; public void setAccountDao(AccountDao accountDao) { this.accountDao = accountDao; } // ...... }
# 问题二改造
增加 ConnectionUtils
public class ConnectionUtils { // 存储当前线程的连接 private final ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); /** * 从当前线程获取连接 */ public Connection getCurrentThreadConn() throws SQLException { // 判断当前线程中是否已经绑定连接,如果没有绑定,需要从连接池获取一个连接绑定到当前线程 Connection connection = threadLocal.get(); if(connection == null) { // 从连接池拿连接并绑定到线程 connection = DruidUtils.getInstance().getConnection(); // 绑定到当前线程 threadLocal.set(connection); } return connection; } }
增加 TransactionManager 事务管理器类
public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } // 开启手动事务控制 public void beginTransaction() throws SQLException { connectionUtils.getCurrentThreadConn().setAutoCommit(false); } // 提交事务 public void commit() throws SQLException { connectionUtils.getCurrentThreadConn().commit(); } // 回滚事务 public void rollback() throws SQLException { connectionUtils.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(), (proxy, method, args) -> { Object result; 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(), (MethodInterceptor) (o, method, objects, methodProxy) -> { Object result; 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> <!--id标识对象,class是类的全限定类名--> <bean id="accountDao" class="com.example.transfer.dao.impl.JdbcAccountDaoImpl"> <property name="ConnectionUtils" ref="connectionUtils"/> </bean> <bean id="transferService" class="com.example.transfer.service.impl.TransferServiceImpl"> <!--set+ name 之后锁定到传值的set方法了,通过反射技术可以调用该方法传入对应的值--> <property name="AccountDao" ref="accountDao"/> </bean> <!--连接管理工具--> <bean id="connectionUtils" class="com.example.transfer.utils.ConnectionUtils"/> <!--事务管理器--> <bean id="transactionManager" class="com.example.transfer.utils.TransactionManager"> <property name="ConnectionUtils" ref="connectionUtils"/> </bean> <!--代理对象工厂--> <bean id="proxyFactory" class="com.example.transfer.factory.ProxyFactory"> <property name="TransactionManager" ref="transactionManager"/> </bean> </beans>
修改 JdbcAccountDaoImpl
public class JdbcAccountDaoImpl implements AccountDao { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } @Override public Account queryAccountByCardNo(String cardNo) throws Exception { // 把从连接池获取连接改成从当前线程获取 // Connection con = DruidUtils.getInstance().getConnection(); Connection con = connectionUtils.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 con = DruidUtils.getInstance().getConnection(); Connection con = connectionUtils.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(name = "transferServlet", urlPatterns = "/transferServlet") public class TransferServlet extends HttpServlet { // 改成从工厂获取代理对象(增强了事务控制的功能) private ProxyFactory proxyFactory = (ProxyFactory) BeanFactory.getBean("proxyFactory"); private TransferService transferService = (TransferService) proxyFactory.getJdkProxy(BeanFactory.getBean("transferService")); // ...... }