61 |RESTful Web Services(25):如何重构DefaultResourceRouter中的Result结构?
徐昊

你好,我是徐昊。今天我们继续使用 TDD 的方式实现 RESTful Web Services。
回顾架构愿景与任务列表
目前我们已经实现了 ResourceRouter,和 UriTemplate 整体的架构愿景如下:


目前的任务列表:
Resource/RootResource/ResourceMethod
从 Path 标注中获取 UriTemplate
如不存在 Path 标注,则抛出异常
在处理请求派分时,可以根据客户端提供的 Http 方法,选择对应的资源方法
当请求与资源方法的 Uri 模版一致,且 Http 方法一致时,派分到该方法
没有资源方法于请求的 Uri 和 Http 方法一致时,返回 404
在处理请求派分时,可以支持多级子资源
当没有资源方法可以匹配请求时,选择最优匹配 SubResourceLocater,通过它继续进行派分
如果 SubResourceLocator 也无法找到满足的请求时,返回 404
代码为:
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 {
        Optional<ResourceMethod> match(UriTemplate.MatchResult result, String method, String[] mediaTypes, UriInfoBuilder builder);
    }
    interface RootResource extends Resource, UriHandler {
    }
    interface ResourceMethod extends UriHandler {
        String getHttpMethod();
        GenericEntity<?> call(ResourceContext resourceContext, UriInfoBuilder builder);
    }
    interface SubResourceLocator extends UriHandler {
    }
    interface UriHandler {
        UriTemplate getUriTemplate();
    }
}
class DefaultResourceRouter implements ResourceRouter {
    private Runtime runtime;
    private List<RootResource> rootResources;
    public DefaultResourceRouter(Runtime runtime, List<RootResource> 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 = rootResources.stream().map(resource -> match(path, resource))
                .filter(Result::isMatched).sorted().findFirst().flatMap(result -> result.findResourceMethod(request, uri));
        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 Result match(String path, RootResource resource) {
        return new Result(resource.getUriTemplate().match(path), resource);
    }
    record Result(Optional<UriTemplate.MatchResult> matched, RootResource resource) implements Comparable<Result> {
        private boolean isMatched() {
            return matched.isPresent();
        }
        @Override
        public int compareTo(Result o) {
            return matched.flatMap(x -> o.matched.map(x::compareTo)).orElse(0);
        }
        private Optional<ResourceMethod> findResourceMethod(HttpServletRequest request, UriInfoBuilder uri) {
            return matched.flatMap(result -> resource.match(result, request.getMethod(),
                    Collections.list(request.getHeaders(HttpHeaders.ACCEPT)).toArray(String[]::new), uri));
        }
    }
}
class RootResourceClass implements ResourceRouter.RootResource {
    private PathTemplate uriTemplate;
    private Class<?> resourceClass;
    private ResourceMethods resourceMethods;
    public RootResourceClass(Class<?> resourceClass) {
        this.resourceClass = resourceClass;
        this.uriTemplate = new PathTemplate(resourceClass.getAnnotation(Path.class).value());
        this.resourceMethods = new ResourceMethods(resourceClass.getMethods());
    }
    @Override
    public Optional<ResourceRouter.ResourceMethod> match(UriTemplate.MatchResult result, String method, String[] mediaTypes, UriInfoBuilder builder) {
        String remaining = Optional.ofNullable(result.getRemaining()).orElse("");
        return resourceMethods.findResourceMethods(remaining, method);
    }
    @Override
    public UriTemplate getUriTemplate() {
        return uriTemplate;
    }
}
class SubResource implements ResourceRouter.Resource {
    private Object subResource;
    private ResourceMethods resourceMethods;
    public SubResource(Object subResource) {
        this.subResource = subResource;
        this.resourceMethods = new ResourceMethods(subResource.getClass().getMethods());
    }
    @Override
    public Optional<ResourceRouter.ResourceMethod> match(UriTemplate.MatchResult result, String method, String[] mediaTypes, UriInfoBuilder builder) {
        String remaining = Optional.ofNullable(result.getRemaining()).orElse("");
        return resourceMethods.findResourceMethods(remaining, method);
    }
}
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 Optional.ofNullable(resourceMethods.get(method)).flatMap(methods -> Result.match(path, methods, r -> r.getRemaining() == null));
    }
}
record Result<T extends ResourceRouter.UriHandler>
        (Optional<UriTemplate.MatchResult> matched,
         T handler, Function<UriTemplate.MatchResult, Boolean> matchFunction) implements Comparable<Result<T>> {
    public static <T extends ResourceRouter.UriHandler> Optional<T> match(String path, List<T> handlers, Function<UriTemplate.MatchResult, Boolean> matchFunction) {
        return handlers.stream().map(m -> new Result<>(m.getUriTemplate().match(path), m, matchFunction))
                .filter(Result::isMatched).sorted().findFirst().map(Result::handler);
    }
    public static <T extends ResourceRouter.UriHandler> Optional<T> match(String path, List<T> handlers) {
        return match(path, handlers, r -> true);
    }
    public boolean isMatched() {
        return matched.map(matchFunction::apply).orElse(false);
    }
    @Override
    public int compareTo(Result<T> o) {
        return matched.flatMap(x -> o.matched.map(x::compareTo)).orElse(0);
    }
}
class SubResourceLocators {
    private final List<ResourceRouter.SubResourceLocator> 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.SubResourceLocator>) DefaultSubResourceLocator::new).toList();
    }
    public Optional<ResourceRouter.SubResourceLocator> findSubResource(String path) {
        return Result.match(path, subResourceLocators);
    }
    static class DefaultSubResourceLocator implements ResourceRouter.SubResourceLocator {
        private PathTemplate uriTemplate;
        private Method method;
        public DefaultSubResourceLocator(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();
        }
    }
}
视频演示
进入今天的环节:
00:00 / 00:00
1.0x
- 2.0x
 - 1.5x
 - 1.25x
 - 1.0x
 - 0.75x
 - 0.5x
 
音量
网页全屏
全屏
00:00
思考题
如何继续划分 ResourceLocator 的任务?
欢迎把你的想法分享在留言区,也欢迎把你的项目代码分享出来。相信经过你的思考与实操,学习效果会更好!
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
笔记
复制
AI
- 深入了解
 - 翻译
- 英语
 - 中文简体
 - 中文繁体
 - 法语
 - 德语
 - 日语
 - 韩语
 - 俄语
 - 西班牙语
 - 阿拉伯语
 
 - 解释
 - 总结
 

本文介绍了如何通过重构DefaultResourceRouter中的Result结构来实现RESTful Web Services。文章首先回顾了架构愿景和任务列表,然后详细介绍了ResourceRouter接口及其实现类DefaultResourceRouter,以及相关的RootResource、ResourceMethod和SubResourceLocator接口及其实现类。通过代码示例展示了如何使用TDD的方式实现RESTful Web Services,并讨论了如何继续划分ResourceLocator的任务。文章通过代码和思考题引导读者深入思考和实践,以提高学习效果。文章内容涉及到Java中的接口、类、方法等技术细节,适合对RESTful Web Services感兴趣的技术人员阅读学习。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《徐昊 · TDD 项目实战 70 讲》,新⼈⾸单¥98
《徐昊 · TDD 项目实战 70 讲》,新⼈⾸单¥98
立即购买
登录 后留言
全部留言(1)
- 最新
 - 精选
 
Jason函数式接口有点难懂2022-09-07归属地:上海
 收起评论

