spring 6最近发布了新版本,带来了一个新特性HTTP interfaces。可以将http服务定义为一个java接口,通过Http服务代理工厂生成http代理类并通过接口方法进行http调用。
我们先建一个maven项目,增加pom文件依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
新建一个启动类DemoApplication。
package com.fallrain.application;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class);
}
}
推荐一个在线的rest http服务JSONPlaceholder。
根据http返回值创建一个实体类。
package com.fallrain.application.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String body;
private String category;
private String cover;
private String createdAt;
private String id;
private String isDraft;
private String title;
private String views;
}
然后去创建http请求接口。
package com.fallrain.application.service;
import com.fallrain.application.entity.User;
import org.springframework.stereotype.Component;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import java.util.List;
@HttpExchange("https://mockend.com/Fall-Rain/mockend/posts")
public interface UserApi {
@GetExchange
List<User> getUsers();
}
编写一个测试类。
import com.fallrain.application.DemoApplication;
import com.fallrain.application.entity.User;
import com.fallrain.application.service.UserApi;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
@SpringBootTest(classes = DemoApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class TestApplication {
@Test
public void demo() {
//构建一个web客户端
WebClient webClient = WebClient.builder().build();
//根据web客户端去构建服http服务的代理工厂
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build();
//根据代理工厂创建UserApi的代理类
UserApi userApi = factory.createClient(UserApi.class);
//执行getUsers的方法
for (User user : userApi.getUsers()) {
System.out.println(user);
}
}
}
运行demo测试方法。
14:20:33.242 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [TestApplication]: using SpringBootContextLoader
14:20:33.248 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [TestApplication]: no resource found for suffixes {-context.xml, Context.groovy}.
14:20:33.270 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using ContextCustomizers for test class [TestApplication]: [ExcludeFilterContextCustomizer, DuplicateJsonObjectContextCustomizer, MockitoContextCustomizer, TestRestTemplateContextCustomizer, WebTestClientContextCustomizer, DisableObservabilityContextCustomizer, PropertyMappingContextCustomizer, Customizer]
14:20:33.387 [main] DEBUG org.springframework.test.context.util.TestContextSpringFactoriesUtils - Skipping candidate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener] due to a missing dependency. Specify custom TestExecutionListener classes or make the default TestExecutionListener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttributeSource]
14:20:33.388 [main] DEBUG org.springframework.test.context.util.TestContextSpringFactoriesUtils - Skipping candidate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] due to a missing dependency. Specify custom TestExecutionListener classes or make the default TestExecutionListener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute]
14:20:33.390 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners for test class [TestApplication]: [ServletTestExecutionListener, DirtiesContextBeforeModesTestExecutionListener, ApplicationEventsTestExecutionListener, MockitoTestExecutionListener, DependencyInjectionTestExecutionListener, DirtiesContextTestExecutionListener, EventPublishingTestExecutionListener, ResetMocksTestExecutionListener, RestDocsTestExecutionListener, MockRestServiceServerResetTestExecutionListener, MockMvcPrintOnlyOnFailureTestExecutionListener, WebDriverTestExecutionListener, MockWebServiceServerTestExecutionListener]
14:20:33.391 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [TestApplication]
14:20:33.393 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [TestApplication]
14:20:33.394 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [TestApplication]
14:20:33.394 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [TestApplication]
14:20:33.394 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [TestApplication]
14:20:33.394 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [TestApplication]
14:20:33.403 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [TestApplication]
14:20:33.403 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [TestApplication]
14:20:33.404 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [TestApplication]
14:20:33.404 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [TestApplication]
14:20:33.405 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [TestApplication]
14:20:33.405 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [TestApplication]
14:20:33.407 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: class [TestApplication], class annotated with @DirtiesContext [false] with mode [null]
14:20:33.409 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [TestApplication]
14:20:33.409 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [TestApplication]
. ____ _ __ _ _
/ / ___'_ __ _ _(_)_ __ __ _
( ( )___ | '_ | '_| | '_ / _` |
/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |___, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.2)
2023-02-06T14:20:33.708+08:00 INFO 15456 --- [ main] TestApplication : Starting TestApplication using Java 17.0.5 with PID 15456 (started by fallrain in D:codingspring6)
2023-02-06T14:20:33.711+08:00 INFO 15456 --- [ main] TestApplication : No active profile set, falling back to 1 default profile: "default"
2023-02-06T14:20:34.185+08:00 INFO 15456 --- [ main] o.s.c.a.ConfigurationClassPostProcessor : Cannot enhance @Configuration bean definition 'httpPostProcessor' since its singleton instance has been created too early. The typical cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring such methods as 'static'.
2023-02-06T14:20:34.997+08:00 INFO 15456 --- [ main] TestApplication : Started TestApplication in 1.559 seconds (process running for 2.269)
User(body=Eos culpa et voluptatem., category=three, cover=https://picsum.photos/seed/96789/1920/270, createdAt=2016-02-08T08:12:09Z, id=1, isDraft=true, title=corrupti ducimus, views=647)
User(body=Illo saepe consequuntur a libero deserunt a eos, perferendis sint, accusantium qui dolorum sed at., category=three, cover=https://picsum.photos/seed/48269/1920/270, createdAt=2011-10-16T13:55:55Z, id=2, isDraft=false, title=repellendus, views=609)
User(body=Unde veniam., category=one, cover=https://picsum.photos/seed/97760/1920/270, createdAt=2014-08-12T22:37:04Z, id=3, isDraft=false, title=inventore, views=395)
User(body=Debitis et neque facilis magnam., category=one, cover=https://picsum.photos/seed/02770/1920/270, createdAt=2012-06-04T21:28:05Z, id=4, isDraft=true, title=optio ut hic, views=764)
User(body=Error nisi deserunt consectetur ea a., category=one, cover=https://picsum.photos/seed/79932/1920/270, createdAt=2018-12-29T15:15:32Z, id=5, isDraft=true, title=itaque nam sed, views=757)
User(body=Mollitia doloribus a reprehenderit vitae, incidunt incidunt., category=two, cover=https://picsum.photos/seed/65976/1920/270, createdAt=2017-10-08T08:45:12Z, id=6, isDraft=true, title=earum, views=921)
User(body=Reprehenderit et, asperiores sed reprehenderit nisi, architecto a reprehenderit ipsa., category=two, cover=https://picsum.photos/seed/79823/1920/270, createdAt=2012-10-30T16:06:08Z, id=7, isDraft=true, title=debitis in a, views=714)
User(body=Perspiciatis minima sit., category=one, cover=https://picsum.photos/seed/81255/1920/270, createdAt=2015-11-19T10:59:18Z, id=8, isDraft=false, title=maxime iusto, views=117)
User(body=Dolore perferendis deleniti ab, in molestiae quia, autem perspiciatis aliquam corrupti., category=two, cover=https://picsum.photos/seed/93942/1920/270, createdAt=2016-03-26T23:50:25Z, id=9, isDraft=false, title=et iusto, views=847)
User(body=Voluptatem reprehenderit cumque laborum, tempora magnam ea quo cum., category=three, cover=https://picsum.photos/seed/40537/1920/270, createdAt=2013-02-22T02:46:26Z, id=10, isDraft=true, title=quam earum, views=616)
Disconnected from the target VM, address: '127.0.0.1:61702', transport: 'socket'
Process finished with exit code 0
由于官方并没有将代理类放到ioc容器当中,所以每次使用都要手动构建。这里我们可以通过spring的拓展点ResourceLoaderAware和BeanDefinitionRegistryPostProcessor,利用扫描目录的方式将代理类放到ioc容器当中。建立一个HttpPostProcessor并实现ResourceLoaderAware和BeanDefinitionRegistryPostProcessor。
package com.fallrain.application.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import java.io.IOException;
@Configuration
public class HttpPostProcessor implements ResourceLoaderAware, BeanDefinitionRegistryPostProcessor {
private ResourceLoader resourceLoader;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
try {
//获取指定目录下的class文件
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader).getResources("classpath*:com/fallrain/application/**/*.class");
//根据resources创建数据读取工厂
MetadataReaderFactory metaReader = new CachingMetadataReaderFactory(resourceLoader);
for (Resource resource : resources) {
//获取元数据
MetadataReader metadataReader = metaReader.getMetadataReader(resource);
//判断是否存在HttpExchange注解(是否为http interface的接口调用)
if (metadataReader.getAnnotationMetadata().hasAnnotation(HttpExchange.class.getName())) {
//构建一个web客户端
WebClient webClient = WebClient.builder().build();
//根据web客户端去构建服http服务的代理工厂
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build();
//利用类的全限定名通过Class.forName获取class对象并利用http服务的代理工厂创建出代理对象
Object client = factory.createClient(Class.forName(metadataReader.getClassMetadata().getClassName()));
//将创建出来的代理对象放到io容器当中
beanFactory.registerSingleton(metadataReader.getClassMetadata().getClassName(), client);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
}
}
这时我们的测试类就可以使用@Autowired进行注入然后进行调用。
import com.fallrain.application.DemoApplication;
import com.fallrain.application.entity.User;
import com.fallrain.application.service.UserApi;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SpringBootTest(classes = DemoApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class TestApplication {
@Autowired
//获取UserApi的代理类
private UserApi userApi;
@Test
public void demo() {
//执行getUsers的方法
for (User user : userApi.getUsers()) {
System.out.println(user);
}
}
}
到这里spring framework 6新特性HTTP Interface介绍完成。欢迎大家一起讨论。