6个类告诉你Spring Data Jpa的实现原理

    技术2022-07-10  138

    用使用过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的骨架,具体的实现肯定有很多复杂问题得解决。如果有任何疑问,请关注我的公众号:程序袁小黑,联系我哦。

    更多精彩内容,请关注我的微信公众号

    Processed: 0.011, SQL: 9