鑒權(quán)認(rèn)證+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("認(rèn)證信息失敗,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認(rèn)證接口異常:"+ e.getMessage());
? ? ? ? }
? ? ? ? if (0 != (Integer) object.get("code")) {
? ? ? ? ? ? throw new RuntimeException("調(diào)用3A認(rèn)證接口失?。?+ object.get("msg"));
? ? ? ? }
? ? }
}內(nèi)部feign調(diào)用不用認(rèn)證
@Configuration
public class FeignRequestInterceptorConfig implements RequestInterceptor {?
? ? ?@Bean
? ? ?@LoadBalanced
? ? ?RestTemplate restTemplate(){
? ? ? ? ?return new RestTemplate();
? ? ?}
? ? @Override
? ? public void apply(RequestTemplate requestTemplate) {
? ? ? ? requestTemplate.header("feign","YES");
? ? }
}需要認(rèn)證的接口
?? ?@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 做微服務(wù)調(diào)用,為方便統(tǒng)一處理 Feign,想到了用 AOP 實現(xiàn),即使用 within 指示器匹配 feign.Client 接口的實現(xiàn)進(jìn)行 AOP 切入。代碼如下,通過 @Before 注解在執(zhí)行方法前打印日志,并在代碼中定義了一個標(biāo)記了@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)用服務(wù)后可以看到日志中有輸出,的確實現(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]
一開始這個項目使用的是客戶端的負(fù)載均衡,也就是讓 Ribbon 來做負(fù)載均衡,代碼沒啥問題。后來因為后端服務(wù)通過 Nginx 實現(xiàn)服務(wù)端負(fù)載均衡,所以開發(fā)同學(xué)把@FeignClient 的配置設(shè)置了 URL 屬性,直接通過一個固定 URL 調(diào)用后端服務(wù):
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 進(jìn)行接口調(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
這就很費(fèi)解了。難道為 Feign 指定了 URL,其實現(xiàn)就不是 feign.Clinet 了嗎?要明白原因,我們需要分析一下 FeignClient 的創(chuàng)建過程,也就是分析FeignClientFactoryBean 類的 getTarget 方法。源碼第 4 行有一個 if 判斷,當(dāng) 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)過代理增強(qiáng)的,明顯是一個 Bean:

所以,沒有指定 URL 的 @FeignClient 對應(yīng)的 LoadBalanceFeignClient,是可以通過feign.Client 切入的。在我們上面貼出來的源碼的 16 行可以看到,當(dāng) URL 不為空的時候,client 設(shè)置為了LoadBalanceFeignClient 的 delegate 屬性。
其原因注釋中有提到,因為有了 URL 就不需要客戶端負(fù)載均衡了,但因為 Ribbon 在 classpath 中,所以需要從LoadBalanceFeignClient 提取出真正的 Client。斷點調(diào)試下可以看到,這時 client 是一個ApacheHttpClient

那么,這個 ApacheHttpClient 是從哪里來的呢?這里,我教你一個小技巧:如果你希望知道一個類是怎樣調(diào)用棧初始化的,可以在構(gòu)造方法中設(shè)置一個斷點進(jìn)行調(diào)試。這樣,你就可以在 IDE 的棧窗口看到整個方法調(diào)用棧,然后點擊每一個棧幀看到整個過程。
用這種方式,我們可以看到,是 HttpClientFeignLoadBalancedConfiguration 類實例化的 ApacheHttpClient:

進(jìn)一步查看 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+) 無法切入設(shè)置過 URL 的@FeignClient ClientWithUrl:因此,定義了 URL 的 FeignClient 采用 within(feign.Client+) 無法切入。那,如何解決這個問題呢?有一位同學(xué)提出,修改一下切點表達(dá)式,通過 @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
但仔細(xì)一看就會發(fā)現(xiàn),這次切入的是 ClientWithUrl 接口的 API 方法,并不是client.Feign 接口的 execute 方法,顯然不符合預(yù)期。
這位同學(xué)犯的錯誤是,沒有弄清楚真正希望切的是什么對象。@FeignClient 注解標(biāo)記在Feign Client 接口上,所以切的是 Feign 定義的接口,也就是每一個實際的 API 接口。而通過 feign.Client 接口切的是客戶端實現(xiàn)類,切到的是通用的、執(zhí)行所有 Feign 調(diào)用的execute 方法。那么問題來了,ApacheHttpClient 不是 Bean 無法切入,切 Feign 接口本身又不符合要求。怎么辦呢?
經(jīng)過一番研究發(fā)現(xiàn),ApacheHttpClient 其實有機(jī)會獨立成為 Bean。查看HttpClientFeignConfiguration 的源碼可以發(fā)現(xiàn),當(dāng)沒有 ILoadBalancer 類型的時候,自動裝配會把 ApacheHttpClient 設(shè)置為 Bean。
這么做的原因很明確,如果我們不希望做客戶端負(fù)載均衡的話,應(yīng)該不會引用 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 默認(rèn)使用 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 相關(guān)的坑點和你說清楚了。除此之外,我們在業(yè)務(wù)開發(fā)時,還有一個繞不開的點是,Spring 程序的配置問題。接下來,我們就看具體吧。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
java 服務(wù)器接口快速開發(fā)之servlet詳細(xì)教程
Servlet(Server Applet)是Java Servlet的簡稱,稱為小服務(wù)程序或服務(wù)連接器,用Java編寫的服務(wù)器端程序,具有獨立于平臺和協(xié)議的特性,主要功能在于交互式地瀏覽和生成數(shù)據(jù),生成動態(tài)Web內(nèi)容2021-06-06

