blinkfox / fenix

This is an extension library to the Spring Data JPA complex or dynamic SQL query. 这是一个比 MyBatis 更加强大的 Spring Data JPA 扩展库,为解决复杂动态 JPQL (或 SQL) 而生。https://blinkfox.github.io/fenix
https://blinkfox.github.io/fenix
Apache License 2.0
348 stars 73 forks source link

新增功能1: 增加方法调用拦截器, 新增功能2: 增加sql拦截器功能 #47

Closed hexian closed 2 years ago

hexian commented 3 years ago

新增功能1: 增加方法调用拦截器, 测试方法: com.blinkfox.fenix.repository.UserRepositoryTest#queryCustomPage

新增功能2: 增加sql拦截器功能, 如下2个测试方法: com.blinkfox.fenix.repository.UserRepositoryTest#interceptorQuery com.blinkfox.fenix.repository.UserRepositoryTest#interceptorQueryFenix

pengten commented 3 years ago

这个是什么背景?是否有关联issues

hexian commented 3 years ago

哦哦,昨晚凌晨才写完,太困了,公司电脑屏蔽github没法回复,明天周六或者今天晚上我在github上补充下功能产生的背景------------------ 原始邮件 ------------------ @.> 发送时间: 2021年7月2日(星期五) 凌晨0:36 @.>; @.**@.>; 主题: Re: [blinkfox/fenix] 新增功能1: 增加方法调用拦截器, 新增功能2: 增加sql拦截器功能 (#47)

hexian commented 3 years ago

这个是什么背景?是否有关联issues

新增功能1: 增加方法调用拦截器, 测试方法: com.blinkfox.fenix.repository.UserRepositoryTest#queryCustomPage

你好,这个功能是这样的,在Spring Data JPA的方法调用链中增加一个拦截器,具体我想到2个应用场景: 场景1: 比如 Spring Data JPA默认的分页返回值只能是Page,它有2个不好的地方,第一个,它是Spring Data JPA定义的一个类,我们没办法对他的字段用json框架的注解取别名,因为这样我们一般会自己定义一个PageVO什么的类,然后从Spring Data JPA的Page对象取出来参数一个一个set到我们自定义的PageVO里面,这个过程可以在Spring Data JPA的方法调用链中增加一个拦截器来统一完成这个事情,

场景2: 比如我们返回的如果不是JPA的实体对象,是自定义的VO,有一些字段比如:生日,年龄,身份证,手机号等敏感数据,我们一般入库的时候需要加密,查询出来的时候需要解密,一般都是查询出来之后再手动转换,如果增加一个切面方法然后再定义一个自定义的注解,那么我们就可以在这个切面中判断是否有某个注解然后自动完成入库前加密,查询出来的数据自动进行解密(框架不实现,而是提供一个接口,用户自己去实现判断有注解自动进行脱敏/还原等功能)

以上是我想到的2个应用场景,提交的类里面加入切面的逻辑是 在 FenixJpaRepositoryFactoryBean 这个类增加了 addRepositoryProxyPostProcessor, 用户如果实现了 FenixRepositoryProxyPostProcessor 这个接口则会被自动加入到方法切面链中(比如测试类中的: EnhanceRepositoryProxyPostProcessor), 然后在 TransformMethodInterceptor 类中就是在拦截, 这个主要是模拟一个场景1的问题,CustomerPage 是我们一个自定义的Page对象,继承自PageImpl, 里面使用@JSONField注解修改了序列化后的json的key,同时Page里面一些过多的字段也使用@JSONField(serialize =false)进行了屏蔽,可以直接返回给前端使用了。

hexian commented 3 years ago

这个是什么背景?是否有关联issues

新增功能2: 增加sql拦截器功能, 如下2个测试方法: com.blinkfox.fenix.repository.UserRepositoryTest#interceptorQuery com.blinkfox.fenix.repository.UserRepositoryTest#interceptorQueryFenix

这个功能产生的目的是,对于 SELECT u.id, u.name , u.age FROM User AS u 这样的因为受限于JPA的内部实现需要加上一个别名才可以感觉有点麻烦,我想的是我们Hibernate有一个sql语句的拦截功能,就是实现EmptyInterceptor这个接口, 然后我想到我们也可以提供一个这样的方法,然后自定义一个注解@FenixSqlInterceptor(strategy = FenixSqlInterceptor.SqlColumnStrategy.CAMEL2UNDERLINE) ,如果JPA的方法上标注了这个注解的话我们就进行sql的拦截, 所以我定义了一个SqlInterceptor接口和@FenixSqlInterceptor注解,那么我们可以借助durid或者一些成熟的sql parse对sql语句进行改写,自动将SELECT u.id, u.name , u.age FROM User AS u变成SELECT u.id as id, u.name as name, u.age as age FROM User AS u 也就是借助sql parse的功能自动加上别名,主要的改动点在com.blinkfox.fenix.jpa.FenixQueryLookupStrategy#resolveQuery这个方法,里面用户是否加入了FenixSqlInterceptor接口来回调SqlInterceptor的方法, 这里需要说明一下因为Spring Data JPA内置的@Query注解是静态的字符串,所以需要在Spring Data JPA框架将@Query中的sql/hql转换成JPA的javax.persistence.Query之前替换掉注解里面的静态sql,因为注解在运行时会生成一个JDK的动态代理类,所以定义了一个工具类AnnotationHelper里面有一个updateAnnotationProperty方法来在运行时修改注解的属性值的方法

hexian commented 3 years ago

FenixPredicateBuilder 类新增方法引用功能

hexian commented 3 years ago

FenixPredicateBuilder 类新增方法引用功能

新增类: com.blinkfox.fenix.lambda.SerializedLambda com.blinkfox.fenix.lambda.SFunction com.blinkfox.fenix.helper.ClassHelper com.blinkfox.fenix.SerializedLambdaTest

修改类: com.blinkfox.fenix.specification.predicate.FenixPredicateBuilder com.blinkfox.fenix.repository.BlogRepository com.blinkfox.fenix.repository.BlogRepositoryTest

测试方法: com.blinkfox.fenix.SerializedLambdaTest#testLambda com.blinkfox.fenix.repository.BlogRepositoryTest#queryBlogsWithSpecifition

pengten commented 3 years ago

个人建议两块功能分开合并,不然会很乱。老师一直都很忙,如果能描述下详细思路更好,便于理解您每一行代码的具体意义。

另外,个人感觉对于sql做过多干涉可能会超出fenix的职能范围,代码量也不小,不清楚对原有设计的影响有哪些。还是最好有详细实现思路,这样也能更有利于老师评估。

hexian commented 3 years ago

个人建议两块功能分开合并,不然会很乱。老师一直都很忙,如果能描述下详细思路更好,便于理解您每一行代码的具体意义。

另外,个人感觉对于sql做过多干涉可能会超出fenix的职能范围,代码量也不小,不清楚对原有设计的影响有哪些。还是最好有详细实现思路,这样也能更有利于老师评估。

哦哦,这个可以理解,我觉得可以先clone一下fork出来的先跑下看看,不着急合并,思路我整理下:

新增功能1: 扩展点在源码上的体现: package org.springframework.data.repository.core.support;

public abstract class TransactionalRepositoryFactoryBeanSupport<T extends Repository<S, ID>, S, ID> extends RepositoryFactoryBeanSupport<T, S, ID> implements BeanFactoryAware {

    @Override
    protected final RepositoryFactorySupport createRepositoryFactory() {

        RepositoryFactorySupport factory = doCreateRepositoryFactory();

        RepositoryProxyPostProcessor exceptionPostProcessor = this.exceptionPostProcessor;

        if (exceptionPostProcessor != null) {
          // 这里是内部加入的默认的 RepositoryProxyPostProcessor 实现,   标记点1
            factory.addRepositoryProxyPostProcessor(exceptionPostProcessor);
        }

        RepositoryProxyPostProcessor txPostProcessor = this.txPostProcessor;

        if (txPostProcessor != null) {
            factory.addRepositoryProxyPostProcessor(txPostProcessor);
        }

        return factory;
    }

}

以上是Spring Data JPA内部的类, FenixJpaRepositoryFactoryBean 类的继承结构为: FenixJpaRepositoryFactoryBean ==> JpaRepositoryFactoryBean ==> TransactionalRepositoryFactoryBeanSupport

那么重写此方法,手动调用 factory.addRepositoryProxyPostProcessor(exceptionPostProcessor); 也就是在 FenixJpaRepositoryFactoryBean 的里面有:

public class FenixJpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID> extends JpaRepositoryFactoryBean<T, S, ID> {

protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
    FenixJpaRepositoryFactory repositoryFactory = new FenixJpaRepositoryFactory(entityManager);
    // 加入sql拦截器
    repositoryFactory.setSqlInterceptor(sqlInterceptor);
    // 参考"标记点1",手动调用 factory.addRepositoryProxyPostProcessor(exceptionPostProcessor);  这里就是功能1的入口点
    this.addRepositoryProxyPostProcessor(entityManager, repositoryFactory);

    return repositoryFactory;
}

/**
 * 新增JPA方法调用拦截处理器
 * @param entityManager
 * @param repositoryFactory
 */
private void addRepositoryProxyPostProcessor(EntityManager entityManager, FenixJpaRepositoryFactory repositoryFactory) {
    if (Objects.nonNull(this.postProcessors)){
        List<FenixRepositoryProxyPostProcessor> postProcessorList = this.postProcessors.getIfAvailable();
        if (!CollectionUtils.isEmpty(postProcessorList)){
            // 排序集合(数字越小优先级越高)
            postProcessorList.sort(Comparator.comparingInt(FenixRepositoryProxyPostProcessor::getOrder));
            for (int i = 0; i < postProcessorList.size(); i++) {
                FenixRepositoryProxyPostProcessor postProcessor = postProcessorList.get(i);

                postProcessor.setApplicationContext(applicationContext);
                postProcessor.setEntityManager(entityManager);
                postProcessor.setJpaRepositoryFactory(repositoryFactory);
                postProcessor.setJpaRepositoryFactoryBean(this);

                // 方法调用链增加拦截链方法
                repositoryFactory.addRepositoryProxyPostProcessor(postProcessor);
            }
        }
    }
}

}

hexian commented 3 years ago

个人建议两块功能分开合并,不然会很乱。老师一直都很忙,如果能描述下详细思路更好,便于理解您每一行代码的具体意义。

另外,个人感觉对于sql做过多干涉可能会超出fenix的职能范围,代码量也不小,不清楚对原有设计的影响有哪些。还是最好有详细实现思路,这样也能更有利于老师评估。

新增功能2: 增加sql拦截器功能 这个功能的伪代码功能是:

   if (Objects.nonNull(this.sqlInterceptor)){
        // 提供一个预处理sql的机会, 获取修改后的sql
        querySql = sqlInterceptor.onPrepareStatement(this.japMethod, querySql);
    }
   query = em.createQuery(querySql);

应该没有什么影响,只是在 em.createQuery(querySql) 之前增加了修改可以修改sql的功能,具体如何实现用户自己决定,如果某个方法出现问题,可以去掉注解,手动加上别名进行兜底。

blinkfox commented 3 years ago

@hexian 不好意思,最近很少上 github,才看到。我抽空看看,不晓得是否对现有功能是否有影响。

hexian commented 3 years ago

@hexian 不好意思,最近很少上 github,才看到。我抽空看看,不晓得是否对现有功能是否有影响。

您好,我试了一下@query是发生在Repository创建阶段, 应该不存在并发问题,Repository应该不会创建多次,

方法调用链图1 方法调用链图2

UserRepository创建单例bean的链路: 方法调用链图3

nestingLee commented 2 years ago

这个挺好,建议合并。

blinkfox commented 2 years ago

感谢贡献,新版本关于结果转换相关功能重新实现了,你的改动量太大,很抱歉不能合并,后续如果觉得有必要,可以重新修改提起合并请求。我先关闭这个合并了。