共计 17403 个字符,预计需要花费 44 分钟才能阅读完成。
本篇文章波及底层设计以及原理,以及问题定位,比拟深刻,篇幅较长,所以拆分成高低两篇:
- 上:问题简略形容以及 Spring Cloud RefreshScope 的原理
- 下:以后 spring-cloud-openfeign + spring-cloud-sleuth 带来的 bug 以及如何修复
最近在我的项目中想实现 OpenFeign 的配置能够动静刷新(次要是 Feign 的 Options 配置),例如:
feign:
client:
config:
default:
# 链接超时
connectTimeout: 500
# 读取超时
readTimeout: 8000
咱们可能会察看到调用某个 FeignClient 的超时工夫不合理,须要长期批改下,咱们不想因为这种事件重启过程或者刷新整个 ApplicationContext,所以将这部分配置放入 spring-cloud-config 中并应用动静刷新的机制进行刷新。官网提供了这个配置办法,参考:官网文档 – Spring @RefreshScope Support
即在我的项目中减少配置:
feign.client.refresh-enabled: true
然而在咱们的我的项目中,减少了这个配置后,启动失败,报找不到相干 Bean 的谬误:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'feign.Request.Options-testService1Client' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:863)
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1344)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:309)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1160)
at org.springframework.cloud.openfeign.FeignContext.getInstance(FeignContext.java:57)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getOptionsByName(FeignClientFactoryBean.java:363)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.configureUsingConfiguration(FeignClientFactoryBean.java:195)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.configureFeign(FeignClientFactoryBean.java:158)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.feign(FeignClientFactoryBean.java:132)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget(FeignClientFactoryBean.java:382)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:371)
at org.springframework.cloud.openfeign.FeignClientsRegistrar.lambda$registerFeignClient$0(FeignClientsRegistrar.java:235)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1231)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1173)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:564)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524)
... 74 more
问题剖析
通过这个 Bean 名称,其实能够看进去这个 Bean 是咱们开始提到要动静刷新的 Feign.Options,外面有连贯超时、读取超时等配置。名字前面的局部是咱们创立的 FeignClient 下面 @FeignClient
注解外面的 contextId。
在创立 FeignClient 的时候,须要加载这个 Feign.Options Bean,每个 FeignClient 都有本人的 ApplicationContext,这个 Feign.Options Bean 就是属于每个 FeignClient 独自的 ApplicationContext 的。这个是通过 Spring Cloud 的 NamedContextFactory 实现的。对于 NamedContextFactory 的深入分析,能够参考我的这篇文章:[]()
对于 OpenFeign 的配置开启动静刷新,其实就是对于 FeignClient 就是要刷新每个 FeignClient 的 Feign.Options 这个 Bean。那么如何实现呢?咱们先来看 spring-cloud 的动静刷新 Bean 的实现形式。首先咱们要搞清楚,什么是 Scope。
Bean 的 Scope
从字面意思下面了解,Scope 即 Bean 的作用域。从实现下面了解,Scope 即咱们在获取 Bean 的时候,这个 Bean 是如何获取的。
Spring 框架中自带两个耳熟能详的 Scope,即 singleton 和 prototype。singleton 即每次从 BeanFactory 获取一个 Bean 的时候(getBean
),对于同一个 Bean 每次返回的都是同一个对象,即单例模式。prototype 即每次从 BeanFactory 获取一个 Bean 的时候,对于同一个 Bean 每次都新创建一个对象返回,即工厂模式。
同时,咱们还能够依据本人须要去扩大 Scope,定义获取 Bean 的形式。举一个简略的例子,咱们自定义一个 TestScope。自定义的 Scope 须要先定义一个实现 org.springframework.beans.factory.config.Scope
接口的类,来定义在这个 Scope 下的 Bean 的获取相干的操作。
public interface Scope {
// 获取这个 bean,在 BeanFactory.getBean 的时候会被调用
Object get(String name, ObjectFactory<?> objectFactory);
// 在调用 BeanFactory.destroyScopedBean 的时候,会调用这个办法
@Nullable
Object remove(String name);
// 注册 destroy 的 callback
// 这个是可选实现,提供给内部注册销毁 bean 的回调。能够在 remove 的时候,执行这里传入的 callback。void registerDestructionCallback(String name, Runnable callback);
// 如果一个 bean 不在 BeanFactory 中,而是依据上下文创立的,例如每个 http 申请创立一个独立的 bean,这样从 BeanFactory 中就拿不到了,就会从这里拿
// 这个也是可选实现
Object resolveContextualObject(String key);
// 可选实现,相似于 session id 用户辨别不同上下文的
String getConversationId();}
咱们来实现一个简略的 Scope:
public static class TestScope implements Scope {
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {return objectFactory.getObject();
}
@Override
public Object remove(String name) {return null;}
@Override
public void registerDestructionCallback(String name, Runnable callback) { }
@Override
public Object resolveContextualObject(String key) {return null;}
@Override
public String getConversationId() {return null;}
}
这个 Scope 只是实现了 get 办法。间接通过传入的 objectFactory 创立一个新的 bean。这种 Scope 下每次调用 BeanFactory.getFactory 都会返回一个新的 Bean,主动装载到不同 Bean 的这种 Scope 下的 Bean 也是不同的实例。编写测试:
@Configuration
public static class Config {
@Bean
// 自定义 Scope 的名字是 testScope
@org.springframework.context.annotation.Scope(value = "testScope")
public A a() {return new A();
}
// 主动装载进来
@Autowired
private A a;
}
public static class A {public void test() {System.out.println(this);
}
}
public static void main(String[] args) {
// 创立一个 ApplicationContext
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
// 注册咱们自定义的 Scope
annotationConfigApplicationContext.getBeanFactory().registerScope("testScope", new TestScope());
// 注册咱们须要的配置 Bean
annotationConfigApplicationContext.register(Config.class);
// 调用 refresh 初始化 ApplicationContext
annotationConfigApplicationContext.refresh();
// 获取 Config 这个 Bean
Config config = annotationConfigApplicationContext.getBean(Config.class);
// 调用主动装载的 Bean
config.a.test();
// 从 BeanFactory 调用 getBean 获取 A
annotationConfigApplicationContext.getBean(A.class).test();
annotationConfigApplicationContext.getBean(A.class).test();}
执行代码,丛输入上能够看出,这三个 A 都是不同的对象:
com.hopegaming.spring.cloud.parent.ScopeTest$A@5241cf67
com.hopegaming.spring.cloud.parent.ScopeTest$A@716a7124
com.hopegaming.spring.cloud.parent.ScopeTest$A@77192705
咱们再来批改咱们的 Bean,让它成为一个 Disposable Bean:
public static class A implements DisposableBean {public void test() {System.out.println(this);
}
@Override
public void destroy() throws Exception {System.out.println(this + "is destroyed");
}
}
再批改下咱们的自定义 Scope:
public static class TestScope implements Scope {
private Runnable callback;
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {return objectFactory.getObject();
}
@Override
public Object remove(String name) {System.out.println(name + "is removed");
this.callback.run();
System.out.println("callback finished");
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {System.out.println("registerDestructionCallback is called");
this.callback = callback;
}
@Override
public Object resolveContextualObject(String key) {System.out.println("resolveContextualObject is called");
return null;
}
@Override
public String getConversationId() {System.out.println("getConversationId is called");
return null;
}
}
在测试代码中,减少调用 destroyScopedBean 销毁 bean:
annotationConfigApplicationContext.getBeanFactory().destroyScopedBean("a");
运行代码,能够看到对应的输入:
registerDestructionCallback is called
a is removed
com.hopegaming.spring.cloud.parent.ScopeTest$A@716a7124 is destroyed
callback finished
对于 DisposableBean 或者其余有相干生命周期类型的 Bean,BeanFactory 会通过 registerDestructionCallback 将生命周期须要的操作回调传进来。应用 BeanFactory.destroyScopedBean
销毁 Bean 的时候,会调用 Scope 的 remove 办法,咱们能够在操作实现时,调用 callback 回调实现 Bean 生命周期。
接下来咱们尝试实现一种单例的 Scope,形式非常简单,次要基于 ConcurrentHashMap:
public static class TestScope implements Scope {private final ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Runnable> callback = new ConcurrentHashMap<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {System.out.println("get is called");
return map.compute(name, (k, v) -> {if (v == null) {v = objectFactory.getObject();
}
return v;
});
}
@Override
public Object remove(String name) {this.map.remove(name);
System.out.println(name + "is removed");
this.callback.get(name).run();
System.out.println("callback finished");
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {System.out.println("registerDestructionCallback is called");
this.callback.put(name, callback);
}
@Override
public Object resolveContextualObject(String key) {return null;}
@Override
public String getConversationId() {return null;}
}
咱们应用两个 ConcurrentHashMap 缓存这个 Scope 下的 Bean,以及对应的 Destroy Callback。在这种实现下,就相似于单例模式的实现了。再应用上面的测试程序测试下:
public static void main(String[] args) {AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
annotationConfigApplicationContext.getBeanFactory().registerScope("testScope", new TestScope());
annotationConfigApplicationContext.register(Config.class);
annotationConfigApplicationContext.refresh();
Config config = annotationConfigApplicationContext.getBean(Config.class);
config.a.test();
annotationConfigApplicationContext.getBean(A.class).test();
//Config 类中注册 Bean 的办法名称为 a,所以 Bean 名称也为 a
annotationConfigApplicationContext.getBeanFactory().destroyScopedBean("a");
config.a.test();
annotationConfigApplicationContext.getBean(A.class).test();}
咱们在销毁 Bean 之前,应用主动装载和 BeanFactory.getBean 别离去申请获取 A 这个 Bean 并调用 test 办法。而后销毁这个 Bean。在这之后,再去应用主动装载的和 BeanFactory.getBean 别离去申请获取 A 这个 Bean 并调用 test 办法。能够从输入中看出,BeanFactory.getBean 申请的是新的 Bean 了,然而主动装载的外面还是已销毁的那个 bean。那么如何实现让主动装载的也是新的 Bean,也就是从新注入呢?
这就波及到了 Scope 注解下面的另一个配置,即指定代理模式:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {@AliasFor("scopeName")
String value() default "";
@AliasFor("value")
String scopeName() default "";
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;}
其中第三个配置,ScopedProxyMode 是配置获取这个 Bean 的时候,获取的是原始 Bean 对象还是代理的 Bean 对象(这也同时影响了主动装载):
public enum ScopedProxyMode {
// 走默认配置,没有其余外围配置则是 NO
DEFAULT,
// 应用原始对象作为 Bean
NO,
// 应用 JDK 的动静代理
INTERFACES,
// 应用 CGLIB 动静代理
TARGET_CLASS
}
咱们来测试下指定 Scope Bean 的理论对象为代理的成果,咱们批改下下面的测试代码,应用 CGLIB 动静代理。批改代码:
@Configuration
public static class Config {
@Bean
@org.springframework.context.annotation.Scope(value = "testScope"
// 指定代理模式为基于 CGLIB
, proxyMode = ScopedProxyMode.TARGET_CLASS
)
public A a() {return new A();
}
@Autowired
private A a;
}
编写测试主办法:
public static void main(String[] args) {AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
annotationConfigApplicationContext.getBeanFactory().registerScope("testScope", new TestScope());
annotationConfigApplicationContext.register(Config.class);
annotationConfigApplicationContext.refresh();
Config config = annotationConfigApplicationContext.getBean(Config.class);
config.a.test();
annotationConfigApplicationContext.getBean(A.class).test();
// 查看 Bean 实例的类型
System.out.println(config.a.getClass());
System.out.println(annotationConfigApplicationContext.getBean(A.class).getClass());
// 这时候咱们须要留神,代理 Bean 的名称有所变动,须要通过 ScopedProxyUtils 获取
annotationConfigApplicationContext.getBeanFactory().destroyScopedBean(ScopedProxyUtils.getTargetBeanName("a"));
config.a.test();
annotationConfigApplicationContext.getBean(A.class).test();}
执行程序,输入为:
get is called
registerDestructionCallback is called
com.hopegaming.spring.cloud.parent.ScopeTest$A@3dd69f5a
get is called
com.hopegaming.spring.cloud.parent.ScopeTest$A@3dd69f5a
class com.hopegaming.spring.cloud.parent.ScopeTest$A$$EnhancerBySpringCGLIB$$2fa625ee
class com.hopegaming.spring.cloud.parent.ScopeTest$A$$EnhancerBySpringCGLIB$$2fa625ee
scopedTarget.a is removed
com.hopegaming.spring.cloud.parent.ScopeTest$A@3dd69f5a is destroyed
callback finished
get is called
registerDestructionCallback is called
com.hopegaming.spring.cloud.parent.ScopeTest$A@3aa3193a
get is called
com.hopegaming.spring.cloud.parent.ScopeTest$A@3aa3193a
从输入中能够看出:
- 每次对于主动装载的 Bean 的调用,都会调用自定义 Scope 的 get 办法从新获取 Bean
- 每次通过 BeanFactory 获取 Bean,也会调用自定义 Scope 的 get 办法从新获取 Bean
- 获取的 Bean 实例,是一个 CGLIB 代理对象
- 在 Bean 被销毁后,无论是通过 BeanFactory 获取 Bean 还是主动装载的 Bean,都是新的 Bean
那么 Scope 是如何实现这些的呢?咱们接下来简略剖析下源码
Scope 基本原理
如果一个 Bean 没有申明任何 Scope,那么他的 Scope 就会被赋值成 singleton,也就是 默认的 Bean 都是单例的。这个对应 BeanFactory 注册 Bean 之前须要生成 Bean 定义,在 Bean 定义的时候会赋上这个默认值,对应源码:
AbstractBeanFactory
protected RootBeanDefinition getMergedBeanDefinition(String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd)
throws BeanDefinitionStoreException {
// 省略咱们不关怀的源码
if (!StringUtils.hasLength(mbd.getScope())) {mbd.setScope(SCOPE_SINGLETON);
}
// 省略咱们不关怀的源码
}
在申明一个 Bean 具备非凡 Scope 之前,咱们须要定义这个自定义 Scope 并把它注册到 BeanFactory 中。这个 Scope 名称必须全局惟一,因为之后辨别不同 Scope 就是通过这个名字进行辨别的。注册 Scope 对应源码:
AbstractBeanFactory
@Override
public void registerScope(String scopeName, Scope scope) {Assert.notNull(scopeName, "Scope identifier must not be null");
Assert.notNull(scope, "Scope must not be null");
// 不能为 singleton 和 prototype 这两个预设的 scope
if (SCOPE_SINGLETON.equals(scopeName) || SCOPE_PROTOTYPE.equals(scopeName)) {throw new IllegalArgumentException("Cannot replace existing scopes'singleton'and'prototype'");
}
// 放入 scopes 这个 map 中,key 为名称,value 为自定义 Scope
Scope previous = this.scopes.put(scopeName, scope);
// 能够看出,前面放入的会替换后面的,这个咱们要尽量避免呈现。if (previous != null && previous != scope) {if (logger.isDebugEnabled()) {logger.debug("Replacing scope'" + scopeName + "'from [" + previous + "] to [" + scope + "]");
}
}
else {if (logger.isTraceEnabled()) {logger.trace("Registering scope'" + scopeName + "'with implementation [" + scope + "]");
}
}
}
当申明一个 Bean 具备非凡的 Scope 之后,获取这个 Bean 的时候,就会有非凡的逻辑,参考通过 BeanFactory 获取 Bean 的外围源码代码:
AbstractBeanFactory
@SuppressWarnings("unchecked")
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
// 省略咱们不关怀的源码
// 创立 Bean 实例
if (mbd.isSingleton()) {// 创立或者返回单例实例} else if (mbd.isPrototype()) {// 每次创立一个新实例} else {
// 走到这里代表这个 Bean 属于自定义 Scope
String scopeName = mbd.getScope();
// 必须有 Scope 名称
if (!StringUtils.hasLength(scopeName)) {throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");
}
// 通过 Scope 名称获取对应的 Scope,自定义 Scope 须要手动注册进来
Scope scope = this.scopes.get(scopeName);
if (scope == null) {throw new IllegalStateException("No Scope registered for scope name'" + scopeName + "'");
}
try {
// 调用自定义 Scope 的 get 办法获取 Bean
Object scopedInstance = scope.get(beanName, () -> {
// 同时将创立 Bean 须要的生命周期的回调传入,用于创立 Bean
beforePrototypeCreation(beanName);
try {return createBean(beanName, mbd, args);
}
finally {afterPrototypeCreation(beanName);
}
});
beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {throw new ScopeNotActiveException(beanName, scopeName, ex);
}
}
// 省略咱们不关怀的源码
}
同时,如果咱们定义 Scope Bean 的代理形式为 CGLIB,那么在获取 Bean 定义的时候,就会依据原始 Bean 定义创立 Scope 代理的 Bean 定义,对应源码:
ScopedProxyUtils
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
BeanDefinitionRegistry registry, boolean proxyTargetClass) {
// 原始指标 Bean 名称
String originalBeanName = definition.getBeanName();
// 获取原始指标 Bean 定义
BeanDefinition targetDefinition = definition.getBeanDefinition();
// 获取代理 Bean 名称
String targetBeanName = getTargetBeanName(originalBeanName);
// 创立类型为 ScopedProxyFactoryBean 的 Bean
RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
// 依据原始指标 Bean 定义的属性,配置代理 Bean 定义的相干属性,省略这部分源码
// 依据原始指标 Bean 的主动装载属性,复制到代理 Bean 定义
proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
proxyDefinition.setPrimary(targetDefinition.isPrimary());
if (targetDefinition instanceof AbstractBeanDefinition) {proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition);
}
// 设置原始 Bean 定义为不主动装载并且不为 Primary
// 这样通过 BeanFactory 获取 Bean 以及主动装载的都是代理 Bean 而不是原始指标 Bean
targetDefinition.setAutowireCandidate(false);
targetDefinition.setPrimary(false);
// 应用新名称注册 Bean
registry.registerBeanDefinition(targetBeanName, targetDefinition);
return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}
private static final String TARGET_NAME_PREFIX = "scopedTarget.";
// 这个就是获取代理 Bean 名称的工具办法,咱们下面 Destroy Bean 的时候也有用到
public static String getTargetBeanName(String originalBeanName) {return TARGET_NAME_PREFIX + originalBeanName;}
这个代理 Bean 有啥作用呢?其实 次要用途就是每次调用 Bean 的任何办法的时候,都会通过 BeanFactory 获取这个 Bean 进行调用。参考源码:
代理类 ScopedProxyFactoryBean
public class ScopedProxyFactoryBean extends ProxyConfig
implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean {private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
// 这个就是通过 SimpleBeanTargetSource 生成的理论代理,对于 Bean 的办法调用都会通过这个 proxy 进行调用
private Object proxy;
}
SimpleBeanTargetSource
就是理论的代理源,他的实现非常简单,外围办法就是应用 Bean 名称通过 BeanFactory 获取这个 Bean:
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception {return getBeanFactory().getBean(getTargetBeanName());
}
}
通过 BeanFactory 获取这个 Bean,通过下面源码剖析能够晓得,对于自定义 Scope 的 Bean 就会调用自定义 Scope 的 get 办法。
而后是 Bean 的销毁,在 BeanFactory 创立这个 Bean 对象的时候,就会调用自定义 Scope 的 registerDestructionCallback 将 Bean 销毁的回调传入:
AbstractBeanFactory
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) {AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null);
if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) {if (mbd.isSingleton()) {
// 对于 singleton
registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, acc));
}
else {
// 对于自定义 Scope
Scope scope = this.scopes.get(mbd.getScope());
if (scope == null) {throw new IllegalStateException("No Scope registered for scope name'" + mbd.getScope() + "'");
}
// 调用 registerDestructionCallback
scope.registerDestructionCallback(beanName, new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, acc));
}
}
}
在咱们想销毁 Scope Bean 的时候,须要调用的是 BeanFactory 的 destroyScopedBean 办法,这个办法会调用自定义 Scope 的 remove:
AbstractBeanFactory
@Override
public void destroyScopedBean(String beanName) {RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// 仅针对自定义 Scope Bean 应用
if (mbd.isSingleton() || mbd.isPrototype()) {
throw new IllegalArgumentException("Bean name'" + beanName + "'does not correspond to an object in a mutable scope");
}
String scopeName = mbd.getScope();
Scope scope = this.scopes.get(scopeName);
if (scope == null) {throw new IllegalStateException("No Scope SPI registered for scope name'" + scopeName + "'");
}
// 调用自定义 Scope 的 remove 办法
Object bean = scope.remove(beanName);
if (bean != null) {destroyBean(beanName, bean, mbd);
}
}
微信搜寻“我的编程喵”关注公众号,每日一刷,轻松晋升技术,斩获各种 offer: