返回顶部
首页 > 资讯 > 精选 >ResponseBodyAdvice如何使用
  • 128
分享到

ResponseBodyAdvice如何使用

2023-07-05 11:07:16 128人浏览 薄情痞子
摘要

本篇内容介绍了“ResponseBodyAdvice如何使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!前言ResponseBodyAdv

本篇内容介绍了“ResponseBodyAdvice如何使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

    前言

    ResponseBodyAdvice接口可以在将handler方法的返回值写入response前对返回值进行处理,例如将返回值封装成一个与客户端约定好的对象以便于客户端处理响应数据。

    SpringBoot版本:2.4.1

    一. ResponseBodyAdvice的使用

    假如已经存在一个Controller,如下所示。

    @RestControllerpublic class DemoController {    @RequestMapping(value = "/api/v1/demo1/getdefault", method = RequestMethod.GET)    public ResponseEntity<Demo1> getDefaultDemo1() {        return new ResponseEntity<>(Demo1.defaultDemo1, httpstatus.INTERNAL_SERVER_ERROR);    }    @RequestMapping(value = "/api/v1/demo2/getdefault", method = RequestMethod.GET)    public Demo2 getDefaultDemo2() {        return Demo2.defaultDemo2;    }}public class Demo1 {    private int id;    private String name;    public static Demo1 defaultDemo1 = new Demo1(1, "Admin");    public Demo1() {}    public Demo1(int id, String name) {this.id = id;this.name = name;    }    // 省略getter和setter}public class Demo2 {    private int id;    private String desc;    public static Demo2 defaultDemo2 = new Demo2(1, "Root");    public Demo2() {}    public Demo2(int id, String desc) {this.id = id;        this.desc = desc;    }    // 省略getter和setter}

    上述Controller中有两个方法,并且返回值分别为ResponseEntity<Demo1>和Demo2。此时客户端收到响应之后,针对响应体的处理变得十分不方便,如果增加更多的方法,并且返回值都不相同,那么客户端将需要根据不同的请求来特定的处理响应体。因此为了方便客户端处理响应数据,服务器端专门创建了一个返回结果类ReturnResult,并且规定服务器端的所有handler方法执行后往response中写入的响应体都必须为ReturnResult。在这种情况下,使用ResponseBodyAdvice可以在不修改已有业务代码的情况下轻松实现上述需求。假设自定义的返回结果类ReturnResult如下所示。

    public class ReturnResult<T> {    private int statusCode;    private T body;    public ReturnResult() {}    public ReturnResult(T body) {        this.body = body;    }    // 省略getter和setter}

    ReturnResult的body就是原本需要写入response的响应内容,现在整个ReturnResult为需要写入response的响应内容,相当于ReturnResult对handler方法的返回值进行了一层封装。

    现在创建一个ReturnResultAdvice类并实现ResponseBodyAdvice接口,如下所示。

    @ControllerAdvicepublic class ReturnResultAdvice implements ResponseBodyAdvice<Object> {    @Override    public boolean supports(@Nullable MethodParameter returnType, @Nullable Class converterType) {        return true;    }    @Override    public Object beforeBodyWrite(Object body, @Nullable MethodParameter returnType,                                  @Nullable MediaType selectedContentType, @Nullable Class selectedConverterType,                                  @Nullable ServerHttpRequest request, @Nullable ServerHttpResponse response) {        if (body == null) {            return null;        }        if (body instanceof ReturnResult) {            return body;        }        return new ReturnResult<>(body);    }}

    ReturnResultAdvice的beforeBodyWrite() 方法会在handler方法返回值写入response前被调用。

    此时调用DemoController的接口,会发现响应数据结构统一为ReturnResult。

    小节:由@ControllerAdvice注解修饰并实现ResponseBodyAdvice接口的类所实现的beforeBodyWrite()方法会在handler方法返回值写入response前被调用,并且handler方法返回值会作为入参传入beforeBodyWrite(),从而可以在返回值写入response前对返回值进行一些定制操作,例如对返回值进行一层封装。

    二. ResponseBodyAdvice的原理

    首先说明一下为什么第一小节中DemoController的getDefaultDemo1() 方法的返回值类型为ResponseEntity<Demo1>,但是实际往response写的响应体内容为ResponseEntity中的body。首先所有ResponseBodyAdvice接口的调用是发生在AbstractMessageConverterMethodProcessor的writeWithMessageConverters() 方法中,这个方法的声明如下所示。

    protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,            ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)            throws ioException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException;

    其中value就是需要写入响应体的值,同时也是ResponseBodyAdvice要处理的值。然后如果handler方法的返回值是非ResponseEntity对象且handler方法由@ResponseBody注解修饰,那么writeWithMessageConverters() 方法的调用发生在RequestResponseBodyMethodProcessor#handleReturnValue方法中;

    如果handler方法的返回值是ResponseEntity对象,那么writeWithMessageConverters() 方法的调用发生在HttpEntityMethodProcessor#handleReturnValue中,分别看一下在这两个方法中调用writeWithMessageConverters() 时传入的参数,就可以解释之前的疑问了。

    RequestResponseBodyMethodProcessor#handleReturnValue

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,        ModelAndViewContainer mavContainer, NativeWEBRequest webRequest)        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {    ......    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);}

    HttpEntityMethodProcessor#handleReturnValue()

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {    ......    HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue;    ......    writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage);    ......}

    现在正式开始对ResponseBodyAdvice的原理进行分析。

    已知所有ResponseBodyAdvice接口的调用是发生在AbstractMessageConverterMethodProcessor的writeWithMessageConverters() 方法中,其部分源码如下所示。

    protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {    ......    if (selectedMediaType != null) {        selectedMediaType = selectedMediaType.removeQualityValue();        for (HttpMessageConverter<?> converter : this.messageConverters) {            GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?                    (GenericHttpMessageConverter<?>) converter : null);            if (genericConverter != null ?                    ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :                    converter.canWrite(valueType, selectedMediaType)) {                // ResponseBodyAdvice的调用发生在这里                body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,                        (Class<? extends HttpMessageConverter<?>>) converter.getClass(),                        inputMessage, outputMessage);                if (body != null) {                    Object theBody = body;                    LogFORMatUtils.traceDebug(logger, traceOn ->                            "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");                    addContentDispositionHeader(inputMessage, outputMessage);                    if (genericConverter != null) {                        genericConverter.write(body, targetType, selectedMediaType, outputMessage);                    }                    else {                        ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);                    }                }                else {                    if (logger.isDebugEnabled()) {                        logger.debug("Nothing to write: null body");                    }                }                return;            }        }    }    ......}

    AbstractMessageConverterMethodProcessor的getAdvice() 方法会返回其在构造函数中加载好的RequestResponseBodyAdviceChain对象,下面看一下RequestResponseBodyAdviceChain的beforeBodyWrite() 方法。

    public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType,        Class<? extends HttpMessageConverter<?>> converterType,        ServerHttpRequest request, ServerHttpResponse response) {    return processBody(body, returnType, contentType, converterType, request, response);}private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,        Class<? extends HttpMessageConverter<?>> converterType,        ServerHttpRequest request, ServerHttpResponse response) {    // 从加载好的ResponseBodyAdvice中获取适用于当前handler的ResponseBodyAdvice    for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {        if (advice.supports(returnType, converterType)) {            // 执行ResponseBodyAdvice的beforeBodyWrite()方法以处理handler方法返回值            body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,                    contentType, converterType, request, response);        }    }    return body;}private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {    // 获取ResponseBodyAdvice集合    List<Object> availableAdvice = getAdvice(adviceType);    if (CollectionUtils.isEmpty(availableAdvice)) {        return Collections.emptyList();    }    List<A> result = new ArrayList<>(availableAdvice.size());    for (Object advice : availableAdvice) {        // 判断ResponseBodyAdvice是否由@ControllerAdvice注解修饰        if (advice instanceof ControllerAdviceBean) {            ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;            // 判断ResponseBodyAdvice是否适用于当前handler            if (!adviceBean.isApplicableToBeanType(parameter.getContaininGClass())) {                continue;            }            advice = adviceBean.resolveBean();        }        if (adviceType.isAssignableFrom(advice.getClass())) {            result.add((A) advice);        }    }    return result;}

    在RequestResponseBodyAdviceChain中,beforeBodyWrite() 方法调用了processBody() 方法,processBody() 方法会遍历所有加载好并且适用于当前handler的ResponseBodyAdvice并执行,至此,所有由@ControllerAdvice注解修饰的ResponseBodyAdvice接口会在这里执行。

    小节:由@ControllerAdvice注解修饰的ResponseBodyAdvice接口会被springMVC框架加载到RequestResponseBodyMethodProcessorHttpEntityMethodProcessor这两个返回值处理器中,当这两个返回值处理器将返回值写入response前,适用于当前handler的ResponseBodyAdvice接口会被调用,从而可以完成对返回值的定制化改造。

    三. ResponseBodyAdvice的加载

    由第二小节可知,正是因为RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor这两个返回值处理器会将由@ControllerAdvice注解修饰的ResponseBodyAdvice接口加载,才能够实现将返回值写入response前调用这些ResponseBodyAdvice接口对返回值进行一些操作。那么本小节将对esponseBodyAdvice接口的加载进行学习。

    首先给出结论:ResponseBodyAdvice的加载发生在RequestMappingHandlerAdapterafterPropertiesSet()方法中。

    已知,RequestMappingHandlerAdapter实现了InitializingBean接口,因此RequestMappingHandlerAdapter实现了afterPropertiesSet() 方法。该方法实现如下。

    public void afterPropertiesSet() {    // 加载ControllerAdviceBean相关内容(同时就会将由@ControllerAdvice注解修饰的ResponseBodyAdvice接口加载)    initControllerAdviceCache();    if (this.argumentResolvers == null) {        List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);    }    if (this.initBinderArgumentResolvers == null) {        List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();        this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);    }    if (this.returnValueHandlers == null) {        // 获取返回值处理器,在这里就会完成RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor的初始化,初始化的同时就会完成ResponseBodyAdvice接口的加载        List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();        this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);    }}

    上述实现中,initControllerAdviceCache() 方法会加载ControllerAdviceBean相关内容到RequestMappingHandlerAdapter中,这其中就包含由@ControllerAdvice注解修饰的ResponseBodyAdvice接口。然后在getDefaultReturnValueHandlers() 方法中会创建返回值处理器,在创建RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor时会使用加载好的ResponseBodyAdvice接口完成这两个返回值处理器的初始化。上述两个方法的部分源码如下所示。

    initControllerAdviceCache()

    private void initControllerAdviceCache() {    if (getApplicationContext() == null) {        return;    }    // 获取由@ControllerAdvice注解修饰的bean    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());    List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();    for (ControllerAdviceBean adviceBean : adviceBeans) {        Class<?> beanType = adviceBean.getBeanType();        if (beanType == null) {            throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);        }        Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);        if (!attrMethods.isEmpty()) {            this.modelAttributeAdviceCache.put(adviceBean, attrMethods);        }        Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);        if (!binderMethods.isEmpty()) {            this.initBinderAdviceCache.put(adviceBean, binderMethods);        }        // 如果ControllerAdviceBean实现了ResponseBodyAdvice接口,那么这个ControllerAdviceBean需要加载到requestResponseBodyAdvice中        if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {            requestResponseBodyAdviceBeans.add(adviceBean);        }    }    if (!requestResponseBodyAdviceBeans.isEmpty()) {        this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);    }    ......}

    getDefaultReturnValueHandlers()

    private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {    List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);    ......    // 创建并加载HttpEntityMethodProcessor    handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),            this.contentNeGotiationManager, this.requestResponseBodyAdvice));    ...    // 创建并加载RequestResponseBodyMethodProcessor    handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),            this.contentNegotiationManager, this.requestResponseBodyAdvice));    ......    return handlers;}

    根据getDefaultReturnValueHandlers() 方法可知,在创建HttpEntityMethodProcessor或者RequestResponseBodyMethodProcessor时,会将RequestMappingHandlerAdapter加载好的ResponseBodyAdvice传入构造函数,并且,无论是HttpEntityMethodProcessor还是RequestResponseBodyMethodProcessor,其构造函数最终都会调用到父类AbstractMessageConverterMethodArgumentResolver的构造函数,并在其中初始化一个RequestResponseBodyAdviceChain以完成ResponseBodyAdvice的加载。构造函数源码如下所示。

    HttpEntityMethodProcessor#HttpEntityMethodProcessor

    public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,        @Nullable ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {    super(converters, manager, requestResponseBodyAdvice);}

    AbstractMessageConverterMethodProcessor#AbstractMessageConverterMethodProcessor

    protected AbstractMessageConverterMethodProcessor(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters,        @Nullable ContentNegotiationManager manager, @Nullable List&lt;Object&gt; requestResponseBodyAdvice) {    super(converters, requestResponseBodyAdvice);    this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());    this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());    this.safeExtensions.addAll(SAFE_EXTENSIONS);}

    AbstractMessageConverterMethodArgumentResolver#AbstractMessageConverterMethodArgumentResolver

    public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters,        @Nullable List<Object> requestResponseBodyAdvice) {    Assert.notEmpty(converters, "'messageConverters' must not be empty");    this.messageConverters = converters;    this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);    this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);}

    小节:RequestMappingHandlerAdapter会在其实现的afterPropertiesSet()方法中加载由@ControllerAdvice注解修饰的ResponseBodyAdvice接口,然后会创建并加载返回值处理器,在创建RequestResponseBodyMethodProcessorHttpEntityMethodProcessor这两个返回值处理器时会传入加载好的ResponseBodyAdvice,从而完成了ResponseBodyAdvice的加载。

    “ResponseBodyAdvice如何使用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

    --结束END--

    本文标题: ResponseBodyAdvice如何使用

    本文链接: https://lsjlt.com/news/351507.html(转载时请注明来源链接)

    有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

    猜你喜欢
    • ResponseBodyAdvice如何使用
      本篇内容介绍了“ResponseBodyAdvice如何使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!前言ResponseBodyAdv...
      99+
      2023-07-05
    • Spring中ResponseBodyAdvice的使用详解
      目录1 ResponseBodyAdvice的简介2 ResponseBodyAdvice的使用1 准备一个SpringBoot项目环境3 添加一个返回包装类4 添加控制类5 接口测...
      99+
      2024-04-02
    • ResponseBodyAdvice的使用原理源码解析
      目录前言正文一. ResponseBodyAdvice的使用二. ResponseBodyAdvice的原理三. ResponseBodyAdvice的加载总结前言 Respons...
      99+
      2023-03-13
      ResponseBodyAdvice原理 ResponseBodyAdvice源码解析
    • 如何使用redis
      这期内容当中小编将会给大家带来有关如何使用redis ,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。安装redis后,在命令行输入“redis-cli"会车输入...
      99+
      2024-04-02
    • patchnavicat如何使用
      小编给大家分享一下patchnavicat如何使用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!1、把patchNavicat....
      99+
      2024-04-02
    • phpmyadmin如何使用
      这篇文章给大家分享的是有关phpmyadmin如何使用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。创建数据库在 phpMyAdmin 的主界面中,可以看见有 “语言-langu...
      99+
      2024-04-02
    • 如何使用navicat
      小编给大家分享一下如何使用navicat,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!Navicat注册和激活的过程就是Navicat安装的过程,激活过程也比较简便,只要输入Navicat...
      99+
      2024-04-02
    • 如何使用ORM
      本篇内容介绍了“如何使用ORM”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!ObjectvieSQL简介O...
      99+
      2024-04-02
    • autotrace如何使用
      这篇文章给大家分享的是有关autotrace如何使用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。autotrace定义:autotrace是一个可以自动获取SQL执行计划和相关...
      99+
      2024-04-02
    • SqlLoader如何使用
      这篇文章将为大家详细讲解有关SqlLoader如何使用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。SQL*Loader(SQLLDR)是Oracle的高速批量数据加载工...
      99+
      2024-04-02
    • navicat如何使用
      这篇文章将为大家详细讲解有关navicat如何使用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。Navicat可以支持连接多种数据库,使用上的功能也比较强大。如果使用了I...
      99+
      2024-04-02
    • 如何使用DataGrip
      小编给大家分享一下如何使用DataGrip,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!DataGrip是什么DataGrip是...
      99+
      2024-04-02
    • navicat12如何使用
      这篇文章主要介绍navicat12如何使用,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!Navicat 12 具备多项改进和新功能,能满足你对数据库开发的需求。过百种增强的功能和耳目...
      99+
      2024-04-02
    • 如何使用CassandraUnit
      这篇文章主要为大家展示了“如何使用CassandraUnit”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何使用CassandraUnit”这篇文章吧。  C...
      99+
      2024-04-02
    • Hibernate如何使用
      这篇文章主要介绍Hibernate如何使用,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!  hibernate的来源  数据在各个层次之间流转,在流转过程中会发生数据类型转换等一系列...
      99+
      2024-04-02
    • 如何使用NPM
      这篇文章主要为大家展示了“如何使用NPM”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何使用NPM”这篇文章吧。1. 初始化包我们可以运行 npm init命...
      99+
      2024-04-02
    • serialize()如何使用
      这篇文章主要讲解了“serialize()如何使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“serialize()如何使用”吧!   .seriali...
      99+
      2024-04-02
    • div如何使用
      本篇内容主要讲解“div如何使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“div如何使用”吧!<!doctype html> <...
      99+
      2024-04-02
    • callbacks.locked()如何使用
      本文小编为大家详细介绍“callbacks.locked()如何使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“callbacks.locked()如何使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢...
      99+
      2024-04-02
    • 如何使用Vuex
      小编给大家分享一下如何使用Vuex,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!什么是Vuex?vuex是一个专门为vue.js...
      99+
      2024-04-02
    软考高级职称资格查询
    编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
    • 官方手机版

    • 微信公众号

    • 商务合作