源碼分析 | 手寫mybait-spring核心功能(幹貨好文一次學會工廠bean、類代理、bean注冊的使用)
一、前言介紹
一個知識點的學習過程基本分為;運作helloworld、熟練使用api、源碼分析、核心專家。在分析mybaits以及mybatis-spring源碼之前,我也隻是簡單的使用,因為它好用。但是他是怎麼做的多半是憑自己的經驗去分析,但始終覺得這樣的感覺缺少點什麼,在幾次夙興夜寐,靡有朝矣之後決定徹底的研究一下,之後在去仿照着寫一版核心功能。依次來補全自己的技術棧的空缺。在現在技術知識像爆炸一樣迸發,而我們多半又忙于工作業務開發。就像一個不會修車的老司機,隻能一腳油門,一腳刹車的奔波。車速很快,但經不起壞,累覺不愛。好!為了解決這樣問題,也為了錢程似錦(形容錢多的想家裡的棉布一樣),努力!
開動之前先慶祝下我的iPhone4s又活了,還是那麼好用(嗯!有點卡);
二、以往章節
關于mybaits & spring 源碼分析以及demo功能的章節彙總,可以通過下列内容進行系統的學習,同時以下章節會有部分内容涉及到demo版本的mybaits;
源碼分析 | Mybatis接口沒有實作類為什麼可以執行增删改查
源碼分析 | 像盜墓一樣分析Spring是怎麼初始化xml并注冊bean的
源碼分析 | 基于jdbc實作一個Demo版的Mybatis
三、一碟小菜類代理
往往從最簡單的内容才有抓手。先看一個接口到實作類的使用,在将這部分内容轉換為代理類。
- 定義一個 IUserDao 接口并實作這個接口類
- interface IUserDao {
String queryUserInfo();
}
public class UserDao implements IUserDao {
@Override
public String queryUserInfo() {
return "實作類";
}
-
new() 方式執行個體化
IUserDao userDao = new UserDao();
userDao.queryUserInfo();
這是最簡單的也是最常用的使用方式,new 個對象。
-
proxy 方式執行個體化
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] classes = {IUserDao.class};
InvocationHandler handler = (proxy, method, args) -> "你被代理了 " + method.getName();
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler);
String res = userDao.queryUserInfo();
logger.info("測試結果:{}", res);
Proxy.newProxyInstance 代理類執行個體化方式,對應傳入類的參數即可
ClassLoader,是這個類加載器,我們可以擷取目前線程的類加載器
InvocationHandler 是代理後實際操作方法執行的内容,在這裡可以添加自己業務場景需要的邏輯,在這裡我們隻傳回方法名
測試結果:
23:20:18.841 [main] INFO org.itstack.demo.test.ApiTest - 測試結果:你被代理了 queryUserInfo
Process finished with exit code 0
四、盛宴來自Bean工廠
在使用Spring的時候,我們會采用注冊或配置檔案的方式,将我們的類交給Spring管理。例如;
UserDao是接口IUserDao的實作類,通過上面配置,就可以執行個體化一個類供我們使用,但如果IUserDao沒有實作類或者我們希望去動态改變他的實作類比如挂載到别的地方(像mybaits一樣),并且是由spring bean工廠管理的,該怎麼做呢?
-
FactoryBean的使用
FactoryBean 在spring起到着二當家的地位,它将近有70多個小弟(實作它的接口定義),那麼它有三個方法;
T getObject() throws Exception; 傳回bean執行個體對象
Class<?> getObjectType(); 傳回執行個體類類型
boolean isSingleton(); 判斷是否單例,單例會放到Spring容器中單執行個體緩存池中
那麼我們現在就将上面用到的代理類交給spring的FactoryBean進行管理,代碼如下;
ProxyBeanFactory.java & bean工廠實作類
public class ProxyBeanFactory implements FactoryBean {
@Override
public IUserDao getObject() throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] classes = {IUserDao.class};
InvocationHandler handler = (proxy, method, args) -> "你被代理了 " + method.getName();
return (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler);
}
@Override
public Class<?> getObjectType() {
return IUserDao.class;
}
@Override
public boolean isSingleton() {
return true;
}
spring-config.xml & 配置bean類資訊
ApiTest.test_IUserDao() & 單元測試
@Test
public void test_IUserDao() {
BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
String res = userDao.queryUserInfo();
logger.info("測試結果:{}", res);
一月 20, 2020 23:43:35 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
資訊: Loading XML bean definitions from class path resource [spring-config.xml]
23:43:35.440 [main] INFO org.itstack.demo.test.ApiTest - 測試結果:你被代理了 queryUserInfo
咋樣,神奇不!你的接口都不需要實作類,就被安排的明明白白的。記住這個方法FactoryBean和動态代理。
-
BeanDefinitionRegistryPostProcessor 類注冊
你是否有懷疑過你媳婦把你錢沒收了之後都存放到哪去了,為啥你每次get都那麼費勁,像垃圾回收了一樣,不可達。
好嘞,媳婦那就别想了,研究下你的bean都被注冊到哪了就可以了。在spring的bean管理中,所有的bean最終都會被注冊到類DefaultListableBeanFactory中,接下來我們就主動注冊一個被我們代理了的bean。
RegisterBeanFactory.java & 注冊bean的實作類
public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(ProxyBeanFactory.class);
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// left intentionally blank
}
這裡包含4塊主要内容,分别是;
實作BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry方法,擷取bean注冊對象
定義bean,GenericBeanDefinition,這裡主要設定了我們的代理類工廠。我們已經測試過他擷取一個代理類
建立bean定義處理類,BeanDefinitionHolder,這裡需要的主要參數;定義bean、bean名稱
最後将我們自己的bean注冊到spring容器中去,registry.registerBeanDefinition()
BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
String res = userDao.queryUserInfo();
logger.info("測試結果:{}", res);
一月 20, 2020 23:42:29 上午 org.springframework.beans.factory.support.DefaultListableBeanFactory registerBeanDefinition
資訊: Overriding bean definition for bean 'userDao' with a different definition: replacing [Generic bean: class [org.itstack.demo.bean.RegisterBeanFactory]; scope=; abstract=false; lazyInit=false; autowireMode=1; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in class path resource [spring-config.xml]] with [Generic bean: class [org.itstack.demo.bean.ProxyBeanFactory]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null]
23:42:29.754 [main] INFO org.itstack.demo.test.ApiTest - 測試結果:你被代理了 queryUserInfo
納尼?是不有一種滿腦子都是騷操作的感覺,自己注冊的bean自己知道在哪了,咋回事了。
五、老闆郎上主食呀(mybaits-spring)
如果通過上面的知識點;代理類、bean工廠、bean注冊,将我們一個沒有實作類的接口安排的明明白白,讓他執行啥就執行啥,那麼你是否可以想到,這個沒有實作類的接口,可以通過我們的折騰,去調用到我們的mybaits呢!
如下圖,通過mybatis使用的配置,我們可以看到資料源DataSource交給SqlSessionFactoryBean,SqlSessionFactoryBean執行個體化出的SqlSessionFactory,再交給MapperScannerConfigurer。而我們要實作的就是MapperScannerConfigurer這部分;
-
需要實作哪些核心類
為了更易了解也更易于對照,我們将實作mybatis-spring中的流程核心類,如下;
MapperFactoryBean {給每一個沒有實作類的接口都代理一個這樣的類,用于操作資料庫執行crud}
MapperScannerConfigurer {掃描包下接口類,免去配置。這樣是上圖中核心配置類}
SimpleMetadataReader {這個類完全和mybaits-spring中的類一樣,為了解析class檔案。如果你對類加載處理很好奇,可以閱讀我的《用java實作jvm虛拟機》}
SqlSessionFactoryBean {這個類核心内容就一件事,将我們寫的demo版的mybaits結合進來}
在分析之前先看下我們實作主食是怎麼食用的,如下;
<property name="resource" value="spring/mybatis-config-datasource.xml"/>
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
<!-- 給出需要掃描Dao接口包 -->
<property name="basePackage" value="org.itstack.demo.dao"/>
-
(類介紹)SqlSessionFactoryBean
這類本身比較簡單,主要實作了FactoryBean, InitializingBean用于幫我們處理mybaits核心流程類的加載處理。(關于demo版的mybaits已經在上文中提供學習連結)
SqlSessionFactoryBean.java
public class SqlSessionFactoryBean implements FactoryBean, InitializingBean {
private String resource;
private SqlSessionFactory sqlSessionFactory;
@Override
public void afterPropertiesSet() throws Exception {
try (Reader reader = Resources.getResourceAsReader(resource)) {
this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public SqlSessionFactory getObject() throws Exception {
return sqlSessionFactory;
}
@Override
public Class<?> getObjectType() {
return sqlSessionFactory.getClass();
}
@Override
public boolean isSingleton() {
return true;
}
public void setResource(String resource) {
this.resource = resource;
}
實作InitializingBean主要用于加載mybatis相關内容;解析xml、構造SqlSession、連結資料庫等
FactoryBean,這個類我們介紹過,主要三個方法;getObject()、getObjectType()、isSingleton()
-
(類介紹)MapperScannerConfigurer
這類的内容看上去可能有點多,但是核心事情也就是将我們的dao層接口掃描、注冊
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor {
private String basePackage;
private SqlSessionFactory sqlSessionFactory;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
try {
// classpath*:org/itstack/demo/dao/**/*.class
String packageSearchPath = "classpath*:" + basePackage.replace('.', '/') + "/**/*.class";
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
for (Resource resource : resources) {
MetadataReader metadataReader = new SimpleMetadataReader(resource, ClassUtils.getDefaultClassLoader());
ScannedGenericBeanDefinition beanDefinition = new ScannedGenericBeanDefinition(metadataReader);
String beanName = Introspector.decapitalize(ClassUtils.getShortName(beanDefinition.getBeanClassName()));
beanDefinition.setResource(resource);
beanDefinition.setSource(resource);
beanDefinition.setScope("singleton");
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(sqlSessionFactory);
beanDefinition.setBeanClass(MapperFactoryBean.class);
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// left intentionally blank
}
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
類的掃描注冊,classpath:org/itstack/demo/dao/**/.class,解析calss檔案擷取資源資訊;Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
周遊Resource,這裡就你的class資訊,用于注冊bean。ScannedGenericBeanDefinition
這裡有一點,bean的定義設定時候,是把beanDefinition.setBeanClass(MapperFactoryBean.class);設定進去的。同時在前面給他設定了構造參數。(細細品味)
最後執行注冊registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
-
(類介紹)MapperFactoryBean
這個類就非常有意思了,因為你所有的dao接口類,實際就是他。他這裡幫你執行你對sql的所有操作的分發處理。為了更加簡化清晰,目前這裡隻實作了查詢部分,在mybatis-spring源碼中分别對select、update、insert、delete、其他等做了操作。
public class MapperFactoryBean implements FactoryBean {
private Class<T> mapperInterface;
private SqlSessionFactory sqlSessionFactory;
public MapperFactoryBean(Class<T> mapperInterface, SqlSessionFactory sqlSessionFactory) {
this.mapperInterface = mapperInterface;
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public T getObject() throws Exception {
InvocationHandler handler = (proxy, method, args) -> {
System.out.println("你被代理了,執行SQL操作!" + method.getName());
try {
SqlSession session = sqlSessionFactory.openSession();
try {
return session.selectOne(mapperInterface.getName() + "." + method.getName(), args[0]);
} finally {
session.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return method.getReturnType().newInstance();
};
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mapperInterface}, handler);
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
@Override
public boolean isSingleton() {
return true;
}
T getObject(),中是一個java代理類的實作,這個代理類對象會被挂到你的注入中。真正調用方法内容時會執行到代理類的實作部分,也就是“你被代理了,執行SQL操作!”
InvocationHandler,代理類的實作部分非常簡單,主要開啟SqlSession,并通過固定的key;“org.itstack.demo.dao.IUserDao.queryUserInfoById”執行SQL操作;
session.selectOne(mapperInterface.getName() + "." + method.getName(), args[0]);
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
SELECT id, name, age, createTime, updateTime
FROM user
where id = #{id}
</select>
最終傳回了執行結果,關于查詢到結果資訊會反射操作成對象類,這部分内容可以遇到demo版本的mybatis
六、酒倒滿走一個
好!到這一切開發内容就完成了,測試走一個。
mybatis-config-datasource.xml & 資料源配置
<?xml version="1.0" encoding="UTF-8"?>
/p>
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_ddd?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/User_Mapper.xml"/>
<mapper resource="mapper/School_Mapper.xml"/>
</mappers>
test-config.xml & 配置xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
default-autowire="byName">
<context:component-scan base-package="org.itstack"/>
<aop:aspectj-autoproxy/>
<bean id="sqlSessionFactory" class="org.itstack.demo.like.spring.SqlSessionFactoryBean">
<property name="resource" value="spring/mybatis-config-datasource.xml"/>
</bean>
<bean class="org.itstack.demo.like.spring.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
<!-- 給出需要掃描Dao接口包 -->
<property name="basePackage" value="org.itstack.demo.dao"/>
</bean>
SpringTest.java & 單元測試
public class SpringTest {
private Logger logger = LoggerFactory.getLogger(SpringTest.class);
@Test
public void test_ClassPathXmlApplicationContext() {
BeanFactory beanFactory = new ClassPathXmlApplicationContext("test-config.xml");
IUserDao userDao = beanFactory.getBean("IUserDao", IUserDao.class);
User user = userDao.queryUserInfoById(1L);
logger.info("測試結果:{}", JSON.toJSONString(user));
}
測試結果;
一月 20, 2020 23:51:43 上午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
資訊: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@30b8a058: startup date [Mon Jan 20 23:51:43 CST 2020]; root of context hierarchy
一月 20, 2020 23:51:43 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
資訊: Loading XML bean definitions from class path resource [test-config.xml]
你被代理了,執行SQL操作!queryUserInfoById
2020-01-20 23:51:45.592 [main] INFO org.itstack.demo.SpringTest[26] - 測試結果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}
酒幹熱火笑紅塵,春秋幾載年輪,不問。回首皆是Spring!Gun!變心!你被代理了!
七、綜上總結
通過這些核心關鍵類的實作;SqlSessionFactoryBean、MapperScannerConfigurer、SqlSessionFactoryBean,我們将spring與mybaits集合起來使用,解決了沒有實作類的接口怎麼處理資料庫CRUD操作
那麼這個知識點可以用到哪裡,不要隻想着面試!在我們業務開發中是不會有很多其他資料源操作,比如ES、Hadoop、資料中心等等,包括自建。那麼我們就可以做成一套統一資料源處理服務,以優化服務開發效率
由于這次工程類是在itstack-demo-code-mybatis中繼續開發,如果需要擷取源碼可以關注公衆号:bugstack蟲洞棧,回複:源碼分析
八、推薦閱讀
這麼折騰學習畢業進大廠不是問題
工作兩年履歷寫的差教你優化
講一下我自己的學習路線,給你一些參考
基于Springboot的中間件開發,了解一下
公衆号:bugstack蟲洞棧 | 作者小傅哥多年從事一線網際網路 Java 開發的學習曆程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心内容。如果能為您提供幫助,請給予支援(關注、點贊、分享)!
作者:小傅哥
部落格:
https://bugstack.cn- 彙總系列原創專題文章
原文位址
https://www.cnblogs.com/xiaofuge/p/13081105.html