简介
某些情况下需要统一入口,如:提供给第三方调用的接口等。减少接口对接时的复杂性。
代码实现
- GenericController.java
统一入口,通过bean name进行调用service层invoke方法
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("api")
@RequiredArgsConstructor
public class GenericController {
private final ObjectMapper objectMapper;
@PostMapping("/{serviceName}")
public Object invokeService(@PathVariable String serviceName, @RequestBody(required = false) Map<String, Object> requestBody) throws InvocationTargetException, IllegalAccessException {
// log.info("{}接口入参:{}", serviceName, requestBody);
// 从缓存获取ServiceInfo
ServiceInfo<?> serviceInfo = ServiceCacheUtils.cache.get(serviceName);
//这里可以进行判空,但是没必要。没有的就让它抛异常。
// 将请求参数转换为具体的类型
Object requestObject = objectMapper.convertValue(requestBody, serviceInfo.getRequestType());
// 调用Service的invoke方法并获取返回值
Object responseObject = serviceInfo.invoke(requestObject);
// log.info("{}接口出参:{}", serviceName, responseObject);
return responseObject;
}
}
- GenericService.java
/**
* 通用接口
*/
public interface GenericService<T> {
/**
* 通用方法
*/
Object invoke(T request);
}
- UserGenericServiceImpl.java
实现通用接口用户service层类
public class UserGenericServiceImpl implements GenericService<UserDTO> {
@Override
public User invoke(UserDTO dto) {
log.info("UserDTO:{}", dto);
User user = new User();
user.setId(1L);
user.setName("Meta39");
return user;
}
}
- HelloWorldGenericServiceImpl.java
实现通用接口打招呼service层类
@Slf4j
@Service("helloWorld")
public class HelloWorldGenericServiceImpl implements GenericService<GenericServiceDTO> {
@Override
public String invoke(GenericServiceDTO dto) {
log.info("GenericServiceDto: {}", dto);
return "Hello World";
}
}
- ServiceInfo.java
缓存存储反射调用的类和请求参数、返回参数实体类
@Getter
@AllArgsConstructor
public class ServiceInfo<T> {
private final GenericService<T> service;
private final Method method;
private final Class<T> requestType;
public Object invoke(Object requestObject) throws IllegalAccessException, InvocationTargetException {
return method.invoke(service, requestObject);
}
}
- GenericServiceDto.java
第1个数据传输对象看看泛型入参是否可用
@Data
public class GenericServiceDTO {
private String name;
}
- UserDTO.java
第2个数据传输对象看看泛型入参是否可用
@Data
public class UserDTO {
private Integer id;
}
- ServiceCacheUtils.java
把 spring bean 放入缓存并存储对应的请求类型,这样就可以知道每个 GenericService 接口的实现类具体的泛型请求类型是什么。
/**
* 缓存创建的 bean
* @since 2024-07-16
*/
public abstract class ServiceCacheUtils {
private ServiceCacheUtils() {
}
public static ConcurrentHashMap<String, ServiceInfo<?>> cache = new ConcurrentHashMap<>();
}
- ApplicationContextUtils.java
获取 bean 工具类
/**
* 获取Bean
*/
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
//构造函数私有化,防止其它人实例化该对象
private ApplicationContextUtils() {
}
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextUtils.applicationContext = applicationContext;
}
//通过name获取 Bean.(推荐,因为bean的name是唯一的,出现重名的bean启动会报错。)
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
//通过class获取Bean.(确保bean的name不会重复。因为可能会出现在不同包的同名bean导致获取到2个实例)
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean(这个是最稳妥的)
public static <T> T getBean(String name, Class<T> clazz) {
return applicationContext.getBean(name, clazz);
}
public static String[] getBeanNamesForType(Class<?> type) {
return applicationContext.getBeanNamesForType(type);
}
}
- ServiceCacheApplicationRunner.java
SpringBoot启动完成后立马把实现了GenericService的类的bean name 和ServiceInfo存储的请求参数类型写入缓存
/**
* 启动后把所有实现了 GenericService 接口的类写入缓存。这样在调用方法的时候就可以直接获取类进行方法调用。
*
* @since 2024-07-16
*/
@Component
public class ServiceCacheApplicationRunner implements ApplicationRunner {
@Override
@SuppressWarnings("unchecked")
public void run(ApplicationArguments args) throws NoSuchMethodException {
String[] beanNames = ApplicationContextUtils.getBeanNamesForType(GenericService.class);
for (String beanName : beanNames) {
GenericService<Object> service = (GenericService<Object>) ApplicationContextUtils.getBean(beanName);
Type[] genericInterfaces = service.getClass().getGenericInterfaces();
ParameterizedType parameterizedType = (ParameterizedType) genericInterfaces[0];
Class<Object> requestType = (Class<Object>) parameterizedType.getActualTypeArguments()[0];
Method method = service.getClass().getMethod("invoke", requestType);
// 显式类型转换
ServiceInfo<Object> serviceInfo = new ServiceInfo<>(service, method, requestType);
//写入缓存
ServiceCacheUtils.cache.put(beanName, serviceInfo);
}
}
}
测试
helloWorld
控制台输出
2024-07-31 23:22:47.204 INFO 10348 --- [spring-boot3-demo] [omcat-handler-1] c.f.s.s.i.HelloWorldGenericServiceImpl : GenericServiceDto: GenericServiceDTO(name=哈哈哈哈哈)
2024-07-31 23:22:47.212 INFO 10348 --- [spring-boot3-demo] [omcat-handler-1] c.f.springboot3demo.filter.GlobalFilter : 请求内容:
method: POST
uri: /api/helloWorld
request: { "name":"哈哈哈哈哈" }
2024-07-31 23:22:47.213 INFO 10348 --- [spring-boot3-demo] [omcat-handler-1] c.f.springboot3demo.filter.GlobalFilter : 响应内容:
status: 200
response: Hello World
user
控制台输出
2024-07-31 23:24:46.199 INFO 10348 --- [spring-boot3-demo] [omcat-handler-4] c.f.s.s.impl.UserGenericServiceImpl : UserDTO:UserDTO(id=1)
2024-07-31 23:24:46.213 INFO 10348 --- [spring-boot3-demo] [omcat-handler-4] c.f.springboot3demo.filter.GlobalFilter : 请求内容:
method: POST
uri: /api/user
request: { "id":1 }
2024-07-31 23:24:46.213 INFO 10348 --- [spring-boot3-demo] [omcat-handler-4] c.f.springboot3demo.filter.GlobalFilter : 响应内容:
status: 200
response: {"id":1,"name":"Meta39"}
注意事项
这种方式实现的统一入口,暂时发现一个弊端,没法使用Spring validation 参数校验框架,否则会抛异常。但是可以通过Spring Assert在代码里判断。
@Slf4j
@Service("helloWorld")
public class HelloWorldGenericServiceImpl implements GenericService<GenericServiceDTO> {
@Override
public String invoke(GenericServiceDTO dto) {
log.info("GenericServiceDto: {}", dto);
//如下所示
Assert.notNull(dto, "请求体不能为空");
return "Hello World";
}
}