SpringCloudGateway源码阅读(一)核心概念及

一、核心概念

路由Route

路由是网关的核心抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码public class Route implements Ordered {
// 唯一id
private final String id;
// 跳转uri
private final URI uri;
// SpringBean优先级
private final int order;
// 断言
private final AsyncPredicate<ServerWebExchange> predicate;
// 当前路由特有的过滤器
private final List<GatewayFilter> gatewayFilters;
// 元数据
private final Map<String, Object> metadata;
}

ServerWebExchange

ServerWebExchange是spring-web的一个接口,提供ServerHttpRequest、ServerHttpResponse,ApplicationContext等实例的get方法。

断言AsyncPredicate

断言用于判断路由是否匹配某个ServerWebExchange。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
csharp复制代码public interface AsyncPredicate<T> extends Function<T, Publisher<Boolean>> {
// 构造与AsyncPredicate
default AsyncPredicate<T> and(AsyncPredicate<? super T> other) {
return new AndAsyncPredicate<>(this, other);
}
// 构造非AsyncPredicate
default AsyncPredicate<T> negate() {
return new NegateAsyncPredicate<>(this);
}
// 构造或AsyncPredicate
default AsyncPredicate<T> or(AsyncPredicate<? super T> other) {
return new OrAsyncPredicate<>(this, other);
}
// 构造默认AsyncPredicate
static AsyncPredicate<ServerWebExchange> from(
Predicate<? super ServerWebExchange> predicate) {
return new DefaultAsyncPredicate<>(GatewayPredicate.wrapIfNeeded(predicate));
}
// 默认AsyncPredicate
class DefaultAsyncPredicate<T> implements AsyncPredicate<T> {

private final Predicate<T> delegate;

public DefaultAsyncPredicate(Predicate<T> delegate) {
this.delegate = delegate;
}

@Override
public Publisher<Boolean> apply(T t) {
return Mono.just(delegate.test(t));
}

}
// 非AsyncPredicate
class NegateAsyncPredicate<T> implements AsyncPredicate<T> {

private final AsyncPredicate<? super T> predicate;

public NegateAsyncPredicate(AsyncPredicate<? super T> predicate) {
this.predicate = predicate;
}

@Override
public Publisher<Boolean> apply(T t) {
return Mono.from(predicate.apply(t)).map(b -> !b);
}

}
// 与AsyncPredicate
class AndAsyncPredicate<T> implements AsyncPredicate<T> {

private final AsyncPredicate<? super T> left;

private final AsyncPredicate<? super T> right;

public AndAsyncPredicate(AsyncPredicate<? super T> left,
AsyncPredicate<? super T> right) {
this.left = left;
this.right = right;
}

@Override
public Publisher<Boolean> apply(T t) {
return Mono.from(left.apply(t)).flatMap(
result -> !result ? Mono.just(false) : Mono.from(right.apply(t)));
}
}
// 或AsyncPredicate
class OrAsyncPredicate<T> implements AsyncPredicate<T> {

private final AsyncPredicate<? super T> left;

private final AsyncPredicate<? super T> right;

public OrAsyncPredicate(AsyncPredicate<? super T> left,
AsyncPredicate<? super T> right) {
this.left = left;
this.right = right;
}

@Override
public Publisher<Boolean> apply(T t) {
return Mono.from(left.apply(t)).flatMap(
result -> result ? Mono.just(true) : Mono.from(right.apply(t)));
}

}

}

路由过滤器GatewayFilter

针对于路由的过滤器,无法离开Route而存在。

1
2
3
java复制代码public interface GatewayFilter extends ShortcutConfigurable {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

全局过滤器GlobalFilter

全局的过滤器,所有路由都必须执行的过滤器。后续Bean实例化的时候,会适配成GatewayFilter被使用。

1
2
3
kotlin复制代码public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

过滤器链GatewayFilterChain

GatewayFilterChain过滤器链允许GatewayFilter过滤器按顺序,挨个执行。

1
2
3
kotlin复制代码public interface GatewayFilterChain {
Mono<Void> filter(ServerWebExchange exchange);
}

路由定义RouteDefinition

类似于Spring的BeanDefinition,Route的一种构建方式就是通过RouteDefinition,比如从properties文件中解析得到的路由规则定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码public class RouteDefinition {
// 唯一id
private String id;
// 断言定义
private List<PredicateDefinition> predicates = new ArrayList<>();
// 过滤器定义
private List<FilterDefinition> filters = new ArrayList<>();
// 跳转uri
private URI uri;
// 元数据
private Map<String, Object> metadata = new HashMap<>();
// Spring优先级
private int order = 0;
}

断言定义PredicateDefinition

从配置文件加载的断言定义,构造Route时,会用RoutePredicateFactory#applyAsync转换成AsyncPredicate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arduino复制代码public class PredicateDefinition {
private String name;
private Map<String, String> args = new LinkedHashMap<>();
public PredicateDefinition() {
}
// predicates:
// - Path=/echo // 解析'Path=/echo'放入args
public PredicateDefinition(String text) {
int eqIdx = text.indexOf('=');
// 设置name
setName(text.substring(0, eqIdx));

String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");
// 设置args
for (int i = 0; i < args.length; i++) {
this.args.put(NameUtils.generateName(i), args[i]);
}
}
}

路由过滤器定义FilterDefinition

从配置文件加载的路由过滤器定义,构造Route时,会用GatewayFilterFactory#apply转换为GatewayFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arduino复制代码public class FilterDefinition {
private String name;
private Map<String, String> args = new LinkedHashMap<>();
public FilterDefinition() {
}
// 解析配置文件 PrefixPath=/httpbin
public FilterDefinition(String text) {
int eqIdx = text.indexOf('=');
if (eqIdx <= 0) {
setName(text);
return;
}
setName(text.substring(0, eqIdx));

String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");

for (int i = 0; i < args.length; i++) {
this.args.put(NameUtils.generateName(i), args[i]);
}
}
}

获取路由定义RouteDefinitionLocator

RouteDefinitionLocator具有获取路由定义的能力。

1
2
3
csharp复制代码public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}

最后对外暴露的实际是CompositeRouteDefinitionLocator,他组合了所有RouteDefinitionLocator。

获取路由RouteLocator

RouteLocator具有获取路由Route的能力。网关处理请求时只会调用RouteLocator获取Route,通过Route的断言和过滤处理请求。

1
2
3
csharp复制代码public interface RouteLocator {
Flux<Route> getRoutes();
}

Spring容器加载的时候,会把路由都放到CachingRouteLocator里,后续运行时只会和CachingRouteLocator打交道。

二、加载路由

讲解从Gateway的Bean装配,到加载RouteCachingRouteLocator

自动配置

Spring-Cloud-Gateway的自动配置类是org.springframework.cloud.gateway.config.GatewayAutoConfiguration。重点看一些重要的Bean装配,全局Filter等下一章节讲请求流程的时候再分析。

Gateway配置文件

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码@Bean
public GatewayProperties gatewayProperties() {
return new GatewayProperties();
}
@ConfigurationProperties("spring.cloud.gateway")
@Validated
public class GatewayProperties {
// 路由
private List<RouteDefinition> routes = new ArrayList<>();
// 默认过滤器
private List<FilterDefinition> defaultFilters = new ArrayList<>();
}

RouteDefinitionLocator相关

  • PropertiesRouteDefinitionLocator:配置文件创建RouteDefinitionLocator
1
2
3
4
5
6
less复制代码@Bean
@ConditionalOnMissingBean
public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(
GatewayProperties properties) {
return new PropertiesRouteDefinitionLocator(properties);
}
  • InMemoryRouteDefinitionRepository:内存级别路由定义,无法持久化,支持动态新增和删除路由定义
1
2
3
4
5
less复制代码@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
return new InMemoryRouteDefinitionRepository();
}
  • CompositeRouteDefinitionLocator:组合其他所有的RouteDefinitionLocator,并且是Primary的
1
2
3
4
5
6
7
less复制代码@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(
List<RouteDefinitionLocator> routeDefinitionLocators) {
return new CompositeRouteDefinitionLocator(
Flux.fromIterable(routeDefinitionLocators));
}

RouteLocator相关

  • RouteLocatorBuilder:辅助用编码方式注入自定义的RouteLocator。
1
2
3
4
5
typescript复制代码@Bean
public RouteLocatorBuilder routeLocatorBuilder(
ConfigurableApplicationContext context) {
return new RouteLocatorBuilder(context);
}
  • RouteDefinitionRouteLocator:用RouteDefinitionLocator和GatewayFilterFactory和RoutePredicateFactory构造Route,创建RouteLocator。
1
2
3
4
5
6
7
8
9
typescript复制代码@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,
List<GatewayFilterFactory> gatewayFilters,
List<RoutePredicateFactory> predicates,
RouteDefinitionLocator routeDefinitionLocator,
ConfigurationService configurationService) {
return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates,
gatewayFilters, properties, configurationService);
}
  • CachingRouteLocator:委托CompositeRouteLocator聚合其他所有RouteLocator,实现RouteLocator。
1
2
3
4
5
6
7
less复制代码@Bean
@Primary
@ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
return new CachingRouteLocator(
new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}

加载路由入口类RouteRefreshListener

1
2
3
4
typescript复制代码@Bean
public RouteRefreshListener routeRefreshListener(ApplicationEventPublisher publisher) {
return new RouteRefreshListener(publisher);
}

加载路由到CachingRouteLocator

RouteRefreshListener

加载路由到RouteLocator的入口,通过Spring事件触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
csharp复制代码public class RouteRefreshListener implements ApplicationListener<ApplicationEvent> {

private final ApplicationEventPublisher publisher;

public RouteRefreshListener(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent
|| event instanceof RefreshScopeRefreshedEvent
|| event instanceof InstanceRegisteredEvent) {
reset();
}
// ... 省略其他事件判断,也可能触发reset
}

// 发布RefreshRoutesEvent事件
private void reset() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}

}

CachingRouteLocator

CachingRouteLocator收到RefreshRoutesEvent,委托CompositeRouteLocator获取Flux<Route>放入自己的缓存cache中。

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
59
60
61
62
63
64
65
66
67
68
69
typescript复制代码public class CachingRouteLocator implements Ordered, RouteLocator,
ApplicationListener<RefreshRoutesEvent>, ApplicationEventPublisherAware {
// 写死的缓存cache的key
private static final String CACHE_KEY = "routes";
// CompositeRouteLocator
private final RouteLocator delegate;
// Route列表
private final Flux<Route> routes;
// 缓存
private final Map<String, List> cache = new ConcurrentHashMap<>();
// 事件发布者
private ApplicationEventPublisher applicationEventPublisher;

public CachingRouteLocator(RouteLocator delegate) {
this.delegate = delegate;
// 这里并不会触发fetch,只有当需要routes的时候才会触发fetch,比如getRoutes()
routes = CacheFlux.lookup(cache, CACHE_KEY, Route.class)
.onCacheMissResume(this::fetch);
}
// 委托CompositeRouteLocator获取Route并排序
private Flux<Route> fetch() {
return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE);
}
// 获取Route,如果routes为空,会触发fetch
@Override
public Flux<Route> getRoutes() {
return this.routes;
}
// 清空缓存
public Flux<Route> refresh() {
this.cache.clear();
return this.routes;
}
// 接收RefreshRoutesEvent
@Override
public void onApplicationEvent(RefreshRoutesEvent event) {
try {
// 委托CompositeRouteLocator获取Route并排序
fetch()
.collect(Collectors.toList())
.subscribe(list -> Flux.fromIterable(list)
.materialize().collect(Collectors.toList()).subscribe(signals -> {
applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this));
// 放入缓存
cache.put(CACHE_KEY, signals);
}, throwable -> handleRefreshError(throwable)));
}
catch (Throwable e) {
handleRefreshError(e);
}
}
// 发生异常发布RefreshRoutesResultEvent事件
private void handleRefreshError(Throwable throwable) {
applicationEventPublisher
.publishEvent(new RefreshRoutesResultEvent(this, throwable));
}
void handleRefresh() {
refresh();
}
@Override
public int getOrder() {
return 0;
}
@Override
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}

CompositeRouteLocator

通过其他RouteLocator获取所有Route。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码public class CompositeRouteLocator implements RouteLocator {
// 其他所有RouteLocator,除了CachingRouteLocator
private final Flux<RouteLocator> delegates;
public CompositeRouteLocator(Flux<RouteLocator> delegates) {
this.delegates = delegates;
}
@Override
public Flux<Route> getRoutes() {
return this.delegates.flatMapSequential(RouteLocator::getRoutes);
}
}

1 编码方式获取Route

1
2
3
4
5
6
7
8
9
10
11
less复制代码@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.host("**.abc.org").and().path("/anything/png")
.filters(f ->
f.prefixPath("/httpbin")
.addResponseHeader("X-TestHeader", "foobar"))
.uri(uri)
)
.build();
}

RouteLocatorBuilder.Builder构造的RouteLocator,Bean注入的时候直接构造完成了。具体构造过程不细看了,也是利用了RoutePredicateFactory和GatewayFilterFactory。

1
2
3
4
5
6
csharp复制代码public static class Builder {
private List<Route.AsyncBuilder> routes = new ArrayList<>();
public RouteLocator build() {
return () -> Flux.fromIterable(this.routes)
.map(routeBuilder -> routeBuilder.build());
}

2 RouteDefinitionRouteLocator

RouteDefinitionRouteLocator只负责通过RouteDefinition创建Route,委托CompositeRouteDefinitionLocator获取RouteDefinition,并通过ConfigurationService、RoutePredicateFactory、GatewayFilterFactory将RouteDefinition转换为Route返回。

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
arduino复制代码public class RouteDefinitionRouteLocator
implements RouteLocator, BeanFactoryAware, ApplicationEventPublisherAware {
// 默认过滤器名字
public static final String DEFAULT_FILTERS = "defaultFilters";
// 委托CompositeRouteDefinitionLocator
private final RouteDefinitionLocator routeDefinitionLocator;
// ConfigurationService操作RoutePredicateFactory和GatewayFilterFactory
// 转换断言和过滤器
private final ConfigurationService configurationService;
// name - RoutePredicateFactory
private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap<>();
// name - GatewayFilterFactory
private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap<>();
// spring.cloud.gateway配置文件
private final GatewayProperties gatewayProperties;

@Override
public Flux<Route> getRoutes() {
Flux<Route> routes = this.routeDefinitionLocator
// 委托CompositeRouteDefinitionLocator获取RouteDefinition
.getRouteDefinitions()
// 转换为Route
.map(this::convertToRoute);
// 如果spring.cloud.gateway.failOnRouteDefinitionError=true(默认)
// 仅仅打印日志
if (!gatewayProperties.isFailOnRouteDefinitionError()) {
routes = routes.onErrorContinue((error, obj) -> {
logger.warn(...);
});
}
return routes;
}
}
  • CompositeRouteDefinitionLocator

getRouteDefinitions循环所有RouteDefinitionLocator获取所有RouteDefinition。

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
kotlin复制代码public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {
private final Flux<RouteDefinitionLocator> delegates;
private final IdGenerator idGenerator;
public CompositeRouteDefinitionLocator(Flux<RouteDefinitionLocator> delegates) {
this(delegates, new AlternativeJdkIdGenerator());
}
public CompositeRouteDefinitionLocator(Flux<RouteDefinitionLocator> delegates,
IdGenerator idGenerator) {
this.delegates = delegates;
this.idGenerator = idGenerator;
}

@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return this.delegates
// 委托所有其他RouteDefinitionLocator获取RouteDefinition
.flatMapSequential(RouteDefinitionLocator::getRouteDefinitions)
.flatMap(routeDefinition -> {
// 如果RouteDefinition没有设置id,随便给一个
if (routeDefinition.getId() == null) {
return randomId().map(id -> {
routeDefinition.setId(id);
return routeDefinition;
});
}
return Mono.just(routeDefinition);
});
}
// 获取随机id
protected Mono<String> randomId() {
return Mono.fromSupplier(idGenerator::toString)
.publishOn(Schedulers.boundedElastic());
}
}

RouteDefinitionLocator有很多种,以PropertiesRouteDefinitionLocator为例。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator {
private final GatewayProperties properties;
public PropertiesRouteDefinitionLocator(GatewayProperties properties) {
this.properties = properties;
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(this.properties.getRoutes());
}
}
  • RouteDefinitionRouteLocator.convertToRoute

通过ConfigurationService、RoutePredicateFactory、GatewayFilterFactory将RouteDefinition转换为Route。

1
2
3
4
5
6
7
8
9
10
11
scss复制代码private Route convertToRoute(RouteDefinition routeDefinition) {
// 将routeDefinition里的PredicateDefinition
// 通过RoutePredicateFactory转换为AsyncPredicate
AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
// 将routeDefinition里的FilterDefinition
// 通过GatewayFilterFactory转换为GatewayFilter
List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);
// 组装Route
return Route.async(routeDefinition).asyncPredicate(predicate)
.replaceFilters(gatewayFilters).build();
}

RouteDefinitionRouteLocator#combinePredicates转换PredicateDefinitionAsyncPredicate

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
scss复制代码private AsyncPredicate<ServerWebExchange> combinePredicates(
RouteDefinition routeDefinition) {
List<PredicateDefinition> predicates = routeDefinition.getPredicates();
// 先获取第一个断言定义,转换为AsyncPredicate
AsyncPredicate<ServerWebExchange> predicate = lookup(routeDefinition,
predicates.get(0));
// 后续定义用and连接
for (PredicateDefinition andPredicate : predicates.subList(1,
predicates.size())) {
AsyncPredicate<ServerWebExchange> found = lookup(routeDefinition,
andPredicate);
predicate = predicate.and(found);
}
return predicate;
}
private AsyncPredicate<ServerWebExchange> lookup(RouteDefinition route,
PredicateDefinition predicate) {
// 通过PredicateDefinition的name找到对应的RoutePredicateFactory
RoutePredicateFactory<Object> factory = this.predicates.get(predicate.getName());
// ConfigurationService操作RouteDefinition和RoutePredicateFactory
Object config = this.configurationService.with(factory)
.name(predicate.getName())
.properties(predicate.getArgs())
.eventFunction((bound, properties) -> new PredicateArgsEvent(
RouteDefinitionRouteLocator.this, route.getId(), properties))
.bind();
return factory.applyAsync(config);
}

RouteDefinitionRouteLocator#getFiltersFilterDefinition转换为GatewayFilter

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
scss复制代码private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {
List<GatewayFilter> filters = new ArrayList<>();
// 如果默认过滤器不为空,加入默认过滤器
if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {
filters.addAll(loadGatewayFilters(DEFAULT_FILTERS,
new ArrayList<>(this.gatewayProperties.getDefaultFilters())));
}
// 如果routeDefinition里的过滤器不为空,加入过滤器
if (!routeDefinition.getFilters().isEmpty()) {
filters.addAll(loadGatewayFilters(routeDefinition.getId(),
new ArrayList<>(routeDefinition.getFilters())));
}
// 排序
AnnotationAwareOrderComparator.sort(filters);
return filters;
}
List<GatewayFilter> loadGatewayFilters(String id,List<FilterDefinition> filterDefinitions) {
ArrayList<GatewayFilter> ordered = new ArrayList<>(filterDefinitions.size());
for (int i = 0; i < filterDefinitions.size(); i++) {
// Filter定义
FilterDefinition definition = filterDefinitions.get(i);
// 根据Filter定义的name获取GatewayFilterFactory
GatewayFilterFactory factory = this.gatewayFilterFactories
.get(definition.getName());
// 配置
Object configuration = this.configurationService.with(factory)
.name(definition.getName())
.properties(definition.getArgs())
.eventFunction((bound, properties) -> new FilterArgsEvent(
RouteDefinitionRouteLocator.this, id, (Map<String, Object>) properties))
.bind();
// 转换为GatewayFilter
GatewayFilter gatewayFilter = factory.apply(configuration);
if (gatewayFilter instanceof Ordered) {
ordered.add(gatewayFilter);
}
else {
ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));
}
}

return ordered;
}

总结

  • 理清Route、RouteLocator、RouteDefinition、RouteDefinitionLocator的含义。最后对运行时实际暴露的对象只有CachingRouteLocator和Route。
  • RouteRefreshListener接收容器事件,发布RefreshRoutesEvent事件,触发路由加载至CachingRouteLocator。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%