微服务使用

微服务架构

Microservice架构模式就是将整个Web应用组织为一系列小的Web服务。这些小的Web服务可以独立地编译及部署,并通过各自暴露的API接口相互通讯。它们彼此相互协作,作为一个整体为用户提供功能,却可以独立地进行扩容。

spring cloud

Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等。

基础环境搭建

目前微服务基础环境由三部分组成,包括服务中心、配置中心和网关。

服务中心用来有效管理服务的注册信息;配置中心用来集中管理各服务的配置信息;网关统一向外系统提供REST API服务。

使用的Spring Boot版本为 1.5.2.RELEASE。

服务中心 【Netflix Eureka】

服务注册与发现,用到的是Spring Cloud Netflix,主要内容是对Netflix公司一系列开源产品的包装,它为Spring Boot应用提供了自配置的Netflix OSS整合。通过一些简单的注解,开发者就可以快速的在应用中配置一下常用模块并构建庞大的分布式系统。它主要提供的模块包括:服务发现(Eureka),断路器(Hystrix),智能路由(Zuul),客户端负载均衡(Ribbon、Feign)等。

创建一个基础的Spring Boot工程,并在pom.xml中引入需要的依赖内容:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>

在的Spring Boot应用中添加@EnableEurekaServer注解启动服务注册中功能。

1
2
3
4
5
6
7
@EnableEurekaServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}

在默认设置下,该服务注册中心也会将自己作为客户端来尝试注册它自己,所以我们需要禁用它的客户端注册行为,在application.properties添加ureka.client.register-with-eureka=falseeureka.client.fetch-registry=false。完整的application.properties配置信息如下:

1
2
3
4
server.port=8081
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/

启动后,服务中心地址http://host:port/

配置中心 【Spring Cloud Config】

Spring Cloud Config为服务端和客户端提供了分布式系统的外部化配置支持。配置服务器为各应用的所有环境提供了一个中心化的外部配置。配置服务器默认采用git来存储配置信息,这里我们使用mongodb来存储配置信息(更多信息)

创建Config Server工程,在pom.xml中引入

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>

创建Spring Boot的程序主类,并添加@EnableConfigServer注解,开启Config Server

1
2
3
4
5
6
7
@EnableConfigServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}

继承org.springframework.cloud.config.server.environment.EnvironmentRepository类,重写findOne方法。

1
2
3
4
@Override
public Environment findOne(String application, String profile, String label) {
//TODO
}

网关 【Netflix Zuul】

通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

引入依赖spring-cloud-starter-zuul、spring-cloud-starter-eureka。

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

应用主类使用@EnableZuulProxy注解开启Zuul

1
2
3
4
5
6
7
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}

@SpringCloudApplication注解整合了@SpringBootApplication@EnableDiscoveryClient@EnableCircuitBreaker

配置zuul通过serviceId映射,将网关注册到服务中心。

eureka.client.serviceUrl.defaultZone=http://host:port/eureka/

自定义过滤器,继承ZuulFilter,需要实现四个方法:

filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:

pre:可以在请求被路由之前调用
routing:在路由请求时候被调用
post:在routing和error过滤器之后被调用
error:处理请求时发生错误时被调用

filterOrder:通过int值来定义过滤器的执行顺序

shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回true,所以该过滤器总是生效。

run:过滤器的具体逻辑。需要注意,这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。

实现了自定义过滤器之后,实例化该过滤器使其生效。

开发者使用

项目层次结构

在顶层项目KOALA下面,创建对应的微服务module,命名规范为koala-${servicename}。下面是服务提供者和服务消费者,命名规范为koala-${servicename}-servicekoala-${servicename}-sharelib。具体结果如下图所示:

其依赖关系如下图所示:

服务注册

将koala-servicename-service注册到服务中心。

在pom.xml中引入maven依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在启动类中引入@EnableDiscoveryClient注解

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
public class DisasterServerApp {

public static void main(String[] args) {
SpringApplication.run(DisasterServerApp.class, args);
}

}

配置application.properties

1
2
3
spring.application.name:注册的微服务SERVICEID
spring.cloud.config.profile:配置文件profile属性,可以用来区分环境
eureka.client.serviceUrl.defaultZone:微服务注册中心地址

使用客户端负载均衡

1
2
3
4
5
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}

服务配置

将微服务配置信息写入配置中心。

在pom中引入maven依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

将application.properties修改成bootstrap.properties,并加入下列配置项

spring.cloud.config.label:服务端依赖的资源文件标记
spring.cloud.config.discovery.enabled:使用注册中心生效
spring.cloud.config.discovery.serviceId=注册中心SERVICEID

将配置信息写入配置中心

目前只能通过接口写入,写入的json格式实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"application": "SERVICEID-1",
"profile": "MICROSERVICE",
"label": "MASTER",
"properties": {
"postgre.driverClass": "org.postgresql.Driver",
"postgre.url": "jdbc:postgresql://10.10.10.10:5432/pql",
"postgre.user": "username",
"postgre.password": "password",
"postgre.initialPoolSize": "1",
"postgre.minPoolSize": "1",
"postgre.maxPoolSize": "2",
"postgre.acquireIncrement": "1",
"postgre.maxIdleTime": "60",
"postgre.acquireRetryAttempts": "1",
"postgre.acquireRetryDelay": "5000",
"postgre.breakAfterAcquireFailure": "false"
}
}

health组件配置

health组件为公司内部基础组件,目前最新版本为:1.0.5。health的主要作用是将app注册到zookeeper中。报警程序通过监控zookeeper上注册的app状态及health接口判断app是否健康存活。

引入health的maven依赖

1
2
3
4
5
<dependency>
<groupId>com.mlog</groupId>
<artifactId>sharelibs-health</artifactId>
<version>1.0.5</version>
</dependency>

配置appinfo.properties

1
2
3
4
5
6
7
8
9
10
productName:项目组名
appGroup:服务组名
appName:服务名称
ip:注册IP
port:服务端口
frameWork:项目框架
language:开发语言
register.zk.servers:zookeeper集群地址
baseSleepTimeMs:注册间隔时间
maxRetries:重试次数

通过javaagent方式启动

1
2
3
4
if [ -f lib/sharelibs-health*.jar ];then
healthAgent=`ls -lrt lib/sharelibs-health-*.jar |awk -F" " '{print $NF}'`
AGENT_OPTS="$AGENT_OPTS -javaagent:$healthAgent"
fi

exception组件配置

exception为公司内部基础组件,目前最新版本为:1.0.2。exception组件的主要作用是为服务端提供统一的异常处理机制。目前有
AuthorityException:权限异常DataException:数据异常ParamException:参数异常RequestException:请求异常ServerException:服务异常五种异常类型。

引入exception的maven依赖

1
2
3
4
5
<dependency>
<groupId>com.mlog</groupId>
<artifactId>sharelibs-exception</artifactId>
<version>1.0.2</version>
</dependency>

编写BaseController类继承ServerExceptionHandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BaseController extends ServerExceptionHandle {

/**
* 异常处理
*
* @param ex
* @param response
* @return
*/
@ExceptionHandler
@ResponseBody
public Map<String, Object> exceptionHandle(Exception ex, HttpServletResponse response) {
if ("MissingServletRequestParameterException".equals(ex.getClass().getSimpleName())) {
ex = ParamException.build(ParamError.ParamLosedError);
} else if ("MethodArgumentTypeMismatchException".equals(ex.getClass().getSimpleName())) {
ex = ParamException.build(ParamError.ParamTypeError);
}
return super.exceptionHandle(ex, response);
}

}

controller继承BaseController,并在rest-api方法中抛出异常

可以通过五个异常类的build方法创建并抛出异常。

1
2
DataException.build(DataError.StatusError);
DataException.build(DataError.StatusError, "指定范围内不存在强对流天气");

log组件配置

mlog-log组件为公司内部基础组件,目前最新版本为1.0.6。log组件的作用是将日志及异常信息用logstash抓取到指定目录,提供日志的可视化检索。

引入log的maven依赖

1
2
3
4
5
<dependency>
<groupId>com.mlog.log</groupId>
<artifactId>mlog-log</artifactId>
<version>1.0.6</version>
</dependency>

配置log4j.properties配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#root
log4j.rootLogger= INFO, console

#your app conf
log4j.logger.com.mlog.koala.aqi=INFO, console, file
log4j.additivity.com.mlog.koala.aqi=false

#console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Threshold=INFO
log4j.appender.console.layout=com.mlog.log.layout.EasyPatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p [%8t] %50.50c : %m%n

#file to be caught
log4j.appender.file=com.mlog.log.appender.RollingFileAppender
log4j.appender.file.Threshold=info
log4j.appender.file.layout=com.mlog.log.layout.NullPatternLayout

#your log default product and app
mlog.product:项目名
mlog.app:app名字
mlog.flag:app分类
#your error uncaught to
mlog.err.dir:错误日志列印位置
#your log file to
mlog.log.dir:日志列印位置
#alertcenter http url
mlog.alertcenter.url:服务地址

获取日志对象

1
private final static Logger logger = LogManager.getLogger(Class.class)

swagger文档

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。

在pom.xml中引入swagger的maven依赖

1
2
3
4
5
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.2.2</version>
</dependency>

实例化Docket对象

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
.apis(RequestHandlerSelectors.basePackage("com.mlog.koala.disaster.controller"))
.paths(PathSelectors.any()).build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("强对流天气临近预报查询在线API文档").description("由@象辑科技发布")
.license("Apache 2.0").licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.termsOfServiceUrl("http://www.mlogcn.com/").contact("象辑科技").version("1.0.1").build();
}

swagger注解简介

@Api

value

controller的描述信息;

tags

controller的名称;

示例:

1
2
@Api(value = "提供用户基本信息的增删改查操作接口", tags = { "用户接口" })
public class UserController {}
@ApiOperation

produces

返回的格式类型

consumes

接收的格式类型

value

主要描述信息

notes

详细描述信息

示例:

1
2
@ApiOperation(value = "获取用户列表", notes = "获取当前数据库中存储的全部用户信息", produces="application/json", consumes = "application/json")
public List<User> getAllUserList();
@ApiImplicitParams @ApiImplicitParam

paramType

有3种类型,query、path、body;分别对应spring中的@RequestParam 、@PathVariable 、@RequestBody

name

参数名称

value

参数描述信息

defaultValue

默认值

required

是否必传

dataType

数据类型

allowableValues

允许输入的值;目前好像只针对String类型的有效

示例

1
2
3
4
5
@ApiImplicitParams({
@ApiImplicitParam(allowableValues = "zhangsan, lisi", paramType = "query", name = "username", value = "用户姓名", defaultValue = "zhangsan", required = true, dataType = "String"),
@ApiImplicitParam(paramType = "path", name = "id", value = "用户ID", defaultValue = "1001", required = true, dataType = "Long"),
@ApiImplicitParam(paramType = "body", name = "user", value = "用户详细信息", required = true, dataType = "User") })
public String updateUser(@RequestParam(name = "username") String username, @PathVariable Long id, @RequestBody User user);
@ApiParam

同@ApiImplicitParams @ApiImplicitParam,不过更简化,推荐使用。

1
@ApiParam("开始时间,格式yyyyMMddHHmm")
@ApiResponses @ApiResponse

code

httpCode编码

message

描述信息

response

返回数据类型

responseContainer

返回数据类型容器,可以选择 List Set Map

示例

1
2
3
4
@ApiResponses(value = {@ApiResponse(code = 400, message = "参数异常", response = ErrorBean.class),
@ApiResponse(code = 500, message = "服务异常", response = ErrorBean.class),
@ApiResponse(code = 401, message = "权限异常", response = ErrorBean.class),
@ApiResponse(code = 404, message = "数据异常", response = ErrorBean.class)})
@ApiModelProperty

实体类属性上使用的注解,建议只使用value属性。

value

属性列描述信息

@ApiIgnore

用来标注不使用swagger的API方法。

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/mete/disaster/forecast")
@Api(value = "DISASTER", tags = {"强对流天气临近预报查询API"})
public class DisasterDataController extends BaseController {
@Autowired
private IDisasterDataService disasterDataService;

@ApiOperation(value = "雷电预报全量查询", produces = "application/json",
notes = "查询输入的时间区间(闭区间)范围内全部的雷电预报数据")
@ApiResponses(value = {@ApiResponse(code = 400, message = "参数异常", response = ErrorBean.class),
@ApiResponse(code = 500, message = "服务异常", response = ErrorBean.class),
@ApiResponse(code = 401, message = "权限异常", response = ErrorBean.class),
@ApiResponse(code = 404, message = "数据异常", response = ErrorBean.class)})
@RequestMapping(value = "/thunder/all", method = RequestMethod.GET)
public List<DisasterItem> queryAllThunderDisaster(
@ApiParam("开始时间,格式yyyyMMddHHmm") @RequestParam("start") @DateTimeFormat(
pattern = "yyyyMMddHHmm") Date startTime,
@ApiParam("结束时间,格式yyyyMMddHHmm") @RequestParam("end") @DateTimeFormat(
pattern = "yyyyMMddHHmm") Date endTime)
throws Exception {
return disasterDataService.queryAllDisaster("thunder", startTime, endTime);
}
}

generator代码生成

generator是专门为微服务生成对应的java SDK的,目前最新版本为1.1.0。

引入generator及feign的maven依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>com.mlog.koala</groupId>
<artifactId>koala-generator</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

编写生成类MicroServiceGenerator

需要配置的4个参数:

appName:微服务的SERVICEID;

scanPackage:需要扫描的controller的包路径;

targetPackage:需要生成的SDK service路径;

creatPath:SDK service生成的跟目录;

完整示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class MicroServiceGenerator {

public static void main(String[] args) throws IOException {
String appName = "DISASTERSERVICE-1";
String scanPackage = "com.mlog.koala.disaster.controller";
String targetPackage = "com.mlog.koala.disaster.service";
String filePath = System.getProperty("user.dir");
String creatPath = filePath.replaceAll("service", "sharelib");
SDKGreneratorServer server = new SDKGreneratorServer(appName, scanPackage, targetPackage, creatPath);
server.creat();
}
}

消费者使用

目前微服务支持以下两种调用方式:

通过ServiceID走微服务网关调用;通过java SDK调用。

网关调用

通过SERVICEID及服务的url地址,从网关中调用rest-api服务。
调用规则为

1
http://api-gateway/SERVICEID/VERSION/path

如:
假设微服务网关host地址为 http://10.10.10.10:8080,
微服务A的SERVICEID为:APP-1,其中有一个接口地址为 /hello/world,那么网关的调取地址为:

http://10.10.10.10:8080/APP/v1/hello/world

注意:为了规范url调取路径,serviceid和版本将统一转换为小写

java SDK调用

在pom.xml中引入对应的微服务发布的SDK包(sharelib),注意不能使用@ComponentScan扫描Feigin接口
// @EnableFeignClients(basePackageClasses = NationStationService.class)
@EnableFeignClients(basePackages = “com.mlog.koala.meta.service*”)
@EnableEurekaClient

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableEurekaClient
// @EnableFeignClients(basePackageClasses = NationStationService.class)
@EnableFeignClients(basePackages = "com.mlog.koala.meta.service*")
public class TestMicroApp {

public static void main(String[] args) {
SpringApplication.run(TestMicroApp.class, args);
}
}

在配置文件中加入服务中心的地址,

1
eureka.client.serviceUrl.defaultZone

自动注入Service接口对象

1
2
@Autowired
private NationStationService nationStationService;