Spring Boot源码分析 - Configuration注解

@Configuration注解

@Configuration注解指示一个类声明一个或多个@Bean方法, 并且可以由Spring容器处理, 以在运行时为这些bean生成bean定义和服务请求.

使用ConfigurationClassParser来对@Configuration标注的类进行解析, 封装成ConfigurationClass实例. 具体的实现通过ConfigurationClassPostProcessor来实现的.

ConfigurationClassPostProcessor

实现了BeanDefinitionRegistryPostProcessor接口, 间接实现了BeanFactorPostProcessor接口.

  • #postProcessBeanDefinitionRegistry(): 注册所有ConfigurationClass中的BeanDefinition, 包括@Bean注解的方法, @ImporResource引入的资源中定义的bean, 和@Import注解引入的ImportBeanDefinitionRegistrar中注册的BeanDefinition
  • #postProcessBeanFactory(): 在运行时以通过cglig增强的类来替换ConfigurationClass, 为服务bean请求做准备. 增强的实现是通过ConfigurationClassEnhancer完成的.

插入一点, ConfigurationClassEnhancer实现了直接使用bean注册方法来获取bean的操作, 提供了一个BeanMethodInterceptor的内部类来实行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

@Configuration
public class Config {
@Bean
public A a() {
...
return a;
}
@Bean
public B b() {
b.setA(a());
...
return b;
}
}

Full ConfigurationClass VS Lite ConfigurationClass

先说区别: full的ConfigurationClass会使用CGLIB进行增强.

查看类ConfigurationClassUtils, 其中有两个方法#isFullConfigurationClass()#isLiteConfigurationClass().

方法的实现是去检查BeanDefinition中的ConfigurationClassPostProcessor.configurationClass属性, 是full还是lite.

这个属性的值又来源于#checkConfigurationClassCandidate()方法, 如果BeanDefinition使用的是@Configuration注解, 则为full; 如果是@Component, @ComponentScan, @Import或者@ImportResource中的任何一种, 则为lite. 如果是ConfigurationClass, 则会继续为其添加顺序属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
...
if (isFullConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
}
else if (isLiteConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
}
else {
return false;
}
// It's a full or lite configuration candidate... Let's determine the order value, if any.
Map<String, Object> orderAttributes = metadata.getAnnotationAttributes(Order.class.getName());
if (orderAttributes != null) {
beanDef.setAttribute(ORDER_ATTRIBUTE, orderAttributes.get(AnnotationUtils.VALUE));
}
}

Nginx实现Elasticsearch的HTTP基本认证

Elasticssearch的HTTP基本认证实现有两种方案: x-pack和nginx反向代理. 前者收费, 后者不太适合生产使用. 如果仅仅是开发测试, 第二种完全足够.

创建密码

1
htpasswd -bc ./passwd [username] [password]

Docker compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: '3'
services:
elasticsearch:
image: elasticsearch:5.5.2
container_name: elasticsearch
restart: unless-stopped
volumes:
- /tmp/elasticsearch:/usr/share/elasticsearch/data
nginx:
image: nginx:latest
container_name: elasticsearch-proxy
ports:
- 9200:9200
links:
- elasticsearch
volumes:
- ./passwd:/etc/nginx/.passwd
- ./default.conf:/etc/nginx/conf.d/default.conf

nginx配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
upstream es {
server elasticsearch:9200;
keepalive 15;
}

server {

listen 9200;
server_name localhost;
access_log /dev/stdout;
error_log /dev/stdout;

location / {
auth_basic "Administrator’s Area";
auth_basic_user_file /etc/nginx/.passwd;
proxy_http_version 1.1;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Proxy-Connection "Keep-Alive";
proxy_pass http://es;
}

location /health {
access_log off;
return 200 "healthy\n";
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

Alpine容器安装Docker和OpenShift Client Tools

安装Docker

1
2
3
4
5
6
7
8
9
10
11
12
13
echo "http://dl-2.alpinelinux.org/alpine/edge/main" > /etc/apk/repositories
echo "http://dl-2.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories
echo "http://dl-2.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories

apk -U --no-cache \
--allow-untrusted add \
shadow \
docker \
py-pip \
openrc \
&& pip install docker-compose

rc-update add docker boot

安装OpenShift Client Tools

需要先安装glibc

1
2
3
4
apk --no-cache add ca-certificates wget
wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk
apk add glibc-2.28-r0.apk

curl --retry 7 -Lo /tmp/client-tools.tar.gz "https://mirror.openshift.com/pub/openshift-v3/clients/3.9.1/linux/oc.tar.gz"

1
2
3
4
5
6
7
8
curl --retry 7 -Lo /tmp/client-tools.tar.gz "https://mirror.openshift.com/pub/openshift-v3/clients/3.9.1/linux/oc.tar.gz"

tar zxf /tmp/client-tools.tar.gz -C /usr/local/bin oc \
&& rm /tmp/client-tools.tar.gz \
&& apk del .build-deps

# ADDED: Resolve issue x509 oc login issue
apk add --update ca-certificates

参考: github issue

Zuul网关Ribbon重试

相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#如果路由转发请求发生超时(连接超时或处理超时), 只要超时时间的设置小于Hystrix的命令超时时间,那么它就会自动发起重试. 默认为false. 或者对指定响应状态码进行重试
zuul.retryable = true
zuul.routes.<route>.retryable = false

#同一实例上的最大重试次数, 默认值为0. 不包括首次调用
ribbon.MaxAutoRetries=0
#重试其他实例的最大重试次数, 不包括第一次选的实例. 默认为1
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作执行重试, 默认值为false, 只重试`GET`请求
ribbon.OkToRetryOnAllOperations=false
#连接超时, 默认2000
ribbon.ConnectTimeout=15000
#响应超时, 默认5000
ribbon.ReadTimeout=15000
#每个host的最大连接数
ribbon.MaxHttpConnectionsPerHost=50
#最大连接数
ribbon.MaxTotalHttpConnections=200
#何种响应状态码才进行重试
ribbon.retryableStatusCodes=404,502

实现

  1. SimpleRouteLocator#getRoute返回的route对象中会带上retryable的设置.
  2. PreDecorationFilter在对RequestContext进行装饰的时候会将retryable的设置通过keyFilterConstants.RETRYABLE_KEY注入RequestContext中.
  3. RibbonRoutingFilter#buildCommandContext会使用RequestContextretryable设置构造RibbonCommandContext对象.
  4. RibbonCommandFactory使用RibbonCommandContext构建出RibbonCommand对象.
  5. RibbonCommand#run中, 当retryabletrue时, 会调用IClientexecute方法处理请求. 为false时, 会调用IClientexecuteWithLoadBalancer方法执行请求.
    • execute会在失败时进行重试(不超过超时限制)
    • executeWithLoadBalancer方法是先通过LoadBalancer选择出一个Server, 然后构建出请求地址.
  6. IClient#execute执行时, 通过LoadBalancedRetryPolicyFactory创建一个LoadBalancedRetryPolicy对象. LoadBalancedRetryPolicy持有上面ribbon.XXX的设置. 当响应状态码不在ribbon.retryableStatusCodes设置中, 则会直接返回响应. 如果属于可重试的响应状态码, 则会将响应封装为HttpClientStatusCodeException抛出. 异常被RetryTemplate捕获, 然后使用LoadBalancedRetryPolicy对当前状态(MaxAutoRetries, MaxAutoRetriesNextServer)计算出能否进行一次重试. 直至成功, 或者当前状态不满足条件.

Hystrix工作原理三

异常处理

Hystrix异常类型

  • HystrixRuntimeException
  • HystrixBadRequestException
  • HystrixTimeoutException
  • RejectedExecutionException

HystrixRuntimeException

HystrixCommand失败时抛出, 不会触发fallback.

HystrixBadRequestException

用提供的参数或状态表示错误的异常, 而不是执行失败. 与其他HystrixCommand抛出的异常不同, 这个异常不会触发fallback, 也不会记录进failure的指标, 因而也不会触发断路器,

应该在用户输入引起的错误是抛出, 否则会它与容错和后退行为的目的相悖.

不会触发fallback, 也不会记录到错误的指标中, 也不会触发断路器.

RejectedExecutionException

线程池发生reject时抛出

HystrixTimeoutException

HystrixCommand.run()或者HystrixObservableCommand.construct()时抛出, 会记录timeout的次数. 如果希望某些类型的失败被记录为timeout, 应该将这些类型的失败包装为HystrixTimeoutException

异常处理

ignoreExceptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
@Override
public Observable<R> call(Throwable t) {
circuitBreaker.markNonSuccess();
Exception e = getExceptionFromThrowable(t);
executionResult = executionResult.setExecutionException(e);
if (e instanceof RejectedExecutionException) {
return handleThreadPoolRejectionViaFallback(e);
} else if (t instanceof HystrixTimeoutException) {
return handleTimeoutViaFallback();
} else if (t instanceof HystrixBadRequestException) {
return handleBadRequestByEmittingError(e);
} else {
/*
* Treat HystrixBadRequestException from ExecutionHook like a plain HystrixBadRequestException.
*/
if (e instanceof HystrixBadRequestException) {
eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey);
return Observable.error(e);
}
return handleFailureViaFallback(e);
}
}
};

Feign中响应状态码处理

Feign使用SynchronousMethodHandler做请求的执行和响应的处理. 响应处理的部分, 对[200, 300)区间的状态, 会将response返回; 如果是404, 根据@FeignClientdecode404(默认为false)和方法返回值判断是否熔断, 如果响应返回404, decodefalse, 同时方法返回值不是void, 会包装成FeignException抛出; 其他的状态, 通过包装成FeignException抛出.

FeignExceptionRuntimeException的实现, 如果没有ignore的话, 会计入熔断器的计算中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final class SynchronousMethodHandler implements MethodHandler {
Object executeAndDecode(RequestTemplate template) throws Throwable {
...
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
return decode(response);
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
return decode(response);
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
...
}
}

Ribbon中响应状态码处理

在Zuul中, 路由使用Ribbon做负载均衡, 同时使用Hystrix做断路器, 使用RibbonCommand接口的实现. RibbonCommand的实现并没有对响应编码封装异常, 因此也不会触发熔断器.

AbstractRibbonCommandRibbonCommand的抽象实现, 所有其他实现的父类. 核心run()方法并没有针对响应编码重新封装异常.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwareClient<RQ, RS>, RQ extends ClientRequest, RS extends HttpResponse>
extends HystrixCommand<ClientHttpResponse> implements RibbonCommand {
...
@Override
protected ClientHttpResponse run() throws Exception {
final RequestContext context = RequestContext.getCurrentContext();

RQ request = createRequest();
RS response;

boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
&& ((AbstractLoadBalancingClient)this.client).isClientRetryable((ContextAwareRequest)request);

if (retryableClient) {
response = this.client.execute(request, config);
} else {
response = this.client.executeWithLoadBalancer(request, config);
}
context.set("ribbonResponse", response);

// Explicitly close the HttpResponse if the Hystrix command timed out to
// release the underlying HTTP connection held by the response.
//
if (this.isResponseTimedOut()) {
if (response != null) {
response.close();
}
}

return new RibbonHttpResponse(response);
}
...
}

Observable.error(ex)会捕获run()方法抛出的异常.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class HystrixCommand<R> extends AbstractCommand<R> implements HystrixExecutable<R>, HystrixInvokableInfo<R>, HystrixObservable<R> {
...
final protected Observable<R> getExecutionObservable() {
return Observable.defer(new Func0<Observable<R>>() {
@Override
public Observable<R> call() {
try {
return Observable.just(run());
} catch (Throwable ex) {
return Observable.error(ex);
}
}
}).doOnSubscribe(new Action0() {
@Override
public void call() {
// Save thread on which we get subscribed so that we can interrupt it later if needed
executionThread.set(Thread.currentThread());
}
});
}
...
}

Hystrix 超时处理

在Hystrix版本1.4之前, Seamphore策略是不支持超时的. 目前spring-cloud-netflix的1.4.4中使用的是1.5.12

如果开启了timeout, HystrixCommand会lift一个HystrixObservableTimeoutOperatorObservable中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class AbstractCommand<R> implements HystrixInvokableInfo<R>, HystrixObservable<R> {
private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) {
...
Observable<R> execution;
if (properties.executionTimeoutEnabled().get()) {
execution = executeCommandWithSpecifiedIsolation(_cmd)
.lift(new HystrixObservableTimeoutOperator<R>(_cmd));
} else {
execution = executeCommandWithSpecifiedIsolation(_cmd);
}

return execution.doOnNext(markEmits)
.doOnCompleted(markOnCompleted)
.onErrorResumeNext(handleFallback)
.doOnEach(setRequestContext);
}
}

这个HystrixObservableTimeoutOperator会添加注册TimeListener. TimeListener是以tick的方式运行, 即启动一个线程延迟executionTimeoutInMilliseconds运行, 然后每次在executionTimeoutInMilliseconds + n * executionTimeoutInMilliseconds时运行.

如果判断操作超时? 看tick方法的实现, 线程每次运行时, 尝试修改Command的状态从NOT_EXECUTEDTIMED_OUT. 如果成功, 说明运行超时. 最后抛出HystrixTimeoutException异常, 被handleFallback处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// if we can go from NOT_EXECUTED to TIMED_OUT then we do the timeout codepath
// otherwise it means we lost a race and the run() execution completed or did not start
if (originalCommand.isCommandTimedOut.compareAndSet(TimedOutStatus.NOT_EXECUTED, TimedOutStatus.TIMED_OUT)) {
// report timeout failure
originalCommand.eventNotifier.markEvent(HystrixEventType.TIMEOUT, originalCommand.commandKey);
// shut down the original request
s.unsubscribe();
final HystrixContextRunnable timeoutRunnable = new HystrixContextRunnable(originalCommand.concurrencyStrategy, hystrixRequestContext, new Runnable() {
@Override
public void run() {
child.onError(new HystrixTimeoutException());
}
});
timeoutRunnable.run();
//if it did not start, then we need to mark a command start for concurrency metrics, and then issue the timeout
}