将@Service直接暴露为http接口

  • 将@Service直接暴露为http接口
  • 思路
  • 实现
  • 扫描service接口及方法
  • 将service方法注册到spring mvc requestMapping中,交由spring mvc管理
  • 解决参数映射、结果映射的问题
  • 代码
  • RequestMappingHandlerAdapterConfig
  • ControllessRequestMappingRegister
  • ControllessMethodArgumentReturnValueHandler
  • EnableControlless
  • 测试
  • 项目地址


将@Service直接暴露为http接口

本人目前在做公司的基础服务开发,一般是将具有基础能力的jar包提供给公司的其他部门做开发使用,
我们提供的基础能力包只会有service层,不会包含接入层(http controller),但是如果只是service包的话,
我们团队的测试同学就不太方便测试,所以写了个小工具,可以将service直接暴露为http接口。
仅仅是作为测试使用,并不支持用于生产环境哦。

思路

  1. 扫描service接口及方法
  2. 将service方法注册到spring mvc requestMapping中,交由spring mvc管理
  3. 解决参数映射、结果映射的问题

实现

按照上面的思路,开始一个个解决

扫描service接口及方法

这个很简单,通过bean工厂获取service bean,然后再通过反射获取service的方法即可,代码如下

//获取service bean
Map<String, Object> serviceBeans = applicationContext.getBeansWithAnnotation(Service.class);

反射获取service方法

Class<?> beanClass = bean.getClass();
        String simpleName = beanClass.getSimpleName();
        Class<?>[] interfaces = ClassUtils.getAllInterfacesForClass(beanClass);
        for (Class<?> itf : interfaces) {
            Method[] methods = itf.getDeclaredMethods();
            for (Method method : methods) {
//                addMapping(bean, simpleName, method);
            }
        }

将service方法注册到spring mvc requestMapping中,交由spring mvc管理

这个没有思路,只能调试源码,看spring mvc是怎么用的,然后再想

跟随源码

DispatcherServlet#doService->DispatcherServlet#doDispatch->DispatcherServlet#getHandler,可以看到一个重要的类HandlerMapping,官方文档和java doc都有介绍

java 项目暴露接口 java暴露服务接口_java 项目暴露接口

HandlerMapping的解释就是:用来将一个request映射到对应的处理器,有两个主要的实现,其中一个是RequestMappingHandlerMapping(用来支持@RequestMapping注解声明的方法)

看到这里好像是找到了下一个比较重要的类RequestMappingHandlerMapping,看一下它的文档

java 项目暴露接口 java暴露服务接口_ide_02

根据@RequestMapping和@Controller创建RequestMappingInfo的实例

从spring的测试用例中可以看到HandlerMapping是怎样使用的

java 项目暴露接口 java暴露服务接口_java 项目暴露接口_03


这段代码,就是先注册接口, 然后再模拟了处理请求,所以这个接口就是我们要用的

我们只要把需要注册的接口通过RequestMappingHandlerMapping注册就可以了,代码如下

@Configuration
@Order
public class RequestMappingHandlerAdapterConfig {

    @Autowired
    public void initHandlerMapping(RequestMappingHandlerMapping mapping, ApplicationContext applicationContext) {
        Map<String, Object> serviceBeans = applicationContext.getBeansWithAnnotation(Service.class);
        Collection<Object> objects = serviceBeans.values();
        ControllessRequestMappingRegister register = new ControllessRequestMappingRegister(mapping);
        register.register(objects);
    }
}

先通过applicationContext获取到service bean,然后定义了一个注册RequestMapping的注册器,在注册器中注册了需要暴露的接口

public class ControllessRequestMappingRegister {

    private final RequestMappingHandlerMapping mapping;

    public ControllessRequestMappingRegister(RequestMappingHandlerMapping mapping) {
        this.mapping = mapping;
    }

    public void register(Collection<?> mappingBeans) {
        mappingBeans.forEach(this::addMapping);
    }

    private void addMapping(Object bean) {
        Class<?> beanClass = bean.getClass();
        String simpleName = beanClass.getSimpleName();
        Class<?>[] interfaces = ClassUtils.getAllInterfacesForClass(beanClass);
        for (Class<?> itf : interfaces) {
            Method[] methods = itf.getDeclaredMethods();
            for (Method method : methods) {
                addMapping(bean, simpleName, method);
            }
        }
    }

    private void addMapping(Object bean, String className, Method method) {
        String name = method.getName();
        String path = "/" + className + "/" + name;
        RequestMappingInfo info =
                RequestMappingInfo
                        .paths(path)
                        .methods(RequestMethod.GET, RequestMethod.POST)
                        .build();
        mapping.registerMapping(info, bean, method);
        System.err.printf("ControllessRequestMappingRegister register request mapping, bean class : %s, method: %s, path: %s, http method: %s\n", className, name, path, "GET,POST");
    }


}

通过主动注册RequestMapping实现

@Configuration
@Order
public class RequestMappingHandlerAdapterConfig {

    @Autowired
    public void initHandlerMapping(RequestMappingHandlerMapping mapping, ApplicationContext applicationContext) {
        Map<String, Object> serviceBeans = applicationContext.getBeansWithAnnotation(Service.class);
        Collection<Object> objects = serviceBeans.values();
        ControllessRequestMappingRegister register = new ControllessRequestMappingRegister(mapping);
        register.register(objects);
    }

    @Autowired
    public void initMappingHandlerAdapter(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
        ControllessMethodArgumentReturnValueHandler resolverAndReturnValueHandler = new ControllessMethodArgumentReturnValueHandler(requestMappingHandlerAdapter);
        requestMappingHandlerAdapter.setReturnValueHandlers(Collections.singletonList(resolverAndReturnValueHandler));
        requestMappingHandlerAdapter.setArgumentResolvers(Collections.singletonList(resolverAndReturnValueHandler));
    }


}

先测试一下
定义一个service

@Service
public class TestService implements ITestService {

    @Override
    public String call(String name) {
        return "call me baby: " + name;
    }
}

启动一下

java 项目暴露接口 java暴露服务接口_java 项目暴露接口_04


有我们的这条日志

使用http访问一下试试

java 项目暴露接口 java暴露服务接口_ide_05


控制台信息

java 项目暴露接口 java暴露服务接口_java 项目暴露接口_06


说明接口调用成功,但是返回结果的时候失败了

解决参数映射、结果映射的问题

跟踪源码,发现比较重要的HandlerMethodReturnValueHandler

java 项目暴露接口 java暴露服务接口_spring_07


看一下selectHandler的逻辑

java 项目暴露接口 java暴露服务接口_ide_08


会遍历所有的returnValueHandlers,通过handler.supportsReturnType(returnType)判断handler是否支持处理该类型的返回值,如果支持就会返回这个handler,用这个handler处理返回结果,所以应该是handler匹配错误引起的调用错误。再跟一下源码,看到returnValueHandlers的内容如下

java 项目暴露接口 java暴露服务接口_ide_09


看这些对象的类名,大概就知道了他们支持的处理类型,

例如:

ModelAndViewMethodReturnValueHandler支持的返回值类型就是

@Override
	public boolean supportsReturnType(MethodParameter returnType) {
		return ModelAndView.class.isAssignableFrom(returnType.getParameterType());
	}

RequestResponseBodyMethodProcessor支持的返回值类型就是

@Override
	public boolean supportsReturnType(MethodParameter returnType) {
		return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
				returnType.hasMethodAnnotation(ResponseBody.class));
	}

所以RequestResponseBodyMethodProcessor应该就是我们期望处理返回值的handler

继续跟踪源码

java 项目暴露接口 java暴露服务接口_ide_10


可以看到,此时匹配到的是ViewNameMethodReturnValueHandler,并不是我们期望的

我们期望匹配到的是RequestResponseBodyMethodProcessor,但是RequestResponseBodyMethodProcessor又只支持@ResponseBody声明的方法,我们的方法没有这个声明,所以我们只能自己实现一个RequestResponseBodyMethodProcessor,并且注册到returnValueHandlers中

返回值处理的思路就到这里
其实参数也是需要处理的,思路类似,直接贴代码喽

代码

RequestMappingHandlerAdapterConfig
@Configuration
@Order
public class RequestMappingHandlerAdapterConfig {

    @Autowired
    public void initHandlerMapping(RequestMappingHandlerMapping mapping, ApplicationContext applicationContext) {
        Map<String, Object> serviceBeans = applicationContext.getBeansWithAnnotation(Service.class);
        Collection<Object> objects = serviceBeans.values();
        ControllessRequestMappingRegister register = new ControllessRequestMappingRegister(mapping);
        register.register(objects);
    }

    @Autowired
    public void initMappingHandlerAdapter(RequestMappingHandlerAdapter requestMappingHandlerAdapter, ApplicationContext applicationContext) {
        ControllessMethodArgumentReturnValueHandler resolverAndReturnValueHandler = new ControllessMethodArgumentReturnValueHandler(requestMappingHandlerAdapter, applicationContext);
        requestMappingHandlerAdapter.setReturnValueHandlers(Collections.singletonList(resolverAndReturnValueHandler));
        requestMappingHandlerAdapter.setArgumentResolvers(Collections.singletonList(resolverAndReturnValueHandler));
    }

}
ControllessRequestMappingRegister
public class ControllessRequestMappingRegister {

    private final RequestMappingHandlerMapping mapping;

    public ControllessRequestMappingRegister(RequestMappingHandlerMapping mapping) {
        this.mapping = mapping;
    }

    public void register(Collection<?> mappingBeans) {
        mappingBeans.forEach(this::addMapping);
    }

    private void addMapping(Object bean) {
        Class<?> beanClass = bean.getClass();
        String simpleName = beanClass.getSimpleName();
        Class<?>[] interfaces = ClassUtils.getAllInterfacesForClass(beanClass);
        for (Class<?> itf : interfaces) {
            Method[] methods = itf.getDeclaredMethods();
            for (Method method : methods) {
                addMapping(bean, simpleName, method);
            }
        }
    }

    private void addMapping(Object bean, String className, Method method) {
        String name = method.getName();
        String path = "/" + className + "/" + name;
        RequestMappingInfo info =
                RequestMappingInfo
                        .paths(path)
                        .methods(RequestMethod.GET, RequestMethod.POST)
                        .build();
        mapping.registerMapping(info, bean, method);
        System.err.printf("ControllessRequestMappingRegister register request mapping, bean class : %s, method: %s, path: %s, http method: %s\n", className, name, path, "GET,POST");
    }


}
ControllessMethodArgumentReturnValueHandler
/**
 * Created on 2021/5/26.
 *
 * @author lan
 * @since 1.0
 */
public class ControllessMethodArgumentReturnValueHandler implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

    private final List<HandlerMethodReturnValueHandler> returnValueHandlers;
    private final List<HandlerMethodArgumentResolver> argumentResolvers;
    private final ServiceRequestResponseBodyMethodProcessor serviceRequestResponseBodyMethodProcessor;

    private final ApplicationContext applicationContext;

    public ControllessMethodArgumentReturnValueHandler(RequestMappingHandlerAdapter requestMappingHandlerAdapter, ApplicationContext applicationContext) {
        this.returnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        this.argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
        this.serviceRequestResponseBodyMethodProcessor = new ServiceRequestResponseBodyMethodProcessor(requestMappingHandlerAdapter.getMessageConverters());
        this.applicationContext = applicationContext;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return isService(parameter.getExecutable()) || argumentResolvers.stream().anyMatch(handlerMethodArgumentResolver -> handlerMethodArgumentResolver.supportsParameter(parameter));
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        if (isService(parameter.getExecutable())) {
            if (BeanUtils.isSimpleProperty(parameter.getNestedParameterType())) {
                return argumentResolvers.stream()
                        .filter(handlerMethodArgumentResolver -> handlerMethodArgumentResolver instanceof RequestParamMethodArgumentResolver)
                        .findFirst()
                        .map(handlerMethodArgumentResolver -> resolveArgument(handlerMethodArgumentResolver, parameter, mavContainer, webRequest, binderFactory))
                        .orElse(null);

            }
            return resolveArgument(serviceRequestResponseBodyMethodProcessor, parameter, mavContainer, webRequest, binderFactory);
        }
        return argumentResolvers.stream()
                .filter(handlerMethodArgumentResolver -> handlerMethodArgumentResolver.supportsParameter(parameter))
                .findFirst()
                .map(handlerMethodArgumentResolver -> resolveArgument(handlerMethodArgumentResolver, parameter, mavContainer, webRequest, binderFactory))
                .orElse(null);
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return isService(returnType.getExecutable()) || returnValueHandlers.stream().anyMatch(returnValueHandler -> returnValueHandler.supportsReturnType(returnType));
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        if (isService(returnType.getExecutable())) {
            handleReturnValue(serviceRequestResponseBodyMethodProcessor, returnValue, returnType, mavContainer, webRequest);
        } else {
            returnValueHandlers.stream()
                    .filter(returnValueHandler -> returnValueHandler.supportsReturnType(returnType))
                    .findFirst()
                    .ifPresent(returnValueHandler -> handleReturnValue(returnValueHandler, returnValue, returnType, mavContainer, webRequest));
        }
    }

    private boolean isService(Executable executable) {
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(Service.class);
        Class<?> declaringClass = executable.getDeclaringClass();
        Map<String, ?> beansOfType = applicationContext.getBeansOfType(declaringClass);
        HashSet<?> objects = new HashSet<>(beansOfType.values());
        return beans.values().stream()
                .anyMatch(objects::contains);
    }

    private Object resolveArgument(HandlerMethodArgumentResolver handlerMethodArgumentResolver, MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        try {
            return handlerMethodArgumentResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void handleReturnValue(HandlerMethodReturnValueHandler returnValueHandler, Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
        try {
            returnValueHandler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    static class ServiceRequestResponseBodyMethodProcessor extends RequestResponseBodyMethodProcessor {

        public ServiceRequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters) {
            super(converters);
        }

        @Override
        protected boolean checkRequired(MethodParameter parameter) {
            return true;
        }
    }

}
EnableControlless

做成了一个Springboot starter形式的jar包,所以加了个注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RequestMappingHandlerAdapterConfig.class)
public @interface EnableControlless {
}

测试

@SpringBootApplication
@EnableControlless
public class ControllessTestApplication {

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

}
public interface ITestService {

    String call(String name);

    Map<String, Object> haha(Map<String, Object> map);
    
    Map<String, Object> haha2(Map<String, Object> map, String name);
}
/**
 * Created on 2021/5/26.
 *
 * @author lan
 * @since 1.0
 */
@Service
public class TestService implements ITestService {

    @Override
    public String call(String name) {
        System.out.println("name: " + name);
        return "call me baby: " + name;
    }

    @Override
    public Map<String, Object> haha(Map<String, Object> map) {
        System.out.println(map);
        map.put("time", LocalDateTime.now().toString());
        return map;
    }

    @Override
    public Map<String, Object> haha2(Map<String, Object> map, String name) {
        System.out.println("name: " + name + ", map: " + map);
        map.put("name", name);
        return map;
    }
}

http 请求

GET http://localhost:8080/TestService/call?name=21

###
GET http://localhost:8080/TestService/haha
Content-Type: application/json

{
  "author": "lanicc"
}


###
GET http://localhost:8080/TestService/haha2?name=lanicc
Content-Type: application/json

{
  "author": "lanicc",
  "call": 1111
}

java 项目暴露接口 java暴露服务接口_java 项目暴露接口_11


java 项目暴露接口 java暴露服务接口_spring_12


java 项目暴露接口 java暴露服务接口_spring_13