用使用過Spring JPA的同學是不是覺得非常好用呢?還有就是Mybatis 爲什麼定義了一個接口就可以訪問數據庫了呢?這裏我們實現一個簡單版的。
Spring JPA是怎麼使用的
@Repository
public interface UserDao extends JpaRepository<User, Long> { // 首先這是一個interface,繼承interface JpaRepository ,並且模板聲明操作的對象及key是什麼類型
/**
* 根據名字查找用戶
* @param name 名字
* @return 用戶
*/
User findByNameEquals(String name); // 只用定義好一個方法就好,不用實現。
}
這裏可以可以先告訴大家結論:SpringData系下的JPA,ElasticSearch等等操作都是骨架上使用了@Configuration + 動態代理 + FactoryBean 三個元素配合使用造出來的。這裏只討論骨架是如何實現的,有童鞋一看這三個元素就直接明白了應該也是個大神。
這裏只討論SpringData是如何粘合在一起的,不討論細節。
我實現一個簡單版的SpringData,幫助大家理解。
實現大家得了解FactoryBean是什麼東東,這個大家網上查查,一堆資料,如果不明白可以關注我的微信公衆號,本人非常樂意解答。
我需要創建一個FactoryBean,用來生成每個繼承了 JpaTemplate
的 interface 的bean。並且註冊到Spring容器中。
以下是第一版 JpaFactoryBean
代碼,並不是JpaFactoryBean 的完全體。
public class JpaFactoryBean<T extends JpaTemplate> implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
return null;
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return true;
}
}
JpaTemplate
是什麼呢?JpaTemplate
是我實現的一個interface。
public interface JpaTemplate {
void findMethod();
void insertMethod();
void deleteMethod();
void updateMethod();
}
然後我的UserDao層想自己繼承這個interface。(簡化:這裏不引入JpaTemplate是泛型的情況。)
public interface UserDao extends JpaTemplate{
void findByUserDao();
}
這樣我們就大概實現了FactoryBean,這裏已經有上面原始Spring Jpa的樣子了,我們先立下flag(要實現的目標)。然後再思考如何到達這個目標。
然後重點看@Configuration部分,這部分是最複雜的,也是最重要的。
其實Spring體系就是一個大的裝飾模式,ApplicationContext
就是對Resource
的修飾(ApplicationContext就是Resource體系的暴露給用戶使用的接口)。由此可見資源體系在Spring中的重要定位。
接下來,我將實現我需要將繼承了JapTemplate的interface註冊到Spring容器中(暫時不考慮類繼承JapTemplate的情況)。爲什麼呢?原因有點複雜,和Spring啓動加載的順序有關,這裏不展開講。
掃描想要的interface
掃描繼承或實現了interface JpaTemplate的類,如果沒看我上一篇文章又看不懂我下面代碼的童鞋這裏可以看我的微信公衆號:程序袁小黑,在理論目錄的Spring判斷查找Spring是如何加載的class。這裏有比較詳細的介紹。
public class JpaScanner extends ClassPathScanningCandidateComponentProvider {
private boolean considerNestedRepositoryInterfaces;
private final BeanDefinitionRegistry registry; //BeanDefinition註冊器
public JpaScanner(Iterable<? extends TypeFilter> includeFilters, BeanDefinitionRegistry registry) {
super(false); // 不要使用默認的過濾器。
Assert.notNull(includeFilters, "Include filters must not be null!");
Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
this.registry = registry;
if (includeFilters.iterator().hasNext()) {
for (TypeFilter filter : includeFilters) {
addIncludeFilter(filter);
}
} else {
//dao 過濾器,要繼承了BaseBizEsDao,或者 JpaTemplate 的類纔行。
super.addIncludeFilter(new AssignableTypeFilter(JpaTemplate.class));
super.addExcludeFilter(new ClassExcludeFilter(JpaTemplate.class));
}
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return true; //這裏是針對掃描出來的beanDefintion進行二次篩選,看看結果集要的是哪些。這裏我胃口大,全都要了。
}
/**
* dao 過濾器,要繼承了 JpaTemplate 的接口或實現了的類纔行。
*/
@Override
public void addIncludeFilter(@NonNull TypeFilter includeFilter) {
super.addIncludeFilter(includeFilter);
}
@Override
@NonNull
public Set<BeanDefinition> findCandidateComponents(@NonNull String basePackage) {
return super.findCandidateComponents(basePackage);
}
@Override
protected BeanDefinitionRegistry getRegistry() {
return registry;
}
/**
* 去掉針對的class
*/
private static class ClassExcludeFilter extends AbstractTypeHierarchyTraversingFilter {
private final Set<String> classNames = new HashSet<>();
ClassExcludeFilter(Object... sources) {
super(false, false);
for (Object source : sources) {
if (source instanceof Class<?>) {
this.classNames.add(((Class<?>) source).getName());
}
}
}
protected boolean matchClassName(@NonNull String className) {
return this.classNames.contains(className);
}
}
}
上面代碼的功能是掃描某個路徑下的文件並解析成Set<BeanDefintion>
。
下一步大家想到什麼嗎?就是我們手動註冊到Spring容器中了。怎麼註冊容器到Spring中呢?Spring提供了一個了一個ImportBeanDefinitionRegistrar
interface給我們,這個interface類似Aware類型的interface,但是經常搭配@Import使用,而@Import搭配@Configuration使用,所以我將其視爲是@Configuration一部分。
下面是實現ImportBeanDefinitionRegistrar
並註冊掃描到的Set<BeanDefintion>
到Spring Boot的容器中。
public class JpaRegistry implements ResourceLoaderAware , ImportBeanDefinitionRegistrar, EnvironmentAware {
private final String baseScannerPackages ="com.taldh.springdata.dao";
private ResourceLoader resourceLoader;
private Environment environment;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//這個方法實現不符合單一原則,這裏只是演示,大家不要學習。
JpaScanner jpaScanner = new JpaScanner(Collections.emptySet(), registry);
jpaScanner.setEnvironment(environment);
jpaScanner.setResourceLoader(resourceLoader);
Set<BeanDefinition> candidateComponents = jpaScanner.findCandidateComponents(baseScannerPackages);
candidateComponents.forEach(component -> {
// 玄機在這裏,註冊的時候註冊進去的是FactoryBean,而不是直接掃描到的Bean。
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(JpaFactoryBean.class.getName());
Class<?> tClazz = null;
try {
tClazz = Class.forName(component.getBeanClassName());
} catch (ClassNotFoundException e) {
// 這裏不會出現異常,因爲class是經過掃描出來的。所以忽略這個異常即可。
}
builder.addConstructorArgValue(tClazz);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
// 最終的註冊結果如下:注意beanName是userDao,beanClass是FactoryBean,這樣我使用@Autowire的時候,Spring的AutowireBostPostProcessor處理器就會幫我把userDao對應的FactoryBean注入到對應的field中。
registry.registerBeanDefinition( StringUtils.uncapitalize(Objects.requireNonNull(tClazz).getSimpleName()), beanDefinition);
});
}
}
上面最重點的部分是:registerBeanDefinitions
函數,至於ResourceLoaderAware,EnvironmentAware 是Aware體系的一部分,Aware網上一大堆資料,我在這裏就不想丟別人的書包了。
registerBeanDefinitions
主要分兩步:
- 掃描bean,使用的是前面的scanner類。
- 註冊bean,最關鍵的點:註冊到BeanFactory的beanName是userDao,beanClass是FactoryBean,這樣我使用@Autowire的時候,Spring的AutowireBostPostProcessor處理器就會幫我把userDao對應的FactoryBean注入到對應的field中。
註冊的細節搞定了,那麼註冊這個動作怎麼完成呢?
還記得我們之前說的@Import嗎?我們藉助它來完成。
@Configuration
@Import(JpaRegistry.class)
public class JpaConfig {
}
@Import方法會幫我們觸發registerBeanDefinitions
, 爲什麼要加上呢?直接@Component不可以嗎?使用@Component有風險,這個涉及Spring加載Bean的啓動順序,可以這麼說:使用@Configuration的註冊優先順序高於@Component(本人看的Spring Boot的ApplicationContext是這樣,但是其他的Context比如Xml,Annotation,這些並不是)。我想要在Spring Boot自動掃描之前先,自己把bean註冊到容器中,避免有的使用者這樣註解interface。
@Repository
public interface UserDao extends JpaTemplate{
void findByUserDao();
}
好了,@Configuration的部分講解到這裏結束。
JpaFactoryBean
然後接下來的事情就簡單了,我主要實現Factorybean,讓FactoryBean的getObject返回構造好的userDao對象就好了。
FactoryBean的getObject可以符合我們創建的對象,放入spring的單例容器中。
public class JpaFactoryBean<T extends JpaTemplate> implements FactoryBean<T>, InitializingBean { //InitializingBean是一個鉤子方法。實現afterPropertiesSet會讓我們創建bean的時候,先執行afterPropertiesSet。
private final Class<?> tClass;
private T dao;
public JpaFactoryBean(Class<?> tClass) {
this.tClass = tClass;
}
@Override
public T getObject() throws Exception {
return dao;
}
@Override
public Class<?> getObjectType() {
return tClass;
}
@Override
public boolean isSingleton() {
return true;
}
@SuppressWarnings("unchecked")
@Override
public void afterPropertiesSet() throws Exception {
// interface在jvm中是不能運行的,我得給它生成代理類。
// 這個實現也不符合單一原則,這裏只是案例。
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTargetClass(tClass);
proxyFactory.setProxyTargetClass(false);
proxyFactory.addAdvice((MethodInterceptor) methodInvocation -> {
System.out.println("開始執行方法:"+methodInvocation.getMethod());
Object result = null;
switch (methodInvocation.getMethod().getName()) {
case "insertMethod":
System.out.println("執行數據庫的insert的操作");
result = 1;
break;
case "findMethod":
System.out.println("執行數據庫的find方法的操作");
result = 2;
break;
case "deleteMethod":
System.out.println("執行數據庫的delete方法的操作");
result = 3;
break;
case "updateMethod":
System.out.println("執行數據庫的update方法的操作");
result = 4;
break;
default:
System.out.println("執行自定義方法的操作");
result = "Hello Jpa";
}
System.out.println("執行的結果:"+result);
return result;
});
dao = (T) proxyFactory.getProxy();
}
}
InitializingBean是一個鉤子方法。實現afterPropertiesSet會讓我們創建bean的時候,先執行afterPropertiesSet。硬要把代理也放入getObject函數 或者構造函數之中也行,但是設計就不怎麼優美,不符合單一原則而已。
afterPropertiesSet 實現的功能主要是代理這個interface。後面其實會有更多細節,比如訪問數據庫等等,在這裏就不細化下去了。
寫了怎麼這麼久,我們來看看效果。
測試案例
import org.junit.jupiter.api.Test;
@SpringJUnitConfig(JpaConfig.class)
public class JpaTest {
@Autowired
UserDao userDao;
@Test
public void testUserApi() {
userDao.findByUserDao();
userDao.insertMethod();
userDao.findMethod();
userDao.updateMethod();
userDao.deleteMethod();
}
}
有個細節請大家注意到,我在上面的代碼中的import明確@Test 是 org.junit.jupiter.api.Test,我使用的測試unit是spring-boot-test。
下面是效果:
開始執行方法:public abstract void com.taldh.springdata.dao.UserDao.findByUserDao()
執行自定義方法的操作
執行的結果:Hello Jpa
開始執行方法:public abstract int com.taldh.springdata.dao.JpaTemplate.insertMethod()
執行數據庫的insert的操作
執行的結果:1
開始執行方法:public abstract int com.taldh.springdata.dao.JpaTemplate.findMethod()
執行數據庫的find方法的操作
執行的結果:2
開始執行方法:public abstract int com.taldh.springdata.dao.JpaTemplate.updateMethod()
執行數據庫的update方法的操作
執行的結果:4
開始執行方法:public abstract int com.taldh.springdata.dao.JpaTemplate.deleteMethod()
執行數據庫的delete方法的操作
執行的結果:3
到這裏這次Jpa原理之旅到此結束了,當然這裏只是展示了這些東西是Spring Data Jpa的骨架,具體的實現肯定有很多複雜問題得解決。如果有任何疑問,請關注我的公衆號:程序袁小黑,聯繫我哦。
更多精彩內容,請關注我的微信公衆號