Java does not write code to implement Http requests, only using interfaces and annotations, you can't miss the Rest Proxy

What is Rest Proxy?

The Rest Proxy component is a highly reusable component that I extracted in my long-term system docking work. It has been hailed as an "artifact" by my colleagues. This may be the coolest http docking artifact you have ever used. How cool is it to use it? Students who have done microservices should know Feign Refmodules, this component is something like that.

The specific is this: write an interface, define several methods, and then add annotations, complete! , just call the method directly in the business, no need to write any implementation class!

REST request proxy (based on Http Interface)

This component is built based on SpringBoot2.7.7 and supports complete AutoConfiguraiton. Only minimal configuration is required to complete the configuration. The latest version is 1.1.2, which adds standard REST annotations.

Feature List

  • Non-perceptual automatic injection, only need interface + annotation to easily complete the docking
  • Elegant API saves you the trouble of learning
  • High-performance implementation, intelligent result processing binding
  • Simpler initialization configuration than Retrofit and seamless integration with Spring
  • Support file upload and download without sense docking
  • Automatic unwrapping result wrapper class
  • Automatic conversion of parameters and request bodies
  • custom authentication
  • Dynamic replacement implementation (under construction)

quick start

You can refer to the modules in the source code rest-proxy-coreto testview the test cases and understand the basic usage.
In addition, we provide examplethe project, you can directly refer to its writing method to migrate the relevant logic to your SpringBoot project.

For details, please check the open source warehouse: https://git.flyfish.group/flyfish-group/rest-proxy.git
and then switch to the examplebranch for the demo.

Dependency import

Please use maven to import dependencies

   <dependency>
        <groupId>group.flyfish</groupId>
        <artifactId>rest-proxy-core</artifactId>
        <version>1.1.2</version>
    </dependency>

1. Define the request source

We provide a very flexible way to define the request source (Base URL).

The request source will be spliced ​​to each interface as a prefix when the interface is called, which is convenient for path reuse .

The component supports three methods: global request source , multi-request source , and annotation-specified request source , and the configuration is very simple.

1.1 Global request source

# application.yml
rest:
  client:
    # 通过配置文件指定全局请求源,所有未指定请求源的RestService都会以该请求开头
    base-url: https://mybase-source.domain

1.2 Multiple request sources

# application.yml
rest:
  client:
    urls:
      # 定义多个请求源,每个请求源有一个唯一的key,支持全限定url以及路径
      baidu: https://ug.baidu.com
      yapi: http://yapi.flyfish.group/api

1.3 Annotation specifies the request source

We support the request source at the Service service (class) level, and the request source specification at the Method (method) level.

Class request source:

@RestService(baseUrl = "https://ug.baidu.com")
public interface TestService {
    
    
   
    @GET("/action")
    String getActionString();
}

method request source

@RestService
public interface TestService {
    
    
    
    @GET(baseUrl = "https://ug.baidu.com", uri = "/action")
    String getActionString();
}

2. Define the RestService request service class

We provide @RestServicea series of HTTP annotations to help you quickly define the interface without writing any implementation.
Here is a basic example for your reference:

/**
 * 使用@RestService,如果不指定任何参数,
 * 默认会取得全局请求源,也就是rest.client.baseUrl的值
 */
@RestService
public interface TestService {
    
    

    /**
     * 通过关键字string作为query,查询用户列表
     * @param keyword 关键字,会以?keyword={value}的方式传递
     * @return 结果
     */
    @GET("/users")
    List<User> getUsers(String keyword);

    /**
     * 保存用户
     * @param user 请求体,使用@RestBody标记为请求体后,参数会自动转换
     * @return 保存结果
     */
    @POST("/users")
    User saveUser(@RestBody User user);

    /**
     * 更新用户信息
     * @param id 用户id路径参数。使用@RestPathParam注解结合花括号{}来标记路径参数
     * @param user 用户数据
     * @return 结果
     */
    @PUT("/users/{id}")
    User updateUser(@RestPathParam("id") Long id, @RestBody User user);

    /**
     * 更新用户的名称
     * @param id 用户id路径参数
     * @param name 用户名称,使用 mergeBody选项,将除了指定注解类型的其他参数合并到body中,如{name: ''}
     * @return 结果
     */
    @PATCH(value = "/users/{id}", mergedBody = true)
    User updateUser(@RestPathParam("id") Long id, String name);

    /**
     * 删除用户
     * @param id 用户id
     * @return 结果
     */
    @DELETE("/users/{id}")
    User deleteUser(@RestPathParam("id") Long id);
}

If you need to use a request source alone, please refer to:

# application.yml
rest:
  client:
    urls:
      baidu: https://api.baidu.com
      other: http://other.com
@RestService("baidu")
public interface BaiduService {
    
    
    
    @POST("/id-cards")
    String updateIdCard(@RestBody IdCardDto idCard);
}

In addition, we also support file upload and download, please refer to

@RestService(baseUrl = "http://localhost:8999", timeout = 500)
public interface TestRestService {
    
    

    /**
     * 上传一个预定义的文件,使用Multipart包装类,并传入?type=xxx以及一个字符串body:token=xxx
     * @param file 文件信息
     * @param type 文件类型名称
     * @param token 凭据
     * @return 结果
     */
    @POST("/files")
    FileEntity uploadPart(@RestPart Multipart file, String type, @RestPart("token") Long token);

    /**
     * 使用input stream上传一个文件,此外还支持byte[],可以用@RestPart.Filename指定文件名
     * @param file 文件
     * @param name 文件名
     * @return 结果
     */
    @POST("/files")
    FileEntity uploadAnno(@RestPart("fbl") InputStream file, @RestPart.Filename("fbl") String name);

    /**
     * 下载一个文件为byte[]
     * @return 下载结果
     */
    @GET("/files")
    byte[] downloadByte();
}

When calling, only the following steps are required

@Service
public class Test {
    
    
    
    @Resource 
    private TestRestService testRestService;
    
    /**
     * 测试入口
     */
    @Test
    public void test() {
    
    
        // 使用file对象包装
        File file = new File("/Users/wangyu/Desktop/2022年终述职报告.docx");
        FileEntity result = testRestService.uploadPart(new Multipart("file", file.getName(), file), "docx", 55L);
        // 使用input stream
        try (InputStream in = Files.newInputStream(file.toPath())) {
    
    
            FileEntity entity = testRestService.uploadAnno(in, file.getName());
            System.out.println(entity);
        } catch (IOException e) {
    
    
            throw new RuntimeException(e);
        }
        // 下载
        testRestService.downloadByte();
    }
}

3. Enable interface scanning

Finally, we add annotations to the spring boot startup class, and we can use it directly!

Here you need to manually specify the basePackages to be scanned. The component will not scan other packages without your permission.

package group.flyfish.demo;

import group.flyfish.rest.annotation.EnableRestApiProxy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableRestApiProxy(basePackages = "group.flyfish.demo.service")
public class RestProxyDemoApplication {
    
    

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

}

4. Let's start happy docking!

At this point, our quick start tutorial is complete.

Result map processing and binding

Everyone has a tacit habit when implementing the interface, that is, adding a wrapper class to the interface response, which looks like this:

{
    
    
  "success": true,
  "code": 0,
  "message": "成功",
  "result": {
    
    
    "name": "名称",
    "code": "编码"
  }
}

But we don't write like this when we write Java code. Ever since, here is a result unpacking logic involved .

By default, Rest Proxy will not unpack the result. But you can add @AutoMappingannotations to mark automatic unwrapping.

Here we provide a default data structure for the default behavior of unpacking, and the specific format is as defined in the above example .
If the system you want to call does not return in the interface packaging format, you need to write the corresponding logic yourself. You need to implement the interface and write the and methods RestResultMappingcorrectly .mapresolve

mapThe method is used to implement the unpacking logic, that is, how to get real data from the packaged result , such as the above resultfields.
In addition, it is recommended to handle the abnormal result by yourself in this method codeand actively throw it RestClientException.

convertThe method is used to wrap the return value we @RestServicedefined in methodto ensure that the real type is returned.
For example, the method signature is: User getUser(), and the wrapper class is: Result. At this time, we need the return type, which can be wrapped with the methods in Result<User>the tool class we provide . For details, please refer to the following examples.TypeResolveUtils
wrap

The following is the default implementation logic in the system, for reference only:

/**
 * 默认缺省的结果映射
 *
 * @author wangyu
 */
@Slf4j
public class DefaultRestResultMapping implements RestResultMapping {
    
    
    
    /**
     * 模糊的结果映射
     *
     * @param body 结果
     * @return 映射后的结果
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T> T map(Object body) throws RestClientException {
    
    
        // 多一步类型检查,保证转换的结果类型正确
        if (body instanceof RestResult) {
    
    
            RestResult<?> result = (RestResult<?>) body;
            if (result.isSuccess()) {
    
    
                return (T) result.getResult();
            }
            log.error("【RestProxy】请求发生异常!状态码:{},时间:{},信息:{}", result.getCode(),
                    DateUtils.formatDate(new Date(result.getTimestamp()), "yyyy-MM-dd HH:mm:ss"), result.getMessage());
            throw new RestClientException(result.getMessage());
        }
        return (T) body;
    }

    /**
     * 解析返回类型
     *
     * @param resultType 返回类型
     * @return 结果
     */
    @Override
    public Type resolve(Type resultType) {
    
    
        return TypeResolveUtils.wrap(resultType, RestResult.class);
    }
}

After completing the above steps, you can use @AutoMappingannotation tags to unpack and process!
The final effect achieved is from TestServiceto TestUnwrapService, as follows:

@RestService
public interface TestService {
    
    
    
    RestResult<User> getUser();
}
@RestService
@AutoMapping(DefaultRestResultMapping.class)
public interface TestUnwrapService {
    
    

    User getUser();
}

Customize your rest client

We provide many rich customization options to help you use components better.

1. Configuration file customization

The following is a full description of configuration parameters:

# application.yml
rest:
  client:
    # 总是信任ssl证书
    always-trust: true
    # 全局连接超时时间
    connection-timeout: 30s
    # 全局请求源
    base-url: http://22.11.33.22:5001
    # 多请求源配置,key: url
    urls:
      other: https://ug.baidu.com
      yapi: http://yapi.flyfish.group/api

2. Configuration class customization

We also provide many configuration classes for in-depth customization of your program.

2.1 Configure custom hooks

You can RestPropertiesModifiermodify the configuration at runtime by implementing the interface and registering it as a SpringBean in the program.
The general usage scenario is that our multi-request source may be read from other beans, databases or caches, so it needs to be set through code, as follows:

public class UrlMappingAutoConfigure {
    
    

    /**
     * 从WorkflowProperties这个bean读取url配置并放入
     * @param workflow 其他bean
     * @return 结果
     */
    @Bean
    public RestPropertiesModifier configureUrls(WorkflowProperties workflow) {
    
    
        return properties -> {
    
    
            // urls请求源map,可以直接操作
            Map<String, String> urls = properties.getUrls();
            String baseUrl = workflow.getEngine().getBaseUrl();
            String businessBaseUrl = workflow.getEngine().getBusinessBaseUrl();
            String controlBaseUrl = workflow.getEngine().getControlBaseUrl();
            // 配置基础路径集合
            FlowableUrlMapping.URLS.forEach((key, url) -> urls.put(key, baseUrl + url));
            FlowableUrlMapping.BUSINESS_URLS.forEach((key, url) -> urls.put(key, businessBaseUrl + url));
            FlowableUrlMapping.CONTROL_URLS.forEach((key, url) -> urls.put(key, controlBaseUrl + url));
        };
    }
}

2.2 Configuration injection hook

If your program needs to inject the parameter class of the rest client in some beans RestClientProperties, please do not use @Resource directly.
Be sure to use PropertiesConfigurablehooks to complete, otherwise it will lead to project bean dependency problems.

Here is an example of usage inside the framework:

package group.flyfish.rest.core.factory;

/**
 * 生产httpClient
 *
 * @author wangyu
 */
@Slf4j
public final class HttpClientFactoryBean implements FactoryBean<CloseableHttpClient>, PropertiesConfigurable {
    
    

    // 使用非公平锁
    private final ReentrantLock lock = new ReentrantLock();
    // 客户端实例,单例
    private volatile CloseableHttpClient client;
    // 配置,配置没进来就不初始化
    private RestClientProperties properties;
    
    // ...代码省略
    
    @Override
    public Class<?> getObjectType() {
    
    
        return CloseableHttpClient.class;
    }

    /**
     * 配置属性,完成初始化
     *
     * @param properties 属性
     */
    @Override
    public void configure(RestClientProperties properties) {
    
    
        this.properties = properties;
    }
}

2.3 Request interface authentication configuration

Request authentication is a very important part of interface docking, and we still provide flexible support

2.3.1 Global default authentication configuration

You can directly declare a singleton RestAuthProviderinstance to specify global authentication logic.
If you declare more than one bean, you need to @Primarytell which specific injected bean is which.
as follows:

/**
 * 使用@Component注解注册为bean即可被自动配置
 *
 * @author wangyu
 */
@Component
public class YapiAuthProvider implements RestAuthProvider {
    
    

    /**
     * 通过入侵client提供鉴权
     * yapi是使用query鉴权的,所以增加query即可
     *
     * @param builder rest客户端构建器
     */
    @Override
    public void provide(RestClientBuilder builder) {
    
    
        // 支持添加认证头的方式,在此处也可以调用其他rest服务获取接口
        // builder.addHeader("Authorization", "token")
        builder.addParam("token", token);
    }
}

2.3.2 Class-level specified authentication

In addition to global authentication, class-level authentication is also supported, as long as you @RestServicespecify the class in the annotation.

/**
 * yapi服务,支持鉴权
 *
 * @author wangyu
 */
@RestService(value = "yapi", authProvider = YapiAuthProvider.class)
public interface YapiService {
    
    
    
}

2.4 Client configuration at class level and method level

In addition to specifying global configuration, you can also specify some request configuration parameters through annotations, please refer to:

package group.flyfish.rest.annotation;

import java.lang.annotation.*;

/**
 * 标记服务为rest proxy
 *
 * @author wangyu
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestService {
    
    

    /**
     * 通过标识符找到地址,不需要#开头
     *
     * @return 结果
     */
    String value() default "";

    /**
     * 服务级别的基本url,字典请使用#开头
     *
     * @return 结果
     */
    String baseUrl() default "";

    /**
     * 超时时间,-1则取默认,0表示无限
     *
     * @return 结果
     */
    int timeout() default -1;

    /**
     * 鉴权提供者类
     *
     * @return 具体实现了RestAuthProvider的类
     */
    Class<?> authProvider() default Object.class;
}

package group.flyfish.rest.annotation;

import group.flyfish.rest.enums.HttpMethod;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * 启用Rest请求的方法会自动代理实现,
 * 并封装返回值
 *
 * @author wangyu
 */
@Target({
    
    ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestApi {
    
    

    /**
     * uri的别名
     *
     * @return 结果
     */
    @AliasFor("uri")
    String value() default "";

    /**
     * 请求uri,使用次标注必须指定BaseUrl或者配置(现在还不支持)
     *
     * @return uri
     */
    @AliasFor("value")
    String uri() default "";

    /**
     * 请求方法
     *
     * @return 结果
     */
    HttpMethod method() default HttpMethod.GET;

    /**
     * 多个参数时使用合并的body
     *
     * @return 结果
     */
    boolean mergedBody() default false;

    /**
     * 可选指定的url,不会从默认地址请求
     *
     * @return url
     */
    String url() default "";

    /**
     * 基本路径,包含host
     *
     * @return baseUrl
     */
    String baseUrl() default "";

    /**
     * 是否带上认证token
     *
     * @return 结果
     */
    boolean credentials() default false;
}

Sponsorship and tipping

If you think my project is helpful to you, please star or buy me a drink☕️, thank you~
At the same time, all friends are welcome to submit P/R and jointly maintain this project.

Guess you like

Origin blog.csdn.net/wybaby168/article/details/129296785