注解驱动(Annotation-Driven)

@DubboComponentScan

起始版本: 2.5.7

<dubbo:annotation>历史遗留问题

1. 注解支持不充分

在 Dubbo 2.5.7之前的版本 ,Dubbo 提供了两个核心注解 @Service 以及 @Reference,分别用于Dubbo 服务提供和 Dubbo 服务引用。

其中,@Service 作为 XML 元素 <dubbo:service>的替代注解,与 Spring Framework@org.springframework.stereotype.Service 类似,用于服务提供方 Dubbo 服务暴露。与之相对应的@Reference,则是替代<dubbo:reference 元素,类似于 Spring 中的 @Autowired

2.5.7 之前的Dubbo,与早期的 Spring Framework 2.5 存在类似的不足,即注解支持不够充分。注解需要和 XML 配置文件配合使用,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xmlns="http://www.springframework.org/schema/beans"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsdhttp://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"><dubbo:application name="annotation-provider"/><dubbo:registry address="127.0.0.1:4548"/><dubbo:annotation package="com.alibaba.dubbo.config.spring.annotation.provider"/></beans>

2. @Service Bean 不支持 Spring AOP

同时,使用 <dubbo:annotation> 方式扫描后的Dubbo @Service ,在 Spring 代理方面存在问题,如 GitHub 上的 issue https://github.com/alibaba/dubbo/issues/794:

关于dubbo @Service注解生成ServiceBean时, interface获取成spring 的代理对象的bug

在项目里, 我使用了

@Service
@Transactional
@com.alibaba.dubbo.config.annotation.Service
public class SUserJpushServiceImp

的形式, 来暴露服务。但是在发布服务的时候, interface class 是通过serviceConfig.setInterface(bean.getClass().getInterfaces()[0]); 的形式获取, 刚好, 我的service都使用了@Transactional注解, 对象被代理了。所以获取到的interface是Spring的代理接口...

不少热心的小伙伴不仅发现这个历史遗留问题,而且提出了一些修复方案。同时,为了更好地适配 Spring 生命周期以及将 Dubbo 完全向注解驱动编程模型过渡,因此,引入了全新 Dubbo 组件扫描注解 - @DubboComponentScan

注: <dubbo:annotation> Spring AOP 问题将在 2.5.9 中修复:https://github.com/alibaba/dubbo/issues/1125

3. @Reference 不支持字段继承性

假设有一个 Spring Bean AnnotationAction 直接通过字段annotationService 标记 @Reference 引用AnnotationService :

package com.alibaba.dubbo.examples.annotation.action;import com.alibaba.dubbo.config.annotation.Reference;
import com.alibaba.dubbo.examples.annotation.api.AnnotationService;
import org.springframework.stereotype.Component;@Component("annotationAction")
public class AnnotationAction {@Referenceprivate AnnotationService annotationService;public String doSayHello(String name) {return annotationService.sayHello(name);}}

AnnotationAction 被 XML 元素 <dubbo:annotation> 扫描后:

<dubbo:annotation package="com.alibaba.dubbo.examples.annotation.action"/>

字段 annotationService 能够引用到 AnnotationService,执行 doSayHello 方法能够正常返回。

如果将字段annotationService 抽取到AnnotationAction 的父类BaseAction 后,AnnotationService 无法再被引用,改造如下所示:

AnnotationAction.java

@Component("annotationAction")
public class AnnotationAction extends BaseAction {public String doSayHello(String name) {return getAnnotationService().sayHello(name);}}

BaseAction.java

public abstract class BaseAction {@Referenceprivate AnnotationService annotationService;protected AnnotationService getAnnotationService() {return annotationService;}
}

改造后,再次执行 doSayHello 方法,NullPointerException 将会被抛出。说明<dubbo:annotation> 并不支持@Reference 字段继承性。

了解了历史问题,集合整体愿景,下面介绍@DubboComponentScan 的设计原则。

设计原则

Spring Framework 3.1 引入了新 Annotation - @ComponentScan , 完全替代了 XML 元素<context:component-scan> 。同样, @DubboComponentScan 作为 Dubbo 2.5.7 新增的 Annotation,也是XML 元素 <dubbo:annotation> 的替代方案。

在命名上(类名以及属性方法),为了简化使用和关联记忆,Dubbo 组件扫描 Annotation@DubboComponentScan,借鉴了 Spring Boot 1.3 引入的 @ServletComponentScan。定义如下:

public @interface DubboComponentScan {/*** Alias for the {@link #basePackages()} attribute. Allows for more concise annotation* declarations e.g.: {@code @DubboComponentScan("org.my.pkg")} instead of* {@code @DubboComponentScan(basePackages="org.my.pkg")}.** @return the base packages to scan*/String[] value() default {};/*** Base packages to scan for annotated @Service classes. {@link #value()} is an* alias for (and mutually exclusive with) this attribute.* <p>* Use {@link #basePackageClasses()} for a type-safe alternative to String-based* package names.** @return the base packages to scan*/String[] basePackages() default {};/*** Type-safe alternative to {@link #basePackages()} for specifying the packages to* scan for annotated @Service classes. The package of each class specified will be* scanned.** @return classes from the base packages to scan*/Class<?>[] basePackageClasses() default {};}

注意:basePackages() 和 value() 均能支持占位符(placeholder)指定的包名

在职责上,@DubboComponentScan 相对于 Spring Boot @ServletComponentScan 更为繁重,原因在于处理 Dubbo @Service 类暴露 Dubbo 服务外,还有帮助 Spring Bean @Reference字段或者方法注入 Dubbo 服务代理。

在场景上,Spring Framework @ComponentScan 组件扫描逻辑更为复杂。而在 @DubboComponentScan 只需关注 @Service 和 @Reference 处理。

在功能上, @DubboComponentScan 不但需要提供完整 Spring AOP 支持的能力,而且还得具备@Reference 字段可继承性的能力。

了解基本设计原则后,下面通过完整的示例,简介@DubboComponentScan 使用方法以及注意事项。

使用方法

后续通过服务提供方(@Serivce)以及服务消费方(@Reference)两部分来介绍@DubboComponentScan使用方法。

假设,服务提供方和服务消费分均依赖服务接口DemoService:

package com.alibaba.dubbo.demo;public interface DemoService {String sayHello(String name);}

服务提供方(@Serivce

实现 DemoService

服务提供方实现DemoService - AnnotationDemoService ,同时标注 Dubbo @Service :

package com.alibaba.dubbo.demo.provider;import com.alibaba.dubbo.config.annotation.Service;
import com.alibaba.dubbo.demo.DemoService;/*** Annotation {@link DemoService} 实现** @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>*/
@Service
public class AnnotationDemoService implements DemoService {@Overridepublic String sayHello(String name) {return "Hello , " + name;}}

服务提供方 Annotation 配置

将 AnnotationDemoService 暴露成Dubbo 服务,需要依赖 Spring Bean:AplicationConfigProtocolConfig 以及 RegistryConfig 。这三个 Spring Bean 过去可通过 XML 文件方式组装 Spring Bean:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xmlns="http://www.springframework.org/schema/beans"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsdhttp://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"><!-- 当前应用信息配置 --><dubbo:application name="dubbo-annotation-provider"/><!-- 连接注册中心配置 --><dubbo:registry id="my-registry" address="N/A"/><dubbo:protocol name="dubbo" port="12345"/></beans>

以上装配方式不予推荐,推荐使用 Annotation 配置,因此可以换成 Spring @Configuration Bean 的形式:

package com.alibaba.dubbo.demo.config;import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 服务提供方配置** @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>*/
@Configuration
@DubboComponentScan("com.alibaba.dubbo.demo.provider") // 扫描 Dubbo 组件
public class ProviderConfiguration {/*** 当前应用配置*/@Bean("dubbo-annotation-provider")public ApplicationConfig applicationConfig() {ApplicationConfig applicationConfig = new ApplicationConfig();applicationConfig.setName("dubbo-annotation-provider");return applicationConfig;}/*** 当前连接注册中心配置*/@Bean("my-registry")public RegistryConfig registryConfig() {RegistryConfig registryConfig = new RegistryConfig();registryConfig.setAddress("N/A");return registryConfig;}/*** 当前连接注册中心配置*/@Bean("dubbo")public ProtocolConfig protocolConfig() {ProtocolConfig protocolConfig = new ProtocolConfig();protocolConfig.setName("dubbo");protocolConfig.setPort(12345);return protocolConfig;}
}

服务提供方引导类

package com.alibaba.dubbo.demo.bootstrap;import com.alibaba.dubbo.demo.DemoService;
import com.alibaba.dubbo.demo.config.ProviderConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;/*** 服务提供方引导类** @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>*/
public class ProviderBootstrap {public static void main(String[] args) {// 创建 Annotation 配置上下文AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();// 注册配置 Beancontext.register(ProviderConfiguration.class);// 启动上下文context.refresh();// 获取 DemoService BeanDemoService demoService = context.getBean(DemoService.class);// 执行 sayHello 方法String message = demoService.sayHello("World");// 控制台输出信息System.out.println(message);}}

ProviderBootstrap 启动并执行后,控制输出与预期一致:

Hello , World

以上直接结果说明 @DubboComponentScan("com.alibaba.dubbo.demo.provider") 扫描后,标注 Dubbo@Service 的 AnnotationDemoService 被注册成 Spring Bean,可从 Spring ApplicationContext 自由获取。

服务消费方(@Reference

服务 DemoService

package com.alibaba.dubbo.demo.consumer;import com.alibaba.dubbo.config.annotation.Reference;
import com.alibaba.dubbo.demo.DemoService;/*** Annotation 驱动 {@link DemoService} 消费方** @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>*/
public class AnnotationDemoServiceConsumer {@Reference(url = "dubbo://127.0.0.1:12345")private DemoService demoService;public String doSayHell(String name) {return demoService.sayHello(name);}
}

服务消费方 Annotation 配置

与服务提供方配置类似,服务消费方也许 Dubbo 相关配置 Bean - ConsumerConfiguration

package com.alibaba.dubbo.demo.config;import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan;
import com.alibaba.dubbo.demo.consumer.AnnotationDemoServiceConsumer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 服务消费方配置** @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>*/
@Configuration
@DubboComponentScan
public class ConsumerConfiguration {/*** 当前应用配置*/@Beanpublic ApplicationConfig applicationConfig() {ApplicationConfig applicationConfig = new ApplicationConfig();applicationConfig.setName("dubbo-annotation-consumer");return applicationConfig;}/*** 当前连接注册中心配置*/@Beanpublic RegistryConfig registryConfig() {RegistryConfig registryConfig = new RegistryConfig();registryConfig.setAddress("N/A");return registryConfig;}/*** 注册 AnnotationDemoServiceConsumer,@DubboComponentScan 将处理其中 @Reference 字段。* 如果 AnnotationDemoServiceConsumer 非 Spring Bean 的话,* 即使 @DubboComponentScan 指定 package 也不会进行处理,与 Spring @Autowired 同理*/@Beanpublic AnnotationDemoServiceConsumer annotationDemoServiceConsumer() {return new AnnotationDemoServiceConsumer();}}

服务消费方引导类

服务消费方需要先引导服务提供方,下面的实例将会启动两个 Spring 应用上下文,首先引导服务提供方 Spring 应用上下文,同时,需要复用前面Annotation 配置 ProviderConfiguration

    /*** 启动服务提供方上下文*/private static void startProviderContext() {// 创建 Annotation 配置上下文AnnotationConfigApplicationContext providerContext = new AnnotationConfigApplicationContext();// 注册配置 BeanproviderContext.register(ProviderConfiguration.class);// 启动服务提供方上下文providerContext.refresh();}

然后引导服务消费方Spring 应用上下文:

    /*** 启动并且返回服务消费方上下文** @return AnnotationConfigApplicationContext*/private static ApplicationContext startConsumerContext() {// 创建服务消费方 Annotation 配置上下文AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext();// 注册服务消费方配置 BeanconsumerContext.register(ConsumerConfiguration.class);// 启动服务消费方上下文consumerContext.refresh();// 返回服务消费方 Annotation 配置上下文return consumerContext;}

完整的引导类实现:

package com.alibaba.dubbo.demo.bootstrap;import com.alibaba.dubbo.demo.config.ConsumerConfiguration;
import com.alibaba.dubbo.demo.config.ProviderConfiguration;
import com.alibaba.dubbo.demo.consumer.AnnotationDemoServiceConsumer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;/*** 服务消费端引导类** @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>*/
public class ConsumerBootstrap {public static void main(String[] args) {// 启动服务提供方上下文startProviderContext();// 启动并且返回服务消费方上下文ApplicationContext consumerContext = startConsumerContext();// 获取 AnnotationDemoServiceConsumer BeanAnnotationDemoServiceConsumer consumer = consumerContext.getBean(AnnotationDemoServiceConsumer.class);// 执行 doSayHello 方法String message = consumer.doSayHello("World");// 输出执行结果System.out.println(message);}/*** 启动并且返回服务消费方上下文** @return AnnotationConfigApplicationContext*/private static ApplicationContext startConsumerContext() {// 创建服务消费方 Annotation 配置上下文AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext();// 注册服务消费方配置 BeanconsumerContext.register(ConsumerConfiguration.class);// 启动服务消费方上下文consumerContext.refresh();// 返回服务消费方 Annotation 配置上下文return consumerContext;}/*** 启动服务提供方上下文*/private static void startProviderContext() {// 创建 Annotation 配置上下文AnnotationConfigApplicationContext providerContext = new AnnotationConfigApplicationContext();// 注册配置 BeanproviderContext.register(ProviderConfiguration.class);// 启动服务提供方上下文providerContext.refresh();}}

运行ConsumerBootstrap结果,仍然符合期望,AnnotationDemoServiceConsumer 输出:

Hello , World

Spring AOP 支持

前面提到 <dubbo:annotation> 注册 Dubbo @Service 组件后,在 Spring AOP 支持方面存在问题。事务作为 Spring AOP 的功能扩展,自然也会在 <dubbo:annotation>中不支持。

@DubboComponentScan 针对以上问题,实现了对 Spring AOP 是完全兼容。将上述服务提供方 Annotation 配置做出一定的调整,标注@EnableTransactionManagement 以及自定义实现PlatformTransactionManager:

@Configuration
@DubboComponentScan("com.alibaba.dubbo.demo.provider") // 扫描 Dubbo 组件
@EnableTransactionManagement // 激活事务管理
public class ProviderConfiguration {// 省略其他配置 Bean 定义/*** 自定义事务管理器*/@Bean@Primarypublic PlatformTransactionManager transactionManager() {return new PlatformTransactionManager() {@Overridepublic TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {System.out.println("get transaction ...");return new SimpleTransactionStatus();}@Overridepublic void commit(TransactionStatus status) throws TransactionException {System.out.println("commit transaction ...");}@Overridepublic void rollback(TransactionStatus status) throws TransactionException {System.out.println("rollback transaction ...");}};}
}

同时调整 AnnotationDemoService - 增加@Transactional 注解:

@Service
@Transactional
public class AnnotationDemoService implements DemoService {// 省略实现,保持不变
}

再次运行ConsumerBootstrap , 观察控制台输出内容:

get transaction ...
commit transaction ...
Hello , World

输入内容中多处了两行,说明自定义 PlatformTransactionManagergetTransaction(TransactionDefinition) 以及 commit(TransactionStatus) 方法被执行,进而说明AnnotationDemoService 的sayHello(String) 方法执行时,事务也伴随执行。

注意事项

ConsumerConfiguration 上的 @DubboComponentScan 并没有指定 basePackages 扫描,这种情况会将ConsumerConfiguration 当做 basePackageClasses ,即扫描ConsumerConfiguration 所属的 packagecom.alibaba.dubbo.demo.config 以及子 package。由于当前示例中,不存在标注 Dubbo @Service的类,因此在运行时日志(如果开启的话)会输出警告信息:

WARN :  [DUBBO] No Spring Bean annotating Dubbo's @Service was found in Spring BeanFactory, dubbo version: 2.0.0, current host: 127.0.0.1

以上信息大可不必担忧,因为 @DubboComponentScan 除了扫描 Dubbo @Service 组件以外,还将处理@Reference字段注入。然而读者特别关注@Reference字段注入的规则。

以上实现为例,AnnotationDemoServiceConsumer 必须申明为 Spring @Bean 或者 @Component(或者其派生注解),否则 @DubboComponentScan 不会主动将标注 @Reference字段所在的声明类提成为 Spring Bean,换句话说,如果 @Reference字段所在的声明类不是 Spring Bean 的话, @DubboComponentScan 不会处理@Reference注入,其原理与 Spring @Autowired 一致。

以上使用不当可能会导致相关问题,如 GitHub 上曾有小伙伴提问:https://github.com/alibaba/dubbo/issues/825

li362692680 提问:

@DubboComponentScan注解在消费端扫描包时扫描的是 @Service注解??不是@Reference注解?? 启动时报 DubboComponentScanRegistrar-85]-[main]-[INFO] 0 annotated @Service Components { [] }

笔者(mercyblitz)回复:

@Reference 类似于 @Autowired 一样,首先其申明的类必须被 Spring 上下文当做一个Bean,因此,Dubbo 并没有直接将 @Reference 字段所在的类提升成 Bean。

综上所述,这并不是一个问题,而是用法不当!

已知问题

最新发布的 Dubbo 2.5.8 中,@DubboComponentScan 在以下特殊场景下存在 Spring @Service 不兼容情况:

假设有两个服务实现类 A 和 B,同时存放在com.acme 包下:

  • A 标注 Dubbo @Service
  • B 标注 Dubbo @Service 和 Spring @Service

当 Spring @ComponentScan 先扫描com.acme 包时,B 被当做 Spring Bean 的候选类。随后,@DubboComponentScan 也扫描相同的包。当应用启动时,A 和 B 虽然都是 Spring Bean,可仅A 能够暴露 Dubbo 服务,B 则丢失。

问题版本:2.5.72.5.8

问题详情:https://github.com/alibaba/dubbo/issues/1120

修复版本:2.5.9(下个版本)

原文:http://dubbo.apache.org/zh-cn/blog/dubbo-annotation-driven.html

Dubbo 注解驱动(Annotation-Driven)相关推荐

  1. 【Spring注解驱动开发】二狗子让我给他讲讲@EnableAspectJAutoProxy注解

    写在前面 最近,二狗子入职了新公司,新入职的那几天确实有点飘.不过慢慢的,他发现他身边的人各个身怀绝技啊,有Spring源码的贡献者,有Dubbo源码的贡献者,有MyBatis源码的贡献者,还有研究A ...

  2. Spring Boot 基于注解驱动源码分析--自动配置

    Spring作为Java开发最常用的容器管理框架,使用注解为我们提供很多便捷,下面通过源码分析Spring基于注解驱动自动配置的原理 首先介绍两个关键类: ConfigurationClassPost ...

  3. SSH框架搭建 笔记 (含spring注解驱动)

    分类: web 开发2014-04-27 12:33 354人阅读 评论(0) 收藏 举报 框架springinterface注解 好久没有搭建框架了,今天整理下以前的知识,整合下SSH,没想到手生了 ...

  4. Spring 2.5 基于注解驱动的 Spring MVC

    基于注解的配置有越来越流行的趋势,Spring 2.5 顺应这种趋势,为 Spring MVC 提供了完全基于注解的配置.本文将介绍 Spring 2.5 新增的 Sping MVC 注解功能,讲述如 ...

  5. 使用 Spring 2.5 基于注解驱动的 Spring MVC--转

    概述 继 Spring 2.0 对 Spring MVC 进行重大升级后,Spring 2.5 又为 Spring MVC 引入了注解驱动功能.现在你无须让 Controller 继承任何接口,无需在 ...

  6. SpringMVC注解驱动标签做了什么操作

    怎样开启注解驱动 SpringMVC开启注解驱动 <!-- mvc的注解驱动 --><mvc:annotation-driven/> <mvc:annotation-dr ...

  7. dubbo学习(四)配置dubbo 注解方式配置

    provider service注解暴露服务 @Service public class AnnotationServiceImpl implements AnnotationService {@Ov ...

  8. SpringMVC配置静态资源加载, 中文乱码处理,注解驱动

    常规配置(Controller加载控制) SpringMVC的处理器对应的bean必须按照规范格式开发,未避免加入无效的bean可通过bean加载过滤器进行包含设定或排除设定,表现层bean标注通常设 ...

  9. SpringMVC注解驱动开发

    前言 此文章是对SpringMVC注解开发的demo配置以及通过Debug对启动流程做一个大概的分析. 介绍 通过SpringMVC注解驱动开发,我们就无需使用web.xml.springmvc配置文 ...

最新文章

  1. 深度学习最近发现详细分析报告
  2. boost 库 enable_shared_from_this 实现原理分析
  3. 2015年百度一面试题
  4. dubbo web工程示例_带有Dubbo的Spring Cloud Alibaba
  5. CodeForces - 1321D Navigation S.ystem(最短路+思维)
  6. C++并发编程实战(豆瓣评分5.4)
  7. C语言(CED)递归实现汉诺塔问题
  8. mysql not in 性能_SQL中Execpt和not in 性能区别
  9. 中班音乐计算机反思,中班音乐游戏打字机教案反思
  10. .Net Core Win2008R2 运行环境问题 502.5
  11. 谷歌地图 地点搜索(模糊搜索)
  12. 什么牌子的降噪耳机好用又实惠?平价好用的蓝牙耳机推荐
  13. CRC-16/CCITT算法实现
  14. python实现数据库的增删改查
  15. 程序员常用的16款火爆软件,你get到了哪些软件?
  16. 怎样的婚恋网站,才能符合单身的交友信息呢。
  17. doraemon的python tcp协议和udp协议
  18. iphone11各机型对比_iPhone 11系列手机买哪款好?iPhone 11系列对比评测
  19. MediaPlayer代码分析(1)-初始化和设置数据的过程
  20. MySQL最全整理(面试题+笔记+导图),面试大厂不再被MySql难倒!

热门文章

  1. 【Python基础】盘点 Python 10 大常用数据结构(上篇)
  2. 转发:Datawhale第七期组队学习计划
  3. 网易云信又双叒受到Gartner关注,看看这次的报告说了什么?
  4. 技术实践 | 网易云信 QUIC 加速服务架构与实践
  5. 3年营收翻8倍,网易智慧企业业务多品牌战略升级
  6. 黑客变身夜 | 程序员名画Cosplay指南
  7. 5.34. PECL FAQ
  8. 监控 SQL Server 的运行状况
  9. 2018秋招面经:斗鱼、滴滴、百度、美团、小米、腾讯
  10. 《Netty 实战》Netty In Action中文版 第2章——你的第一款Netty应用程序(一)