我有以下情况:
我的项目包含多个实体,每个实体都有其各自的控制器,服务和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
的所有路由,都需要大量复制粘贴和代码重复操作。
因此,问题是:如何为我的所有控制器以一种简洁干净的方式做到这一点?
我已经对此进行了相当多的研究,并得出以下结论:
HandlerInterceptor
可能有助于将User从每个请求的标头中移出,但由于我不在此项目中使用视图,因此它似乎并不适合整体使用(这只是后退-结束),它对我的存储库查询无能为力@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)
。
在这种情况下,您认为什么是最好的方法?
答案 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
扩展的JpaRepository和JpaSpecificationExecutor的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);
}
您可能会注意到,这看起来像是我对某个特定子接口的原始解决方案,您是对的。不过,我建议您更严格一些,不要使用最后一个选项,即使它在我的测试用例中也可以,并且您可能会满意的。
最后,我认为到目前为止,我们已经涵盖了大多数基础。