微服务通用测试接口实践

951 阅读4分钟

本文已参与「新人创作礼」活动.一起开启掘金创作之路。

微服务通用测试接口实践

一、需求

众多微服务已经部署在线上,如何快速确定某个微服务运行状态是否ok?除了查看consul中注册的微服务实例是否通过健康检查,本文试图给每个微服务提供一个通用测试接口,以便测试微服务接口可用性

二、目标:

  • 提供简单helloworld接口,返回简单字符串
  • 提供sleep接口,根据传入url参数,睡眠等待响应时间后返回简单字符串,以便测试超时情况
  • 提供exception接口,模拟微服务接口抛出异常时处理情况
  • 提供call接口,根据传入参数,动态调用目标微服务的上述接口

 

三、实战

  1. 新建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(200010000))
                .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参数来实现动态调用
  1. 实现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来实现

  1. 增加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();
    }

}
  1. 发布web-common

mvn clean install -DskipTests

mvn clean deploy -DskipTests

  1. 目标微服务添加对该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. 测试

(1)启动application和appointment(为了模拟application动态调用appointment)

image.png

(2) 浏览器访问 http://localhost:8080/swagger-ui.html

(3) 通过swagger测试接口

三、Todo

  • 网关增加debug接口白名单