前言
用惯了spring全家桶之后,试试dropwizard的Hello World也别有一帆风味。为了增强对外访问API的能力,需要引入open feign。这里简单在dropwizard中使用feign。
1. 什么Dropwizard
Dropwizard is a Java framework for developing ops-friendly, high-performance, RESTful web services.
Dropwizard
使成熟、稳定的java生态系统更加简单、轻量(light-weight), 让你更专注于业务逻辑。
Dropwizard 为配置(configuration)、统计(application metrics)、日志(logging)、operational tools提供了开箱即用的能力。让您和您的团队能够在最短的时间内开发出具有生产环境的质量的Web服务。
下面的简介来自REST微服务架构之Dropwizard
DropWizard是由Yammer开发团队贡献的一个后台服务开发框架,其集成了Java生态系统中各个问题域中最优秀的组件,帮助开发者快速的打造一个Rest风格的后台服务。
对开发者来说,使用DropWizard有如下好处:
1、和Maven集成良好,也就是说和Gradle集成也很良好;
2、开发迅速,部署简单;
3、代码结构好,可读性高;
4、自动为服务提供OM框架;
5、让开发者自然的把一个应用拆分为一个个的小服务
DropWizard结构的Web服务组成
1、Configuration:用于设置该服务的配置,比方说在服务开放在哪个端口,数据库配置是怎样的等等。
2、Application(即Service):该服务的主入口,定义该服务使用哪个配置文件,开放哪些Resource,该服务需要哪些HealthCheck等等。
3、Resource:定义一个资源,包括如何获取该资源,对该资源做Get/Post/Delete/Query时,对应的各种业务逻辑。
4、Representation:定义了一个服务返回值对象,当服务返回该对象时,会自动的把该对象按属性值生成一个Json格式的字符串返回给服务调用者。
5、HealthCheck:在DropWizard为每个服务提供的OM框架中用到,通过它可以随时检测当前服务是否可用。
Dropwizard内置了Jetty
Web应用程序不能没有HTTP,所以Dropwizard使用Jetty HTTP库将一个令人难以置信的HTTP服务器直接嵌入到您的项目中。 Dropwizard项目不需要将应用程序交给一个复杂的应用程序服务器,而是一个main
方法,它会自动连接一个HTTP服务器。将应用程序作为一个简单的过程运行,消除了Java在生产中的一些不好的东西(没有PermGen问题,没有应用程序服务器配置和维护,没有复杂的部署工具,没有类加载器(class loader)故障,没有隐藏的应用程序日志,没有尝试调整一个垃圾收集器来处理多个应用程序工作负载),并允许您使用所有现有的Unix进程管理工具。
Dropwizard 使用Jersey提供Rest能力
Dropwizard 使用Jackson来处理json
Dropwizard 提供了Metrics类库
2. Hello World For Dropwizard
吹完牛逼,开始干活。
照例,首先本次测试(https://github.com/Ryan-Miao/l4dropwizard)的完整结构图如下:
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 29 30 31 32 33 34
| . ├── dependency-reduced-pom.xml ├── l4dropwizard.iml ├── pom.xml ├── readme.md └── src └── main ├── java │ └── com │ └── test │ ├── HelloWorldApplication.java │ ├── configuration │ │ ├── HelloWorldConfiguration.java │ │ └── modules │ │ ├── ConnectAndReadConfig.java │ │ └── GithubApiConfig.java │ └── domain │ ├── connect │ │ ├── GithubClient.java │ │ └── GithubConnector.java │ ├── entiry │ │ ├── GithubUser.java │ │ └── Saying.java │ ├── health │ │ └── TemplateHealthCheck.java │ └── resource │ ├── GithubResource.java │ └── HelloWorldResource.java └── resources └── config └── dev.yml
14 directories, 16 files
|
2.1 添加依赖
依旧是maven项目,pom中添加dropwizard
1 2 3 4 5 6 7 8 9 10 11 12 13
| <properties> <dropwizard.version>1.0.6</dropwizard.version> <java.version>1.8</java.version> <mainClass>com.test.HelloWorldApplication</mainClass> </properties> <dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>
|
2.2 添加配置中心
dropwizard采用yaml作为配置文件,同时需要有个配置类对应yaml中的属性。
创建config/dev.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| template: Hello, %s! defaultName: Stranger
server: # softNofileLimit: 1000 # hardNofileLimit: 1000 applicationConnectors: - type: http port: 8080
#this requires the alpn-boot library on the JVM's boot classpath #- type: h2 # port: 8445 # keyStorePath: example.keystore # keyStorePassword: example adminConnectors: - type: http port: 8082
|
然后,新建对应的配置类com.test.configuration.HelloWorldConfiguration
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 29 30 31 32 33 34 35 36 37 38
| package com.test.configuration;
import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.Configuration; import org.hibernate.validator.constraints.NotEmpty;
public class HelloWorldConfiguration extends Configuration {
@NotEmpty private String template;
@NotEmpty private String defaultName = "Stranger";
@JsonProperty public String getTemplate() { return template; }
@JsonProperty public void setTemplate(String template) { this.template = template; }
@JsonProperty public String getDefaultName() { return defaultName; }
@JsonProperty public void setDefaultName(String name) { this.defaultName = name; } }
|
下一步就是启动类:com.test.application.HelloWorldApplication
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| package com.test;
import com.test.domain.health.TemplateHealthCheck; import com.test.domain.resource.HelloWorldResource; import com.test.configuration.HelloWorldConfiguration; import io.dropwizard.Application; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment;
import java.util.Map;
public class HelloWorldApplication extends Application<HelloWorldConfiguration> {
public static void main(String[] args) throws Exception { new HelloWorldApplication().run(args); }
@Override public String getName() { return "hello-world"; }
@Override public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) { }
@Override public void run(HelloWorldConfiguration configuration, Environment environment) throws Exception { final HelloWorldResource resource = new HelloWorldResource( configuration.getTemplate(), configuration.getDefaultName() ); final TemplateHealthCheck healthCheck = new TemplateHealthCheck(configuration.getTemplate()); environment.healthChecks().register("template", healthCheck); environment.jersey().register(resource); environment.jersey().register(healthCheck);
} }
|
到此,配置基本完成,只需要添加接口resource
就好。
2.3 创建第一个API
对应于springmvc中conroller, dropwizard采用jersey,使用resourc作为接口类:com.test.com.test.resource.HelloWorldResource
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 29 30 31 32 33 34 35 36 37 38 39
| package com.test.domain.resource;
import com.codahale.metrics.annotation.Timed; import com.test.domain.entiry.Saying;
import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong;
@Path("/hello-world") @Produces(MediaType.APPLICATION_JSON) public class HelloWorldResource { private final String template; private final String defaultName; private final AtomicLong counter;
public HelloWorldResource(String template, String defaultName) { this.template = template; this.defaultName = defaultName; this.counter = new AtomicLong(); }
@GET @Timed public Saying sayHello(@QueryParam("name") Optional<String> name) { final String value = String.format(template, name.orElse(defaultName)); return new Saying(counter.incrementAndGet(), value); }
}
|
这里的template没啥意思,官网用在这里就是为了彰显下读取配置文件的能力: 通过configuration类来操作配置属性。
另外,需要注意的是,resource并不能像Spring一样自动扫描,需要手动去environment.jersey().register(resource);
。
2.4 启动
启动前还需要配置fat jar,同Spring-boot一样,fat jar首选. 在pom配置:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> </manifest> </archive> </configuration> </plugin>
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</version> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>${mainClass}</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.6.0</version> <configuration> <mainClass>${mainClass}</mainClass> <arguments> <argument>server</argument> <argument>target/classes/config/dev.yml</argument> </arguments> <systemProperties> <systemProperty> <key>application.name</key> <value>HelloWorld</value> </systemProperty> <systemProperty> <key>application.home</key> <value>.</value> </systemProperty> <systemProperty> <key>application.environment</key> <value>dev</value> </systemProperty> </systemProperties> </configuration> </plugin> </plugins> </build>
|
接下来,打包:
然后,run jar:
1
| java -jar target\l4dropwizard-1.0-SNAPSHOT.jar server target/classes/config/dev.yml
|
浏览器访问http://localhost:8080/hello-world?name=Ryan
将得到:
1 2 3 4
| { "id": 1, "content": "Hello, Ryan!" }
|
至此,hello world完成。
什么是Feign
Feign是一个网络请求客户端,简化了网络请求代码,使得我们可以采用更加友好的方式发送请求,并且管理请求。Feign采用注解驱动模板,所以目前只支持text-based apis.
Dropwizard with Feign
依赖
首先,添加依赖:
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 29 30 31
| <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-core</artifactId> <version>${feign.version}</version> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-hystrix</artifactId> <version>${feign.version}</version> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-slf4j</artifactId> <version>${feign.version}</version> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-jackson</artifactId> <version>${feign.version}</version> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-gson</artifactId> <version>${feign.version}</version> </dependency> <dependency> <groupId>io.reactivex</groupId> <artifactId>rxjava</artifactId> <version>1.2.1</version> <scope>compile</scope> </dependency>
|
配置
Feign的配置主要有三个,一个是isolation.thread
线程存活时间。一个是connectTimeoutMillis
连接超时,一个是readTimeoutMillis
。
本次测试将采用github的公共API,获取用户信息。首先配置线程存活时间。在dev.yml中添加:
1 2
| hystrixConfig: hystrix.command.GithubConnector#getUserProfile(String).execution.isolation.thread.timeoutInMilliseconds: 7000
|
然后是两个超时配置:
1 2 3 4 5
| githubApiConfig: baseUrl: "https://api.github.com" getUserProfile: connectTimeoutMillis: 2000 readTimeoutMillis: 5000
|
Dropwizard通过配置类和配置文件绑定的方式获取配置内容。因此,需要对应的在配置类中创建对应的字段。
com.test.configuration.modules.ConnectAndReadConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.test.configuration.modules;
public class ConnectAndReadConfig { private int connectTimeoutMillis; private int readTimeoutMillis;
public int getConnectTimeoutMillis() { return connectTimeoutMillis; }
public int getReadTimeoutMillis() { return readTimeoutMillis; } }
|
com.test.configuration.modules.GithubApiConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.test.configuration.modules;
public class GithubApiConfig { private String baseUrl; private ConnectAndReadConfig getUserProfile;
public String getBaseUrl() { return baseUrl; }
public ConnectAndReadConfig getGetUserProfile() { return getUserProfile; } }
|
在com.test.configuration.HelloWorldConfiguration中添加:
1 2 3 4 5
| @NotEmpty private Map<String, Object> hystrixConfig;
@NotNull private GithubApiConfig githubApiConfig;
|
然后在application中配置好hystrix的配置:
在HelloWorldApplication#run方法中
1 2 3 4 5
| //init hystrix config Map<String, Object> hystrixConfig = configuration.getHystrixConfig(); for (final Map.Entry<String, Object> config : hystrixConfig.entrySet()) { ConfigurationManager.getConfigInstance().setProperty(config.getKey(), config.getValue()); }
|
创建Feign的connector接口
创建接口com.test.domain.connect.GithubConnector:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.test.domain.connect;
import com.test.domain.entiry.GithubUser; import feign.Headers; import feign.Param; import feign.RequestLine; import rx.Observable;
public interface GithubConnector {
@RequestLine("GET /users/{username}") @Headers({"Accept: application/vnd.github.v3+json"}) Observable<GithubUser> getUserProfile(@Param("username") String username); }
|
创建调用客户端
创建客户端com.test.domain.connect.GithubClient
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| package com.test.domain.connect;
import com.test.configuration.modules.ConnectAndReadConfig; import com.test.configuration.modules.GithubApiConfig; import com.test.domain.entiry.GithubUser; import feign.Request; import feign.Response; import feign.gson.GsonDecoder; import feign.gson.GsonEncoder; import feign.hystrix.HystrixFeign; import feign.slf4j.Slf4jLogger; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable;
import java.io.IOException; import java.util.UUID;
public class GithubClient { public static final Logger LOGGER = LoggerFactory.getLogger(GithubClient.class);
private GithubApiConfig githubApiConfig;
public GithubClient(GithubApiConfig githubApiConfig) { this.githubApiConfig = githubApiConfig; }
public Observable<GithubUser> getUserProfile(String username) { String baseUrl = githubApiConfig.getBaseUrl(); ConnectAndReadConfig getUserProfile = githubApiConfig.getGetUserProfile(); GithubConnector connector = HystrixFeign.builder() .decoder(new GsonDecoder()) .encoder(new GsonEncoder()) .logger(new Slf4jLogger()) .options(new Request.Options(getUserProfile.getConnectTimeoutMillis(), getUserProfile.getReadTimeoutMillis())) .errorDecoder((methodKey, response) -> { StringBuilder msg = new StringBuilder("status=").append(response.status()) .append(";request_headers=").append(response.request().headers()) .append(";response_headers=").append(response.headers()) .append(";body="); Response.Body body = response.body(); if (body != null && body.length() > 0) { try { msg.append(IOUtils.toString(body.asReader())); } catch (IOException e) { msg.append("can not read body,"+e.getMessage()); } }
return new RuntimeException(msg.toString()); }) .requestInterceptor(template -> template.header("requestId", UUID.randomUUID().toString())) .target(GithubConnector.class, baseUrl);
return connector.getUserProfile(username).onErrorReturn(error -> { LOGGER.error("Get github user profile failed. ", error); return null; }); } }
|
创建一个接口测试
最后,创建一个接口来测试下:
com.test.domain.resource.GithubResource
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 29 30 31 32 33 34 35
| package com.test.domain.resource;
import com.codahale.metrics.annotation.Timed; import com.test.configuration.modules.GithubApiConfig; import com.test.domain.connect.GithubClient; import com.test.domain.entiry.GithubUser;
import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType;
@Path("/github") @Produces(MediaType.APPLICATION_JSON) public class GithubResource {
private GithubApiConfig githubApiConfig;
public GithubResource(GithubApiConfig githubApiConfig) { this.githubApiConfig = githubApiConfig; }
@GET @Timed @Path("/users/{username}") public GithubUser getUserProfile(@PathParam("username") final String username){ GithubClient client = new GithubClient(githubApiConfig); return client.getUserProfile(username).toBlocking().first(); }
}
|
run main方法启动。访问localhost:8080/github/users/Ryan-Miao
就可以得到我的github信息了:
1 2 3 4 5 6 7 8 9 10
| { "login": "Ryan-Miao", "id": 11866078, "avatar_url": "https://avatars3.githubusercontent.com/u/11866078?v=4", "url": "https://api.github.com/users/Ryan-Miao", "name": "Ryan Miao", "email": null, "location": "中国深圳", "blog": "https://ryan-miao.github.io/" }
|
至此,feign的简单集成就搞定了。
一些注意事项
feign采用hystrix的配置的时候,grop key是baseUrl.上栗中,grop Key为https://api.github.com
, commandKey为接口+方法和参数,上栗中为GithubConnector#getUserProfile(String)
。因此,配置线程超时用了commandKey如上。如果要配置coreSize之类的,必须使用url做为group key了。
source
https://github.com/Ryan-Miao/l4dropwizard
参考