自己動(dòng)手實(shí)現(xiàn)簡(jiǎn)易版Feign

Feign的功能

主要功能是讓開(kāi)發(fā)人員只使用簡(jiǎn)單注解就能像調(diào)用一般RPC一樣調(diào)用HTTP請(qǐng)求,在此基礎(chǔ)上荧止,框架擴(kuò)展了原SpringMVC參數(shù)只能為一個(gè)類的功能。

基本思路

代理實(shí)現(xiàn)->bean的注冊(cè)->多參數(shù)實(shí)現(xiàn)

代碼實(shí)現(xiàn)

注解定義

需要定義三個(gè)注解。
第一個(gè)“HttpConsumer”奈揍,用于消費(fèi)端注冊(cè)服務(wù)绅项。

/**
 * 標(biāo)注需要代理為http請(qǐng)求發(fā)送
 *
 * @author kun
 * @data 2022/1/15 17:50
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpConsumer {
    /**
     * 域名
     *
     * @return  域名
     */
    String domain();

    /**
     * 端口紊册,默認(rèn)80
     *
     * @return  端口號(hào)
     */
    String port() default "80";
}

第二個(gè)“HttpRequest”,用于服務(wù)提供方定義服務(wù)快耿。

/**
 * 用于標(biāo)記可以通過(guò)http訪問(wèn)的服務(wù)
 *
 * @author kun
 * @data 2022/1/15 17:55
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpRequest {
    /**
     * http請(qǐng)求url
     *
     * @return  url
     */
    String value();
}

第三個(gè)“MultiRequestBody”囊陡,用于實(shí)現(xiàn)多變量傳參。

/**
 * 標(biāo)注方法參數(shù)
 *
 * @author kun
 * @data 2022/1/15 17:57
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiRequestBody {
    /**
     * 解析參數(shù)時(shí)用到JSON中的key
     *
     * @return  JSON格式參數(shù)
     */
    String value();
}

動(dòng)態(tài)代理

創(chuàng)建代理的目的是將方法轉(zhuǎn)化為http調(diào)用掀亥。

/**
 * HttpConsumer注解代理類
 *
 * @author kun
 * @data 2022/1/15 19:30
 */
public class HttpConsumerInterceptor implements MethodInterceptor {

    private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static final String HTTP_HEAD = "http://";

    private final Class<?> proxyKlass;

    private final HttpDomain httpDomain;

    private final OkHttpClient okHttpClient;

    public HttpConsumerInterceptor(Class<?> proxyKlass, HttpDomain httpDomain, OkHttpClient okHttpClient) {
        this.proxyKlass = proxyKlass;
        this.httpDomain = httpDomain;
        this.okHttpClient = okHttpClient;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        HttpRequest httpRequest = method.getAnnotation(HttpRequest.class);
        if (Objects.isNull(httpRequest)) {
            throw new IllegalStateException("method[" + method.getName() + "] must annotated with @HttpRequest!");
        }
        HttpRequest klassAnnotation = proxyKlass.getAnnotation(HttpRequest.class);
        String namespace = Objects.isNull(klassAnnotation) ? null : klassAnnotation.value();
        String url = getUrl(httpRequest.value(), namespace);
        Request request = buildRequest(method, args, url);
        Call call = okHttpClient.newCall(request);
        Response response = call.execute();
        ResponseBody body = response.body();
        if (Objects.isNull(body)) {
            return null;
        }
        byte[] bytes = body.bytes();
        String res = new String(bytes, StandardCharsets.UTF_8);
        if (StringUtils.isEmpty(res)) {
            return null;
        }
        try {
            return OBJECT_MAPPER.readValue(res, method.getReturnType());
        } catch (Throwable t) {
            Map<?, ?> map = OBJECT_MAPPER.readValue(res, Map.class);
            Object err = map.get("error");
            if (Objects.nonNull(err)) {
                throw new RuntimeException(err.toString());
            }
            throw new RuntimeException(t);
        }
    }

    private String getUrl(String requestPath, String namespace) {
        if (Objects.isNull(namespace)) {
            return HTTP_HEAD + httpDomain.getDomain() + ":" + httpDomain.getPort() + "/" + requestPath;
        }
        return HTTP_HEAD + httpDomain.getDomain() + ":" + httpDomain.getPort() + "/" + namespace + "/" + requestPath;
    }

    private Request buildRequest(Method method, Object[] args, String url) throws JsonProcessingException {
        Request.Builder builder = new Request.Builder();
        Map<String, Object> paramMap = new HashMap<>(4);
        Parameter[] parameters = method.getParameters();
        for (int i=0; i<parameters.length; i++) {
            Parameter parameter = parameters[i];
            MultiRequestBody multiRequestBody = parameter.getAnnotation(MultiRequestBody.class);
            if (Objects.isNull(multiRequestBody)) {
                throw new IllegalStateException("method[" + method.getName() + "] param must annotated with @MultiRequest!");
            }
            paramMap.put(multiRequestBody.value(), args[i]);
        }
        // 將參數(shù)構(gòu)建為一個(gè)大JSON
        String param = OBJECT_MAPPER.writeValueAsString(paramMap);
        RequestBody requestBody = RequestBody.create(param, JSON);
        builder.post(requestBody);
        builder.url(url);
        return builder.build();
    }
}

自定義FactoryBean

使用FactoryBean生成bean

/**
 * @author kun
 * @data 2022/1/16 14:50
 */
public class HttpConsumerProxyFactoryBean implements FactoryBean<Object> {

    private final Class<?> proxyKlass;

    private final HttpDomain httpDomain;

    private final OkHttpClient okHttpClient;

    public HttpConsumerProxyFactoryBean(Class<?> proxyKlass, HttpDomain httpDomain, OkHttpClient okHttpClient) {
        this.proxyKlass = proxyKlass;
        this.httpDomain = httpDomain;
        this.okHttpClient = okHttpClient;
    }

    @Override
    public Object getObject() {
        Enhancer enhancer = new Enhancer();
        if (proxyKlass.isInterface()) {
            enhancer.setInterfaces(new Class[]{proxyKlass});
        } else {
            enhancer.setSuperclass(proxyKlass);
        }
        HttpConsumerInterceptor httpConsumerInterceptor = new HttpConsumerInterceptor(proxyKlass, httpDomain, okHttpClient);
        enhancer.setCallback(httpConsumerInterceptor);
        return enhancer.create();
    }

    @Override
    public Class<?> getObjectType() {
        return proxyKlass;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

注冊(cè)bean

使用BeanDefinition方式注冊(cè)bean

/**
 * @author kun
 * @data 2022/1/16 13:59
 */
@EnableConfigurationProperties(HttpConsumerProperties.class)
public class HttpConsumerPostProcessor implements BeanClassLoaderAware, EnvironmentAware, BeanFactoryPostProcessor, ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger(HttpConsumerPostProcessor.class);

    private ClassLoader classLoader;

    private ApplicationContext context;

    private ConfigurableEnvironment environment;

    private ConfigurableListableBeanFactory beanFactory;

    private final Map<String, BeanDefinition> httpClientBeanDefinitions = new HashMap<>(4);

    private OkHttpClient okHttpClient;

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        this.beanFactory = configurableListableBeanFactory;
        this.okHttpClient = buildOkHttpClient(environment);
        postProcessBeanFactory(beanFactory, (BeanDefinitionRegistry) beanFactory);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = (ConfigurableEnvironment) environment;
    }

    private OkHttpClient buildOkHttpClient(ConfigurableEnvironment environment) {
        HttpConsumerProperties properties = BinderUtils.bind(environment, HttpConsumerProperties.PREFIX, HttpConsumerProperties.class);
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(properties.getCoreThreads(), properties.getMaxThreads(),
                properties.getKeepAliveTime(), TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp Custom Dispatcher", false));
        Dispatcher dispatcher = new Dispatcher(executor);
        dispatcher.setMaxRequests(properties.getMaxRequests());
        dispatcher.setMaxRequestsPerHost(properties.getMaxRequests());
        builder.dispatcher(dispatcher);
        builder.connectTimeout(properties.getConnectTimeOut(), TimeUnit.SECONDS);
        builder.readTimeout(properties.getReadTimeOut(), TimeUnit.SECONDS);
        builder.writeTimeout(properties.getWriteTimeOut(), TimeUnit.SECONDS);
        builder.connectionPool(new ConnectionPool(properties.getMaxIdleConnections(), properties.getConnectionKeepAliveTime(), TimeUnit.SECONDS));
        return builder.build();
    }

    private void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry) {
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
            String beanClassName = definition.getBeanClassName();
            // 當(dāng)用@Bean 返回的類型是Object時(shí)撞反,beanClassName是null
            if (Objects.isNull(beanClassName)) {
                continue;
            }
            Class<?> clazz = ClassUtils.resolveClassName(definition.getBeanClassName(), this.classLoader);
            ReflectionUtils.doWithFields(clazz, this::parseElement, this::annotatedWithHttpConsumer);
        }
        for (String beanName : httpClientBeanDefinitions.keySet()) {
            if (context.containsBean(beanName)) {
                throw new IllegalArgumentException("[HttpConsumer Starter] Spring context already has a bean named " + beanName
                 + ", please change @HttpConsumer field name.");
            }
            registry.registerBeanDefinition(beanName, httpClientBeanDefinitions.get(beanName));
            logger.info("registered HttpConsumer factory bean \"{}\" in spring context.", beanName);
        }
    }

    private void parseElement(Field field) {
        Class<?> interfaceClass = field.getType();
        if (!interfaceClass.isInterface()) {
            throw new IllegalArgumentException("field [" + field.getName() + "] annotated with @HttpConsumer must be interface!");
        }
        HttpConsumer httpConsumer = AnnotationUtils.getAnnotation(field, HttpConsumer.class);
        HttpDomain httpDomain = HttpDomain.from(httpConsumer);
        // 支持占位符${}
        httpDomain.setDomain(beanFactory.resolveEmbeddedValue(httpDomain.getDomain()));
        httpDomain.setPort(beanFactory.resolveEmbeddedValue(httpDomain.getPort()));
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
                .rootBeanDefinition(HttpConsumerProxyFactoryBean.class)
                .addConstructorArgValue(interfaceClass)
                .addConstructorArgValue(httpDomain)
                .addConstructorArgValue(okHttpClient);
        BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        beanDefinition.setPrimary(true);
        beanDefinition.setAutowireCandidate(true);
        httpClientBeanDefinitions.put(field.getName(), beanDefinition);
    }

    private boolean annotatedWithHttpConsumer(Field field) {
        return field.isAnnotationPresent(HttpConsumer.class);
    }
}

多參數(shù)適配

主要是多SpringMVC的參數(shù)進(jìn)行配置

/**
 * 參數(shù)解析
 *
 * @author kun
 * @data 2022/1/15 21:06
 */
public class MultiRequestBodyResolver implements HandlerMethodArgumentResolver {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static final String JSON_REQUEST_BODY = "JSON_REQUEST_BODY";

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(MultiRequestBody.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        String requestBody = getRequestBody(nativeWebRequest);
        OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        JsonNode rootNode = OBJECT_MAPPER.readTree(requestBody);
        if (StringUtils.isEmpty(rootNode)) {
            throw new IllegalArgumentException("requestBody must not empty!");
        }
        MultiRequestBody multiRequestBody = methodParameter.getParameterAnnotation(MultiRequestBody.class);
        if (Objects.isNull(multiRequestBody)) {
            throw new IllegalArgumentException("param must annotated with @MultiRequestBody!");
        }
        String key = !StringUtils.isEmpty(multiRequestBody.value()) ? multiRequestBody.value() : methodParameter.getParameterName();
        JsonNode value = rootNode.get(key);
        if (Objects.isNull(value)) {
            return null;
        }
        Class<?> paramType = methodParameter.getParameterType();
        return OBJECT_MAPPER.readValue(value.toString(), paramType);
    }

    /**
     *
     * 獲取請(qǐng)求體的JSON字符串
     */
    private String getRequestBody(NativeWebRequest webRequest) {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        String jsonBody = (String) webRequest.getAttribute(JSON_REQUEST_BODY, NativeWebRequest.SCOPE_REQUEST);
        if (!StringUtils.isEmpty(jsonBody)) {
            return jsonBody;
        }
        try (BufferedReader reader = servletRequest.getReader()) {
            jsonBody = IOUtils.toString(reader);
            webRequest.setAttribute(JSON_REQUEST_BODY, jsonBody, NativeWebRequest.SCOPE_REQUEST);
            return jsonBody;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
/**
 * JSON解析數(shù)據(jù)
 *
 * @author kun
 * @data 2022/1/15 21:03
 */
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer pathMatchConfigurer) {

    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer contentNegotiationConfigurer) {

    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer asyncSupportConfigurer) {

    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer defaultServletHandlerConfigurer) {

    }

    @Override
    public void addFormatters(FormatterRegistry formatterRegistry) {

    }

    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {

    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry resourceHandlerRegistry) {

    }

    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {

    }

    @Override
    public void addViewControllers(ViewControllerRegistry viewControllerRegistry) {

    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry viewResolverRegistry) {

    }

    /**
     * 參數(shù)解析器
     *
     * @param list  解析器
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> list) {
        list.add(new MultiRequestBodyResolver());
    }

    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> list) {

    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> list) {
        MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter();
        LinkedList<MediaType> mediaTypes = new LinkedList<>();
        mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        jacksonConverter.setSupportedMediaTypes(mediaTypes);
        jacksonConverter.setDefaultCharset(StandardCharsets.UTF_8);
        list.add(jacksonConverter);
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> list) {

    }

    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> list) {

    }

    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> list) {

    }

    @Override
    public Validator getValidator() {
        return null;
    }

    @Override
    public MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}

編寫為Spring starter

/**
 * 服務(wù)調(diào)用方生效
 *
 * @author kun
 * @data 2022/1/16 15:36
 */
@ConditionalOnClass(OkHttpClient.class)
public class HttpClientAuthConfig {

    @Bean
    @ConditionalOnMissingBean
    public HttpConsumerPostProcessor httpConsumerPostProcessor() {
        return new HttpConsumerPostProcessor();
    }
}
/**
 * 服務(wù)提供方生效
 *
 * @author kun
 * @data 2022/1/16 15:40
 */
@ConditionalOnClass(WebMvcConfigurer.class)
public class SpringMvcConfigurerAutoConfig {

    @Bean
    @ConditionalOnMissingBean
    public WebMvcConfig webMvcConfig() {
        return new WebMvcConfig();
    }

    @Bean
    @ConditionalOnMissingBean
    public HttpRequestValidator httpRequestValidator() {
        return new HttpRequestValidator();
    }
}

測(cè)試

服務(wù)提供方

/**
 * @author kun
 * @data 2022/1/22 14:25
 */
@HttpRequest("demo")
public interface DemoHttpService {

    @HttpRequest("checkSuccess")
    String checkSuccess(@MultiRequestBody("param1") String param1, @MultiRequestBody("param2") String param2);
}

服務(wù)調(diào)用方

/**
 * @author kun
 * @data 2022/1/22 14:37
 */
@Configuration
public class HttpConfig {

    @HttpConsumer(domain = "localhost", port = "8080")
    private DemoHttpService demoHttpService;
}
/**
 * @author kun
 * @data 2022/1/22 14:40
 */
@Component
public class DemoConsumer {
    @Resource
    private DemoHttpService demoHttpService;

    public String checkSuccess() {
        return demoHttpService.checkSuccess("param1", "param2");
    }
}

完整代碼地址

https://github.com/wanggangkun/myFeign

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市铺浇,隨后出現(xiàn)的幾起案子痢畜,更是在濱河造成了極大的恐慌,老刑警劉巖鳍侣,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丁稀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡倚聚,警方通過(guò)查閱死者的電腦和手機(jī)线衫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惑折,“玉大人授账,你說(shuō)我怎么就攤上這事〔沂唬” “怎么了白热?”我有些...
    開(kāi)封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)粗卜。 經(jīng)常有香客問(wèn)我屋确,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任攻臀,我火速辦了婚禮焕数,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刨啸。我一直安慰自己堡赔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布设联。 她就那樣靜靜地躺著善已,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仑荐。 梳的紋絲不亂的頭發(fā)上雕拼,一...
    開(kāi)封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音粘招,去河邊找鬼啥寇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛洒扎,可吹牛的內(nèi)容都是我干的辑甜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼袍冷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼磷醋!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起胡诗,我...
    開(kāi)封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤邓线,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后煌恢,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體骇陈,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年瑰抵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了你雌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡二汛,死狀恐怖婿崭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肴颊,我是刑警寧澤氓栈,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站婿着,受9級(jí)特大地震影響颤绕,放射性物質(zhì)發(fā)生泄漏幸海。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一奥务、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧袜硫,春花似錦氯葬、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至秽澳,卻和暖如春闯睹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背担神。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工楼吃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妄讯。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓孩锡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親亥贸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子躬窜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Spring官網(wǎng)的Feign文檔[https://cloud.spring.io/spring-cloud-sta...
    丶含光閱讀 929評(píng)論 0 3
  • Java繼承關(guān)系初始化順序 父類的靜態(tài)變量-->父類的靜態(tài)代碼塊-->子類的靜態(tài)變量-->子類的靜態(tài)代碼快-->父...
    第六象限閱讀 2,147評(píng)論 0 9
  • 1-Java基礎(chǔ) 1.1-String和StringBuffer區(qū)別,為什么是可變的炕置,不可變的 String 類中...
    楊慶祥閱讀 954評(píng)論 0 0
  • Q1:IoC 是什么荣挨? IoC 即控制反轉(zhuǎn),簡(jiǎn)單來(lái)說(shuō)就是把原來(lái)代碼里需要實(shí)現(xiàn)的對(duì)象創(chuàng)建朴摊、依賴反轉(zhuǎn)給容器來(lái)幫忙實(shí)現(xiàn)默垄,...
    Java程序員石頭閱讀 167評(píng)論 0 1
  • 疫情現(xiàn)在比較穩(wěn)定了,小區(qū)樓下每天該遛狗的遛狗仍劈、該買菜的買菜厕倍、該逛街的逛街。然而我眉頭一皺贩疙,現(xiàn)在還是得緊繃神經(jīng)讹弯、嚴(yán)守...
    小漢同學(xué)閱讀 455評(píng)論 0 2