将 Spring JPA 规范应用于多个存储库和查询

Apply Spring JPA Specification to multiple repositories and queries

我有以下情况:

我的项目包含多个实体,每个实体都有其各自的控制器、服务和 JPA 存储库。所有这些实体都通过 "companyUuid" 属性.

与特定公司相关联

我的控制器中的每个传入请求都会有一个 "user" header,它将向我提供发出该请求的用户的详细信息,包括他与哪家公司相关联。

我需要从 header 中检索与用户关联的公司并过滤该公司的每个后续查询,这基本上就像向每个查询添加 WHERE companyUuid = ...

作为解决方案,我所做的是创建规范的通用函数 object:

public class CompanySpecification {

public static <T> Specification<T> fromCompany(String companyUuid) {
    return (e, cq, cb) -> cb.equal(e.get("companyUuid"), companyUuid);
}}

实现存储库如下:

public interface ExampleRepository extends JpaRepository<Example, Long>, JpaSpecificationExecutor<Example> { }

更改了 "find" 调用以包含规范:

exampleRepository.findAll(CompanySpecification.fromCompany(companyUuid), pageRequest);

当然,这需要在控制器函数中添加@RequestHeader才能让用户进入header。

虽然这个解决方案工作得很好,但它需要大量 copy-pasting 和代码重复才能为我的 @RestControllers.

的所有路线完成它

因此,问题是:我怎样才能以一种优雅、干净的方式为我的所有控制器做到这一点?

我对此进行了大量研究,得出以下结论:

  1. Spring JPA 和 Hibernate 似乎没有提供动态使用规范来限制所有查询的方法(参考:
  2. Spring MVC HandlerInterceptor 可能有助于让用户在每个请求中脱离 header,但它似乎并不适合整体,因为我不使用此项目中的视图(它只是一个 back-end),它对我的​​存储库查询无能为力
  3. Spring AOP 对我来说似乎是一个不错的选择,我试了一下。我的意图是保持所有存储库调用不变,并将规范添加到存储库调用中。我创建了以下 @Aspect:
@Aspect
@Component
public class UserAspect {

    @Autowired(required=true)
    private HttpServletRequest request;

    private String user;

    @Around("execution(* com.example.repository.*Repository.*(..))")
    public Object filterQueriesByCompany(ProceedingJoinPoint jp) throws Throwable {
        Object[] arguments = jp.getArgs();
        Signature signature = jp.getSignature();

        List<Object> newArgs = new ArrayList<>();
        newArgs.add(CompanySpecification.fromCompany(user));

        return jp.proceed(newArgs.toArray());
    }

    @Before("execution(* com.example.controller.*Controller.*(..))")
    public void getUser() {
        user = request.getHeader("user");
    }
}

这本来可以完美地工作,因为它几乎不需要对控制器、服务和存储库进行任何修改。虽然,我对函数签名有疑问。由于我在我的服务中调用 findAll(Pageable p),函数的签名已经在我的建议中定义,我无法从建议中更改为替代版本 findAll(Specification sp, Pageagle p)

您认为在这种情况下最好的方法是什么?

这是一个想法:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
public class UserAspect {

    @Around("execution(* com.example.repository.*Repository.findAll())")
    public Object filterQueriesByCompany(ProceedingJoinPoint jp) throws Throwable {
        Object target = jp.getThis();
        Method method = target.getClass().getMethod("findAll", Specification.class);
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        return method.invoke(target, CompanySpecification.fromCompany(request.getHeader("user")));
    }

}

上述方面从存储库中拦截了 findAll() 方法,并且它没有继续调用,而是用另一个对 findAll(Specification) 方法的调用来代替。请注意我是如何获得 HttpServletRequest 实例的。

当然,这是一个起点,而不是开箱即用的解决方案。

我不是 Spring 或 Java EE 用户,但我可以在方面部分帮助您。我也用谷歌搜索了一下,因为没有导入和包名称的代码片段有点不连贯,所以我不能只复制、粘贴和 运行 它们。从 JavaDocs for JpaRepository and JpaSpecificationExecutor 来看,您在 ExampleRepository 中扩展了这两个文件,您正试图拦截

Page<T> PagingAndSortingRepository.findAll(Pageable pageable)

(由JpaRepository继承)并调用

List<T> JpaSpecificationExecutor.findAll(Specification<T> spec, Pageable pageable)

相反,对吗?

所以理论上我们可以在我们的切入点和建议中使用这些知识,以便更加类型安全并避免丑陋的反射技巧。这里唯一的问题是截获的调用 returns Page<T> 而您要调用的方法却是 returns List<T>。调用方法肯定需要前者而不是后者,除非您总是使用 Iterable<T> 这是两个相关接口的超级接口。或者您可能只是忽略 return 值?如果您不回答该问题或展示您如何修改代码来执行此操作,将很难真正回答您的问题。

因此,让我们假设 returned 结果被忽略或作为 Iterable 处理。然后你的 pointcut/advice 对看起来像这样:

@Around("execution(* findAll(*)) && args(pageable) && target(exampleRepository)")
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, ExampleRepository exampleRepository) throws Throwable {
  return exampleRepository.findAll(CompanySpecification.fromCompany(user), pageable);
}

我测试过,它有效。我还认为它比您尝试的或 Eugen 提出的更优雅、类型安全和可读。

P.S.: 另一种选择是在 return 从方面建议中手动将列表转换为相应的页面,如果调用代码确实期望页面对象是 returned.


因后续问题更新:

欧根写道:

For another entity, let's say Foo, the repository would be public interface FooRepository extends JpaRepository<Foo, Long>, JpaSpecificationExecutor<Foo> { }

好吧,那么让我们概括一下切入点并假设它应该始终以 classes 为目标,它扩展了两个有问题的接口:

@Around(
  "execution(* findAll(*)) && " +
  "args(pageable) && " + 
  "target(jpaRepository) && " +
  //"within(org.springframework.data.jpa.repository.JpaRepository+) && " +
  "within(org.springframework.data.jpa.repository.JpaSpecificationExecutor+)"
)
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, JpaRepository jpaRepository) throws Throwable {
  return ((JpaSpecificationExecutor) jpaRepository)
    .findAll(CompanySpecification.fromCompany(user), pageable);
}

我注释掉的切入点部分是可选的,因为我已经通过使用建议签名的 target() 参数绑定缩小到 JpaRepository 方法调用。但是,应该使用第二个 within(),以确保拦截的 class 实际上也扩展了第二个接口,这样我们就可以转换并执行另一个方法而不会有任何问题。


更新二:

正如 Eugen 所说,如果将目标对象绑定到类型 JpaSpecificationExecutor,您也可以摆脱强制转换 - 但前提是您之前的建议代码中不需要 JpaRepository那。否则你将不得不以另一种方式投射。在这里它似乎并不是真正需要的,所以他的想法确实使解决方案更加简洁和富有表现力。感谢您的贡献。 :-)

@Around(
  "target(jpaSpecificationExecutor) && " +
  "execution(* findAll(*)) && " +
  "args(pageable) && " + 
  "within(org.springframework.data.jpa.repository.JpaRepository+)"
)
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, JpaSpecificationExecutor jpaSpecificationExecutor) throws Throwable {
  return jpaSpecificationExecutor.findAll(CompanySpecification.fromCompany(user), pageable);
}

或者,如果您不想将 execution()within() 合并(个人喜好):

@Around(
  "target(jpaSpecificationExecutor) && " +
  "execution(* org.springframework.data.jpa.repository.JpaRepository+.findAll(*)) && " +
  "args(pageable)" 
)
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, JpaSpecificationExecutor jpaSpecificationExecutor) throws Throwable {
  return jpaSpecificationExecutor.findAll(CompanySpecification.fromCompany(user), pageable);
}

类型安全性较低,但如果您认为没有其他具有 * findAll(Pageable) 签名的 classes,也是一种选择:

@Around("target(jpaSpecificationExecutor) && execution(* findAll(*)) && args(pageable)")
public Object filterQueriesByCompany(ProceedingJoinPoint thisJoinPoint, Pageable pageable, JpaSpecificationExecutor jpaSpecificationExecutor) throws Throwable {
  return jpaSpecificationExecutor.findAll(CompanySpecification.fromCompany(user), pageable);
}

您可能会注意到,这看起来很像我针对某个特定子界面的原始解决方案,您是对的。不过,我建议更严格一点,不要使用最后一个选项,即使它在我的测试用例中有效,你可能会接受它。

最后,我认为我们现在已经涵盖了大部分基础。