lanicc

mvc-controlless——我的第一个开源库

2021-07-19

一个可以将Service接口模拟为Controller的库

将@Service直接暴露为http接口

方便测试的小工具,可以将service直接暴露为http接口。
仅仅是作为测试使用,并不推荐用于生产环境哦。

思路

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

实现

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

扫描service接口及方法

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

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

反射获取service方法

1
2
3
4
5
6
7
8
9
        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都有介绍
在这里插入图片描述

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

看到这里好像是找到了下一个比较重要的类RequestMappingHandlerMapping,看一下它的文档
在这里插入图片描述

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

从spring的测试用例中可以看到HandlerMapping是怎样使用的
在这里插入图片描述
这段代码,就是先注册接口, 然后再模拟了处理请求,所以这个接口就是我们要用的

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@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的注册器,在注册器中注册了需要暴露的接口

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
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实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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

1
2
3
4
5
6
7
8
@Service
public class TestService implements ITestService {

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

启动一下
在这里插入图片描述
有我们的这条日志
使用http访问一下试试
在这里插入图片描述
控制台信息
在这里插入图片描述
说明接口调用成功,但是返回结果的时候失败了

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

跟踪源码,发现比较重要的HandlerMethodReturnValueHandler
在这里插入图片描述
看一下selectHandler的逻辑
在这里插入图片描述
会遍历所有的returnValueHandlers,通过handler.supportsReturnType(returnType)判断handler是否支持处理该类型的返回值,如果支持就会返回这个handler,用这个handler处理返回结果,所以应该是handler匹配错误引起的调用错误。再跟一下源码,看到returnValueHandlers的内容如下
在这里插入图片描述
看这些对象的类名,大概就知道了他们支持的处理类型,
例如:
ModelAndViewMethodReturnValueHandler支持的返回值类型就是

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

RequestResponseBodyMethodProcessor支持的返回值类型就是

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

所以RequestResponseBodyMethodProcessor应该就是我们期望处理返回值的handler
继续跟踪源码
在这里插入图片描述
可以看到,此时匹配到的是ViewNameMethodReturnValueHandler,并不是我们期望的
我们期望匹配到的是RequestResponseBodyMethodProcessor,但是RequestResponseBodyMethodProcessor又只支持@ResponseBody声明的方法,我们的方法没有这个声明,所以我们只能自己实现一个RequestResponseBodyMethodProcessor,并且注册到returnValueHandlers中

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

代码

RequestMappingHandlerAdapterConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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

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
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

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

/**
* 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包,所以加了个注解

1
2
3
4
5
6
7
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RequestMappingHandlerAdapterConfig.class)
public @interface EnableControlless {
}

测试

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableControlless
public class ControllessTestApplication {

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

}
1
2
3
4
5
6
7
8
9

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);
}
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
/**
* 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 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

项目地址

https://github.com/lanicc/mvc-controller-less

Tags: Spring