鑒權認證+aop+注解+過濾feign請求的實例
注解類
@Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Auth { ? ? String code() default ""; }
切面
@Aspect @Component public class AuthAspect {? ? ? public static final String FEIGN_FLAG = "YES"; ? ? public static final String URL = "http://service/xxxx"; ? ? ? @Autowired ? ? private RestTemplate restTemplate; ? ? ? @Pointcut("@annotation(com.jvv.csr.service.base.annotation.Auth)") ? ? public void auAspect(){} ? ? ? @Before(value = "auAspect() && @annotation(param)") ? ? public void doBefore(Auth param){ ? ? ? ? ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ? ? ? ? HttpServletRequest request = attributes.getRequest(); ? ? ? ? String code = request.getHeader("feign"); ? ? ? ? if(FEIGN_FLAG.equals(code)){ ? ? ? ? ? ? return; ? ? ? ? } ? ? ? ? Long networkId = null; ? ? ? ? String token = null; ? ? ? ? Long scope = null; ? ? ? ? try { ? ? ? ? ? ? networkId = Long.valueOf(request.getHeader("networkId")); ? ? ? ? ? ? token = request.getHeader("authToken"); ? ? ? ? ? ? scope = Long.valueOf(request.getHeader("scope")); ? ? ? ? } catch (NumberFormatException e) { ? ? ? ? ? ? throw new RuntimeException("認證信息失敗,head頭信息傳入錯誤:"+ e.getMessage()); ? ? ? ? } ? ? ? ? HashMap object = null; ? ? ? ? try { ? ? ? ? ? ? MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>(); ? ? ? ? ? ? paramMap.add("networkId",networkId); ? ? ? ? ? ? paramMap.add("scope",scope); ? ? ? ? ? ? paramMap.add("token",token); ? ? ? ? ? ? paramMap.add("ecode",param.code()); ? ? ? ? ? ? object = restTemplate.postForObject(URL,paramMap,HashMap.class); ? ? ? ? } catch (Exception e) { ? ? ? ? ? ? throw new RuntimeException("調(diào)用3A認證接口異常:"+ e.getMessage()); ? ? ? ? } ? ? ? ? if (0 != (Integer) object.get("code")) { ? ? ? ? ? ? throw new RuntimeException("調(diào)用3A認證接口失敗:"+ object.get("msg")); ? ? ? ? } ? ? } }
內(nèi)部feign調(diào)用不用認證
@Configuration public class FeignRequestInterceptorConfig implements RequestInterceptor {? ? ? ?@Bean ? ? ?@LoadBalanced ? ? ?RestTemplate restTemplate(){ ? ? ? ? ?return new RestTemplate(); ? ? ?} ? ? @Override ? ? public void apply(RequestTemplate requestTemplate) { ? ? ? ? requestTemplate.header("feign","YES"); ? ? } }
需要認證的接口
?? ?@Auth(code = "co-005-1-1") ?? ?@RequestMapping(value ="" ,method = RequestMethod.POST) ?? ?public ResultVO add(@RequestBody ?GoodsAllInfoInsertParam insertParam){ ? ?? ??? ?ResultVO resultVO = new ResultVO(CodeEnum.SUCCESS,goodsService.addInfo(insertParam)); ?? ??? ?return resultVO; ?? ?}
feign aop切不到的詭異案例
我曾遇到過這么一個案例
使用 Spring Cloud 做微服務調(diào)用,為方便統(tǒng)一處理 Feign,想到了用 AOP 實現(xiàn),即使用 within 指示器匹配 feign.Client 接口的實現(xiàn)進行 AOP 切入。代碼如下,通過 @Before 注解在執(zhí)行方法前打印日志,并在代碼中定義了一個標記了@FeignClient 注解的 Client 類,讓其成為一個 Feign 接口:
package org.geekbang.time.commonmistakes.springpart2.aopfeign.feign;? import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; ? @FeignClient(name = "client") public interface Client { ? ? @GetMapping("/feignaop/server") ? ? String api(); }
package org.geekbang.time.commonmistakes.springpart2.aopfeign;? import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; ? @Configuration @EnableFeignClients(basePackages = "org.geekbang.time.commonmistakes.springpart2.aopfeign.feign") public class Config { }
package org.geekbang.time.commonmistakes.springpart2.aopfeign;? import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; ? @Aspect @Slf4j @Component public class WrongAspect { ? ? @Before("within(feign.Client+)") ? ? public void before(JoinPoint pjp) { ? ? ? ? log.info("within(feign.Client+) pjp {}, args:{}", pjp, pjp.getArgs()); ? ? } }
通過 Feign 調(diào)用服務后可以看到日志中有輸出,的確實現(xiàn)了 feign.Client 的切入,切入的是 execute 方法:
[15:48:32.850] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspec
Binary data, feign.Request$Options@5c16561a]
一開始這個項目使用的是客戶端的負載均衡,也就是讓 Ribbon 來做負載均衡,代碼沒啥問題。后來因為后端服務通過 Nginx 實現(xiàn)服務端負載均衡,所以開發(fā)同學把@FeignClient 的配置設置了 URL 屬性,直接通過一個固定 URL 調(diào)用后端服務:
package org.geekbang.time.commonmistakes.springpart2.aopfeign.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @FeignClient(name = "anotherClient", url = "http://localhost:45678") public interface ClientWithUrl { @GetMapping("/feignaop/server") String api(); }
但這樣配置后,之前的 AOP 切面竟然失效了,也就是 within(feign.Client+) 無法切入ClientWithUrl 的調(diào)用了。為了還原這個場景,我寫了一段代碼,定義兩個方法分別通過 Client 和 ClientWithUrl 這兩個 Feign 進行接口調(diào)用:
package org.geekbang.time.commonmistakes.springpart2.aopfeign; import lombok.extern.slf4j.Slf4j; import org.geekbang.time.commonmistakes.springpart2.aopfeign.feign.Client; import org.geekbang.time.commonmistakes.springpart2.aopfeign.feign.ClientWithUrl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Slf4j @RequestMapping("feignaop") @RestController public class FeignAopConntroller { @Autowired private Client client; @Autowired private ClientWithUrl clientWithUrl; @Autowired private ApplicationContext applicationContext; @GetMapping("client") public String client() { return client.api(); } @GetMapping("clientWithUrl") public String clientWithUrl() { return clientWithUrl.api(); } @GetMapping("server") public String server() { return "OK"; } }
可以看到,調(diào)用 Client 后 AOP 有日志輸出,調(diào)用 ClientWithUrl 后卻沒有:
[15:50:32.850] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspec
Binary data, feign.Request$Options@5c16561
這就很費解了。難道為 Feign 指定了 URL,其實現(xiàn)就不是 feign.Clinet 了嗎?要明白原因,我們需要分析一下 FeignClient 的創(chuàng)建過程,也就是分析FeignClientFactoryBean 類的 getTarget 方法。源碼第 4 行有一個 if 判斷,當 URL 沒有內(nèi)容也就是為空或者不配置時調(diào)用 loadBalance 方法,在其內(nèi)部通過 FeignContext 從容器獲取 feign.Client 的實例:
<T> T getTarget() { FeignContext context = this.applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); if (!StringUtils.hasText(this.url)) { ... return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, this.url)); }. .. String url = this.url + cleanPath(); Client client = getOptional(context, Client.class); if (client != null) { if (client instanceof LoadBalancerFeignClient) { // not load balancing because we have a url, // but ribbon is on the classpath, so unwrap client = ((LoadBalancerFeignClient) client).getDelegate(); }builder.client(client); }. .. }protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) { Client client = getOptional(context, Client.class); if (client != null) { builder.client(client); Targeter targeter = get(context, Targeter.class); return targeter.target(this, builder, context, target); } ... } protected <T> T getOptional(FeignContext context, Class<T> type) { return context.getInstance(this.contextId, type); }
調(diào)試一下可以看到,client 是 LoadBalanceFeignClient,已經(jīng)是經(jīng)過代理增強的,明顯是一個 Bean:
所以,沒有指定 URL 的 @FeignClient 對應的 LoadBalanceFeignClient,是可以通過feign.Client 切入的。在我們上面貼出來的源碼的 16 行可以看到,當 URL 不為空的時候,client 設置為了LoadBalanceFeignClient 的 delegate 屬性。
其原因注釋中有提到,因為有了 URL 就不需要客戶端負載均衡了,但因為 Ribbon 在 classpath 中,所以需要從LoadBalanceFeignClient 提取出真正的 Client。斷點調(diào)試下可以看到,這時 client 是一個ApacheHttpClient
那么,這個 ApacheHttpClient 是從哪里來的呢?這里,我教你一個小技巧:如果你希望知道一個類是怎樣調(diào)用棧初始化的,可以在構(gòu)造方法中設置一個斷點進行調(diào)試。這樣,你就可以在 IDE 的棧窗口看到整個方法調(diào)用棧,然后點擊每一個棧幀看到整個過程。
用這種方式,我們可以看到,是 HttpClientFeignLoadBalancedConfiguration 類實例化的 ApacheHttpClient:
進一步查看 HttpClientFeignLoadBalancedConfiguration 的源碼可以發(fā)現(xiàn),LoadBalancerFeignClient 這個 Bean 在實例化的時候,new 出來一個ApacheHttpClient 作為 delegate 放到了 LoadBalancerFeignClient 中:
@Bean @ConditionalOnMissingBean(Client.class) public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory, HttpClient httpClient) { ApacheHttpClient delegate = new ApacheHttpClient(httpClient); return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory) } public LoadBalancerFeignClient(Client delegate, CachingSpringLoadBalancerFactory lbClientFactory, SpringClientFactory clientFactory) { this.delegate = delegate; this.lbClientFactory = lbClientFactory; this.clientFactory = clientFactory; }
顯然,ApacheHttpClient 是 new 出來的,并不是 Bean,而 LoadBalancerFeignClient是一個 Bean。有了這個信息,我們再來捋一下,為什么 within(feign.Client+) 無法切入設置過 URL 的@FeignClient ClientWithUrl:因此,定義了 URL 的 FeignClient 采用 within(feign.Client+) 無法切入。那,如何解決這個問題呢?有一位同學提出,修改一下切點表達式,通過 @FeignClient 注解來切:
package org.geekbang.time.commonmistakes.springpart2.aopfeign; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect @Slf4j //@Component public class Wrong2Aspect { @Before("@within(org.springframework.cloud.openfeign.FeignClient)") public void before(JoinPoint pjp) { log.info("@within(org.springframework.cloud.openfeign.FeignClient) pjp {}, args:{}", pjp, pjp.getArgs()); } }
修改后通過日志看到,AOP 的確切成功了:
[15:53:39.093] [http-nio-45678-exec-3] [INFO ] [o.g.t.c.spring.demo4.Wrong2Aspe
但仔細一看就會發(fā)現(xiàn),這次切入的是 ClientWithUrl 接口的 API 方法,并不是client.Feign 接口的 execute 方法,顯然不符合預期。
這位同學犯的錯誤是,沒有弄清楚真正希望切的是什么對象。@FeignClient 注解標記在Feign Client 接口上,所以切的是 Feign 定義的接口,也就是每一個實際的 API 接口。而通過 feign.Client 接口切的是客戶端實現(xiàn)類,切到的是通用的、執(zhí)行所有 Feign 調(diào)用的execute 方法。那么問題來了,ApacheHttpClient 不是 Bean 無法切入,切 Feign 接口本身又不符合要求。怎么辦呢?
經(jīng)過一番研究發(fā)現(xiàn),ApacheHttpClient 其實有機會獨立成為 Bean。查看HttpClientFeignConfiguration 的源碼可以發(fā)現(xiàn),當沒有 ILoadBalancer 類型的時候,自動裝配會把 ApacheHttpClient 設置為 Bean。
這么做的原因很明確,如果我們不希望做客戶端負載均衡的話,應該不會引用 Ribbon 組件的依賴,自然沒有 LoadBalancerFeignClient,只有 ApacheHttpClient:
@Configuration @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnMissingBean(CloseableHttpClient.class) @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = tru protected static class HttpClientFeignConfiguration { @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient(HttpClient httpClient) { return new ApacheHttpClient(httpClient); } }
那,把 pom.xml 中的 ribbon 模塊注釋之后,是不是可以解決問題呢?
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency>
但,問題并沒解決,啟動出錯誤了:
Caused by: java.lang.IllegalArgumentException: Cannot subclass final class feig
at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:657)
at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGe
這里,又涉及了 Spring 實現(xiàn)動態(tài)代理的兩種方式:Spring Boot 2.x 默認使用 CGLIB 的方式,但通過繼承實現(xiàn)代理有個問題是,無法繼承final 的類。因為,ApacheHttpClient 類就是定義為了 final
public final class ApacheHttpClient implements Client {
為解決這個問題,我們把配置參數(shù) proxy-target-class 的值修改為 false,以切換到使用JDK 動態(tài)代理的方式:
spring.aop.proxy-target-class=false
修改后執(zhí)行 clientWithUrl 接口可以看到,通過 within(feign.Client+) 方式可以切入feign.Client 子類了。以下日志顯示了 @within 和 within 的兩次切入:
[16:29:55.303] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.Wrong2Aspe
[16:29:55.310] [http-nio-45678-exec-1] [INFO ] [o.g.t.c.spring.demo4.WrongAspec
Binary data, feign.Request$Options@387550b0]
這下我們就明白了,Spring Cloud 使用了自動裝配來根據(jù)依賴裝配組件,組件是否成為Bean 決定了 AOP 是否可以切入,在嘗試通過 AOP 切入 Spring Bean 的時候要注意加上上一講的兩個案例,我就把 IoC 和 AOP 相關的坑點和你說清楚了。除此之外,我們在業(yè)務開發(fā)時,還有一個繞不開的點是,Spring 程序的配置問題。接下來,我們就看具體吧。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
詳解SpringBoot讀取resource目錄下properties文件的常見方式
這篇文章主要介紹了SpringBoot讀取resource目錄下properties文件的常見方式,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02Java隊列同步器之CountDownLatch實現(xiàn)詳解
這篇文章主要介紹了Java隊列同步器之CountDownLatch實現(xiàn)詳解,CountDownLatch是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程執(zhí)行完后再執(zhí)行,例如,應用程序的主線程希望在負責啟動框架服務的線程已經(jīng)啟動所有框架服務之后執(zhí)行,需要的朋友可以參考下2023-12-12Mybatis-Plus支持GBase8s分頁查詢的實現(xiàn)示例
本文主要介紹了使?Mybatis-Plus?支持?GBase8s?的分頁查詢,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-01-01Spring Boot Feign服務調(diào)用之間帶token問題
這篇文章主要介紹了Spring Boot Feign服務調(diào)用之間帶token的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09java 實現(xiàn)Comparable接口排序,升序、降序、倒敘
這篇文章主要介紹了java 實現(xiàn)Comparable接口排序,升序、降序、倒敘,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08淺談Java中SimpleDateFormat 多線程不安全原因
SimpleDateFormat是Java中用于日期時間格式化的一個類,本文主要介紹了淺談Java中SimpleDateFormat 多線程不安全原因,感興趣的可以了解一下2024-01-01