当前位置: 代码迷 >> 综合 >> soul-网关实战(二)如何成为了Soul的贡献者
  详细解决方案

soul-网关实战(二)如何成为了Soul的贡献者

热度:75   发布时间:2023-11-13 11:01:45.0

soul-网关实战(二)如何成为了Soul的贡献者

  • 提交第一个PR
    • 发现issue
    • 提交issue
    • 解决方案
      • 定位问题
      • 解决方案
    • 提交PR
  • 踩坑(注意事项)
  • 结语

提交第一个PR

最近在做一个对外开放的openDataApi数据服务项目,说到对外开放,自然少不了要做一系列比如说:鉴权、防火墙、熔断/限流、负载均衡等等安全保障策略。那如何实现这些策略呢?哈哈哈,不用想看标题,当然是用soul啦~ 接下来,我就在实战项目中使用soul,从发现soul网关的bug,到报告bug,最后提交改进补丁等一些列操作进行一次经验分享,以及中间遇到的一些坑,为大家顺利成为soul贡献者提供一点参考,废话不多说,让我们开始吧~

发现issue

想要成为贡献者,那就得找到项目中的bug或者优化的建议,这点没啥可说的。那么对于像我这样刚接触soul的新手可以通过哪些途径获取这些issue呢?下面是我总结出来的一些途径:

  1. 查看官网的issue列表
    issue列表
    issue详情

通过查看官方的issue列表,这里开发者会不定期更新提供一些 issue 让大家参与到soul项目的开发中来,并且这些issue都有很详细的说明,告诉你要做什么,大概怎么做等。如果你看到哪个issue,碰巧又是自己稍微费点力就能完成,那还等什么?赶快回复吧!

  1. 阅读源码,嗅出代码中的坏味道

毋庸置疑,这种方式就需要你具有一定的技术能力以及编码素养了~

  1. 测试应用,暴露异常

如果你没有从issue列表中挑到适合自己的,也没有时间通过阅读源码,嗅出一些代码坏味道。那么就通过测试应用,让异常直接暴露在你面前, 让issue自己来找你吧!下面就是我在压力测试soul rate_limiter 限流插件过程中发现的问题,跟你分享一下:

  • 首先启动 soul-admin、soul-bootstrap、soul-examples-http 三个项目,在 soul-admin–> System Manage --> Plugin --> rate_limiter -->打开插件在 soul-admin–> PluginList --> rate_limiter --> 添加选择器,添加规则
    add Selector
    add rule
  • 执行压力测试
	sb -u 'http://172.16.52.152:9195/http/order/findById?id=4' -c 10 -N 20
  • 查看控制台
2021-01-28 15:43:10.475  INFO 16096 --- [-work-threads-7] o.d.soul.plugin.base.AbstractSoulPlugin  : rate_limiter rule success match , rule name :http_rateLimiter_rule
2021-01-28 15:43:10.475 ERROR 16096 --- [oul-netty-nio-4] reactor.core.publisher.Operators         : Operator called default onErrorDroppedjava.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread soul-netty-nio-4at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:77) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Mono.block(Mono.java:1663) ~[reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at org.springframework.data.redis.connection.ReactiveRedisConnection.close(ReactiveRedisConnection.java:60) ~[spring-data-redis-2.2.3.RELEASE.jar:2.2.3.RELEASE]at org.springframework.data.redis.core.script.DefaultReactiveScriptExecutor.lambda$execute$7(DefaultReactiveScriptExecutor.java:167) ~[spring-data-redis-2.2.3.RELEASE.jar:2.2.3.RELEASE]at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.runFinally(FluxDoFinally.java:156) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.cancel(FluxDoFinally.java:145) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drainLoop(Operators.java:2051) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drain(Operators.java:2020) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.cancel(Operators.java:1832) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoReduceSeed$ReduceSeedSubscriber.cancel(MonoReduceSeed.java:96) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.cancel(FluxMapFuseable.java:351) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.cancel(MonoPeekTerminal.java:137) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators.terminate(Operators.java:1108) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoFlatMap$FlatMapMain.cancel(MonoFlatMap.java:180) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators.terminate(Operators.java:1108) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoSubscribeOn$SubscribeOnSubscriber.cancel(MonoSubscribeOn.java:208) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.cancel(MonoPeekTerminal.java:137) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.cancel(FluxOnAssembly.java:500) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators.terminate(Operators.java:1108) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoFlatMap$FlatMapInner.cancel(MonoFlatMap.java:264) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoFlatMap$FlatMapMain.cancel(MonoFlatMap.java:181) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.cancel(FluxOnAssembly.java:500) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.cancel(FluxOnAssembly.java:500) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.cancel(FluxOnAssembly.java:500) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.cancel(MonoPeekTerminal.java:137) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.cancel(MonoPeekTerminal.java:137) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.cancel(FluxOnAssembly.java:500) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drainLoop(Operators.java:2051) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drain(Operators.java:2020) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.cancel(Operators.java:1832) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drainLoop(Operators.java:2051) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drain(Operators.java:2020) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.cancel(Operators.java:1832) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drainLoop(Operators.java:2051) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drain(Operators.java:2020) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.cancel(Operators.java:1832) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.cancel(MonoPeekTerminal.java:137) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drainLoop(Operators.java:2051) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.drain(Operators.java:2020) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.cancel(Operators.java:1832) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators.terminate(Operators.java:1108) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreInner.cancel(MonoIgnoreThen.java:244) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.cancel(MonoIgnoreThen.java:184) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.cancel(MonoPeekTerminal.java:137) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.cancel(MonoPeekTerminal.java:137) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.core.publisher.Operators.terminate(Operators.java:1108) [reactor-core-3.3.1.RELEASE.jar:3.3.1.RELEASE]at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:408) [reactor-netty-0.9.2.RELEASE.jar:0.9.2.RELEASE]at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.java:163) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:510) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:518) ~[netty-transport-4.1.43.Final.jar:4.1.43.Final]at io.netty.util.concurrent.SingleThreadEventExecutor$6.run(SingleThreadEventExecutor.java:1050) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.43.Final.jar:4.1.43.Final]at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_251]2021-01-28 15:43:10.475  INFO 16096 --- [-work-threads-6] o.d.soul.plugin.base.AbstractSoulPlugin  : rate_limiter selector success match , selector name :http_rateLimiter
2021-01-28 15:43:10.475  INFO 16096 --- [-work-threads-6] o.d.soul.plugin.base.AbstractSoulPlugin  : rate_limiter rule success match , rule name :http_rateLimiter_rule

提交issue

在压力测试过程中,发现控制台有ERROR异常日志打印,检查发现不是配置问题,那就说明rate_limiter插件应该存在问题的,然后就提issue。下面我大概讲一下注意的事项:

  1. 查看一下你遇到的问题是否已经在issue中
  2. 如果没有,New issue,选择issue 的标签如:bug,enhancement,discussion,然后根据自带的模板填写相应内容就好了。这里注意一下,大家好像用的都是英文交流,所以得用简单点的英文描述问题,越详细越好,让人能简单复现你的问题
  3. 如果这个issue自己搞不定,那就保持关注,在讨论中进一步提供必要信息。
    New issue

解决方案

定位问题

首先,看到 java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread soul-netty-nio-4 这个error信息,可以知道是soul线程报出的异常,有 block()方法阻塞线程。因为rate_limiter插件的流控策略是基于redis算法桶实现的,而soul的线程模型不允许阻塞的方法,这里我们就初步猜想,可能是redis存在阻塞性方法(我咋能知道呢,这个阿磊给我说的,哈哈哈)。查看org.dromara.soul.plugin.ratelimiter.handler.RateLimiterPluginDataHandler 的 handlerPlugin方法,可以看到这里使用的是ReactiveRedisTemplate<String, String> reactiveRedisTemplate = new ReactiveRedisTemplate<>(lettuceConnectionFactory, serializationContext);

  @Overridepublic void handlerPlugin(final PluginData pluginData) {
    if (Objects.nonNull(pluginData) && pluginData.getEnabled()) {
    //init redisRateLimiterConfig rateLimiterConfig = GsonUtils.getInstance().fromJson(pluginData.getConfig(), RateLimiterConfig.class);//spring data redisTemplateif (Objects.isNull(Singleton.INST.get(ReactiveRedisTemplate.class))|| Objects.isNull(Singleton.INST.get(RateLimiterConfig.class))|| !rateLimiterConfig.equals(Singleton.INST.get(RateLimiterConfig.class))) {
    LettuceConnectionFactory lettuceConnectionFactory = createLettuceConnectionFactory(rateLimiterConfig);lettuceConnectionFactory.afterPropertiesSet();RedisSerializer<String> serializer = new StringRedisSerializer();RedisSerializationContext<String, String> serializationContext =RedisSerializationContext.<String, String>newSerializationContext().key(serializer).value(serializer).hashKey(serializer).hashValue(serializer).build();ReactiveRedisTemplate<String, String> reactiveRedisTemplate = new ReactiveRedisTemplate<>(lettuceConnectionFactory, serializationContext);Singleton.INST.single(ReactiveRedisTemplate.class, reactiveRedisTemplate);Singleton.INST.single(RateLimiterConfig.class, rateLimiterConfig);}}}

然后跟一下 ReactiveRedisTemplate类,找到redis关闭连接的地方org.springframework.data.redis.core.script.DefaultReactiveScriptExecutor

/*** Executes the given action object within a connection that is allocated eagerly and released after {@link Flux}* termination.** @param <T> return type* @param action callback object to execute* @return object returned by the action*/private <T> Flux<T> execute(ReactiveRedisCallback<T> action) {
    Assert.notNull(action, "Callback object must not be null");ReactiveRedisConnectionFactory factory = getConnectionFactory();ReactiveRedisConnection conn = factory.getReactiveConnection();try {
    return Flux.defer(() -> action.doInRedis(conn)).doFinally(signal -> conn.close());} catch (RuntimeException e) {
    conn.close();throw e;}}

点开conn.close()方法可以看到

	/** (non-Javadoc)* @see java.io.Closeable#close()*/@Overridedefault void close() {
    closeLater().block();}

果然,关闭连接的方法是阻塞的。那么问题这里就定位清楚了,是redis关闭连接的方法出了问题,这就很好解决了。接下来我们需要重新定义 ReactiveRedisTemplate 类,修改关闭连接就OK了。

解决方案

  1. 新建 SoulReactiveRedisTemplate.java
package org.dromara.soul.plugin.ratelimiter.handler;import java.util.List;import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.script.ReactiveScriptExecutor;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisElementReader;
import org.springframework.data.redis.serializer.RedisElementWriter;
import org.springframework.data.redis.serializer.RedisSerializationContext;import reactor.core.publisher.Flux;/*** The type reactive redisTemplate.** @author zhanglei*/
public class SoulReactiveRedisTemplate<K, V> extends ReactiveRedisTemplate<K, V> {
    private final ReactiveScriptExecutor<K> reactiveScriptExecutor;public SoulReactiveRedisTemplate(final ReactiveRedisConnectionFactory connectionFactory,final RedisSerializationContext<K, V> serializationContext) {
    super(connectionFactory, serializationContext);this.reactiveScriptExecutor = new SoulReactiveScriptExecutor<>(connectionFactory, serializationContext);}@Overridepublic <T> Flux<T> execute(final RedisScript<T> script, final List<K> keys, final List<?> args) {
    return reactiveScriptExecutor.execute(script, keys, args);}@Overridepublic <T> Flux<T> execute(final RedisScript<T> script, final List<K> keys, final List<?> args, final RedisElementWriter<?> argsWriter,final RedisElementReader<T> resultReader) {
    return reactiveScriptExecutor.execute(script, keys, args, argsWriter, resultReader);}
}
  1. 新建 SoulReactiveScriptExecutor.java
package org.dromara.soul.plugin.ratelimiter.handler;import java.nio.ByteBuffer;
import java.util.List;import org.springframework.data.redis.connection.ReactiveRedisConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.ReactiveRedisCallback;
import org.springframework.data.redis.core.script.DefaultReactiveScriptExecutor;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisElementReader;
import org.springframework.data.redis.serializer.RedisElementWriter;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.util.Assert;import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;/*** The type reactive script executor.** @author zhanglei*/
public class SoulReactiveScriptExecutor<K> extends DefaultReactiveScriptExecutor<K> {
    public SoulReactiveScriptExecutor(final ReactiveRedisConnectionFactory connectionFactory,final RedisSerializationContext<K, ?> serializationContext) {
    super(connectionFactory, serializationContext);}@Overridepublic <T> Flux<T> execute(final RedisScript<T> script, final List<K> keys, final List<?> args, final RedisElementWriter<?> argsWriter,final RedisElementReader<T> resultReader) {
    Assert.notNull(script, "RedisScript must not be null!");Assert.notNull(argsWriter, "Argument Writer must not be null!");Assert.notNull(resultReader, "Result Reader must not be null!");Assert.notNull(keys, "Keys must not be null!");Assert.notNull(args, "Args must not be null!");return execute(connection -> {
    ReturnType returnType = ReturnType.fromJavaType(script.getResultType());ByteBuffer[] keysAndArgs = keysAndArgs(argsWriter, keys, args);int keySize = keys.size();return super.eval(connection, script, returnType, keySize, keysAndArgs, resultReader);});}private <T> Flux<T> execute(final ReactiveRedisCallback<T> action) {
    Assert.notNull(action, "Callback object must not be null");ReactiveRedisConnectionFactory factory = getConnectionFactory();return Flux.usingWhen(Mono.fromSupplier(factory::getReactiveConnection), action::doInRedis,ReactiveRedisConnection::closeLater);}
}
  1. 替换org.dromara.soul.plugin.ratelimiter.handler.RateLimiterPluginDataHandler 的 handlerPlugin方法,中使用的ReactiveRedisTemplate<String, String> reactiveRedisTemplate = new ReactiveRedisTemplate<>(lettuceConnectionFactory, serializationContext);
 @Overridepublic void handlerPlugin(final PluginData pluginData) {
    if (Objects.nonNull(pluginData) && pluginData.getEnabled()) {
    //init redisRateLimiterConfig rateLimiterConfig = GsonUtils.getInstance().fromJson(pluginData.getConfig(), RateLimiterConfig.class);//spring data redisTemplateif (Objects.isNull(Singleton.INST.get(ReactiveRedisTemplate.class))|| Objects.isNull(Singleton.INST.get(RateLimiterConfig.class))|| !rateLimiterConfig.equals(Singleton.INST.get(RateLimiterConfig.class))) {
    LettuceConnectionFactory lettuceConnectionFactory = createLettuceConnectionFactory(rateLimiterConfig);lettuceConnectionFactory.afterPropertiesSet();RedisSerializer<String> serializer = new StringRedisSerializer();RedisSerializationContext<String, String> serializationContext =RedisSerializationContext.<String, String>newSerializationContext().key(serializer).value(serializer).hashKey(serializer).hashValue(serializer).build();ReactiveRedisTemplate<String, String> reactiveRedisTemplate = new SoulReactiveRedisTemplate<>(lettuceConnectionFactory, serializationContext);Singleton.INST.single(ReactiveRedisTemplate.class, reactiveRedisTemplate);Singleton.INST.single(RateLimiterConfig.class, rateLimiterConfig);}}}

至此问题解决~

提交PR

当你提的issue,通过阅读相应的源码,然后找到问题所在,并稍微使一把力就可以解决,那么还等什么,赶紧提PR啊!这里特别感谢一下soul committer 阿磊,主要还是他解决了问题,给我修改方案,然后我就有了一次PR的机会,嘿嘿嘿~

具体的贡献者指南,详情请参考官网,贡献者指南: https://dromara.org/zh/projects/soul/contributor/
具体的编码规范,详情请参考官网,编码规范: https://dromara.org/zh/projects/soul/code-conduct/**
提交图

踩坑(注意事项)

下面主要讲一下,这个过程中踩的一些坑:

  1. 代码提交行为规范
    • 确保执行mvn clean install -Dmaven.javadoc.skip=true可以编译和测试通过。
      因为你提交以后,codecov-io 会进行代码检测,提交的代码必须通过ci 。我就在这卡了许久~
    • 确保使用Checkstyle检查代码,违反验证规则的需要有特殊理由。模板位置在https://github.com/dromara/soul/blob/master/script/soul_checkstyle.xml,请使用checkstyle 8.8运行规则。
  2. 不要提交与本次解决issue无关的代码
    committer会根据你要解决的issue,检查你的提交范围 ,有无效的提交,会让你恢复。我就把自己本地测试的一些配置,代码提交了,汗~
  3. 新增的java文件,需要加上License,可以随便找个类,copy一个,不加编译不过的
    License
  4. 特别注意一下,行尾不要留空行,不然编译也不过
    行尾空格

结语

以上便是本次我在soul贡献第一个pr的大概历程,以及踩的一些坑,就想着稍微总结一下,说不定还会对你有用呢,嘿嘿嘿~ 后面会持续进行一些源码解析以及使用教程的心得更新,感谢关注!