在使用Spring Cloud Gateway时,需要配置路由,以便将不同的请求转发到不同的服务上。路由配置有两种,一是在配置文件中配置,而是自定义RouteLocator来进行代码配置。当请求到达Spring Cloud Gateway时,会获取所有路由信息,然后通过断言来匹配路由。本文就来分析一下Spring Cloud Gateway是怎么实现路由管理的。
CachingRouteLocator
创建过程
在RoutePredicateHandlerMapping中,被注入的是CachingRouteLocator类型的对象。这个类采用装饰器模式封装了其他的路由定位器,目的是在创建时就获取路由信息。
private static final String CACHE_KEY = "routes";
private final RouteLocator delegate;
private final Flux<Route> routes;
private final Map<String, List> cache = new ConcurrentHashMap<>();
private ApplicationEventPublisher applicationEventPublisher;
public CachingRouteLocator(RouteLocator delegate) {
this.delegate = delegate;
/*
* 获取路由信息
* 从cache中获取routes键对应的值,如果不存在,则调用fetch方法来获取
*/
routes = CacheFlux.lookup(cache, CACHE_KEY, Route.class).onCacheMissResume(this::fetch);
}
private Flux<Route> fetch() {
// 获取路由并排序
return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE);
}
现在的问题就是这个委托对象是什么?找到这个问题的答案就要找到创建时传递给构造方法的参数是什么。同样地,在GatewayAutoConfiguration中会注册该类型的bean。
/*
* 该bean的优先级较高,将上面routeDefinitionRouteLocator的返回值先封装为CompositeRouteLocator,
* 然后再封装为CachingRouteLocator,
* CompositeRouteLocator和CachingRouteLocator都是RouteLocator的另外两个特殊的实现类。
*
* RouteDefinitionRouteLocator在调用getRoutes时才对路由信息进行处理,
* 所以CachingRouteLocator会在初始化时就调用每个RouterLocator的getRoutes组装好所有的route对象并进行缓存,
* 以供RoutePredicateHandlerMapping调用。
*/
@Bean
@Primary
@ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
// TODO: property to disable composite?
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
/*
* 默认情况下,这里的routeLocators包含两个元素,除了上面的routeDefinitionRouteLocator方法创建的bean外,
* 还有RouteLocatorBuilder$Builder中的lambda(暂时没找到是哪里出入这个的)。
*/
return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}
这里是将多个RouteLocator对象封装进入CompositeRouteLocator,默认情况下GatewayAutoConfiguration就会注册一个真正的路由定位器RouteDefinitionRouteLocator,当然用户可以通过代码配置来自定义。
实际上Spring的bean工厂中有多个类型的RouteLocator,那为什么被注入RoutePredicateHandlerMapping中的是CachingRouteLocator呢?这是因为在其bean方法上加了@Primary注解,导致这个bean的优先级比其他同类型的高。
获取路由
被调用的获取路由的方法如下,就是直接返回构造方法中被设置的routes属性。
@Override
public Flux<Route> getRoutes() {
return this.routes;
}
CompositeRouteLocator
public class CompositeRouteLocator implements RouteLocator {
private final Flux<RouteLocator> delegates;
public CompositeRouteLocator(Flux<RouteLocator> delegates) {
this.delegates = delegates;
}
@Override
public Flux<Route> getRoutes() {
// 从多个路由定位器中获取路由信息
return this.delegates.flatMapSequential(RouteLocator::getRoutes);
}
@Override
public Flux<Route> getRoutesByMetadata(Map<String, Object> metadata) {
return this.delegates.flatMapSequential(routeLocator -> routeLocator.getRoutesByMetadata(metadata));
}
}
这个类的实现很简单,委托给多个路由定位器对象来获取路由信息。下面主要来分析一下框架默认提供的RouteDefinitionRouteLocator。
RouteDefinitionRouteLocator
bean注册
/*
* RouteLocator是用来获取路由的,RouteDefinitionRouteLocator只是其中一个实现类,
* 封装了RouteDefinitionLocator、GatewayFilterFactory、RoutePredicateFactory,
* 可以看到其组合了RouteDefinitionLocator,实际上是上面的CompositeRouteDefinitionLocator类型的对象。
* RouteDefinitionRouteLocator是指根据RouteDefinition来获取Route对象。
*/
@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,
List<GatewayFilterFactory> gatewayFilters, List<RoutePredicateFactory> predicates,
RouteDefinitionLocator routeDefinitionLocator, ConfigurationService configurationService) {
return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, gatewayFilters, properties,
configurationService);
}
这里设置了多个参数,其中最最重要的是RouteDefinitionLocator。同样的套路,该对象实际上是一个包装对象,封装了其他多个RouteDefinitionLocator。
/*
* 该bean会封装所有的RouteDefinitionLocator,这里的bean的优先级更高。
* 会将上面的propertiesRouteDefinitionLocator和InMemoryRouteDefinitionRepository进行封装。
*/
@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(List<RouteDefinitionLocator> routeDefinitionLocators) {
// 默认情况下,这里的routeDefinitionLocators包含2个元素,分别是上面两个bean定义。
return new CompositeRouteDefinitionLocator(Flux.fromIterable(routeDefinitionLocators));
}
/*
* 要注意区分RouteDefinitionLocator和RouteLocator,前者是存储路由定义信息,后者是查找路由信息
* 两者的关系参考类图:https://img-blog.csdnimg.cn/20210401131318609.png
*/
/*
* 存储从配置文件中读取的路由信息
* 这是个RouteDefinitionLocator
*/
@Bean
@ConditionalOnMissingBean
public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(GatewayProperties properties) {
return new PropertiesRouteDefinitionLocator(properties);
}
// 这个也是个RouteDefinitionLocator
@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
return new InMemoryRouteDefinitionRepository();
}
其中routeDefinitionLocator方法注册的bean就是实际上被注入RouteDefinitionRouteLocator中的路由定义定位器对象,这也是方法上的@Primary注解的作用。
注意区分路由定位器(RouteLocator),和路由定义定位器(RouteDefinitionLocator)。前者的用途是获取路由信息,而后者是存储了路由信息。所以在自定义的时候,对于”找“,可以自定义路由定位器;对于“存”,可以自定义路由定义定位器。
构造方法
/**
* Default filters name.
*/
public static final String DEFAULT_FILTERS = "defaultFilters";
protected final Log logger = LogFactory.getLog(getClass());
private final RouteDefinitionLocator routeDefinitionLocator;
private final ConfigurationService configurationService;
private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap<>();
private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap<>();
private final GatewayProperties gatewayProperties;
public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,
List<RoutePredicateFactory> predicates, List<GatewayFilterFactory> gatewayFilterFactories,
GatewayProperties gatewayProperties, ConfigurationService configurationService) {
this.routeDefinitionLocator = routeDefinitionLocator;
this.configurationService = configurationService;
// 初始化断言信息
initFactories(predicates);
// 初始化filter信息,与初始化断言信息类似
gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
this.gatewayProperties = gatewayProperties;
}
初始化断言工厂
private void initFactories(List<RoutePredicateFactory> predicates) {
predicates.forEach(factory -> {
// 这里key是RoutePredicateFactory实现类的类名称的前缀,如AfterRoutePredicateFactory则key为After
String key = factory.name();
if (this.predicates.containsKey(key)) {
this.logger.warn("A RoutePredicateFactory named " + key + " already exists, class: "
+ this.predicates.get(key) + ". It will be overwritten.");
}
/*
* 如果已经存在了同一个key的predicateFactory,则覆盖掉,
* 因为该方法只有在RouteDefinitionRouterLocator组件被初始化时才会被调用, 所以这里说明以SCG内置的断言工厂为主
*/
this.predicates.put(key, factory);
if (logger.isInfoEnabled()) {
logger.info("Loaded RoutePredicateFactory [" + key + "]");
}
});
}
这里主要是在对重复设置的断言工厂做判断。
获取路由
// 返回路由信息
@Override
public Flux<Route> getRoutes() {
// 通过routeDefinitionLocator获取所有的路由定义信息
return getRoutes(this.routeDefinitionLocator.getRouteDefinitions());
}
private Flux<Route> getRoutes(Flux<RouteDefinition> routeDefinitions) {
// 将路由信息转为路由对象
Flux<Route> routes = routeDefinitions.map(this::convertToRoute);
if (!gatewayProperties.isFailOnRouteDefinitionError()) {
// instead of letting error bubble up, continue
routes = routes.onErrorContinue((error, obj) -> {
if (logger.isWarnEnabled()) {
logger.warn("RouteDefinition id " + ((RouteDefinition) obj).getId()
+ " will be ignored. Definition has invalid configs, " + error.getMessage());
}
});
}
return routes.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition matched: " + route.getId());
}
return route;
});
}
private Route convertToRoute(RouteDefinition routeDefinition) {
// 获取路由定义中的断言信息并组合成一个断言(如果有多个的话)
AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
// 获取路由定义中的filter
List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);
// 构建Route对象
return Route.async(routeDefinition).asyncPredicate(predicate).replaceFilters(gatewayFilters).build();
}
可以看到,这里是通过路由定义定位器来获取的路由信息,对于每条路由信息,会组合它的多条断言,获取该路由能够使用的过滤器,最后构建Route对象,可以说主要的实现在于convertToRoute方法。
组合断言
private AsyncPredicate<ServerWebExchange> combinePredicates(RouteDefinition routeDefinition) {
List<PredicateDefinition> predicates = routeDefinition.getPredicates();
if (predicates == null || predicates.isEmpty()) {
// this is a very rare case, but possible, just match all
return AsyncPredicate.from(exchange -> true);
}
AsyncPredicate<ServerWebExchange> predicate = lookup(routeDefinition, predicates.get(0));
/*
* 如果该路由定义了多个断言,将其用and运算连接起来
*/
for (PredicateDefinition andPredicate : predicates.subList(1, predicates.size())) {
// 转化为异步的断言
AsyncPredicate<ServerWebExchange> found = lookup(routeDefinition, andPredicate);
// 用and连接起来
predicate = predicate.and(found);
}
return predicate;
}
如果一个路由定义有多个断言的话,那么它们之间是“与”的关系,即必须都满足。
获取过滤器
private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {
List<GatewayFilter> filters = new ArrayList<>();
// TODO: support option to apply defaults after route specific filters?
// 如果配置了全局默认过滤器,则添加到集合中
if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {
filters.addAll(loadGatewayFilters(routeDefinition.getId(),
new ArrayList<>(this.gatewayProperties.getDefaultFilters())));
}
// 添加路由定义中配置的过滤器
final List<FilterDefinition> definitionFilters = routeDefinition.getFilters();
if (!CollectionUtils.isEmpty(definitionFilters)) {
filters.addAll(loadGatewayFilters(routeDefinition.getId(), definitionFilters));
}
// 对过滤器进行排序
AnnotationAwareOrderComparator.sort(filters);
return filters;
}
@SuppressWarnings("unchecked")
List<GatewayFilter> loadGatewayFilters(String id, List<FilterDefinition> filterDefinitions) {
ArrayList<GatewayFilter> ordered = new ArrayList<>(filterDefinitions.size());
// 遍历过滤器定义
for (int i = 0; i < filterDefinitions.size(); i++) {
// 获取过滤器定义
FilterDefinition definition = filterDefinitions.get(i);
// 根据过滤器名称获取filter工厂
GatewayFilterFactory factory = this.gatewayFilterFactories.get(definition.getName());
if (factory == null) {
throw new IllegalArgumentException(
"Unable to find GatewayFilterFactory with name " + definition.getName());
}
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition " + id + " applying filter " + definition.getArgs() + " to "
+ definition.getName());
}
// @formatter:off
// 根据filter的定义,利用filter工厂生成配置对象
Object configuration = this.configurationService.with(factory)
.name(definition.getName())
.properties(definition.getArgs())
.eventFunction((bound, properties) -> new FilterArgsEvent(
// TODO: why explicit cast needed or java compile fails
RouteDefinitionRouteLocator.this, id, (Map<String, Object>) properties))
.bind();
// @formatter:on
// some filters require routeId
// TODO: is there a better place to apply this?
if (configuration instanceof HasRouteId) {
HasRouteId hasRouteId = (HasRouteId) configuration;
// 设置路由id
hasRouteId.setRouteId(id);
}
// 生成GatewayFilter
GatewayFilter gatewayFilter = factory.apply(configuration);
if (gatewayFilter instanceof Ordered) {
ordered.add(gatewayFilter);
}
else { // 如果没有实现Ordered接口,则封装为OrderedGatewayFilter
ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));
}
}
return ordered;
}
总之,路由的获取最终还是要依靠路由定义定位器。
CompositeRouteDefinitionLocator
public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {
private static final Log log = LogFactory.getLog(CompositeRouteDefinitionLocator.class);
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.flatMapSequential(RouteDefinitionLocator::getRouteDefinitions)
.flatMap(routeDefinition -> { // 处理返回的多条路由定义
if (routeDefinition.getId() == null) {
// 如果路由id是null,则生成一个
return randomId().map(id -> {
routeDefinition.setId(id);
if (log.isDebugEnabled()) {
log.debug("Id set on route definition: " + routeDefinition);
}
return routeDefinition;
});
}
return Mono.just(routeDefinition);
});
}
protected Mono<String> randomId() {
return Mono.fromSupplier(idGenerator::generateId).map(UUID::toString).publishOn(Schedulers.boundedElastic());
}
}
和CompositeRouteLocator类似,这里主要是从实际的路由定义定位器来获取路由定义。
PropertiesRouteDefinitionLocator
我们可以在配置文件中配置路由定义,比如:
spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://httpbin.org:80
predicates:
- Path=/get
- id: host_route
uri: http://httpbin.org:80
predicates:
- Host=*.myhost.org
最终,这些配置会被封装为GatewayProperties对象,而上面配置中的路由信息会被PropertiesRouteDefinitionLocator返回。
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());
}
}
@ConfigurationProperties(GatewayProperties.PREFIX)
@Validated
public class GatewayProperties {
/**
* Properties prefix.
*/
public static final String PREFIX = "spring.cloud.gateway";
private final Log logger = LogFactory.getLog(getClass());
/**
* List of Routes.
*/
// 路由信息
@NotNull
@Valid
private List<RouteDefinition> routes = new ArrayList<>();
/**
* List of filter definitions that are applied to every route.
*
*/
// 这个列表里的过滤器会作用在每个路由,效果类似GlobalFilter
private List<FilterDefinition> defaultFilters = new ArrayList<>();
private List<MediaType> streamingMediaTypes = Arrays.asList(MediaType.TEXT_EVENT_STREAM,
new MediaType("application", "stream+json"), new MediaType("application", "grpc"),
new MediaType("application", "grpc+protobuf"), new MediaType("application", "grpc+json"));
/**
* Option to fail on route definition errors, defaults to true. Otherwise, a warning
* is logged.
*/
private boolean failOnRouteDefinitionError = true;
public List<RouteDefinition> getRoutes() {
return routes;
}
public void setRoutes(List<RouteDefinition> routes) {
this.routes = routes;
if (routes != null && routes.size() > 0 && logger.isDebugEnabled()) {
logger.debug("Routes supplied from Gateway Properties: " + routes);
}
}
public List<FilterDefinition> getDefaultFilters() {
return defaultFilters;
}
public void setDefaultFilters(List<FilterDefinition> defaultFilters) {
this.defaultFilters = defaultFilters;
}
public List<MediaType> getStreamingMediaTypes() {
return streamingMediaTypes;
}
public void setStreamingMediaTypes(List<MediaType> streamingMediaTypes) {
this.streamingMediaTypes = streamingMediaTypes;
}
public boolean isFailOnRouteDefinitionError() {
return failOnRouteDefinitionError;
}
public void setFailOnRouteDefinitionError(boolean failOnRouteDefinitionError) {
this.failOnRouteDefinitionError = failOnRouteDefinitionError;
}
@Override
public String toString() {
return new ToStringCreator(this).append("routes", routes).append("defaultFilters", defaultFilters)
.append("streamingMediaTypes", streamingMediaTypes)
.append("failOnRouteDefinitionError", failOnRouteDefinitionError).toString();
}
}
InMemoryRouteDefinitionRepository
另外还有一种用得不是很多的路由定义定位器:InMemoryRouteDefinitionRepository。这种定位器可以在运行时对路由信息进行增删。
public class InMemoryRouteDefinitionRepository implements RouteDefinitionRepository {
private final Map<String, RouteDefinition> routes = synchronizedMap(new LinkedHashMap<String, RouteDefinition>());
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(r -> {
if (ObjectUtils.isEmpty(r.getId())) {
return Mono.error(new IllegalArgumentException("id may not be empty"));
}
routes.put(r.getId(), r);
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
if (routes.containsKey(id)) {
routes.remove(id);
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition not found: " + routeId)));
});
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
Map<String, RouteDefinition> routesSafeCopy = new LinkedHashMap<>(routes);
return Flux.fromIterable(routesSafeCopy.values());
}
}
但是这里的修改只是对该类型对象中的routes属性进行修改,而没有修改上面CachingRouteLocator中缓存起来的路由信息。那么需要一种机制让它知道应该要刷新路由信息,实际上CachingRouteLocator确实实现了事件机制来进行路由刷新。
刷新路由
@Override
public void onApplicationEvent(RefreshRoutesEvent event) {
try {
// 如果是scoped的情况
if (this.cache.containsKey(CACHE_KEY) && event.isScoped()) {
// 调用重载的fetch方法来获取满足元数据的路由信息
final Mono<List<Route>> scopedRoutes = fetch(event.getMetadata()).collect(Collectors.toList())
.onErrorResume(s -> Mono.just(List.of()));
scopedRoutes.subscribe(scopedRoutesList -> {
// 这里为什么要合并非scoped路由?这样岂不是将满足与不满足的都获取到了吗?那这里的scoped还有什么意义?
Flux.concat(Flux.fromIterable(scopedRoutesList), getNonScopedRoutes(event)).materialize()
.collect(Collectors.toList()).subscribe(signals -> {
// 发布刷新路由结果事件,表示路由已刷新
applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this));
// 修改cache
cache.put(CACHE_KEY, signals);
}, this::handleRefreshError);
}, this::handleRefreshError);
}
else {
// 重新获取所有路由
final Mono<List<Route>> allRoutes = fetch().collect(Collectors.toList());
allRoutes.subscribe(list -> Flux.fromIterable(list).materialize().collect(Collectors.toList())
.subscribe(signals -> {
applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this));
// 重新设置路由信息
cache.put(CACHE_KEY, signals);
}, this::handleRefreshError), this::handleRefreshError);
}
}
catch (Throwable e) {
handleRefreshError(e);
}
}
private Flux<Route> getNonScopedRoutes(RefreshRoutesEvent scopedEvent) {
return this.getRoutes() // 获取路由
// 筛选元数据不满足的那些路由
.filter(route -> !RouteLocator.matchMetadata(route.getMetadata(), scopedEvent.getMetadata()));
}
这里分为了事件是全局刷新还是局部刷新两种情况,全局刷新很好理解,注解调用无参的fetch方法重新获取路由信息。 局部刷新要根据事件对象中的元数据进行筛选,调用有参的fetch方法。
private Flux<Route> fetch(Map<String, Object> metadata) {
return this.delegate.getRoutesByMetadata(metadata).sort(AnnotationAwareOrderComparator.INSTANCE);
}
@Override
public Flux<Route> getRoutesByMetadata(Map<String, Object> metadata) {
return getRoutes(this.routeDefinitionLocator.getRouteDefinitions()
.filter(routeDef -> RouteLocator.matchMetadata(routeDef.getMetadata(), metadata)));
}
static boolean matchMetadata(Map<String, Object> toCheck, Map<String, Object> expectedMetadata) {
if (CollectionUtils.isEmpty(expectedMetadata)) {
return true;
}
else {
return toCheck != null
&& expectedMetadata.entrySet().stream().allMatch(keyValue -> toCheck.containsKey(keyValue.getKey())
&& toCheck.get(keyValue.getKey()).equals(keyValue.getValue()));
}
}
在RouteDefinitionRouteLocator的方法中,重新从路由定义定位器中获取路由,然后进行过滤筛选。在RouteLocator的匹配方法中,对两个map进行比较,要求key都存在,且value要相等。
再回到CachingRouteLocator中的onApplicationEvent方法,获取到了满足元数据的路由后,调用了getNonScopedRoutes方法来获取不满足元数据的路由,然后将满足与不满足的合并在一起。
目前确实没有搞清楚知道这里的操作意义,按理说只保留满足元数据的路由,否则事件是不是scoped的又有什么意义呢?(等搞清楚后再做补充)
自定义路由定位器
在Spring Cloud Gateway中,是可以自定义路由定位器的,这些路由定位器对象会被封装进入CompositeRouteLocator对象中。
RouteLocatorBuilder
为了方便自定义路由定位器,Spring Cloud Gateway定义和注入了RouteLocatorBuilder类型的bean。
/*
* 用于构建RouteLocator,方便创建路由定位器。
*/
@Bean
public RouteLocatorBuilder routeLocatorBuilder(ConfigurableApplicationContext context) {
return new RouteLocatorBuilder(context);
}
然后可以使用类似下面的代码配置。
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://httpbin.org:80"))
.route("host_route", r -> r.host("*.myhost.org")
.uri("http://httpbin.org:80"))
.build();
}
}
RouteLocatorBuilder的routes方法返回一个Builder(它的内部类)对象,然后通过每个route方法来定义路由,最后的build方法用来构建。route方法的第一个参数是路由id,第二个参数是一个PredicateSpec类型的对象,可以通过它定义的方法来设置断言,比如上面的path、host和uri方法。