User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   SpringMVC   ›   正文
SpringMVC

SpringMVC—URI Builder模式(UriComponents/UriComponentsBuilder)

2020-08-21 01:01:34
1039  0 0
参考目录 隐藏
1) URI Builder
2) UriComponents
3) UriComponentsBuilder
4) API都不难理解,此处给出一些使用案例供以参考:
5) ServletUriComponentsBuilder
6) MvcUriComponentsBuilder

阅读完需:约 14 分钟

URI Builder

Spring MVC作为一个web层框架,避免不了处理URI、URL等和HTTP协议相关的元素,因此它提供了非常好用、功能强大的URI Builder模式来完成,这就是本文重点需要讲述的脚手架~
Spring MVC从3.1开始提供了一种机制,可以通过UriComponentsBuilder和UriComponents面向对象的构造和编码URI。

UriComponents

它表示一个不可变的URI组件集合,将组件类型映射到字符串值。

URI:统一资源标识符。 URL:统一资源定位符。
还是傻傻分不清楚?

URI和URL的区别

它包含用于所有组件的方便getter,与java.net.URI类似,但具有更强大的编码选项和对URI模板变量的支持。

// @since 3.1  自己是个抽象类。一般构建它我们使用UriComponentsBuilder构建器
public abstract class UriComponents implements Serializable {

	// 捕获URI模板变量名
	private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
	
	@Nullable
	private final String scheme;
	@Nullable
	private final String fragment;

	// 唯一构造,是protected 的
	protected UriComponents(@Nullable String scheme, @Nullable String fragment) {
		this.scheme = scheme;
		this.fragment = fragment;
	}

	... // 省略它俩的get方法(无set方法)
	@Nullable
	public abstract String getSchemeSpecificPart();
	@Nullable
	public abstract String getUserInfo();
	@Nullable
	public abstract String getHost();
	// 如果没有设置port,就返回-1
	public abstract int getPort();
	@Nullable
	public abstract String getPath();
	public abstract List<String> getPathSegments();
	@Nullable
	public abstract String getQuery();
	public abstract MultiValueMap<String, String> getQueryParams();

	// 此方法是public且是final的哦~
	// 注意它的返回值还是UriComponents
	public final UriComponents encode() {
		return encode(StandardCharsets.UTF_8);
	}
	public abstract UriComponents encode(Charset charset);

	// 这是它最为强大的功能:对模版变量的支持
	// 用给定Map映射中的值替换**所有**URI模板变量
	public final UriComponents expand(Map<String, ?> uriVariables) {
		return expandInternal(new MapTemplateVariables(uriVariables));
	}
	// 给定的是变量数组,那就按照顺序替换
	public final UriComponents expand(Object... uriVariableValues) {...}
	public final UriComponents expand(UriTemplateVariables uriVariables) { ... }

	// 真正的expand方法,其实还是子类来实现的
	abstract UriComponents expandInternal(UriTemplateVariables uriVariables);
	// 规范化路径移除**序列**,如“path/…”。
	// 请注意,规范化应用于完整路径,而不是单个路径段。
	public abstract UriComponents normalize();
	// 连接所有URI组件以返回完全格式的URI字符串。
	public abstract String toUriString();
	public abstract URI toUri();

	@Override
	public final String toString() {
		return toUriString();
	}

	// 拷贝
	protected abstract void copyToUriComponentsBuilder(UriComponentsBuilder builder);
	... // 提供静态工具方法expandUriComponent和sanitizeSource
}

它包含有和Http相关的各个部分:如schema、port、path、query等等。此抽象类有两个实现类:OpaqueUriComponents和HierarchicalUriComponents

Hierarchical:分层的 Opaque:不透明的

由于在实际使用中会使用构建器来创建实例,所以都是面向抽象类编程,并不需要关心具体实现,因此实现类部分此处省略~

UriComponentsBuilder

从命名中就可以看出,它使用了Builder模式,用于构建UriComponents。实际应用中我们所有的UriComponents都应是通过此构建器构建出来的~

// @since 3.1
public class UriComponentsBuilder implements UriBuilder, Cloneable {
	... // 省略所有正则(包括提取查询参数、scheme、port等等等等)
	... // 它所有的构造函数都是protected的
	
	// ******************鞋面介绍它的实例化静态方法(7种)******************

	// 创建一个空的bulder,里面schema,port等等啥都木有
	public static UriComponentsBuilder newInstance() {
		return new UriComponentsBuilder();
	}
	// 直接从path路径里面,分析出一个builder。较为常用
	public static UriComponentsBuilder fromPath(String path) {...}
	public static UriComponentsBuilder fromUri(URI uri) {...}
	// 比如这种:/hotels/42?filter={value}
	public static UriComponentsBuilder fromUriString(String uri) {}
	// 形如这种:https://example.com/hotels/42?filter={value}
	// fromUri和fromHttpUrl的使用方式差不多~~~~
	public static UriComponentsBuilder fromHttpUrl(String httpUrl) {}
	
	// HttpRequest是HttpMessage的子接口。它的原理是:fromUri(request.getURI())(调用上面方法fromUri)
	// 然后再调用本类的adaptFromForwardedHeaders(request.getHeaders())
	// 解释:从头Forwarded、X-Forwarded-Proto等拿到https、port等设置值~~
	// 详情请参见http标准的Forwarded头~
	// @since 4.1.5
	public static UriComponentsBuilder fromHttpRequest(HttpRequest request) {}
	// origin 里面放的是跨域访问的域名地址。比如 www.a.com 访问 www.b.com会形成跨域
	// 这个时候访问 www.b.com 的时候,请求头里会携带 origin:www.a.com(b服务需要通过这个来判断是否允许a服务跨域访问)
	// 方法可以获取到协议,域名和端口。个人觉得此方法没毛卵用~~~
	// 和fromUriString()方法差不多,不过比它精简(因为这里只需要关注scheme、host和port)
	public static UriComponentsBuilder fromOriginHeader(String origin) {}

	// *******************下面都是实例方法*******************
	// @since 5.0.8
	public final UriComponentsBuilder encode() {
		return encode(StandardCharsets.UTF_8);
	}
	public UriComponentsBuilder encode(Charset charset) {}

	// 调用此方法生成一个UriComponents
	public UriComponents build() {
		return build(false);
	}
	public UriComponents build(boolean encoded) {
		// encoded=true,取值就是FULLY_ENCODED 全部编码
		// 否则只编码模版或者不编码
		return buildInternal(encoded ? EncodingHint.FULLY_ENCODED :
				(this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE)
				);
	}
	// buildInternal内部就会自己new子类:OpaqueUriComponents或者HierarchicalUriComponents
	// 以及执行UriComponents.expand方法了(若指定了参数的话),使用者不用关心了
	
	// 显然这就是个多功能方法了:设置好参数。build后立马Expand
	public UriComponents buildAndExpand(Map<String, ?> uriVariables) {
		return build().expand(uriVariables);
	}
	public UriComponents buildAndExpand(Object... uriVariableValues) {}

	//build成为一个URI。注意这里编码方式是:EncodingHint.ENCODE_TEMPLATE
	@Override
	public URI build(Object... uriVariables) {
		return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri();
	}
	@Override
	public URI build(Map<String, ?> uriVariables) {
		return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri();
	}

	// @since 4.1
	public String toUriString() { ... }

	// ====重构/重新设置Builder====
	public UriComponentsBuilder uri(URI uri) {}
	public UriComponentsBuilder uriComponents(UriComponents uriComponents) {}
	@Override
	public UriComponentsBuilder scheme(@Nullable String scheme) {
		this.scheme = scheme;
		return this;
	}
	@Override
	public UriComponentsBuilder userInfo(@Nullable String userInfo) {
		this.userInfo = userInfo;
		resetSchemeSpecificPart();
		return this;
	}
	public UriComponentsBuilder host(@Nullable String host){ ... }
	... // 省略其它部分

	// 给URL后面拼接查询参数(键值对)
	@Override
	public UriComponentsBuilder query(@Nullable String query) {}
	// 遇上相同的key就替代,而不是直接在后面添加了(上面query是添加)
	@Override
	public UriComponentsBuilder replaceQuery(@Nullable String query) {}
	@Override
	public UriComponentsBuilder queryParam(String name, Object... values) {}
	... replaceQueryParam

	// 可以先单独设置参数,但不expend哦~
	public UriComponentsBuilder uriVariables(Map<String, Object> uriVariables) {}

	@Override
	public Object clone() {
		return cloneBuilder();
	}
	// @since 4.2.7
	public UriComponentsBuilder cloneBuilder() {
		return new UriComponentsBuilder(this);
	}
	...
}

API都不难理解,此处给出一些使用案例供以参考:

public static void main(String[] args) {
    String url;
    UriComponents uriComponents = UriComponentsBuilder.newInstance()
            //.encode(StandardCharsets.UTF_8)
            .scheme("https").host("www.baidu.com").path("/test").path("/{template}") //此处{}就成 不要写成${}
            //.uriVariables(传一个Map).build();
            .build().expand("myhome"); // 此效果同上一句,但推荐这么使用,方便一些
    url = uriComponents.toUriString();
    System.out.println(url); // https://www.baidu.com/test/myhome

    // 从URL字符串中构造(注意:toUriString方法内部是调用了build和expend方法的~)
    System.out.println(UriComponentsBuilder.fromHttpUrl(url).toUriString()); // https://www.baidu.com/test/myhome
    System.out.println(UriComponentsBuilder.fromUriString(url).toUriString()); // https://www.baidu.com/test/myhome

    // 给URL中放添加参数 query和replaceQuery
    uriComponents = UriComponentsBuilder.fromHttpUrl(url).query("name=中国&age=18").query("&name=二次拼接").build();
    url = uriComponents.toUriString();
    // 效果描述:&test前面这个&不写也是木有问题的。并且两个name都出现了哦~~~
    System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=中国&name=二次拼接&age=18

    uriComponents = UriComponentsBuilder.fromHttpUrl(url).query("name=中国&age=18").replaceQuery("name=二次拼接").build();
    url = uriComponents.toUriString();
    // 这种够狠:后面的直接覆盖前面“所有的”查询串
    System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=二次拼接

    //queryParam/queryParams/replaceQueryParam/replaceQueryParams
    // queryParam:一次性指定一个key,queryParams一次性可以搞多个key
    url = "https://www.baidu.com/test/myhome"; // 重置一下
    uriComponents = UriComponentsBuilder.fromHttpUrl(url).queryParam("name","中国","美国").queryParam("age",18)
            .queryParam("name","英国").build();
    url = uriComponents.toUriString();
    // 发现是不会有repalace的效果的~~~~~~~~~~~~~
    System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=中国&name=美国&name=英国&age=18
    
    // 关于repalceParam相关方法,交给各位自己去试验吧~~~

    // 不需要domain,构建局部路径,它也是把好手
    uriComponents = UriComponentsBuilder.fromPath("").path("/test").build();
    // .fromPath("/").path("/test") --> /test
    // .fromPath("").path("/test") --> /test
    // .fromPath("").path("//test") --> /test
    // .fromPath("").path("test") --> /test
    System.out.println(uriComponents.toUriString()); // /test?name=fsx
}

使用这种方式来构建URL还是非常方便的,它的容错性非常高,写法灵活且不容易出错,完全面向模块化思考,值得推荐。

  1. URI构建的任意部分(包括查询参数、scheme等等)都是可以用{}这种形式的模版参数的
  2. 被替换的模版中还支持这么来写:/myurl/{name:[a-z]}/show,这样用expand也能正常赋值

它还有个子类:ServletUriComponentsBuilder,是对Servlet容器的适配,也非常值得一提

ServletUriComponentsBuilder

它主要是扩展了一些静态工厂方法,用于创建一些相对路径(相当于当前请求HttpServletRequest)。

// @since 3.1
public class ServletUriComponentsBuilder extends UriComponentsBuilder {
	@Nullable
	private String originalPath;
	
	// 不对外提供public的构造函数
	// initFromRequest:设置schema、host、port(HTTP默认80,https默认443)
	public static ServletUriComponentsBuilder fromContextPath(HttpServletRequest request) {
		ServletUriComponentsBuilder builder = initFromRequest(request);
		// 注意:此处路径全部替换成了ContextPath
		builder.replacePath(request.getContextPath());
		return builder;
	}

	// If the servlet is mapped by name, e.g. {@code "/main/*"}, the path
	// 它在UriComponentsBuilderMethodArgumentResolver中有用
	public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) {}

	public static ServletUriComponentsBuilder fromRequestUri(HttpServletRequest request) {
		ServletUriComponentsBuilder builder = initFromRequest(request);
		builder.initPath(request.getRequestURI());
		return builder;
	}
	private void initPath(String path) {
		this.originalPath = path;
		replacePath(path);
	}
	public static ServletUriComponentsBuilder fromRequest(HttpServletRequest request) {}

	// fromCurrentXXX方法... 
	public static ServletUriComponentsBuilder fromCurrentContextPath() {}
	// 生路其它Current方法
	
	// @since 4.0 移除掉originalPath的后缀名,并且把此后缀名return出来~~
	// 此方法必须在UriComponentsBuilder.path/pathSegment方法之前调用~
	@Nullable
	public String removePathExtension() { }
}

说明:Spring5.1后不推荐使用它来处理X-Forwarded-*等请求头了,推荐使用ForwardedHeaderFilter来处理~

使用UriComponentsBuilder类的最大好处是方便地注入到Controller中,在方法参数中可直接使用。详见UriComponentsBuilderMethodArgumentResolver,它最终return的是:ServletUriComponentsBuilder.fromServletMapping(request),这样我们在Controller内就可以非常容易且优雅的得到URI的各个部分了(不用再自己通过request慢慢get)~


MvcUriComponentsBuilder

此类效果类似于ServletUriComponentsBuilder,它负责从Controller控制器标注有@RequestMapping的方法中获取UriComponentsBuilder,从而构建出UriComponents。

// @since 4.0
public class MvcUriComponentsBuilder {

	// Bean工厂里·UriComponentsContributor·的通用名称
	// 关于UriComponentsContributor,RequestParamMethodArgumentResolver和PathVariableMethodArgumentResolver都是它的子类
	public static final String MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME = "mvcUriComponentsContributor";
	// 用于创建动态代理对象
	private static final SpringObjenesis objenesis = new SpringObjenesis();
	// 支持Ant风格的Path
	private static final PathMatcher pathMatcher = new AntPathMatcher();
	// 参数名
	private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

	// 课件解析查询参数、path参数最终是依赖于我们的MethodArgumentResolver
	// 他们也都实现了UriComponentsContributor接口~~~
	private static final CompositeUriComponentsContributor defaultUriComponentsContributor;
	static {
		defaultUriComponentsContributor = new CompositeUriComponentsContributor(new PathVariableMethodArgumentResolver(), new RequestParamMethodArgumentResolver(false));
	}

	// final的,只能通过构造器传入
	private final UriComponentsBuilder baseUrl;
	
	// 此构造方法是protected的
	protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) {
		this.baseUrl = baseUrl;
	}

	// 通过BaseUrl创建一个实例
	public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) {
		return new MvcUriComponentsBuilder(baseUrl);
	}

	// 从控制器里。。。
	// 这个一个控制器类里有多个Mapping,那么只会有第一个会被生效
	public static UriComponentsBuilder fromController(Class<?> controllerType) {
		return fromController(null, controllerType);
	}

	// 注意此方法也是public的哦~~~~  builder可以为null哦~~
	public static UriComponentsBuilder fromController(@Nullable UriComponentsBuilder builder, Class<?> controllerType) {

		// 若builder为null,那就用它ServletUriComponentsBuilder.fromCurrentServletMapping(),否则克隆一个出来
		builder = getBaseUrlToUse(builder);

		// 拿到此控制器的pathPrefixes。
		// 关于RequestMappingHandlerMapping的pathPrefixes,出门右拐有详细说明来如何使用
		String prefix = getPathPrefix(controllerType);
		builder.path(prefix);

		// 找到类上的RequestMapping注解,若没标注,默认就是'/'
		// 若有此注解,拿出它的mapping.path(),若是empty或者paths[0]是empty,都返回'/'
		// 否则返回第一个:paths[0]
		String mapping = getClassMapping(controllerType);
		builder.path(mapping);

		return builder;
	}

	// 这个方法应该是使用得最多的~~~~ 同样的借用了MethodIntrospector.selectMethods这个方法
	// 它的path是结合来的:String path = pathMatcher.combine(typePath, methodPath);
	// fromMethodInternal方法省略,但最后一步调用了applyContributors(builder, method, args)这个方法
	// 它是使用`CompositeUriComponentsContributor`来处理赋值URL的template(可以自己配置,也可以使用默认的)
	// 默认使用的便是PathVariableMethodArgumentResolver和RequestParamMethodArgumentResolver

	// 当在处理请求的上下文之外使用MvcUriComponentsBuilder或应用与当前请求不匹配的自定义baseurl时,这非常有用。
	public static UriComponentsBuilder fromMethodName(Class<?> controllerType, String methodName, Object... args) {

		Method method = getMethod(controllerType, methodName, args);
		// 第一个参数是baseUrl,传的null 没传就是ServletUriComponentsBuilder.fromCurrentServletMapping()
		return fromMethodInternal(null, controllerType, method, args);
	}
	// @since 4.2
	public static UriComponentsBuilder fromMethod(Class<?> controllerType, Method method, Object... args) {}
	// @since 4.2
	public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl, @Nullable Class<?> controllerType, Method method, Object... args) {}

	// info必须是MethodInvocationInfo类型
	// Create a {@link UriComponentsBuilder} by invoking a "mock" controller method.  用于mock
	// 请参见on方法~~
	public static UriComponentsBuilder fromMethodCall(Object info) {}
	public static <T> T on(Class<T> controllerType) {
		return controller(controllerType);
	}
	// 此方法是核心:ControllerMethodInvocationInterceptor是个私有静态内部类
	// 实现了org.springframework.cglib.proxy.MethodInterceptor接口以及
	// org.aopalliance.intercept.MethodInterceptor接口
	// org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.MethodInvocationInfo接口
	// ReflectionUtils.isObjectMethod(method)
	public static <T> T controller(Class<T> controllerType) {
		Assert.notNull(controllerType, "'controllerType' must not be null");
		return ControllerMethodInvocationInterceptor.initProxy(controllerType, null);
	}

	// @since 4.1
	// 请看上面对@RequestMapping注解中name属性的介绍和使用
	// ${s:mvcUrl('PC#getPerson').arg(0,"123").build()
	// 这个标签s:mvcUrl它对应的解析函数其实就是MvcUriComponentsBuilder.fromMappingName
	// 也就是这个方法`PC#getPerson`就二十所谓的mappingName,若不指定它由HandlerMethodMappingNamingStrategy生成
	// 底层依赖方法:RequestMappingInfoHandlerMapping.getHandlerMethodsForMappingName
	public static MethodArgumentBuilder fromMappingName(String mappingName) {
		return fromMappingName(null, mappingName);
	}

	// **************以上都是静态工厂方法,下面是些实例方法**************
	// 调用的是静态方法fromController,See class-level docs
	public UriComponentsBuilder withController(Class<?> controllerType) {
		return fromController(this.baseUrl, controllerType);
	}
	// withMethodName/withMethodCall/withMappingName/withMethod等都是依赖于对应的静态工厂方法,略
}

MvcUriComponentsBuilder提供的功能被广泛应用到Mock接口中,并且它提供的MvcUriComponentsBuilder#fromMappingName的API是集成模版引擎的关键,我个人认为所想深入了解Spring MVC或者在此基础上扩展,了解它的URI Builder模式的必要性还是较强的。

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

0 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 594473浏览

随机文章
MyBatis笔记2—HelloWorld
5年前
Java—Javassist(动态代理)(动态字节)
4年前
SpringBoot—JPA整合
5年前
SpringMVC笔记—数据显示到前端(补充)
5年前
SpringCloud—Ribbon(负载均衡器)
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 594473 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付