完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
|
摘要: 在 2016 年 11 月份的《技术雷达》中,ThoughtWorks 给予了微服务很高的评价。同时,也有越来越多的组织将实施微服务作为架构演进的一个必选方向。只不过在拥有众多遗留系统的组织内,将曾经的单体系统拆分为微服务并不是一件容易的事情。
map.put("key", "new"); }public void outerMethod() { Map map = new HashMap<>(); map.put("key", "old"); System.out.println(map); // {key=old} this.innerMethod(map); System.out.println(map); // {key=new} } 这段代码在同一个进程中运行是没有问题的,因为两个方法共享同一片内存空间,innerMethod 对 map 的修改可以直接反映到 outerMethod 方法中。但是在微服务场景下事实就并非如此了,此时 innerMethod 和 outerMethod 运行在两个独立的进程中,进程间的内存相互隔离,innerMethod修改的内容必须要主动回传才能被 outerMethod 接收到,仅仅修改参数里的值是无法达到回传数据的目的的。此处副作用的概念是指在方法体中对传入参数的内容进行了修改,并由此对外部上下文产生了可察觉的影响。显然副作用是不友好且应该被避免的,但由于是遗留系统,我们不能保证其中不会存在诸如此类写法的代码,所以我们还是需要在微服务改造过程中,对副作用的影响效果进行保持,以获得更好的兼容性。1.7 尽量少改动(最好不改动)遗留系统的内部代码多数情况下,并非所有遗留系统的代码都是可以被平滑改造的:比如,上面提到的方法具有副作用的情况,以及传入和传出参数为不可序列化对象(未实现 Serializable 接口)的情况等。我们虽然不能百分之百保证不对遗留系统的代码进行修改,但至少应该保证这些改动被控制在最小范围内,尽量采取变通的方式——例如添加而不是修改代码——这种仅添加的改造方式至少可以保证代码是向后兼容的。1.8 良好的容错能力不同于进程内调用,跨进程的网络通信可靠性不高,可能由于各种原因而失败。因此在进行微服务改造的时候,远程方法调用需要更多考虑容错能力。当远程方法调用失败的时候,可以进行重试、恢复或者降级,否则不加处理的失败会沿着调用链向上传播(冒泡),从而导致整个系统的级联失败。1.9 改造结果可插拔针对遗留系统的微服务改造不可能保证一次性成功,需要不断尝试和改进,这就要求在一段时间内原有代码与改造后的代码并存,且可以通过一些简单的配置让系统在原有模式和微服务模式之间进行无缝切换。优先尝试微服务模式,一旦出现问题可以快速切换回原有模式(手动或自动),循序渐进,直到微服务模式变得稳定。1.10 更多当然微服务改造的要求远不止上面提到的这些点,还应该包括诸如:配置管理、服务注册与发现、负载均衡、网关、限流降级、扩缩容、监控和分布式事务等,然而这些需求大部分是要在微服务系统已经升级改造完毕,复杂度不断增加,流量上升到一定程度之后才会遇到和需要的,因此并不是本文关注的重点。但这并不意味着这些内容就不重要,没有他们微服务系统同样也是无法正常、平稳、高速运行的。二、模拟一个单体系统2.1 系统概述我们需要构建一个具有三层架构的单体系统来模拟遗留系统,这是一个简单的 Spring Boot 应用,项目名叫做 hello-dubbo。本文涉及到的所有源代码均可以到 Github 上查看和下载。首先,系统存在一个模型 User 和对该模型进行管理的 DAO,并通过 UserService 向上层暴露访问 User 模型的接口;另外,还存在一个 HelloService,其调用 UserService 并返回一条问候信息;之后,由 Controller 对外暴露 RESTful 接口;最终再通过 Spring Boot 的 Application 整合成一个完整应用。2.2 模块化拆分通常来说,一个具有三层架构的单体系统,其 Controller、Service 和 DAO 是存在于一整个模块内的,如果要进行微服务改造,就要先对这个整体进行拆分。拆分的方法是以 Service 层为分界,将其分割为两个子模块:Service 层往上作为一个子模块(称为 hello-web),对外提供 RESTful 接口;Service 层往下作为另外一个子模块(称为 hello-core),包括 Service、DAO 以及模型。hello-core 被 hello-web 依赖。当然,为了更好的体现面向契约的编程精神,可以把 hello-core 再进一步拆分:所有的接口和模型都独立出来,形成 hello-api,而 hello-core 依赖 hello-api。最终,拆分后的模块关系如下:hello-dubbo |-- hello-web(包含 Application 和 Controller)|-- hello-core(包含 Service 和 DAO 的实现) |-- hello-api(包含 Service 和 DAO 的接口以及模型)2.3 核心代码分析2.3.1 Userpublic class User implements Serializable { private String id; private String name; private Date createdTime;public String getId() {return this.id;} public void setId(String id) {this.id = id;}public String getName() {return this.name;} public void setName(String name) {this.name = name;}public Date getCreatedTime() {return this.createdTime;} public void setCreatedTime(Date createdTime) {this.createdTime = createdTime;}@Override public String toString() { } User 模型是一个标准的 POJO,实现了 Serializable 接口(因为模型数据要在网络上传输,因此必须能够支持序列化和反序列化)。为了方便控制台输出,这里覆盖了默认的 toString 方法。2.3.2 UserRepository public interface UserRepository { User getById(String id);void create(User user); } UserRepository 接口是访问 User 模型的 DAO,为了简单起见,该接口只包含两个方法:getById 和 create。2.3.3 InMemoryUserRepository@Repository public class InMemoryUserRepository implements UserRepository { private static final Map STORE = new HashMap<>();static { public User getById(String id) {return STORE.get(id);}@Override public void create(User user) {STORE.put(user.getId(), user);} } InMemoryUserRepository 是 UserRepository 接口的实现类。该类型使用一个 Map 对象 STORE 来存储数据,并通过静态代码块向该对象内添加了一个默认用户。getById 方法根据 id 参数从 STORE 中获取用户数据,而 create 方法就是简单将传入的 user 对象存储到 STORE 中。由于所有这些操作都只是在内存中完成的,因此该类型被叫做 InMemoryUserRepository。2.3.4 UserServicepublic interface UserService { User getById(String id);void create(User user); } 与 UserRepository 的方法一一对应,向更上层暴露访问接口。2.3.5 DefaultUserService@Service("userService") public class DefaultUserService implements UserService { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUserService.class);@Autowired private UserRepository userRepository;@Override public User getById(String id) {}@Override public void create(User user) { } DefaultUserService 是 UserService 接口的默认实现,并通过 @Service 注解声明为一个服务,服务 id 为 userService(该 id 在后面会需要用到)。该服务内部注入了一个 UserRepository 类型的对象 userRepository。getUserById 方法根据 id 从 userRepository 中获取数据,而 createUser 方法则将传入的 user 参数通过 userRepository.create 方法存入,并在存入之前设置了该对象的创建时间。很显然,根据 1.6 节关于副作用的描述,为 user 对象设置创建时间的操作就属于具有副作用的操作,需要在微服务改造之后加以保留。为了方便看到系统工作效果,这两个方法里面都打印了日志。2.3.6 HelloServicepublic interface HelloService { String sayHello(String userId); } HelloService 接口只提供一个方法sayHello,就是根据传入的userId 返回一条对该用户的问候信息。2.3.7 DefaultHelloService@Service("helloService") public class DefaultHelloService implements HelloService { @Autowired private UserService userService;@Override public String sayHello(String userId) { } DefaultHelloService 是 HelloService 接口的默认实现,并通过 @Service 注解声明为一个服务,服务 id 为 helloService(同样,该名称在后面的改造过程中会被用到)。该类型内部注入了一个 UserService 类型的对象 userService。sayHello 方法根据 userId 参数通过 userService 获取用户信息,并返回一条经过格式化后的消息。2.3.8 Application@SpringBootApplication public class Application { public static void main(String[] args) throws Exception {SpringApplication.run(Application.class, args);} } Application 类型是 Spring Boot 应用的入口,详细描述请参考 Spring Boot 的官方文档,在此不详细展开。2.3.9 Controller@RestController public class Controller { @Autowired private HelloService helloService;@Autowired private UserService userService;@RequestMapping("/hello/{userId}") public String sayHello(@PathVariable("userId") String userId) {return this.helloService.sayHello(userId);}@RequestMapping(path = "/create", method = RequestMethod.POST) public String createUser(@RequestParam("userId") String userId, @RequestParam("name") String name) { } Controller 类型是一个标准的 Spring MVC Controller,在此不详细展开讨论。仅仅需要说明的是这个类型注入了 HelloService 和 UserService 类型的对象,并在 sayHello 和 createUser 方法中调用了这两个对象中的有关方法。2.4 打包运行hello-dubbo 项目包含三个子模块:hello-api、hello-core 和 hello-web,是用 Maven 来管理的。到目前为止所涉及到的 POM 文件都比较简单,为了节约篇幅,就不在此一一列出了,感兴趣的朋友可以到项目的 Github 仓库上自行研究。hello-dubbo 项目的打包和运行都非常直接:编译、打包和安装在项目根目录下执行命令$ mvn clean install运行在 hello-web 目录下执行命令$ mvn spring-boot:run |-- hello-service-reference(包含 Dubbo 有关服务引用的配置)|-- hello-api(包含 Service 和 DAO 的接口以及模型)第二个微服务系统:hello-service-provider(包含 Dubbo 有关服务暴露的配置) |-- hello-core(包含 Service 和 DAO 的实现)|-- hello-api(包含 Service 和 DAO 的接口以及模型)hello-web 与原来一样,是一个面向最终用户提供 Web 服务的终端系统,其只包含 Application、Controller、Service 接口、 DAO 接口以及模型,因此它本身是不具备任何业务能力的,必须通过依赖 hello-service-reference 模块来远程调用 hello-service-provider 系统才能完成业务。而 hello-service-provider 系统则需要暴露可供 hello-service-reference 模块调用的远程接口,并实现 Service 及 DAO 接口定义的具体业务逻辑。本章节就是要重点介绍 hello-service-provider 和 hello-service-reference 模块是如何构建的,以及它们在微服务改造过程中所起到的作用。3.2 暴露远程服务Spring Boot 和 Dubbo 的结合使用可以引入诸如 spring-boot-starter-dubbo 这样的起始包,使用起来会更加方便。但是考虑到项目的单纯性和通用性,本文仍然延用 Spring 经典的方式进行配置。首先,我们需要创建一个新的模块,叫做 hello-service-provider,这个模块的作用是用来暴露远程服务接口的。依托于 Dubbo 强大的服务暴露及整合能力,该模块不用编写任何代码,仅需添加一些配置即可完成。注:有关 Dubbo 的具体使用和配置说明并不是本文讨论的重点,请参考官方文档。3.2.1 添加 dubbo-services.xml 文件dubbo-services.xml 配置是该模块的关键,Dubbo 就是根据这个文件,自动暴露远程服务的。这是一个标准 Spring 风格的配置文件,引入了 Dubbo 命名空间,需要将其摆放在 src/main/resources/META-INF/spring 目录下,这样 Maven 在打包的时候会自动将其添加到 classpath。 由于已经在 POM 文件中定义了打包的相关配置,因此直接在 hello-service-provider 目录下运行以下命令即可:$ mvn clean package 成功执行以后,会在其 target 目录下生成一个名为 hello-service-provider-0.1.0-SNAPSHOT-assembly.tar.gz 的压缩包,里面的内容如图所示: 注:在 macOS 系统里,使用 multicast 机制进行服务注册与发现,需要添加-Djava.net.preferIPv4Stack=true 参数,否则会抛出异常。可以使用如下命令来判断服务是否正常运行:$ netstat -antl | grep 20880 如果有类似如下的信息输出,则说明运行正常。如果是在正式环境运行,就需要将上一步生成的压缩包解压,然后运行 bin 目录下的相应脚本即可。3.2.7 总结使用这种方式来暴露远程服务具有如下一些优势:使用 Dubbo 进行远程服务暴露,无需关注底层实现细节对原系统没有任何入侵,已有系统可以继续按照原来的方式启动和运行暴露过程可插拔Dubbo 服务与原有服务在开发期和运行期均可以共存无需编写任何代码3.3 引用远程服务3.3.1 添加服务引用与 hello-service-provider 模块的处理方式相同,为了不侵入原有系统,我们创建另外一个模块,叫做 hello-service-reference。这个模块只有一个配置文件 dubbo-references.xml 放置在 src/main/resources/META-INF/spring/ 目录下。文件的内容非常简单明了: Oops!系统并不能像期望的那样正常运行,会抛出如下异常: Public class Controller { @Autowired private HelloService helloService;@Autowired private UserService userService;... } 显然,helloService 和 userService 都是无法注入的,这是为什么呢?原因自然跟我们修改 hello-web 这个模块的依赖关系有关。原本 hello-web 是依赖于 hello-core的,hello-core 里面声明了 HelloService 和 UserService 这两个服务(通过 @Service 注解),然后 Controller 在 @Autowired 的时候就可以自动绑定了。但是,现在我们将 hello-core替换成了 hello-service-reference,在 hello-service-reference 的配置文件中声明了两个对远程服务的引用,按道理来说这个注入应该是可以生效的,但显然实际情况并非如此。仔细思考不难发现,我们在执行 mvn exec:java 命令启动 hello-service-provider 模块的时候指定了启动 com.alibaba.dubbo.container.Main 类型,然后才会开始启动并加载 Dubbo 的有关配置,这一点从日志中可以得到证实(日志里面会打印出来很多带有 [DUBBO] 标签的内容),显然在这次运行中,我们并没有看到类似这样的日志,说明 Dubbo 在这里没有被正确启动。归根结底还是 Spring Boot 的原因,即 Spring Boot 需要一些配置才能够正确加载和启动 Dubbo。让 Spring Boot 支持 Dubbo 有很多种方法,比如前面提到的 spring-boot-starter-dubbo 起始包,但这里同样为了简单和通用,我们依旧采用经典的方式来解决。继续思考,该模块没有成功启动 Dubbo,仅仅是因为添加了对 hello-service-reference 的引用,而 hello-service-reference 模块就只有一个文件 dubbo-references.xml,这就说明 Spring Boot 并没有加载到这个文件。顺着这个思路,只需要让 Spring Boot 能够成功加载这个文件,问题就可以了。Spring Boot 也确实提供了这样的能力,只可惜无法完全做到代码无侵入,只能说这些改动是可以被接受的。修改方式是替换 Application 中的注解(至于为什么要修改成这样的结果,超出了本文的讨论范围,请自行 Google)。@Configuration @EnableAutoConfiguration @ComponentScan @ImportResource("classpath:META-INF/spring/dubbo-references.xml") public class Application { public static void main(String[] args) throws Exception {SpringApplication.run(Application.class, args);} } 这里的主要改动,是将一个 @SpringBootApplication 注解替换为 @Configuration、@EnableAutoConfiguration、@ComponentScan 和 @ImportResource 四个注解。不难看出,最后一个 @ImportResource 就是我们需要的。这时再重新尝试启动,就一切正常了。 ...// 为方法添加返回值 User create(User user); } 然后,修改实现类中相应的方法,将变更后的 user 对象返回:@Service("userService") public class DefaultUserService implements UserService { ...@Override public User create(User user) { } 最后,修改调用方实现,接收返回值:@RestController public class Controller { ...@RequestMapping(path = "/create", method = RequestMethod.POST) public String createUser(@RequestParam("userId") String userId, @RequestParam("name") String name) { } 编译、运行并测试(如下图),正如我们所期望的,括号中的创建时间又回来了。其工作原理与本节开始时所描述的是一样的,只是方向相反而已。在此不再详细展开,留给大家自行思考。 ...// 保持原有方法不变 void create(User user);// 添加一个方法,新方法需要有返回值 User __rpc_create(User user); } 然后,在实现类中实现这个新方法:@Service("userService") public class DefaultUserService implements UserService { ...// 保持原有方法实现不变 @Override public void create(User user) { @Override public User __rpc_create(User user) { } 有一点需要展开解释:在 __rpc_create 方法中,因为 user 参数是以引用的方式传递给 create方法的,因此 create 方法对参数所做的修改是能够被 __rpc_create 方法获取到的。这以后就与前面回传的逻辑是相同的了。第三,在服务引用端添加本地存根(有关本地存根的概念及用法,请参考官方文档)。需要在 hello-service-reference 模块中添加一个类 UserServiceStub,内容如下:public class UserServiceStub implements UserService { private UserService userService;public UserServiceStub(UserService userService) {this.userService = userService;}@Override public User getById(String id) {return this.userService.getById(id);}@Override public void create(User user) {User newUser = this.__rpc_create(user); user.setCreatedTime(newUser.getCreatedTime());}@Override public User __rpc_create(User user) {return this.userService.__rpc_create(user);} } 该类型即为本地存根。简单来说,就是在调用方调用本地代理的方法之前,会先去调用本地存根中相应的方法,因此本地存根与服务提供方和服务引用方需要实现同样的接口。本地存根中的构造函数是必须的,且方法签名也是被约定好的——需要传入本地代理作为参数。其中 getById 和 __rpc_create 方法都是直接调用了本地代理中的方法,不必过多关注,重点来说说 create 方法。首先,create 调用了本地存根中的 __rpc_create 方法,这个方法透过本地代理访问到了服务提供方的相应方法,并成功接收了返回值 newUser,这个返回值是包含修改后的 createdTime 字段的,于是我们要做的事情就是从 newUser 对象里面获取到 createdTime 字段的值,并设置给 user 参数,以达到产生副作用的效果。此时 user 参数会带着新设置的 createdTime 的值,将其“传递”给 create 方法的调用方。最后,在 dubbo-references.xml 文件中修改一处配置,以启用该本地存根: interface="net.tangrui.demo.dubbo.hello.service.UserService" version="1.0" stub="net.tangrui.demo.dubbo.hello.service.stub.UserServiceStub" /> 鉴于本地存根的工作机制,我们是不需要修改调用方 hello-web 模块中的任何代码及配置的。编译、运行并测试,同样可以达到我们想要的效果。 |
|
相关推荐 |
|
BP86211D 12V/0.5A家用电器方案DEMO资料分析(电路原理图及BOM)
1471 浏览 0 评论
PD诱骗取电芯片_PD_Sink端芯片之XSP05实战应用电路
2496 浏览 1 评论
BLDC、PMSM电机智能栅极驱动芯片之TMC6140知识分享
1251 浏览 0 评论
国产电源芯片DP4054 软硬件兼容TP4054 规格书资料
1708 浏览 0 评论
3512 浏览 3 评论
/9
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2025-12-2 03:49 , Processed in 0.687024 second(s), Total 37, Slave 27 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191

淘帖
1204