67|RESTful Web Services(31):对于option的测试策略应该是怎样的?
徐昊
你好,我是徐昊。今天我们继续使用 TDD 的方式实现 RESTful Web Services。
回顾架构愿景与任务列表
目前的任务列表:
Resource/RootResource/ResourceMethods
当 HEAD 方法映射到 GET 方法时,忽略 GET 的返回值
当没有 OPTIONS 方法时,提供默认实现
代码为:
package geektime.tdd.rest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.container.ResourceContext;
import jakarta.ws.rs.core.GenericEntity;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
interface ResourceRouter {
OutboundResponse dispatch(HttpServletRequest request, ResourceContext resourceContext);
interface Resource extends UriHandler {
Optional<ResourceMethod> match(UriTemplate.MatchResult result, String httpMethod, String[] mediaTypes, ResourceContext resourceContext, UriInfoBuilder builder);
}
interface ResourceMethod extends UriHandler {
String getHttpMethod();
GenericEntity<?> call(ResourceContext resourceContext, UriInfoBuilder builder);
}
}
class DefaultResourceRouter implements ResourceRouter {
private Runtime runtime;
private List<Resource> rootResources;
public DefaultResourceRouter(Runtime runtime, List<Resource> rootResources) {
this.runtime = runtime;
this.rootResources = rootResources;
}
@Override
public OutboundResponse dispatch(HttpServletRequest request, ResourceContext resourceContext) {
String path = request.getServletPath();
UriInfoBuilder uri = runtime.createUriInfoBuilder(request);
Optional<ResourceMethod> method = UriHandlers.mapMatched(path, rootResources, (result, resource) -> findResourceMethod(request, resourceContext, uri, result, resource));
if (method.isEmpty()) return (OutboundResponse) Response.status(Response.Status.NOT_FOUND).build();
return (OutboundResponse) method.map(m -> m.call(resourceContext, uri)).map(entity -> Response.ok(entity).build())
.orElseGet(() -> Response.noContent().build());
}
private Optional<ResourceMethod> findResourceMethod(HttpServletRequest request, ResourceContext resourceContext, UriInfoBuilder uri, Optional<UriTemplate.MatchResult> matched, Resource handler) {
return handler.match(matched.get(), request.getMethod(),
Collections.list(request.getHeaders(HttpHeaders.ACCEPT)).toArray(String[]::new), resourceContext, uri);
}
}
class DefaultResourceMethod implements ResourceRouter.ResourceMethod {
private String httpMethod;
private UriTemplate uriTemplate;
private Method method;
public DefaultResourceMethod(Method method) {
this.method = method;
this.uriTemplate = new PathTemplate(Optional.ofNullable(method.getAnnotation(Path.class)).map(Path::value).orElse(""));
this.httpMethod = Arrays.stream(method.getAnnotations()).filter(a -> a.annotationType().isAnnotationPresent(HttpMethod.class))
.findFirst().get().annotationType().getAnnotation(HttpMethod.class).value();
}
@Override
public String getHttpMethod() {
return httpMethod;
}
@Override
public UriTemplate getUriTemplate() {
return uriTemplate;
}
@Override
public GenericEntity<?> call(ResourceContext resourceContext, UriInfoBuilder builder) {
return null;
}
@Override
public String toString() {
return method.getDeclaringClass().getSimpleName() + "." + method.getName();
}
}
class ResourceMethods {
private Map<String, List<ResourceRouter.ResourceMethod>> resourceMethods;
public ResourceMethods(Method[] methods) {
this.resourceMethods = getResourceMethods(methods);
}
private static Map<String, List<ResourceRouter.ResourceMethod>> getResourceMethods(Method[] methods) {
return Arrays.stream(methods).filter(m -> Arrays.stream(m.getAnnotations())
.anyMatch(a -> a.annotationType().isAnnotationPresent(HttpMethod.class)))
.map(DefaultResourceMethod::new)
.collect(Collectors.groupingBy(ResourceRouter.ResourceMethod::getHttpMethod));
}
public Optional<ResourceRouter.ResourceMethod> findResourceMethods(String path, String method) {
return findMethod(path, method).or(() -> findAlternative(path, method));
}
private Optional<ResourceRouter.ResourceMethod> findAlternative(String path, String method) {
return "HEAD".equals(method) ? findMethod(path, "GET").map(HeadResourceMethod::new) : Optional.empty();
}
private Optional<ResourceRouter.ResourceMethod> findMethod(String path, String method) {
return Optional.ofNullable(resourceMethods.get(method)).flatMap(methods -> UriHandlers.match(path, methods, r -> r.getRemaining() == null));
}
}
class HeadResourceMethod implements ResourceRouter.ResourceMethod {
ResourceRouter.ResourceMethod method;
public HeadResourceMethod(ResourceRouter.ResourceMethod method) {
this.method = method;
}
@Override
public String getHttpMethod() {
return method.getHttpMethod();
}
@Override
public GenericEntity<?> call(ResourceContext resourceContext, UriInfoBuilder builder) {
method.call(resourceContext, builder);
return null;
}
@Override
public UriTemplate getUriTemplate() {
return method.getUriTemplate();
}
}
class SubResourceLocators {
private final List<ResourceRouter.Resource> subResourceLocators;
public SubResourceLocators(Method[] methods) {
subResourceLocators = Arrays.stream(methods).filter(m -> m.isAnnotationPresent(Path.class) &&
Arrays.stream(m.getAnnotations()).noneMatch(a -> a.annotationType().isAnnotationPresent(HttpMethod.class)))
.map((Function<Method, ResourceRouter.Resource>) SubResourceLocator::new).toList();
}
public Optional<ResourceRouter.ResourceMethod> findSubResourceMethods(String path, String method, String[] mediaTypes, ResourceContext resourceContext, UriInfoBuilder builder) {
return UriHandlers.mapMatched(path, subResourceLocators, (result, locator) -> locator.match(result.get(), method, mediaTypes, resourceContext, builder));
}
static class SubResourceLocator implements ResourceRouter.Resource {
private PathTemplate uriTemplate;
private Method method;
public SubResourceLocator(Method method) {
this.method = method;
this.uriTemplate = new PathTemplate(method.getAnnotation(Path.class).value());
}
@Override
public UriTemplate getUriTemplate() {
return uriTemplate;
}
@Override
public String toString() {
return method.getDeclaringClass().getSimpleName() + "." + method.getName();
}
@Override
public Optional<ResourceRouter.ResourceMethod> match(UriTemplate.MatchResult result, String httpMethod, String[] mediaTypes, ResourceContext resourceContext, UriInfoBuilder builder) {
Object resource = builder.getLastMatchedResource();
try {
Object subResource = method.invoke(resource);
return new ResourceHandler(subResource, uriTemplate).match(result, httpMethod, mediaTypes, resourceContext, builder);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
class ResourceHandler implements ResourceRouter.Resource {
private UriTemplate uriTemplate;
private ResourceMethods resourceMethods;
private SubResourceLocators subResourceLocators;
private Function<ResourceContext, Object> resource;
public ResourceHandler(Class<?> resourceClass) {
this(resourceClass, new PathTemplate(getTemplate(resourceClass)), rc -> rc.getResource(resourceClass));
}
private static String getTemplate(Class<?> resourceClass) {
if (!resourceClass.isAnnotationPresent(Path.class)) throw new IllegalArgumentException();
return resourceClass.getAnnotation(Path.class).value();
}
public ResourceHandler(Object resource, UriTemplate uriTemplate) {
this(resource.getClass(), uriTemplate, rc -> resource);
}
private ResourceHandler(Class<?> resourceClass, UriTemplate uriTemplate, Function<ResourceContext, Object> resource) {
this.uriTemplate = uriTemplate;
this.resourceMethods = new ResourceMethods(resourceClass.getMethods());
this.subResourceLocators = new SubResourceLocators(resourceClass.getMethods());
this.resource = resource;
}
@Override
public Optional<ResourceRouter.ResourceMethod> match(UriTemplate.MatchResult result, String httpMethod, String[] mediaTypes, ResourceContext resourceContext, UriInfoBuilder builder) {
builder.addMatchedResource(resource.apply(resourceContext));
String remaining = Optional.ofNullable(result.getRemaining()).orElse("");
return resourceMethods.findResourceMethods(remaining, httpMethod)
.or(() -> subResourceLocators.findSubResourceMethods(remaining, httpMethod, mediaTypes, resourceContext, builder));
}
@Override
public UriTemplate getUriTemplate() {
return uriTemplate;
}
}
视频演示
进入今天的环节:
00:00 / 00:00
1.0x
- 2.0x
- 1.5x
- 1.25x
- 1.0x
- 0.75x
- 0.5x
音量
网页全屏
全屏
00:00
思考题
今天的问题比较有意思:目前的代码中,有个重大的 Bug,请思考这个 Bug 是什么?
欢迎把你的思路和想法分享出来,我们下节课再见!
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 英语
- 中文简体
- 中文繁体
- 法语
- 德语
- 日语
- 韩语
- 俄语
- 西班牙语
- 阿拉伯语
- 解释
- 总结
RESTful Web Services(31):对于option的测试策略应该是怎样的? 本文介绍了作者徐昊使用TDD的方式实现RESTful Web Services的过程。文章首先回顾了当前的任务列表,包括Resource/RootResource/ResourceMethods等。接着详细介绍了DefaultResourceRouter、DefaultResourceMethod、ResourceMethods等类的实现原理和功能。其中,DefaultResourceMethod类通过过滤注解实现了对HTTP方法的获取,而ResourceMethods类则实现了对资源方法的查找和匹配。此外,文章还介绍了SubResourceLocators类的实现原理,用于查找子资源方法。最后,文章提出了一个思考题,让读者思考当前代码中存在的重大Bug是什么。 通过本文,读者可以了解到作者在使用TDD的方式实现RESTful Web Services时所采取的具体步骤和策略,以及各个类的功能和实现原理。同时,读者也可以通过思考题对文章内容进行进一步思考和讨论。整体而言,本文对于RESTful Web Services的实现方式和测试策略进行了详细的介绍和讨论,适合对该领域感兴趣的技术人员阅读。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《徐昊 · TDD 项目实战 70 讲》,新⼈⾸单¥98
《徐昊 · TDD 项目实战 70 讲》,新⼈⾸单¥98
立即购买
登录 后留言
精选留言
由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论