[英]Apply Spring JPA Specification to multiple repositories and queries
我有以下情況:
我的項目包含多個實體,每個實體都有其各自的控制器,服務和JPA存儲庫。 所有這些實體都通過“ companyUuid”屬性與特定公司關聯。
我的控制器中的每個傳入請求都將帶有一個“用戶”標頭,該標頭將向我提供發出該請求的用戶的詳細信息,包括與他關聯的公司。
我需要從標題中檢索與用戶關聯的公司,並過濾該公司的每個后續查詢,這實際上就像在每個查詢中添加WHERE companyUuid = ...
我作為解決方案所做的是用於創建Specification對象的通用函數:
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");
}
}
這將是完美的,因為幾乎不需要對控制器,服務和存儲庫進行任何修改。 雖然,我對函數簽名有疑問。 由於我在Service中調用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用戶,但是我可以在方面方面為您提供幫助。 我也用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提出的內容更加優雅,類型安全且可讀性強。
PS:另一個選擇是,如果調用代碼確實希望返回頁面對象,則在從方面建議中將列表返回之前,手動將列表轉換為相應的頁面。
由於后續問題而更新:
歐根寫道:
對於另一個實體,假設
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);
}
我注釋掉的切入點部分是可選的,因為我已經通過使用建議簽名的target()
參數綁定縮小了JpaRepository
方法的調用。 但是,應該使用第二個within()
,以確保被攔截的類實際上也擴展了第二個接口,因此我們可以強制轉換並執行另一個方法,而不會出現任何問題。
更新2:
正如Eugen所說,如果將目標對象綁定到JpaSpecificationExecutor
類型,也可以擺脫JpaSpecificationExecutor
類型JpaSpecificationExecutor
,但JpaRepository
是您之前在建議代碼中不需要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);
}
您可能會注意到,這看起來像是我對某個特定子接口的原始解決方案,而您是對的。 不過,我建議您更嚴格一些,不要使用最后一個選項,即使它在我的測試用例中也可以,並且您可能會滿意。
最后,我認為到目前為止,我們已經涵蓋了大多數基礎。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.