SpringCloud入门

什么是微服务框架?与单体系统的区别如何实施微服务为什么选择SpringCloud?SpringCloud简介版本说明

May 19, 2019 · 1 min · jiezi

springcloud二springcloudalibaba集成sentinel入门

Sentinel 介绍随着微服务的流行,服务和服务之间的稳定性变得越来越重要。 Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 Sentinel 具有以下特征: 丰富的应用场景: Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。<!--more--> springcloud如何使用 Sentinel第一步:引入pom <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>0.2.1.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>第二步:新建一个启动类和controller @SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}@RestControllerpublic class TestController { @GetMapping(value = "/hello") @SentinelResource("hello") public String hello() { return "Hello Sentinel"; }}第三步:引入dashboard可以直接下载sentinel-dashboard的jar包,也可以自己编译,我这边这里clone了代码自己编译,代码地址:https://github.com/alibaba/Se...,执行命令 ...

May 11, 2019 · 1 min · jiezi

Spring-Cloud-Gateway-VS-Zuul-比较怎么选择

Spring Cloud Gateway 是 Spring Cloud Finchley 版推出来的新组件,用来代替服务网关:Zuul。 那 Spring Cloud Gateway 和 Zuul 都有哪些区别呢,咱们来比较一下。 1、开源组织 Spring Cloud Gateway 是 Spring Cloud 微服务平台的一个子项目,属于 Spring 开源社区,依赖名叫:spring-cloud-starter-gateway。 https://spring.io/projects/sp...Zuul 是 Netflix 公司的开源项目,Spring Cloud 在 Netflix 项目中也已经集成了 Zuul,依赖名叫:spring-cloud-starter-netflix-zuul。 https://github.com/Netflix/zuul2、底层实现 https://stackoverflow.com/que...据 Spring Cloud Gateway 原作者的解释: Zuul构建于 Servlet 2.5,兼容 3.x,使用的是阻塞式的 API,不支持长连接,比如 websockets。另外 Spring Cloud Gateway构建于 Spring 5+,基于 Spring Boot 2.x 响应式的、非阻塞式的 API。同时,它支持 websockets,和 Spring 框架紧密集成,开发体验相对来说十分不错。 3、性能表现 这个没什么好比的,要比就和 Zuul 2.x 比,Zuul 2.x 在底层上有了很大的改变,使用了异步无阻塞式的 API,性能改善明显,不过现在 Spring Cloud 也没集成 Zuul 2.x,所以就没什么好比的。 ...

May 10, 2019 · 1 min · jiezi

微服务熔断限流Hystrix之流聚合

简介上一篇介绍了 Hystrix Dashboard 监控单体应用的例子,在生产环境中,监控的应用往往是一个集群,我们需要将每个实例的监控信息聚合起来分析,这就用到了 Turbine 工具。Turbine有一个重要的功能就是汇聚监控信息,并将汇聚到的监控信息提供给Hystrix Dashboard来集中展示和监控。 流程 实验工程说明工程名端口作用eureka-server8761注册中心service-hi8762服务提供者service-consumer8763服务消费者service-turbine8765Turbine服务核心代码eureka-server 、service-hi、service-consumer 工程代码与上一节 微服务熔断限流Hystrix之Dashboard 相同,下面是 service-turbine 工程的核心代码。 pom.xml<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-turbine</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId></dependency>application.ymlserver: port: 8765spring: application: name: service-turbineeureka: client: service-url: defaultZone: http://localhost:8761/eureka/turbine: app-config: service-consumer cluster-name-expression: new String("default") combine-host-port: true参数说明: turbine.app-config:指定要监控的应用名turbine.cluster-name-expression:指定集群的名字turbine.combine-host-port:表示同一主机上的服务通过host和port的组合来进行区分,默认情况下是使用host来区分,这样会使本地调试有问题启动类@SpringBootApplication@EnableEurekaClient@EnableHystrixDashboard@EnableTurbinepublic class ServiceTurbineApplication { public static void main(String[] args) { SpringApplication.run( ServiceTurbineApplication.class, args ); }}模拟多实例启动多个 service-consumer 工程,来模拟多实例,可以通过命令java -jar service-consumer.jar --server.port=XXXX 来实现。 为了方便,在编辑器中实现启动工程。但 idea 不支持单个应用的多次启动, 需要开启并行启动: ...

May 9, 2019 · 1 min · jiezi

微服务熔断限流Hystrix之Dashboard

简介Hystrix Dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix Dashboard可以直观地看到各Hystrix Command的请求响应时间,请求成功率等数据。 快速上手工程说明工程名端口作用eureka-server8761注册中心service-hi8762服务提供者service-consumer8763服务消费者核心代码eureka-server 工程pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>application.ymlserver: port: 8761eureka: instance: hostname: localhost client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://${eureka.instance.hostname}:/${server.port}/eureka/spring: application: name: eureka-server启动类@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run( EurekaServerApplication.class, args ); }}service-hi 工程pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>application.ymlserver: port: 8762spring: application: name: service-hieureka: client: service-url: defaultZone: http://localhost:8761/eureka/HelloController@RestControllerpublic class HelloController { @GetMapping("/hi") public String hi() { return "hello ~"; } @GetMapping("/hey") public String hey() { return "hey ~"; } @GetMapping("/oh") public String oh() { return "ah ~"; } @GetMapping("/ah") public String ah() { //模拟接口1/3的概率超时 Random rand = new Random(); int randomNum = rand.nextInt(3) + 1; if (3 == randomNum) { try { Thread.sleep( 3000 ); } catch (InterruptedException e) { e.printStackTrace(); } } return "来了老弟~"; }}启动类@SpringBootApplication@EnableEurekaClientpublic class ServiceHiApplication { public static void main(String[] args) { SpringApplication.run( ServiceHiApplication.class, args ); }}service-consumer 工程pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId></dependency>application.ymlserver: port: 8763 tomcat: uri-encoding: UTF-8 max-threads: 1000 max-connections: 20000spring: application: name: service-consumereureka: client: service-url: defaultZone: http://localhost:8761/eureka/management: endpoints: web: exposure: include: "*" cors: allowed-origins: "*" allowed-methods: "*"HelloService@Servicepublic class HelloService { @Autowired private RestTemplate restTemplate; /** * 简单用法 */ @HystrixCommand public String hiService() { return restTemplate.getForObject("http://SERVICE-HI/hi" , String.class); } /** * 定制超时 */ @HystrixCommand(commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "30000") }) public String heyService() { return restTemplate.getForObject("http://SERVICE-HI/hey" , String.class); } /** * 定制降级方法 */ @HystrixCommand(fallbackMethod = "getFallback") public String ahService() { return restTemplate.getForObject("http://SERVICE-HI/ah" , String.class); } /** * 定制线程池隔离策略 */ @HystrixCommand(fallbackMethod = "getFallback", threadPoolKey = "studentServiceThreadPool", threadPoolProperties = { @HystrixProperty(name="coreSize", value="30"), @HystrixProperty(name="maxQueueSize", value="50") } ) public String ohService() { return restTemplate.getForObject("http://SERVICE-HI/oh" , String.class); } public String getFallback() { return "Oh , sorry , error !"; }}HelloController@RestControllerpublic class HelloController { @Autowired private HelloService helloService; @GetMapping("/hi") public String hi() { return helloService.hiService(); } @GetMapping("/hey") public String hey() { return helloService.heyService(); } @GetMapping("/oh") public String oh() { return helloService.ohService(); } @GetMapping("/ah") public String ah() { return helloService.ahService(); }}启动类@SpringBootApplication@EnableEurekaClient@EnableHystrixDashboard@EnableHystrix@EnableCircuitBreakerpublic class ServiceConsumerApplication { public static void main(String[] args) { SpringApplication.run( ServiceConsumerApplication.class, args ); } @LoadBalanced @Bean public RestTemplate restTemplate() { return new RestTemplate(); }}Hystrix Dashboard 的使用JSON格式监控信息先访问http://localhost:8762/hi 再打开http://localhost:8763/actuator/hystrix.stream,可以看到一些具体的数据: ...

May 6, 2019 · 2 min · jiezi

APOLLO配置中心

MySQLApollo的表结构对timestamp使用了多个default声明,所以需要5.6.5以上版本。Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB ApolloConfigDB: /* Navicat Premium Data Transfer Source Server : mylocal Source Server Type : MySQL Source Server Version : 50724 Source Host : localhost:3306 Source Schema : ApolloConfigDB Target Server Type : MySQL Target Server Version : 50724 File Encoding : 65001 Date: 30/04/2019 14:07:01*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for App-- ----------------------------DROP TABLE IF EXISTS `App`;CREATE TABLE `App` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Name` varchar(500) NOT NULL DEFAULT 'default' COMMENT '应用名', `OrgId` varchar(32) NOT NULL DEFAULT 'default' COMMENT '部门Id', `OrgName` varchar(64) NOT NULL DEFAULT 'default' COMMENT '部门名字', `OwnerName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerName', `OwnerEmail` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerEmail', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId` (`AppId`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Name` (`Name`(191))) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='应用表';-- ------------------------------ Records of App-- ----------------------------BEGIN;INSERT INTO `App` VALUES (1, 'SampleApp', 'Sample App', 'TEST1', '样例部门1', 'apollo', 'apollo@acme.com', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `App` VALUES (2, 'myapp', '测试应用', 'TEST1', '样例部门1', 'apollo', 'apollo@acme.com', b'0', 'apollo', '2019-04-16 13:41:28', 'apollo', '2019-04-16 13:41:28');COMMIT;-- ------------------------------ Table structure for AppNamespace-- ----------------------------DROP TABLE IF EXISTS `AppNamespace`;CREATE TABLE `AppNamespace` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Name` varchar(32) NOT NULL DEFAULT '' COMMENT 'namespace名字,注意,需要全局唯一', `AppId` varchar(32) NOT NULL DEFAULT '' COMMENT 'app id', `Format` varchar(32) NOT NULL DEFAULT 'properties' COMMENT 'namespace的format类型', `IsPublic` bit(1) NOT NULL DEFAULT b'0' COMMENT 'namespace是否为公共', `Comment` varchar(64) NOT NULL DEFAULT '' COMMENT '注释', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_AppId` (`AppId`), KEY `Name_AppId` (`Name`,`AppId`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='应用namespace定义';-- ------------------------------ Records of AppNamespace-- ----------------------------BEGIN;INSERT INTO `AppNamespace` VALUES (1, 'application', 'SampleApp', 'properties', b'0', 'default app namespace', b'0', '', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `AppNamespace` VALUES (2, 'application', 'myapp', 'properties', b'0', 'default app namespace', b'0', 'apollo', '2019-04-16 13:41:28', 'apollo', '2019-04-16 13:41:28');INSERT INTO `AppNamespace` VALUES (3, 'test', 'myapp', 'properties', b'1', '', b'0', 'apollo', '2019-04-16 13:57:05', 'apollo', '2019-04-16 13:57:05');COMMIT;-- ------------------------------ Table structure for Audit-- ----------------------------DROP TABLE IF EXISTS `Audit`;CREATE TABLE `Audit` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `EntityName` varchar(50) NOT NULL DEFAULT 'default' COMMENT '表名', `EntityId` int(10) unsigned DEFAULT NULL COMMENT '记录ID', `OpName` varchar(50) NOT NULL DEFAULT 'default' COMMENT '操作类型', `Comment` varchar(500) DEFAULT NULL COMMENT '备注', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='日志审计表';-- ------------------------------ Records of Audit-- ----------------------------BEGIN;INSERT INTO `Audit` VALUES (1, 'App', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:28', NULL, '2019-04-16 13:41:28');INSERT INTO `Audit` VALUES (2, 'AppNamespace', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:28', NULL, '2019-04-16 13:41:28');INSERT INTO `Audit` VALUES (3, 'Cluster', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:28', NULL, '2019-04-16 13:41:28');INSERT INTO `Audit` VALUES (4, 'Namespace', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:28', NULL, '2019-04-16 13:41:28');INSERT INTO `Audit` VALUES (5, 'Release', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:41', NULL, '2019-04-16 13:41:41');INSERT INTO `Audit` VALUES (6, 'ReleaseHistory', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:41', NULL, '2019-04-16 13:41:41');INSERT INTO `Audit` VALUES (7, 'Item', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:42:16', NULL, '2019-04-16 13:42:16');INSERT INTO `Audit` VALUES (8, 'Release', 3, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:42:23', NULL, '2019-04-16 13:42:23');INSERT INTO `Audit` VALUES (9, 'ReleaseHistory', 3, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:42:23', NULL, '2019-04-16 13:42:23');INSERT INTO `Audit` VALUES (10, 'Namespace', 3, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:57:05', NULL, '2019-04-16 13:57:05');INSERT INTO `Audit` VALUES (11, 'AppNamespace', 3, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:57:05', NULL, '2019-04-16 13:57:05');INSERT INTO `Audit` VALUES (12, 'Release', 4, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:57:30', NULL, '2019-04-16 13:57:30');INSERT INTO `Audit` VALUES (13, 'ReleaseHistory', 4, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:57:30', NULL, '2019-04-16 13:57:30');COMMIT;-- ------------------------------ Table structure for Cluster-- ----------------------------DROP TABLE IF EXISTS `Cluster`;CREATE TABLE `Cluster` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Name` varchar(32) NOT NULL DEFAULT '' COMMENT '集群名字', `AppId` varchar(32) NOT NULL DEFAULT '' COMMENT 'App id', `ParentClusterId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父cluster', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_AppId_Name` (`AppId`,`Name`), KEY `IX_ParentClusterId` (`ParentClusterId`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='集群';-- ------------------------------ Records of Cluster-- ----------------------------BEGIN;INSERT INTO `Cluster` VALUES (1, 'default', 'SampleApp', 0, b'0', '', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `Cluster` VALUES (2, 'default', 'myapp', 0, b'0', 'apollo', '2019-04-16 13:41:28', 'apollo', '2019-04-16 13:41:28');COMMIT;-- ------------------------------ Table structure for Commit-- ----------------------------DROP TABLE IF EXISTS `Commit`;CREATE TABLE `Commit` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `ChangeSets` longtext NOT NULL COMMENT '修改变更集', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `Comment` varchar(500) DEFAULT NULL COMMENT '备注', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `AppId` (`AppId`(191)), KEY `ClusterName` (`ClusterName`(191)), KEY `NamespaceName` (`NamespaceName`(191))) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='commit 历史表';-- ------------------------------ Records of Commit-- ----------------------------BEGIN;INSERT INTO `Commit` VALUES (1, '{\"createItems\":[{\"namespaceId\":2,\"key\":\"spring.test\",\"value\":\"测试apollo\",\"lineNum\":1,\"id\":2,\"isDeleted\":false,\"dataChangeCreatedBy\":\"apollo\",\"dataChangeCreatedTime\":\"2019-04-16 13:42:15\",\"dataChangeLastModifiedBy\":\"apollo\",\"dataChangeLastModifiedTime\":\"2019-04-16 13:42:15\"}],\"updateItems\":[],\"deleteItems\":[]}', 'myapp', 'default', 'application', NULL, b'0', 'apollo', '2019-04-16 13:42:16', 'apollo', '2019-04-16 13:42:16');COMMIT;-- ------------------------------ Table structure for GrayReleaseRule-- ----------------------------DROP TABLE IF EXISTS `GrayReleaseRule`;CREATE TABLE `GrayReleaseRule` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Cluster Name', `NamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Namespace Name', `BranchName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'branch name', `Rules` varchar(16000) DEFAULT '[]' COMMENT '灰度规则', `ReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '灰度对应的release', `BranchStatus` tinyint(2) DEFAULT '1' COMMENT '灰度分支状态: 0:删除分支,1:正在使用的规则 2:全量发布', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Namespace` (`AppId`,`ClusterName`,`NamespaceName`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='灰度规则表';-- ------------------------------ Table structure for Instance-- ----------------------------DROP TABLE IF EXISTS `Instance`;CREATE TABLE `Instance` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `DataCenter` varchar(64) NOT NULL DEFAULT 'default' COMMENT 'Data Center Name', `Ip` varchar(32) NOT NULL DEFAULT '' COMMENT 'instance ip', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), UNIQUE KEY `IX_UNIQUE_KEY` (`AppId`,`ClusterName`,`Ip`,`DataCenter`), KEY `IX_IP` (`Ip`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='使用配置的应用实例';-- ------------------------------ Records of Instance-- ----------------------------BEGIN;INSERT INTO `Instance` VALUES (1, 'myapp', 'default', '', '10.153.111.139', '2019-04-16 14:02:53', '2019-04-16 14:02:53');COMMIT;-- ------------------------------ Table structure for InstanceConfig-- ----------------------------DROP TABLE IF EXISTS `InstanceConfig`;CREATE TABLE `InstanceConfig` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `InstanceId` int(11) unsigned DEFAULT NULL COMMENT 'Instance Id', `ConfigAppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config App Id', `ConfigClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config Cluster Name', `ConfigNamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config Namespace Name', `ReleaseKey` varchar(64) NOT NULL DEFAULT '' COMMENT '发布的Key', `ReleaseDeliveryTime` timestamp NULL DEFAULT NULL COMMENT '配置获取时间', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), UNIQUE KEY `IX_UNIQUE_KEY` (`InstanceId`,`ConfigAppId`,`ConfigNamespaceName`), KEY `IX_ReleaseKey` (`ReleaseKey`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Valid_Namespace` (`ConfigAppId`,`ConfigClusterName`,`ConfigNamespaceName`,`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='应用实例的配置信息';-- ------------------------------ Records of InstanceConfig-- ----------------------------BEGIN;INSERT INTO `InstanceConfig` VALUES (1, 1, 'myapp', 'default', 'application', '20190416134223-7f43754dbb4ca14b', '2019-04-16 14:02:52', '2019-04-16 14:02:52', '2019-04-16 14:02:52');INSERT INTO `InstanceConfig` VALUES (2, 1, 'myapp', 'default', 'test', '20190416135729-be85754dbb4ca14c', '2019-04-16 14:02:52', '2019-04-16 14:02:52', '2019-04-16 14:02:52');COMMIT;-- ------------------------------ Table structure for Item-- ----------------------------DROP TABLE IF EXISTS `Item`;CREATE TABLE `Item` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `NamespaceId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '集群NamespaceId', `Key` varchar(128) NOT NULL DEFAULT 'default' COMMENT '配置项Key', `Value` longtext NOT NULL COMMENT '配置项值', `Comment` varchar(1024) DEFAULT '' COMMENT '注释', `LineNum` int(10) unsigned DEFAULT '0' COMMENT '行号', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_GroupId` (`NamespaceId`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='配置项目';-- ------------------------------ Records of Item-- ----------------------------BEGIN;INSERT INTO `Item` VALUES (1, 1, 'timeout', '100', 'sample timeout配置', 1, b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `Item` VALUES (2, 2, 'spring.test', '测试apollo', NULL, 1, b'0', 'apollo', '2019-04-16 13:42:16', 'apollo', '2019-04-16 13:42:16');COMMIT;-- ------------------------------ Table structure for Namespace-- ----------------------------DROP TABLE IF EXISTS `Namespace`;CREATE TABLE `Namespace` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Cluster Name', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Namespace Name', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId_ClusterName_NamespaceName` (`AppId`(191),`ClusterName`(191),`NamespaceName`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_NamespaceName` (`NamespaceName`(191))) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='命名空间';-- ------------------------------ Records of Namespace-- ----------------------------BEGIN;INSERT INTO `Namespace` VALUES (1, 'SampleApp', 'default', 'application', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `Namespace` VALUES (2, 'myapp', 'default', 'application', b'0', 'apollo', '2019-04-16 13:41:28', 'apollo', '2019-04-16 13:41:28');INSERT INTO `Namespace` VALUES (3, 'myapp', 'default', 'test', b'0', 'apollo', '2019-04-16 13:57:05', 'apollo', '2019-04-16 13:57:05');COMMIT;-- ------------------------------ Table structure for NamespaceLock-- ----------------------------DROP TABLE IF EXISTS `NamespaceLock`;CREATE TABLE `NamespaceLock` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', `NamespaceId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '集群NamespaceId', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT 'default' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', `IsDeleted` bit(1) DEFAULT b'0' COMMENT '软删除', PRIMARY KEY (`Id`), UNIQUE KEY `IX_NamespaceId` (`NamespaceId`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='namespace的编辑锁';-- ------------------------------ Table structure for Release-- ----------------------------DROP TABLE IF EXISTS `Release`;CREATE TABLE `Release` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `ReleaseKey` varchar(64) NOT NULL DEFAULT '' COMMENT '发布的Key', `Name` varchar(64) NOT NULL DEFAULT 'default' COMMENT '发布名字', `Comment` varchar(256) DEFAULT NULL COMMENT '发布说明', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `Configurations` longtext NOT NULL COMMENT '发布配置', `IsAbandoned` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否废弃', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId_ClusterName_GroupName` (`AppId`(191),`ClusterName`(191),`NamespaceName`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_ReleaseKey` (`ReleaseKey`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='发布';-- ------------------------------ Records of Release-- ----------------------------BEGIN;INSERT INTO `Release` VALUES (1, '20161009155425-d3a0749c6e20bc15', '20161009155424-release', 'Sample发布', 'SampleApp', 'default', 'application', '{\"timeout\":\"100\"}', b'0', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `Release` VALUES (2, '20190416134141-7f43754dbb4ca14a', '20190416134137-release', '', 'myapp', 'default', 'application', '{}', b'0', b'0', 'apollo', '2019-04-16 13:41:41', 'apollo', '2019-04-16 13:41:41');INSERT INTO `Release` VALUES (3, '20190416134223-7f43754dbb4ca14b', '20190416134221-release', '', 'myapp', 'default', 'application', '{\"spring.test\":\"测试apollo\"}', b'0', b'0', 'apollo', '2019-04-16 13:42:23', 'apollo', '2019-04-16 13:42:23');INSERT INTO `Release` VALUES (4, '20190416135729-be85754dbb4ca14c', '20190416135728-release', '', 'myapp', 'default', 'test', '{}', b'0', b'0', 'apollo', '2019-04-16 13:57:30', 'apollo', '2019-04-16 13:57:30');COMMIT;-- ------------------------------ Table structure for ReleaseHistory-- ----------------------------DROP TABLE IF EXISTS `ReleaseHistory`;CREATE TABLE `ReleaseHistory` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `BranchName` varchar(32) NOT NULL DEFAULT 'default' COMMENT '发布分支名', `ReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联的Release Id', `PreviousReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '前一次发布的ReleaseId', `Operation` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '发布类型,0: 普通发布,1: 回滚,2: 灰度发布,3: 灰度规则更新,4: 灰度合并回主分支发布,5: 主分支发布灰度自动发布,6: 主分支回滚灰度自动发布,7: 放弃灰度', `OperationContext` longtext NOT NULL COMMENT '发布上下文信息', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_Namespace` (`AppId`,`ClusterName`,`NamespaceName`,`BranchName`), KEY `IX_ReleaseId` (`ReleaseId`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='发布历史';-- ------------------------------ Records of ReleaseHistory-- ----------------------------BEGIN;INSERT INTO `ReleaseHistory` VALUES (1, 'SampleApp', 'default', 'application', 'default', 1, 0, 0, '{}', b'0', 'apollo', '2019-04-14 14:13:47', 'apollo', '2019-04-14 14:13:47');INSERT INTO `ReleaseHistory` VALUES (2, 'myapp', 'default', 'application', 'default', 2, 0, 0, '{\"isEmergencyPublish\":false}', b'0', 'apollo', '2019-04-16 13:41:41', 'apollo', '2019-04-16 13:41:41');INSERT INTO `ReleaseHistory` VALUES (3, 'myapp', 'default', 'application', 'default', 3, 2, 0, '{\"isEmergencyPublish\":false}', b'0', 'apollo', '2019-04-16 13:42:23', 'apollo', '2019-04-16 13:42:23');INSERT INTO `ReleaseHistory` VALUES (4, 'myapp', 'default', 'test', 'default', 4, 0, 0, '{\"isEmergencyPublish\":false}', b'0', 'apollo', '2019-04-16 13:57:30', 'apollo', '2019-04-16 13:57:30');COMMIT;-- ------------------------------ Table structure for ReleaseMessage-- ----------------------------DROP TABLE IF EXISTS `ReleaseMessage`;CREATE TABLE `ReleaseMessage` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Message` varchar(1024) NOT NULL DEFAULT '' COMMENT '发布的消息内容', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Message` (`Message`(191))) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='发布消息';-- ------------------------------ Records of ReleaseMessage-- ----------------------------BEGIN;INSERT INTO `ReleaseMessage` VALUES (2, 'myapp+default+application', '2019-04-16 13:42:23');INSERT INTO `ReleaseMessage` VALUES (3, 'myapp+default+test', '2019-04-16 13:57:30');COMMIT;-- ------------------------------ Table structure for ServerConfig-- ----------------------------DROP TABLE IF EXISTS `ServerConfig`;CREATE TABLE `ServerConfig` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `Key` varchar(64) NOT NULL DEFAULT 'default' COMMENT '配置项Key', `Cluster` varchar(32) NOT NULL DEFAULT 'default' COMMENT '配置对应的集群,default为不针对特定的集群', `Value` varchar(2048) NOT NULL DEFAULT 'default' COMMENT '配置项值', `Comment` varchar(1024) DEFAULT '' COMMENT '注释', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_Key` (`Key`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='配置服务自身配置';-- ------------------------------ Records of ServerConfig-- ----------------------------BEGIN;INSERT INTO `ServerConfig` VALUES (1, 'eureka.service.url', 'default', 'http://localhost:8080/eureka/', 'Eureka服务Url,多个service以英文逗号分隔', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `ServerConfig` VALUES (2, 'namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `ServerConfig` VALUES (3, 'item.value.length.limit', 'default', '20000', 'item value最大长度限制', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `ServerConfig` VALUES (4, 'config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `ServerConfig` VALUES (5, 'item.key.length.limit', 'default', '128', 'item key 最大长度限制', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');COMMIT;SET FOREIGN_KEY_CHECKS = 1;ApolloPortalDB: ...

April 30, 2019 · 24 min · jiezi

Spring-Cloud-Zuul-1x的websocket支持实践

一、组件Spring Cloud Netflix Edgware.SR3spring-cloud-starter-zuul 1.3.5.RELEASEnginx 1.14.0Vue + SockJS + STOMP 二、实现• 下游服务添加websocket支持,包括依赖、配置和使用• zuul添加spring-cloud-netflix-zuul-websocket依赖• zuul配置文件添加如下配置:下游使用websocket的服务和对应的websocket设置zuul: ws: brokerages: 下游使用websocket的服务名: end-points: /endpoint brokers: /brokers destination-prefixes:• zuul添加websocket配置类@Configurationpublic class WebsokcetConfig { @Bean public ZuulPropertiesResolver zuulPropertiesResolver() { return wsBrokerage -> discoveryClient.getInstances(wsBrokerage.getId()).stream().findAny() .orElseGet(() -> new DefaultServiceInstance("", "", 0, false)) .getUri().toString(); } @Bean public WebSocketHttpHeadersCallback webSocketHttpHeadersCallback() { return userAgentSession -> new WebSocketHttpHeaders() {{ add("Authorization", new CustomBasicAuthRequestInterceptor(username, password).encodeBase64()); }}; }}• 前端使用sockJs和stomp连接网关的websocketfunction connect() { var socket = new SockJS('http://zuul的地址/endpoint', null, { 'transports': ['websocket'] }); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); // 订阅用户消息通知 stompClient.subscribe("/brokers",handleNotification); }); function handleNotification(message) { showGreeting(message); }}• nginx添加对websocket的支持 location /gateway { //添加如下配置 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host:$server_port; }

April 30, 2019 · 1 min · jiezi

springcloud一springcloudalibaba集成rocketmq

前言在之前的工作中,微服务框架使用的是springcloud,消息中间件使用的rocketmq,这段时间看到阿里出了spring cloud alibaba集成了rocketmq,出于好奇,写了个demo 一些概念官方对 Spring Cloud Stream 的一段介绍:Spring Cloud Stream 是一个用于构建基于消息的微服务应用框架。基于 SpringBoot 创建具有生产级别的单机 Spring 应用,并且使用 Spring Integration 与 Broker 进行连接。Binder :Components responsible to provide integration with the external messaging systems.【与外部消息中间件进行集成】Binding:Bridge between the external messaging systems and application provided Producers and Consumers of messages (created by the Destination Binders).【在消息中间件与应用程序提供的 Provider 和 Consumer 之间提供了一个桥梁,开发者只需使用应用程序的 生产者或消费者生产或消费数据即可,屏蔽了开发者与底层消息中间件的接触。】Message: The canonical data structure used by producers and consumers to communicate with Destination Binders (and thus other applications via external messaging systems).【生产者和消费者用于与目标绑定器通信的规范数据结构。】快速在本地启动rocketmq第一步:下载:https://www.apache.org/dyn/cl...第二步:解压第三步:修改三个配置文件:runbroker.sh,runserver.sh,tools.sh,将其中JAVA_HOME改成自己电脑的环境配置,修改完如下 ...

April 30, 2019 · 2 min · jiezi

LCN502-lcn模式源码分析二

前言上一篇文章(https://segmentfault.com/a/11...)我们在springboot2.1.3上集成了lcn5.0.2并简单做了一个lcn模式的demo。LCN官网将源码都给了出来,但是分析源码的部分目前还不是很多,这篇文章主要分析一下LCN模式源码 事务控制原理分析源码之前,我们首先看一下LCN整体的框架模型: TX-LCN由两大模块组成, TxClient、TxManager。TxClient作为模块的依赖框架,提供TX-LCN的标准支持,TxManager作为分布式事务的控制放。事务发起方或者参与反都由TxClient端来控制。 原理图: lcn模式不难发现,开启处理的地方在拦截器(com.codingapi.txlcn.tc.aspect.TransactionAspect)里面 @Around("lcnTransactionPointcut() && !txcTransactionPointcut()" + "&& !tccTransactionPointcut() && !txTransactionPointcut()") public Object runWithLcnTransaction(ProceedingJoinPoint point) throws Throwable { //将执行分布式事务的方法放在DTXInfo对象里面 DTXInfo dtxInfo = DTXInfo.getFromCache(point); LcnTransaction lcnTransaction = dtxInfo.getBusinessMethod().getAnnotation(LcnTransaction.class); dtxInfo.setTransactionType(Transactions.LCN); dtxInfo.setTransactionPropagation(lcnTransaction.propagation()); //调用方法,正式开启(或继续,这里取决于是否是事务发起方)分布式事务 return dtxLogicWeaver.runTransaction(dtxInfo, point::proceed); }走进runTransaction方法,我们可以看到一下内容(伪代码,方便分析) public class DTXLogicWeaver { //执行分布式事务的核心方法 public Object runTransaction(){ //1.拿到当前模块的事务上下文和全局事务上下文 DTXLocalContext dtxLocalContext = DTXLocalContext.getOrNew(); TxContext txContext; // ---------- 保证每个模块在一个DTX下只会有一个TxContext ---------- if (globalContext.hasTxContext()) { // 有事务上下文的获取父上下文 txContext = globalContext.txContext(); dtxLocalContext.setInGroup(true);//加入事务组 log.debug("Unit[{}] used parent's TxContext[{}].", dtxInfo.getUnitId(), txContext.getGroupId()); } else { // 没有的开启本地事务上下文 txContext = globalContext.startTx();//下层创建了事务组 } //2.设置本地事务上下文的一些参数 if (Objects.nonNull(dtxLocalContext.getGroupId())) { dtxLocalContext.setDestroy(false); } dtxLocalContext.setUnitId(dtxInfo.getUnitId()); dtxLocalContext.setGroupId(txContext.getGroupId());//从全局上下文获取 dtxLocalContext.setTransactionType(dtxInfo.getTransactionType()); //3.设置分布式事务参数 TxTransactionInfo info = new TxTransactionInfo(); info.setBusinessCallback(business);//业务执行器(核心) info.setGroupId(txContext.getGroupId());//从全局上下文获取 info.setUnitId(dtxInfo.getUnitId()); info.setPointMethod(dtxInfo.getBusinessMethod()); info.setPropagation(dtxInfo.getTransactionPropagation()); info.setTransactionInfo(dtxInfo.getTransactionInfo()); info.setTransactionType(dtxInfo.getTransactionType()); info.setTransactionStart(txContext.isDtxStart()); //4.LCN事务处理器 try { return transactionServiceExecutor.transactionRunning(info); } finally { // 线程执行业务完毕清理本地数据 if (dtxLocalContext.isDestroy()) { // 通知事务执行完毕 synchronized (txContext.getLock()) { txContext.getLock().notifyAll(); } // TxContext生命周期是? 和事务组一样(不与具体模块相关的) if (!dtxLocalContext.isInGroup()) { globalContext.destroyTx(); } DTXLocalContext.makeNeverAppeared(); TracingContext.tracing().destroy(); } log.debug("<---- TxLcn end ---->"); } } }执行业务操作 ...

April 28, 2019 · 2 min · jiezi

SpringBoot统一配置中心

一直使用springboot搭建后端项目,所有的配置都写到自己的resource目录下,随着微服务的项目越来越多,每个项目都需要自己的各种配置文件。而且后期一旦想要修改配置文件,就得重新发布一遍非常的麻烦,现在就来教教大家怎么统一在github上管理 这些配置,并做到一处修改处处生效,不需要重新发布项目。1 创建统一服务项目可以使用STS来初始化项目,选择自己的以来就好。 <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.mike</groupId> <artifactId>config-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>config-server</name> <description>config server</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>创建bootstrap.yml文件,当然你可以使用application.yml或application.properties spring: application: name: config-repo cloud: config: server: git: uri: https://github.com/mike/config-repo.git #github仓库地址 username: mike # 用户名 password: 123456 # 密码在github上创建一个config-repo仓库,并添加配置文件:两个不同环境的配置hello-pj-dev.yml ...

April 26, 2019 · 2 min · jiezi

微服务容错限流Hystrix入门

为什么需要容错限流复杂分布式系统通常有很多依赖,如果一个应用不能对来自依赖 故障进行隔离,那么应用本身就处在被拖垮的风险中。在一个高流量的网站中,某个单一后端一旦发生延迟,将会在数秒内导致 所有应用资源被耗尽(一个臭鸡蛋影响一篮筐)。如秒杀、抢购、双十一等场景,在某一时间点会有爆发式的网络流量涌入,如果没有好的网络流量限制,任由流量压到后台服务实例,很有可能造成资源耗尽,服务无法响应,甚至严重的导致应用崩溃。Hystrix是什么Hystrix 能使你的系统在出现依赖服务失效的时候,通过隔离系统所依赖的服务,防止服务级联失败,同时提供失败回退机制,更优雅地应对失效,并使你的系统能更快地从异常中恢复。 Hystrix能做什么在通过第三方客户端访问(通常是通过网络)依赖服务出现高延迟或者失败时,为系统提供保护和控制在分布式系统中防止级联失败快速失败(Fail fast)同时能快速恢复提供失败回退(Fallback)和优雅的服务降级机制提供近似实时的监控、报警和运维控制手段Hystrix设计原则防止单个依赖耗尽容器(例如 Tomcat)内所有用户线程降低系统负载,对无法及时处理的请求快速失败(fail fast)而不是排队提供失败回退,以在必要时让失效对用户透明化使用隔离机制(例如『舱壁』/『泳道』模式,熔断器模式等)降低依赖服务对整个系统的影响针对系统服务的度量、监控和报警,提供优化以满足近似实时性的要求在 Hystrix 绝大部分需要动态调整配置并快速部署到所有应用方面,提供优化以满足快速恢复的要求能保护应用不受依赖服务的整个执行过程中失败的影响,而不仅仅是网络请求Hystrix设计思想来源舱壁隔离模式货船为了进行防止漏水和火灾的扩散,会将货仓分隔为多个,当发生灾害时,将所在货仓进行隔离就可以降低整艘船的风险。 断路器模式熔断器就像家里的保险丝,当电流过载了就会跳闸,不过Hystrix的熔断机制相对复杂一些。 熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的. 当熔断器开关关闭时,请求被允许通过熔断器。如果当前健康状况高于设定阈值,开关继续保持关闭。如果当前健康状况低于设定阈值,开关则切换为打开状态。当熔断器开关打开时,请求被禁止通过。当熔断器开关处于打开状态,经过一段时间后,熔断器会自动进入半开状态,这时熔断器只允许一个请求通过。当该请求调用成功时,熔断器恢复到关闭状态。若该请求失败,熔断器继续保持打开状态, 接下来的请求被禁止通过。Hystrix工作流程官网原图 中文版 流程说明每次调用创建一个新的HystrixCommand,把依赖调用封装在run()方法中.执行execute()/queue做同步或异步调用.当前调用是否已被缓存,是则直接返回结果,否则进入步骤 4判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤 8,进行降级策略,如果关闭进入步骤 5判断线程池/队列/信号量是否跑满,如果跑满进入降级步骤8,否则继续后续步骤 6调用HystrixCommand的run方法.运行依赖逻辑 6.1. 调用是否出现异常,否:继续,是进入步骤8,6.2. 调用是否超时,否:返回调用结果,是进入步骤8搜集5、6步骤所有的运行状态(成功, 失败, 拒绝,超时)上报给熔断器,用于统计从而判断熔断器状态getFallback()降级逻辑.四种触发getFallback调用情况(图中步骤8的箭头来源):返回执行成功结果 两种资源隔离模式线程池隔离模式使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)。 信号量隔离模式使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃改类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)。 线程池隔离模式 VS 信号量隔离模式 Hystrix主要配置项 快速上手pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-core</artifactId> <version>1.5.12</version></dependency><dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-metrics-event-stream</artifactId> <version>1.5.12</version></dependency><dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-javanica</artifactId> <version>1.5.12</version></dependency>HystrixConfig@Configurationpublic class HystrixConfig { /** * 声明一个HystrixCommandAspect代理类,现拦截HystrixCommand的功能 */ @Bean public HystrixCommandAspect hystrixCommandAspect() { return new HystrixCommandAspect(); }}HelloService@Servicepublic class HelloService { @HystrixCommand(fallbackMethod = "helloError", commandProperties = { @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"), @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"), @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "2")}, threadPoolProperties = { @HystrixProperty(name = "coreSize", value = "5"), @HystrixProperty(name = "maximumSize", value = "5"), @HystrixProperty(name = "maxQueueSize", value = "10") }) public String sayHello(String name) { try { Thread.sleep( 15000 ); return "Hello " + name + " !"; } catch (InterruptedException e) { e.printStackTrace(); } return null; } public String helloError(String name) { return "服务器繁忙,请稍后访问~"; }}启动类@SpringBootApplication@RestControllerpublic class HystrixSimpleApplication { @Autowired private HelloService helloService; public static void main(String[] args) { SpringApplication.run( HystrixSimpleApplication.class, args ); } @GetMapping("/hi") public String hi(String name) { return helloService.sayHello( name ); }}测试访问 http://localhost:80809/hi?name=zhangsan ...

April 26, 2019 · 1 min · jiezi

springCloud集成分布式事务LCN-502-一

前言之前我写过一个基于springboot1.5.6+lcn4.1.0 的集成文章https://segmentfault.com/a/11...,基于目前更新的lcn5.0.2分析了一波源码。在此做一些记录(ps:如果对分布式事务不是很了解,可以先看下我上面贴的链接,本文基于有基础的情况去分析的) TX-LCN的3种模式LCN5.0.2有3种模式,分别是LCN模式,TCC模式,TXC模式 LCN模式: LCN模式是通过代理Connection的方式实现对本地事务的操作,然后在由TxManager统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。 该模式的特点: - 该模式对代码的嵌入性为低。- 该模式仅限于本地存在连接对象且可通过连接对象控制事务的模块。- 该模式下的事务提交与回滚是由本地事务方控制,对于数据一致性上有较高的保障。- 该模式缺陷在于代理的连接需要随事务发起方一共释放连接,增加了连接占用的时间。TCC模式: TCC事务机制相对于传统事务机制(X/Open XA Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。主要由三步操作,Try: 尝试执行业务、 Confirm:确认执行业务、 Cancel: 取消执行业务。 该模式的特点: - 该模式对代码的嵌入性高,要求每个业务需要写三种步骤的操作。- 该模式对有无本地事务控制都可以支持使用面广。- 数据一致性控制几乎完全由开发者控制,对业务开发难度要求高。TXC模式: TXC模式命名来源于淘宝,实现原理是在执行SQL之前,先查询SQL的影响数据,然后保存执行的SQL快走信息和创建锁。当需要回滚的时候就采用这些记录数据回滚数据库,目前锁实现依赖redis分布式锁控制。 该模式的特点: - 该模式同样对代码的嵌入性低。- 该模式仅限于对支持SQL方式的模块支持。- 该模式由于每次执行SQL之前需要先查询影响数据,因此相比LCN模式消耗资源与时间要多。- 该模式不会占用数据库的连接资源。LCN原理这里引用一下官网的原理图: 总的来说,核心步骤就是创建事务组,假如事务组,通知事务组 创建事务组 是指在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务标示GroupId的过程。加入事务组 添加事务组是指参与方在执行完业务方法以后,将该模块的事务信息通知给TxManager的操作通知事务组 是指在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager,TxManager将根据事务最终状态和事务组的信息来通知相应的参与模块提交或回滚事务,并返回结果给事务发起方。集成springCloud (ps:我这里使用的springboot版本是2.1.3)<!----pom.xml 今天是2019.4.24 最新版本是5.0.2 ---><properties> <codingapi.txlcn.version>5.0.2.RELEASE</codingapi.txlcn.version></properties><dependency> <groupId>com.codingapi.txlcn</groupId> <artifactId>txlcn-tc</artifactId> <version>${codingapi.txlcn.version}</version> </dependency> <dependency> <groupId>com.codingapi.txlcn</groupId> <artifactId>txlcn-tm</artifactId> <version>${codingapi.txlcn.version}</version> </dependency> <dependency> <groupId>com.codingapi.txlcn</groupId> <artifactId>txlcn-txmsg-netty</artifactId> <version>${codingapi.txlcn.version}</version> </dependency>A启动类上加注解:@EnableDistributedTransaction 表明这是一个txmanager的client @SpringBootApplication@EnableEurekaClient@EnableFeignClients@EnableDistributedTransaction@MapperScan("cn.iamcrawler.crawlergoddess.mapper")@ComponentScan("cn.iamcrawler.crawlergoddess,cn.iamcrawler.crawler_common.feign")public class CrawlerGoddessApplication { public static void main(String[] args) { SpringApplication.run(CrawlerGoddessApplication.class, args); }}yml(ps 我把密码隐藏了,数据库我用的是postgresql,lcn默认的是mysql): ...

April 24, 2019 · 1 min · jiezi

理解Spring Cloud Gateway Filters的执行顺序

本文基于Spring Cloud Gateway 2.1.1.RELEASE。 在讲SCG的Filter的排序问题之前得先比较一下Spring Cloud Gateway在对待Filter的方面与Zuul2有着哪些不同。 Filter的ScopeSCG采用的是Global Filter和Route Filter相结合的方式Zuul2则都是Global FilterSCG所谓Route Filter就是像下面这样的: spring: cloud: gateway: routes: - id: tomcat_route uri: http://tomcat:8080 predicates: - Path=/tomcat/docs filters: - StripPrefix=1 - RemoveRequestHeader=X-Request-Foo上面的StripPrefix和RemoveRequestHeader就是Route Filter,而SCG的Global Filter则是隐式的,无需显式配置,它们会在请求过来的时候被SCG调用。 也就是说你可以配置不同的Route,然后为每个Route配置不同的Route Filter,这一切都是在配置阶段就决定下来的。 而Zuul2则都是Global Filter,因此你得运行时在每个Filter内部自己决定是否要干活,除此之外,发送到Origin(被代理的服务)的url也得你自己设置,下面是一个例子(来自Zuul2 Sample): public class Routes extends HttpInboundSyncFilter { @Override public boolean shouldFilter(HttpRequestMessage httpRequestMessage) { // ... return true; } @Override public HttpRequestMessage apply(HttpRequestMessage request) { // ... // Route healthchecks to the healthcheck endpoint.; context.setEndpoint(ZuulEndPointRunner.PROXY_ENDPOINT_FILTER_NAME); context.setRouteVIP("tomcat"); return request; }}Filter的角色在SCG概念中只有一种Filter(撇开Global和Route的区别),它用代码来区分Pre Filter、Post Filter。在文档中还提到了Routing Filter,其实也是Pre Filter。Zuul2在代码中显示得提供了InboundFilter(负责进来的请求)、OutboundFilter(负责出去的响应)、ProxyEndpoint(负责请求到Origin,串起Inbound和Outbound)。下面是SCG的Pre Filter(裁剪自官方例子12.2 Writing Custom GatewayFilter Factories): ...

April 22, 2019 · 2 min · jiezi

初识 Nacos(上) 学习《Spring Cloud 服务发现新选择》

本文来自于我的个人主页:初识 Nacos(上) 学习《Spring Cloud 服务发现新选择》,转载请保留链接 ;)最近在从零接触Alibaba 开源项目Nacos,学习的是小马哥(mercyblitz)的技术周报,之前看了后忘记总结,导致也没有什么印象。所以现在决定学习一章,写一篇学习感悟,并且持续更新下去。首先这一章节主要讲得是服务发现(Service Discovery),作为 Spring Cloud 最核心功能特性之一,受到业界的广泛关注。Spring Cloud 整体架构在现行的 Spring Cloud 服务发现技术体系中,以 Spring Cloud Eureka 为典型代表,它作为官方推荐解决方案,被业 界广泛运用,然而其设计缺陷也非常之明显。还有Spring Cloud Zookeeper和Spring Cloud Consul。那么先介绍这三种的特点吧。Spring Cloud Eureka 特点优点:Spring Cloud 背书 - 推荐服务发现方案CAP 理论 - AP模型,数据最终一致简单易用 - 开箱即用,控制台管理缺点:内存限制 - 客户端上报完整注册信息,造成服务端内存浪费单一调度更新 - 客户端简单轮训更新,增加服务器压力集群伸缩性限制 - 广播式复制模型,增加服务器压力Spring Cloud Zookeeper 特点优点:成熟协调系统 - Dubbo、Spring Cloud等适配方案CAP理论 - CP模型,ZAB算法,强数据一致性缺点:维护成本 - 客户端、Session状态、网络故障伸缩性限制 - 内存、GC、连接Spring Cloud Consul 特点优点:通用方案 - 适用于 Service Mesh、 Java 生态CAP理论 - AP 模型,Raft+Gossip 算法,数据最终一致缺点:可靠性无法保证 - 未经过大规模验证非 Java 生态 - 维护和问题排查困难综上所述,让我得出了Spring Cloud服务发现方案对比结果:那么这三种服务发现的基本模式是怎样的呢?现在来谈谈Spring cloud 服务器发现模式。首先都是服务器启动 - 启动注册中心然后增加客户端依赖 - sping-cloud-start-*最后就是客户端注册 - 记得在XXApplication.java文件中添加@EnableDiscoveryClient,注解开启服务注册与发现功能。以下我以Eureka发现模式为例:首先去Spring Initializr快速创建Eureka服务端和客户端应用程序,然后导入自己的IDE。当然你如果嫌麻烦,也可以直接导入已经写好的工程。然后在resources-application.properties中分别配置好两者的端口号,像客户端这块还需要写好应用名称、以及Eureka 服务器地址。最后我们就直接可以runXXApplication.java了,像我的服务端端口是12345,就访问localhost:12345。页面跳转如下图所示,恭喜你的Eureka服务已经起来了。Eureka-client亦如此,成功run起来后,在之前的服务端页面,也就是localhost:12345,刷新下会在Instances currently registered with Eureka出现EUREKA-CLIENT的状态信息。spring-cloud-alibaba-nacos-discovery 作为 Spring Cloud Alibaba 服务发现的核心模块,其架构基础与 Spring Cloud 现行方案相同,均构建在 Spring Cloud Commons 抽象。因此,它在 Spring Cloud 服务发现的使用上,开发人员将不会心存任何的违和感。Alibaba Nacos 生态介绍从功能特性而言,spring-cloud-alibaba-nacos-discovery 仅是 Nacos 在 Spring Cloud 服务发现的解决方案,Nacos 在 Spring Cloud 中还支持分布式配置的特性。与开源产品不同的是,Nacos 曾经历过中国特色的超大流量考验,以及巨型规模的集群实施,无论从经验积累还是技术沉淀,现行 Spring Cloud 解决方案 都是无法比拟的。然而这并非说明它完美无缺,在内部的评估和讨论中,也发现其中差距和文化差异。为了解决这些问题,讨论将从整体架构和设计思考两个方面,介绍 Nacos 与 Spring 技术栈整合情况,以及与其他开源方案的适配思考,整体上,降低 Nacos 使用门槛,使迁移成本接近为零,达到“一次开发,到处运行”的目的。那么接下来我们通过Github上,Spring Cloud Alibaba项目中官方给出的指导文档来配置启动 Nacos吧。下载注册中心首先需要获取 Nacos Server,支持直接下载和源码构建两种方式。直接下载:Nacos Server 下载页源码构建:进入 Nacos Github 项目页面,将代码 git clone 到本地自行编译打包,参考此文档。推荐使用源码构建方式以获取最新版本启动注册中心启动 Server,进入解压后文件夹或编译打包好的文件夹,找到如下相对文件夹 nacos/bin,并对照操作系统实际情况之下如下命令。Linux/Unix/Mac 操作系统,执行命令 sh startup.sh -m standaloneWindows 操作系统,执行命令 cmd startup.cmd增加第三方依赖首先,修改 pom.xml 文件,引入 Nacos Discovery Starter。 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>外部化配置在应用的 /src/main/resources/application.properties 配置文件中配置 Nacos Server 地址 spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848激活服务发现使用 @EnableDiscoveryClient 注解开启服务注册与发现功能(SpringApplication.run) @SpringBootApplication @EnableDiscoveryClient public class ProviderApplication { public static void main(String[] args) { SpringApplication.run(Application.class, args); } @RestController class EchoController { @RequestMapping(value = “/echo/{string}”, method = RequestMethod.GET) public String echo(@PathVariable String string) { return string; } } }应用启动增加配置,在 nacos-discovery-provider-example 项目的 /src/main/resources/application.properties 中添加基本配置信息 spring.application.name=service-provider server.port=18082启动应用,支持 IDE 直接启动和编译打包后启动。IDE直接启动:找到 nacos-discovery-provider-example 项目的主类 ProviderApplication,执行 main 方法启动应用。打包编译后启动:在 nacos-discovery-provider-example 项目中执行 mvn clean package 将工程编译打包,然后执行 java -jar nacos-discovery-provider-example.jar启动应用。验证检验服务发现在浏览器输入此地址http://127.0.0.1:8848/nacos/v1/ns/instances?serviceName=service-provider 并点击跳转,可以看到服务节点已经成功注册到 Nacos Server。 ...

April 20, 2019 · 2 min · jiezi

调用链监控 CAT 之 URL埋点实践

URL监控埋点作用一个http请求来了之后,会自动打点,能够记录每个url的访问情况,并将以此请求后续的调用链路串起来,可以在cat上查看logview可以在cat Transaction及Event 页面上都看到URL和URL.Forward(如果有Forward请求的话)两类数据;Transaction数据中URL点进去的数据就是被访问的具体URL(去掉参数的前缀部分)请将catFilter存放filter的第一个,这样可以保证最大可能性监控所有的请求实践工程说明工程名端口作用cat-ui8082调用入口服务cat-business-consumer8083业务消费服务cat-order-service8084订单服务cat-storage-service8085库存服务上图是本节实例的埋点图,首先 cat-ui 的入口 和 调用点 加入cat埋点,cat-business-consumer的入口和调用点加入埋点,cat-order-service 和 cat-storage-service 不再调用其他微服务,所以只在入口加入埋点。通过这样的埋点,可以组成一条完整的调用链。关键代码调用链上下文通用类CatContextImpl.java/** * Cat.context接口实现类,用于context调用链传递,相关方法Cat.logRemoteCall()和Cat.logRemoteServer() /public class CatContextImpl implements Cat.Context { private Map<String, String> properties = new HashMap<>(16); @Override public void addProperty(String key, String value) { properties.put(key, value); } @Override public String getProperty(String key) { return properties.get(key); }}CatHttpConstants/* * 添加header常量,用于http协议传输rootId、parentId、childId三个context属性 /public class CatHttpConstants { /* * http header 常量 / public static final String CAT_HTTP_HEADER_ROOT_MESSAGE_ID = “X-CAT-ROOT-MESSAGE-ID”; public static final String CAT_HTTP_HEADER_PARENT_MESSAGE_ID = “X-CAT-ROOT-PARENT-ID”; public static final String CAT_HTTP_HEADER_CHILD_MESSAGE_ID = “X-CAT-ROOT-CHILD-ID”;}CatServletFilter/* * http协议传输,远程调用链目标端接收context的filter, * 通过header接收rootId、parentId、childId并放入CatContextImpl中,调用Cat.logRemoteCallServer()进行调用链关联 * 注:若不涉及调用链,则直接使用cat-client.jar中提供的filter即可 * 使用方法(视项目框架而定): * 1、web项目:在web.xml中引用此filter * 2、Springboot项目,通过注入bean的方式注入此filter */public class CatServletFilter implements Filter { private String[] urlPatterns = new String[0]; @Override public void init(FilterConfig filterConfig) throws ServletException { String patterns = filterConfig.getInitParameter(“CatHttpModuleUrlPatterns”); if (patterns != null) { patterns = patterns.trim(); urlPatterns = patterns.split(","); for (int i = 0; i < urlPatterns.length; i++) { urlPatterns[i] = urlPatterns[i].trim(); } } } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; String url = request.getRequestURL().toString(); for (String urlPattern : urlPatterns) { if (url.startsWith(urlPattern)) { url = urlPattern; } } CatContextImpl catContext = new CatContextImpl(); catContext.addProperty( Cat.Context.ROOT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID)); catContext.addProperty(Cat.Context.PARENT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID)); catContext.addProperty(Cat.Context.CHILD, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID)); Cat.logRemoteCallServer(catContext); Transaction t = Cat.newTransaction( CatConstants.TYPE_URL, url); try { Cat.logEvent(“Service.method”, request.getMethod(), Message.SUCCESS, request.getRequestURL().toString()); Cat.logEvent(“Service.client”, request.getRemoteHost()); filterChain.doFilter(servletRequest, servletResponse); t.setStatus(Transaction.SUCCESS); } catch (Exception ex) { t.setStatus(ex); Cat.logError(ex); throw ex; } finally { t.complete(); } } @Override public void destroy() { }}本节实例中每个工程都会用到调用链上下文通用类。cat-ui 工程CatRestInterceptor@Componentpublic class CatRestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { Transaction t = Cat.newTransaction(CatConstants.TYPE_REMOTE_CALL, request.getURI().toString()); try { HttpHeaders headers = request.getHeaders(); // 保存和传递CAT调用链上下文 Cat.Context ctx = new CatContextImpl(); Cat.logRemoteCallClient(ctx); headers.add(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT)); headers.add(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT)); headers.add(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD)); // 保证请求继续被执行 ClientHttpResponse response = execution.execute(request, body); t.setStatus(Transaction.SUCCESS); return response; } catch (Exception e) { Cat.getProducer().logError(e); t.setStatus(e); throw e; } finally { t.complete(); } }}CatServletFilter 对 cat-ui 的入口进行了埋点,CatRestInterceptor 实现 ClientHttpRequestInterceptor接口 可以对 RestTemplate 发起的请求进行拦截,利用这一点对调用点埋点,同时在 Http Header 中存入 调用链的上下文,将调用链传递下去。cat-business-consumer、cat-order-service、cat-storage-service 中的埋点与 cat-ui 埋点的方式相同。测试发起请求curl http://127.0.0.1:8082/startcat 监控界面可以看到本节实例的服务。点开 “logView” 可以看到完整的调用链信息。点击 “Graph” 查看图表形式的调用链信息。源码https://github.com/gf-huanchu…参考https://github.com/dianping/c…欢迎扫码或微信搜索公众号《程序员果果》关注我,关注有惊喜~ ...

April 16, 2019 · 2 min · jiezi

Java异常处理12条军规

摘要: 简单实用的建议。原文:Java异常处理12条军规公众号:Spring源码解析Fundebug经授权转载,版权归原作者所有。在Java语言中,异常从使用方式上可以分为两大类:CheckedExceptionUncheckedException在Java中类的异常结构图如下:可检查异常需要在方法上声明,一般要求调用者必须感知异常可能发生,并且对可能发生的异常进行处理。可以理解成系统正常状态下很可能发生的情况,通常发生在通过网络调用外部系统或者使用文件系统时,在这种情况下,错误是可能恢复的,调用者可以根据异常做出必要的处理,例如重试或者资源清理等。非检查异常是不需要在throws子句中声明的异常。JVM根本不会强制您处理它们,因为它们主要是由于程序错误而在运行时生成的。它们扩展了RuntimeException。最常见的例子是NullPointerException 可能不应该重试未经检查的异常,并且正确的操作通常应该是什么都不做,并让它从您的方法和执行堆栈中出来。在高执行级别,应记录此类异常。Error是最为严重的运行时错误,几乎是不可能恢复和处理,一些示例是OutOfMemoryError,LinkageError和StackOverflowError。它们通常会使程序或程序的一部分崩溃。只有良好的日志记录练习才能帮助您确定错误的确切原因.在异常处理时的几点建议:1. 永远不要catch中吞掉异常,否则在系统发生错误时,你永远不知道到底发生了什么catch (SomeException e) { return null;}2. 尽量使用特定的异常而不是一律使用Exception这样太泛泛的异常public void foo() throws Exception { //错误的做法}public void foo() throws MyBusinessException1, MyBusinessException2 { //正确的做法}一味的使用Exception,这样就违背了可检查异常的设计初衷,因为调用都不知道Exception到底是什么,也不知道该如何处理。捕获异常时,也不要捕获范围太大,例如捕获Exception,相反,只捕获你能处理的异常,应该处理的异常。即然方法的声明者在方法上声明了不同类型的可检查异常,他是希望调用者区别对待不同异常的。3. Never catch Throwable class永远不要捕获Throwable,因为Error也是继承自它,Error是Jvm都处理不了的错误,你能处理?所以基于有些Jvm在Error时就不会让你catch住。4. 正确的封装和传递异常**不要丢失异常栈,因为异常栈对于定位原始错误很关键catch (SomeException e) {throw new MyServiceException(“Some information: " + e.getMessage()); //错误的做法}一定要保留原始的异常:catch (SomeException e) { throw new MyServiceException(“Some information: " , e); //正确的打开方式}5. 要打印异常,就不要抛出,不要两者都做catch (SomeException e) { LOGGER.error(“Some information”, e); throw e;}这样的log没有任何意义,只会打印出一连串的error log,对于定位问题无济于事。6. 不要在finally块中抛出异常如果在finally中抛出异常,将会覆盖原始的异常,如果finally中真的可能会发生异常,那一定要处理并记录它,不要向上抛。7. 不要使用printStackTrace要给异常添加上有用的上下文信息,单纯的异常栈,没有太大意义8. Throw early catch late异常界著名的原则,错误发生时及早抛出,然后在获得所以全部信息时再捕获处理.也可以理解为在低层次抛出的异常,在足够高的抽象层面才能更好的理解异常,然后捕获处理。9. 对于使用一些重量级资源的操作,发生异常时,一定记得清理如网络连接,数据库操作等,可以用try finally来做clean up的工作。10. 不要使用异常来控制程序逻辑流程我们总是不经意间这么做了,这样使得代码变更丑陋,使得正常业务逻辑和错误处理混淆不清;而且也可能会带来性能问题,因为异常是个比较重的操作。11. 及早校验用户的输入在最边缘的入口校验用户的输入,这样使得我们不用再更底层逻辑中处处校验参数的合法性,能大大简化业务逻辑中不必要的异常处理逻辑;相反,在业务中不如果担心参数的合法性,则应该使用卫语句抛出运行时异常,一步步把对参数错误的处理推到系统的边缘,保持系统内部的清洁。12. 在打印错误的log中尽量在一行中包含尽可能多的上下文LOGGER.debug(“enter A”);LOGGER.debug(“enter B”); //错误的方式LOGGER.debug(“enter A, enter B”);//正确的方式 ...

April 12, 2019 · 1 min · jiezi

调用链监控 CAT 之 入门

简介CAT 是一个实时和接近全量的监控系统,它侧重于对Java应用的监控,基本接入了美团上海所有核心应用。目前在中间件(MVC、RPC、数据库、缓存等)框架中得到广泛应用,为美团各业务线提供系统的性能指标、健康状况、监控告警等。优势实时处理:信息的价值会随时间锐减,尤其是事故处理过程中。全量数据:全量采集指标数据,便于深度分析故障案例。高可用:故障的还原与问题定位,需要高可用监控来支撑。故障容忍:故障不影响业务正常运转、对业务透明。高吞吐:海量监控数据的收集,需要高吞吐能力做保证。可扩展:支持分布式、跨 IDC 部署,横向扩展的监控系统。开源产品比较快速上手本地部署步骤1:部署tomcat准备一个tomcat,修改 tomcat conf 目录下 server.xml,防中文乱码。Connector port=“8080” protocol=“HTTP/1.1” URIEncoding=“utf-8” connectionTimeout=“20000” redirectPort=“8443” /> <!– 增加 URIEncoding=“utf-8” –>步骤2:程序对于/data/目录具体读写权限(重要)Linux要求/data/目录能进行读写操作,如果/data/目录不能写,建议使用linux的软链接链接到一个固定可写的目录。此目录会存一些CAT必要的配置文件以及运行时候的数据存储目录。CAT支持CAT_HOME环境变量,可以通过JVM参数修改默认的路径。mkdir /datachmod -R 777 /data/Windows对程序运行盘下的/data/appdatas/cat和/data/applogs/cat有读写权限。例如cat服务运行在e盘的tomcat中,则需要对e:/data/appdatas/cat和e:/data/applogs/cat有读写权限。步骤3: 配置/data/appdatas/cat/client.xml ($CAT_HOME/client.xml)<?xml version=“1.0” encoding=“utf-8”?><config mode=“client”> <servers> <server ip=“127.0.0.1” port=“2280” http-port=“8080”/> </servers></config>此配置文件的作用是所有的客户端都需要一个地址指向CAT的服务端。步骤4: 安装CAT的数据库下载cat源码包:https://codeload.github.com/d…解压后,数据库的脚本文件为 script/CatApplication.sqlmysql -uroot -Dcat < CatApplication.sql步骤5: 配置/data/appdatas/cat/datasources.xml($CAT_HOME/datasources.xml)<?xml version=“1.0” encoding=“utf-8”?><data-sources> <data-source id=“cat”> <maximum-pool-size>3</maximum-pool-size> <connection-timeout>1s</connection-timeout> <idle-timeout>10m</idle-timeout> <statement-cache-size>1000</statement-cache-size> <properties> <driver>com.mysql.jdbc.Driver</driver> <url><![CDATA[jdbc:mysql://127.0.0.1:3306/cat]]></url> <!– 请替换为真实数据库URL及Port –> <user>root</user> <!– 请替换为真实数据库用户名 –> <password>root</password> <!– 请替换为真实数据库密码 –> <connectionProperties><![CDATA[useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&socketTimeout=120000]]></connectionProperties> </properties> </data-source></data-sources>步骤6: war打包官方下载:http://unidal.org/nexus/servi…重命名为cat.war进行部署,注意此war是用jdk8,服务端请使用jdk8版本步骤7: war部署将上一步打包的war包部署到本机tomcat的webapps下。启动tomcat,打开控制台的URL,http://127.0.0.1:8080/cat/s/config?op=routerConfigUpdate 默认用户名:admin 默认密码:admin 。配置客户端路由。<?xml version=“1.0” encoding=“utf-8”?><router-config backup-server=“你的本机ip(不要用127.0.0.1)” backup-server-port=“2280”> <default-server id=“你的本机ip(不要用127.0.0.1)” weight=“1.0” port=“2280” enable=“true”/> <network-policy id=“default” title=“默认” block=“false” server-group=“default_group”> </network-policy> <server-group id=“default_group” title=“default-group”> <group-server id=“你的本机ip(不要用127.0.0.1)”/> </server-group> <domain id=“cat”> <group id=“default”> <server id=“你的本机ip(不要用127.0.0.1)” port=“2280” weight=“1.0”/> </group> </domain></router-config>提交后,重启tomcat,访问http://127.0.0.1:8080/cat,出现如下界面,说明搭建成功。测试安装jar包进入cat源码包的 lib/java/jar ,将cat-client-3.0.0.jar 包 安装到本地maven仓库。mvn install:install-file -DgroupId=com.dianping.cat -DartifactId=cat-client Dversion=3.0.0 -Dpackaging=jar -Dfile=cat-client-3.0.0.jar创建工程创建一个springboot 工程,关键代码如下。pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>com.dianping.cat</groupId> <artifactId>cat-client</artifactId> <version>3.0.0</version></dependency>app.properties需要在你的项目中创建 src/main/resources/META-INF/app.properties 文件, 并添加如下内容:app.name={appkey}appkey 只能包含英文字母 (a-z, A-Z)、数字 (0-9)、下划线 (_) 和中划线 (-)application.ymlserver: port: 8760spring: application: name: cat-simple启动类@SpringBootApplication@RestControllerpublic class CatSimpleApplication { public static void main(String[] args) { SpringApplication.run( CatSimpleApplication.class, args ); } @PostMapping("/hi") public String hi(HttpServletRequest request){ String url = request.getRequestURL().toString(); // 创建一个 Transaction Transaction transaction = Cat.newTransaction( “URL”, url ); try { // 处理业务 myBusiness(); // 设置状态 transaction.setStatus(Transaction.SUCCESS); } catch (Exception e) { // 设置错误状态 transaction.setStatus(e); // 记录错误信息 Cat.logError(e); } finally { // 结束 Transaction transaction.complete(); } return “hello”; } @PostMapping("/error") public String error(HttpServletRequest request){ String url = request.getRequestURL().toString(); // 创建一个 Transaction Transaction transaction = Cat.newTransaction( “URL”, url ); try { // 处理业务 int i = 1 / 0; // 设置状态 transaction.setStatus(Transaction.SUCCESS); } catch (Exception e) { // 设置错误状态 transaction.setStatus(e); // 记录错误信息 Cat.logError(e); } finally { // 结束 Transaction transaction.complete(); } return “500”; } private void myBusiness() { //模拟业务处理的时间 try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); } }}请求 http://localhost:8760/hicurl -X POST http://localhost:8760/hi请求 http://localhost:8760/errorcurl -X POST http://localhost:8760/error查看监控信息进入 cat 控制台,点击 Transaction 按钮 ,之后点击全部,会看到有哪些客户端,如图:点击客户端 cat-simple ,出现如图:如上图,可以清晰的看到 请求的 总个数(tatal)、均值(avg)、最大/最小(max/min)、标准差(std)等,其他都比较直观,标准差稍微复杂一点,大家自己可以推演一下怎么做增量计算。那集合运算,比如95线(表示95%请求的完成时间)、999线(表示99.9%请求的完成时间)点击 “log View” 可以查看 错误信息,如图:源码https://github.com/gf-huanchu…参考https://github.com/dianping/c…关注我的公众号,精彩内容不能错过~ ...

April 11, 2019 · 2 min · jiezi

SpringCloud之zuul

简介Zuul是所有从设备和web站点到Netflix流媒体应用程序后端的请求的前门。作为一个边缘服务应用程序,Zuul的构建是为了支持动态路由、监视、弹性和安全性。它还可以根据需要将请求路由到多个Amazon自动伸缩组。Zuul使用了一系列不同类型的过滤器,使我们能够快速灵活地将功能应用到edge服务中。这些过滤器帮助我们执行以下功能:身份验证和安全性——识别每个资源的身份验证需求并拒绝不满足这些需求的请求。洞察和监控——在边缘跟踪有意义的数据和统计数据,以便为我们提供准确的生产视图。动态路由——根据需要动态地将请求路由到不同的后端集群。压力测试——逐步增加集群的流量,以评估性能。减少负载——为每种类型的请求分配容量,并删除超过限制的请求。静态响应处理——直接在边缘构建一些响应,而不是将它们转发到内部集群多区域弹性——跨AWS区域路由请求,以使我们的ELB使用多样化,并使我们的优势更接近我们的成员工作原理在高级视图中,Zuul 2.0是一个Netty服务器,它运行预过滤器(入站过滤器),然后使用Netty客户机代理请求,然后在运行后过滤器(出站过滤器)后返回响应。过滤器是Zuul业务逻辑的核心所在。它们能够执行非常大范围的操作,并且可以在请求-响应生命周期的不同部分运行,如上图所示。Inbound Filters在路由到源之前执行,可以用于身份验证、路由和装饰请求。Endpoint Filters 可用于返回静态响应,否则内置的ProxyEndpoint过滤器将把请求路由到源。Outbound Filters 在从源获取响应后执行,可用于度量、装饰用户响应或添加自定义头。还有两种类型的过滤器:同步和异步。因为我们是在一个事件循环上运行的,所以千万不要阻塞过滤器。如果要阻塞,可以在一个异步过滤器中阻塞,在一个单独的threadpool上阻塞——否则可以使用同步过滤器。实用过滤器DebugRequest——查找一个查询参数来为请求添加额外的调试日志Healthcheck -简单的静态端点过滤器,返回200,如果一切引导正确ZuulResponseFilter -添加信息头部提供额外的细节路由,请求执行,状态和错误原因GZipResponseFilter -可以启用gzip出站响应SurgicalDebugFilter ——可以将特定的请求路由到不同的主机进行调试使用技巧依赖: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>MyFilter 过滤器@Componentpublic class MyFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(MyFilter.class); /** * pre:路由之前 * routing:路由之时 * post: 路由之后 * error:发送错误调用 * @return / @Override public String filterType() { return “pre”; } /* * 过滤的顺序 * @return / @Override public int filterOrder() { return 0; } /* * 这里可以写逻辑判断,是否要过滤,本文true,永远过滤 * @return / @Override public boolean shouldFilter() { return true; } /* * 过滤器的具体逻辑。 * 可用很复杂,包括查sql,nosql去判断该请求到底有没有权限访问。 * @return * @throws ZuulException / @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); log.info(String.format("%s >>> %s", request.getMethod(), request.getRequestURL().toString())); Object accessToken = request.getParameter(“token”); if(accessToken == null) { log.warn(“token is empty”); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); try { ctx.getResponse().getWriter().write(“token is empty”); }catch (Exception e){} return null; } log.info(“ok”); return null; }}application.yml配置路由转发eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/server: port: 8769spring: application: name: cloud-service-zuulzuul: routes: api-a: path: /api-a/* serviceId: cloud-service-ribbon api-b: path: /api-b/** serviceId: cloud-service-feign启用zuul@SpringBootApplication@EnableZuulProxy@EnableEurekaClient@EnableDiscoveryClientpublic class CloudServiceZuulApplication { public static void main(String[] args) { SpringApplication.run(CloudServiceZuulApplication.class, args); }}路由熔断/** * 路由熔断 */@Componentpublic class ProducerFallback implements FallbackProvider { private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class); //指定要处理的 service。 @Override public String getRoute() { return “spring-cloud-producer”; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { if (cause != null && cause.getCause() != null) { String reason = cause.getCause().getMessage(); logger.info(“Excption {}",reason); } return fallbackResponse(); } public ClientHttpResponse fallbackResponse() { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return “OK”; } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(“The service is unavailable.".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; }}总结Zuul网关有自动转发机制,但其实Zuul还有更多的应用场景,比如:鉴权、流量转发、请求统计等等,这些功能都可以使用Zuul来实现。’更多资源:zuul重试参考博文:http://www.ityouknow.com/spri…最后如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。 ...

April 10, 2019 · 2 min · jiezi

springcloud gateway聚合swagger2

问题描述在搭建分布式应用时,每个应用通过nacos在网关出装配了路由,我们希望网关也可以将所有的应用的swagger界面聚合起来。这样前端开发的时候只需要访问网关的swagger就可以,而不用访问每个应用的swagger。框架springcloud+gateway+nacos+swagger问题分析swagger页面是一个单页面应用,所有的显示的数据都是通过和springfox.documentation.swagger.web.ApiResponseController进行数据交互,首先通过/swagger-resources获取swagger资源信息,获取的信息格式如下:[{name: “default”, url: “/v2/api-docs”, swaggerVersion: “2.0”, location: “/v2/api-docs”}],其中name代表swagger生成的接口组的组名,如图所示:url代表swagger接口组的详细信息可以通过 localhost:8081/v2/api-docs来获取,如下图:在网关处,如果访问/swagger-resources能够获取到所有应用的swagger的资源信息,那么我们的问题就可以解决了,所以我们需要做的是修改/swagger-resources接口的处理方式,使得这个接口能够按照我们的需求返回swagger资源。解决方案我们首先在网关处引入swagger的相关依赖,然后实现一个获取其他应用的swagger资源的组件: /** * 聚合各个服务的swagger接口 * * @author ksyzz * @since <pre>2019/04/09</pre> / @Component public class MySwaggerResourceProvider implements SwaggerResourcesProvider { /* * swagger2默认的url后缀 / private static final String SWAGGER2URL = “/v2/api-docs”; /* * 网关路由 / private final RouteLocator routeLocator; /* * 网关应用名称 / @Value("${spring.application.name}") private String self; @Autowired public MySwaggerResourceProvider(RouteLocator routeLocator) { this.routeLocator = routeLocator; } @Override public List<SwaggerResource> get() { List<SwaggerResource> resources = new ArrayList<>(); List<String> routeHosts = new ArrayList<>(); // 由于我的网关采用的是负载均衡的方式,因此我需要拿到所有应用的serviceId // 获取所有可用的host:serviceId routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null) .filter(route -> !self.equals(route.getUri().getHost())) .subscribe(route -> routeHosts.add(route.getUri().getHost())); // 记录已经添加过的server,存在同一个应用注册了多个服务在nacos上 Set<String> dealed = new HashSet<>(); routeHosts.forEach(instance -> { // 拼接url,样式为/serviceId/v2/api-info,当网关调用这个接口时,会自动通过负载均衡寻找对应的主机 String url = “/” + instance + SWAGGER2URL; if (!dealed.contains(url)) { dealed.add(url); SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setUrl(url); swaggerResource.setName(instance); resources.add(swaggerResource); } }); return resources; } }然后定义一个接口类: /* * swagger聚合接口,三个接口都是swagger-ui.html需要访问的接口 * * @author ksyzz * @since <pre>2019/04/09</pre> */ @RestController @RequestMapping("/swagger-resources") public class SwaggerResourceController { private MySwaggerResourceProvider swaggerResourceProvider; @Autowired public SwaggerResourceController(MySwaggerResourceProvider swaggerResourceProvider) { this.swaggerResourceProvider = swaggerResourceProvider; } @RequestMapping(value = “/configuration/security”) public ResponseEntity<SecurityConfiguration> securityConfiguration() { return new ResponseEntity<>(SecurityConfigurationBuilder.builder().build(), HttpStatus.OK); } @RequestMapping(value = “/configuration/ui”) public ResponseEntity<UiConfiguration> uiConfiguration() { return new ResponseEntity<>(UiConfigurationBuilder.builder().build(), HttpStatus.OK); } @RequestMapping public ResponseEntity<List<SwaggerResource>> swaggerResources() { return new ResponseEntity<>(swaggerResourceProvider.get(), HttpStatus.OK); } }然后启动网关,访问 http://网关地址/swagger-ui.html,可以看到在箭头处,可以切换不同应用的swagger页面。 ...

April 10, 2019 · 1 min · jiezi

SpringCloud之Hystrix

简介在分布式环境中,许多服务依赖关系中的一些必然会失败。Hystrix是一个库,它通过添加延迟容忍和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点、停止跨服务的级联故障并提供回退选项来实现这一点,所有这些选项都提高了系统的总体弹性。目标Hystrix的设计目的如下:为通过第三方客户端库访问的依赖项(通常通过网络)提供保护和控制延迟和故障。停止复杂分布式系统中的级联故障。故障快速恢复。在可能的情况下,后退并优雅地降级。启用近实时监视、警报和操作控制。背景为了解决什么问题?复杂分布式体系结构中的应用程序有几十个依赖项,每个依赖项在某个时候都不可避免地会失败。如果主机应用程序没有从这些外部故障中隔离出来,那么它就有可能与这些外部故障一起宕机。例如,对于一个依赖于30个服务的应用程序,其中每个服务都有99.99%的正常运行时间,您可以这样期望:99.9930 = 99.7% uptime0.3% of 1 billion requests = 3,000,000 failures2+ hours downtime/month even if all dependencies have excellent uptime.现实通常更糟。即使当所有依赖项都运行良好时,即使0.01%的停机时间对几十个服务中的每个服务的总体影响也相当于一个月潜在的停机时间(如果您不为恢复而设计整个系统)。如下面的图演变:当一切正常时,请求流可以是这样的:当许多后端系统之一成为潜在,它可以阻止整个用户请求:对于高流量,一个后端依赖项成为潜在,可能会导致所有服务器上的所有资源在几秒钟内饱和。应用程序中通过网络或客户机库到达可能导致网络请求的每个点都是潜在故障的来源。比故障更糟的是,这些应用程序还可能导致服务之间的延迟增加,从而备份队列、线程和其他系统资源,从而导致系统中出现更多级联故障。工作原理工作流程图:1. 构造一个HystrixCommand或HystrixObservableCommand对象第一步是构造一个HystrixCommand或HystrixObservableCommand对象来表示对依赖项的请求。将请求发出时需要的任何参数传递给构造函数。如果期望依赖项返回单个响应,则构造一个HystrixCommand对象。例如:HystrixCommand command = new HystrixCommand(arg1, arg2);如果期望依赖项返回发出响应的可观察对象,则构造一个HystrixObservableCommand对象。例如:HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);2.执行命令有四种方法可以执行命令,使用以下四种方法之一的Hystrix命令对象(前两种方法只适用于简单的HystrixCommand对象,不适用于HystrixObservableCommand):execute()) — blocks, then returns the single response received from the dependency (or throws an exception in case of an error)queue()) — returns a Future with which you can obtain the single response from the dependencyobserve()) — subscribes to the Observable that represents the response(s) from the dependency and returns an Observable that replicates that source ObservabletoObservable()) — returns an Observable that, when you subscribe to it, will execute the Hystrix command and emit its responses3.是否缓存了响应如果为该命令启用了请求缓存,并且在缓存中可用对请求的响应,则此缓存的响应将立即以可观察到的形式返回。4. 电路打开了吗?当您执行该命令时,Hystrix将与断路器一起检查电路是否打开。如果电路打开(或“跳闸”),那么Hystrix将不执行命令,而是将流路由到(8)获取回退。如果电路被关闭,则流继续到(5),检查是否有可用的容量来运行命令。5.线程池/队列/信号量是否已满?如果与该命令关联的线程池和队列(或信号量,如果不在线程中运行)已满,那么Hystrix将不执行该命令,而是立即将流路由到(8)获取回退。6.HystrixObservableCommand.construct()或HystrixCommand.run ()这里,Hystrix通过为此目的编写的方法调用对依赖项的请求,方法如下:HystrixCommand.run()) — returns a single response or throws an exceptionHystrixObservableCommand.construct()) — returns an Observable that emits the response(s) or sends an onError notification如果run()或construct()方法超过了命令的超时值,线程将抛出一个TimeoutException(如果命令本身不在自己的线程中运行,则单独的计时器线程将抛出一个TimeoutException)。在这种情况下,Hystrix将响应路由到8。获取回退,如果最终返回值run()或construct()方法没有取消/中断,那么它将丢弃该方法。请注意,没有办法强制潜在线程停止工作——Hystrix在JVM上能做的最好的事情就是抛出InterruptedException。如果由Hystrix包装的工作不尊重interruptedexception,那么Hystrix线程池中的线程将继续它的工作,尽管客户机已经收到了TimeoutException。这种行为可能会使Hystrix线程池饱和,尽管负载“正确释放”。大多数Java HTTP客户端库不解释interruptedexception。因此,请确保正确配置HTTP客户机上的连接和读/写超时。如果该命令没有抛出任何异常并返回一个响应,那么Hystrix将在执行一些日志记录和度量报告之后返回此响应。在run()的情况下,Hystrix返回一个可观察的对象,该对象发出单个响应,然后发出一个onCompleted通知;在construct()的情况下,Hystrix返回由construct()返回的相同的可观察值。7.计算电路健康Hystrix向断路器报告成功、失败、拒绝和超时,断路器维护一组滚动计数器,用于计算统计数据。它使用这些统计数据来确定电路应该在什么时候“跳闸”,在这一点上,它会短路任何后续的请求,直到恢复期结束,在此期间,它会在第一次检查某些健康检查之后再次关闭电路。8.回退Hystrix试图恢复你的回滚命令执行失败时:当一个异常的构造()或()运行(6),当命令电路短路,因为打开(4),当命令的线程池和队列或信号能力(5),或者当命令已超过其超时长度。详情参考官网:https://github.com/Netflix/Hy…9. 返回成功的响应如果Hystrix命令成功,它将以可观察到的形式返回响应或响应给调用者。根据您如何调用上面步骤2中的命令,这个可观察对象可能在返回给您之前进行转换:execute() — 以与.queue()相同的方式获取一个Future,然后在这个Future上调用get()来获取可观察对象发出的单个值.queue() — 将可观察对象转换为BlockingObservable,以便将其转换为未来,然后返回此未来observe() — 立即订阅可观察对象,并开始执行命令的流;返回一个可观察对象,当您订阅该对象时,将重播排放和通知toObservable() — 返回可观察值不变;您必须订阅它,才能真正开始执行命令的流程更多原理可以移步官网https://github.com/Netflix/Hy…使用加入依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>在ribbon中使用使用@EnableHystrix开启@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient@EnableHystrixpublic class CloudServiceRibbonApplication { public static void main(String[] args) { SpringApplication.run(CloudServiceRibbonApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); }}该注解对该方法创建了熔断器的功能,并指定了fallbackMethod熔断方法,熔断方法直接返回了一个字符串,字符串为"hi,"+name+",sorry,error!"@Servicepublic class TestService { @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = “hiError”) public String hiService(String name) { return restTemplate.getForObject(“http://CLOUD-EUREKA-CLIENT/hi?name="+name,String.class); } public String hiError(String name) { return “hi,"+name+",sorry,error!”; }}在Feign中使用feign.hystrix.enabled: true 开启hystrixeureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/server: port: 8765spring: application: name: cloud-service-feignfeign.hystrix.enabled: true@EnableFeignClients启动@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient@EnableFeignClientspublic class CloudServiceFeginApplication { public static void main(String[] args) { SpringApplication.run(CloudServiceFeginApplication.class, args); }}fallback:配置连接失败等错误的返回类@FeignClient(value = “cloud-eureka-client”,fallback = TestServiceHystric.class)public interface TestService { @RequestMapping(value = “/hi”,method = RequestMethod.GET) String sayHiFromClientOne(@RequestParam(value = “name”) String name);}当访问接口有问题时,直接调用此接口返回。@Componentpublic class TestServiceHystric implements TestService{ @Override public String sayHiFromClientOne(String name) { return “sorry “+name; }}更多使用技巧可参考官网:https://github.com/Netflix/Hy…总结在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。更多优质文章:http://www.ityouknow.com/spri…https://www.fangzhipeng.com/s...http://blog.didispace.com/tag…最后如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。 ...

April 7, 2019 · 2 min · jiezi

ApiBoot - ApiBoot Alibaba Oss 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。ApiBoot添加快速集成Aliyun的对象存储服务Oss,提供常用的文件操作方法,当然也提供自定义扩展,以致于满足绝大数业务场景,并且通过扩展可以实现上传文件进度条、下载文件进度条、存储空间操作、静态网站托管、访问日志、防盗链、分片上传、追加上传、断点续传等等。引入ApiBoot Alibaba Oss在pom.xml配置文件内添加依赖,如下所示:<!–ApiBoot Alibaba Oss–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-alibaba-oss</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,具体查看ApiBoot版本依赖配置参数列表配置参数参数介绍默认值是否必填api.boot.oss.regionoss所属地域空是api.boot.oss.bucket-nameoss存储空间名称空是api.boot.oss.access-key-id阿里云账户accessKeyId空是api.boot.oss.access-key-secret阿里云账户accessKeySecret空是api.boot.oss.domainoss存储空间所绑定的自定义域名,如果不配置,上传文件成功后返回默认格式化的文件访问路径空否上传文件在使用ApiBoot Oss时,只需要注入ApiBootOssService类就可以完成默认方法的使用,如下所示:@Autowiredprivate ApiBootOssService apiBootOssService;流上传/** * 流方式上传 /@Testpublic void uploadBytes() { ApiBootObjectStorageResponse response = apiBootOssService.upload(“admin.txt”, “admin”.getBytes()); logger.info(“文件名称:{}”, response.getObjectName()); logger.info(“文件访问路径:{}”, response.getObjectUrl());}本地文件上传/** 本地文件上传*/@Testpublic void uploadFile() { ApiBootObjectStorageResponse response = apiBootOssService.upload(“logo.png”, “/Users/yuqiyu/Downloads/logo.png”); logger.info(“文件名称:{}”, response.getObjectName()); logger.info(“文件访问路径:{}”, response.getObjectUrl());}文件流上传/*** 文件流方式上传** @throws Exception*/@Testpublic void uploadInputStream() throws Exception { FileInputStream inputStream = new FileInputStream(new File("/Users/yuqiyu/Downloads/logo.png")); ApiBootObjectStorageResponse response = apiBootOssService.upload(“测试.png”, inputStream); logger.info(“文件名称:{}”, response.getObjectName()); logger.info(“文件访问路径:{}”, response.getObjectUrl());}通过文件的输入流完成对象存储文件的上传下载文件/** * 下载文件 /@Testpublic void download() { apiBootOssOverrideService.download(“测试.png”, “/Users/yuqiyu/Downloads/测试.png”);}在上面的示例中,文件会自动下载到/Users/yuqiyu/Downloads/目录下,文件名称为测试.png。删除文件/** 删除文件示例*/@Testpublic void delete() { apiBootOssOverrideService.delete(“测试.png”);}删除对象存储空间内的文件时只需要传递文件名即可。MultipartFile 上传文件如果你是通过SpringMvc提供的MultipartFile对象进行上传文件,可以通过如下示例进行上传:MultipartFile multipartFile = ..;// 流方式上传ApiBootObjectStorageResponse responseByte = apiBootOssService.upload(“测试.png”, multipartFile.getBytes());// 文件输入流方式上传ApiBootObjectStorageResponse responseIs = apiBootOssService.upload(“测试.png”, multipartFile.getInputStream());自定义扩展ApiBoot Alibaba Oss提供的方法毕竟是有限的,因此ApiBoot提供了自定义的扩展方式,让使用者可以根据Oss官方文档进行扩展,包含上传文件进度条、下载文件进度条、存储空间操作、静态网站托管、访问日志、防盗链、分片上传、追加上传、断点续传等等。自定义扩展首先需要创建类并继承ApiBootOssService,如下所示://…public class ApiBootOssOverrideService extends ApiBootOssService { /** * logger instance / static Logger logger = LoggerFactory.getLogger(ApiBootOssOverrideService.class); /* * 实现父类构造函数 * * @param endpoint 外网节点 * @param bucketName 存储空间名称 * @param accessKeyId 阿里云账号授权Id * @param accessKeySecret 阿里云账号授权Secret * @param domain 自定义域名 / public ApiBootOssOverrideService(String endpoint, String bucketName, String accessKeyId, String accessKeySecret, String domain) { super(endpoint, bucketName, accessKeyId, accessKeySecret, domain); } /* * 创建bucket存储 * * @param bucketName 存储名称 */ public void createBucket(String bucketName) { OSSClient ossClient = getOssClient(); Bucket bucket = ossClient.createBucket(bucketName); logger.info(“新创建存储空间名称:{}”, bucket.getName()); logger.info(“新创建存储空间所属人:{}”, bucket.getOwner().getDisplayName()); closeOssClient(ossClient); }}如上createBucket方法所示ApiBootOssService内部提供了获取OssClient以及关闭OssClient连接的方法,可以直接调用。扩展生效我们自定义的扩展,需要将实例放入SpringIOC容器内,方便我们在使用处进行注入,要注意,由于构造函数参数的原因,无法直接通过@Service或者@Component注解进行标注,需要通过如下方式://…@Bean@ConditionalOnMissingBeanApiBootOssOverrideService apiBootOssOverrideService(ApiBootOssProperties apiBootOssProperties) { return new ApiBootOssOverrideService(apiBootOssProperties.getRegion().getEndpoint(), apiBootOssProperties.getBucketName(), apiBootOssProperties.getAccessKeyId(), apiBootOssProperties.getAccessKeySecret(), apiBootOssProperties.getDomain());}ApiBootOssProperties属性配置类,是ApiBoot内置的,可以在任意地方进行注入,这里目的只是为了拿到相关配置进行构造参数实例化使用。本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-alibaba-oss ...

April 4, 2019 · 1 min · jiezi

ApiBoot - ApiBoot Http Converter 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。FastJson是阿里巴巴提供的一款Json格式化插件。ApiBoot提供了FastJson驱动转换接口请求的Json字符串数据,添加该依赖后会自动格式化时间(格式:YYYY-MM-DD HH:mm:ss)、空对象转换为空字符串返回、空Number转换为0等,还会自动装载ValueFilter接口的实现类来完成自定义的数据格式转换。引入Http ConverterApiBoot Http Converter使用非常简单,只需要在pom.xml添加如下依赖:<!–ApiBoot Http Converter–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-http-converter</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,具体查看ApiBoot版本依赖相关配置ApiBoot Http Converter通过使用SpringBoot内置的配置参数名来确定是否开启,在SpringBoot内可以通过spring.http.converters.preferred-json-mapper来修改首选的Json格式化插件,SpringBoot已经提供了三种,分别是:gson、jackson、jsonb,当我们配置该参数为fastJson或不进行配置就会使用ApiBoot Http Converter提供的fastJson来格式化转换Json返回数据。如下所示:spring: http: converters: # 不配置默认使用fastJson preferred-json-mapper: fastJson自定义ValueFilterValueFilter是FastJson的概念,用于自定义转换实现,比如:自定义格式化日期、自动截取小数点等。下面提供一个ValueFilter的简单示例,具体的使用请参考FastJson官方文档。ValueFilter示例在使用ValueFilter时一般都会搭配一个对应的自定义@Annotation来进行组合使用,保留自定义小数点位数的示例如下所示:创建 BigDecimalFormatter Annotation@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)public @interface BigDecimalFormatter { /** * 小数位数,默认保留两位 * @return / int scale() default 2;}创建 BigDecimal ValueFilterpublic class BigDecimalValueFilter implements ValueFilter { /* * logback / Logger logger = LoggerFactory.getLogger(BigDecimalValueFilter.class); /* * @param object 对象 * @param name 对象的字段的名称 * @param value 对象的字段的值 / @Override public Object process(Object object, String name, Object value) { if (ValidateTools.isEmpty(value) || !(value instanceof BigDecimal)) { return value; } return convertValue(object, name, value); } /* * 转换值 * * @param object 字段所属对象实例 * @param name 字段名称 * @param value 字段的值 * @return / Object convertValue(Object object, String name, Object value) { try { /* * 反射获取field / Field field = object.getClass().getDeclaredField(name); /* *判断字段是否存在@BigDecimalFormatter注解 */ if (field.isAnnotationPresent(BigDecimalFormatter.class)) { BigDecimalFormatter bigDecimalFormatter = field.getAnnotation(BigDecimalFormatter.class); // 执行格式化 BigDecimal decimal = (BigDecimal) value; System.out.println(bigDecimalFormatter.scale()); // 保留小数位数,删除多余 value = decimal.setScale(bigDecimalFormatter.scale(), BigDecimal.ROUND_DOWN).doubleValue(); } } catch (Exception e) { logger.error(“格式化BigDecimal字段出现异常:{}”, e.getMessage()); } return value; }}使用 BigDecimalFormatter Annotation@BigDecimalFormatterprivate BigDecimal decimalValue;本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-http-converter ...

April 4, 2019 · 1 min · jiezi

ApiBoot - ApiBoot Quartz 使用文档

ApiBoot QuartzApiBoot内部集成了Quartz,提供了数据库方式、内存方式的进行任务的存储,其中数据库方式提供了分布式集群任务调度,任务自动平滑切换执行节点。引用ApiBoot Quartz在pom.xml配置文件内添加,如下配置:<!–ApiBoot Quartz–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-quartz</artifactId></dependency>备注:如果使用ApiBoot Quartz的内存方式,仅需要添加上面的依赖即可。相关配置参数名称是否必填默认值描述api.boot.quartz.job-store-type否memory任务存储源方式,默认内存方式api.boot.quartz.scheduler-name否scheduler调度器名称api.boot.quartz.auto-startup否true初始化后是否自动启动调度程序api.boot.quartz.startup-delay否0初始化完成后启动调度程序的延迟。api.boot.quartz.wait-for-jobs-to-complete-on-shutdown否false是否等待正在运行的作业在关闭时完成。api.boot.quartz.overwrite-existing-jobs否false配置的作业是否应覆盖现有的作业定义。api.boot.quartz.properties否 Quartz自定义的配置属性,具体参考quartz配置api.boot.quartz.jdbc否 配置数据库方式的Jdbc相关配置内存方式ApiBoot Quartz在使用内存方式存储任务时,不需要做配置调整。数据库方式需要在application.yml配置文件内修改api.boot.quartz.job-store-type参数,如下所示:api: boot: quartz: # Jdbc方式 job-store-type: jdbcQuartz所需表结构Quartz的数据库方式内部通过DataSource获取数据库连接对象来进行操作数据,所操作数据表的表结构是固定的,ApiBoot把Quartz所支持的所有表结构都进行了整理,访问Quartz支持数据库建表语句列表查看,复制执行对应数据库语句即可。创建任务类我们只需要让新建类集成QuartzJobBean就可以完成创建一个任务类,如下简单示例:/** * 任务定义示例 * 与Quartz使用方法一致,ApiBoot只是在原生基础上进行扩展,不影响原生使用 * <p> * 继承QuartzJobBean抽象类后会在项目启动时会自动加入Spring IOC * * @author:恒宇少年 - 于起宇 * <p> * DateTime:2019-03-28 17:26 * Blog:http://blog.yuqiyu.com * WebSite:http://www.jianshu.com/u/092df3f77bca * Gitee:https://gitee.com/hengboy * GitHub:https://github.com/hengboy /public class DemoJob extends QuartzJobBean { /* * logger instance */ static Logger logger = LoggerFactory.getLogger(DemoJob.class); @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { logger.info(“定时任务Job Key : {}”, context.getJobDetail().getKey()); logger.info(“定时任务执行时所携带的参数:{}”, JSON.toJSONString(context.getJobDetail().getJobDataMap())); //…处理逻辑 }}任务参数在任务执行时传递参数是必须的,ApiBoot Quartz提供了比较方便的传递方式,不过最终Quartz会把传递的值都会转换为String类型数据。任务Key默认值ApiBoot Quartz的newJob方法所创建的定时任务,如果在不传递Job Key参数时,会默认使用UUID随机字符串作为Job Key以及Trigger Key。自定义任务开始时间任务开始时间可以通过startAtTime方法进行设置,在不设置的情况下,任务创建完成后会立刻执行。Cron 表达式任务创建Cron类型任务如下所示:String jobKey = apiBootQuartzService.newJob(ApiBootCronJobWrapper.Context() .jobClass(DemoJob.class) .cron(“0/5 * * * * ?”) .param( ApiBootJobParamWrapper.wrapper().put(“param”, “测试”)) .wrapper());Cron 表达式任务由ApiBootCronJobWrapper类进行构建。上面的DemoJob任务类将会每隔5秒执行一次。Loop 重复任务Loop循环任务,当在不传递重复执行次数时,不进行重复执行,仅仅执行一次,如下所示:String jobKey = apiBootQuartzService.newJob( ApiBootLoopJobWrapper.Context() // 参数 .param( ApiBootJobParamWrapper.wrapper() .put(“userName”, “恒宇少年”) .put(“userAge”, 24) ) // 每次循环的间隔时间,单位:毫秒 .loopIntervalTime(2000) // 循环次数 .repeatTimes(5) // 开始时间,10秒后执行 .startAtTime(new Date(System.currentTimeMillis() + 10000)) // 任务类 .jobClass(DemoJob.class) .wrapper() );Loop 任务由ApiBootLoopJobWrapper类进行构建。上面的定时任务将会重复执行5次,连上自身执行的一次也就是会执行6次,每次的间隔时间为2秒,在任务创建10秒后进行执行。Once 一次性任务Once一次性任务,任务执行一次会就会被自动释放,如下所示:Map paramMap = new HashMap(1);paramMap.put(“paramKey”, “参数值”);String jobKey = apiBootQuartzService.newJob( ApiBootOnceJobWrapper.Context() .jobClass(DemoJob.class) // 参数 .param( ApiBootJobParamWrapper.wrapper() .put(“mapJson”, JSON.toJSONString(paramMap)) ) // 开始时间,2秒后执行 .startAtTime(new Date(System.currentTimeMillis() + 2000)) .wrapper());Once 任务由ApiBootOnceJobWrapper类进行构建。在参数传递时可以是对象、集合,不过需要进行转换成字符串才可以进行使用。暂停任务执行任务在执行过程中可以进行暂停操作,通过ApiBoot Quartz提供的pauseJob方法就可以很简单的实现,当然暂停时需要传递Job Key,Job Key可以从创建任务方法返回值获得。暂停任务如下所示:// 暂停指定Job Key的任务apiBootQuartzService.pauseJob(jobKey);// 暂停多个执行中任务apiBootQuartzService.pauseJobs(jobKey,jobKey,jobKey);恢复任务执行任务执行完暂停后,如果想要恢复可以使用如下方式:// 恢复指定Job Key的任务执行apiBootQuartzService.resumeJob(jobKey);// 恢复多个暂停任务apiBootQuartzService.resumeJobs(jobKey,jobKey,jobKey);修改Cron表达式修改Cron表达式的场景如下:已创建 & 未执行已创建 & 已执行修改方法如下所示:// 修改执行Job Key任务的Cron表达式apiBootQuartzService.updateJobCron(jobKey, “0/5 * * * * ?”);删除任务想要手动释放任务时可以使用如下方式:// 手动删除指定Job Key任务apiBootQuartzService.deleteJob(jobKey);// 手动删除多个任务apiBootQuartzService.deleteJobs(jobKey,jobKey,jobKey);删除任务的顺序如下:暂停触发器移除触发器删除任务本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-quartz ...

April 4, 2019 · 1 min · jiezi

ApiBoot - ApiBoot Swagger 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。ApiBoot通过整合Swagger2完成自动化接口文档生成,只需要一个简单的注解我们就可以实现文档的开启,而且文档上面的所有元素都可以自定义配置,通过下面的介绍来详细了解ApiBoot Swagger的简易之处。引入ApiBoot Swagger在pom.xml配置文件内通过添加如下依赖进行集成:<!–ApiBoot Swagger–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-swagger</artifactId></dependency>注意:ApiBoot所提供的依赖都不需要添加版本号,但是需要添加版本依赖,具体查看ApiBoot版本依赖@EnableApiBootSwagger在添加依赖后需要通过@EnableApiBootSwagger注解进行开启ApiBoot Swagger相关的配置信息自动化构建,可以配置在XxxApplication入口类上,也可以是配置类,让SpringBoot加载到即可。相关配置配置参数参数介绍默认值api.boot.swagger.enable是否启用trueapi.boot.swagger.title文档标题ApiBoot快速集成Swagger文档api.boot.swagger.description文档描述ApiBoot通过自动化配置快速集成Swagger2文档,仅需一个注解、一个依赖即可。api.boot.swagger.base-package文档扫描的packageXxxApplication同级以及子级packageapi.boot.swagger.version文档版本号api.boot.versionapi.boot.swagger.license文档版权ApiBootapi.boot.swagger.license-url文档版权地址https://github.com/hengboy/ap…api.boot.swagger.contact.name文档编写人名称恒宇少年api.boot.swagger.contact.website文档编写人主页http://blog.yuqiyu.comapi.boot.swagger.contact.email文档编写人邮箱地址jnyuqy@gmail.comapi.boot.swagger.authorization.name整合Oauth2后授权名称ApiBoot Security Oauth 认证头信息api.boot.swagger.authorization.key-name整合Oauth2后授权Header内的key-nameAuthorizationapi.boot.swagger.authorization.auth-regex整合Oauth2后授权表达式^.*$以上是目前版本的所有配置参数,大多数都存在默认值,可自行修改。整合ApiBoot Security Oauth如果你的项目添加了Oauth2资源保护,在Swagger界面上访问接口时需要设置AccessToken到Header才可以完成接口的访问,ApiBoot Security Oauth默认开放Swagger所有相关路径,如果项目内并非通过ApiBoot Security Oauth2来做安全认证以及资源保护,需要自行开放Swagger相关路径。整合ApiBoot Security Oauth很简单,访问ApiBoot Security Oauth 查看。携带Token访问Api启动添加ApiBoot-Swagger依赖的项目后,访问http://localhost:8080/swagger-ui.html页面查看Swagger所生成的全部文档,页面右侧可以看到Authorize,点击后打开配置AccessToken的界面,配置的AccessToken必须携带类型,如:Bearer 0798e1c7-64f4-4a2f-aad1-8c616c5aa85b。注意:通过ApiBoot Security Oauth所获取的AccessToken类型都为Bearer。本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-swagger

April 4, 2019 · 1 min · jiezi

SpringCloud之服务调用

简介SpringCloud的服务调用有两个东西:Ribbon是一个客户端的负载均衡器,它提供对大量的HTTP和TCP客户端的访问控制。Feign也是用的Ribbon。原理分析ribbon实现的关键点是为ribbon定制的RestTemplate,ribbon利用了RestTemplate的拦截器机制,在拦截器中实现ribbon的负载均衡。负载均衡的基本实现就是利用applicationName从服务注册中心获取可用的服务地址列表,然后通过一定算法负载,决定使用哪一个服务地址来进行http调用。详情可参考这篇文章https://blog.csdn.net/qq_2059…使用总体流程如下图:服务提供者注:这里需要有个服务注册与发现的注册中心,参考上一篇【SpringCloud之Eureka】创建一个服务提供者:1.pom.xml文件<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>cn.xbmchina</groupId> <artifactId>cloud-parent</artifactId> <version>1.0-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>cn.xbmchina</groupId> <artifactId>cloud-eureka-client</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-eureka-client</name> <description>一个服务提供者</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2.写接口提供给外部调用这里用到@EnableEurekaClient注解,就是说将这个模块向eureka-server端注册,以便其他模块可以发现该服务。@SpringBootApplication@EnableEurekaClient@RestControllerpublic class CloudEurekaClientApplication { public static void main(String[] args) { SpringApplication.run(CloudEurekaClientApplication.class, args); } @Value("${server.port}”) String port; @RequestMapping("/hi”) public String home(@RequestParam(value = “name”, defaultValue = “zero”) String name) { return “hi " + name + " ,i am from port:” + port; }}3.配置文件连接到eureka服务端注:application.name=cloud-eureka-client在下面的其他模块调用时是有用处的。server: port: 8762spring: application: name: cloud-eureka-clienteureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/服务消费者使用ribbon版1.pom.xml文件,添加相关依赖<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>cn.xbmchina</groupId> <artifactId>cloud-parent</artifactId> <version>1.0-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>cn.xbmchina</groupId> <artifactId>cloud-service-ribbon</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-service-ribbon</name> <description>一个服务消费者</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> </dependencies></project>2.利用RestTemplate 调用CLOUD-EUREKA-CLIENT上面定义的服务提供接口注:@LoadBalanced :是为了负载均衡用的。就是说你如果有几台机都是同一接口的话,它会轮流的调用。@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient@EnableHystrix@RestControllerpublic class CloudServiceRibbonApplication { public static void main(String[] args) { SpringApplication.run(CloudServiceRibbonApplication.class, args); } @Bean @LoadBalanced //负载均衡 RestTemplate restTemplate() { return new RestTemplate(); } @Autowired RestTemplate restTemplate; @GetMapping(value = “/hi”) public String hi(@RequestParam String name) { return restTemplate.getForObject(“http://CLOUD-EUREKA-CLIENT/hi?name="+name,String.class); }}3.配置文件连接到eureka服务端注:application.name=cloud-eureka-client在下面的其他模块调用时是有用处的。eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/server: port: 8764spring: application: name: cloud-service-ribbon使用Feign版其实原理跟ribbon没啥差异,与上面不同的地方:它是用接口来弄的使用注解@EnableFeignClients1.pom.xml 依赖 <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>2.在启动类中加入@EnableFeignClients注解@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient@EnableFeignClientspublic class CloudServiceFeginApplication { public static void main(String[] args) { SpringApplication.run(CloudServiceFeginApplication.class, args); }}3.调用服务注:这个是接口;@FeignClient中的值是服务端的名称。@FeignClient(value = “cloud-eureka-client”)public interface TestService { @RequestMapping(value = “/hi”,method = RequestMethod.GET) String helloworld(@RequestParam(value = “name”) String name);}源码参考GitHub:https://github.com/xbmchina/c…总结服务调用类似于用一个浏览器去调用服务器的接口然后接收或传递数据进行下一步的处理,不过这里是可以相互调用。场景:比如很多的电商网站有订单模块,仓库模块,支付模块等之间的相互调用。例如:生成订单,然后仓库模块减少,然后支付模块收到钱,接着通知仓库模块安排配送等。用SpringCloud的分开各个模块,各负其职,这就是微服务的意义之一。最后如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。 ...

April 3, 2019 · 2 min · jiezi

Spring Cloud Eureka 注册中心集群搭建,Greenwich 最新版!

Spring Cloud 的注册中心可以由 Eureka、Consul、Zookeeper、ETCD 等来实现,这里推荐使用 Spring Cloud Eureka 来实现注册中心,它基于 Netflix 的 Eureka 做了二次封装,完成分布式服务中服务治理的功能,微服务系统中的服务注册与发现都通过这个注册中心来进行管理。今天栈长就来分享一个 Eureka 注册中心玩法,从 0 到分布式集群一步到位,单机版的咱就不玩了,没意义。本文基于最新的 Spring Cloud Greenwich.SR1 以及 Spring Boot 2.1.3 版本进行分享。快速构建一个 Eureka Server 项目打开 Spring 的快速构建网址,如下图所示,选择对应的参数,最后选择 Eureka Server 依赖,生成项目示例代码即可。https://start.spring.io/栈长这里是生成了一个 Maven 示例项目。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>cn.javastack</groupId> <artifactId>spring-cloud–eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-cloud–eureka-server</name> <description>Demo project for Spring Cloud Eureka Server</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>主要是加入了 Eureka Server 和 Spring Test 依赖包,还有 Spring Boot 和 Spring Cloud 的基础依赖。Maven就不多介绍了,不熟悉的,请关注Java技术栈微信公众号,在后台回复:Maven,即可获取栈长整理的一系列 Maven 系列教程文章。开启 Eureka Server 功能@EnableEurekaServer@SpringBootApplicationpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }}在启动类上加入 @EnableEurekaServer 注解,@EnableEurekaServer注解即开启注册中心服务器的功能。Spring Boot就不多介绍了,不熟悉的,请关注Java技术栈微信公众号,在后台回复:Boot,即可获取栈长整理的一系列 Spring Boot 系列教程文章。添加 Eureka Server 配置在 application.yml 中加入如下配置:spring: application: name: register-centereureka: instance: prefer-ip-address: false instance-id: ${spring.cloud.client.ip-address}:${server.port} lease-expiration-duration-in-seconds: 30 lease-renewal-interval-in-seconds: 5 server: enable-self-preservation: true eviction-interval-timer-in-ms: 5000 client: register-with-eureka: true fetch-registry: true serviceUrl: defaultZone: http://eureka1:8761/eureka/, http://eureka2:8762/eureka/logging.level.com.netflix: eureka: OFF discovery: OFF—spring: profiles: rc1server: port: 8761eureka.instance.hostname: eureka1—spring: profiles: rc2server: port: 8762eureka.instance.hostname: eureka2配置这里不细讲,下篇文章栈长单独分享这些参数的含义,关注微信公众号:Java技术栈,获取第一时间推送。这里做了两台注册中心的高可用配置rc1,rc2,也可以做多台,既然是高可用,每个注册中心都向别的注册中心注册自己。注意不要用Localhost如上图所示,如果大家在实战中遇到集群不可用,出现在 unavailable-replicas 里面时,说明是你配置的问题。如果 defaultZone 用了 localhost,prefer-ip-address 设置的是 false,则集群不行,不能用 localhost,要配置 hosts,并代替 localhost。127.0.0.1 localhost eureka1 eureka2启动 Eureka 注册中心这样两个注册心的 Eureka Server 就搭好了,启动的时候使用不同的 Profile 来指定不同的端口。spring-boot:run -Dspring-boot.run.profiles=rc1spring-boot:run -Dspring-boot.run.profiles=rc2按上方面命令启动两个 Eureka Server,然后再来验证一下注册情况,分别打开两个 Eureka Server 控制台页面。http://localhost:8761/http://localhost:8762/我们可以看到两个注册的注册中心实例了。好了,今天的分享就到这里了,近期会分享更多 Eureka 高级玩法,栈长正在拼命撰写中……关注Java技术栈微信公众号可获取及时推送。在公众号后台回复:cloud,获取栈长整理的更多的 Spring Cloud 教程,都是实战干货,以下仅为部分预览。Spring Cloud 最新 Finchley 版本踩坑Spring Cloud 多版本如何选择Spring Cloud 是什么,和 Dubbo 对比Spring Cloud 配置中心高可用搭建Spring Cloud Eureka 自我保护机制……大家有什么问题,也可以点击这个链接加入Java技术栈知识星球,和大家共同讨论,也可以向栈长提问,快 2000 人已加入。本文原创首发于微信公众号:Java技术栈(id:javastack),关注公众号在后台回复 “cloud” 可获取更多 Spring Cloud 教程,转载请原样保留本信息。 ...

April 3, 2019 · 2 min · jiezi

两年了,我写了这些干货!

开公众号差不多两年了,有不少原创教程,当原创越来越多时,大家搜索起来就很不方便,因此做了一个索引帮助大家快速找到需要的文章!Spring Boot系列SpringBoot+SpringSecurity处理Ajax登录请求SpringBoot+Vue前后端分离(一):使用SpringSecurity完美处理权限问题1SpringBoot+Vue前后端分离(二):使用SpringSecurity完美处理权限问题2SpringBoot+Vue前后端分离(三):SpringSecurity中密码加盐与SpringBoot中异常统一处理SpringBoot+Vue前后端分离(四):axios请求封装和异常统一处理SpringBoot+Vue前后端分离(五):权限管理模块中动态加载Vue组件SpringBoot+Vue前后端分离(六):使用SpringSecurity完美处理权限问题SpringBoot中自定义参数绑定SpringBoot中使用POI,快速实现Excel导入导出SpringBoot中发送QQ邮件SpringBoot中使用Freemarker构建邮件模板SpringBoot+WebSocket实现在线聊天(一)SpringBoot+WebSocket实现在线聊天(二)SpringSecurity登录使用JSON格式数据SpringSecurity登录添加验证码SpringSecurity中的角色继承问题Spring Boot中通过CORS解决跨域问题Spring Boot数据持久化之JdbcTemplateSpring Boot多数据源配置之JdbcTemplate最简单的SpringBoot整合MyBatis教程极简Spring Boot整合MyBatis多数据源Spring Boot中的yaml配置简介SpringBoot整合Swagger2,再也不用维护接口文档了Spring Boot中,Redis缓存还能这么用!干货|一文读懂 Spring Data Jpa!Spring基础配置Spring常用配置Spring常用配置(二)SpringMVC基础配置SpringMVC常用配置JavaWeb之最简洁的配置实现文件上传初识Spring Boot框架DIY一个Spring Boot的自动配置使用Spring Boot开发Web项目为我们的Web添加HTTPS支持在Spring Boot框架下使用WebSocket实现消息推送一个JavaWeb搭建的开源Blog系统,整合SSM框架Spring Cloud系列1.使用Spring Cloud搭建服务注册中心2.使用Spring Cloud搭建高可用服务注册中心3.Spring Cloud中服务的发现与消费4.Eureka中的核心概念5.什么是客户端负载均衡6.Spring RestTemplate中几种常见的请求方式7.RestTemplate的逆袭之路,从发送请求到负载均衡8.Spring Cloud中负载均衡器概览9.Spring Cloud中的负载均衡策略10.Spring Cloud中的断路器Hystrix11.Spring Cloud自定义Hystrix请求命令12.Spring Cloud中Hystrix的服务降级与异常处理13.Spring Cloud中Hystrix的请求缓存14.Spring Cloud中Hystrix的请求合并15.Spring Cloud中Hystrix仪表盘与Turbine集群监控16.Spring Cloud中声明式服务调用Feign17.Spring Cloud中Feign的继承特性18.Spring Cloud中Feign配置详解19.Spring Cloud中的API网关服务Zuul20.Spring Cloud Zuul中路由配置细节21.Spring Cloud Zuul中异常处理细节22.分布式配置中心Spring Cloud Config初窥23.Spring Cloud Config服务端配置细节(一)24.Spring Cloud Config服务端配置细节(二)之加密解密25.Spring Cloud Config客户端配置细节26.Spring Cloud Bus之RabbitMQ初窥27.Spring Cloud Bus整合RabbitMQ28.Spring Cloud Bus整合Kafka29.Spring Cloud Stream初窥30.Spring Cloud Stream使用细节31.Spring Cloud系列勘误Docker系列Docker教程合集MongoDB系列1.Linux上安装MongoDB2.MongoDB基本操作3.MongoDB数据类型4.MongoDB文档更新操作5.MongoDB文档查询操作(一)6.MongoDB文档查询操作(二)7.MongoDB文档查询操作(三)8.MongoDB查看执行计划9.初识MongoDB中的索引10.MongoDB中各种类型的索引11.MongoDB固定集合12.MongoDB管道操作符(一)13.MongoDB管道操作符(二)14.MongoDB中MapReduce使用15.MongoDB副本集搭建16.MongoDB副本集配置17.MongoDB副本集其他细节18.初识MongoDB分片19.Java操作MongoDBRedis系列教程1.Linux上安装Redis2.Redis中的五种数据类型简介3.Redis字符串(STRING)介绍4.Redis字符串(STRING)中BIT相关命令5.Redis列表与集合6.Redis散列与有序集合7.Redis中的发布订阅和事务8.Redis快照持久化9.Redis之AOF持久化10.Redis主从复制(一)11.Redis主从复制(二)12.Redis集群搭建13.Jedis使用14.Spring Data Redis使用Git系列1.Git概述2.Git基本操作3.Git中的各种后悔药4.Git分支管理5.Git关联远程仓库6.Git工作区储藏兼谈分支管理中的一个小问题7.Git标签管理Elasticsearch系列引言elasticsearch安装与配置初识elasticsearch中的REST接口elasticsearch修改数据elasticsearch文档操作elasticsearch API约定(一)elasticsearch API约定(二)elasticsearch文档读写模型elasticsearch文档索引API(一)elasticsearch文档索引API(二)elasticsearch文档Get APIelasticsearch文档Delete APIelasticsearch文档Delete By Query API(一)elasticsearch文档Delete By Query API(二)elasticsearch文档Update API我的Github开源项目开源项目(一): SpringBoot+Vue前后端分离开源项目-微人事开源项目(二): SpringBoot+Vue前后端分离开源项目-V部落开源项目(三): 一个开源的五子棋大战送给各位小伙伴!开源项目(四):一个开源的会议管理系统献给给位小伙伴!开源项目(五):一个JavaWeb搭建的开源Blog系统,整合SSM框架杂谈从高考到程序员之毕业流水帐从高考到现在起早贪黑几个月,我写完了人生第一本书!当公司倒闭时,你在干什么?华为云 open day,带你看看别人家的公司其他小程序开发框架WePY和mpvue使用感受两步解决maven依赖导入失败问题干货|6个牛逼的基于Vue.js的后台控制面板,接私活必备Ajax上传图片以及上传之前先预览一个简单的案例带你入门Dubbo分布式框架WebSocket刨根问底(一)WebSocket刨根问底(二)WebSocket刨根问底(三)之群聊Nginx+Tomcat搭建集群,Spring Session+Redis实现Session共享IntelliJ IDEA中创建Web聚合项目(Maven多模块项目)Linux上安装Zookeeper以及一些注意事项初识ShiroShiro中的授权问题Shiro中的授权问题(二)更多资料,请关注公众号牧码小子,回复 Java, 获取松哥为你精心准备的Java干货! ...

April 3, 2019 · 1 min · jiezi

Spring 官方文档完整翻译

以下所有文档均包含多个版本,并支持多语言(英文及中文)。Spring Boot 中文文档Spring Framework 中文文档Spring Cloud 中文文档Spring Security 中文文档Spring Session 中文文档Spring AMQP 中文文档Spring DataSpring Data JPASpring Data JDBCSpring Data RedisContributing如果你希望参与文档的校对及翻译工作,请在 这里 提 PR。

April 3, 2019 · 1 min · jiezi

SpringCloud之Consul

注:尊重原创,本文参考这位大神【纯洁的微笑】的,因为他写得太好了简介Consul是一个分布式服务注册与发现,用于跨任何运行时平台和公共或私有云连接、保护和配置服务。它内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案。它是GO语言写。官方文档:https://www.consul.io/docs/gu…原理它使用 Raft 算法(https://www.jdon.com/artichec…)来保证一致性, 比复杂的 Paxos 算法更直接. 相比较而言, zookeeper 采用的是 Paxos.client: 客户端, 无状态, 将 HTTP 和 DNS 接口请求转发给局域网内的服务端集群。server: 服务端, 保存配置信息, 高可用集群, 在局域网内与本地客户端通讯, 通过广域网与其它数据中心通讯。 每个数据中心的 server 数量推荐为 3 个或是 5 个(一般是奇数,不要问我为啥)。Consul 客户端、服务端还支持夸中心的使用,更加提高了它的高可用性。1、当 Producer 启动的时候,会向 Consul 发送一个 post 请求,告诉 Consul 自己的 IP 和 Port2、Consul 接收到 Producer 的注册后,每隔10s(默认)会向 Producer 发送一个健康检查的请求,检验Producer是否健康3、当 Consumer 发送 GET 方式请求 /api/address 到 Producer 时,会先从 Consul 中拿到一个存储服务 IP 和 Port 的临时表,从表中拿到 Producer 的 IP 和 Port 后再发送 GET 方式请求 /api/address4、该临时表每隔10s会更新,只包含有通过了健康检查的 Producer使用安装Consul 不同于 Eureka 需要单独安装,访问Consul 官网下载 Consul 的最新版本,我这里是 consul_1.4.4。服务生产者producer1.pom.xml,添加相关依赖<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>cn.xbmchina</groupId> <artifactId>cloud-parent</artifactId> <version>1.0-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>cn.xbmchina</groupId> <artifactId>cloud-consul-producer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-consul-producer</name> <description>consul-producer</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>启动类中需要设置@EnableDiscoveryClient@SpringBootApplication@EnableDiscoveryClientpublic class CloudConsulProducerApplication { public static void main(String[] args) { SpringApplication.run(CloudConsulProducerApplication.class, args); } @RequestMapping("/hello”) public String hello() { return “helle consul”; }}application.yml配置相关端口和服务名称spring: application: name: spring-cloud-consul-producer2 cloud: consul: host: localhost port: 8500 discovery: ##注册到consul的服务名称 service-name: service-producer server: port: 8505服务消费者consumer同理也需要添加相应的pom.xml<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>cn.xbmchina</groupId> <artifactId>cloud-parent</artifactId> <version>1.0-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>cn.xbmchina</groupId> <artifactId>cloud-consul-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-consul-consumer</name> <description>consul消费者</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>调用生产者Producer中的服务import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cloud.client.ServiceInstance;import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;@RestControllerpublic class CallHelloController { @Autowired private LoadBalancerClient loadBalancer;//用于负载均衡 @RequestMapping("/call”) public String call() { ServiceInstance serviceInstance = loadBalancer.choose(“service-producer”); System.out.println(“服务地址:” + serviceInstance.getUri()); System.out.println(“服务名称:” + serviceInstance.getServiceId()); String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + “/hello”, String.class); System.out.println(callServiceResult); return callServiceResult; }}application.yml配置连接注册到consulspring: application: name: cloud-consul-consumer cloud: consul: host: 127.0.0.1 port: 8500 discovery: ##设置不需要注册到 consul 中 register: falseserver: port: 8503效果:访问地址http://localhost:8503/call,如果报错的话,你可以看一下日志,是否是你本机名没在hosts中配置,导致的。总结Consul 真厉害!!最后如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。 ...

April 1, 2019 · 2 min · jiezi

openfegin一个小问题 Illegal character in path at index

使用openfeign遇到一个简单错误:Caused by: java.net.URISyntaxException: Illegal character in path at index 32: http://test-service/v1/tests/py/{pid} at java.net.URI$Parser.fail(URI.java:2848) ~[?:1.8.0_181] at java.net.URI$Parser.checkChars(URI.java:3021) ~[?:1.8.0_181] at java.net.URI$Parser.parseHierarchical(URI.java:3105) ~[?:1.8.0_181] at java.net.URI$Parser.parse(URI.java:3053) ~[?:1.8.0_181] at java.net.URI.<init>(URI.java:588) ~[?:1.8.0_181] at java.net.URI.create(URI.java:850) ~[?:1.8.0_181] at org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient.execute(LoadBalancerFeignClient.java:56) ~[spring-cloud-openfeign-core-2.0.2.RELEASE.jar:2.0.2.RELEASE] at org.springframework.cloud.sleuth.instrument.web.client.feign.TraceLoadBalancerFeignClient.execute(TraceLoadBalancerFeignClient.java:67) ~[spring-cloud-sleuth-core-2.0.2.RELEASE.jar:2.0.2.RELEASE] at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:98) ~[feign-core-9.7.0.jar:?] at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:77) ~[feign-core-9.7.0.jar:?]从错误上看,这只是一个非法的url造成的,但是实际检查发现,根本原因不是这个,我有一个commonservice需要调用testservice,调用代码:@GetMapping(value= “v1/tests/py/{pid}”) Message<List<PPyVO>> getPPy(@PathVariable(“pid”) Long pid);发现调用这个接口的时候,传递的参数是null,原来我认为就算是null也应该把{pid}给替换掉,但是实际上并不会,所以产生了这个问题。

March 18, 2019 · 1 min · jiezi

SpringCloud服务间调用

本篇简介在上一篇我们介绍了SpringCloud中的注册中心组件Eureka。Eureka的作用是做服务注册与发现的,目的是让不同的服务与服务之间都可以通过注册中心进行间接关联,并且可以通过注册中心有效的管理不同服务与服务的运行状态。但在微服务的架构中,服务与服务只知道对方的服务地址是没有用的,它们的本质还是需要彼此进行通信的,这也是微服务最核心的功能之一。既然提到了服务与服务之间的通信,那我们自然而然会想到大名鼎鼎的HttpClient。因为在其它的项目架构中我们基本都可以通过它来进行不同服务与服务之间的调用。在SpringCloud中我们依然可以使用HttpClient进行服务与服务调用,只不过如果采用HttpClient调用的话,会有一些弊端。例如: 如果同一个服务有多个负载的话,采用HttpClient调用时,没有办法处理负载均衡的问题。还有另一个问题就是HttpClient只是提供了核心调用的方法并没有对调用进行封装,所以在使用上不太方便,需要自己对HttpClient进行简单的封装。调用方式在SpringCloud中为了解决服务与服务调用的问题,于是提供了两种方式来进行调用。也就是RestTemplate和Feign。虽然从名字上看这两种调用的方式不同,但在底层还是和HttpClient一样,采用http的方式进行调用的。只不过是对HttpClient进行的封装。下面我们来详细的介绍一下这两种方式的区别,我们首先看一下RestTemplate的方式。RestTemplate方式调用RestTemplate为了方便掩饰我们服务间的调用,所以我们需要创建三个项目。它们分别为eureka(注册中心)、server(服务提供方)、client(服务调用方)。因为上一篇中我们已经介绍了eureka的相关内容。所以在这一篇中我们将不在做过多的介绍了。下面我们看一下server端的配置。因为实际上Server端和Client端是相互的。不一定client端一定要调用server端。server端一样可以调用client端。但对于eureka来说,它们都是client端。因为上一篇中我们已经介绍了eureka是分为server端和client端的,并且已经介绍client端相关内容。所以我们下面我们直接看一下server端的配置内容:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-feign-serverserver: port: 8082为了掩饰我们服务间的调用,所以我们需要创建一个Controller,并编写一个简单的接口来供client调用。下面为server的源码。package com.jilinwula.feign.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/server")public class Controller { @GetMapping("/get") public Object get() { Map<String, String> map = new HashMap<String, String>(); map.put(“code”, “0”); map.put(“msg”, “success”); map.put(“data”, “吉林乌拉”); return map; }}下面我们访问一下这个接口看看,是否能正确返回数据。(备注:注意别忘记了在启动类上添加@EnableEurekaClient注解。)下面我们还是使用.http文件的方式发起接口请求。GET http://127.0.0.1:8082/server/get返回结果:GET http://127.0.0.1:8082/server/getHTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Fri, 15 Mar 2019 08:20:33 GMT{ “msg”: “success”, “code”: “0”, “data”: “吉林乌拉”}Response code: 200; Time: 65ms; Content length: 42 bytes我们看已经成功的返回了接口的数据了。下面我们看一下eureka。看看是否成功的检测到了server端的服务。下面为eureka管理界面地址:http://127.0.0.1:8761 我们看eureka已经成功的检测到了server端注册成功了。下面我们看一下client端的代码,我们还是向server端一样,创建一个Controller,并编写一个接口。下面为具体配置及代码。application.yml:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-feign-clientserver: port: 8081 Controller:package com.jilinwula.feign.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/client")public class Controller { @GetMapping("/get") public Object get() { Map<String, String> map = new HashMap<String, String>(); map.put(“code”, “0”); map.put(“msg”, “success”); map.put(“data”, “吉林乌拉”); return map; }}下面为访问的接口地址:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Fri, 15 Mar 2019 08:56:42 GMT{ “msg”: “success”, “code”: “0”, “data”: “吉林乌拉”}Response code: 200; Time: 273ms; Content length: 42 bytes现在我们在访问一下Eureka地址看一下Client服务注册的是否成功。http://127.0.0.1:8761 RestTemplate实例化我们发现server和client端都已经成功的在注册中心注册成功了。这也就是我们接下来要介绍的服务间调用的前提条件。在开发Spring项目时我们知道如果我们想要使有哪个类或者哪个对象,那就需要在xml中或者用注解的方式实例化对象。所以既然我们打算使用RestTemplate类进行调用,那我们必须要先实例化RestTemplate类。下面我们就看一下怎么在实例化RestTemplate类。因为不论采用的是RestTemplate方式调用还是采用Feign方式,均是在服务的client端进行开发的,在服务的server是无需做任何更改的。所以下面我们看一下client端的改动。下面为项目源码:package com.jilinwula.feign;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.context.annotation.Bean;import org.springframework.web.client.RestTemplate;@SpringBootApplication@EnableEurekaClientpublic class JilinwulaSpringcloudFeignClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudFeignClientApplication.class, args); } @Bean public RestTemplate initRestTemplate() { return new RestTemplate(); }}RestTemplate调用方式一为了掩饰方便我们直接在启动类上添加了一个@Bean注解。然后手动实例化了一个对象,并且要特别注意,在使用RestTemplate时,必须要先实例化,否则会抛出空指针异常。下面我们演示一下怎么使用RestTemplate来调用server端的接口。下面为Controller中的代码的改动。package com.jilinwula.feign.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;@RestController@RequestMapping("/client")public class Controller { @Autowired private RestTemplate template; @GetMapping("/get") public Object get() { String result = template.getForObject(“http://127.0.0.1:8082/server/get”, String.class); return result; }}上面的代码比较简单,我们就不详细的介绍了,主要是RestTemplate中提供了getForObject方法(实际上RestTemplate提供了很多种调用的方法,主要分为Get或者Post),可以指定要调用接口的地址,指定返回的值的类型。然后就会直接返回要调用接口的结果。下面我们测试一下,还是调用client接口,看看能否正确的返回server端的数据。http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Fri, 15 Mar 2019 09:42:02 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 362ms; Content length: 42 bytesRestTemplate调用方式二我们看结果,已经成功的返回的server端的数据了,虽然返回的数据没有格式化,但返回的结果数据确实是server端的数据。这也就是RestTemplate的简单使用。但上述的代码是有弊端的,因为我们直接将调用的server端的接口地址直接写死了,这样当服务接口变更时,是需要更改客户端代码的,这显示是不合理的。那怎么办呢?这时就知道注册中心的好处了。因为注册中心知道所有服务的地址,这样我们通过注册中心就可以知道server端的接口地址,这样就避免了server端服务更改时,要同步更改client代码了。下面我们在优化一下代码,看看怎么通过注册中心来获取server端的地址。package com.jilinwula.feign.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cloud.client.ServiceInstance;import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;@RestController@RequestMapping("/client")public class Controller { @Autowired private RestTemplate template; @Autowired private LoadBalancerClient loadBalancerClient; @GetMapping("/get") public Object get() { ServiceInstance serviceInstance = loadBalancerClient.choose(“jilinwula-springcloud-feign-server”); String url = String.format(“http://%s:%s/server/get”, serviceInstance.getHost(), serviceInstance.getPort()); String result = template.getForObject(url, String.class); return result; }}在SpringClourd中提供了LoadBalancerClient接口。通过这个接口我们可以通过用户中心的Application的名字来获取该服务的地址和端口。也就是下图中红色标红的名字(注意名字大小写)。 通过这些我们就可以获取到完整的服务接口地址了,这样就可以直接通过RestTemplate进行接口调用了。下面我们在看一下调用的结果。接口地址:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 09:08:32 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 53ms; Content length: 42 bytesRestTemplate调用方式三这样我们就解决了第一次服务接口地址写死的问题了。但上述的接口还有一个弊端就是我们每次调用服务时都要先通过Application的名字来获取ServiceInstance对象,然后才可以发起接口调用。实际上在SpringCloud中为我们提供了@LoadBalanced注解,只要将该注解添加到RestTemplate中的获取的地方就可以了。下面为具体修改:启动类:package com.jilinwula.feign;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.loadbalancer.LoadBalanced;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.context.annotation.Bean;import org.springframework.web.client.RestTemplate;@SpringBootApplication@EnableEurekaClientpublic class JilinwulaSpringcloudFeignClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudFeignClientApplication.class, args); } @Bean @LoadBalanced public RestTemplate initRestTemplate() { return new RestTemplate(); }}我们在RestTemplate实例化的地方添加了@LoadBalanced注解,这样在我们使用RestTemplate时就该注解就会自动将调用接口的地址替换成真正的服务地址。下面我们看一下Controller中的改动:package com.jilinwula.feign.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;@RestController@RequestMapping("/client")public class Controller { @Autowired private RestTemplate template; @GetMapping("/get") public Object get() { String url = String.format(“http://%s/server/get”, “jilinwula-springcloud-feign-server”); String result = template.getForObject(url, String.class); return result; }}代码和第一次的代码基本一样,唯一的区别就是获取服务地址和端口的地方替换成了注册中心中的Application的名字,并且我们的RestTemplate在使用上和第一次没有任何区别,只是在url中不同。下面我们看一下返回的结果。GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 09:55:46 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 635ms; Content length: 42 bytes默认负载均衡策略上述内容就是使用RestTemplate来进行服务间调用的方式。并且采用这样的方式可以很方便的解决负载均衡的问题。因为@LoadBalanced注解会自动采用默信的负载策略。下面我们看验证一下SpringCloud默认的负载策略是什么。为了掩饰负载策略,所以我们在新增一个server服务,并且为了掩饰这两个server返回结果的不同,我们故意让接口返回的数据不一致,来方便我们测试。下面为新增的server服务端的配置信息及controller源码。application.yml:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-feign-serverserver: port: 8083 Controller:package com.jilinwula.feign.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/server")public class Controller { @GetMapping("/get") public Object get() { Map<String, String> map = new HashMap<String, String>(); map.put(“code”, “0”); map.put(“msg”, “success”); map.put(“data”, “jilinwula”); return map; }}调用以下接口:GET http://127.0.0.1:8083/server/get返回结果:GET http://127.0.0.1:8083/server/getHTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Sat, 16 Mar 2019 10:49:07 GMT{ “msg”: “success”, “code”: “0”, “data”: “jilinwula”}Response code: 200; Time: 100ms; Content length: 47 bytes现在我们访问一下注册中心看一下现在注册中心的变化。注册中心地址:http://127.0.0.1:8761 我们看上图注册中心已经显示Application名字为JILINWULA-SPRINGCLOUD-FEIGN-SERVER的有两个服务已经注册成功了。下面我们直接调用client中的接口,看一下client默认会返回哪个server端的信息。client接口地址:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 47Date: Sat, 16 Mar 2019 10:58:39 GMT{“msg”:“success”,“code”:“0”,“data”:“jilinwula”}Response code: 200; Time: 24ms; Content length: 47 bytes看上面返回的结果是server2的接口数据。我们在请求一下接口在看一下返回的结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 11:01:01 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 15ms; Content length: 42 bytes更改默认负载均衡策略一我们看这回返回的接口数据就是第一个server端的信息了。并且我们可以频繁的调用client中的接口,并观察发现它们会交替返回的。所以我们基本可以确定SpringCloud默认的负载策略为轮询方式。也就是会依次调用。在SpringCloud中提供了很多种负载策略。比较常见的为:随机、轮询、哈希、权重等。下面我们介绍一下怎么修改默认的负载策略。SpringCloud底层采用的是Ribbon来实现的负载均衡。Ribbon是一个负载均衡器,Ribbon的核心组件为IRule,它也就是所有负载策略的父类。下面为IRule接口的源码:package com.netflix.loadbalancer;public interface IRule { Server choose(Object var1); void setLoadBalancer(ILoadBalancer var1); ILoadBalancer getLoadBalancer();}该类只提供了3个方法,它们的作用分别是选择一个服务名字、设置ILoadBalancer和返回ILoadBalancer。下面我们看一下IRule接口的常见策略子类。常见的有RandomRule、RoundRobinRule、WeightedResponseTimeRule等。分别对应着随机、轮询、和权重。下面我们看一下怎么更改默认的策略方式。更改默认策略也是在client端中操作的,所以我们看一下client端的代码更改:package com.jilinwula.feign;import com.netflix.loadbalancer.IRule;import com.netflix.loadbalancer.RandomRule;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.loadbalancer.LoadBalanced;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.context.annotation.Bean;import org.springframework.web.client.RestTemplate;@SpringBootApplication@EnableEurekaClientpublic class JilinwulaSpringcloudFeignClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudFeignClientApplication.class, args); } @Bean @LoadBalanced public RestTemplate initRestTemplate() { return new RestTemplate(); } @Bean public IRule initIRule() { return new RandomRule(); }}我们在启动类上新实例化了一个IRule对象,并且指定该对象实例化的子类为RandomRule,也就是随机的方式。所以当我们Client端启动服务调用服务时,就会采用随机的方式进行调用,因为我们已经将IRule对象默认的实例化方式更改了。下面我们测试一下,继续访问Client端接口:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 11:36:01 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 15ms; Content length: 42 bytes更改默认负载均衡策略二在这里我们就不依依演示了,但如果我们多次调用接口就会发现,Client接口返回的结果不在是轮询的方式了,而是变成了随机了,这就说明我们已经成功的将SpringCloud默认的负载策略更改了。下面我们换一种方式来更改默认的负载策略。这种方式和上面的有所不同,而是在配置文件中配置的,下面为具体的配置。(备注:为了不影响测试效果,我们需要将刚刚在启动类中的实例化的IRule注释掉)eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-feign-clientserver: port: 8081jilinwula-springcloud-feign-server: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule我们在配置文件中指定了注册中心中的server端的Application名字,然后指定了默认的负载策略类。下面我们测试一下。访问以下接口:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 11:54:42 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 13ms; Content length: 42 bytesFeign方式调用我们在实际的开发中,可以使用上述两种方式来更改SpringCloud中默认的负载策略。下面我们看一下SpringCloud中另一种服务间调用方式也就是Feign方式。使用Feign方式和RestTemplate不同,我们需要先添加Feign的依赖,具体依赖如下(备注:该依赖同样是在client端添加的):pom.xml:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.1.1.RELEASE</version></dependency>其次我们还需要在启动类中添加@EnableFeignClients注解。具体代码如下:package com.jilinwula.feign;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.cloud.netflix.feign.EnableFeignClients;@SpringBootApplication@EnableEurekaClient@EnableFeignClientspublic class JilinwulaSpringcloudFeignClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudFeignClientApplication.class, args); }}接下来我们需要在Client端创建一个新的接口并定义Client端需要调用的服务方法。具体代码如下:package com.jilinwula.feign.server;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;@FeignClient(name = “jilinwula-springcloud-feign-server”)public interface ServerApi { @GetMapping("/server/get") String get();}上述接口基本上和server端的Controller一致,唯一的不同就是我们指定了@FeignClient注解,该注解的需要指定一个名字,也就是注册中心中Applicaiton的名字,也就是要调用的服务名字。下面我们看一下Controller中的代码更改:package com.jilinwula.feign.controller;import com.jilinwula.feign.server.ServerApi;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/client")public class Controller { @Autowired private ServerApi serverApi; @GetMapping("/get") public Object get() { String result = serverApi.get(); return result; }}我们在Controller中直接使用了我们自定义的接口,并直接调用我们接口中定义的方法,下面我们调用一下Client接口看看这样的方式是否可以调用成功。接口地址:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 12:54:50 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 14ms; Content length: 42 bytes我们看这样的方式也是可以成功的调用server端的接口的,只不过这样的方式可能会让觉的不太方便,因为这样的方式是需要Client端定义和Server端一样的接口的。上述内容就是本篇的全部内容,在实际的项目开发中,这两种方式均可实现服务与服务间的调用,并且这两种方式都有彼此的弊端,所以并没有特别推荐的方式。在下一篇中,我们将介绍配置中心相关内容,谢谢。项目源码https://github.com/jilinwula/jilinwula-springcloud-feign原文链接http://jilinwula.com/article/… ...

March 16, 2019 · 3 min · jiezi

服务的熔断机制 | 从0开始构建SpringCloud微服务(14)

照例附上项目github链接。本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程。本章主要讲解服务的熔断机制。什么是服务的熔断机制对该服务的调用执行熔断,对于后续请求,不再继续调用该目标服务,而是直接返回,从而可以快速释放资源。有利于保护系统。熔断机制 : 当服务过载了,或者是流量激增,超过了服务的负荷,使用熔断机制,将服务掐断,保护自己不至于崩溃的机制。熔断机制是对系统的防护。当有非常大的请求数目来访问我们的服务的时候,若请求超出了我们的承受范围,或者是超过了我们预先设定的某个阈值,我们就将服务断掉,响应给用户一个默认的值。这个值不是用户所请求的值,到那时这个值可能包含了一些有价值的提示性的信息,如告诉用户我们的服务已经断掉了,请稍后再访问等。基于Hystrix的服务熔断机制原理我们可以把熔断器想象为一个保险丝,在电路系统中,一般在所有的家电系统连接外部供电的线路中间都会加一个保险丝,当外部电压过高,达到保险丝的熔点时候,保险丝就会被熔断,从而可以切断家电系统与外部电路的联通,进而保障家电系统不会因为电压过高而损坏。Hystrix提供的熔断器就有类似功能,当在一定时间段内服务调用方调用服务提供方的服务的次数达到设定的阈值,并且出错的次数也达到设置的出错阈值,就会进行服务降级,让服务调用方之间执行本地设置的降级策略,而不再发起远程调用。但是Hystrix提供的熔断器具有自我反馈,自我恢复的功能,Hystrix会根据调用接口的情况,让熔断器在closed,open,half-open三种状态之间自动切换。open状态说明打开熔断,也就是服务调用方执行本地降级策略,不进行远程调用。closed状态说明关闭了熔断,这时候服务调用方直接发起远程调用。half-open状态,则是一个中间状态,当熔断器处于这种状态时候,直接发起远程调用。三种状态的转换:closed->open:正常情况下熔断器为closed状态,当访问同一个接口次数超过设定阈值并且错误比例超过设置错误阈值时候,就会打开熔断机制,这时候熔断器状态从closed->open。open->half-open:当服务接口对应的熔断器状态为open状态时候,所有服务调用方调用该服务方法时候都是执行本地降级方法,那么什么时候才会恢复到远程调用那?Hystrix提供了一种测试策略,也就是设置了一个时间窗口,从熔断器状态变为open状态开始的一个时间窗口内,调用该服务接口时候都委托服务降级方法进行执行。如果时间超过了时间窗口,则把熔断状态从open->half-open,这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用还是失败,则重新设置熔断器状态为open状态,从新记录时间窗口开始时间。half-open->closed: 当熔断器状态为half-open,这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用成功,则重新设置熔断器状态为closed状态。如何集成Hystrix这里我们以之前的天气预报微服务系统为例,讲解如何在其中集成Hystrix。由于在天气预报微服务系统中,天气预报微服务将会调用多个其他微服务,所以我们让该服务集成Hystrix,当其他被调用的服务挂掉的时候,将会使熔断状态进入open模式,从而向用户返回一些默认的信息。添加依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> <version>2.1.1.RELEASE</version> </dependency>@EnableCircuitBreaker注解@SpringBootApplication@EnableDiscoveryClient@EnableFeignClients@EnableCircuitBreakerpublic class Sifoudemo02Application { public static void main(String[] args) { SpringApplication.run(Sifoudemo02Application.class, args); }}书写回调方法在方法前面加上@HystrixCommand注解,启用断路器,由于这个方法我们是依赖城市数据API微服务,与天气数据API微服务的,所以当这两个微服务异常之后,就会触发这个熔断机制。fallbackMethod为我们医用熔断机制后,请求失败时默认调用的方法,该方法的返回类型要与原方法一致。 //传入城市Id得到对应城市的天气数据 @GetMapping("/cityId/{cityId}") @HystrixCommand(fallbackMethod=“defaulGetReprotByCityId”) public Weather getReportByCityId(@PathVariable(“cityId”)String cityId)throws Exception{ //获取城市Id列表 //由城市数据API微服务来提供数据 List<City>cityList=null; try { cityList=dataClient.listCity(); }catch (Exception e) { logger.error(“Exception!",e); } Weather weather=weatherReportService.getDataByCityId(cityId); return weather; } public Weather defaulGetReprotByCityId(String cityId) { Weather weather=new Weather(); return weather; }运行测试在测试时我们将天气数据API微服务,与城市数据API微服务关闭,然后发送请求,得到的为默认返回的天气数据。也可以返回一段信息,提示用户服务不可用。

March 16, 2019 · 1 min · jiezi

微服务的集中化配置 | 从0开始构建SpringCloud微服务(13)

照例附上项目github链接。本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程。本章主要讲解微服务的集中化配置。微服务为什么需要集中化配置微服务数量多,配置多手工管理配置繁琐使用Config实现Server端的配置中心集成Config Server添加依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>@EnableConfigServer注解//@ServletComponentScan(basePackages=“com.demo.web.servlet”)@SpringBootApplication@EnableDiscoveryClient@EnableConfigServerpublic class Sifoudemo02Application { public static void main(String[] args) { SpringApplication.run(Sifoudemo02Application.class, args); }}修改配置文件spring.application.name: msa-weather-config-serverserver.port= 8088eureka.client.serviceUrl.defaultZone: http://localhost:8761/eureka/spring.cloud.config.server.git.uri=https://github.com/ShimmerPig/spring-cloud-config-server-test01spring.cloud.config.server.git.searchPaths=config-repo这里指定的是我github上面的一个仓库将服务端启动后,发送请求进行访问,可以查看在云端配置中心的配置文件,当以后需要使用繁多的配置时,我们可以通过此方法对服务进行集中化的配置。

March 16, 2019 · 1 min · jiezi

spring cloud consul注册的服务有报错 critical

测试spring cloud 使用consul注册服务的时候,出现critical,如下:怎么解决这个问题,现在只能看到health check检查失败了。受限调用这个请求Get http://consulIp:8500/v1/agent/checks,调完请求,就会拿到返回数据:{ …… “service:test-service-xx-xx-xx-xx”: { “Node”: “zookeeper-server1”, “CheckID”: “service:test-service-xx-xx-xx-xx”, “Name”: “Service ’test-service’ check”, “Status”: “critical”, “Notes”: “”, “Output”: “HTTP GET http://xxx.xx.xxx.xxx:19008/actuator/health: 404 Output: <html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id=‘created’>Fri Mar 15 11:03:30 CST 2019</div><div>There was an unexpected error (type=Not Found, status=404).</div><div>No message available</div></body></html>”, “ServiceID”: “test-service-xx-xx-xx-xx”, “ServiceName”: “test-service”, “ServiceTags”: [ “version=1.0”, “secure=false” ], “Definition”: {}, “CreateIndex”: 0, “ModifyIndex”: 0 } ……..}就能看到consul调用http://xxx.xx.xxx.xxx:19008/actuator/health来检查servoce健康,却发现接口404,所以才会在页面出现错误。我的测试环境是:spring cloud Finch1ey.SR2consul v1.4.3bootstrap.yml配置是:spring: cloud: consul: host: xxx.xxx.xxx.xxx port: 8500 discovery: prefer-ip-address: true tags: version=1.0 instance-id: ${spring.application.name}:${spring.cloud.client.ip-address} healthCheckInterval: 15s health-check-url: http://${spring.cloud.client.ip-address}:${server.port}/actuator/health显然consul不能在这个服务上找到actuator/health接口,因为我用了actuator,所以service中应该配置了spring cloud actuator。经过检查发现没有配置,所以actuator这个端点不能使用,加上这个包,问题就解决了。 ...

March 15, 2019 · 1 min · jiezi

spring cloud consul使用ip注册服务

我测试spring cliud使用consul作为注册中心的时候,发现服务注册的时候,注册的都是hostname,比如:注册了一个commonservice,在consul中是这样的:{ “ID”:“commonservice123”, “address”:“testcommonserver” ……..}这肯定是不对的。加入我有一个服务payservice需要调用commonservice,payservice从consul中获取的commonservice的地址是testcommonserver,而payservice所在的服务器地址是121.57.68.98上,这台服务器无法解析hostname是testcommonserver的服务器的ip地址,无法调用commonservie,这时候就会报下面这个错误:unKnownHostException…….为了解决这个问题,我需要在注册服务的时候,让服务以ip的方式注册,我的测试环境是:spring cloud Finch1ey.SR2consul v1.4.3修改bootstrap.yml配置文件:spring: cloud: consul: host: xxx.xxx.xxx.xxxx port: 8500 discovery: prefer-ip-address: true //这个必须配 tags: version=1.0 instance-id: ${spring.application.name}:${spring.cloud.client.ip-address} healthCheckInterval: 15s health-check-url: http://${spring.cloud.client.ip-address}:${server.port}/actuator/health${spring.cloud.client.ip-address}这个属性是spring cloud内置,用来获取ip,不同的spring cloud版本可能稍有不同,如果想要确定自己的版本是什么样的,可以查看这个文件:HostInfoEnvironmentPostProcessor @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { InetUtils.HostInfo hostInfo = getFirstNonLoopbackHostInfo(environment); LinkedHashMap<String, Object> map = new LinkedHashMap<>(); map.put(“spring.cloud.client.hostname”, hostInfo.getHostname()); map.put(“spring.cloud.client.ip-address”, hostInfo.getIpAddress()); MapPropertySource propertySource = new MapPropertySource( “springCloudClientHostInfo”, map); environment.getPropertySources().addLast(propertySource); }这时候再启动项目测试,发现注册地址变了:{ “ID”:“commonservice123”, “address”:“10.52.xx.xx” ……..}注册的address变成了服务的内网地址,如果其它服务和commonservice在同一个网络中,可以通过内网访问,这样也是可以的,但是如果内网不能访问,其它服务仍然不能访问,这时候就需要注册服务的时候以公网的ip注册才行。修改bootstrap.yml配置文件:spring: cloud: consul: host: xxx.xxx.xxx.xxx port: 8500 config: data-key: data format: yaml discovery: prefer-ip-address: true //这个必须配 tags: version=1.0 instance-id: ${spring.application.name}:${spring.cloud.client.ip-address} healthCheckInterval: 15s health-check-url: http://${spring.cloud.client.ip-address}:${server.port}/actuator/health inetutils: preferred-networks: - 公网ip1 - 公网ip2可以看到增加一个inetutils配置,这个配置是spring cloud的网络工具类,这个配置的含义是如果获取ip时获取到多个ip(内网、外网),就优先选择我配置的ip中存在的ip,这样再测试就会发现,注册service的时候就变成了公网ip。 ...

March 15, 2019 · 1 min · jiezi

API网关 | 从0开始构建SpringCloud微服务(12)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程。本章主要讲解API网关。项目存在的问题在目前的项目中我们构建了许多的API微服务,当第三方服务想要调用我们的API微服务的时候是通过微服务的名称进行调用的,没有一个统一的入口。使用API网关的意义集合多个API统一API入口API网关就是为了统一服务的入口,可以方便地实现对平台的众多服务接口进行管控,对访问服务的身份进行验证,防止数据的篡改等。我们的一个微服务可能需要依赖多个API微服务,API网关可以在中间做一个api的聚集。那么我们的微服务只需要统一地经过API网关就可以了(只需要依赖于API网关就可以了),不用关心各种API微服务的细节,需要调用的API微服务由API网关来进行转发。使用API网关的利与弊API网关的利避免将内部信息泄露给外部为微服务添加额外的安全层支持混合通信协议降低构建微服务的复杂性微服务模拟与虚拟化API网关的弊在架构上需要额外考虑更多编排与管理路由逻辑配置要进行统一的管理可能引发单点故障API网关的实现Zuul:SpringCloud提供的API网关的组件Zuul是Netflix开源的微服务网关,他可以和Eureka,Ribbon,Hystrix等组件配合使用。Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求审查与监控:动态路由:动态将请求路由到不同后端集群压力测试:逐渐增加指向集群的流量,以了解性能负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求静态响应处理:边缘位置进行响应,避免转发到内部集群多区域弹性:跨域AWS Region进行请求路由,旨在实现ELB(ElasticLoad Balancing)使用多样化Spring Cloud对Zuul进行了整合和增强。目前,Zuul使用的默认是Apache的HTTP Client,也可以使用Rest Client,可以设置ribbon.restclient.enabled=true。集成Zuul在原来项目的基础上,创建一个msa-weather-eureka-client-zuul作为API网关。添加依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId></dependency>添加注解添加@EnableZuulProxy启用Zuul的代理功能。@SpringBootApplication@EnableDiscoveryClient@EnableZuulProxypublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}添加配置这里配置的是设置路由的url。当有人访问 /city/ 路径的时候,就对访问这个路径的请求做一个转发,转发到msa-weather-city-eureka服务中去,同理,当有人访问 /data/ 路径的时候,API网关也会将这个请求转发到msa-weather-data-eureka服务中去。zuul: routes: city: path: /city/** service-id: msa-weather-city-eureka data: path: /data/** service-id: msa-weather-data-eureka微服务依赖API网关原来天气预报微服务依赖了城市数据API微服务,以及天气数据API微服务,这里我们对其进行修改让其依赖API网关,让API网关处理天气预报微服务其他两个微服务的调用。首先删去原来调用其他两个API微服务的Feign客户端——CityClient以及WeatherDataClient,创建一个新的Feign客户端——DataClient。package com.demo.service;import java.util.List;import org.springframework.cloud.netflix.feign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import com.demo.vo.City;import com.demo.vo.WeatherResponse;@FeignClient(“msa-weather-eureka-client-zuul”)public interface DataClient { //获取城市列表 @RequestMapping(value="/city/cities",method=RequestMethod.GET) List<City> listCity()throws Exception; //通过城市Id查询对应城市的天气数据 @RequestMapping(value="/data/weather/cityId",method=RequestMethod.GET) WeatherResponse getDataByCityId(@RequestParam(“cityId”)String cityId);}在service中使用该客户端对API网关进行调用,API网关根据我们请求的路径去调用相应的微服务。@Servicepublic class WeatherReportServiceImpl implements WeatherReportService{ //@Autowired //private WeatherDataClient weatherDataClient; @Autowired private DataClient dataClient; //根据城市Id获取天气预报的数据 @Override public Weather getDataByCityId(String cityId) { //由天气数据API微服务来提供根据城市Id查询对应城市的天气的功能 WeatherResponse resp=dataClient.getDataByCityId(cityId); Weather data=resp.getData(); return data; }}在controller也是同理。 //传入城市Id得到对应城市的天气数据 @GetMapping("/cityId/{cityId}") public Weather getReportByCityId(@PathVariable(“cityId”)String cityId,Model model)throws Exception{ //获取城市Id列表 //由城市数据API微服务来提供数据 List<City>cityList=null; try { cityList=dataClient.listCity(); }catch (Exception e) { logger.error(“Exception!",e); } Weather weather=weatherReportService.getDataByCityId(cityId); return weather; }测试结果 ...

March 14, 2019 · 1 min · jiezi

微服务的消费 | 从0开始构建SpringCloud微服务(11)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程。本章主要讲解微服务的消费。微服务的消费模式微服务的消费模式主要有:服务直连模式客户端发现模式服务端发现模式下面我们主要讲解客户端发现模式,以及服务端发现模式。客户端发现模式客户端发现模式的具体流程如下:1)服务实例启动后,将自己的位置信息提交到服务注册表中。2)客户端从服务注册表进行查询,来获取可用的服务实例。3)客户端自行使用负载均衡算法从多个服务实例中选择出一个。服务端发现模式服务端发现模式与客户端发现模式的区别在于:服务端发现模式的负载均衡由负载均衡器(独立的)来实现,客户端不需要关心具体调用的是哪一个服务。微服务的消费者下面我们价格介绍一些常见的微服务的消费者。HttpClientHttpClient是常见的微服务的消费者,它是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP协议最新的版本和建议。RibbonRibbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Ribbon常与Eureka结合使用。在典型的分布式部署中,Eureka为所有的微服务提供服务注册,Ribbon提供服务消费的客户端,含有许多负载均衡的算法。FeignFeign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign整合了Ribbon和Hystrix(关于Hystrix我们后面再讲),可以让我们不再需要显式地使用这两个组件。总起来说,Feign具有如下特性:可插拔的注解支持,包括Feign注解和JAX-RS注解;支持可插拔的HTTP编码器和解码器;支持Hystrix和它的Fallback;支持Ribbon的负载均衡;支持HTTP请求和响应的压缩。集成Feign本节我们主要讲解如何集成Feign,实现微服务的消费功能。添加配置pom.xml配置文件,添加如下依赖。 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> application.yml文件,添加如下配置服务的名称与工程的名称一致,并且设置请求一个服务的connect与read的超时时间。spring: application: name: micro-weather-eureka-client-feigneureka: client: service-url: defaultZone: http://localhost:8761/eureka/feign: client: config: feignName: connectTimeout: 5000 readTimeout: 5000添加注解添加@EnableFeignClients注解,启动Feign功能。package com.demo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.web.servlet.ServletComponentScan;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.netflix.feign.EnableFeignClients;import org.springframework.scheduling.annotation.EnableScheduling;//@ServletComponentScan(basePackages=“com.demo.web.servlet”)@SpringBootApplication@EnableDiscoveryClient@EnableScheduling@EnableFeignClientspublic class Sifoudemo02Application { public static void main(String[] args) { SpringApplication.run(Sifoudemo02Application.class, args); }}创建Feign客户端创建一个Feign客户端,通过其CityClient的接口,可以调用城市数据API微服务mas-weather-city-eureka中的cities接口,返回城市列表。package com.demo.service;import java.util.List;import org.springframework.cloud.netflix.feign.FeignClient;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import com.demo.vo.City;//创建一个Feign客户端 获取城市数据微服务中的城市列表信息@FeignClient(“msa-weather-city-eureka”)public interface CityClient { @RequestMapping(value="/cities",method=RequestMethod.GET) List<City> listCity()throws Exception;}使用Feign客户端@RestControllerpublic class CityController{ @Autowired private CityClient cityClient; @GetMapping("/cities") public List<City> listCity(){ List<City> cityList=cityClient.listCity(); return cityList; }}测试先启动Eureka服务端,然后启动被请求的微服务——城市数据API微服务,最后启动Feign客户端。 ...

March 11, 2019 · 1 min · jiezi

服务的高可用 | 从0开始构建SpringCloud微服务(10)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程。本章主要讲解实现服务的高可用。什么是高可用高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。假设系统一直能够提供服务,我们说系统的可用性是100%。如果系统每运行100个时间单位,会有1个时间单位无法提供服务,我们说系统的可用性是99%。很多公司的高可用目标是4个9,也就是99.99%,这就意味着,系统的年停机时间为8.76个小时。如何保障系统的高可用我们都知道,单点是系统高可用的大敌,单点往往是系统高可用最大的风险和敌人,应该尽量在系统设计的过程中避免单点。方法论上,高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他backup能够顶上。保证系统高可用,架构设计的核心准则是:冗余。有了冗余之后,还不够,每次出现故障需要人工介入恢复势必会增加系统的不可服务实践。所以,又往往是通过“自动故障转移”来实现系统的高可用。下面我们重点讲解通过集成Eureka设置多节点的注册中心,从而实现服务的高可用。实现服务的高可用集成Eureka我们需要将前面拆分好的四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务,集成Eureka,实现服务的发现与注册功能。主要的操作步骤为:添加pom.xml配置添加application.yml配置添加注解至于如何进行添加,我在上一章进行了详细的讲述,这里就不展开了。启动服务我们将各个服务生成jar包,并通过命令行指定端口进行启动,启动命令如下:java -jar micro-weather-eureka-server-1.0.0.jar –server.port=8761测试结果从上图可以看到我们有4个微服务,每个微服务启动了两个实例。每个实例已经集成了Eureka客户端,并且在Eureka服务器中完成了注册。各个微服务之间可以通过应用的名称相互访问,每个微服务启动了多个实例,这样当我们的任何一个实例挂掉了,因为在其他节点有注册,所以还可以提供服务,如此一来我们便实现了服务的高可用!

March 11, 2019 · 1 min · jiezi

SpringCloud注册中心Eureka

本篇概论在上一篇中我们介绍了微服务相关的内容。微服务的本质就是让服务与服务之间进行互相调用。那么在调用之前需要有一个前提。就是不同的服务与服务之间怎么知道彼此的存在的呢?因为服务都是独立部署的,根本没有任何关联。如果都不知道要调用的服务地址,那还怎么进行互相调用呢?为了解决这样的问题,于是SrpingCloud提供了注册中心的组件。我们在开发项目时,都向注册中心注册,在进行服务调用时,注册中心返回要调用服务的地址,这样就解决了上述问题了。创建Eureka服务端步骤在SrpingCloud中Eureka是注册中心的组件,通过Eureka我们可以很方便的搭建一个注册中心。在SrpingCloud中Eureka组件分为服务端和客户端。如果有Dubbo项目开发经验的人可以理解为Eureka中的服务端,就相当于zokeerper服务,也就是记录服务地址的。而Eureka中的客户端,即是我们Dubbo项目中真真正正的服务。下面我们来详细介绍一下,怎么搭建一个Eureka服务端。搭建一个Eureka服务端非常的简单,和创建一个SpringBoot的项目差不多,不同之处,就是添加依赖不一样。下面我们来详细介绍一下。在IDEA中选择Spring Initializr选项。也就是如下图所示:设置项目的相关参数,在这一点和创建SpringBoot的项目没有任何区别。这一块是最重要的,也就是唯一和创建SpringBoot项目不同的地方。这一步还是和SpringBoot项目一样,直接点击完成就可以了。下图就是项目的架构图,比较简单和SpringBoot一样,唯一的区别就是pom.xml中的不同,也就是依赖不同。下面我启动直接启动项目看一下运行结果。启动日志:com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server at com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute(RetryableEurekaHttpClient.java:112) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient.execute(SessionedEurekaHttpClient.java:77) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.DiscoveryClient.register(DiscoveryClient.java:829) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.InstanceInfoReplicator.run(InstanceInfoReplicator.java:121) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.InstanceInfoReplicator$1.run(InstanceInfoReplicator.java:101) [eureka-client-1.9.8.jar:1.9.8] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_191] at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_191] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_191] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_191] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_191] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_191] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_191]2019-03-09 18:20:09.617 INFO 1752 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ‘‘2019-03-09 18:20:09.618 INFO 1752 — [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8080我们发现和SpringBoot启动有一些不同,我们什么都没有改,也就是都是用默认的配置,启动的时候,居然抛出异常了。而在SpringBoot的项目中是可以正常启动的。虽然项目抛出了异常,但项目已经启动成功了,因为日志已经正确的输出了项目的端口了。下面我们直接访问这个端口,看看会返回什么信息。访问地址:http://127.0.0.1:8080@EnableEurekaServer注解看上面的返回结果我们应该很熟悉,这是因为我们没有写Controller导致的,在SpringBoot的文章中我们介绍过,这里就不详细介绍了。但这显然是不对的,因为刚刚我们介绍过SpringCloud中是使用Eureka来提供注册中心服务的,并且Eureka有客户端和服务端之分,所以我们只在项目添加了Eureka的依赖还是不够的,我们还要在项目的代码中添加注解,来标识当前的Eureka服务是客户端服务还是服务端服务。这也就是本篇介绍的第一个注解。也就是@EnableEurekaServer注解。我们只要将该注解添加到SpringCloud项目中的启动类中即可。具体代码如下:package com.jilinwula.eureka;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication@EnableEurekaServerpublic class JilinwulaSpringcloudEurekaServerApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudEurekaServerApplication.class, args); }}然后我们继续启动项目。在访问地址:http://127.0.0.1:8080看一下项目返回的结果。项目启动日志如下:com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server at com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute(RetryableEurekaHttpClient.java:112) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient.execute(SessionedEurekaHttpClient.java:77) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.DiscoveryClient.register(DiscoveryClient.java:829) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.InstanceInfoReplicator.run(InstanceInfoReplicator.java:121) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.InstanceInfoReplicator$1.run(InstanceInfoReplicator.java:101) [eureka-client-1.9.8.jar:1.9.8] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_191] at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_191] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_191] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_191] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_191] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_191] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_191]2019-03-09 18:42:29.427 INFO 1837 — [ Thread-19] o.s.c.n.e.server.EurekaServerBootstrap : isAws returned false2019-03-09 18:42:29.428 INFO 1837 — [ Thread-19] o.s.c.n.e.server.EurekaServerBootstrap : Initialized server context2019-03-09 18:42:29.461 INFO 1837 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ‘‘2019-03-09 18:42:29.461 INFO 1837 — [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8080看上述日志,项目启动时还是会抛出异常,我们先不用考虑,在后续的内容中我们在解释为什么启动时会抛出异常。还是和第一次启动一样,虽然启动抛出异常了,但项目还是启动成功了。下面我们继续访问以下地址,看一下访问结果。http://127.0.0.1:8080返回结果:自定义注册中心地址我们看这回返回的不是默认的错误页面了,而是返回了一个管理界面。这个管理界面就是SpringCloud中Eureka组建为我们提供的注册中心的管理界面,通过这个界面,我们可以非常方便的,来管理哪些服务注册成功、哪些注册失败以及服务其它状态等。看上图中的界面显示,现在没有任何一个服务注册成功。下面我们看一下刚刚项目启动时,抛出的异常。如果我们现在观察项目的启动日志,我们会发现,日志是每隔一段时间,就会抛出和启动时一样的异常。这是为什么呢?这是因为Eureka服务端和客户端是通过心跳方式检测的服务状态。刚刚我们通过@EnableEurekaServer注解启动了Eureka的服务端。实际上@EnableEurekaServer注解在底层实现时,除了标识项目为Eureka的服务端外,还会默认标识项目为Eureka的客户端。也就是通过@EnableEurekaServer注解标识的项目,默认即是Eureka的客户端还是Eureka的服务端。所以上述报错的原因就是Eureka的客户端与找到Eureka的服务端才抛出的异常。那怎么解决呢?既然我们知道了异常的根本原因,那我们解决就比较简单了,我们只要在项目中正确的配置Eureka的服务端的地址就可以解决上述的问题。具体配置如下。我们知道在创建SpringClourd项目默认会为我们创建application.properties文件,我们首先将该文件修改为yml文件(原因在之前的文章中已经介绍过了)。具体配置如下。application.yml配置:eureka: client: service-url: defaultZone: http://127.0.0.1:8080/eureka/配置完上述参数后,我们重新启动项目,然后在观察一下日志,看看是不是还会抛出异常?(第一次启动项目时,还会是抛出异常的,因为我们的Eureka服务端还没有启动成功呢,所以还是会抛出异常的,我们只要看心跳之后,会不会抛出异常即可。)下面为启动后的日志:2019-03-09 21:00:27.909 INFO 1930 — [ Thread-21] o.s.c.n.e.server.EurekaServerBootstrap : isAws returned false2019-03-09 21:00:27.909 INFO 1930 — [ Thread-21] o.s.c.n.e.server.EurekaServerBootstrap : Initialized server context2019-03-09 21:00:27.949 INFO 1930 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ‘‘2019-03-09 21:00:27.949 INFO 1930 — [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 80802019-03-09 21:00:27.952 INFO 1930 — [ main] inwulaSpringcloudEurekaServerApplication : Started JilinwulaSpringcloudEurekaServerApplication in 4.318 seconds (JVM running for 4.816)2019-03-09 21:00:28.288 INFO 1930 — [(1)-192.168.0.3] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet ‘dispatcherServlet'2019-03-09 21:00:28.288 INFO 1930 — [(1)-192.168.0.3] o.s.web.servlet.DispatcherServlet : Initializing Servlet ‘dispatcherServlet'2019-03-09 21:00:28.295 INFO 1930 — [(1)-192.168.0.3] o.s.web.servlet.DispatcherServlet : Completed initialization in 6 ms2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Disable delta property : false2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Force full registry fetch : false2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Application is null : false2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Application version is -1: true2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server2019-03-09 21:00:57.745 WARN 1930 — [nio-8080-exec-1] c.n.e.registry.AbstractInstanceRegistry : DS: Registry: lease doesn’t exist, registering resource: UNKNOWN - 192.168.0.32019-03-09 21:00:57.745 WARN 1930 — [nio-8080-exec-1] c.n.eureka.resources.InstanceResource : Not Found (Renew): UNKNOWN - 192.168.0.32019-03-09 21:00:57.763 INFO 1930 — [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/192.168.0.3 - Re-registering apps/UNKNOWN2019-03-09 21:00:57.763 INFO 1930 — [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/192.168.0.3: registering service…2019-03-09 21:00:57.770 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : The response status is 2002019-03-09 21:00:57.807 INFO 1930 — [nio-8080-exec-3] c.n.e.registry.AbstractInstanceRegistry : Registered instance UNKNOWN/192.168.0.3 with status UP (replication=false)2019-03-09 21:00:57.809 INFO 1930 — [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/192.168.0.3 - registration status: 2042019-03-09 21:00:58.329 INFO 1930 — [nio-8080-exec-4] c.n.e.registry.AbstractInstanceRegistry : Registered instance UNKNOWN/192.168.0.3 with status UP (replication=true)我们看项目这时已经不会抛出异常了,并且通过观察发现,每隔一段时间就会有日志输出,这也就是上面介绍的Eureka的服务端和Eureka的客户端的心跳机制。下面我们继续访问http://127.0.0.1:8080地址来看看此时的注册中心和刚刚相比,是否有不一样的地方。修改默认项目名我们看这时的注册中心已经检测到了有服务注册了,只不过这个服务就是Eureka的服务端自己,并且名字为UNKNOWN。如果有强迫者的人如果看到UNKNOWN那一定会感觉不舒服,不了解了Eureka组件的还以为注册中心出错了呢。下面我们修改一下项目参数,将UNKNOWN改成我们指定的名字。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8080/eureka/spring: application: name: jilinwula-springcloud-eureka-server我们在看一下注册中的的变化,看看还是不是已经成功的将UNKNOWN修改为我们指定的项目名字了。register-with-eureka配置我们看注册中心已经成功的显示我们配置文件中的项目名字了。在实际的开发中,我们基本不会让注册中心显示Eureka的服务端自己的服务,这样可能会导致和Eureka的客户端相混淆。所以通常的做法是让注册中心将Eureka的服务端屏蔽掉,说是屏蔽实际上是让Eureka的服务端不向注册中心注册。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8080/eureka/ register-with-eureka: falsespring: application: name: jilinwula-springcloud-eureka-server我们在观察一下注册中心看看还是否可以检测到Eureka服务端自己。我们发现这时注册中心已经检测到不任何服务了。下面我们将Eureka服务端的端口设置为默认的端口8761,因为8080端口可能会被占用。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/ register-with-eureka: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8761创建Eureka客户端步骤这时我们Eureka服务端的基本配置就介绍完了,下面我们介绍一下Eureka组件的客户端。下面我们还是向Eureka服务端一样从创建项目开始。具体步骤如下:我们还是在IDEA中选择Spring Initializr选项。也就是如下图所示:设置项目的相关参数,和Eureka服务端没有任何区别。这一步非常关键,因为它和Eureka服务端和SpringBoot都是不一样的。备注:为了保证Eureka服务端和客户端可以注册成功,我们要特别注意保证两个项目中SpringBoot及其SpringCloud的版本一致。由于剩下的步骤和Eureka服务端一样,我们就不做过多的介绍了。下面我们还是和Eureka服务端一样,配置注册中心的服务地址。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-eureka-clientserver: port: 8081@EnableEurekaClient注解如果只修改上面的配置,注册中心是不会检测到Eureka客户端的,因为我们还没有在该项目的启动类上添加Eureka客户端的注解。具体配置如下:package com.jilinwula.jilinwulaspringcloudeurekaclient;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication@EnableEurekaClientpublic class JilinwulaSpringcloudEurekaClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudEurekaClientApplication.class, args); }}@EnableEurekaClient注解与@EnableDiscoveryClient注解区别也就是添加@EnableEurekaClient注解。实际上除了该注解外,我们还可以用@EnableDiscoveryClient注解来达到同样的作用。它们两个注解的区别是注册中心的实现方式有很多种,如果是采用的是Eureka服务的话,那客户端直接使用@EnableEurekaClient注解和@EnableDiscoveryClient注解都可以。如果注册中心采用的是zookeeper或者其它服务时,那我们注册中心客户端就不能采用@EnableEurekaClient注解了,而是要使用@EnableDiscoveryClient注解。所以在实际的开发中我们推荐使用@EnableDiscoveryClient注解,这样当我们更换注册中心实现时,就不用修改代码了。上述代码中我们为了和Eureka服务端一致,所以我们采用@EnableDiscoveryClient注解。下面我们启动一下项目看看注册中心是否可以成功的检测Eureka客户端的存在。当我们按照上面的配置启动Eureka客户端时,我们发现日志居然报错了,并且项目自动停止运行了。具体日志如下:2019-03-09 23:00:55.844 INFO 2082 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081: registering service…2019-03-09 23:00:55.852 INFO 2082 — [ main] inwulaSpringcloudEurekaClientApplication : Started JilinwulaSpringcloudEurekaClientApplication in 2.115 seconds (JVM running for 2.567)2019-03-09 23:00:55.866 INFO 2082 — [ Thread-8] o.s.c.n.e.s.EurekaServiceRegistry : Unregistering application JILINWULA-SPRINGCLOUD-EUREKA-CLIENT with eureka with status DOWN2019-03-09 23:00:55.866 WARN 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1552143655866, current=DOWN, previous=UP]2019-03-09 23:00:55.869 INFO 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : Shutting down DiscoveryClient …2019-03-09 23:00:55.883 INFO 2082 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081 - registration status: 2042019-03-09 23:00:55.883 INFO 2082 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081: registering service…2019-03-09 23:00:55.888 INFO 2082 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081 - registration status: 2042019-03-09 23:00:55.889 INFO 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : Unregistering …2019-03-09 23:00:55.893 INFO 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081 - deregister status: 2002019-03-09 23:00:55.902 INFO 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : Completed shut down of DiscoveryClient这是为什么呢?这个问题的原因是因为版本不同导致的,也就是有的Eureka版本有BUG导致的,少了一个依赖我们只要把缺少的依赖添加上即可。缺少的依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>下面我们继续启动Eureka客户端,然后看看注册中心是否可以检测到Eureka客户端。这时我们发现注册中心已经成功的检测到了Eureka客户端的服务了。除此之外,我们发现此时的注册中心和以往相比有了其它的不同,我们发现注册中心显示警告信息了。这是为什么呢?这是因为注册中心有预警机制,因为我为了掩饰项目,会频繁的启动重启项目,这样注册中心的心跳就会时常检测不到Eureka客户端的心跳,所以就会认为该服务已下线。所以Eureka注册中心当服务下线少于一定比率时,就会显示警告信息,以此来表示有的服务运行不稳定。当然我们还是可以通过配置参数来消除上面的警告。具体参数如下:register-with-eureka配置eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8761该参数的是意思是默认认为服务均在线,并且还有一点要注意,该参数是在Eureka服务端配置的。我们重新启动完Eureka服务端后,在看一下注册中心中的变化。Eureka服务端双注册中心配置这时我们发现警告信息又变了,这说明我们的配置参数启作用了。那为什么还会提示警告呢?这是因为我们配置了改参数,所以注册中心就不会准确的检查服务上下线状态了。所以提示了另一个警告。下面我们将Eureka服务端配置成多节点,在实际的项目开发中,我们知道一个节点可能会出现问题,如果Eureka服务端出现了问题,那么就相当于整个服务都不能调用了,所以为了保证高可用,通常会将Eureka服务端配置成多个节点,下面我们先尝试将Eureka服务端配置成双节点。既然是双节点,那当然是有两个Eureka服务端项目了,由于创建Eureka服务端的步骤,我们已经很熟悉了,所以我们只介绍它们配置文件的不同。首先我们先看一下第一个Eureka服务端注册中心配置:eureka: client: service-url: defaultZone: http://127.0.0.1:8762/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8761第二个Eureka服务端注册中心配置:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8762我们发现这两个配置基本相同,唯一的不同就是配置注册中地方,它们彼此配置的是对方的服务地址。也就是让两个Eureka服务端彼此注册,这样就只要我们Eureka客户端注册任意一个注册中心,这两个注册中心都可以检测到Eureka客户端的存在,因为底层它们会进行数据同步。下面我们看一下现在的注册中心的变化。8761注册中心:8762注册中心:我们看我们的Eureka客户端只配置了一个注册中心,但两个注册中心都检测到了Eureka客户端的存在。这就是刚刚提到过的当两个注册中心彼此注册时,就会进行数据通信,所以8762注册中心也检测到了该Eureka客户端的存在。下面我们将8761注册中心停止服务,然后在观察一下8762的注册中心,看看是否有何变化。8762注册中心:我们发现虽然我们将8761注册中心停止了服务,但8762注册中心依然检测到了Eureka客户端的存在。下面我们重新启动一下Eureka客户端然后在看一下8762注册中心还是否可以检测到Eureka客户端的存在。这时我们发现8762注册中心已经检测不到Eureka客户端的服务了。那应该怎么办呢?解决的办法很简单,那就是让我们的Eureka客户端注册两个注册中心。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/,http://127.0.0.1:8762/eureka/spring: application: name: jilinwula-springcloud-eureka-clientserver: port: 8081这时我们在访问一下注册中心,看一下服务是否可以检测到。Eureka服务端三注册中心配置这时我们的注册中心已经成功的检测到了Eureka客户端了。下面我们介绍一下怎么部署Eureka服务端3节点。既然2节点我们已经知道了要彼此注册,那么3节点,我们应该已经猜到了,那就是让每一个节点都注册另外两个节点的服务。具体配置如下:8761注册中心:eureka: client: service-url: defaultZone: http://127.0.0.1:8762/eureka/,http://127.0.0.1:8763/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 87618762注册中心:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/,http://127.0.0.1:8763/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 87628763注册中心:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/,http://127.0.0.1:8762/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8763Eureka客户端:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/,http://127.0.0.1:8762/eureka/,http://127.0.0.1:8763/eureka/spring: application: name: jilinwula-springcloud-eureka-clientserver: port: 8081下面我们看一下访问任何一个注册中心,来看一下注册中心是否可以检测到Eureka客户端的服务,及其它注册中心的存在。我们看注册中心已经成功的检测到了Eureka客户端的服务了,并且红色标识的地方已经检测到了其它两个注册中心的地址了,所以我们在访问注册中心时,就可以通过下面红色标识的地方,来了解项目中的Eureka服务端有几个注册中心。上述内容就是SpringClould中Eureka组件的详细介绍,如有不正确或者需要交流的欢迎留言,下一篇我们将介绍怎么在SpringClould中进行不同的服务与服务之间的调用。谢谢。 项目源码https://github.com/jilinwula/jilinwula-springcloud-eureka.git原文链接http://jilinwula.com/article/24342 ...

March 10, 2019 · 4 min · jiezi

SpringCloud 配置中心

一、创建Config配置中心项目1.添加依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>2.启动类,需要添加@EnableConfigServerimport org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.config.server.EnableConfigServer;/** * @author fusw /@SpringBootApplication@EnableConfigServerpublic class ConfigCenterApplication { public static void main(String[] args) { SpringApplication.run(ConfigCenterApplication.class, args); }}3.配置文件eureka.instance.appname=base-iov-configsecurity.user.name=testsecurity.user.password=test# GitLab 的配置方式,必须有 .git 后缀# 配置默认 git 仓库的地址spring.cloud.config.server.git.uri=http://xxxx/config-repo.git# git 仓库地址下的相对地址,可以配置多个,用“,”分割spring.cloud.config.server.git.search-paths=#配置中心clone仓库配置文件后存放的地址spring.cloud.config.server.git.basedir=/data/deploy/config-repospring.cloud.config.server.git.force-pull=true# git 仓库的账号spring.cloud.config.server.git.username=test# git 仓库的密码spring.cloud.config.server.git.password=test# 配置 其它git 仓库的地址 spring.cloud.config.server.git.repos.x.uri, iot为例spring.cloud.config.server.git.repos.iot.uri=http://xxxx/iot/config-repo.gitspring.cloud.config.server.git.repos.iot.search-paths=spring.cloud.config.server.git.repos.iot.basedir=/data/deploy/config-repospring.cloud.config.server.git.repos.iot.force-pull=true# git 仓库的账号spring.cloud.config.server.git.repos.iot.username=test# git 仓库的密码spring.cloud.config.server.git.repos.iot.password=test#前缀一定要配置,用来和默认仓库区分spring.cloud.config.server.git.repos.iot.pattern=Iot#注册中心eureka.client.serviceUrl.defaultZone=http://xxx/eureka/二、git 存放配置的仓库创建一个git仓库用来存放各项目配置,可以在项目一级目录根据项目创建文件夹(各项目文件夹的名称可以随便起,Config配置中心只根据配置文件名找配置),然后各个项目文件夹存放不同环境的配置文件,例如:Iot-TestProject-dev.ymlIot-TestProject-prd.ymlIot-TestProject-test.yml最好不要放置test.yml类似的默认配置,如果在各个项目配置文件中没有用spring.profiles.active指明使用的配置文件,那么会加载默认的配置文件。三、各个SpringCloud的项目配置接入配置中心在resource下的bootstrap.properties配置文件中,配置中心的相关配置如下# 使用默认仓库的配置文件spring.cloud.config.name=TestProject# 使用iot厂库的配置文件#spring.cloud.config.name=Iot-TestProject#使用哪个环境的配置文件,和上面的配置配合,决定了使用哪一个配置文件:TestProject -test.ymlspring.cloud.config.profile=test# 对应 存放配置文件仓库的git分支spring.cloud.config.label=masterspring.cloud.config.username=testspring.cloud.config.password=test# 开启 Config 服务发现支持spring.cloud.config.discovery.enabled=true# 指定 Server 端的 name,也就是 Server 端 spring.application.name 的值spring.cloud.config.discovery.serviceId=base-iov-config

March 8, 2019 · 1 min · jiezi

天气预报微服务 | 从0开始构建SpringCloud微服务(8)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是单块架构改造成微服务架构的过程,最终将原来单块架构的天气预报服务拆分为四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务。本章主要讲解天气预报微服务的实现。天气预报微服务的实现配置pom文件对原来单块架构的天气预报服务进行改进,去除多余的依赖,最终的pom文件如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>sifoudemo02</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sifoudemo02</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> <!– <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> –> </plugin> </plugins> </build> </project>提供接口在Service中提供根据城市Id获取天气数据的方法。这里的天气数据后期将会由天气数据API尾服务从缓存中获取。@Servicepublic class WeatherReportServiceImpl implements WeatherReportService { @Override public Weather getDataByCityId(String cityId) { // TODO 改为由天气数据API微服务来提供 Weather data = new Weather(); data.setAqi(“81”); data.setCity(“深圳”); data.setGanmao(“容易感冒!多穿衣”); data.setWendu(“22”); List<Forecast> forecastList = new ArrayList<>(); Forecast forecast = new Forecast(); forecast.setDate(“25日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); forecast = new Forecast(); forecast.setDate(“26日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); forecast = new Forecast(); forecast.setDate(“27日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); forecast = new Forecast(); forecast.setDate(“28日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); forecast = new Forecast(); forecast.setDate(“29日星期天”); forecast.setType(“晴”); forecast.setFengxiang(“无风”); forecast.setHigh(“高温 11度”); forecast.setLow(“低温 1度”); forecastList.add(forecast); data.setForecast(forecastList); return data; }}在Controller中提供根据城市Id获取相关天气预报数据并进行前端UI界面展示的接口。@RestController@RequestMapping("/report”)public class WeatherReportController { private final static Logger logger = LoggerFactory.getLogger(WeatherReportController.class); @Autowired private WeatherReportService weatherReportService; @GetMapping("/cityId/{cityId}”) public ModelAndView getReportByCityId(@PathVariable(“cityId”) String cityId, Model model) throws Exception { // 获取城市ID列表 // TODO 改为由城市数据API微服务来提供数据 List<City> cityList = null; try { // TODO 改为由城市数据API微服务提供数据 cityList = new ArrayList<>(); City city = new City(); city.setCityId(“101280601”); city.setCityName(“深圳”); cityList.add(city); } catch (Exception e) { logger.error(“Exception!”, e); } model.addAttribute(“title”, “猪猪的天气预报”); model.addAttribute(“cityId”, cityId); model.addAttribute(“cityList”, cityList); model.addAttribute(“report”, weatherReportService.getDataByCityId(cityId)); return new ModelAndView(“weather/report”, “reportModel”, model); }} ...

March 8, 2019 · 2 min · jiezi

天气数据API微服务 | 从0开始构建SpringCloud微服务(7)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是单块架构改造成微服务架构的过程,最终将原来单块架构的天气预报服务拆分为四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务。本章主要讲解天气数据API微服务的实现。天气数据API微服务的实现配置pom文件对原来单块架构的天气预报服务进行改进,去除多余的依赖,最终的pom文件如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>sifoudemo02</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sifoudemo02</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> <!– <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> –> </plugin> </plugins> </build> </project>提供接口在service中保留如下接口:(1)根据城市Id查询天气的接口getDataByCityId(2)根据城市名称查询天气的接口getDataByCityName注意:原来我们的天气数据是先从缓存中获取的,若查询不到则调用第三方接口获取天气信息,并将其保存到缓存中。在我们拆分成微服务架构之后调用第三方接口的行为由天气数据采集微服务中的定时任务进行。因此在天气数据API微服务中我们的天气数据直接从缓存中进行获取,若在缓存中获取不到对应城市的数据,则直接抛出错误。@Servicepublic class WeatherDataServiceImpl implements WeatherDataService { private final static Logger logger = LoggerFactory.getLogger(WeatherDataServiceImpl.class); private static final String WEATHER_URI = “http://wthrcdn.etouch.cn/weather_mini?"; @Autowired private StringRedisTemplate stringRedisTemplate; @Override public WeatherResponse getDataByCityId(String cityId) { String uri = WEATHER_URI + “citykey=” + cityId; return this.doGetWeahter(uri); } @Override public WeatherResponse getDataByCityName(String cityName) { String uri = WEATHER_URI + “city=” + cityName; return this.doGetWeahter(uri); } private WeatherResponse doGetWeahter(String uri) { String key = uri; String strBody = null; ObjectMapper mapper = new ObjectMapper(); WeatherResponse resp = null; ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); // 先查缓存,缓存有的取缓存中的数据 if (stringRedisTemplate.hasKey(key)) { logger.info(“Redis has data”); strBody = ops.get(key); } else { logger.info(“Redis don’t has data”); // 缓存没有,抛出异常 throw new RuntimeException(“Don’t has data!”); } try { resp = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { //e.printStackTrace(); logger.error(“Error!",e); } return resp; }}在controller中提供根据城市Id和名称获取天气数据的接口。@RestController@RequestMapping("/weather”)public class WeatherController { @Autowired private WeatherDataService weatherDataService; @GetMapping("/cityId/{cityId}”) public WeatherResponse getWeatherByCityId(@PathVariable(“cityId”) String cityId) { return weatherDataService.getDataByCityId(cityId); } @GetMapping("/cityName/{cityName}”) public WeatherResponse getWeatherByCityName(@PathVariable(“cityName”) String cityName) { return weatherDataService.getDataByCityName(cityName); }} ...

March 8, 2019 · 2 min · jiezi

天气数据采集微服务 | 从0开始构建SpringCloud微服务(6)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是单块架构改造成微服务架构的过程,最终将原来单块架构的天气预报服务拆分为四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务。本章主要讲解天气数据采集微服务的实现。各微服务的主要功能天气数据采集微服务的实现配置pom文件对原来单块架构的天气预报服务进行改进,去除多余的依赖,最终的pom文件如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>sifoudemo02</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sifoudemo02</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> <!– <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> –> </plugin> </plugins> </build> </project>提供接口在Service中保留根据城市的Id同步天气数据的接口。实现方式为通过城市Id调用第三方接口获取对应天气数据,并将其同步到Redis缓存中。@Servicepublic class WeatherDataCollectionServiceImpl implements WeatherDataCollectionService{ private static final String WEATHER_URL = “http://wthrcdn.etouch.cn/weather_mini?"; //设置缓存无效的时间 private static final long TIME_OUT = 1800L; // 1800s //httpClient的客户端 @Autowired private RestTemplate restTemplate; @Autowired private StringRedisTemplate stringRedisTemplate; //根据城市的Id同步天气数据 @Override public void syncDataByCityId(String cityId) { String url = WEATHER_URL + “citykey=” + cityId; this.saveWeatherData(url); } //把天气数据放在缓存中 private void saveWeatherData(String url) { //将rul作为天气数据的key进行保存 String key=url; String strBody=null; ValueOperations<String, String>ops=stringRedisTemplate.opsForValue(); //调用服务接口来获取数据 ResponseEntity<String>respString=restTemplate.getForEntity(url, String.class); //判断请求状态 if(respString.getStatusCodeValue()==200) { strBody=respString.getBody(); } ops.set(key, strBody,TIME_OUT,TimeUnit.SECONDS); } }定时任务保留根据城市列表同步全部天气数据的定时任务,这里的城市列表后期会通过调用城市数据API微服务得到。//同步天气数据public class WeatherDataSyncJob extends QuartzJobBean{ private final static Logger logger = LoggerFactory.getLogger(WeatherDataSyncJob.class); @Autowired private WeatherDataCollectionService weatherDataCollectionService; @Override protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException { logger.info(“Weather Data Sync Job. Start!”); //城市ID列表 //TODO 改为由城市数据API微服务来提供城市列表的数据 List<City>cityList=null; try { //TODO 改为由城市数据API微服务来提供城市列表的数据 //获取xml中的城市ID列表 //cityList=cityDataService.listCity(); cityList=new ArrayList<>(); City city=new City(); city.setCityId(“101280601”); cityList.add(city); } catch (Exception e) {// e.printStackTrace(); logger.error(“Exception!”, e); } //遍历所有城市ID获取天气 for(City city:cityList) { String cityId=city.getCityId(); logger.info(“Weather Data Sync Job, cityId:” + cityId); //实现根据cityid定时同步天气数据到缓存中 weatherDataCollectionService.syncDataByCityId(cityId); } logger.info(“Weather Data Sync Job. End!”); } } ...

March 7, 2019 · 2 min · jiezi

服务的拆分 | 从0开始构建SpringCloud微服务(5)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是单块架构改造成微服务架构的过程,最终将原来单块架构的天气预报服务拆分为四个微服务:城市数据API微服务,天气数据采集微服务,天气数据API微服务,天气预报微服务。本章主要讲解城市数据API微服务的实现。各微服务的主要功能服务注册机制多个微服务之间获知对方的存在并进行通信,需要通过服务注册机制。当我们的微服务启动的时候,就会将信息注册到服务注册表或者服务注册中心中。中心可以通过心跳机制等感知服务的状态,并广播给其他的微服务。一个服务调用另一个服务,调用其他服务的服务称为服务的消费者,被调用的服务称为服务的提供者。城市数据API微服务的实现配置pom文件对原来单块架构的天气预报服务进行改进,去除多余的依赖,最终的pom文件如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>sifoudemo02</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sifoudemo02</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> <!– <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <extensions>true</extensions> –> </plugin> </plugins> </build> </project>提供接口在service中保留获取本地xml文件中城市列表的方法listCity。@Servicepublic class CityDataServiceImpl implements CityDataService{ @Override public List<City> listCity() throws Exception { Resource resource=new ClassPathResource(“citylist.xml”); BufferedReader br=new BufferedReader(new InputStreamReader(resource.getInputStream(), “utf-8”)); StringBuffer buffer=new StringBuffer(); String line=””; while((line=br.readLine())!=null) { buffer.append(line); } br.close(); CityList cityList=(CityList)XmlBuilder.xmlStrToOject(CityList.class, buffer.toString()); return cityList.getCityList(); }}在controller中保留获取城市列表的接口。@RestController@RequestMapping("/cities”)public class CityController { @Autowired private CityDataService cityDataService; //返回城市列表 @GetMapping public List<City>listCity()throws Exception{ return cityDataService.listCity(); }}测试结果 ...

March 7, 2019 · 1 min · jiezi

Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节主要讲的是通过引入Quartz实现天气数据的同步。存在问题当用户请求我们的数据的时候才去拉最新的数据,并将其更新到Redis缓存中,效率较低。且缓存中的数据只要存在就不再次做请求,不对数据进行更新,但是天气数据大概是每半个小时就做一次更新的,所以我们传给用户的数据可能不是较新的,数据存在一定误差。解决方案通过作业调度框架Quartz实现天气数据的自动同步。前期工作要实现定时拉取接口中的数据到Redis缓存中,需要一个城市Id的列表。通过对城市Id列表的遍历,调用weatherDataService中根据城市Id同步数据到Redis中的syncDataByCityId方法,我们就能实现所有城市数据的同步了。城市列表的构建由于在程序运行的过程中动态调用服务是有延时的,所以需要减少动态调用服务,因此我们将城市列表缓存到本地。xml文件的构建使用xml文件将列表存储到本地中,需要的时候再从本地进行读取,这样会比调用第三方的服务更快。xml文件如下:<?xml version=“1.0” encoding=“UTF-8”?><c c1=“0”><d d1=“101280101” d2=“广州” d3=“guangzhou” d4=“广东”/><d d1=“101280102” d2=“番禺” d3=“panyu” d4=“广东”/><d d1=“101280103” d2=“从化” d3=“conghua” d4=“广东”/><d d1=“101280104” d2=“增城” d3=“zengcheng” d4=“广东”/><d d1=“101280105” d2=“花都” d3=“huadu” d4=“广东”/><d d1=“101280201” d2=“韶关” d3=“shaoguan” d4=“广东”/><d d1=“101280202” d2=“乳源” d3=“ruyuan” d4=“广东”/><d d1=“101280203” d2=“始兴” d3=“shixing” d4=“广东”/><d d1=“101280204” d2=“翁源” d3=“wengyuan” d4=“广东”/><d d1=“101280205” d2=“乐昌” d3=“lechang” d4=“广东”/><d d1=“101280206” d2=“仁化” d3=“renhua” d4=“广东”/><d d1=“101280207” d2=“南雄” d3=“nanxiong” d4=“广东”/><d d1=“101280208” d2=“新丰” d3=“xinfeng” d4=“广东”/><d d1=“101280209” d2=“曲江” d3=“qujiang” d4=“广东”/><d d1=“101280210” d2=“浈江” d3=“chengjiang” d4=“广东”/><d d1=“101280211” d2=“武江” d3=“wujiang” d4=“广东”/><d d1=“101280301” d2=“惠州” d3=“huizhou” d4=“广东”/><d d1=“101280302” d2=“博罗” d3=“boluo” d4=“广东”/><d d1=“101280303” d2=“惠阳” d3=“huiyang” d4=“广东”/><d d1=“101280304” d2=“惠东” d3=“huidong” d4=“广东”/><d d1=“101280305” d2=“龙门” d3=“longmen” d4=“广东”/><d d1=“101280401” d2=“梅州” d3=“meizhou” d4=“广东”/><d d1=“101280402” d2=“兴宁” d3=“xingning” d4=“广东”/><d d1=“101280403” d2=“蕉岭” d3=“jiaoling” d4=“广东”/><d d1=“101280404” d2=“大埔” d3=“dabu” d4=“广东”/><d d1=“101280406” d2=“丰顺” d3=“fengshun” d4=“广东”/><d d1=“101280407” d2=“平远” d3=“pingyuan” d4=“广东”/><d d1=“101280408” d2=“五华” d3=“wuhua” d4=“广东”/><d d1=“101280409” d2=“梅县” d3=“meixian” d4=“广东”/><d d1=“101280501” d2=“汕头” d3=“shantou” d4=“广东”/><d d1=“101280502” d2=“潮阳” d3=“chaoyang” d4=“广东”/><d d1=“101280503” d2=“澄海” d3=“chenghai” d4=“广东”/><d d1=“101280504” d2=“南澳” d3=“nanao” d4=“广东”/><d d1=“101280601” d2=“深圳” d3=“shenzhen” d4=“广东”/><d d1=“101280701” d2=“珠海” d3=“zhuhai” d4=“广东”/><d d1=“101280702” d2=“斗门” d3=“doumen” d4=“广东”/><d d1=“101280703” d2=“金湾” d3=“jinwan” d4=“广东”/><d d1=“101280800” d2=“佛山” d3=“foshan” d4=“广东”/><d d1=“101280801” d2=“顺德” d3=“shunde” d4=“广东”/><d d1=“101280802” d2=“三水” d3=“sanshui” d4=“广东”/><d d1=“101280803” d2=“南海” d3=“nanhai” d4=“广东”/><d d1=“101280804” d2=“高明” d3=“gaoming” d4=“广东”/><d d1=“101280901” d2=“肇庆” d3=“zhaoqing” d4=“广东”/><d d1=“101280902” d2=“广宁” d3=“guangning” d4=“广东”/><d d1=“101280903” d2=“四会” d3=“sihui” d4=“广东”/><d d1=“101280905” d2=“德庆” d3=“deqing” d4=“广东”/><d d1=“101280906” d2=“怀集” d3=“huaiji” d4=“广东”/><d d1=“101280907” d2=“封开” d3=“fengkai” d4=“广东”/><d d1=“101280908” d2=“高要” d3=“gaoyao” d4=“广东”/><d d1=“101281001” d2=“湛江” d3=“zhanjiang” d4=“广东”/><d d1=“101281002” d2=“吴川” d3=“wuchuan” d4=“广东”/><d d1=“101281003” d2=“雷州” d3=“leizhou” d4=“广东”/><d d1=“101281004” d2=“徐闻” d3=“xuwen” d4=“广东”/><d d1=“101281005” d2=“廉江” d3=“lianjiang” d4=“广东”/><d d1=“101281006” d2=“赤坎” d3=“chikan” d4=“广东”/><d d1=“101281007” d2=“遂溪” d3=“suixi” d4=“广东”/><d d1=“101281008” d2=“坡头” d3=“potou” d4=“广东”/><d d1=“101281009” d2=“霞山” d3=“xiashan” d4=“广东”/><d d1=“101281010” d2=“麻章” d3=“mazhang” d4=“广东”/><d d1=“101281101” d2=“江门” d3=“jiangmen” d4=“广东”/><d d1=“101281103” d2=“开平” d3=“kaiping” d4=“广东”/><d d1=“101281104” d2=“新会” d3=“xinhui” d4=“广东”/><d d1=“101281105” d2=“恩平” d3=“enping” d4=“广东”/><d d1=“101281106” d2=“台山” d3=“taishan” d4=“广东”/><d d1=“101281107” d2=“蓬江” d3=“pengjiang” d4=“广东”/><d d1=“101281108” d2=“鹤山” d3=“heshan” d4=“广东”/><d d1=“101281109” d2=“江海” d3=“jianghai” d4=“广东”/><d d1=“101281201” d2=“河源” d3=“heyuan” d4=“广东”/><d d1=“101281202” d2=“紫金” d3=“zijin” d4=“广东”/><d d1=“101281203” d2=“连平” d3=“lianping” d4=“广东”/><d d1=“101281204” d2=“和平” d3=“heping” d4=“广东”/><d d1=“101281205” d2=“龙川” d3=“longchuan” d4=“广东”/><d d1=“101281206” d2=“东源” d3=“dongyuan” d4=“广东”/><d d1=“101281301” d2=“清远” d3=“qingyuan” d4=“广东”/><d d1=“101281302” d2=“连南” d3=“liannan” d4=“广东”/><d d1=“101281303” d2=“连州” d3=“lianzhou” d4=“广东”/><d d1=“101281304” d2=“连山” d3=“lianshan” d4=“广东”/><d d1=“101281305” d2=“阳山” d3=“yangshan” d4=“广东”/><d d1=“101281306” d2=“佛冈” d3=“fogang” d4=“广东”/><d d1=“101281307” d2=“英德” d3=“yingde” d4=“广东”/><d d1=“101281308” d2=“清新” d3=“qingxin” d4=“广东”/><d d1=“101281401” d2=“云浮” d3=“yunfu” d4=“广东”/><d d1=“101281402” d2=“罗定” d3=“luoding” d4=“广东”/><d d1=“101281403” d2=“新兴” d3=“xinxing” d4=“广东”/><d d1=“101281404” d2=“郁南” d3=“yunan” d4=“广东”/><d d1=“101281406” d2=“云安” d3=“yunan” d4=“广东”/><d d1=“101281501” d2=“潮州” d3=“chaozhou” d4=“广东”/><d d1=“101281502” d2=“饶平” d3=“raoping” d4=“广东”/><d d1=“101281503” d2=“潮安” d3=“chaoan” d4=“广东”/><d d1=“101281601” d2=“东莞” d3=“dongguan” d4=“广东”/><d d1=“101281701” d2=“中山” d3=“zhongshan” d4=“广东”/><d d1=“101281801” d2=“阳江” d3=“yangjiang” d4=“广东”/><d d1=“101281802” d2=“阳春” d3=“yangchun” d4=“广东”/><d d1=“101281803” d2=“阳东” d3=“yangdong” d4=“广东”/><d d1=“101281804” d2=“阳西” d3=“yangxi” d4=“广东”/><d d1=“101281901” d2=“揭阳” d3=“jieyang” d4=“广东”/><d d1=“101281902” d2=“揭西” d3=“jiexi” d4=“广东”/><d d1=“101281903” d2=“普宁” d3=“puning” d4=“广东”/><d d1=“101281904” d2=“惠来” d3=“huilai” d4=“广东”/><d d1=“101281905” d2=“揭东” d3=“jiedong” d4=“广东”/><d d1=“101282001” d2=“茂名” d3=“maoming” d4=“广东”/><d d1=“101282002” d2=“高州” d3=“gaozhou” d4=“广东”/><d d1=“101282003” d2=“化州” d3=“huazhou” d4=“广东”/><d d1=“101282004” d2=“电白” d3=“dianbai” d4=“广东”/><d d1=“101282005” d2=“信宜” d3=“xinyi” d4=“广东”/><d d1=“101282006” d2=“茂港” d3=“maogang” d4=“广东”/><d d1=“101282101” d2=“汕尾” d3=“shanwei” d4=“广东”/><d d1=“101282102” d2=“海丰” d3=“haifeng” d4=“广东”/><d d1=“101282103” d2=“陆丰” d3=“lufeng” d4=“广东”/><d d1=“101282104” d2=“陆河” d3=“luhe” d4=“广东”/></c>创建如下两个类,并且根据xml的内容定义其属性。package com.demo.vo;import javax.xml.bind.annotation.XmlAccessType;import javax.xml.bind.annotation.XmlAccessorType;import javax.xml.bind.annotation.XmlAttribute;import javax.xml.bind.annotation.XmlRootElement;@XmlRootElement(name=“d”)@XmlAccessorType(XmlAccessType.FIELD)public class City { @XmlAttribute(name=“d1”) private String cityId; @XmlAttribute(name=“d2”) private String cityName; @XmlAttribute(name=“d3”) private String cityCode; @XmlAttribute(name=“d4”) private String province; public String getCityId() { return cityId; } public void setCityId(String cityId) { this.cityId = cityId; } public String getCityName() { return cityName; } public void setCityName(String cityName) { this.cityName = cityName; } public String getCityCode() { return cityCode; } public void setCityCode(String cityCode) { this.cityCode = cityCode; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } }package com.demo.vo;import java.util.List;import javax.xml.bind.annotation.XmlAccessType;import javax.xml.bind.annotation.XmlAccessorType;import javax.xml.bind.annotation.XmlElement;import javax.xml.bind.annotation.XmlRootElement;@XmlRootElement(name = “c”)@XmlAccessorType(XmlAccessType.FIELD)public class CityList { @XmlElement(name = “d”) private List<City> cityList; public List<City> getCityList() { return cityList; } public void setCityList(List<City> cityList) { this.cityList = cityList; }}引入工具类,实现将xml转换成java对象的过程。public class XmlBuilder { /** * 将XML转为指定的POJO * @param clazz * @param xmlStr * @return * @throws Exception */ public static Object xmlStrToOject(Class<?> clazz, String xmlStr) throws Exception { Object xmlObject = null; Reader reader = null; JAXBContext context = JAXBContext.newInstance(clazz); // XML 转为对象的接口 Unmarshaller unmarshaller = context.createUnmarshaller(); reader = new StringReader(xmlStr); xmlObject = unmarshaller.unmarshal(reader); if (null != reader) { reader.close(); } return xmlObject; }}获取城市列表的接口创建CityDataService,定义获取城市列表的方法。@Servicepublic class CityDataServiceImpl implements CityDataService{ @Override public List<City> listCity() throws Exception { Resource resource=new ClassPathResource(“citylist.xml”); BufferedReader br=new BufferedReader(new InputStreamReader(resource.getInputStream(), “utf-8”)); StringBuffer buffer=new StringBuffer(); String line=""; while((line=br.readLine())!=null) { buffer.append(line); } br.close(); CityList cityList=(CityList)XmlBuilder.xmlStrToOject(CityList.class, buffer.toString()); return cityList.getCityList(); }}根据城市Id同步天气数据的接口首先通过城市Id构建对应天气数据的url,然后通过restTemplate的getForEntity方法发起请求,获取返回的内容后使用set方法将其保存到Redis服务器中。 @Override public void syncDataByCityId(String cityId) { String url=WEATHER_URI+“citykey=” + cityId; this.saveWeatherData(url); } //将天气数据保存到缓存中,不管缓存中是否存在数据 private void saveWeatherData(String url) { //将url作为天气的key进行保存 String key=url; String strBody=null; ValueOperations<String, String>ops=stringRedisTemplate.opsForValue(); //通过客户端的get方法发起请求 ResponseEntity<String>respString=restTemplate.getForEntity(url, String.class); //判断请求状态 if(respString.getStatusCodeValue()==200) { strBody=respString.getBody(); } ops.set(key, strBody,TIME_OUT,TimeUnit.SECONDS); }Quartz的引入Quartz是一个Quartz是一个完全由java编写的开源作业调度框架,在这里的功能相当于一个定时器,定时执行指定的任务。创建同步天气数据的任务在Quartz中每个任务就是一个job,在这里我们创建一个同步天气数据的job。通过cityDataService的listCity方法获取xml文件中所有城市的列表,通过对城市列表的迭代得到所有城市的Id,然后通过weatherDataService的syncDataByCityId方法将对应Id的城市天气数据更新到Redis缓存中//同步天气数据public class WeatherDataSyncJob extends QuartzJobBean{ private final static Logger logger = LoggerFactory.getLogger(WeatherDataSyncJob.class); @Autowired private CityDataService cityDataService; @Autowired private WeatherDataService weatherDataService; @Override protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException { logger.info(“Weather Data Sync Job. Start!”); //城市ID列表 List<City>cityList=null; try { //获取xml中的城市ID列表 cityList=cityDataService.listCity(); } catch (Exception e) {// e.printStackTrace(); logger.error(“Exception!”, e); } //遍历所有城市ID获取天气 for(City city:cityList) { String cityId=city.getCityId(); logger.info(“Weather Data Sync Job, cityId:” + cityId); //实现根据cityid定时同步天气数据到缓存中 weatherDataService.syncDataByCityId(cityId); } logger.info(“Weather Data Sync Job. End!”); } }配置QuartzTIME设置的是更新的频率,表示每隔TIME秒就执行任务一次。@Configurationpublic class QuartzConfiguration { private static final int TIME = 1800; // 更新频率 // JobDetail @Bean public JobDetail weatherDataSyncJobDetail() { return JobBuilder.newJob(WeatherDataSyncJob.class).withIdentity(“weatherDataSyncJob”) .storeDurably().build(); } // Trigger @Bean public Trigger weatherDataSyncTrigger() { SimpleScheduleBuilder schedBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(TIME).repeatForever(); return TriggerBuilder.newTrigger().forJob(weatherDataSyncJobDetail()) .withIdentity(“weatherDataSyncTrigger”).withSchedule(schedBuilder).build(); }}测试结果天气数据同步结果 ...

February 28, 2019 · 4 min · jiezi

上手spring cloud(二)微应用之间的服务调用

微应用之间的服务调用服务调用示例以商品下单为例,比如将业务拆分为商品服务和订单服务,订单服务会调用商品服务的库存扣减。单个微服务工程,统一按以下目录编排:-product–product-common 商品服务公用对象–product-client 商品服务客户端,以jar包方式被订单服务依赖–product-server 商品服务,要注册到Eureka Server,外部通过product-client来调用我们的微服务工程之间,依赖关系如下:我们从商品微服务工程开始外层product初始pom.xml:<modelVersion>4.0.0</modelVersion><parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/></parent><groupId>com.hicoview</groupId><artifactId>product</artifactId><version>0.0.1-SNAPSHOT</version><name>product</name><description>Demo project for Spring Boot</description><modules> <module>product-common</module> <module>product-client</module> <module>product-server</module></modules><properties> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> <product-common.version>0.0.1-SNAPSHOT</product-common.version></properties><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.hicoview</groupId> <artifactId>product-common</artifactId> <version>${product-common.version}</version> </dependency> </dependencies></dependencyManagement>product-common初始pom.xml:<modelVersion>4.0.0</modelVersion><parent> <groupId>com.hicoview</groupId> <artifactId>product</artifactId> <version>0.0.1-SNAPSHOT</version></parent><artifactId>product-common</artifactId><dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency></dependencies>product-client初始pom.xml:<modelVersion>4.0.0</modelVersion><parent> <groupId>com.hicoview</groupId> <artifactId>product</artifactId> <version>0.0.1-SNAPSHOT</version></parent><artifactId>product-client</artifactId><dependencies> <dependency> <groupId>com.hicoview</groupId> <artifactId>product-common</artifactId> </dependency></dependencies>product-server的初始pom.xml:<modelVersion>4.0.0</modelVersion><parent> <groupId>com.hicoview</groupId> <artifactId>product</artifactId> <version>0.0.1-SNAPSHOT</version></parent><artifactId>product-server</artifactId><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.hicoview</groupId> <artifactId>product-common</artifactId> </dependency></dependencies><build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build>接下来我们调整product-server工程商品服务需注册到Eureka Server,添加Eureka Client相关依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>启动类加注解@EnableDiscoveryClient:package com.hicoview.product;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); }}配置application.yml:eureka: client: service-url: defaultZone: http://localhost:8761/eureka/spring: application: name: productserver: port: 8080启动product-server,访问注册中心http://localhost:8761,PRODUCT注册上去了,再继续往下看。我们写个简单的服务调用示例一下,订单服务下单逻辑调用商品服务进行扣减库存。继续修改product-server工程package com.hicoview.product.service;import java.util.List;public interface ProductService { // 扣减库存 void decreaseStock();}package com.hicoview.product.service.impl;import com.hicoview.product.service.ProductService;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;@Service@Slf4jpublic class ProductServiceImpl implements ProductService { @Override public void decreaseStock() { log.info("——扣减库存—–"); }}spring cloud的RPC服务是使用HTTP方式调用的,所以还要创建ProductController:package com.hicoview.product.controller;import com.hicoview.product.service.ProductService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/product")public class ProductController { @Autowired private ProductService productService; @PostMapping("/decreaseStock") public void decreaseStock() { productService.decreaseStock(); }}接下来修改product-client工程pom.xml增加Feign依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>创建ProductClient:package com.hicoview.product.client;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;@FeignClient(name=“product”)public interface ProductClient { // 通过Feign来代理对PRODUCT服务的HTTP请求 @PostMapping("/product/decreaseStock") void decreaseStock();}再次启动product-server,没问题的话,把product上传到本地maven仓库:mvn -Dmaven.test.skip=true -U clean install然后初始化订单微服务工程后继续。修改order-server工程order-server的服务可能会被User等其他服务调用,也是个Eureka client,添加依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>启动类加注解@EnableDiscoveryClientpackage com.hicoview.product;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); }}配置application.yml:eureka: client: service-url: defaultZone: http://localhost:8761/eureka/spring: application: name: orderserver: port: 8081启动order-server,访问注册中心http://localhost:8761/:ORDER服务注册成功后,继续调整order-server工程由于订单服务要调用商品服务,需添加对product-client依。修改pom.xml:<dependency> <groupId>com.hicoview</groupId> <artifactId>product-client</artifactId></dependency>对版本的管理统一交给上层,修改上层order的pom.xml,增加以下配置:<properties> … <product-client.version>0.0.1-SNAPSHOT</product-client.version></properties><dependencyManagement> <dependencies> … <dependency> <groupId>com.hicoview</groupId> <artifactId>product-client</artifactId> <version>${product-client.version}</version> </dependency> </dependencies></dependencyManagement>回到order-server,修改启动类,添加Feign扫描路径:package com.hicoview.order;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication@EnableDiscoveryClient@EnableFeignClients(basePackages = “com.hicoview.product.client”)public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); }}然后按顺序创建Controller、Service:package com.hicoview.order.controller;import com.hicoview.order.service.OrderService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/order")public class OrderController { @Autowired private OrderService orderService; // 创建订单 @PostMapping("/create") public void create() { orderService.createOrder(); }}public interface OrderService { void createOrder();}@Service@Slf4jpublic class OrderServiceImpl implements OrderService { @Autowired private ProductClient productClient; @Override public void createOrder() { log.info("——创建订单—–"); // 调用商品扣减服务 productClient.decreaseStock(); }}启动order-server,发起下单请求:curl -X POST http://localhost:8081/order/create服务调用成功,order-server和product-server输出:2019-02-26 11:09:58.408 INFO 3021 — [nio-8081-exec-3] c.h.order.service.impl.OrderServiceImpl : ——创建订单—–2019-02-26 11:09:58.430 INFO 3015 — [nio-8080-exec-3] c.h.p.service.impl.ProductServiceImpl : ——扣减库存—–为了示例的极简,上面服务调用没有涉及到入参和返回值。如果需要定义参数或返回值,考虑到内外部都会用到,需将参数bean定义在product-common中作为公共bean。Feign和RestTemplateRest服务的调用,可以使用Feign或RestTemplate来完成,上面示例我们使用了Feign。Feign(推荐)Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。Feign会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign整合了Ribbon和Hystrix(关于Hystrix我们后面再讲),可以让我们不再需要显式地使用这两个组件。RestTemplateRestTemplate提供了多种便捷访问远程Http服务的方法,可以了解下。第一种方式,直接使用RestTemplate,URL写死:// 1. RestTemplate restTemplate = new RestTemplate();restTemplate.getForObject(“http://localhost:8080/product/decreaseStock”, String.class);这种方式直接使用目标的IP,而线上部署的IP地址可能会切换,同一个服务还会启多个进程,所以有弊端。第二种方式,利用LoadBalancerClient,通过应用名获取URL,然后再使用RestTemplate:@Autowiredprivate LoadBalancerClient loadBalancerClient;// 1. 第二种方式,通过应用名字拿到其中任意一个host和portServiceInstance serviceInstance = loadBalancerClient.choose(“PRODUCT”);String url = String.format(“http://%s:%s”, serviceInstance.getHost(), serviceInstance.getPort() + “/product/decreaseStock”);RestTemplate restTemplate = new RestTemplate();restTemplate.getForObject(url, String.class);第三种方式,写一个config把RestTemplate作为一个bean配置上去:@Componentpublic class RestTemplateConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }}@Autowiredprivate RestTemplate restTemplate;// 使用时url里直接用应用名PRODUCT即可restTemplate.getForObject(“http://PRODUCT/product/decreaseStock”, String.class);Ribbon客户端负载均衡器Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。在Spring Cloud中,Ribbon可自动从Eureka Server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。还有微服务之间的调用,通过Feign或RestTemplate找到一个目标服务。以及API网关Zuul的请求转发等内容,实际上都是通过Ribbon来实现的。 ...

February 27, 2019 · 2 min · jiezi

上手spring cloud(三)统一配置中心

统一配置中心Spring Cloud Config为各应用环境提供了一个中心化的外部配置。配置服务器默认采用git来存储配置信息,这样就有助于对配置进行版本管理,并且可以通过git客户端工具来方便维护配置内容。当然它也提供本地化文件系统的存储方式。使用集中式配置管理,在配置变更时,可以通知到各应用程序,应用程序不需要重启。Config Server创建Config Server端工程config-server:File -> New->Product… -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next(springboot选择2.0以上)选择Cloud Discovery -> 选择Eureka Discovery选择Cloud Config -> 选择Config Server由于选择了Eureka Discovery和Config Server,创建成功后pom.xml里已经帮你引入了以下依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>Config Server也是要注册到Eureka,作为Eureka Client,我们还要加入如下依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><!– 避免后面的数据库配置出错,mysql依赖也加了 –><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId></dependency>再给启动类加上注解@EnableDiscoveryClient和@EnableConfigServer:package com.hicoview.config;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.config.server.EnableConfigServer;@SpringBootApplication@EnableDiscoveryClient@EnableConfigServerpublic class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); }}配置application.yml:eureka: client: service-url: defaultZone: http://localhost:8761/eureka/spring: application: name: config cloud: config: server: git: uri: http://code.hicoview.com:8000/backend/config.git username: root password: 8ggf9afd6g9gj # 配置文件下载后存储的本地目录 basedir: /Users/zhutx/springcloud/config/basedirserver: port: 9999然后按照配置的git仓库地址,在github或gitlab上创建config仓库。以商品微服务的配置来演示,在config仓库创建product-dev.yml:server: port: 8080spring: datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: passwd_1986 url: jdbc:mysql://127.0.0.1:3306/SpringCloud_Sell?characterEncoding=utf-8&useSSL=false启动作为Config Server的config-server工程,查看http://localhost:8761:访问配置服务端的以下任意地址,都可以显示出对应格式的配置内容:http://localhost:9999/product-dev.ymlhttp://localhost:9999/product-dev.propertieshttp://localhost:9999/product-dev.json可见,Config Server获取到了远程git仓库上的配置,并将其作为自身的REST服务提供了出去。接下来我们看看配置客户端Config Client(即product-server)怎么引用配置。Config Client我们给product-server加入配置客户端的依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId></dependency>修改application.yml配置:spring: application: name: product cloud: config: discovery: enabled: true service-id: CONFIG profile: deveureka: client: service-url: defaultZone: http://localhost:8761/eureka/这样子就可以从Eureka服务注册中心找到CONFIG服务,并拿到product-dev.yml了。启动product-server,查看eureka注册中心,CONFIG这个Config Server服务已经注册上去了:如果代码里有操作数据库,那么启动其实会出错,因为spring boot不知道配置加载顺序。我们期望先拿到CONFIG的配置,再初始化数据库。解决办法是把product-server的application.yml改成bootstrap.yml就好。微服务工程使用配置服务的情况下,注意将application.yml都改成bootstrap.yml。并且,让bootstrap.yml文件只保留Eureka配置和获取Config Server服务的配置;另外,如果生产环境要使用统一配置中心,可以启动多个Config Server进程,保持高可用。同样的操作,把order-server配置也抽取到外部Spring Cloud Bus下图是当前的配置工作机制,config-server拉取远端git配置,并在本地存一份。然后config-server通过把自身注册到Eureka从而提供了拉取配置的服务,而配置客户端(product和order)通过引入config-client依赖,在启动时便能获取加载到了配置。我们需要做到修改远程配置,应用程序不重启,还需要借助Spring Cloud Bus。Spring Cloud Bus集成了MQ,并为config-server提供了这个配置刷新服务(bus-refresh)。如下图所示,做法是远端git修改配置后,通过webhook调用config-server的/bus-refresh服务,发布RabbitMQ消息,config-client接收消息并更新配置。我们先安装RabbitMQ:# docker安装rabbitmqdocker run -d –hostname my-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3.7.9-management# 验证下docker ps | grep ‘rabbitmq’能成功访问RabbitMQ控制台http://localhost:15671,继续。修改Config Server端,增加依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency>修改application.yml,增加以下配置,把包括bus-refresh在内的所有config server的服务都暴露出来:management: endpoints: web: exposure: include: “*“我们拿product-server来演示,修改product-server的pom增加依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency>然后提供一个接口,方便我们在配置变验证结果:@RestController@RequestMapping("/env”)@RefreshScopepublic class EnvController { @Value("${env}”) private String env; @GetMapping("/print") public String print() { return env; }}测试下,我们修改远端git配置,先增加env配置:server: port: 8080spring: datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: passwd_1986 url: jdbc:mysql://127.0.0.1:3306/SpringCloud_Sell?characterEncoding=utf-8&useSSL=false# 增加了该配置env: dev重启下config-server和product-server。访问 product-server http://localhost:8080/env/print,显示dev然后我们把远端env配置项改成test调用config-server的配置刷新服务 bus-refresh:curl -v -X POST http://localhost:9999/actuator/bus-refresh再次访问 product-server http://localhost:8080/env/print,显示test至此,已经做到了变更配置不重启应用。我们再借助Git仓库的webhook功能,在push指令发生后帮我们发个bus-refresh请求就完美了。Gitlab的话在仓库的这个位置:Repository -> Settings -> Integrations -> Add webhook ...

February 27, 2019 · 1 min · jiezi

上手spring cloud(一)Eureka服务注册与发现

spring cloud面向开发人员,对分布式系统从编程模型上提供了强大的支持。可以说是分布式系统解决方案的全家桶,极大地降低了开发与构建分布式系统的门槛。包括了诸如下列功能:Eureka服务注册发现统一配置中心Spring Cloud Stream异步调用Zuul服务网关Hystrix服务降级容错服务调用跟踪Netflix是一家互联网流媒体播放商,美国视频巨头,最近买下《流浪地球》,并在190多个国家播出的就是它了。随着Netflix转型为一家云计算公司,也开始积极参与开源项目。Netflix OSS(Open Source)就是由Netflix公司开发的框架,解决上了规模之后的分布式系统可能出现的一些问题。spring cloud基于spring boot,为spring boot提供Netflix OSS的集成。这段时间对spring cloud进行了学习总结,留下点什么,好让网友上手spring cloud时少躺坑。我用的版本如下:spring-boot 2.0.2.RELEASEspring-cloud Finchley.RELEASEspring boot不同版本之间在配置和依赖上会有差异,强烈建议创建工程后先把spring boot和spring cloud的版本依赖调整成与本篇一致,快速上手少躺坑。上手完后,你可以尝试升到最新版本,官方spring-cloud页(进去后拉到最下面)提供了spring-cloud与spring-boot版本匹配表。升级完后建议重新测试一下相关功能。Eureka服务注册与发现Eureka Server服务注册中心IntelliJ IDEA(我是2018.2.5的版本),创建工程eureka-server:File -> New->Product… -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next -> 选择Cloud Discovery -> 选择Eureka Server注意创建工程后调整spring boot和spring cloud的版本:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/> <!– lookup parent from repository –></parent>…<properties> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version></properties>由于选择了Eureka Server,创建成功后pom.xml里已经帮你引入了以下依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>我们给启动类加上注解@EnableEurekaServer:package com.hicoview.eureka;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication@EnableEurekaServerpublic class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); }}配置application.yml(习惯yml风格的同学,把application.properties直接改过来):eureka: client: # 默认eureka服务注册中心会将自身作为客户端来尝试注册,所以我们需要禁用它的客户端注册行为 register-with-eureka: false # 默认30秒会更新客户端注册上来的服务清单,启动时就不获取了,不然启动会有报错,虽然不影响 fetch-registry: false server: # 关闭注册中心自我保护(默认是true,生产环境不建议关闭,去掉该配置项或改成true) enable-self-preservation: falsespring: application: name: eurekaserver: port: 8761服务注册中心搞定,启动成功后访问http://localhost:8761,可以看到注册中心页面:Eureka Server高可用只要启动多个注册中心进程即可,多个之间,两两注册到对方。比如,8761端口启动一个eueka服务端,注册到8762:eureka: client: service-url: # 提供其他注册中心的地址,注册中心将自身以客户端注册的方式注册到其他注册中心去 defaultZone: http://localhost:8762/eureka/ register-with-eureka: false fetch-registry: false server: enable-self-preservation: falsespring: application: name: eurekaserver: port: 87618762端口启动一个eueka服务端,注册到8761:eureka: client: service-url: # 提供其他注册中心的地址,注册中心将自身以客户端注册的方式注册到其他注册中心去 defaultZone: http://localhost:8761/eureka/ register-with-eureka: false fetch-registry: false server: enable-self-preservation: falsespring: application: name: eurekaserver: port: 8762访问http://localhost:8761,可以看到8761这里有了8762的副本:访问http://localhost:8762,也是一样,你可以试试。接下来我们用Eureka Client来验证下服务的注册。Eureka Client实际上充当Eureka Client角色应该是各种业务的微服务工程了,这里为了快速演示一下服务注册,临时先搞个无意义的client工程作为Eureka Client示范。创建Eureka Client工程eureka-client:File -> New->Product… -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next -> 选择Cloud Discovery -> 选择Eureka Discovery注意每次创建工程后的第一件事,改spring-boot和spring-cloud的版本,不再赘述由于选择了Eureka Discovery,创建成功后pom.xml里已经帮你引入了以下依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>修改pom.xml,还要加入web依赖,不然无法启动成功:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>启动类加注解@EnableDiscoveryClient:package com.hicoview.client;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class ClientApplication { public static void main(String[] args) { SpringApplication.run(ClientApplication.class, args); }}配置application.yml:eureka: client: service-url: # 注册中心地址,如果注册中心是高可用,那么这里后面可以添加多个地址,逗号分开 defaultZone: http://localhost:8761/eureka/ #defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/spring: application: name: client启动成后,访问注册中心http://localhost:8761:完成了Eureka服务注册示例,接下来我们简单模拟一个业务场景,示范微服务之间的服务调用。 ...

February 27, 2019 · 1 min · jiezi

Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,本节将介绍项目中Redis的引入。Redis下载教程。若对Redis感兴趣,还可以看一下我的另一篇文章造个轮子 | 自己动手写一个Redis存在问题:数据来源于第三方的接口,依赖性太强。可能带来的不良结果:(1)延时性:用户访问我们的时候,我们需要再去访问第三方的接口,我们是数据的中间者,不是数据的产生者,有一定的延时性。(2)访问上限:免费的接口,可能会达到上限。(3)调死:可能将对方的接口给调死。解决方案:使用redis缓存系统,提高整体的并发访问能力。Redis 是一个高性能的key-value数据库,基于内存的缓存系统,对内存的操作时非常快的,所以可以做到及时响应。为什么选择Redis(1)及时响应(2)减少服务调用Redis如何引入Redis是一个key-value结构的数据存储系统,这里我们使用天气数据的uri作为它的key,通过ValueOperations<String, String>ops对象的set方法将数据写入缓存中,通过其get方法可以从缓存中获取数据,并且使用TIME_OUT设置缓存失效的时间。我们并不是每次都去调用第三方的接口,若Redis缓存中有要查找的天气数据,则从缓存中取;若缓存中没有,则请求第三方接口,然后将数据写入Redis缓存中。 private WeatherResponse doGetWeahter(String uri) { String key = uri; String strBody = null; ObjectMapper mapper = new ObjectMapper(); WeatherResponse resp = null; ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); // 先查缓存,缓存有的取缓存中的数据 if (stringRedisTemplate.hasKey(key)) { logger.info(“Redis has data”); strBody = ops.get(key); } else { logger.info(“Redis don’t has data”); // 缓存没有,再调用服务接口来获取 ResponseEntity<String> respString = restTemplate.getForEntity(uri, String.class); if (respString.getStatusCodeValue() == 200) { strBody = respString.getBody(); } // 数据写入缓存 ops.set(key, strBody, TIME_OUT, TimeUnit.SECONDS); } try { resp = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { //e.printStackTrace(); logger.error(“Error!",e); } return resp; } ...

February 27, 2019 · 1 min · jiezi

翻译: Spring Cloud Feign使用文档

转载请注明出处: 翻译: Spring Cloud Feign使用文档Why Feign and not X?Feign使用诸如Jersey和CXF之类的工具来实现ReST或SOAP服务的java客户端, 此外, Feign允许你在http库(如: Apache HC)之上编写自己的代码. 通过自定义解码器(decoders)和错误处理(error handing), Feign可以用最小的开销和最少的代码将你的代码关联到任何基于文本的http接口(http APIS),How does Feign work?Feign是通过将注解(annotations)转换成模板请求来实现它的功能的, Feign可以将请求参数直接应用到这些模板上. 尽管Feign只支持基于文本的接口, 但同样的它能显著地简化系统的方方面面, 如请求重放等, 此外, Feign也可以使你的单元测试更加简单.Java Version CampatibilityFeign 10.x及以上的版本是基于Java 8构建的, 且应该同样支持Java 9、10、11, 如果你需要在JDK 6的版本上使用的话, 请使用Feign 9.x版本.Basics下面的代码是适配Retrofit示例的用法:interface GitHub { @RequestLine(“GET /repos/{owner}/{repo}/contributors”) List<Contributor> contributors(@Param(“owner”) String owner, @Param(“repo”) String repo);}public static class Contributor { String login; int contributions;}public class MyApp { public static void main(String… args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, “https://api.github.com”); // Fetch and print a list of the contributors to this library. List<Contributor> contributors = github.contributors(“OpenFeign”, “feign”); for (Contributor contributor : contributors) { System.out.println(contributor.login + " (" + contributor.contributions + “)”); } }}Interface AnnotationsFeign的注解定义了接口与底层http客户端功能之间的约定, 默认情况下各个注解的约定含义如下:AnnotationInterface TargetUsage@RequestLine接口定义请求的HttpMethod和UriTemplate. 模板中可以使用大括号包围的表达式({expression}), 表达式的值由@Param对应参数的注解值提供.@Param参数定义模板变量, 变量的值应该由名字相对应的表达式提供.@Headers方法、Type定义HeaderTemplate; 使用@Param注解的值解析对应的表达式. 当该注解应用在Type上时, 该模板会被应用到每一个请求上. 当该注解应用在方法上时, 该模板仅会被应用到对应的方法上.@QueryMap参数将键值对类型的Map、POJO展开成地址上的请求参数(query string)@HeaderMap参数将键值对类型的Map展开成请求头Http Headers.@Body方法定义与UriTemplate和HeaderTemplate类似的模板(Template), 该模板可以使用@Param的注解值解析对应的表达式Templates and ExpressionsFeign支持由URI Template - RFC 6570定义的简单字符串(Level 1)表达式, 表达式的值从相关方法上对应@Param注解提供, 示例如下:public interface GitHub { @RequestLine(“GET /repos/{owner}/{repo}/contributors”) List<Contributor> getContributors(@Param(“owner”) String owner, @Param(“repo”) String repository); class Contributor { String login; int contributions; }}public class MyApp { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, “https://api.github.com”); /* The owner and repository parameters will be used to expand the owner and repo expressions * defined in the RequestLine. * * the resulting uri will be https://api.github.com/repos/OpenFeign/feign/contributors / github.contributors(“OpenFeign”, “feign”); }}表达式必须使用大括号({})包裹着, 并且支持使用冒号(:)分隔的正则表达式来限定表达式的值. 如限定上述例子的owner参数的值必须是字母: {owner:[a-zA-Z]}.Request Parameter ExpansionRequestLine和QueryMap遵循 URI Template - RFC 6570规范对一级模板(Level 1 templates)的规定:未被解析的值将会被忽略.所有未编码或者通过@Param注解标记为已编码(encoded)的字符和变量值都使用pct编码(pct-encoded).可以从Advanced Usage一节查看更多示例.What about slashes? /默认情况下, @RequestLine和@QueryMap模板不会对正斜杠/进行编码, 如果需要默认对其进行编码的话, 可以将@RequestLine的decodeSlash属性值设置为false.What about plus? +根据URI规范, +可以使用在URI地址和请求参数(query segments)这两个部分上, 然而在请求参数(query)上对该符号的处理却有可能不一致, 在一些遗留的系统上, +会被解析成一个空白符(space). 对此, Feign采用现代系统对+的解释, 不会将+认为是一个空白符(space), 并将请求参数上的+编码为%2B.如果你希望将+当成空白符(space), 那么请直接使用一个空格 或者直接将其编码为%20.Custom Expansion@Param注解有一个可选的参数expander可以用来控制单个参数的展开行为(expansion), 该属性的值必须指向一个实现了Expander接口的类:public interface Expander { String expand(Object value);}对该方法的返回值的处理与上述规则相同, 如果返回值是null或者是一个空字符串, 那么该值会被忽略. 如果返回值不是使用pct编码(pct-encoded)的, 将会自动转换成pct编码. 可以从 Custom @Param Expansion 一节查看更多示例.Request Headers Expansion@Headers和HeaderMap模板对 Request Parameter Expansion 一节阐述的规则做以下修改, 并遵循之:未被解析的值将会被忽略, 但如果解析到一个空的值(empty header value), 那么对应的请求头会被移除.不会对请求头使用pct编码(pct-encoding).可以从Headers一节查看示例.关于@Param参数和参数名需要注意的点无论是在@RequestLine、@QueryMap、@BodyTemplate还是@Headers上的表达式, 只要表达式内的变量名字相同, 那么它们的值也必然相同. 如下面的例子, contentType的值会同时被解析到请求头(header)和路径(path)上:public interface ContentService { @RequestLine(“GET /api/documents/{contentType}”) @Headers(“Accept: {contentType}”) String getDocumentByType(@Param(“contentType”) String type);}当你在设计你的接口的一定要牢记这一点.Reuqest Body ExpansionBody模板对 Request Parameter Expansion 一节阐述的规则做以下修改, 并遵循之:未被解析的值将会被忽略.展开的值在被解析到请求体之前不会经过Encoder处理.必须指定Content-Type请求头, 可以从 Body Templates一节查看示例.Customization你可以在很多地方对Feign进行定制. 比如, 你可以使用Feign.builder()对自定义的组件构建API接口:interface Bank { @RequestLine(“POST /account/{id}”) Account getAccountInfo(@Param(“id”) String id);}public class BankService { public static void main(String[] args) { Bank bank = Feign.builder().decoder( new AccountDecoder()) .target(Bank.class, “https://api.examplebank.com”); }}Multiple InterfacesFeign客户以对使用Target<T>(默认是HardCodedTarget<T>)定义的对象生成多个API接口, 这样你可以在执行前动态发现服务或者对请求进行装饰.例如, 下面的代码可以实现为从身份服务中获取当前url和授权令牌(auth token), 然后设置到每个请求上:public class CloudService { public static void main(String[] args) { CloudDNS cloudDNS = Feign.builder() .target(new CloudIdentityTarget<CloudDNS>(user, apiKey)); } class CloudIdentityTarget extends Target<CloudDNS> { /* implementation of a Target / }}ExamplesFeign包含了GitHub和Wikipedia的客户端示例代码, 在实践中也可以参考这些项目, 尤其是example daemon.IntegrationsFeign在设计上就希望能够和其他开源项目很好的整合到一起, 我们也很乐于将你喜欢的模块添加进来.GsonGson包含了和JSON接口相关的编码(GsonEncoder)、解码器(GsonDecoder), 将它将它用到Feign.Builder的方式如下:public class Example { public static void main(String[] args) { GsonCodec codec = new GsonCodec(); GitHub github = Feign.builder() .encoder(new GsonEncoder()) .decoder(new GsonDecoder()) .target(GitHub.class, “https://api.github.com”); }}JacksonJackson包含了和JSON接口相关的编码(JacksonEncoder)、解码器(JacksonDecoder), 将它将它用到Feign.Builder的方式如下:public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(GitHub.class, “https://api.github.com”); }}SaxSaxDecoder提供了可以与普通JVM和Android环境兼容的方式解析XML文本, 下面的例子展示了如何使用:public class Example { public static void main(String[] args) { Api api = Feign.builder() .decoder(SAXDecoder.builder() .registerContentHandler(UserIdHandler.class) .build()) .target(Api.class, “https://apihost”); }}JAXBJAXB包含了和XML接口相关的编码器(JAXBEncoder)、解码器(JAXBEncoder), 将它将它用到Feign.Builder的方式如下:public class Example { public static void main(String[] args) { Api api = Feign.builder() .encoder(new JAXBEncoder()) .decoder(new JAXBDecoder()) .target(Api.class, “https://apihost”); }}JAX-RSJAXRSContract使用JAX-RS规范提供的标准覆盖了对注解的处理, 目前实现的是1.1版的规范, 示例如下:interface GitHub { @GET @Path("/repos/{owner}/{repo}/contributors") List<Contributor> contributors(@PathParam(“owner”) String owner, @PathParam(“repo”) String repo);}public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .contract(new JAXRSContract()) .target(GitHub.class, “https://api.github.com”); }}OkHttpOkHttpClient直接将Feign的http请求直接交由OkHttp处理, 后者实现了SPDY协议和提供了更好的网络控制能力.将OkHttp整合到Feign中需要你把OkHttp模块放到classpath下, 然后做如下配置:public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .client(new OkHttpClient()) .target(GitHub.class, “https://api.github.com”); }}RibbonRibbonClient会覆盖Feign客户端的URL解析, 以实现由Ribbon提供的智能路由和弹性能力.将Ribbon与Feign整合需要你将url中的主机名(host)部分替换成Ribbon客户端名. 例如Ribbon客户端明为myAppProd:public class Example { public static void main(String[] args) { MyService api = Feign.builder() .client(RibbonClient.create()) .target(MyService.class, “https://myAppProd”); }}Java 11 Http2Http2Client直接将Feign的http请求交给Java11 New HTTP/2 Client处理, 后者实现了HTTP/2协议.要将New HTTP/2 Client与Feign整合使用, 你需要使用Java SDK 11, 并做如下配置:GitHub github = Feign.builder() .client(new Http2Client()) .target(GitHub.class, “https://api.github.com”);HystrixHystrixFeign实现了由Hystrix提供的断路器功能.要将Hystrix与Feign整合, 你需要将Hystrix模块放到classpath下, 并使用HystrixFeign:public class Example { public static void main(String[] args) { MyService api = HystrixFeign.builder().target(MyService.class, “https://myAppProd”); }}SOAPSOAP包含了XML接口相关的编码器(SOAPEncoder)、解码器(SOAPDecoder).该模块通过JAXB和SOAPMessage实现了对SOAP Body的编码和解码的支持, 通过将SOAPFault包装秤javax.xml.ws.soap.SOAPFaultException实现了对SOAPFault解码的功能, 因此, 对于SOAPFault的处理, 你只需要捕获SOAPFaultException.使用示例如下:public class Example { public static void main(String[] args) { Api api = Feign.builder() .encoder(new SOAPEncoder(jaxbFactory)) .decoder(new SOAPDecoder(jaxbFactory)) .errorDecoder(new SOAPErrorDecoder()) .target(MyApi.class, “http://api”); }}如果SOAP Faults的响应使用了表示错误的状态码(4xx, 5xx, …)的话, 那么你还需要添加一个SOAPErrorDecoder.SLF4JSLF4JModule实现了将Feign的日志重定向到SLF4J, 这允许你很容易的就能使用你想用的日志后端(Logback、Log4J等).要将SLF4J与Feign整合, 你需要将SLF4J模块和对应的日志后端模块放到classpath下, 并做如下配置:public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .logger(new Slf4jLogger()) .target(GitHub.class, “https://api.github.com”); }}DecodersFeign.builder()允许你手动指定额外的配置, 如配置如何对响应进行解析.如果你接口定义的方法的返回值是除了Response、String、byte[]或void之外的类型, 那么你必须配置一个非默认的Decoder.下面的代码展示了如何配置使用feign-gson对JSON解码:public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, “https://api.github.com”); }}如果你想在对响应进行解码之前先对其做处理的话, 你可以使用mapAndDecode方法, 下面的代码展示了对一个jsonp响应的处理, 在将响应交给JSON解码器之前, 需要先对jsonp做处理:public class Example { public static void main(String[] args) { JsonpApi jsonpApi = Feign.builder() .mapAndDecode((response, type) -> jsopUnwrap(response, type), new GsonDecoder()) .target(JsonpApi.class, “https://some-jsonp-api.com”); }}Encoders将一个请求体发送到服务器的最简单的办法是定义一个POST请求方法, 该方法的参数类型是String或byte[], 且参数上不带任何注解, 并且你可能还需要设置Content-Type请求头(如果没有的话):interface LoginClient { @RequestLine(“POST /”) @Headers(“Content-Type: application/json”) void login(String content);}public class Example { public static void main(String[] args) { client.login("{"user_name": "denominator", "password": "secret"}"); }}而通过配置Encoder, 你可以发送一个类型安全的请求体, 下面的例子展示了使用feign-gson扩展来实现编码:static class Credentials { final String user_name; final String password; Credentials(String user_name, String password) { this.user_name = user_name; this.password = password; }}interface LoginClient { @RequestLine(“POST /”) void login(Credentials creds);}public class Example { public static void main(String[] args) { LoginClient client = Feign.builder() .encoder(new GsonEncoder()) .target(LoginClient.class, “https://foo.com”); client.login(new Credentials(“denominator”, “secret”)); }}@Body templates使用@Body注解的模板会使用@Param注解的值来展开模板内部的表达式, 对于POST请求你可能还需要设置Content-Type请求头(如果没有的话):interface LoginClient { @RequestLine(“POST /”) @Headers(“Content-Type: application/xml”) @Body("<login "user_name"="{user_name}" "password"="{password}"/>") void xml(@Param(“user_name”) String user, @Param(“password”) String password); @RequestLine(“POST /”) @Headers(“Content-Type: application/json”) // json curly braces must be escaped! @Body("%7B"user_name": "{user_name}", "password": "{password}"%7D") void json(@Param(“user_name”) String user, @Param(“password”) String password);}public class Example { public static void main(String[] args) { client.xml(“denominator”, “secret”); // <login “user_name”=“denominator” “password”=“secret”/> client.json(“denominator”, “secret”); // {“user_name”: “denominator”, “password”: “secret”} }}HeadersFeign支持在api上为每个请求设置请求头, 也支持为每个客户端的请求设置请求头, 你可以根据实际场景进行选择.Set headers using apis对于那些明确需要设置某些请求头的接口的情况, 适用于将请求头的定义作为接口的一部分.静态配置的请求头可以通过在接口上使用@Headers注解设置:@Headers(“Accept: application/json”)interface BaseApi<V> { @Headers(“Content-Type: application/json”) @RequestLine(“PUT /api/{key}”) void put(@Param(“key”) String key, V value);}也可以在方法上的@Headers使用变量展开动态指定请求头的内容:public interface Api { @RequestLine(“POST /”) @Headers(“X-Ping: {token}”) void post(@Param(“token”) String token);}有时候, 对于同一个接口或客户端的请求头, 其键和值可能会随着不同的方法调用而发生变化, 且不可预知(例如: 自定义元数据请求头字段"x-amz-meta-“或"x-goog-meta-”), 此时可以在接口上声明一个Map参数, 并使用@HeaderMap注解将Map的内容设置为对应请求的请求头:public interface Api { @RequestLine(“POST /”) void post(@HeaderMap Map<String, Object> headerMap);}上述的几个方法都可以在接口上指定请求的请求头, 且不需要在构造时对Feign客户端做任何的定制.Setting headers per target当同一个接口的请求需要针对不同的请求对象(endpoints)配置不同的请求头, 或者需要对同一个接口的每个请求都定制其请求头时, 可以在Feign客户端上使用RequestInterceptor或Target来设置请求头.使用RequestInterceptor设置请求头的例子可以在Request Interceptor一节中查看示例.使用Target设置请求头的示例如下: static class DynamicAuthTokenTarget<T> implements Target<T> { public DynamicAuthTokenTarget(Class<T> clazz, UrlAndTokenProvider provider, ThreadLocal<String> requestIdProvider); @Override public Request apply(RequestTemplate input) { TokenIdAndPublicURL urlAndToken = provider.get(); if (input.url().indexOf(“http”) != 0) { input.insert(0, urlAndToken.publicURL); } input.header(“X-Auth-Token”, urlAndToken.tokenId); input.header(“X-Request-ID”, requestIdProvider.get()); return input.request(); } } public class Example { public static void main(String[] args) { Bank bank = Feign.builder() .target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider)); } }上述方法的最终效果取决于你对RequestInterceptor或Target内部的实现, 可以通过这种方法对每个Feign客户端的所有接口调用设置请求头. 这在一些场景下是非常有用的, 如对每个Feign客户端的所有请求设置认证令牌authentication token. 这些方法是在接口调用者所在的线程中执行的(译者注: 需要注意线程安全), 因此请求头的值可以是在调用时根据上下文动态地设置. 例如, 可以根据不同的调用线程, 从ThreadLocal里读取不同的数据设置请求头.Advanced usageBase Apis大多数情况下服务的接口都遵循相同的约定. Feign使用单继承的方式来实现, 比如下面的例子:interface BaseAPI { @RequestLine(“GET /health”) String health(); @RequestLine(“GET /all”) List<Entity> all();}你可以通过继承的方式来拥有BaseAPI的接口, 并实现其他特定的接口:interface CustomAPI extends BaseAPI { @RequestLine(“GET /custom”) String custom();}很多时候, 接口对资源的表示也是一致的, 因此, 也可以在基类的接口中使用泛型参数:@Headers(“Accept: application/json”)interface BaseApi<V> { @RequestLine(“GET /api/{key}”) V get(@Param(“key”) String key); @RequestLine(“GET /api”) List<V> list(); @Headers(“Content-Type: application/json”) @RequestLine(“PUT /api/{key}”) void put(@Param(“key”) String key, V value);}interface FooApi extends BaseApi<Foo> { }interface BarApi extends BaseApi<Bar> { }Logging你可以通过为Feign客户端设置Logger来记录其http日志, 最简单的实现如下:public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .logger(new Logger.JavaLogger().appendToFile(“logs/http.log”)) .logLevel(Logger.Level.FULL) .target(GitHub.class, “https://api.github.com”); }}Request Interceptors如果你需要跨Feign客户端对所有请求都做修改, 那么你可以配置RequestInterceptor来实现. 例如, 如果你是请求的一个代理, 那么你可能会需要设置X-Forwarded-For请求头:static class ForwardedForInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { template.header(“X-Forwarded-For”, “origin.host.com”); }}public class Example { public static void main(String[] args) { Bank bank = Feign.builder() .decoder(accountDecoder) .requestInterceptor(new ForwardedForInterceptor()) .target(Bank.class, “https://api.examplebank.com”); }}另一个常见的使用拦截器的场景是授权, 比如使用内置的BasicAuthRequestInterceptor:public class Example { public static void main(String[] args) { Bank bank = Feign.builder() .decoder(accountDecoder) .requestInterceptor(new BasicAuthRequestInterceptor(username, password)) .target(Bank.class, “https://api.examplebank.com”); }}Custom @Param Expansion使用@Param注解的参数会用其toString()方法展开获得参数值, 也可以通过制定一个自定义的Param.Expander来控制. 如对日期的格式化:public interface Api { @RequestLine(“GET /?since={date}”) Result list(@Param(value = “date”, expander = DateToMillis.class) Date date);}Dynamic Query Parameters可以通过对Map类型的参数加上QueryMap注解, 将Map的内容构造成查询参数(query parameters):public interface Api { @RequestLine(“GET /find”) V find(@QueryMap Map<String, Object> queryMap);}同样的, 也可以通过使用QueryMapEncoder实现用POJO对象生成查询参数(query parameter):public interface Api { @RequestLine(“GET /find”) V find(@QueryMap CustomPojo customPojo);}当用这种方式时, 如果没有指定一个自定义的QueryMapEncoder, 那么查询参数的(query parameter)内容将根据对象的成员变量生成, 参数名对应变量名. 下面的例子中, 根据POJO对象生成的查询参数(query parameter)的内容是"/find?name={name}&number={number}", 生成的查询参数的顺序是不固定的, 按照惯例, 如果POJO对象的某个变量值为null, 那么该变量会被丢弃.public class CustomPojo { private final String name; private final int number; public CustomPojo (String name, int number) { this.name = name; this.number = number; }}设置自定义QueryMapEncoder的方式如下:public class Example { public static void main(String[] args) { MyApi myApi = Feign.builder() .queryMapEncoder(new MyCustomQueryMapEncoder()) .target(MyApi.class, “https://api.hostname.com”); }}当用@QueryMao注解时, 默认的编码器(encoder)会对对象的字段使用反射来将其展开成查询参数(query string). 如果希望通过对象的getter和setter方法来展开查询参数(query string), 请使用BeanQueryMapEncoder:public class Example { public static void main(String[] args) { MyApi myApi = Feign.builder() .queryMapEncoder(new BeanQueryMapEncoder()) .target(MyApi.class, “https://api.hostname.com”); }}Error Handling你可以通过在Feign实例构造时注册一个自定义的ErrorDecoder来实现对非正常响应的控制:public class Example { public static void main(String[] args) { MyApi myApi = Feign.builder() .errorDecoder(new MyErrorDecoder()) .target(MyApi.class, “https://api.hostname.com”); }}所有HTTP状态码不为2xx的响应都会触发ErrorDecoder的decode方法, 在这个方法内你可以对这些响应针对性地抛出异常, 或做其他额外的处理. 如果希望对请求进行重试, 那么可以抛出RetryableException, 该异常会触发Retryer.Retry默认情况下, Feign会对产生IOException的请求自动重试, 无论使用的是哪种HTTP方法, 都认为IOExcdeption是由短暂的网络问题产生的. 对ErrorDecoder内抛出的RetryableException也会进行请求重试. 你也可以通在Feign实例构造时设置自定义的Retryer来定制重试行为:public class Example { public static void main(String[] args) { MyApi myApi = Feign.builder() .retryer(new MyRetryer()) .target(MyApi.class, “https://api.hostname.com”); }}Retryer的实现需要决定一个请求是否应该进行重试, 可以通过continueOrPropagate(RetryableException e)方法的返回值(true或false)来实现. 每个Feign客户端执行时都会构造一个Retryer实例, 这样的话你可以维护每个请求的重新状态.如果最终重试也失败了, 那么会抛出RetryException, 如果希望抛出导致重试失败的异常, 可以在构造Feign客户端时指定exceptionPropagationPolicy()选项.Static and Default Methods使用Feign的接口可能是静态的或默认的方法(Java 8及以上支持), 这允许Feign客户端包含一些不适用底层接口定义的逻辑. 例如, 使用静态方法可以很轻易地指定通用客户端构造配置, 使用默认方法可以用于组合查询或定义默认参数:interface GitHub { @RequestLine(“GET /repos/{owner}/{repo}/contributors”) List<Contributor> contributors(@Param(“owner”) String owner, @Param(“repo”) String repo); @RequestLine(“GET /users/{username}/repos?sort={sort}”) List<Repo> repos(@Param(“username”) String owner, @Param(“sort”) String sort); default List<Repo> repos(String owner) { return repos(owner, “full_name”); } /* * Lists all contributors for all repos owned by a user. */ default List<Contributor> contributors(String user) { MergingContributorList contributors = new MergingContributorList(); for(Repo repo : this.repos(owner)) { contributors.addAll(this.contributors(user, repo.getName())); } return contributors.mergeResult(); } static GitHub connect() { return Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, “https://api.github.com”); }} ...

February 26, 2019 · 7 min · jiezi

关于springcloud Gateway中的限流

我们在开发系统的时候可能会对系统进行限流的需求, springcloudGateway有自带限流的方案,在此之前可以先去学习一下springcloud gateway中的filter。springcloud Gateway中提供了一个RequestRateLimiterGatewayFilterFactory。这种限流方式用到了redis, 先添加redis的依赖。配置类如下:public class RemoteAddrKeyResolver implements KeyResolver { public static final String BEAN_NAME = “remoteAddrKeyResolver”; @Override public Mono<String> resolve(ServerWebExchange exchange) { System.out.println(“hello”); Mono<String> just = Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); return just; }}@Configurationpublic class RemoteKeyResolver { @Bean(name=“remoteAddrKeyResolver”) public RemoteAddrKeyResolver remoteAddrKeyResolver() { return new RemoteAddrKeyResolver(); }}在此我们是根据ip地址限流的, Mono<String> just = Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress())application.yml配置文件配置如下:spring: application: name: gateway-service# redis:# database: 0# host: localhost# port: 6379# password:# timeout: 1000ms# lettuce:# pool:# max-active: 8# max-idle: 8# min-idle: 1# max-wait: 1000ms# cache:# type: REDIS cloud: gateway: discovery: locator: enabled: true routes: - id: order uri: lb://order-service predicates: - Path=/api/order-service/** filters: - StripPrefix=1 - name: RequestRateLimiter args: key-resolver: ‘#{@remoteAddrKeyResolver}’ redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 2上面贴出的是完整的springcloud Gateway的配置, 主要配置为filters下的-RequestRateLimiter,key-resolver表示使用名为remoteAddKeyResolver的限流配置配置类,此限流方式采用的是令牌桶算法的限流方式redis-rate-limiter.repleushRate :令牌桶每秒填充平均速率。redis-rate-limiter.burstCapacity: 令牌桶的总容量 ...

February 25, 2019 · 1 min · jiezi

从0开始构建SpringCloud微服务(1)

照例附上项目github链接本项目实现的是将一个简单的天气预报系统一步一步改造成一个SpringCloud微服务系统的过程,第一节将介绍普通天气预报系统的简单实现。数据来源:数据来源1:http://wthrcdn.etouch.cn/weather_mini?city=深圳数据来源2:http://wthrcdn.etouch.cn/weather_mini?citykey=101280601数据来源3:http://mobile.weather.com.cn/js/citylist.xml数据格式根据返回的数据格式在vo包下面创建pojo。Service创建WeatherDataService在其中提供如下接口:1)根据城市Id获取城市天气数据的接口。 @Override public WeatherResponse getDataByCityId(String cityId) { String url=WEATHER_URI+ “citykey=” + cityId; return this.doGetWeather(url); }2)根据城市名称获取天气数据的接口。 @Override public WeatherResponse getDataByCityName(String cityName) { String url = WEATHER_URI + “city=” + cityName; return this.doGetWeather(url); }其中doGetWeather方法为抽离出来的请求天气数据的方法。 private WeatherResponse doGetWeahter(String uri) { ResponseEntity<String> respString = restTemplate.getForEntity(uri, String.class); ObjectMapper mapper = new ObjectMapper(); WeatherResponse resp = null; String strBody = null; if (respString.getStatusCodeValue() == 200) { strBody = respString.getBody(); } try { resp = mapper.readValue(strBody, WeatherResponse.class); } catch (IOException e) { e.printStackTrace(); } return resp; }Controller在controller中分别提供根据城市id与名称获取天气数据的接口。@RestController@RequestMapping("/weather")public class WeatherController { @Autowired private WeatherDataService weatherDataService; @GetMapping("/cityId/{cityId}") public WeatherResponse getWeatherByCityId(@PathVariable(“cityId”) String cityId) { return weatherDataService.getDataByCityId(cityId); } @GetMapping("/cityName/{cityName}") public WeatherResponse getWeatherByCityName(@PathVariable(“cityName”) String cityName) { return weatherDataService.getDataByCityName(cityName); }}配置创建Rest的配置类。@Configurationpublic class RestConfiguration { @Autowired private RestTemplateBuilder builder; @Bean public RestTemplate restTemplate() { return builder.build(); } }请求结果: ...

February 23, 2019 · 1 min · jiezi

扩展Spring Cloud Feign 实现自动降级

自动降级目的在Spring Cloud 使用feign 的时候,需要明确指定fallback 策略,不然会提示错误。 先来看默认的feign service 是要求怎么做的。feign service 定义一个 factory 和 fallback 的类@FeignClient(value = ServiceNameConstants.UMPS_SERVICE, fallbackFactory = RemoteLogServiceFallbackFactory.class)public interface RemoteLogService {}但是我们大多数情况的feign 降级策略为了保证幂等都会很简单,输出错误日志即可。类似如下代码,在企业中开发非常不方便@Slf4j@Componentpublic class RemoteLogServiceFallbackImpl implements RemoteLogService { @Setter private Throwable cause; @Override public R<Boolean> saveLog(SysLog sysLog, String from) { log.error(“feign 插入日志失败”, cause); return null; }}自动降级效果@FeignClient(value = ServiceNameConstants.UMPS_SERVICE)public interface RemoteLogService {}Feign Service 完成同样的降级错误输出FeignClient 中无需定义无用的fallbackFactoryFallbackFactory 也无需注册到Spring 容器中代码变化,去掉FeignClient 指定的降级工厂代码变化,删除降级相关的代码核心源码注入我们个性化后的Feign@Configuration@ConditionalOnClass({HystrixCommand.class, HystrixFeign.class})protected static class HystrixFeignConfiguration { @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @ConditionalOnProperty(“feign.hystrix.enabled”) public Feign.Builder feignHystrixBuilder(FeignContext feignContext) { return PigxHystrixFeign.builder(feignContext) .decode404() .errorDecoder(new PigxFeignErrorDecoder()); }}PigxHystrixFeign.target 方法是根据@FeignClient 注解生成代理类的过程,注意注释@Overridepublic <T> T target(Target<T> target) { Class<T> targetType = target.type(); FeignClient feignClient = AnnotatedElementUtils.getMergedAnnotation(targetType, FeignClient.class); String factoryName = feignClient.name(); SetterFactory setterFactoryBean = this.getOptional(factoryName, feignContext, SetterFactory.class); if (setterFactoryBean != null) { this.setterFactory(setterFactoryBean); } // 以下为获取降级策略代码,构建降级,这里去掉了降级非空的非空的校验 Class<?> fallback = feignClient.fallback(); if (fallback != void.class) { return targetWithFallback(factoryName, feignContext, target, this, fallback); } Class<?> fallbackFactory = feignClient.fallbackFactory(); if (fallbackFactory != void.class) { return targetWithFallbackFactory(factoryName, feignContext, target, this, fallbackFactory); } return build().newInstance(target);}构建feign 客户端执行PigxHystrixInvocationHandler的增强Feign build(@Nullable final FallbackFactory<?> nullableFallbackFactory) { super.invocationHandlerFactory((target, dispatch) -> new PigxHystrixInvocationHandler(target, dispatch, setterFactory, nullableFallbackFactory)); super.contract(new HystrixDelegatingContract(contract)); return super.build(); }PigxHystrixInvocationHandler.getFallback() 获取降级策略 @Override @Nullable @SuppressWarnings(“unchecked”) protected Object getFallback() { // 如果 @FeignClient 没有配置降级策略,使用动态代理创建一个 if (fallbackFactory == null) { fallback = PigxFeignFallbackFactory.INSTANCE.create(target.type(), getExecutionException()); } else { // 如果 @FeignClient配置降级策略,使用配置的 fallback = fallbackFactory.create(getExecutionException()); } }PigxFeignFallbackFactory.create 动态代理逻辑 public T create(final Class<?> type, final Throwable cause) { return (T) FALLBACK_MAP.computeIfAbsent(type, key -> { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(key); enhancer.setCallback(new PigxFeignFallbackMethod(type, cause)); return enhancer.create(); }); }PigxFeignFallbackMethod.intercept, 默认的降级逻辑,输出降级方法信息和错误信息,并且把错误格式public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) { log.error(“Fallback class:[{}] method:[{}] message:[{}]”, type.getName(), method.getName(), cause.getMessage()); if (R.class == method.getReturnType()) { final R result = cause instanceof PigxFeignException ? ((PigxFeignException) cause).getResult() : R.builder() .code(CommonConstants.FAIL) .msg(cause.getMessage()).build(); return result; } return null;}关注我们Spring Cloud 微服务开发核心包mica基于Spring Cloud、OAuth2.0开发基于Vue前后分离的开发平台 ...

February 22, 2019 · 2 min · jiezi

Spring Security OAuth 个性化token

个性化Token 目的默认通过调用 /oauth/token 返回的报文格式包含以下参数{ “access_token”: “e6669cdf-b6cd-43fe-af5c-f91a65041382”, “token_type”: “bearer”, “refresh_token”: “da91294d-446c-4a89-bdcf-88aee15a75e8”, “expires_in”: 43199, “scope”: “server”}并没包含用户的业务信息比如用户信息、租户信息等。扩展生成包含业务信息(如下),避免系统多次调用,直接可以通过认证接口获取到用户信息等,大大提高系统性能{ “access_token”:“a6f3b6d6-93e6-4eb8-a97d-3ae72240a7b0”, “token_type”:“bearer”, “refresh_token”:“710ab162-a482-41cd-8bad-26456af38e4f”, “expires_in”:42396, “scope”:“server”, “tenant_id”:1, “license”:“made by pigx”, “dept_id”:1, “user_id”:1, “username”:“admin”}密码模式生成Token 源码解析 主页参考红框部分ResourceOwnerPasswordTokenGranter (密码模式)根据用户的请求信息,进行认证得到当前用户上下文信息protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters()); String username = parameters.get(“username”); String password = parameters.get(“password”); // Protect from downstream leaks of password parameters.remove(“password”); Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password); ((AbstractAuthenticationToken) userAuth).setDetails(parameters); userAuth = authenticationManager.authenticate(userAuth); OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest); return new OAuth2Authentication(storedOAuth2Request, userAuth);}然后调用AbstractTokenGranter.getAccessToken() 获取OAuth2AccessTokenprotected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));}默认使用DefaultTokenServices来获取tokenpublic OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { … 一系列判断 ,合法性、是否过期等判断 OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); tokenStore.storeAccessToken(accessToken, authentication); // In case it was modified refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken;}createAccessToken 核心逻辑// 默认刷新token 的有效期private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.// 默认token 的有效期private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(uuid); token.setExpiration(Date) token.setRefreshToken(refreshToken); token.setScope(authentication.getOAuth2Request().getScope()); return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;}如上代码,在拼装好token对象后会调用认证服务器配置TokenEnhancer( 增强器) 来对默认的token进行增强。TokenEnhancer.enhance 通过上下文中的用户信息来个性化Tokenpublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { final Map<String, Object> additionalInfo = new HashMap<>(8); PigxUser pigxUser = (PigxUser) authentication.getUserAuthentication().getPrincipal(); additionalInfo.put(“user_id”, pigxUser.getId()); additionalInfo.put(“username”, pigxUser.getUsername()); additionalInfo.put(“dept_id”, pigxUser.getDeptId()); additionalInfo.put(“tenant_id”, pigxUser.getTenantId()); additionalInfo.put(“license”, SecurityConstants.PIGX_LICENSE); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken;}基于pig 看下最终的实现效果Pig 基于Spring Cloud、oAuth2.0开发基于Vue前后分离的开发平台,支持账号、短信、SSO等多种登录,提供配套视频开发教程。 https://gitee.com/log4j/pig ...

February 18, 2019 · 2 min · jiezi

关于微服务架构的思考

最近在项目中遇到了一些问题,一个比较多的问题服务和服务直接调用混乱 a服务调用b b服务调用c c服务调用d 导致后期升级会出现很多问题 如果有个流程图也许会好些 但是没有 因此我陷入了思考, 如果进行重构的话那什么样的架构会是较好的价格 我想 设计模式的六大原则 在此也一样适用什么是好的架构明确的分工,服务之间优雅的调用我给出的一个结果这里简单画的一个草图先介绍一下查询:对应查询操作操作:对应增删改操作分为四层 ui: 页面及后台调用网关层: 路由聚合层:查询聚合 操作聚合服务层:订单服务 商品服务遵循的原则各个服务只专注于自己的功能 由聚合层来协调服务之间的关系维护与调用上层通过http调用下层 下层通过mq通知上层 同级不能调用服务要想调用服务 如 a服务想调用b服务 可以 a通过mq传递给聚合层 然后聚合层根据消息调用b ,服务之前的调用交给 聚合层维护后面还会不断完善这篇文章的

February 17, 2019 · 1 min · jiezi

微服务所需组件(大部分是Spring Cloud,持续更新)

服务发现将所有微服务注册到一个Server上,然后通过心跳进行服务健康监测。这样服务A调用服务B可以通过注册中心获取服务B的地址、端口调用Eureka - Eureka 提供云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移Spring Cloud Zookeeper - Spring Cloud Zookeeper 操作Zookeeper的工具包,用于使用zookeeper方式的服务发现和配置管理Consul - Consul 是一个服务发现与配置工具,与Docker容器可以无缝集成服务调用HTTP 客户端restTemplate - Spring Web RestTemplate 是同步客户端执行HTTP请求,在底层HTTP客户端库上公开简单的模板方法API,类使于JDK HttpURLConnection、Feign等Feign - Feign 使Java编写HTTP客户端更加简单负载均衡Ribbon - Ribbon 提供云端负载均衡,有多种负载均衡策略可供选择

February 15, 2019 · 1 min · jiezi

Spring Cloud Sleuth 之Greenwich版本全攻略

微服务架构是一个分布式架构,微服务系统按业务划分服务单元,一个微服务系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性较高,如果出现了错误和异常,很难去定位。主要体现在一个请求可能需要调用很多个服务,而内部服务的调用复杂性决定了问题难以定位。所以在微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题能够快速定位的目的。在微服务系统中,一个来自用户的请求先到达前端A(如前端界面),然后通过远程调用,到达系统的中间件B、C(如负载均衡、网关等),最后到达后端服务D、E,后端经过一系列的业务逻辑计算,最后将数据返回给用户。对于这样一个请求,经历了这么多个服务,怎么样将它的请求过程用数据记录下来呢?这就需要用到服务链路追踪。Spring Cloud SleuthSpring Cloud Sleuth 为服务之间调用提供链路追踪。通过 Sleuth 可以很清楚的了解到一个服务请求经过了哪些服务,每个服务处理花费了多长。从而让我们可以很方便的理清各微服务间的调用关系。此外 Sleuth 可以帮助我们:耗时分析: 通过 Sleuth 可以很方便的了解到每个采样请求的耗时,从而分析出哪些服务调用比较耗时;可视化错误: 对于程序未捕捉的异常,可以通过集成 Zipkin 服务界面上看到;链路优化: 对于调用比较频繁的服务,可以针对这些服务实施一些优化措施。Google开源了Dapper链路追踪组件,并在2010年发表了论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,这篇论文是业内实现链路追踪的标杆和理论基础,具有很高的参考价值。Spring Cloud Sleuth采用了Google的开源项目Dapper的专业术语。Span:基本工作单元,发送一个远程调度任务就会产生一个Span,Span是用一个64位ID唯一标识的,Trace是用另一个64位ID唯一标识的。Span还包含了其他的信息,例如摘要、时间戳事件、Span的ID以及进程ID。Trace:由一系列Span组成的,呈树状结构。请求一个微服务系统的API接口,这个API接口需要调用多个微服务单元,调用每个微服务单元都会产生一个新的Span,所有由这个请求产生的Span组成了这个Trace。Annotation:用于记录一个事件,一些核心注解用于定义一个请求的开始和结束,这些注解如下。cs-Client Sent:客户端发送一个请求,这个注解描述了Span的开始。sr-Server Received:服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳,便可得到网络传输的时间。ss-Server Sent:服务端发送响应,该注解表明请求处理的完成(当请求返回客户端),用ss的时间戳减去sr时间戳,便可以得到服务器请求的时间。cr-Client Received:客户端接收响应,此时Span结束,如果cr的时间戳减去cs时间戳,便可以得到整个请求所消耗的时间。Spring Cloud Sleuth 也为我们提供了一套完整的链路解决方案,Spring Cloud Sleuth 可以结合 Zipkin,将信息发送到 Zipkin,利用 Zipkin 的存储来存储链路信息,利用 Zipkin UI 来展示数据。ZipkinZipkin是一种分布式链路追踪系统。 它有助于收集解决微服务架构中的延迟问题所需的时序数据。 它管理这些数据的收集和查找。 Zipkin的设计基于Google Dapper论文。跟踪器存在于应用程序中,记录请求调用的时间和元数据。跟踪器使用库,它们的使用对用户是无感知的。例如,Web服务器会在收到请求时和发送响应时会记录相应的时间和一些元数据。一次完整链路请求所收集的数据被称为Span。我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的 API 接口之外,它也提供了方便的 UI 组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。Zipkin 提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。接下来的测试为方便直接采用 In-Memory 方式进行存储,生产推荐 Elasticsearch.上图展示了 Zipkin 的基础架构,它主要由 4 个核心组件构成:Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为 Zipkin 内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。Web UI:UI 组件,基于 API 组件实现的上层应用。通过 UI 组件用户可以方便而有直观地查询和分析跟踪信息。案例实战在本案例一共有三个应用,分别为注册中心,eureka-server、eureka-client、eureka-client-feign,三个应用的基本信息如下:应用名端口作用eureka-server8761注册中心eureka-client8763服务提供者eureka-client-feign8765服务消费者其中eureka-server 应用为注册中心,其他两个应用向它注册。eureka-client为服务提供者,提供了一个RESTAPI,eureka-client-feign为服务消费者,通过Feign Client向服务提供者消费服务。在之前的文章已经讲述了如何如何搭建服务注册中心,在这里就省略这一部分内容。服务提供者提供一个REST接口,服务消费者通过FeignClient消费服务。服务提供者eureka-client服务提供者,对外提供一个RESTAPI,并向服务注册中心注册,这部分内容,不再讲述,见源码。需要在工程的pom文件加上sleuth的起步依赖和zipkin的起步依赖,代码如下:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId></dependency><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId></dependency>在工程的配置文件application.yml需要做以下的配置:spring: sleuth: web: client: enabled: true sampler: probability: 1.0 # 将采样比例设置为 1.0,也就是全部都需要。默认是 0.1 zipkin: base-url: http://localhost:9411/ # 指定了 Zipkin 服务器的地址其中spring.sleuth.web.client.enable为true设置的是web开启sleuth功能;spring.sleuth.sampler.probability可以设置为小数,最大值为1.0,当设置为1.0时就是链路数据100%收集到zipkin-server,当设置为0.1时,即10%概率收集链路数据;spring.zipkin.base-url设置zipkin-server的地址。对外提供一个Api,代码如下:@RestControllerpublic class HiController { @Value("${server.port}") String port; @GetMapping("/hi") public String home(@RequestParam String name) { return “hi “+name+",i am from port:” +port; }}服务消费者服务消费者通过FeignClient消费服务提供者提供的服务。同服务提供者一样,需要在工程的pom文件加上sleuth的起步依赖和zipkin的起步依赖,另外也需要在配置文件application.yml做相关的配置,具体同服务提供者。服务消费者通过feignClient进行服务消费,feignclient代码如下:@FeignClient(value = “eureka-client”,configuration = FeignConfig.class)public interface EurekaClientFeign { @GetMapping(value = “/hi”) String sayHiFromClientEureka(@RequestParam(value = “name”) String name);}servcie层代码如下:@Servicepublic class HiService { @Autowired EurekaClientFeign eurekaClientFeign; public String sayHi(String name){ return eurekaClientFeign.sayHiFromClientEureka(name); }}controller代码如下:@RestControllerpublic class HiController { @Autowired HiService hiService; @GetMapping("/hi”) public String sayHi(@RequestParam( defaultValue = “forezp”,required = false)String name){ return hiService.sayHi(name); }上面的代码对外暴露一个API,通过FeignClient的方式调用eureka-client的服务。zipkin-server在Spring Cloud D版本,zipkin-server通过引入依赖的方式构建工程,自从E版本之后,这一方式改变了,采用官方的jar形式启动,所以需要通过下载官方的jar来启动,也通过以下命令一键启动:curl -sSL https://zipkin.io/quickstart.sh | bash -sjava -jar zipkin.jar上面的第一行命令会从zipkin官网下载官方的jar包。如果是window系统,建议使用gitbash执行上面的命令。如果用 Docker 的话,使用以下命令:docker run -d -p 9411:9411 openzipkin/zipkin通过java -jar zipkin.jar的方式启动之后,在浏览器上访问lcoalhost:9411,显示的界面如下:链路数据验证依次启动eureka-server,eureka-client,eureka-client-feign的三个应用,等所有应用启动完成后,在浏览器上访问http://localhost:8765/hi(如果报错,是服务与发现需要一定的时间,耐心等待几十秒),访问成功后,再次在浏览器上访问zipkin-server的页面,显示如下:从上图可以看出每次请求所消耗的时间,以及一些span的信息。从上图可以看出具体的服务依赖关系,eureka-feign-client依赖了eureka-client。使用rabbitmq进行链路数据收集在上面的案例中使用的http请求的方式将链路数据发送给zipkin-server,其实还可以使用rabbitmq的方式进行服务的消费。使用rabbitmq需要安装rabbitmq程序,下载地址http://www.rabbitmq.com/。下载完成后,需要eureka-client和eureka-client-feign的起步依赖加上rabbitmq的依赖,依赖如下:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId></dependency>在配置文件上需要配置rabbitmq的配置,配置信息如下:spring: rabbitmq: host: localhost username: guest password: guest port: 5672另外需要把spring.zipkin.base-url去掉。在上面2个工程中,rabbitmq通过发送链路数据,那么zipkin-server是怎么样知道rabbitmq的地址呢,怎么监听收到的链路数据呢?这需要在程序启动的时候,通过环境变量的形式到环境中,然后zikin-server从环境变量中读取。可配置的属性如下:属性环境变量描述zipkin.collector.rabbitmq.addressesRABBIT_ADDRESSES用逗号分隔的 RabbitMQ 地址列表,例如localhost:5672,localhost:5673zipkin.collector.rabbitmq.passwordRABBIT_PASSWORD连接到 RabbitMQ 时使用的密码,默认为 guestzipkin.collector.rabbitmq.usernameRABBIT_USER连接到 RabbitMQ 时使用的用户名,默认为guestzipkin.collector.rabbitmq.virtual-hostRABBIT_VIRTUAL_HOST使用的 RabbitMQ virtual host,默认为 /zipkin.collector.rabbitmq.use-sslRABBIT_USE_SSL设置为true则用 SSL 的方式与 RabbitMQ 建立链接zipkin.collector.rabbitmq.concurrencyRABBIT_CONCURRENCY并发消费者数量,默认为1zipkin.collector.rabbitmq.connection-timeoutRABBIT_CONNECTION_TIMEOUT建立连接时的超时时间,默认为 60000毫秒,即 1 分钟zipkin.collector.rabbitmq.queueRABBIT_QUEUE从中获取 span 信息的队列,默认为 zipkin比如,通过以下命令启动:RABBIT_ADDRESSES=localhost java -jar zipkin.jar上面的命令等同于一下的命令:java -jar zipkin.jar –zipkin.collector.rabbitmq.addressed=localhost用上面的2条命令中的任何一种方式重新启动zipkin-server程序,并重新启动eureka-client、eureka-server、eureka-client-feign,动完成后在浏览器上访问http://localhost:8765/hi,再访问http://localhost:9411/zipkin/,就可以看到通过Http方式发送链路数据一样的接口。自定义Tag在页面上可以查看每个请求的traceId,每个trace又包含若干的span,每个span又包含了很多的tag,自定义tag可以通过Tracer这个类来自定义。@AutowiredTracer tracer; @GetMapping("/hi") public String home(@RequestParam String name) { tracer.currentSpan().tag(“name”,“forezp”); return “hi “+name+",i am from port:” +port; }将链路数据存储在Mysql数据库中上面的例子是将链路数据存在内存中,只要zipkin-server重启之后,之前的链路数据全部查找不到了,zipkin是支持将链路数据存储在mysql、cassandra、elasticsearch中的。现在讲解如何将链路数据存储在Mysql数据库中。首先需要初始化zikin存储在Mysql的数据的scheme,可以在这里查看https://github.com/openzipkin…,具体如下:CREATE TABLE IF NOT EXISTS zipkin_spans ( trace_id_high BIGINT NOT NULL DEFAULT 0 COMMENT ‘If non zero, this means the trace uses 128 bit traceIds instead of 64 bit’, trace_id BIGINT NOT NULL, id BIGINT NOT NULL, name VARCHAR(255) NOT NULL, parent_id BIGINT, debug BIT(1), start_ts BIGINT COMMENT ‘Span.timestamp(): epoch micros used for endTs query and to implement TTL’, duration BIGINT COMMENT ‘Span.duration(): micros used for minDuration and maxDuration query’) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_spans ADD UNIQUE KEY(trace_id_high, trace_id, id) COMMENT ‘ignore insert on duplicate’;ALTER TABLE zipkin_spans ADD INDEX(trace_id_high, trace_id, id) COMMENT ‘for joining with zipkin_annotations’;ALTER TABLE zipkin_spans ADD INDEX(trace_id_high, trace_id) COMMENT ‘for getTracesByIds’;ALTER TABLE zipkin_spans ADD INDEX(name) COMMENT ‘for getTraces and getSpanNames’;ALTER TABLE zipkin_spans ADD INDEX(start_ts) COMMENT ‘for getTraces ordering and range’;CREATE TABLE IF NOT EXISTS zipkin_annotations ( trace_id_high BIGINT NOT NULL DEFAULT 0 COMMENT ‘If non zero, this means the trace uses 128 bit traceIds instead of 64 bit’, trace_id BIGINT NOT NULL COMMENT ‘coincides with zipkin_spans.trace_id’, span_id BIGINT NOT NULL COMMENT ‘coincides with zipkin_spans.id’, a_key VARCHAR(255) NOT NULL COMMENT ‘BinaryAnnotation.key or Annotation.value if type == -1’, a_value BLOB COMMENT ‘BinaryAnnotation.value(), which must be smaller than 64KB’, a_type INT NOT NULL COMMENT ‘BinaryAnnotation.type() or -1 if Annotation’, a_timestamp BIGINT COMMENT ‘Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp’, endpoint_ipv4 INT COMMENT ‘Null when Binary/Annotation.endpoint is null’, endpoint_ipv6 BINARY(16) COMMENT ‘Null when Binary/Annotation.endpoint is null, or no IPv6 address’, endpoint_port SMALLINT COMMENT ‘Null when Binary/Annotation.endpoint is null’, endpoint_service_name VARCHAR(255) COMMENT ‘Null when Binary/Annotation.endpoint is null’) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_annotations ADD UNIQUE KEY(trace_id_high, trace_id, span_id, a_key, a_timestamp) COMMENT ‘Ignore insert on duplicate’;ALTER TABLE zipkin_annotations ADD INDEX(trace_id_high, trace_id, span_id) COMMENT ‘for joining with zipkin_spans’;ALTER TABLE zipkin_annotations ADD INDEX(trace_id_high, trace_id) COMMENT ‘for getTraces/ByIds’;ALTER TABLE zipkin_annotations ADD INDEX(endpoint_service_name) COMMENT ‘for getTraces and getServiceNames’;ALTER TABLE zipkin_annotations ADD INDEX(a_type) COMMENT ‘for getTraces and autocomplete values’;ALTER TABLE zipkin_annotations ADD INDEX(a_key) COMMENT ‘for getTraces and autocomplete values’;ALTER TABLE zipkin_annotations ADD INDEX(trace_id, span_id, a_key) COMMENT ‘for dependencies job’;CREATE TABLE IF NOT EXISTS zipkin_dependencies ( day DATE NOT NULL, parent VARCHAR(255) NOT NULL, child VARCHAR(255) NOT NULL, call_count BIGINT, error_count BIGINT) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(day, parent, child);在数据库中初始化上面的脚本之后,需要做的就是zipkin-server如何连接数据库。zipkin如何连数据库同连接rabbitmq一样。zipkin连接数据库的属性所对应的环境变量如下:属性环境变量描述zipkin.torage.typeSTORAGE_TYPE默认的为mem,即为内存,其他可支持的为cassandra、cassandra3、elasticsearch、mysqlzipkin.torage.mysql.hostMYSQL_HOST数据库的host,默认localhost zipkin.torage.mysql.portMYSQL_TCP_PORT数据库的端口,默认3306 zipkin.torage.mysql.usernameMYSQL_USER连接数据库的用户名,默认为空 zipkin.torage.mysql.passwordMYSQL_PASS连接数据库的密码,默认为空 zipkin.torage.mysql.dbMYSQL_DBzipkin使用的数据库名,默认是zipkin zipkin.torage.mysql.max-activeMYSQL_MAX_CONNECTIONS最大连接数,默认是10 STORAGE_TYPE=mysql MYSQL_HOST=localhost MYSQL_TCP_PORT=3306 MYSQL_USER=root MYSQL_PASS=123456 MYSQL_DB=zipkin java -jar zipkin.jar等同于以下的命令java -jar zipkin.jar –zipkin.torage.type=mysql –zipkin.torage.mysql.host=localhost –zipkin.torage.mysql.port=3306 –zipkin.torage.mysql.username=root –zipkin.torage.mysql.password=123456使用上面的命令启动zipkin.jar工程,然后再浏览数上访问http://localhost:8765/hi,再访问http://localhost:9411/zipkin/,可以看到链路数据。这时去数据库查看数据,也是可以看到存储在数据库的链路数据,如下:这时重启应用zipkin.jar,再次在浏览器上访问http://localhost:9411/zipkin/,仍然可以得到之前的结果,证明链路数据存储在数据库中,而不是内存中。将链路数据存在在Elasticsearch中zipkin-server支持将链路数据存储在ElasticSearch中。读者需要自行安装ElasticSearch和Kibana,下载地址为https://www. elastic.co/products/elasticsearch。安装完成后启动,其中ElasticSearch的默认端口号为9200,Kibana的默认端口号为5601。同理,zipkin连接elasticsearch也是从环境变量中读取的,elasticsearch相关的环境变量和对应的属性如下:属性环境变量描述zipkin.torage.elasticsearch.hostsES_HOSTSES_HOSTS,默认为空 zipkin.torage.elasticsearch.pipelineES_PIPELINEES_PIPELINE,默认为空 zipkin.torage.elasticsearch.max-requestsES_MAX_REQUESTSES_MAX_REQUESTS,默认为64 zipkin.torage.elasticsearch.timeoutES_TIMEOUTES_TIMEOUT,默认为10s zipkin.torage.elasticsearch.indexES_INDEXES_INDEX,默认是zipkin zipkin.torage.elasticsearch.date-separatorES_DATE_SEPARATORES_DATE_SEPARATOR,默认为“-” zipkin.torage.elasticsearch.index-shardsES_INDEX_SHARDSES_INDEX_SHARDS,默认是5 zipkin.torage.elasticsearch.index-replicasES_INDEX_REPLICASES_INDEX_REPLICAS,默认是1 zipkin.torage.elasticsearch.usernameES_USERNAMEES的用户名,默认为空 zipkin.torage.elasticsearch.passwordES_PASSWORDES的密码,默认是为空 采用以下命令启动zipkin-server:STORAGE_TYPE=elasticsearch ES_HOSTS=http://localhost:9200 ES_INDEX=zipkin java -jar zipkin.jarjava -jar zipkin.jar –STORAGE_TYPE=elasticsearch –ES_HOSTS=http://localhost:9200 –ES_INDEX=zipkin java -jar zipkin.jar –STORAGE_TYPE=elasticsearch –ES_HOSTS=http://localhost:9200 –ES_INDEX=zipkin java -jar zipkin.jar –zipkin.torage.type=elasticsearch –zipkin.torage.elasticsearch.hosts=http://localhost:9200 –zipkin.torage.elasticsearch.index=zipkin 启动完成后,然后在浏览数上访问http://localhost:8765/hi,再访问http://localhost:9411/zipkin/,可以看到链路数据。这时链路数据存储在ElasticSearch。在zipkin上展示链路数据链路数据存储在ElasticSearch中,ElasticSearch可以和Kibana结合,将链路数据展示在Kibana上。安装完成Kibana后启动,Kibana默认会向本地端口为9200的ElasticSearch读取数据。Kibana默认的端口为5601,访问Kibana的主页http://localhost:5601,其界面如下图所示。在上图的界面中,单击“Management”按钮,然后单击“Add New”,添加一个index。我们将在上节ElasticSearch中写入链路数据的index配置为“zipkin”,那么在界面填写为“zipkin-*”,单击“Create”按钮,界面如下图所示:创建完成index后,单击“Discover”,就可以在界面上展示链路数据了,展示界面如下图所示。参考资料https://zipkin.io/https://github.com/spring-clo…https://cloud.spring.io/sprin...https://github.com/openzipkin...https://github.com/openzipkin...https://windmt.com/2018/04/24...https://segmentfault.com/a/11...elatstic 版本为2.6.x,下载地址:https://www.elastic.co/downlo…http://www.cnblogs.com/JreeyQ… ...

February 12, 2019 · 4 min · jiezi

Spring Cloud OAuth2 资源服务器CheckToken 源码解析

CheckToken的目的 当用户携带token 请求资源服务器的资源时, OAuth2AuthenticationProcessingFilter 拦截token,进行token 和userdetails 过程,把无状态的token 转化成用户信息。 ## 详解OAuth2AuthenticationManager.authenticate(),filter执行判断的入口当用户携带token 去请求微服务模块,被资源服务器拦截调用RemoteTokenServices.loadAuthentication ,执行所谓的check-token过程。源码如下 CheckToken 处理逻辑很简单,就是调用redisTokenStore 查询token的合法性,及其返回用户的部分信息 (username )继续看 返回给 RemoteTokenServices.loadAuthentication 最后一句tokenConverter.extractAuthentication 解析组装服务端返回的信息最重要的 userTokenConverter.extractAuthentication(map);最重要的一步,是否判断是否有userDetailsService实现,如果有 的话去查根据 返回的username 查询一次全部的用户信息,没有实现直接返回username,这也是很多时候问的为什么只能查询到username 也就是 EnablePigxResourceServer.details true 和false 的区别。那根据的你问题,继续看 UerDetailsServiceImpl.loadUserByUsername 根据用户名去换取用户全部信息。关于pig基于Spring Cloud、oAuth2.0开发基于Vue前后分离的开发平台,支持账号、短信、SSO等多种登录,提供配套视频开发教程。 https://gitee.com/log4j/pig

January 25, 2019 · 1 min · jiezi

Spring Cloud Greenwich 新特性和F升级分享

2019.01.23 期待已久的Spring Cloud Greenwich 发布了release版本,作为我们团队也第一时间把RC版本替换为release,以下为总结,希望对你使用Spring Cloud Greenwich 有所帮助Greenwich 只支持 Spring Boot 2.1.x 分支。如果使用 2.0.x 请使用Finchley版本,pom坐标主要是适配JAVA11<!–支持Spring Boot 2.1.X–><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.2.RELEASE</version> <type>pom</type> <scope>import</scope></dependency><!–Greenwich.RELEASE–><dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope></dependency>升级netflix版本,DiscoveryClient支持获取InstanceIdSpring Cloud Config 提供了新的存储介质除了Git、File、JDBC,新版本提供 在Cloud Foundry的CredHub存储功能spring: profiles: active: credhub cloud: config: server: credhub: url: https://credhub:8844Spring Cloud Gateway支持整合OAuth2这里提供了一个例子: Spring Cloud Gateway and Spring Security OAuth2整合的时候有个坑可以参考这个issue:ReactiveManagementWebSecurityAutoConfiguration Prevent’s oauth2Login from being defaulted新增重写响应头过滤器spring: cloud: gateway: routes: - id: rewriteresponseheader_route uri: http://example.org filters: - RewriteResponseHeader=X-Response-Foo, , password=[^&]+, password=Feign 的新特性和坑@SpringQueryMap 对Get请求进行了增强终于解决这个问题了不用直接使用OpenFeign新增的@QueryMap,由于缺少value属性 QueryMap注释与Spring不兼容…异常解决对Spring Cloud Finchley 进行直接升级时候发现feign启动报错了APPLICATION FAILED TO START***Description:The bean ‘pigx-upms-biz.FeignClientSpecification’, defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.Action:Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=trueProcess finished with exit code 1第一种粗暴的解决方法,异常日志中说明了,在 bootstrap.yml中配置spring.main.allow-bean-definition-overriding=true这是Spring Boot 2.1 后新增的属性运行bean 覆盖,不要配置到配置中心里面,不然无效第二种,就是把通过同一个服务调用的代码,移动到同一个@FeignClient中contextId ,这个是@FeignClient 新增的一个属性This will be used as the bean name instead of name if present, but will not be used as a service id.就可以用这个属性区分@FeigenClient 标志的同一个service 的接口总结Spring Cloud F – > G 变化很小,微乎其微主要是JAVA11的兼容很遗憾没有看到 Spring Cloud Alibaba 加油。Spring Cloud LoadBalancer 还是老样子。目前来看暂时无法替代 ribbon欢迎加我Q2270033969,讨论Spring Cloud ^_^ ...

January 24, 2019 · 1 min · jiezi

SpringCloud灰度发布实践(整合Apollo配置中心)

代码git地址feature[x] 灰度服务[x] 配置中心[x] 动态路由前言上篇文章介绍了SpringCloud灰度的实现及流程,每次修改服务的元数据信息metadata-map值需要重新调用一次eureka的RestFul接口,不仅如此当服务重启后又会重新读最初的配置值,这样不仅麻烦而且还不可靠。在经过与SpringCloud Config 、Disconf、Apollo等配置中心作出对比后,发现被Apollo友好方便的管理端所深深吸引,再加上该配置中心支持配置文件的灰度发布简直不要太完美。Apollo灰度配置让多个实例共享一个配置文件,示例配置spring.application.name = provide-testserver.port = 7770eureka.client.service-url.defaultZone = http://localhost:1111/eureka/然后新起一个灰度配置,让对应的服务使用该配置。eureka.instance.metadata-map.version = v1事件监听监听Apollo事件,当发现配置文件中的eureka.instance.metadata-map.version值若发生改变,则调用eureka接口更改metadata-map元数据 @ApolloConfigChangeListener(“application”) private void someOnChange(ConfigChangeEvent changeEvent) { changeEvent.changedKeys().forEach(key -> { ConfigChange change = changeEvent.getChange(key); // 灰度配置 if(“eureka.instance.metadata-map.version”.equals(change.getPropertyName())) { String appname = eurekaInstanceConfig.getAppname(); String instanceId = eurekaInstanceConfig.getInstanceId(); String value = StringUtils.isEmpty(change.getNewValue()) ? "" : change.getNewValue(); //TODO 调用eureka更改元数据接口 } }); }这样一来,只需要通过修改配置文件然后就会触发监听事件从而自动触发请求eureka更改元数据的值。动态路由在网关zuul整合了动态路由功能,监听Apollo配置文件使其修改配置文件后可以马上生效。此处不对此功能做过多的介绍,详情见代码配置示例url.map.provide-test = /pt/**url.map.为固定写法,provide-test为服务名称,/pt/**为映射路径参考文章灰度使用在启动类添加注解@SpringBootApplication@EnableDiscoveryClient@EnableGrayConfigpublic class ProviderApplication { public static void main(String[] args) { SpringApplication.run(ProviderApplication.class, args); }}

January 22, 2019 · 1 min · jiezi

SpringCloud 断路器(Hystrix)

介绍雪崩效应 在微服务架构中服务与服务之间可以相互调用,由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会占用越来越多的系统资源,导致服务瘫痪。由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成影响,这就是服务故障的“雪崩”效应。断路器 “断路器”是一种开关装置,当某个服务发生故障监控(类似熔断保险丝),向调用方法返回一个备选的响应,而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。熔断模式 在对某个服务调用不可用达到一个阈值,5秒内20次调用失败就会就会启动熔断模式。熔断器的开关是由服务的健康状况(服务的健康状况 = 请求失败数 / 请求总数)决定的。当断路器开关关闭时请求可以通过,一但服务器健康状况低于设定阈值断路器就会打开,请求会被禁止通过。当断路器处于打开状态时,一段时间后会进入半开状态,这时只允许一个请求通过,如果该请求成功熔断器恢复到关闭状态,如果调用失败断路器继续保持打开。服务降级 服务降级,一般是从整体符合考虑,就是当某个服务熔断之后,服务器将不再被调用,此刻客户端可以自己准备一个本地的fallback回调,返回一个缺省值。Ribbon中使用Hystrix添加maven依赖 在SpringCloud 服务消费者(RestTemplate+Ribbon)基础上对service-ribbon项目进行修改。 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>修改HelloServiceRibbonSer类@Servicepublic class HelloServiceRibbonSer { @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = “helloError”) public String helloServiceRibbon(String helloName) { return restTemplate.getForObject(“http://say-hello/sayHello?helloName="+helloName,String.class); } public String helloError(String helloName) { return “hello,"+helloName+",error!”; }} @Autowired private HelloServiceRibbonSer helloServiceRibbonSer; @GetMapping("/ribbonHello”) public String ribbonHelloCtr(@RequestParam(“helloName”)String helloName){ return helloServiceRibbonSer.helloServiceRibbon(helloName); }在helloServiceRibbon方法的头上加了@HystrixCommand(fallbackMethod = “helloError”)注解,代表对这个方法使用了Hystrix相关的功能,fallbackMethod属性是调用回调之后的处理方法。修改启动类@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient //向服务中心注册@EnableHystrix //启动Hystrixpublic class ServiceRibbonApplication { public static void main(String[] args) { SpringApplication.run(ServiceRibbonApplication.class, args); }}启动项目 启动注册中心和service-ribbon,不启动say-hello项目(服务提供者),在浏览器地址栏访问http://localhost:3333/ribbonHello?helloName=aaa发现调用了fallbackMethod中的方法。Feign中使用Hystrix修改项目 修改SpringCloud 服务消费者(Feign)项目。Feign是自带断路器的,只需要在FeignClient的接口的注解中加上fallback的指定类就行了。@FeignClient(value = “say-hello”,fallback = SayHelloFeignSerHiHystric.class)public interface SayHelloFeignSer { @RequestMapping(value = “/sayHello”,method = RequestMethod.GET) String feignSayHelloSer(@RequestParam(value = “helloName”) String helloName);}@Componentpublic class SayHelloFeignSerHiHystric implements SayHelloFeignSer{ @Override public String feignSayHelloSer(String helloName) { return “发生错误 !"+helloName; }}修改配置文件#打开断路器,D版本之后默认关闭feign.hystrix.enabled=true启动项目 启动注册中心,service-feign,不启动say-hello项目。在浏览器地址栏访问:http://localhost:4444/feignSayHello?helloName=adaad,发现调用了fallback中的方法。 fallbackFactory使用 如果要熔断的功能或者业务比较复杂可以使用一个工厂来封装,然后直接使用fallbackFactory来标注。 修改SayHelloFeignSer@FeignClient(value = “say-hello”,fallbackFactory = HystrixFallBackFactory.class/fallback = SayHelloFeignSerHiHystric.class/)public interface SayHelloFeignSer { @RequestMapping(value = “/sayHello”,method = RequestMethod.GET) String feignSayHelloSer(@RequestParam(value = “helloName”) String helloName);}public interface UserFeignWithFallBackFactoryClient extends SayHelloFeignSer {}@Componentpublic class HystrixFallBackFactory implements FallbackFactory<SayHelloFeignSer> { @Override public SayHelloFeignSer create(Throwable throwable) { return new SayHelloFeignSer(){ @Override public String feignSayHelloSer(String helloName) { return “error”; } @Override public String manyParamsSer(String userName, String userPassword) { return “error”; } @Override public Object objParamsCtr(Object user) { return “error”; } }; }} ...

January 16, 2019 · 1 min · jiezi

SpringCloud 服务消费者(Feign)

Feign简介Feign 是一个声明web服务客户端,这便得编写web服务客户端更容易,使用Feign 创建一个接口并对它进行注解,它具有可插拔的注解支持包括Feign注解与JAX-RS注解,Feign还支持可插拔的编码器与解码器,Spring Cloud 增加了对 Spring MVC的注解。在Spring Cloud中使用Feign, 我们可以做到使用HTTP请求远程服务时能与调用本地方法一样的编码体验。Feign 采用的是基于接口的注解,Feign 整合了ribbon,具有负载均衡的能力。准备工作启动注册中心eureka-server,服务提供者say-hello。对这两个项目各自启动两个实例。创建Feign客户端1.新建一个springboot工程,取名为service-feign<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>service-feign</artifactId> <version>0.0.1-SNAPSHOT</version> <name>service-feign</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2.修改配置文件server.port=4444eureka.client.service-url.defaultZone=http://server2:11112/eureka/,http://server1:11111/eureka/spring.application.name=service-feign3.修改启动类@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient@EnableFeignClientspublic class ServiceFeignApplication { public static void main(String[] args) { SpringApplication.run(ServiceFeignApplication.class, args); }}4.定义Feign接口通过@ FeignClient(“服务名”),来指定调用哪个服务。@FeignClient(value = “say-hello”)public interface SayHelloFeignSer { @RequestMapping(value = “/sayHello”,method = RequestMethod.GET) String feignSayHelloSer(@RequestParam(value = “helloName”) String helloName);}5.创建controller,对外提供接口@RestControllerpublic class FeignServiceController { //编译器报错,无视。 因为这个Bean是在程序启动的时候注入的,编译器感知不到,所以报错。 @Autowired private SayHelloFeignSer sayHelloFeignSer; @GetMapping("/feignSayHello”) public String feignSayHelloCtr(@RequestParam(“helloName”)String helloName){ return sayHelloFeignSer.feignSayHelloSer(helloName); }}6.启动service-feign访问http://localhost:4444/feignSayHello?helloName=adaad,发现浏览器交替显示端口,说明feign已经集成ribbon。Feign多参数请求1.修改say-hello项目,在SayHelloController中添加两个方法/** * get请求多请求参数 * @param userName * @param userPassword * @return / @RequestMapping(value = “/manyParams”,method = RequestMethod.GET) public String manyParamsCtr(@RequestParam(“userName”)String userName,@RequestParam(“userPassword”)String userPassword){ return “用户名:"+userName+",用户密码”+userPassword; } /* * post 对象参数 * @param user * @return / @RequestMapping(value = “/objParams”,method = RequestMethod.POST) public User objParamsCtr(@RequestBody User user){ System.out.println(JSON.toJSON(user)); return user; }public class User { private String userName; private String userPassword; private String userSex; …get(),set()}2.修改service-feign项目Feign接口/* * 多参数get请求 * @param userName * @param userPassword * @return / @RequestMapping(value = “/manyParams”,method = RequestMethod.GET) String manyParamsSer(@RequestParam(“userName”)String userName,@RequestParam(“userPassword”)String userPassword); /* * 对象参数 post请求 * @param user * @return */ @RequestMapping(value = “/objParams”,method = RequestMethod.POST) Object objParamsCtr(@RequestBody Object user);3.修改service-feign项目FeignServiceController @GetMapping("/feignManyParams”) public String feignManyParamsCtr(@RequestParam(“userName”)String userName,@RequestParam(“userPassword”)String userPassword){ return sayHelloFeignSer.manyParamsSer(userName,userPassword); } @PostMapping("/feignObjParam”) public Object feignObjParamCtr(@RequestBody Map<String,Object> map){ return sayHelloFeignSer.objParamsCtr(map); }注:为了不重复创建User实体类,这里用map去接收参数。4.重新启动say-hello,service-feign两个项目通过postman访问 /feignManyParams接口访问 /feignObjParam接口 ...

January 15, 2019 · 2 min · jiezi

SpringCloud Finchley Gateway 缓存请求Body和Form表单

在接入Spring-Cloud-Gateway时,可能有需求进行缓存Json-Body数据或者Form-Urlencoded数据的情况。由于Spring-Cloud-Gateway是以WebFlux为基础的响应式架构设计,所以在原有Zuul基础上迁移过来的过程中,传统的编程思路,并不适合于Reactor Stream的开发。网络上有许多缓存案例,但是在测试过程中出现各种Bug问题,在缓存Body时,需要考虑整体的响应式操作,才能更合理的缓存数据下面提供缓存Json-Body数据或者Form-Urlencoded数据的具体实现方案,该方案经测试,满足各方面需求,以及避免了网络上其他缓存方案所出现的问题定义一个GatewayContext类,用于存储请求中缓存的数据import lombok.Getter;import lombok.Setter;import lombok.ToString;import org.springframework.util.LinkedMultiValueMap;import org.springframework.util.MultiValueMap;@Getter@Setter@ToStringpublic class GatewayContext { public static final String CACHE_GATEWAY_CONTEXT = “cacheGatewayContext”; /** * cache json body / private String cacheBody; /* * cache formdata / private MultiValueMap<String, String> formData; /* * cache reqeust path / private String path;}实现GlobalFilter和Ordered接口用于缓存请求数据1 . 该示例只支持缓存下面3种MediaTypeAPPLICATION_JSON–Json数据APPLICATION_JSON_UTF8–Json数据APPLICATION_FORM_URLENCODED–FormData表单数据2 . 经验总结:在缓存Body时,不能够在Filter内部直接进行缓存,需要按照响应式的处理方式,在异步操作路途上进行缓存Body,由于Body只能读取一次,所以要读取完成后要重新封装新的request和exchange才能保证请求正常传递到下游在缓存FormData时,FormData也只能读取一次,所以在读取完毕后,需要重新封装request和exchange,这里要注意,如果对FormData内容进行了修改,则必须重新定义Header中的content-length已保证传输数据的大小一致import com.choice.cloud.architect.usergate.option.FilterOrderEnum;import com.choice.cloud.architect.usergate.support.GatewayContext;import io.netty.buffer.ByteBufAllocator;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.core.io.ByteArrayResource;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.core.io.buffer.DataBufferUtils;import org.springframework.core.io.buffer.NettyDataBufferFactory;import org.springframework.http.HttpHeaders;import org.springframework.http.MediaType;import org.springframework.http.codec.HttpMessageReader;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpRequestDecorator;import org.springframework.util.MultiValueMap;import org.springframework.web.reactive.function.server.HandlerStrategies;import org.springframework.web.reactive.function.server.ServerRequest;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;import java.io.UnsupportedEncodingException;import java.net.URLEncoder;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;import java.util.List;import java.util.Map;@Slf4jpublic class GatewayContextFilter implements GlobalFilter, Ordered { /* * default HttpMessageReader / private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { /* * save request path and serviceId into gateway context / ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().pathWithinApplication().value(); GatewayContext gatewayContext = new GatewayContext(); gatewayContext.getAllRequestData().addAll(request.getQueryParams()); gatewayContext.setPath(path); /* * save gateway context into exchange / exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT,gatewayContext); HttpHeaders headers = request.getHeaders(); MediaType contentType = headers.getContentType(); long contentLength = headers.getContentLength(); if(contentLength>0){ if(MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){ return readBody(exchange, chain,gatewayContext); } if(MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)){ return readFormData(exchange, chain,gatewayContext); } } log.debug("[GatewayContext]ContentType:{},Gateway context is set with {}",contentType, gatewayContext); return chain.filter(exchange); } @Override public int getOrder() { return Integer.MIN_VALUE; } /* * ReadFormData * @param exchange * @param chain * @return / private Mono<Void> readFormData(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){ HttpHeaders headers = exchange.getRequest().getHeaders(); return exchange.getFormData() .doOnNext(multiValueMap -> { gatewayContext.setFormData(multiValueMap); log.debug("[GatewayContext]Read FormData:{}",multiValueMap); }) .then(Mono.defer(() -> { Charset charset = headers.getContentType().getCharset(); charset = charset == null? StandardCharsets.UTF_8:charset; String charsetName = charset.name(); MultiValueMap<String, String> formData = gatewayContext.getFormData(); /* * formData is empty just return / if(null == formData || formData.isEmpty()){ return chain.filter(exchange); } StringBuilder formDataBodyBuilder = new StringBuilder(); String entryKey; List<String> entryValue; try { /* * remove system param ,repackage form data / for (Map.Entry<String, List<String>> entry : formData.entrySet()) { entryKey = entry.getKey(); entryValue = entry.getValue(); if (entryValue.size() > 1) { for(String value : entryValue){ formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(value, charsetName)).append("&"); } } else { formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&"); } } }catch (UnsupportedEncodingException e){ //ignore URLEncode Exception } /* * substring with the last char ‘&’ / String formDataBodyString = “”; if(formDataBodyBuilder.length()>0){ formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1); } /* * get data bytes / byte[] bodyBytes = formDataBodyString.getBytes(charset); int contentLength = bodyBytes.length; ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator( exchange.getRequest()) { /* * change content-length * @return / @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); if (contentLength > 0) { httpHeaders.setContentLength(contentLength); } else { httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, “chunked”); } return httpHeaders; } /* * read bytes to Flux<Databuffer> * @return / @Override public Flux<DataBuffer> getBody() { return DataBufferUtils.read(new ByteArrayResource(bodyBytes),new NettyDataBufferFactory(ByteBufAllocator.DEFAULT),contentLength); } }; ServerWebExchange mutateExchange = exchange.mutate().request(decorator).build(); log.debug("[GatewayContext]Rewrite Form Data :{}",formDataBodyString); return chain.filter(mutateExchange); })); } /* * ReadJsonBody * @param exchange * @param chain * @return / private Mono<Void> readBody(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){ /* * join the body / return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { /* * read the body Flux<Databuffer> / DataBufferUtils.retain(dataBuffer); Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()))); /* * repackage ServerHttpRequest / ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux<DataBuffer> getBody() { return cachedFlux; } }; /* * mutate exchage with new ServerHttpRequest / ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build(); /* * read body string with default messageReaders */ return ServerRequest.create(mutatedExchange, messageReaders) .bodyToMono(String.class) .doOnNext(objectValue -> { gatewayContext.setCacheBody(objectValue); log.debug("[GatewayContext]Read JsonBody:{}",objectValue); }).then(chain.filter(mutatedExchange)); }); }}在后续Filter中,可以直接从ServerExchange中获取GatewayContext,就可以获取到缓存的数据,如果需要缓存其他数据,则可以根据自己的需求,添加到GatewayContext中即可GatewayContext gatewayContext = exchange.getAttribute(GatewayContext.CACHE_GATEWAY_CONTEXT); ...

January 15, 2019 · 3 min · jiezi

SpringCloud 服务消费者(RestTemplate+Ribbon)

Ribbon简介Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它有助于控制HTTP和TCP的客户端的行为。为Ribbon配置服务提供者地址后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等,我们也可为Ribbon实现自定义的负载均衡算法。启动注册中心和服务提供者1.启动SpringCloud 高可用服务注册中心(Eureka)搭建的注册中心和服务提供者。2.在say-hello项目中添加一个controller,对外提供服务@RestControllerpublic class SayHelloController { @Value("${server.port}") private String serverPort; @GetMapping("/sayHello") public String sayHelloCtr(@RequestParam(“helloName”)String helloName){ return “Hello “+helloName+",我的端口是: “+serverPort; }}3.然后把注册中心和服务提供者say-hello分别启动两个实例。4.查看Eureka注册中心(http://localhost:11111/,http://localhost:11112/)创建服务消费者1.创建一个module项目(service-ribbon)2.添加maven依赖<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>service-ribbon</artifactId> <version>0.0.1-SNAPSHOT</version> <name>service-ribbon</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <!–ribbon中使用断路器–> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>3.修改配置文件server.port=3333eureka.client.service-url.defaultZone=http://server1:11111/eureka/,http://server2:11112/eureka/spring.application.name=service-ribbon4.创建一个开启负载均衡的restRemplate@Configurationpublic class RestTemplateConfig { @Bean @LoadBalanced //表明这个restRemplate开启负载均衡的功能 public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ return restTemplateBuilder .setConnectTimeout(20000) .setReadTimeout(30000) .build(); }}5.添加service消费say-hello提供的服务@Servicepublic class HelloServiceRibbonSer { @Autowired RestTemplate restTemplate; public String helloServiceRibbon(String helloName) { return restTemplate.getForObject(“http://say-hello/sayHello?helloName="+helloName,String.class); } }注:getForObject中的url say-hello为say-hello项目(服务提供者)的应用名称,也就是在创建say-hello项目时在配置文件中配置的spring.application.name=say-hello。sayHello为say-hello项目中接口的地址(controller), helloName是请求参数。6.创建controller,调用service中的方法@RestControllerpublic class HelloServiceRibbonControler { @Autowired private HelloServiceRibbonSer helloServiceRibbonSer; @GetMapping("/ribbonHello”) public String ribbonHelloCtr(@RequestParam(“helloName”)String helloName){ return helloServiceRibbonSer.helloServiceRibbon(helloName); }}7.启动service-ribbon,在浏览器地址栏访问ribbon项目中的controller当一直访问http://localhost:3333/ribbonHello?helloName=aaaa时可以发现浏览器交替显示端口2222和2223,说明已经实现客户端的负载均衡,并且ribbon默认采用轮询的负载均衡算法。Ribbon负载均衡策略自定义负载均衡策略1.修改service-ribbon工程配置文件添加如下配置(使用随机方式)client.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule2.修改RestTemplateConfig配置类添加@Bean public IRule ribbonRule() { return new RandomRule();//实例化与配置文件对应的策略类 }3.修改HelloServiceRibbonControler@RestControllerpublic class HelloServiceRibbonControler { @Autowired private HelloServiceRibbonSer helloServiceRibbonSer; @Autowired private LoadBalancerClient loadBalancerClient; @GetMapping("/ribbonHello”) public String ribbonHelloCtr(@RequestParam(“helloName”)String helloName){ return helloServiceRibbonSer.helloServiceRibbon(helloName); } /** * 随机方式 * @param helloName * @return */ @GetMapping("/ribbonRandomHello”) public String ribbonRandomHelloCtr(@RequestParam(“helloName”)String helloName){ this.loadBalancerClient.choose(“CLIENT”);//随机访问策略 return helloServiceRibbonSer.helloServiceRibbon(helloName); }4.启动项目依次启动eureka注册中心,say-hello项目,service-ribbon项目,访问http://localhost:3333/ribbonRandomHello?helloName=aaaa。可以发现浏览器随机显示端口。 ...

January 15, 2019 · 1 min · jiezi

服务治理Spring Cloud Eureka

1.Spring Cloud Eureka的作用(1)建立Eureka服务端(也称为注册中心)我使用的springboot的版本是2.0.5.RELEASE版本① 添加依赖在书中引入的是spring-cloud-eureka-server,但是当我引入这个依赖的时候,找不到@EnableEurekeServer<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency></dependencies><dependencyManagement> <dependencies> <!– https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>② 配置application-peer1.properties和application-peer2.properties文件以下是application-peer1.properties的配置:server.port=8001spring.application.name=eureka-server#eurekaeureka.instance.hostname=127.0.0.1eureka.client.register-with-eureka=falseeureka.client.fetch-registry=falseeureka.client.service-url.defaultZone=http://127.0.0.1:8002/eureka/eureka.instance.prefer-ip-address=true以下是application-peer2.properties的配置server.port=8002spring.application.name=eureka-server#eurekaeureka.instance.hostname=127.0.0.1eureka.client.register-with-eureka=falseeureka.client.fetch-registry=falseeureka.client.service-url.defaultZone=http://127.0.0.1:8001/eureka/eureka.instance.prefer-ip-address=true③ 建立启动文件,添加@EnableEurekaServer注解@ComponentScan(basePackages = “gdut.ff”)@EnableAutoConfiguration@EnableEurekaServerpublic class Main { public static void main(String args[]) { SpringApplication.run(Main.class, args); }}④ 启动,指定使用的配置文件以上就建立了一个注册中心集群。⑤ 演示效果(2)建立Eureka客户端(也称为服务提供方)① 添加依赖<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency></dependencies><dependencyManagement> <dependencies> <!– https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>② 配置application.propertiesserver.port=9001spring.application.name=hello-serviceeureka.client.service-url.defaultZone=http://127.0.0.1:8001/eureka/,http://127.0.0.1:8002/eureka/eureka.instance.prefer-ip-address=true③ 提供服务@RestController@RequestMapping("/hello")public class HelloController { private final Logger logger = LoggerFactory.getLogger(HelloController.class); @Autowired private DiscoveryClient discoveryClient; @RequestMapping(value = “/index”,method = RequestMethod.GET) public String index() { String serviceId = “hello-service”; List<ServiceInstance> instance = discoveryClient.getInstances(serviceId); if(null != instance) { for(int i = 0;i < instance.size();i++) { System.out.println(instance.get(i).getHost() + “:” + instance.get(i).getPort()); } } return “Hello,world”; }}④ 建立启动文件,添加EnableDiscoveryClient注解@ComponentScan(basePackages = “gdut.ff”)@EnableAutoConfiguration@EnableDiscoveryClientpublic class Main { public static void main(String args[]) { SpringApplication.run(Main.class, args); }}⑤ 启动,指定使用的端口启动Main文件时,指定–server.port=xxx访问http://ip:port/hello/index(3)建立ribbon-consumer(也称为服务调用方)① 添加依赖<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency></dependencies><dependencyManagement> <dependencies> <!– https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>② 配置application.propertiesserver.port=7001spring.application.name=ribbon-consumereureka.client.service-url.defaultZone=http://127.0.0.1:8001/eureka/,http://127.0.0.1:8002/eureka/eureka.instance.prefer-ip-address=true③ 获取服务@RestControllerpublic class ConsumerController { @Autowired RestTemplate restTemplate; @RequestMapping(value = “/ribbon-consumer”, method= RequestMethod.GET) public String helloConsumer() { return restTemplate.getForEntity(“http://hello-service/hello/index”,String.class).getBody(); }}④ 建立启动文件,添加EnableDiscoveryClient注解@ComponentScan(basePackages = “gdut.ff”)@EnableAutoConfiguration@EnableDiscoveryClientpublic class Main { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String args[]) { SpringApplication.run(Main.class, args); }}⑤ 启动访问http://ip:port/ribbon-consumery示例代码 ...

January 13, 2019 · 1 min · jiezi

SpringCloud 高可用服务注册中心(Eureka)

集群原理不同节点Eureka Server通过Replicate(复制)进行数据同步,服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址(即:服务应用名,spring.application.name参数配置),然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。为什么需要集群(1)由于服务消费者本地缓存了服务提供者的地址,即使Eureka Server宕机,也不会影响服务之间的调用,但是一但新服务上线,已经在缓存在本地的服务提供者不可用了,服务消费者也无法知道。(2)当有成千上万个服务向服务注册中心注册的时候,注册中心的负载是非常高的。(3)在分布式系统中,任何地方存在单点故障,整个系统就不是高可用的。搭建Eureka集群1.基于SpringCloud 服务注册与发现(Eureka)创建一个mode工程<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>eureka-server3</artifactId> <version>0.0.1-SNAPSHOT</version> <name>eureka-server3</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2.在resources目录下创建application.ymlspring: application: name: eureka-server3 profiles: active: server1—server: port: 11111spring: profiles: server1eureka: instance: hostname: server1 client: #register-with-eureka: false #fetch-registry: false serviceUrl: defaultZone: http://server2:11112/eureka/—server: port: 11112spring: profiles: server2eureka: instance: hostname: server2 client: #register-with-eureka: false #fetch-registry: false serviceUrl: defaultZone: http://server1:11111/eureka/3.修改host文件添加对application.yml配置文件中hostname的映射10.60.52.40 server110.60.52.40 server24.修改启动类@SpringBootApplication@EnableEurekaServerpublic class EurekaServer3Application { public static void main(String[] args) { SpringApplication.run(EurekaServer3Application.class, args); }}5.启动项目在idea中把项目的Single instance only给去掉,这样一个项目就可以启动多个实例。启动项目,在启动项目的时候回发现控制台报错,因为现在只启动了一个节点,向server2注册时连接拒绝(java.net.ConnectException: Connection refused: connect)等第二个实例启动后就不会再报错。然后修改配置文件中profiles:active: server1,修改为server2,再次启动项目。访问http://localhost:11111/访问:http://localhost:11112/可以看出两个注册中心分别注册了对方。验证高可用1.创建Eureka Client修改sayHello项目中的配置文件,向注册中心中注册。一般在eureka.client.service-url.defaultZone中会把所有注册中心的地址写上,虽然说注册中心可以通过复制的方式进行数据同步。把所有地址都写上可以保证如果第一个注册中心不可用时会选择其他的注册中心进行注册。server.port=2222spring.application.name=say-helloeureka.client.service-url.defaultZone=http://server2:11112/eureka/,http://server1:11111/eureka/2.启动client访问http://localhost:11111/访问http://localhost:11112/发现say-hello已经注册到这两个注册中心中了3.关闭eureka server1访问http://localhost:11111/访问http://localhost:11112/发现say-hello的实例还在在eureka server2中。这时候会发现eureka server2控制台一直在报错,server2开启自我保护机制。4.重启eureka server1后,控制台报错停止,eureka server2关闭自我保护。同时eureka server1会同步eureka server2的实例。 ...

January 13, 2019 · 1 min · jiezi

SpringCloud 服务注册与发现(Eureka)

版本sporingboot:2.0.5springcloud:Finchley.RELEASE创建Maven主工程<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <name>my_cloud</name> <packaging>pom</packaging> <description>Demo project for Spring Boot</description> <modules> <module>eurekaServer</module> <module>sayHello</module> </modules> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories></project>创建model创建两个model工程,创建model一个作为eureka server,一个是client也就是具体的服务。创建eureka server<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>eureka-server</name> <description>Demo project for Spring Boot</description> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>配置文件:server.port=1111eureka.instance.hostname=localhost#这两个是表明自己是eureka Server#是否向服务注册中心注册自己eureka.client.register-with-eureka=false#是否检索服务eureka.client.fetch-registry=false#关闭自我保护机制(生产环境建议开启)#eureka.server.enable-self-preservation=true#服务注册中心的配置内容,指定服务注册中心的位置eureka.client.service-url.defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/spring.application.name: eurka-server在启动类上添加@EnableEurekaServer@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }}启动eureka server,在浏览器地址栏输入http://localhost:1111,可以看到erueka注册中心页面创建服务提供者<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.definesys</groupId> <artifactId>my_cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.definesys</groupId> <artifactId>say-hello</artifactId> <version>0.0.1-SNAPSHOT</version> <name>say-hello</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>配置文件:server.port=2222#服务名称,服务与服务之间相互调用一般都是根据这个namespring.application.name=say-hello#注册中心地址eureka.client.service-url.defaultZone=http://localhost:1111/eureka/创建一个Controller@RestControllerpublic class SayHelloController { @Value("${server.port}”) private String serverPort; @GetMapping("/sayHello”) public String sayHelloCtr(@RequestParam(“helloName”)String helloName){ return “Hello “+helloName+",我的端口是: “+serverPort; }}在启动类上添加@EnableEurekaClient表明自己是一个eureka client@SpringBootApplication@EnableEurekaClientpublic class SayHelloApplication { public static void main(String[] args) { SpringApplication.run(SayHelloApplication.class, args); }}启动服务访问http://localhost:1111,会发现服务已经注册到注册中心中去了。Eureka自我保护机制什么是自我保护 自我保护模式是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的 健壮、稳定的运行。工作机制 Eureka Server 如果在eureka注册中心中看见“EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE“这样一段话,则说明erueka进入自我保护。自我保护模式被激活的条件是:在 1 分钟后,Renews (last min)(Eureka Server 最后 1 分钟收到客户端实例续约的总数。) < Renews threshold(Eureka Server 期望每分钟收到客户端实例续约的总数。)。eureka在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期。一旦开启了保护机制,则服务注册中心维护的服务实例就不是那么准确了,在开发是可以配置eureka.server.enable-self-preservation=false关闭自我保护,这样可以确保注册中心中不可用的实例被及时的剔除eureka不清除已关节点问题 在开发过程中,我们常常希望Eureka Server能够迅速有效地踢出已关停的节点,但是由于Eureka自我保护模式,以及心跳周期长的原因,常常会遇到Eureka Server不踢出已关停的节点的问题 解决办法: Eureka Server端:配置关闭自我保护,并按需配置Eureka Server清理无效节点的时间间隔。 eureka.server.enable-self-preservation # 设为false,关闭自我保护 eureka.server.eviction-interval-timer-in-ms # 清理间隔(单位毫秒,默认是60*1000) Eureka Client端:配置开启健康检查,并按需配置续约更新时间和到期时间。 eureka.client.healthcheck.enabled # 开启健康检查(需要spring-boot-starter-actuator依赖) eureka.instance.lease-renewal-interval-in-seconds # 续约更新时间间隔(默认30秒) eureka.instance.lease-expiration-duration-in-seconds # 续约到期时间(默认90秒) 注:更改Eureka更新频率将打破服务器的自我保护功能,生产环境下不建议自定义这些配置。eureka服务端常用配置 (1)enable-self-preservation: true # 自我保护模式,当出现出现网络分区、eureka在短时间内丢失过多客户端时,会进入自我保护模式,即一个服务长时间没有发送心跳,eureka也不会将其删除,默认为true (2)eviction-interval-timer-in-ms: 60000 #eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒 (3)renewal-percent-threshold: 0.85 #阈值因子,默认是0.85,如果阈值比最小值大,则自我保护模式开启 (4)peer-eureka-nodes-update-interval-ms: 600000 # 集群里eureka节点的变化信息更新的时间间隔,单位为毫秒,默认为10 * 60 * 1000eureka客户端常用配置 (1)register-with-eureka: false #是否注册到eureka (2)registry-fetch-interval-seconds: 30 # 从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒 (3)fetch-registry: false # 实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true 如果是做高可用的发现服务那就要改成true (4)instance-info-replication-interval-seconds: 30 # 复制实例变化信息到eureka服务器所需要的时间间隔(s),默认为30秒 (5)initial-instance-info-replication-interval-seconds: 40 # 最初复制实例信息到eureka服务器所需的时间(s),默认为40秒 (6)eureka-service-url-poll-interval-seconds: 300 #询问Eureka服务url信息变化的时间间隔(s),默认为300秒 (7)eureka-server-connect-timeout-seconds: 5 # eureka需要超时连接之前需要等待的时间,默认为5秒 (8)eureka-server-total-connections: 200 #eureka客户端允许所有eureka服务器连接的总数目,默认是200 (9)heartbeat-executor-thread-pool-size: 2 #心跳执行程序线程池的大小,默认为2 (10)cache-refresh-executor-thread-pool-size: 2 # 执行程序缓存刷新线程池的大小,默认为2 ...

January 10, 2019 · 2 min · jiezi

微服务入门【系列视频课程】

本文首发于本博客 猫叔的博客,转载请申明出处公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。视频教程地址第一章 从传统单体架构走向微服务第二章 传统单体电商架构第三章 SpringCloud入门第四章 Eureka实操与微服务架构搭建第五章 服务拆分和应用间通信第六章 微服务的不解和深入探讨GitHub源码:mic-demo第一章 从传统单体架构走向微服务Hello,大家好,我是猫叔MySelf,本课程将带领大家入门微服务。各位年轻又帅气的靓仔们!新入职公司,接手公司项目,你所看到的是不是就是一座大山你们接触的项目是不是庞大的代码块、并关系错综复杂(一大堆的目录与包)是不是接手后交付周期也很长(入门也是几个通宵)有没有觉得该项目的扩展能力与弹性受限同时,对于大家这样热爱新技术的同学,有时一用上新技术与工具框架就各种BUG而且一个微不足道的小问题,可以导致整个应用挂掉(高层还总是唠叨我们)也是辛苦大家那段时间每夜每夜的加班工作了!在听微服务之前,因为学员层次不一,希望大家有了解到至少一个单体架构的web项目开发经验或大致流程,这样学起来更轻松哦!聪明的老外总是能先于我们发现新的高效的开发模式,近几年前一个老头就提出了我们将要学习的“微服务”:微服务架构风格是一种将单个应用程序作为一套小型服务开发的方法,每种应用程序都在自己的进程中运行,并与轻量级机制(通常是HTTP资源API)进行通信。 这些服务是围绕业务功能构建的,可以通过全自动部署机制独立部署。 这些服务的集中管理最少,可以用不同的编程语言编写,并使用不同的数据存储技术。大叔的图片这个看起来有点复杂,我就不念了,像教科书一样的文案,有兴趣的同学可以上网深入了解。我们整理一下,并优先入门一些重点。其目的是有效的拆分应用,实现敏捷开发和部署只做一件事,并把它做好对于我们这种要求简单的,工作的时候一般都只想做一件事就好了,不要让我顾及太多。单一(隔离)、独立指责我们可以尽情的在自己负责的项目上“玩耍”啦!对于其他服务层的对接仅需要按照各个应用通信接口文档去走即可!通信(同异步)我们总是要和人交流的,对于我们自己独自负责的服务也是需要去交朋友的,因此它需要与其他各个服务进行通信,这里的通信可能是同步的、异步的。对于每个引用都有他们自己的数据,微服务的采纳有助于我们可以针对部分火爆业务采用不同的数据库类型或者分库读取,而不再需要在同一项目整合多个数据库操作。数据独立我们可以发挥不同语言的优势,比如python、nodejs、php….这对技术专项不同的开发团队来说,配合起来将更加容易与得心应手。技术栈灵活(数据流、存储、业务)独立部署、团队组织架构有效调整第二章 单体架构电商Demo讲解一个普通的电商项目:用户服务:注册商品服务:查询商品订单服务:下单、查看订单数据库表:用户表:用户id、用户名、用户密码、创建时间商品表:商品id、商品名、商品详情、商品价格、库存表:商品id、库存数订单表:订单id、用户名、商品id、订单价格、商品总数、创建时间大致的模拟环境:用户下单与查询订单列表,同时还具备管理人员查询库存接口列表:地址: http://localhost:8080/sells/order/order POST参数id 》 用户id goodsId 》 商品id num 》 商品数量例子{ “code”: 200, “msg”: “成功”, “data”: “MS04780334”}地址:localhost:8762/order/order?id=1 GET参数id 》 用户id例子{ “code”: 200, “msg”: “成功”, “data”: [ { “orderId”: “MS04475904”, “goodsId”: “MS000001”, “name”: “猫叔”, “orderPrice”: 1598, “orderNum”: 2, “createTime”: “2018-12-13T05:59:24.000+0000” }, { “orderId”: “MS04663799”, “goodsId”: “MS000002”, “name”: “猫叔”, “orderPrice”: 2999, “orderNum”: 1, “createTime”: “2018-12-12T16:04:18.000+0000” }, { “orderId”: “MS04780334”, “goodsId”: “MS000001”, “name”: “猫叔”, “orderPrice”: 1598, “orderNum”: 2, “createTime”: “2018-12-13T08:10:29.000+0000” } ]}地址: localhost:8763/goods/goods?id=1&goodsId=MS000002 GET参数id 》 管理员idgoodsId 》 商品id例子{ “code”: 200, “msg”: “成功”, “data”: { “inventoryId”: “IN4567825”, “goodsId”: “MS000002”, “inventoryNum”: 13 }}微服务架构拆分当前只有三个服务,一个用户服务、一个订单服务、一个库存服务假设我们的生意狂飙上涨,投放了分销环节、线上广告啥的,这个时候的订单量非常高。那么我们就可以使用微服务的思想,讲订单服务抽离出来。那么我们将使用SpringCloud来完成这一操作与编码。在基于单体架构的情况下,我们将进行手把手的演进,希望同学们可以一起来撸一把。首先是Eureka注册中心,我们需要向某人说明这里是什么,并将单体服务拆分为两个,order-client服务 还有 其余的服务。那么我们就再新建两个对应的Eureka Client的服务并注册到注册中心同时两个服务之间的通讯也需要采用SpringCloud的操作。第三章 SpringCloud入门说到SpringCloud,我们还需要说一下它基于的更厉害的框架,它就是Netflix。Netflix的多个开源组件一起正好可以提供完整的分布式微服务基础架构环境,而SpringCloud就是对Netflix的多个开源组件进一步封装而成的。同时,它利用SpringBoot的开发便利性巧妙地简化了分布式系统基础设施的开发,比如我们今天将学到的服务发现注册(Eureka)、调用框架(声明式服务客户端Fegin)等等。Spring Cloud将目前各个比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,因为我们仅仅配置一下、写几句代码就可以实现一个想要的简单的微服务。第四章 Eureka实操与微服务架构搭建Spring Cloud Eureka1、Eureka是一个基于REST的服务2、基于Netflix Eureka做了二次封装3、核心组件为:Eureka Server 注册中心(服务端)Eureka Client 服务注册(客户端)第五章 服务拆分与应用间通信Spring Cloud 服务间的一种RestFul调用方式1、Feign声明式REST客户端接口 + 注解(开启、指定服务名)本质依旧是一个HTTP客户端第六章 关于微服务的不解与深入探讨我想极具好奇心的同学们一定不满足这么一点的概况,的确,微服务还有更多的知识点需要大家去掌握与了解。负载均衡安全机制配置中心链路追踪容器搭配说了那么多,微服务的缺点是什么呀!?我总是希望唱反调的同学毒液中的片段,世界掌握在我们手中没错,任何思想都有其适用性与自身环境,微服务也不例外。在介绍了微服务原理后,大家会提出什么问提呢?我做学生的时候就提出了几个小问题。微服务架构的取舍?需要避免为了“微服务”而“微服务”。 对传统企业而言,开始时可以考虑引入部分合适的微服务架构原则对已有系统进行改造或新建微服务应用,逐步探索及积累微服务架构经验,而非全盘实施微服务架构。微服务的不足又是哪些?微服务的复杂度分布式事务(CAP理论,AP系统)服务的划分服务的部署各个服务组件的版本与部署乃至升级?一套完善的开发管理规范,包括开发设计规范、分支管理规范、持续集成规范,再利用Docker容器的天然的特性:“版本控制、可移植性、隔离性”就可以实现组件的版本管理。实质上基于在支持DevOps完整流程的容器云平台,即可实现整个开发过程及交付中的持续集成、快速部署、灰度发布及无缝升级。最后希望大家都能在未来深入了解并使用微服务。 ...

January 10, 2019 · 1 min · jiezi

Spring Boot Admin 2.1.0 全攻略

转载请标明出处: https://www.fangzhipeng.com本文出自方志朋的博客Spring Boot Admin简介Spring Boot Admin是一个开源社区项目,用于管理和监控SpringBoot应用程序。 应用程序作为Spring Boot Admin Client向为Spring Boot Admin Server注册(通过HTTP)或使用SpringCloud注册中心(例如Eureka,Consul)发现。 UI是的AngularJs应用程序,展示Spring Boot Admin Client的Actuator端点上的一些监控。常见的功能或者监控如下:显示健康状况显示详细信息,例如JVM和内存指标micrometer.io指标数据源指标缓存指标显示构建信息编号关注并下载日志文件查看jvm系统和环境属性查看Spring Boot配置属性支持Spring Cloud的postable / env-和/ refresh-endpoint轻松的日志级管理与JMX-beans交互查看线程转储查看http跟踪查看auditevents查看http-endpoints查看计划任务查看和删除活动会话(使用spring-session)查看Flyway / Liquibase数据库迁移下载heapdump状态变更通知(通过电子邮件,Slack,Hipchat,……)状态更改的事件日志(非持久性)快速开始创建Spring Boot Admin Server本文的所有工程的Spring Boot版本为2.1.0 、Spring Cloud版本为Finchley.SR2。案例采用Maven多module形式,父pom文件引入以下的依赖(完整的依赖见源码): <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.0.RELEASE</version> <relativePath/> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <spring-cloud.version>Finchley.SR2</spring-cloud.version>在工程admin-server引入admin-server的起来依赖和web的起步依赖,代码如下:<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.1.0</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>然后在工程的启动类AdminServerApplication加上@EnableAdminServer注解,开启AdminServer的功能,代码如下:@SpringBootApplication@EnableAdminServerpublic class AdminServerApplication { public static void main(String[] args) { SpringApplication.run( AdminServerApplication.class, args ); }}在工程的配置文件application.yml中配置程序名和程序的端口,代码如下:spring: application: name: admin-serverserver: port: 8769这样Admin Server就创建好了。创建Spring Boot Admin Client在admin-client工程的pom文件引入admin-client的起步依赖和web的起步依赖,代码如下: <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>在工程的配置文件application.yml中配置应用名和端口信息,以及向admin-server注册的地址为http://localhost:8769,最后暴露自己的actuator的所有端口信息,具体配置如下:spring: application: name: admin-client boot: admin: client: url: http://localhost:8769server: port: 8768management: endpoints: web: exposure: include: ‘’ endpoint: health: show-details: ALWAYS在工程的启动文件如下:@SpringBootApplicationpublic class AdminClientApplication { public static void main(String[] args) { SpringApplication.run( AdminClientApplication.class, args ); }一次启动两个工程,在浏览器上输入localhost:8769 ,浏览器显示的界面如下:查看wallboard:点击wallboard,可以查看admin-client具体的信息,比如内存状态信息:也可以查看spring bean的情况:更多监控信息,自己体验。Spring boot Admin结合SC注册中心使用同上一个案例一样,本案例也是使用的是Spring Boot版本为2.1.0 、Spring Cloud版本为Finchley.SR2。案例采用Maven多module形式,父pom文件引入以下的依赖(完整的依赖见源码),此处省略。搭建注册中心注册中心使用Eureka、使用Consul也是可以的,在eureka-server工程中的pom文件中引入: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>配置eureka-server的端口信息,以及defaultZone和防止自注册。最后系统暴露eureka-server的actuator的所有端口。spring: application: name: eureka-serverserver: port: 8761eureka: client: service-url: defaultZone: http://localhost:8761/eureka register-with-eureka: false fetch-registry: falsemanagement: endpoints: web: exposure: include: “” endpoint: health: show-details: ALWAYS在工程的启动文件EurekaServerApplication加上@EnableEurekaServer注解开启Eureka Server.@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run( EurekaServerApplication.class, args ); }}eureka-server搭建完毕。搭建admin-server在admin-server工程的pom文件引入admin-server的起步依赖、web的起步依赖、eureka-client的起步依赖,如下:<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.1.0</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>然后配置admin-server,应用名、端口信息。并向注册中心注册,注册地址为http://localhost:8761,最后将actuator的所有端口暴露出来,配置如下:spring: application: name: admin-serverserver: port: 8769eureka: client: registryFetchIntervalSeconds: 5 service-url: defaultZone: ${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/ instance: leaseRenewalIntervalInSeconds: 10 health-check-url-path: /actuator/healthmanagement: endpoints: web: exposure: include: “” endpoint: health: show-details: ALWAYS在工程的启动类AdminServerApplication加上@EnableAdminServer注解,开启admin server的功能,加上@EnableDiscoveryClient注解开启eurke client的功能。@SpringBootApplication@EnableAdminServer@EnableDiscoveryClientpublic class AdminServerApplication { public static void main(String[] args) { SpringApplication.run( AdminServerApplication.class, args ); }}搭建admin-client在admin-client的pom文件引入以下的依赖,由于2.1.0采用webflux,引入webflux的起步依赖,引入eureka-client的起步依赖,并引用actuator的起步依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>在工程的配置文件配置应用名、端口、向注册中心注册的地址,以及暴露actuator的所有端口。spring: application: name: admin-clienteureka: instance: leaseRenewalIntervalInSeconds: 10 health-check-url-path: /actuator/health client: registryFetchIntervalSeconds: 5 service-url: defaultZone: ${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/management: endpoints: web: exposure: include: “” endpoint: health: show-details: ALWAYSserver: port: 8762在启动类加上@EnableDiscoveryClie注解,开启DiscoveryClient的功能。@SpringBootApplication@EnableDiscoveryClientpublic class AdminClientApplication { public static void main(String[] args) { SpringApplication.run( AdminClientApplication.class, args ); }}一次启动三个工程,在浏览器上访问localhost:8769,浏览器会显示和上一小节一样的界面。集成spring security在2.1.0版本中去掉了hystrix dashboard,登录界面默认集成到了spring security模块,只要加上spring security就集成了登录模块。只需要改变下admin-server工程,需要在admin-server工程的pom文件引入以下的依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>在admin-server工的配置文件application.yml中配置spring security的用户名和密码,这时需要在服务注册时带上metadata-map的信息,如下:spring: security: user: name: “admin” password: “admin” eureka: instance: metadata-map: user.name: ${spring.security.user.name} user.password: ${spring.security.user.password}写一个配置类SecuritySecureConfig继承WebSecurityConfigurerAdapter,配置如下:@Configurationpublic class SecuritySecureConfig extends WebSecurityConfigurerAdapter { private final String adminContextPath; public SecuritySecureConfig(AdminServerProperties adminServerProperties) { this.adminContextPath = adminServerProperties.getContextPath(); } @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); successHandler.setTargetUrlParameter( “redirectTo” ); http.authorizeRequests() .antMatchers( adminContextPath + “/assets/**” ).permitAll() .antMatchers( adminContextPath + “/login” ).permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage( adminContextPath + “/login” ).successHandler( successHandler ).and() .logout().logoutUrl( adminContextPath + “/logout” ).and() .httpBasic().and() .csrf().disable(); // @formatter:on }}重启启动工程,在浏览器上访问:http://localhost:8769/,会被重定向到登录界面,登录的用户名和密码为配置文件中配置的,分别为admin和admin,界面显示如下:集成邮箱报警功能在spring boot admin中,也可以集成邮箱报警功能,比如服务不健康了、下线了,都可以给指定邮箱发送邮件。集成非常简单,只需要改造下admin-server即可:在admin-server工程Pom文件,加上mail的起步依赖,代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId></dependency>在配置文件application.yml文件中,需要配置邮件相关的配置,如下:spring.mail.host: smtp.163.comspring.mail.username: miles02spring.mail.password:spring.boot.admin.notify.mail.to: 124746406@qq.com做完以上配置后,当我们已注册的客户端的状态从 UP 变为 OFFLINE 或其他状态,服务端就会自动将电子邮件发送到上面配置的地址。源码下载快速开始: https://github.com/forezp/Spr…和spring cloud结合:https://github.com/forezp/Spr…参考资料http://codecentric.github.io/…https://github.com/codecentri…更多阅读史上最简单的 SpringCloud 教程汇总SpringBoot教程汇总Java面试题系列汇总<div><p align=“center”> <img src=“https://www.fangzhipeng.com/img/avatar.jpg" width=“258” height=“258”/> <br> 扫一扫,支持下作者吧</p><p align=“center” style=“margin-top: 15px; font-size: 11px;color: #cc0000;"> <strong>(转载本站文章请注明作者和出处 <a href=“https://www.fangzhipeng.com”>方志朋的博客</a>)</strong></p></div> ...

January 9, 2019 · 2 min · jiezi

Spring Boot 2.1整合Redis

1.普通字符串存储1.引入spring-boot-starter-data-redisjar包,注意spring boot 2.1 没有对应的spring-boot-starter-redis版本,改名为spring-boot-starter-data-redis。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>2.在application中添加redis配置spring: redis: host: 192.168.187.11 port: 63793.测试redis客户端查看,没有任何key192.168.187.11:6379> keys *(empty list or set)@RestController@RequestMapping(value = “/string”)public class RedisStringController { @Autowired private StringRedisTemplate stringRedisTemplate; @PutMapping(value = “/put”) public void put(String key, @RequestParam(required = false, defaultValue = “default”) String value) { stringRedisTemplate.opsForValue().set(key, value); } @GetMapping(value = “/get”) public Object get(String key) { return stringRedisTemplate.opsForValue().get(key); }}使用postman送请求: localhost:8080/string/put?key=hello&value=worldlocalhost:8080/string/get?key=hello2. 对象存储在上述使用中,是无法存储对象的,存储对象的话需要使用RedisTemplate并且要使用响应的序列化机制,下面我们就来测试下:1.引入序列化的jar包,这里我们是使用jackson<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.9.5</version></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.5</version></dependency>2.添加redis配置类@Configurationpublic class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); //使用StringRedisSerializer来序列化和反序列化redis的ke redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); //开启事务 redisTemplate.setEnableTransactionSupport(true); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; }}3.添加测试controller@RestController@RequestMapping(value = “/object”)public class RedisObjectController { @Autowired private RedisTemplate<String, Object> redisTemplate; @GetMapping("/get/{username}") public Object get(@PathVariable String username) { return redisTemplate.opsForValue().get(username); } @PutMapping("/put") public void put(String username, String nickname) { User user = new User(username, nickname); redisTemplate.opsForValue().set(username, user); }}4.使用postman测试使用redis客户端查看192.168.187.11:6379> get qianjiangtao"{"@class":"com.lvmama.tony.model.User","username":"qianjiangtao","nickname":"Tony-J"}“3.spring boot整合redis自动化配置原理分析我们都知道spring boot自动化配置中的配置都是通过spring-configuration-metadata.json来约束的,同理redis也是这样的,我们配置了spring.redis.host,不妨来找下这个配置项{“name”: “spring.redis.host”,“type”: “java.lang.String”,“description”: “Redis server host.”,“sourceType”: “org.springframework.boot.autoconfigure.data.redis.RedisProperties”,“defaultValue”: “localhost”}从这能看出来redis的配置都是通过RedisProperties这个类来配置的,在这里面我们能看到众多的redis配置及默认配置,我们可以从一个入口切入,就拿port切入,来看下port在哪设置的org.springframework.boot.autoconfigure.data.redis.RedisConnectionConfiguration#getStandaloneConfig看出在RedisConnectionConfiguration中的getStandaloneConfig中赋值的,那这个方法又是谁调用的呢?继续找?从图中能看出来有两个地方可能会调用,从类的名字能看出来,spring boot是支持Jedis和Lettuce两种客户端来操作redis,那到底是用哪个呢? 都看看呗从图中截取的源码中能看出来,我是使用了LettuceConnectionConfiguration,看注解是我引入了RedisClient,我什么时候引入的?于是我就看看maven的依赖从maven依赖中能看出一些重要的信息:1.spring-boot-starter-data-redis中其实用的是spring-data-redis,其实是包装了下2.依赖了lettuce-core,原来是从这里引入的,怪不得如何验证呢?不能瞎说 要想知道很简单的,在我们自己写的RedisConfig中打下断点,看看用的RedisConnectionFactory到底是不是LettuceConnectionFactory就能证明了果然如此!简单的流程就是:1.spring boot通过application配置加载redis配置2.解析封装成RedisProperties3.根据@ConditionalOnClass判断使用哪个Redis客户端,封装成LettuceClientConfiguration并创建LettuceConnectionFactory4.通过@Bean创建我们自己的配置类在LettuceConnectionFactory基础上添加我们自己自定义的配置 ...

January 8, 2019 · 1 min · jiezi

springcloud redis 缓存使用注意事项

springcloud redis 缓存使用注意事项

January 3, 2019 · 1 min · jiezi

springboot新版本(2.1.0)、springcloud新版本(Greenwich.M1)实现链路追踪的一些坑

主要问题 由于springboot新版本(2.1.0)、springcloud新版本(Greenwich.M1)实现链路追踪sleuth+zipkin的一些“新特性”,使得我在实现sleuth+zipkin的过程上踩了不少坑。 在springboot1.X版本的时候,实现链路追踪服务需要用户自己实现client以及server,通常在server服务端需要引入各种各样的包(spring-cloud-sleuth-stream,以及支持zipkin的一些相关依赖包等等); 但在spring cloud新版本实现链路追踪sleuth+zipkin的方式上已经不再需要自己再去实现一个server服务端(集成sleuth+zipkin),而是由zinkin官方提供了一个现成的zipkin-server.jar,或者是一个docker镜像,用户可以下载并通过命令进行启动它,用户可以通一些配置来确定sleuth收集到信息后传输到zipkin之间采用http,还是通过rabbit/kafka的方式。在新的版本下,用户只需要关注slenth-client选用何种传输方式(http或mq(rabbit/kafka),如果选择http,则在配置中指明base-url;如果选择mq,则在配置指明相关消息中间件的相关信息host/port/username/password…),至于zipkin的信息storage问题,则由zipkin-server要负责,可以通过zipkin-server.jar 配置一些具体的参数来启动。(下面会细讲)ps:这不是教程贴,这主要是解决一些问题的一些方法,不会有详细的实现过程,但为了简明我会贴上部分代码。背景 最近开始实习了,老大让我自学一下sc(spring cloud),学就学嘛,也不是难事。看完spring cloud的全家桶,老大说让我重点了解一下它的链路追踪服务,后期会有这方面的任务安排给我做,所以呢我就重点关注这一方面,打算自己做个demo练练手,看了网上的教程,膨胀的我选择了个最新的版本,结果发现就这么掉坑里了。。。版本按照惯例,先说下springboot跟spring cloud的版本springboot:2.1.0springcloud:Greenwich.M1个人建议新手不要过分追求新版本,旧版本的还是够用的,比springboot 2.6.0搭配sringcloud Finchley SR2还是挺稳的,如果真的要探索新版本你会发现这里面的坑实在是踩不完,基本要花个一两天才能让自己从坑里跳出去,这样频繁踩坑会让新手很容易放弃~~~ps:不要问我为什么知道。。。主要问题闲话扯完了,可以进入正题了一共四个服务eureka-serverzipkin-server:新版本的zipkin服务端,负责接受sleuth发送过来的数据,完成处理、存储、建立索引,并且提供了一个可视化的ui数据分析界面。需要的同学话可以直接在github上下载https://github.com/openzipkin…嗯就是这两个家伙下面两个是两个服务eureka-server服务注册中心,这个实现我就不讲了,网上搜一大把,各个版本实现基本都是一致的,并不存在版本更新跨度极大的情况。而且这里我把它是打包成一个jar包,在需要的时候直接用java -jar XXX.jar 直接启动至于product跟order(也即实际场景下各种种样的服务A、B、C…)order服务只有一个接口/test,去调用product的接口这里的productclient就是使用feignf去调用order的/product/list接口product只有一个接口/product/list,查找所有商品的列表简单的来说,这里的场景就是order服务–(去调用)–>product服务说完场景后,贴一下这两个服务的相关配置信息(order跟producet的配置基本上是相同的)application.ymlspring: application: #服务名 name: product #由于业务逻辑需要操作数据库,所以这里配置了mysql的一些信息 datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: 123456 url: jdbc:mysql://127.0.0.1:3306/sc_sell?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai jpa: show-sql: true #重点 zipkin: #base-url:当你设置sleuth-cli收集信息后通过http传输到zinkin-server时,需要在这里配置 base-url: http://localhost:9411 enabled: true sleuth: sampler: #收集追踪信息的比率,如果是0.1则表示只记录10%的追踪数据,如果要全部追踪,设置为1(实际场景不推荐,因为会造成不小的性能消耗) probability: 1eureka: client: service-url: #注册中心地址 defaultZone: http://localhost:8999/eureka/logging: level: #这个是设置feign的一个日志级别,key-val的形式设置 org.springframework.cloud.openfeign: debug说完配置信息,就该讲一下依赖了,很简单,client实现链路追踪只需要添加一个依赖spring-cloud-starter-zipkin。就是这个 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>其实这些都是基础操作,是吧,那么来点进阶的。在sleuth-cli跟zipkin-server之间插入一个消息中间件rabbitmq/kafka,这里我举例中只使用rabbitmq来实现将链路追踪的数据存储到DB上,目前zipkin暂时只支持mysql/elasticsearch,这里我使用mysql如果你是刚开始学习sc,给你去实现的话,你肯定会开始打开浏览器开始搜索教程。结果你会发现,大部分博客上都是以前版本的实现方式,一些较旧会让你自己实现一个zipkin-server(我怀疑他们的版本是1.x),你会发现很郁闷,因为这跟你想象的不太一样啊。继续找,终于在茫茫帖子中,找到了一篇是关于springboot2.0.X版本的实现链路追踪的教程,这时候你会兴奋,终于找到靠谱一点的啊,喜出望外有木有啊,但是,事情还没完,它会让你在客户端依赖下面这个依赖包 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-stream</artifactId> </dependency>结果你会发现,你在依赖它的时候,其实是依赖不了,为什么?因为版本的问题,什么?你跟我说你的pom文件没报错啊,但是,你打开idea右边的maven插件看一下这真的是一个巨坑,我一直不明白是怎么回事,直到有一次,我打开了这个页面,花了我一天的时间去摸索是什么原因造成的集成rabbitmq失败,真的是被安排得明明白白最后,豪无头绪的我,继续在网上查找一些springboot2.x版本的一些链路追踪的教程,在搜索了一个下午,我突然想起,诶不对,我应该直接去官网看它的官方教程的啊。。。虽然都英文,大不了我用chrome自带的翻译工具翻译一下咯。结果就立马打开spring的官网,选择了最新的版本,进去找了一下,还真的让我找到了!!!不得不说官方文档的重要性!https://cloud.spring.io/sprin…

December 25, 2018 · 1 min · jiezi

spring cloud eureka服务注册和调用

SPRING INITIALIZR构建工程spring boot 可通过SPRING INITIALIZR构建项目访问SPRING INITIALIZR官网,填写项目相关信息后,生成项目。将下载的项目解压,打开idea,file–>new–>project from existing sources。import project选择maven项目,jdk选择1.8或以上,一直next即可。spring cloud server 提供服务注册1.修改pom.xml<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.gyt</groupId> <artifactId>helloworld.Eureka.server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>helloworld.Eureka.server</name> <description>Demo project for Spring Boot</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!–eureka server –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <!– spring boot test–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RC1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories></project>2.通过SPRING INITIALIZR构建工程后,修改配置文件application.properties,我这里使用的是application.yml,两者只在格式上存在差别。3.启动类application.java添加@EnableEurekaServer注解。@EnableEurekaServer@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}4.访问http://localhost:8000/spring cloud client 提供服务1.同样创建项目后,修改pom.xml<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.gyt</groupId> <artifactId>helloworld.eureka.client</artifactId> <version>0.0.1-SNAPSHOT</version> <name>helloworld.eureka.client</name> <description>Demo project for Spring Boot</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RC1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories></project>2.application.ymleureka: client: serviceUrl: #server对应的地址 defaultZone: http://localhost:8000/eureka/ instance: hostname: localhostserver: port: 8089spring: application: #应用名 name: service-helloworldribbon: eureka: enabled: true其中关于ribbon的学习来自于 https://www.jianshu.com/p/f86...3.ApplicationClient.java@SpringBootApplication@EnableDiscoveryClientpublic class ApplicationClient { public static void main(String[] args) { SpringApplication.run(ApplicationClient.class, args); }}4.HelloController.java@RestControllerpublic class HelloController { @RequestMapping("/hello”) public String index(@RequestParam String name) { return “hello “+name+",hahahaha!”; }}5.访问http://localhost:8089/hello?name=w此时再看服务端,多了service-helloworld这个应用。spring cloud consumer 使用服务1.新建项目,修改pom.xml<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.gyt</groupId> <artifactId>helloworld.eureka.consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>helloworld.eureka.consumer</name> <description>Demo project for Spring Boot</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!–eureka server –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <!– spring boot test–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RC1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories></project>2.application.propertites【这里特意没有用yml】spring.application.name=spring-cloud-consumerserver.port=9001eureka.client.serviceUrl.defaultZone=http://localhost:8000/eureka/3.ApplicationConsumer.java@SpringBootApplication@EnableDiscoveryClient@EnableFeignClientspublic class ApplicationConsumer { public static void main(String[] args) { SpringApplication.run(ApplicationConsumer.class, args); }}4.创建接口类,HelloTest.java//这里的name指定客户端的应用名@FeignClient(name= “service-helloworld”)public interface HelloTest { //这里requestMapping与客户端相同 @RequestMapping(value = “/hello”) public String hello(@RequestParam(value = “name”) String name);}5.ConsumerController.java@RestControllerpublic class ConsumerController { @Autowired HelloTest helloTest; @RequestMapping("/hello/{name}”) public String index(@PathVariable(“name”) String name) { return helloTest.hello(name); }}6.访问http://localhost:9001/hello/w此时,服务端存在两个应用 ...

December 19, 2018 · 2 min · jiezi

spring cloud 访问controller报(type=Not Found, status=404)错误

最近在学习微服务架构过程中,项目启动成功,但在浏览器访问controller对应的requestMapping的时候一直报There was an unexpected error (type=Not Found, status=404).错误。解决过程如下:1. 检查了访问地址路径http://localhost:8089/client1/hello?name=w,对应的application.yml里面配置的端口号和上下文路径没有问题。 controller里面的mapping也没有问题。 2. 百度一下,网上说springboot+thymeleaf的项目需要添加thymeleaf依赖,而此项目只是 springboot。 3. 检查包路径。springboot扫描文件@ComponentScan(basePackages = {"com.spring.*"}), 由此springboot启动类的包必须是项目下的父路径,其他类的包路径必须是其子路径。这里ApplicationClient.java是项目启动类,路径正确,而controller的路径不在ApplicationClient.java的包路径下,扫描不到。问题就在这里啦,修改controller的路径为com.xxx.helloworld.eureka.client.controllers,最后访问成功啦。

December 19, 2018 · 1 min · jiezi

spring cloud gateway 之限流篇

转载请标明出处: https://www.fangzhipeng.com本文出自方志朋的博客在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。常见的限流方式,比如Hystrix适用线程池隔离,超过线程池的负载,走熔断的逻辑。在一般应用服务器中,比如tomcat容器也是通过限制它的线程数来控制并发的;也有通过时间窗口的平均速度来控制流量。常见的限流纬度有比如通过Ip来限流、通过uri来限流、通过用户访问频次来限流。一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也可以在应用层通过Aop这种方式去做限流。本文详细探讨在 Spring Cloud Gateway 中如何实现限流。常见的限流算法计数器算法计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”漏桶算法漏桶算法为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。令牌桶算法从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。Spring Cloud Gateway限流在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:具体源码不打算在这里讲述,读者可以自行查看,代码量较少,先以案例的形式来讲解如何在Spring Cloud Gateway中使用内置的限流过滤器工厂来实现限流。首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码如下: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifatId>spring-boot-starter-data-redis-reactive</artifactId></dependency>在配置文件中做以下的配置:server: port: 8081spring: cloud: gateway: routes: - id: limit_route uri: http://httpbin.org:80/get predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver] filters: - name: RequestRateLimiter args: key-resolver: ‘#{@hostAddrKeyResolver}’ redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 3 application: name: gateway-limiter redis: host: localhost port: 6379 database: 0在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:burstCapacity,令牌桶总容量。replenishRate,令牌桶每秒填充平均速率。key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。public class HostAddrKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); }} @Bean public HostAddrKeyResolver hostAddrKeyResolver() { return new HostAddrKeyResolver(); }可以根据uri去限流,这时KeyResolver代码如下:public class UriKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getURI().getPath()); }} @Bean public UriKeyResolver uriKeyResolver() { return new UriKeyResolver(); } 也可以以用户的维度去限流: @Bean KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst(“user”)); }用jmeter进行压测,配置10thread去循环请求lcoalhost:8081,循环间隔1s。从压测的结果上看到有部分请求通过,由部分请求失败。通过redis客户端去查看redis中存在的key。如下:可见,RequestRateLimiter是使用Redis来进行限流的,并在redis中存储了2个key。关注这两个key含义可以看lua源代码。源码下载https://github.com/forezp/Spr…参考资料http://cloud.spring.io/spring…https://windmt.com/2018/05/09...http://www.spring4all.com/art…扫一扫,支持下作者吧: ...

December 18, 2018 · 1 min · jiezi

SpringCloud灰度发布实践

服务实例eureka-serverzuul -serverApollo-configprovider-test启动两个服务实例,一个带有版本号信息,一个没有port:7770 version:无port: 7771 version:v1consumer-test启动两个服务实例,一个带有版本号信息,一个没有任何信息.分别为:port:8880 version:无port:8881 version: v1场景分析 公司采用的是SpringCloud微服务体系,注册中心为eureka。我们知道,对于eureka-server而言,其他服务都是eureka-client的存在,我们在业务服务中只需要引入@EnableDiscoveryClient即可实现注册. 比如我们想要调用order-service的服务,只需要通过resttemplate或者fegin的方式指定该服务的服务名即可完成调用。这是为什么呢?这一切还得从Ribbon这个背后的男人说起,因为ribbon会根据order-service的服务实例名获取该服务实例的列表,这些列表中包含了每个服务实例的IP和端口号,Ribbon会从中根据定义的负载均衡算法选取一个作为返回. 看到这里一切都有点拨云见雾了,那么意味着是不是只要能够让Ribbon用我们自定义的负载均衡算法就可以为所欲为了呢?显然,是的!简单分析下我们在调用过程中的集中情况:外部调用:请求==>zuul==>服务zuul在转发请求的时候,也会根据Ribbon从服务实例列表中选择一个对应的服务,然后选择转发.内部调用:服务==>Resttemplate==>服务服务==>Fegin==>服务无论是通过Resttemplate还是Fegin的方式进行服务间的调用,他们都会从Ribbon选择一个服务实例返回.上面几种调用方式应该涵盖了我们平时调用中的场景,无论是通过哪种方式调用(排除直接ip:port调用),最后都会通过Ribbon,然后返回服务实例.设计思路eureka预备知识 eureka元数据:标准元数据:主机名,IP地址,端口号,状态页健康检查等信息自定义元数据:通过eureka.instance.metadata-map配置更改元数据:源码地址:com.netflix.eureka.resources.InstanceResource.updateMetadata()接口地址: /eureka/apps/appID/instanceID/metadata?key=value调用方式:PUTE流程解析: 1.在需要灰度发布的服务实例配置中添加eureka.instance.metadata-map.version=v1,注册成功后该信息后保存在eureka中.配置如下:eureka.instance.metadata-map.version=v1 2.自定义zuul拦截器GrayFilter。当请求带着token经过zuul时,根据token得到userId,然后从分布式配置中心Apollo中获取灰度用户列表,并判断该用户是否在该列表中(Apollo非必要配置,由于管理端比较完善所以笔者这里选择采用). 若在列表中,则把version信息存在ThreadLocal中,从而使Ribbon中我们自定义的Rule能够拿到该信息;若不在,则不做任何处理。 但是我们知道hystrix是用线程池做隔离的,线程池中是无法拿到ThreadLocal中的信息的! 所以这个时候我们可以参考Sleuth做分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal.为了方便继承,这里采用Sleuth的方式做处理。Sleuth能够完整记录一条跨服务调用请求的每个服务请求耗时和总的耗时,它有一个全局traceId和对每个服务的SpanId.利用Sleuth全局traceId的思路解决我们的跨线程调用,所以这里可以使用HystrixRequestVariableDefault实现跨线程池的线程变量传递效果. 3.zuul在转发之前会先到我们自定义的Rule中,默认Ribbon的Rule为ZoneAvoidanceRule.自定义编写的Rule只需要继承ZoneAvoidanceRule并且覆盖其父类的PredicateBasedRule#choose()方法即可.该方法是返回具体server,源码如下public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule { public abstract AbstractServerPredicate getPredicate(); @Override public Server choose(Object key) { ILoadBalancer lb = getLoadBalancer(); Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); if (server.isPresent()) { return server.get(); } else { return null; } }}这里就完成了zuul转发到具体服务的灰度流程了,流程图如下4.上面完成了zuul–>服务的灰度过程,接下来就是解决服务与服务间的调用.服务与服务间的调用依然可以用Sleuth的方式解决,只需要把信息添加到header里面即可.在使用RestTemplate调用的时候,可以添加它的拦截器ClientHttpRequestInterceptor,这样可以在调用之前就把header信息加入到请求中.restTemplate.getInterceptors().add(YourHttpRequestInterceptor());5.到这里基本上整个请求流程就比较完整了,但是我们怎么使用自定义的Ribbon的Rule了?这里其实非常简单,只需要在properties文件中配置一下代码即可.yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定义的负载均衡策略类但是这样配置需要指定服务名,意味着需要在每个服务的配置文件中这么配置一次,所以需要对此做一下扩展.打开源码org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration类,该类是Ribbon的默认配置类.可以清楚的发现该类注入了一个PropertiesFactory类型的属性,可以看到PropertiesFactory类的构造方法 public PropertiesFactory() { classToProperty.put(ILoadBalancer.class, “NFLoadBalancerClassName”); classToProperty.put(IPing.class, “NFLoadBalancerPingClassName”); classToProperty.put(IRule.class, “NFLoadBalancerRuleClassName”); classToProperty.put(ServerList.class, “NIWSServerListClassName”); classToProperty.put(ServerListFilter.class, “NIWSServerListFilterClassName”); }所以,我们可以继承该类从而实现我们的扩展,这样一来就不用配置具体的服务名了.至于Ribbon是如何工作的,这里有一篇方志明的文章(传送门)可以加强对Ribbon工作机制的理解6.这里就完成了灰度服务的正确路由,若灰度服务已经发布测试完毕想要把它变成正常服务,则只需要通过eureka的RestFul接口把它在eureka的metadata-map存储的version信息置空即可.7.到这里基本上整个请求流程就比较完整了,上述例子中是以用户ID作为灰度的维度,当然这里可以实现更多的灰度策略,比如IP等,基本上都可以基于此方式做扩展 ...

December 18, 2018 · 1 min · jiezi

从eureka报错中得知的默认配置

配置信息eureka-serverspring.application.name=eureka-serverserver.port=1111eureka.client.register-with-eureka=falseeureka.client.fetch-registry=falseeureka-clientspring.application.name=eureka-clientserver.port=8002eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/错误分析如上配置,配置信息非常简单.在启动eureka-server的时候并未发现任何异常,但是在启动eureka-client的时候,控制台却输出如下信息:com.sun.jersey.api.client.ClientHandlerException: org.apache.http.conn.ConnectTimeoutException: Connect to localhost:8761 timed out at com.sun.jersey.client.apache4.ApacheHttpClient4Handler.handle(ApacheHttpClient4Handler.java:187) ~[jersey-apache-client4-1.19.1.jar:1.19.1] at com.netflix.eureka.cluster.DynamicGZIPContentEncodingFilter.handle(DynamicGZIPContentEncodingFilter.java:48) ~[eureka-core-1.7.0.jar:1.7.0] at com.netflix.discovery.EurekaIdentityHeaderFilter.handle(EurekaIdentityHeaderFilter.java:27) ~[eureka-client-1.7.0.jar:1.7.0] at com.sun.jersey.api.client.Client.handle(Client.java:652) ~[jersey-client-1.19.1.jar:1.19.1] at com.sun.jersey.api.client.WebResource.handle(WebResource.java:682) ~[jersey-client-1.19.1.jar:1.19.1] at com.sun.jersey.api.client.WebResource.access$200(WebResource.java:74) ~[jersey-client-1.19.1.jar:1.19.1] at com.sun.jersey.api.client.WebResource$Builder.post(WebResource.java:570) ~[jersey-client-1.19.1.jar:1.19.1] at com.netflix.eureka.transport.JerseyReplicationClient.submitBatchUpdates(JerseyReplicationClient.java:116) ~[eureka-core-1.7.0.jar:1.7.0] at com.netflix.eureka.cluster.ReplicationTaskProcessor.process(ReplicationTaskProcessor.java:71) ~[eureka-core-1.7.0.jar:1.7.0] at com.netflix.eureka.util.batcher.TaskExecutors$BatchWorkerRunnable.run(TaskExecutors.java:187) [eureka-core-1.7.0.jar:1.7.0]看着这段揪心的提示后反复检查了配置和代码,实在想不明白哪个地方有配置8761端口号.在这个时候打开注册中心localhost:1111后也看到eureka-client服务也注册上来了,那为什么会报这个错了,莫非eureka有自己的默认配置?在一番搜索后得到了答案(问题传送门),因为我并未指定eureka-server的service-url属性,所以在服务注册过来的时候eureka-server会尝试将注册信息复制到默认的service-url,即 localhost:8761上面.所以才会报出这样的错误!解决方案既然上面分析出来了问题,那么解决就很容易 ,即覆盖eureka-server的默认的属性service-url就可以了,在eureka-server添加如下代码:eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/默认配置 在得知上面的解决方案后,不禁思考eureka的这些默认配置在哪里呢?他们的默认属性值又是多少?eureka的这些默认配置是存在org.springframework.cloud.netflix.eureka.EurekaClientConfigBean类里面.@Data@ConfigurationProperties(EurekaClientConfigBean.PREFIX)public class EurekaClientConfigBean implements EurekaClientConfig, EurekaConstants { public static final String PREFIX = “eureka.client”; @Autowired(required = false) PropertyResolver propertyResolver; public static final String DEFAULT_URL = “http://localhost:8761” + DEFAULT_PREFIX + “/”; public static final String DEFAULT_ZONE = “defaultZone”; private static final int MINUTES = 60; private boolean enabled = true; @NestedConfigurationProperty private EurekaTransportConfig transport = new CloudEurekaTransportConfig(); private int registryFetchIntervalSeconds = 30; private int instanceInfoReplicationIntervalSeconds = 30; private int initialInstanceInfoReplicationIntervalSeconds = 40; private int eurekaServiceUrlPollIntervalSeconds = 5 * MINUTES; private String proxyPort; private String proxyHost; private String proxyUserName; private String proxyPassword; private int eurekaServerReadTimeoutSeconds = 8; private int eurekaServerConnectTimeoutSeconds = 5; private String backupRegistryImpl; private int eurekaServerTotalConnections = 200; private int eurekaServerTotalConnectionsPerHost = 50; private String eurekaServerURLContext; private String eurekaServerPort; private String eurekaServerDNSName; private String region = “us-east-1”; //……略看到这里一切都豁然开朗了 ...

December 14, 2018 · 1 min · jiezi

微服务架构实践:从零搭建网站扫码登录

微信扫码登录大家都是应用比较多的登录方式了,现在大的购物网站像京东、淘宝等都支持使用APP扫码登录网站了。今天就用APP扫码登录网站的实例来举例说明微服务架构的搭建过程。微服务架构应该是什么样子在这之前先看一看一个微服务架构落地以后应该是什么样子的。平常所有的微服务架构更多的是从框架来讲的像Dubbo,SpringCloud等,从整个SpringCloud的生态来讲它也只包含微服务的一部分。因为微服务的拆分不可避免的造成了系统的复杂性,团队间的合作管理和持续的交付等等,都是一项比较复杂的工程,如果没有好的团队管理规范和持续交付的流程等微服务是很难落地的。下面简单介绍一下上图中微服务架构的每一层的功能和作用:基础设施层,这一项除非自己搭建IDC,基本上现在的阿里云、腾讯云和百度云等都已经很好的支撑,特别是对于小的公司来说,更节省成本。平台服务层,对于现有的微服务能够快速动态部署那就是Docker了,再加上现有k8s等容器管理工具等,更是让微服务的部署如虎添翼,如果系统已经达到已经规模以后,可以考虑使用此种方式进行动态的扩容,一般情况下使用Docker就能解决部署问题了。支撑服务层,这一层跟微服务框架贴的非常近了,像SpringCloud已经自带了很多功能,像注册中心、配置中心、熔断限流和链路跟踪等,Dubbo也自带注册中心。业务服务层,这一层主要解决的是业务系统如何使用微服务进行解耦,各业务模块间如何进行分层交互等,形成了以基础服务模块为底层和以聚合服务为前端的“大中台小前台”的产品策略。网关服务层,这一层解决了权限控制、外部调用如何进行模块的负载均衡,可以实现在该层实现权限和流量的解耦,来满足不同的端的流量和权限不同的需求。接入层,该层主要是为了解决相同网关多实例的负载均衡的问题,防止单点故障灯。微服务开发框架,现在流行的微服务框架主要是SpringCloud和Dubbo,SpingCloud提供了更加完整的生态,Dubbo更适合内部模块间的快速高并发的调用。持续交付流水线,快速进行需求迭代,从提交代码到部署上线,能够快速的交付。工程实践与规范,这一项做不好,那整个微服务实施起来绝对是痛不欲生啊,基础模块如何定义,基础模块如何与其他模块解耦,如何进行版本的管理这个我在之前的使用Git和Maven进行版本管理和迭代的方法进行了说明。端到端的工具链,这里就是敏捷运维工具,从研发代码到最终上线到生产环境,任何一部都要有工具去实现完成,实现点一个按钮就能最终上线的系统。以上讲了实现微服务架构应该要做哪些事情,现在可以想想你的微服务架构到底落地到生成程度了,闲话少说,书归正传,今天是用APP扫码登录网站这个功能来进行举例说明应该从哪些方面进行微服务的落地实践。网站扫码登录功能这个功能是指在网站上选择使用二维码扫码登录,网站展示二维码,使用已经登录的应用APP扫码并确认登录后,网站就能登录成功,这既简单快捷,又提高了安全性。现在实现扫码登录网站的技术基本上有两种,一种就是轮询,另一种就是长连接,长连接又分为服务器端单向通信和双向通信两种,服务端单向通信只能由服务器端向客户端一直发送数据,双向通信是客户端和服务器端可以相互发送数据。像微信、京东和淘宝都是采用轮询的方式进行扫码登录的,一直使用轮询的方式在请求服务器端。今天我设计的这个扫码登录的功能,是采用的长连接能够双向通信的WebSocket的方式实现的。网站扫码实现流程1.用户在网站上登录时选择扫码登录。2.服务器端收到请求,生成一个临时的令牌,前端生成带令牌的链接地址的二维码,在浏览器上显示。3.PC端同时要与后台建立起websocket连接,等待后台发送登录成功的指令过来。4.用户用应用扫码,这个时候如果已经登陆过,后台就能获取到当前用户的token,如果没有登录到系统中,需要提前做登录。5.用户在应用APP上已经显示了是否确认登录的按钮。6.用户点击确认按钮,应用APP发起后端的api调用。7.后端接收到调用,根据临时名牌向websocket模块发送当前用户的token,pc端接收到登录成功,跳转到用户个人首页。如果用户点击了取消按钮,会根据uid向websocket模块发送取消登录的指令。技术的选型1.微服务框架的选择现在比较流行的是SpringCloud和Dubbo这两个框架,RPC的微服务框架还有Motan都不错,这里我使用SpringCloud和Dubbo这两个框架,使用SpringCloud实现网关和聚合服务模块并对外提供http服务,使用Dubbo实现内部模块间的接口调用。注册中心使用Zookeeper,Zookeeper能够同时支持SpringCloud和Dubbo进行注册。2.Websocket框架选择其实Spring现在已经具备websocket的功能了,但是我没有选择使用它,因为它只是实现了websocket的基本功能,像websocket的集群,客户端的管理等等,使用spring实现的话都得从零开始写。之前就一直使用netty-socketio做websocket的开发,它具备良好的集群、客户端管理等功能,而且它本身通知支持轮询和websocket两种方式,所以选它省事省时。3.存储的选择临时令牌存放在redis中,用来进行websocket连接时的验证,防止恶意的攻击,用户数据放在mysql中。4.源码管理工具和构建工具的选择使用Git作为代码管理工具,方便进行代码持续迭代和发布上线,使用Gitlab作为源码服务器端,可以进行代码的合并管理,使整个代码质量更容易把控。采用Maven做为构建工具,并使用nexus创建自己的Maven私服,用来进行基础服务版本的管理和发布。搭建Sonar服务器,Maven中集成Sonar插件进行代码质量的自动化检测。5.持续构建和部署工具采用Docker部署的方式,快速方便。采用Jekins做持续构建,可以根据git代码变更快速的打包上线。模块功能设计根据《微服务架构:如何用十步解耦你的系统?》中微服务解耦的设计原则:1.将Websocket作为服务独立出来只用来进行数据的通信,保证其功能的单一性,独立对外提供SocketApi接口,通过Dubbo的方式来调用其服务。2.将用户功能作为服务独立出来,进行用户注册和登录的功能,并对外提供UserApi接口,通过Dubbo的方式来调用。3.对外展示的功能包括页面和静态文件都统一到WebServer模块中,需要操作用户数据或者需要使用Websocket进行通信的都统一使用Dubbo调用。4.对于基本的权限认证和动态负载均衡都统一放到Gateway模块中,Gateway可以实现http的负载均衡和websocket的负载均衡。5.如果访问量非常大时,就考虑将Gateway分开部署,单独进行http服务和websocket服务,将两者的流量解耦。6.webserver端访问量大时,可以考虑将静态页面发布到CDN中,减少该模块的负载。开发规范解耦公共服务指定良好的开发管理规范,使用Git做好版本代码的分支管理,每个需求迭代使用单独的分支,保证每次迭代都可以独立上线,Maven私服中每次SocketApi和UserApi的升级都要保留历史版本可用,Dubbo服务做好多版本的兼容支持,这样就能将基础公共的服务进行解耦。总结微服务的引入不仅仅是带来了好处,同时也带来了系统的复杂性,不能只从框架和代码的角度来考虑微服务架构的落地,更要从整个管理的角度去考虑如何括地,否则使用微服务开发只会带来更多麻烦和痛苦。长按二维码,关注公众号煮酒科技(xtech100),输入数字11返回代码哟。

December 10, 2018 · 1 min · jiezi

微服务架构:如何用十步解耦你的系统?

导言:耦合性,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。高内聚低耦合,是软件工程中的概念,是判断设计好坏的标准,主要是面向对象的设计,主要是看类的内聚性是否高,耦合度是否低。SpringCloud和Dubbo都是现在比较成熟的微服务框架,如何使用两者构建搭建你的微服务系统呢?他们是如何将你的系统解耦的?又是怎么解耦的呢?请听我慢慢道来:第一步,解耦现有模块将现有耦合在一起的模块进行重新的设计,设计成可以独立部署的多个模块,使用微服务框架很容易做到,成熟的示例代码都特别多,这里不再多讲。下面是我的微服务实现的一个架构设计图。第二步,抽取公共模块架构设计原则之一就是反向依赖,只从上往下依赖,所以,我们将公共的重复功能的模块抽取出来。必须强调一点的是,公共模块必须足够的功能单一,不能有其他业务的逻辑判断在里面。在整个模块依赖关系里,应该是一棵树状结构的关系图,而不是一个网状的关系图。1)做好代码控制笔者之前就碰到过这种问题,模块划分完了,当需求变更的时候,研发人员根本不管是不是公共模块,只要能快速完成任务,哪里改的快就在哪里改。因此,这个需要内部要做好代码的权限管理,不应该开放所有的提交代码的权限给所有的人。后来我就将公共模块的合并代码的权限收回了,合并代码需要先提交申请,代码review过才能合并代码。这就保证了公共模块代码的功能单一。2)做好版本管理公共模块被多个模块模块使用,任何代码的修改都可能会导致到正在使用的模块无法使用。这个就需要做好各个模块的版本管理,我是使用maven进行版本管理的,定义一个总的父pom项目来进行各个模块的版本管理,任何被其他模块使用的开发包都要在父pom里进行版本管理。当新的需求来了以后,需要对公共模块进行修改时,要更新模块的版本号,同时更新父pom的版本号,需要使用公共模块新功能的模块就修改父pom的版本号,不需要使用公共模块新功能的模块就不用修改父pom的版本号,这样公共模块的新老版本都能使用,即使出现问题,也只会影响到使用新版本的模块。第三步,解耦迭代需求现在的代码迭代速度快,同时会面对多个需求,有的需求紧急,有的需求不紧急,而且紧急程度可能随时会调整,如果将所有的需求都放在一个分支,当只想上线其中几个需求的时候发现无法将不上线需求的代码拆分出来,是不是很尴尬,即使能拆分出来,代码修改过以后又要重新进行部署测试,很费时费力,所以要针对不同的需求重新建立研发分支,这样就将不同需求的分支解耦,保证想上哪个就上哪个,需要上多个需求的就将分支合并上线。第四步,配置解耦为每个模块每个环境配置一个配置文件,这样就可以把不同的环境的配置解耦,不用每次上线都更新一次。但是如果需要修改数据库配置,还是需要重新部署重启应用才能解决。使用微服务的配置中心就能解决这个问题了,比如使用ZooKeeper作为SpringCloud的配置中心,修改ZooKeeper中的节点数据就可以实时更新配置并生效。第五步,权限解耦当采用微服务架构把原来的系统拆分成多个系统以后,你会发现原来简单的问题,现在变的复杂了,比如功能的权限控制,原来是跟业务代码放到一起,现在如果每个业务模块都有功能权限的代码,将是一件非常麻烦的事情。那么解决办法就是将权限功能迁移出来,恰巧使用SpringCloudGateway就能完成这件事情,SpringCloudGateway能够进行负载均衡,各种路由拦截,只要将原来的权限控制代码迁移到Gateway里实现以下就可以了,权限配置管理界面和代码逻辑都不用变。如果是API接口呢,就需要将安全验证等功能放在Gateway里实现就好了。第六步,流量解耦当你的系统访问量越来越大的时候,你会发现每次升级都是一件非常麻烦的事情,领导会跟你说这个功能忙时不能停机影响用户使用呀,只能半夜升级呀,多么痛快的事情啊。有的时候运营人员也会发现,怎么我的后台访问怎么这么慢?问题出在哪里呢?问题就出在,所有的模块都用了一个Gateway,多端同时使用了相同的流量入口,当在举行大促时,并发量非常高,带宽占用非常大,那么其他的功能也会跟着慢下来。不能在举行大促时发券时,我线下支付一直支付不了,这是非常严重的事故了,客服电话会被打爆了。所以,必须要对流量进行拆分,各个端的流量不能相互影响,比如APP端、微信端、运营后台和商户后台等都要分配独立的Gateway,并接入独立的带宽,对于流量大的端可以使用弹性带宽,对于运营后台和商户后台就比较小的固定的带宽即可。这样就大大降低了升级时的难度,是不是再上线时就没那么紧张了?第七步,数据解耦系统刚上线的时候,数据量不大,所有的模块感觉都挺好的,当时间一长,系统访问量非常大的时候会发现功能怎么都变慢了,怎么mysql的cpu经常100%。那么恭喜你,你中招了,你的数据需要解耦了。首先要模块间数据解耦,将不同模块使用独立的数据库,保证各模块之间的数据不相互影响。其次就是冷热数据解耦,同一个模块运行时间长了以后也会积累大量的数据,为了保证系统的性能的稳定,要减少因为数据量太大造成的性能降低,需要对历史数据进行定期的迁移,对于完整数据分析汇总就在其他的库中实现。第八步,扩容解耦一个好的架构设计是要有好的横向扩展的能力,在不需要修改代码只通过增加硬件的方式就能提高系统的性能。SpringCloud和Dubbo的注册中心天生就能够实现动态添加模块的节点,其他模块调用能够实时发现并请求到新的模块节点上。第九步,部署解耦互联网开发在于能够快速的试错,当一个新的版本上线时,经常是需要先让一部分用户进行测试一下,这就是传说中的灰度发布,同一个模块先部署升级几台服务器到新版本,重启完成后流量进来以后,就可以验证当前部署的这几台服务器有没有问题,就继续部署其他的节点,如果有问题马上回滚到上一个版本。使用SpringCloudGateway的WeighRouterFilter就能实现这个功能。第十步,动静解耦当同一个模块的瞬间有非常高并发的时候,对,就是说的秒杀,纯粹的流量解耦还是不够,因为不能让前面的流量冲击后面真正的下单的功能,这个时候就需要更细的流量解耦,要将静态文件的访问通通抛给CDN来解决,动态和静态之间是通过定时器来出发的,时间未到之前一直刷新的是静态的文件,当时间到了之后,生成新的js文件,告诉静态页面可以访问下单功能了。总结在模块划分时,要遵循“一个模块,一个功能”的原则,尽可能使模块达到功能内聚。事实上,微服务架构短期来看,并没有很明显的好处,甚至短期内会影响系统的开发进度,因为高内聚,低耦合的系统对开发设计人员提出了更高的要求。高内聚,低耦合的好处体现在系统持续发展的过程中,高内聚,低耦合的系统具有更好的重用性,维护性,扩展性,可以更高效的完成系统的维护开发,持续的支持业务的发展,而不会成为业务发展的障碍。———— / END / ————

December 4, 2018 · 1 min · jiezi

Spring Cloud Feign Clients 无需 Controller自动暴露Restful接口

前言在开发SpringCloud应用中,Feign作为声明式调用的事实标准极大的简化了Rest远程调用,提供了类本地化的调用方式。服务提供方的接口暴露方式是通过Controller暴露Restful,而在这个Controller的代码现实中大部分都是处理请求然后再调用Service中的方法,是一个比较模板化的功能,但是工作量确不少。本文介绍一种通过动态代理的方式无需Controller直接暴露Restful接口。本文中使用笔者在Github开源的框架来实现,本文的讲解也在这个框架基础之上来说明Git路径:https://github.com/leecho/spr…依赖 <dependency> <groupId>com.github.leecho</groupId> <artifactId>spring-cloud-starter-feign-proxy</artifactId> <version>{last-version}</version> </dependency>实现定义Service Interface首先定义服务接口,使用@FeignClient标示是一个Feign接口,在这个示例Sevice中定义了CURD和文件上传方法,并使用了一些常用参数注解@Api(tags = “DemoService”, description = “Demo Feign Client”)@FeignClient(“demo-service”)public interface DemoService { /** * create demo * * @param demo * @return / @ApiOperation(value = “Create demo”) @PostMapping(value = “/demo”) Demo create(@RequestBody @Valid @ApiParam Demo demo); /* * update demo * * @param demo * @return / @PutMapping(value = “/demo”) Demo update(@RequestBody @Valid Demo demo); /* * delete demo * * @param id * @return / @DeleteMapping(value = “/demo/{id}”) Demo delete(@PathVariable(name = “id”) String id); /* * list demo * * @param id * @param headerValue test header value * @return / @GetMapping(value = “/demo/{id}”) Demo get(@PathVariable(name = “id”) String id, @RequestHeader(name = “header”) String headerValue); /* * list demos * * @return / @GetMapping(value = “/demos”) List<Demo> list(); /* * upload file * * @param file * @return */ @PostMapping(value = “/demo/upload”) String upload(@RequestPart(name = “file”) MultipartFile file);}实现Service在实现类中简单的实现了CURD和上传文件的方法@Slf4j@Primary@Servicepublic class DemoServiceImpl implements DemoService { @Override public Demo create(Demo demo) { log.info(“Create executed : " + demo); return demo; } @Override public Demo update(Demo demo) { log.info(“Update execute :” + demo); return demo; } @Override public Demo delete(String id) { log.info(“Delete execute : " + id); return Demo.builder().name(“demo-” + id).data(“data-” + id).build(); } @Override public Demo get(String id, String header) { Demo demo = Demo.builder() .name(header) .data(header).build(); System.out.println(“Get execute : " + id + “,” + header); return demo; } @Override public List<Demo> list() { System.out.println(“List execute”); List<Demo> demos = new ArrayList<>(); for (int i = 0; i < 5; i++) { Demo demo = Demo.builder() .name(“demo-” + i) .data(“data” + i).build(); demos.add(demo); } return demos; } @Override public String upload(MultipartFile file) { return file.getOriginalFilename(); }}动态生成Restful接口的原理是动态生成Controller类实现ServiceInterface,如果真正的实现类会被其他BEAN依赖,需要通过注解@Primary来方式来防止依赖冲突配置应用在应用中通过@EnableFeignProxies来标示应用在启动的过程中会扫描所有的FeignClients并暴露Restful接口@EnableFeignProxies(basePackages = “com.github.leecho”)@ComponentScan(“com.github.leecho”)@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}测试用例在测试用例中对Service的接口进行测试@SpringBootTest@RunWith(SpringJUnit4ClassRunner.class)@WebAppConfigurationpublic class DemoApplicationTest { @Autowired private WebApplicationContext context; private MockMvc mvc; @Before public void setUp() { mvc = MockMvcBuilders.webAppContextSetup(context).build(); } @Test public void create() throws Exception { mvc.perform(MockMvcRequestBuilders.post("/demo”) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(new ObjectMapper().writeValueAsString(Demo.builder().name(“create”).data(“data”).build())) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void update() throws Exception { mvc.perform(MockMvcRequestBuilders.put("/demo”) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(new ObjectMapper().writeValueAsString(Demo.builder().name(“update”).data(“data”).build())) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void delete() throws Exception { mvc.perform(MockMvcRequestBuilders.delete("/demo/{id}”, “1”) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void get() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/demo/{id}", “1”) .contentType(MediaType.APPLICATION_JSON_UTF8) .header(“header”, “header-value”) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void list() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/demos") .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); } @Test public void upload() throws Exception { mvc.perform(MockMvcRequestBuilders.multipart("/demo/upload").file(new MockMultipartFile(“file”, “test.txt”, “,multipart/form-data”, “upload test”.getBytes())) .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); }}在启动日志中也可以看到Restful接口已经暴露成功。后语关于这个框架的介绍,后续详细的给大家进行介绍。文中所涉及到的代码也在Git中:spring-cloud-feign-proxy-sample ...

November 9, 2018 · 2 min · jiezi

SpringCloud微服务部署

微服务的其中一个特点就是有许许多的粒度小(功能单一,比如用户管理,短信发送管理,邮件发送管理,文件管理等)、能独立部署、扩展、运行的小应用,可以称为api,也就是服务提供者。api之间可以相互调用,但更多的是供app调用,比如学生管理系统,它是面向用户的,是许许多多功能的集合体,它需要调用许多api完成业务功能,所以这学生管理系统可以称为app。eureka的作用传统的单体应用开发,就是将api和app的代码全部集成在一起,在同一个进程中运行,对应java web通俗的说就是全部打包在一个war中部署在一个tomcat中运行。而微服务的每个api,app都拥有自己的进程,也就是都有自己的tomcat,某个api挂了,不影响其他api和app运行。api是采取restfull风格暴漏出去的,所以app(api)调用api时,采取http://ip:端口这形式进行通讯。那么问题来了,如果有很多app都调用了某个api,如果api的ip或端口发生了更改,如果app中对api的ip和端口等信息是写在配置文件的,难道要通知每个app,说这个api的地址和端口变了,app要进行修改重新编译部署(这是举例子而已,实际情况有些企业对常量的配置可能写配置文件,也可能写数据库)。这太麻烦了,如果api的地址和端口有发生变化,app能及时获知自行变更,那就好了。还有个弊端,就是api是否挂了,也没法直观观察到。所以,如果在app和api之间增加个服务管理中心,api像服务管理中心注册信息,app从服务管理中心获取api的信息,api有个唯一标识,api有变更的时候通知服务管理中心,服务管理中心通知相关的app或者app定时从服务管理中心获取最新的api信息,同时服务管理中心具有很高的稳定性、可靠性。eureka就是为了这样的一个服务管理中心,下面是我根据自己的理解画的一张图。下面这张是官方的架构图1.Application Service 相当于服务提供者/api2.Application Client 相当于服务消费者/app3.Make Remote Call,其实就是实现服务的使用/比如httpClient,restTemplate4.us-east-1 Eureka 集群服务5.us-east-1c、us-east-1d、us-east-1e 就是具体的某个eurekaEureka:是纯正的 servlet 应用,需构建成war包部署使用了 Jersey 框架实现自身的 RESTful HTTP接口peer之间的同步与服务的注册全部通过 HTTP 协议实现定时任务(发送心跳、定时清理过期服务、节点同步等)通过 JDK 自带的 Timer 实现内存缓存使用Google的guava包实现eureka集群搭建和eureka类似功能的有zookeeper,etcd等。spring boot已经集成了eureka,所以我们可以像spring boot那样搭建环境,部署运行。我是在window10下使用eclipse学习的。准备工作。在修改hosts文件,在最后面加上(位置:C:WindowsSystem32driversetc)127.0.0.1 01.eureka.server 127.0.0.1 02.eureka.server 127.0.0.1 03.eureka.servereclipse下创建个普通的maven项目eureka-server。pom.xml <project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven- 4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.fei.springcloud</groupId> <artifactId>springcloud-eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka服务端</description> <!– 依赖仓库 设置从aliyun仓库下载 –> <repositories> <repository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <!– 插件依赖仓库 –> <pluginRepositories> <pluginRepository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <properties> <!– 文件拷贝时的编码 –> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!– 编译时的编码 –> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath /> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope><!– 这个不能丢 –> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>启动类Application.javapackage com.fei.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }注意注解:@EnableEurekaServer,表明这是server服务配置文件application.propertieslogging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=8081 server.session-timeout=60 ########### spring.application.name=eureka-server-01 ####下面2个一定要false,因为这程序是要作为服务端但是jar中存在eureka-client.jar,所以要false,否则启动会报错的 #是否注册到eureka eureka.client.register-with-eureka=false #是否获取注册信息 eureka.client.fetch-registry=false #为了便于测试,取消eureka的保护模式,如果启动的话,比如api提供者关闭了,但是eureka仍然保留信息 eureka.server.enable-self-preservation=false #服务名称 eureka.instance.hostname=01.server.eureka #eureka的服务地址,/eureka是固定的 eureka.client.serviceUrl.defaultZone=http://02.server.eureka:8082/eureka/,http://03.server.eureka:8083/eureka/ 注意:eureka.client.serviceUrl.defaultZone的配置,如果是01,则填写02、03的地址和端口;如果是02,则填写01、03的地址和端口,也就是说让01,02,03这3个eureka服务能相互间同步数据,如果是01->02->03->01,则api提供者注册信息到01时,01会同步数据到02,但02不会同步到03,01也不会同步到03,也就是说03缺数据了。看源码,服务的注册信息不会被二次传播,看PeerAwareInstanceRegistryImpl.java启动01 eureka,然后修改application.properties,变为02 eureka的配置logging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=8082 server.session-timeout=60 ########### spring.application.name=eureka-server-02 ####下面2个一定要false,因为这程序是要作为服务端,但是jar中存在eureka-client.jar,所以要false,否则启动会报错的 #是否注册到eureka eureka.client.register-with-eureka=false #是否获取注册信息 eureka.client.fetch-registry=false #为了便于测试,取消eureka的保护模式,如果启动的话,比如api提供者关闭了,但是eureka仍然保留信息 eureka.server.enable-self-preservation=false #服务名称 eureka.instance.hostname=02.server.eureka #eureka的服务地址,/eureka是固定的 eureka.client.serviceUrl.defaultZone=http://01.server.eureka:8081/eureka/,http://03.server.eureka:8083/eureka/然后执行启动类,03也是一样的操作。浏览器访问http://01.server.eureka:8081/(或者http://02.server.eureka:8082/,或者http://03.server.eureka:8083/)看到api提供者创建个普通的maven项目eureka-api,该api是个用户服务提供者。采取spring boot开发模式pom.xml<project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven- 4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.fei.springcloud</groupId> <artifactId>springcloud-eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka服务端</description> <!– 依赖仓库 设置从aliyun仓库下载 –> <repositories> <repository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <!– 插件依赖仓库 –> <pluginRepositories> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <properties> <!– 文件拷贝时的编码 –> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!– 编译时的编码 –> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath /> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope><!– 这个不能丢 –> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 它和eureka 服务端,有个依赖是不一样的。application.propertieslogging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=9081 server.session-timeout=60 ########### spring.application.name=api-user-server #像eureka服务注册信息时,使用ip地址,默认使用hostname eureka.instance.preferIpAddress=true #服务的instance-id默认默认值是${spring.cloud.client.hostname:${spring.aplication.name}:${spring.application.instance_id:${server.port}} , #也就是机器主机名:应用名称:应用端口 eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port} #eureka的服务地址 eureka.client.serviceUrl.defaultZone=http://01.server.eureka:8081/eureka/Application.java package com.fei.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @EnableEurekaClient @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @EnableEurekaClient,不管是消费者还是提供者,对应eureka server来说都是客户端client写个普通的controller,UserProvider.java.提供个根据id获取用户信息的接口 package com.fei.springcloud.provider; import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user”) public class UserProvider { @GetMapping(value="/find/{id}”) public String find(@PathVariable(“id”) String id,HttpServletRequest request){ //实际项目中,这里可以使用JSONObject,返回json字符串 //为了便于测试消费者app的负载均衡,返回服务端端口 String s = “张三”+” 服务端端口:"+request.getLocalPort(); return s; } } 执行Application.java,将application.properties的端口修改为9082,再次执行浏览器访问http://01.server.eureka:8081/Application就是文件中定义的spring.application.name=api-user-server,它会自动转为大写如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。app消费者在Spring Cloud Netflix中,使用Ribbon实现客户端负载均衡,使用Feign实现声明式HTTP客户端调用——即写得像本地函数调用一样.ribbo负载均衡的app消费者创建个普通的maven项目eureka-app-ribbo.pom.xml<project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven- 4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.fei.springcloud</groupId> <artifactId>springcloud-eureka-app-ribbo</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka消费者ribbo</description> <!– 依赖仓库 设置从aliyun仓库下载 –> <repositories> <repository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <!– 插件依赖仓库 –> <pluginRepositories> <pluginRepository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <properties> <!– 文件拷贝时的编码 –> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><!– 编译时的编码 –> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath /> </parent> <dependencies> <!– 客户端负载均衡 –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <!– eureka客户端 –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <!– spring boot实现Java Web服务 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope><!– 这个不能丢 –> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> application.propertieslogging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=7081 server.session-timeout=60 ########### spring.application.name=app-user #像eureka服务注册信息时,使用ip地址,默认使用hostname eureka.instance.preferIpAddress=true #服务的instance-id默认默认值是${spring.cloud.client.hostname}${spring.application.name}:${spring.application.instance_id:${server.port}} , #也就是机器主机名:应用名称:应用端口 eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port} #eureka的服务地址 eureka.client.serviceUrl.defaultZone=http://01.server.eureka:8081/eureka/,http://02.server.eureka:8082/eureka/,http://03.server.eureka:8083/eureka/UserController.java logging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=7081 server.session-timeout=60 ########### spring.application.name=app-user #像eureka服务注册信息时,使用ip地址,默认使用hostname eureka.instance.preferIpAddress=true #服务的instance-id默认默认值是${spring.cloud.client.hostname} :${spring.application.name}:${spring.application.instance_id:${server.port}} , #也就是机器主机名:应用名称:应用端口 eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port} #eureka的服务地址 eureka.client.serviceUrl.defaultZone=http://01.server.eureka:8081/eureka/ ,http://02.server.eureka:8082/eureka/,http://03.server.eureka:8083/eureka/ 使用restTemplate需要自己拼接url启动类Application.javapackage com.fei.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableEurekaClient @SpringBootApplication public class Application { @Bean //定义REST客户端,RestTemplate实例 @LoadBalanced //开启负债均衡的能力 RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }eureka服务浏览器访问http://127.0.0.1:7081/user/find看到信息“张三 服务端端口:9081”,刷新浏览器看到“张三 服务端端口:9082”,说明的确是有负载均衡。但是访问外网的时候,http://127.0.0.1:7081/user/test,也就是域名不在eureka注册过的,就不行了。以后再研究下如何解决。feign的app消费者feign可以写个接口,加上相关的注解,调用的时候,会自动拼接url,调用者就像调用本地接口一样的操作。Feign也用到ribbon,当你使用@ FeignClient,ribbon自动被应用。像ribbo创建个项目,或者直接在ribbo项目修改都OK。pom.xml 把ribbo的依赖修改为feign<project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven- 4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.fei.springcloud</groupId> <artifactId>springcloud-eureka-app-feign</artifactId> <version>0.0.1-SNAPSHOT</version> <description>eureka消费者feign</description> <!– 依赖仓库 设置从aliyun仓库下载 –> <repositories> <repository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content /repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <!– 插件依赖仓库 –> <pluginRepositories> <pluginRepository> <id>alimaven</id> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <properties> <!– 文件拷贝时的编码 –> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!– 编译时的编码 –> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath /> </parent> <dependencies> <!– Feign实现声明式HTTP客户端 –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> <!– eureka客户端 –> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <!– spring boot实现Java Web服务 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope><!– 这个不能丢 –> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>application.properties和上面一样logging.config=classpath:logback.xml logging.path=d:/logs ##tomcat set### # eureka的默认端口是8761 server.port=7081 server.session-timeout=60 ########### spring.application.name=app-user #像eureka服务注册信息时,使用ip地址,默认使用hostname eureka.instance.preferIpAddress=true #服务的instance-id默认默认值是${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}} , #也就是机器主机名:应用名称:应用端口 eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port} #eureka的服务地址 eureka.client.serviceUrl.defaultZone=http://01.server.eureka8081/eureka/,http://02.server.eureka:8082/eureka/,http://03.server.eureka:8083/eureka/增加个UserService.java接口类package com.fei.springcloud.service; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(“API-USER-SERVER”) public interface UserService { @GetMapping(value="/user/find/{id}”) String find(@PathVariable(“id”) String id); }UserController.javapackage com.fei.springcloud.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import com.fei.springcloud.service.UserService; @Controller @RequestMapping("/user”) public class UserController { @Autowired private UserService userService; @GetMapping(value = “/find”) @ResponseBody public String find() { //url中对应api提供者的名称,全大写 String s = userService.find(“123”); return s; } }浏览器访问http://127.0.0.1:7081/user/find,得到信息“张三 服务端端口:9081”,刷新,得到“张三 服务端端口:9082”,Feign也用到ribbon,当你使用@ FeignClient,ribbon自动被应用。所以也会负载均衡ribbo负载均衡策略选择AvailabilityFilteringRule:过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值) | 使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个server的运行状态RandomRule:随机选择一个serverBestAvailabl:选择一个最小的并发请求的server,逐个考察Server,如果Server被tripped了,则忽略RoundRobinRule:roundRobin方式轮询选择, 轮询index,选择index对应位置的serverWeightedResponseTimeRule:根据响应时间分配一个weight(权重),响应时间越长,weight越小,被选中的可能性越低RetryRule:对选定的负载均衡策略机上重试机制,在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的serverZoneAvoidanceRule:复合判断server所在区域的性能和server的可用性选择serverResponseTimeWeightedRule:作用同WeightedResponseTimeRule,二者作用是一样的,ResponseTimeWeightedRule后来改名为WeightedResponseTimeRule在app消费者的application.properties配置文件中加入#ribbo负载均衡策略配置,默认是依次轮询 API-USER-SERVER.ribbon.NFLoadBalancerRuleClassName=com. netflix.loadbalancer.RandomRule其中API-USER-SERVER是api服务提供者的服务名称,也就是说,可以给每个不同的api服务提供者配置不同的复制均衡策略,验证就不贴图了负载均衡策略在消费端配置的缺点在上面的例子中,ribbon的负载均衡是在消费端完成的。流程是这样的:提供者服务A集群,启动2个进程A1,A2,都注册到eureka,app消费端根据api服务者名称获取到A1,A2的具体连接地址,ribbon就对A1,A2进行负载均衡。缺点:1) 如果所有的app消费端的配置策略不好,导致绝大部分的请求都到A1,那A1的压力就大了。也就是说负载策略不是有api提供者所控制的了(这里就不说A1,A2所在的服务器哪个性能更好了,因为如果app/api都是在Docker中运行,k8s负责资源调配的话,可以认为每个服务的进程所在的docker配置是一样的,比如A服务对应的A1,A2系统环境是一致的,B服务对应的B1,B2,B3系统环境是一致的,只是所对应的宿主机服务器估计不一样而已)。2)如果api提供者需要开放给第三方公司的时候,总不能把A1,A2告诉第三方吧,说我们这A服务集群了,有A1,A2,你随意吧。我们实际项目中的做法,都是每个提供者服务都有自己的nginx管理自己的集群,然后把nginx的域名提供给app消费者即可。之所以每个服务提供者都有自己的nginx,是因为docker被k8s管控的时候,ip都是变化的,需要更新到nginx中。如果A,B都共用一个nginx,那A重构建部署的时候,A1,A2的ip变化了,需要更新到nginx中去,那如果导致nginx出现问题了,岂不是影响到B的使用了,所以A,B都有自己的nginx。那spring cloud没有解决方案了吗?有。spring cloud集成了zuul,zuul服务网关,不但提供了和nginx一样的反向代理功能,还提供了负载均衡、监控、过滤等功能,在后面学习zuul的时候,我们再学习。如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。docker+k8s实现的devOps平台(paas平台),构建好镜像后,部署的时候,k8s负责调控资源,将docker分配到不同的节点服务器,同时将docker的ip相关信息更新到nginx。这是自动化的,不需要开发人员手动操作docker,nginx配置。 ...

October 26, 2018 · 4 min · jiezi