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

时间:2018-11-20 14:52:34

标签: java spring-boot spring-data-jpa spring-aop

我有以下情况:

我的项目包含多个实体,每个实体都有其各自的控制器,服务和JPA存储库。所有这些实体都通过“ companyUuid”属性与特定公司相关联。

我的控制器中的每个传入请求都会有一个“用户”标头,该标头将向我提供发出该请求的用户的详细信息,包括与他关联的公司。

我需要从标题中检索与用户关联的公司,并过滤该公司随后进行的每个查询,这本质上就像是在每个查询中添加WHERE companyUuid = ...

我作为解决方案所做的是创建规范对象的通用功能:

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> { }

更改了“查找”调用以包含规范:

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

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

尽管此解决方案绝对可以正常工作,但要实现@RestControllers的所有路由,都需要大量复制粘贴和代码重复操作。

因此,问题是:如何为我的所有控制器以一种简洁干净的方式做到这一点?

我已经对此进行了相当多的研究,并得出以下结论:

  1. Spring JPA和Hibernate似乎没有提供一种动态使用规范来限制所有查询的方法(参考:Automatically Add criteria on each Spring Jpa Repository call
  2. Spring MVC HandlerInterceptor可能有助于将User从每个请求的标头中移出,但由于我不在此项目中使用视图,因此它似乎并不适合整体使用(这只是后退-结束),它对我的​​存储库查询无能为力
  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)

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

2 个答案:

答案 0 :(得分:2)

这是一个主意:

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实例。

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

答案 1 :(得分:0)

我不是Spring或Java EE用户,但是我可以在方面方面为您提供帮助。我也用Google搜索了一下,因为没有导入和程序包名称的代码片段有点不连贯,所以我不能只复制,粘贴和运行它们。从ExampleRepository扩展的JpaRepositoryJpaSpecificationExecutor的JavaDocs来看,您正在尝试拦截

Page<T> PagingAndSortingRepository.findAll(Pageable pageable)

(由JpaRepository继承)并致电

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

相反,对吧?

因此,从理论上讲,我们可以在切入点和建议中使用这些知识,以提高类型安全性并避免难看的反射技巧。唯一的问题是,被拦截的调用返回Page<T>,而您要调用的方法却返回List<T>。除非您始终使用Iterable<T>(这是所讨论的两个接口的超级接口),否则调用方法肯定希望使用前者而不是后者。或者,也许您只是忽略返回值?如果不回答这个问题或不显示如何修改代码来做到这一点,那么很难真正回答问题。

因此,我们假设返回的结果被忽略或作为Iterable处理。然后您的切入点/建议对如下所示:

@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 .:另一个选择是,如果调用代码确实希望返回页面对象,则在从方面建议中将列表返回之前,将列表手动转换为相应的页面。


由于后续问题而更新:

Eugen写道:

  

对于另一个实体,假设Foo,则存储库为public interface FooRepository extends JpaRepository<Foo, Long>, JpaSpecificationExecutor<Foo> { }

那么,让我们简单地概括一下切入点,并假定它始终应以扩展有问题的两个接口的类为目标。

@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);
}

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


更新2:

正如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)签名的类,则可以选择:

@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);
}

您可能会注意到,这看起来像是我对某个特定子接口的原始解决方案,您是对的。不过,我建议您更严格一些,不要使用最后一个选项,即使它在我的测试用例中也可以,并且您可能会满意的。

最后,我认为到目前为止,我们已经涵盖了大多数基础。