本文已参与「新人创作礼」活动.一起开启掘金创作之路。
微服务通用测试接口实践
一、需求
众多微服务已经部署在线上,如何快速确定某个微服务运行状态是否ok?除了查看consul中注册的微服务实例是否通过健康检查,本文试图给每个微服务提供一个通用测试接口,以便测试微服务接口可用性
二、目标:
- 提供简单helloworld接口,返回简单字符串
- 提供sleep接口,根据传入url参数,睡眠等待响应时间后返回简单字符串,以便测试超时情况
- 提供exception接口,模拟微服务接口抛出异常时处理情况
- 提供call接口,根据传入参数,动态调用目标微服务的上述接口
三、实战
- 新建lib module:名字为:web-common
略
2. 实现controller:src/main/java/cn/ucmed/otaku/healthcare/web/DebugController.java
package cn.ucmed.otaku.healthcare.web;
import cn.ucmed.common.rubikexception.BusinessException;
import feign.*;
import feign.codec.Decoder;
import feign.codec.Encoder;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@Api(value = "debug api", description = "debug api")
@RestController
@RequestMapping("/debug")
@Import(FeignClientsConfiguration.class)
@Slf4j
public class DebugController {
@Value("${server.port}")
private int port;
@Value("${spring.application.name}")
private String springApplicationName;
DebugInterface debugInterface;
@Autowired
public DebugController(Decoder decoder, Encoder encoder, Client client) {
debugInterface = Feign.builder()
.options(new Request.Options(2000, 10000))
.retryer(Retryer.NEVER_RETRY)
.client(client)
.encoder(encoder)
.decoder(decoder)
.target(Target.EmptyTarget.create(DebugInterface.class));
}
@GetMapping(value = "/call")
@ResponseBody
@ApiOperation(value = "动态调用其它微服务的debug方法", notes = "动态调用其它微服务的debug方法")
public String call(@RequestParam(value = "service") String service, @RequestParam(value = "method") String method, @RequestParam(value = "param1", required = false, defaultValue = "3000") String param1) throws URISyntaxException {
if (method.equalsIgnoreCase("helloworld")) {
return debugInterface.helloworld(new URI("http://" + service + "/debug/helloworld"));
} else if (method.equalsIgnoreCase("sleep")) {
return debugInterface.helloworld(new URI("http://" + service + "/debug/sleep?ms=" + param1));
} else if (method.equalsIgnoreCase("exception")) {
return debugInterface.helloworld(new URI("http://" + service + "/debug/exception?e=" + param1));
}
return getServiceDesc() + "invalid method";
}
@GetMapping(value = "/helloworld")
@ResponseBody
@ApiOperation(value = "helloworld", notes = "helloworld")
public String helloworld() {
return getServiceDesc() + " helloworld";
}
@GetMapping(value = "/sleep")
@ResponseBody
@ApiOperation(value = "sleep", notes = "sleep")
public String sleep(@RequestParam(value = "ms", defaultValue = "2000") String ms) {
int milliSenocds = Integer.parseInt(ms);
if (milliSenocds < 10) {
milliSenocds = 10;
}
if (milliSenocds > 60000) {
milliSenocds = 60000;
}
log.info("before sleep(): " + milliSenocds);
try {
Thread.sleep(milliSenocds);
} catch (InterruptedException e) {
log.info("sleep InterruptedException");
}
log.info("after sleep(): " + milliSenocds);
return getServiceDesc() + ", sleep milliseconds: " + milliSenocds;
}
@GetMapping(value = "/exception")
@ResponseBody
public String exception(@RequestParam(value = "e", defaultValue = "") String e) throws IOException {
if ("io".equalsIgnoreCase(e)) {
throw new IOException("IOException");
} else if ("be".equalsIgnoreCase(e)) {
throw new BusinessException(1, "BusinessException");
} else if ("re".equalsIgnoreCase(e)) {
throw new RuntimeException("RuntimeException");
} else if ("null".equalsIgnoreCase(e)) {
e = null;
e.equals(e);
}
return getServiceDesc() + " no exception";
}
private String getServiceDesc() {
return "service: " + springApplicationName + ":" + port;
}
}
注:
- call接口比较特别,它需要动态调用其它微服务的接口,因此,这里通过自己实例化feign接口,并通过向对应的接口传递url参数来实现动态调用
- 实现feign接口:src/main/java/cn/ucmed/otaku/healthcare/web/DebugInterface.java
package cn.ucmed.otaku.healthcare.web;
import feign.RequestLine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FeignClient;
import java.net.URI;
@FeignClient(
path = "/debug",
fallback = DebugInterface.DebugInterfaceFallback.class
)
public interface DebugInterface {
// @GetMapping(value = "/helloword")
@RequestLine("GET")
public String helloworld(URI baseUri);
// @GetMapping(value = "/sleep")
@RequestLine("GET")
public String sleep(URI baseUri, String ms);
@RequestLine("GET")
public String exception
(URI baseUri, String ms);
@Slf4j
class DebugInterfaceFallback implements DebugInterface {
@Override
public String helloworld(URI baseUri) {
log.error("helloworld fallback");
return null;
}
@Override
public String sleep(URI baseUri, String ms) {
log.error("sleep fallback");
return null;
}
@Override
public String exception(URI baseUri, String ms) {
log.error("exception fallback");
return null;
}
}
}
注:
该feign接口并没有指定value值,即没有绑定特定的微服务,是因为我们通用测试接口需要动态调用其它微服务测试接口,具体结合DebugController.java来实现
- 增加swagger配置:src/main/java/cn/ucmed/otaku/healthcare/web/Swagger2Config.java
package cn.ucmed.otaku.healthcare.web;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean(name = "debugApi")
public Docket createDebugApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("debug")
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("cn.ucmed.otaku.healthcare.web"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("[debug]通用测试接口")
.description("debug api文档")
.termsOfServiceUrl("hostname").contact(new Contact("卓健科技", "http://www.zwjk.com", "open@zwjk.com"))
.version("1.0")
.build();
}
}
- 发布web-common
mvn clean install -DskipTests
或
mvn clean deploy -DskipTests
- 目标微服务添加对该common库的依赖,以application和appointment为例
(1) pom.xml:
<dependency>
<groupId>cn.ucmed.otaku.healthcare</groupId>
<artifactId>web-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
(2)Applicaiton.java:
package cn.ucmed.otaka.healthcare;
import cn.ucmed.common.cache.RequestData;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;
import tk.mybatis.spring.annotation.MapperScan;
import java.util.Arrays;
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = {"cn.ucmed.otaka.healthcare","cn.ucmed.otaku.healthcare","cn.ucmed.healthcare"})
@EnableAutoConfiguration
@EnableApolloConfig({"application","zsyy.common"})
@MapperScan(basePackages = "cn.ucmed.otaka.healthcare.rubik.dao.mapper")
@EnableFeignClients({"cn.ucmed.healthcare"})
@EnableTransactionManagement
@EnableHystrix
@EnableCircuitBreaker
@Slf4j
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {
log.info("Let's inspect the beans provided by Spring Boot:");
String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
log.info(beanName);
}
};
}
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
@Bean(name = "requestData")
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public RequestData getRequestData() {
return new RequestData();
}
}
注:
- 因为web-common包的包名和微服务包名不同,需要这个注解添加扫描包名:
@SpringBootApplication(scanBasePackages = {"cn.ucmed.otaka.healthcare","cn.ucmed.otaku.healthcare","cn.ucmed.healthcare"})
以便能够把web-common中controller扫描进来
- 所有微服务都需要做上述改动
- 测试
(1)启动application和appointment(为了模拟application动态调用appointment)
(2) 浏览器访问 http://localhost:8080/swagger-ui.html
(3) 通过swagger测试接口
略
三、Todo
- 网关增加debug接口白名单