神奇好望角 The Magical Cape of Good Hope

庸人不必自扰,智者何需千虑?
posts - 26, comments - 50, trackbacks - 0, articles - 11
  BlogJava :: 首页 ::  :: 联系 :: 聚合  :: 管理

目前我们的电影服务只提供了对电影信息的访问服务,现在我们要再增加两项级服务,分别用来访问导演和演员信息。加上原先的电信信息服务,我们把 URI 统一放到 /ms/rest/service/ 的子路径下。最先想到的方法就是为这三个 URI 分别写 JAX-RS 服务:

@Singleton
@Path("service/movie")
public class MovieService {
    // 此处省略若干行
}

@Singleton
@Path("service/director")
public class DirectorService {
    // 此处省略若干行
}

@Singleton
@Path("service/director")
public class ActorService {
    // 此处省略若干行
}
    

这种写法的缺点就是让三个本来有点关系(父级 URI 相同)的服务被放到了毫不相干的三个类里面,不一个个类地查看注解难以看出这点关系。为此,JAX-RS 提供了动态资源绑定的功能,让我们能够对这种情况做一些整理。

首先,我们引入一个服务定位器来处理集中管理这三个子级服务:

@Singleton
@Path("service")
public class ServiceLocator {
    @Inject
    private MovieService movieService;
    @Inject
    private DirectorService directorService;
    @Inject
    private ActorService actorService;
    private Map<String, Object> serviceMap;

    @PostConstruct
    private initServiceMap() {
        serviceMap = new HashMap<>();
        serviceMap.put("movie", movieService);
        serviceMap.put("director", directorService);
        serviceMap.put("actor", actorService);
    }

    @Path("{name}")
    public Object locateService(@PathParam("name") String name) {
        Object service = serviceMap.get(name);
        if (service == null) {
            throw new WebApplicationException(Status.SERVICE_UNAVAILABLE);
        }
        return service;
    }
}
    

该类中的 locateService 方法根据服务的名称返回相应的服务实例,注意该方法只有一个 @Path 注解,因为它并不清楚请求的具体内容;返回对象的类型为 Object,表明动态资源定位不要求服务类实现相同的接口,只需要它们的方法带有相应的 JAX-RS 注解,就能够被 JAX-RS 自动发现和处理(专业术语称为 introspect,内省),以 MovieService 为例:

@Singleton
public class MovieService {
    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Movie find(@PathParam("id") int id) {
        Movie movie = movieDao.get(id);
        if (movie != null) {
            return movie;
        } else {
            throw new WebApplicationException(Status.NOT_FOUND);
        }
    }

    // 此处省略若干行
}

这样,每个请求实际上都由两个类先后处理。例如,处理请求 GET /ms/rest/service/movie/1 的时候,先由 ServiceLocator 返回相配的服务实例 movieService,然后再由该实例的 find 方法返回结果。比起最开始那三个简单的类,虽然多了一层调用,但换来了更加清晰的结构。

动态资源定位是一个非常灵活强大的功能,用好的话,完全可以把 URI 层次整理成一个类似于文件目录结构的抽象文件系统。

posted @ 2011-12-21 16:00 蜀山兆孨龘 阅读(2901) | 评论 (0)编辑 收藏

笼子大了什么鸟都有。同样的道理,不论多么细心地设计 URI 结构,在系统复杂到一定程度后,仍然难以避免路径冲突。为此,JAX-RS 使用一些规则来定义路径匹配的优先级。

如果某个请求路径可以对上多个 URI 匹配模式,那么 JAX-RS 就把可能匹配上的 URI 模式先拼接完整,按照下列规则依次进行比较,直到找出最适合的匹配模式:

  1. 首先,字面字符数量更多的 URI 模式优先。“字面字符”就是写死的路径段,不包含路径分隔符 / 和模板参数。例如 /ms/rest/movie/{id : \\d+} 包含 11 个字面字符。
  2. 其次,模板参数个数最多的 URI 模式优先。例如 /ms/rest/movie/{id : \\d+} 带一个模板参数。
  3. 最后,含正则表达式的模板参数个数最多的 URI 模式优先。例如 /ms/rest/movie/{id : \\d+} 带一个含正则表达式的模板参数。

现在看一个例子。回顾一下,/ms/rest/movie/{id : \\d+} 已经用来根据 ID 获取电影信息。为了制造麻烦,现在引入 /ms/rest/movie/{title} 来根据电影标题获取电影信息。先请你猜一猜 /ms/rest/movie/300 代表啥?ID 为 300 的神秘电影,还是我们可爱的勇士?只能跟着规则一条一条地看:

  1. 首先,两个 URI 匹配模式的字面字符都是 11,下一步。
  2. 其次,两个 URI 匹配模式都带一个模板参数,下一步。
  3. 最后,只有 /ms/rest/movie/{id : \\d+} 带了一个含正则表达式的模板参数,胜利!所以返回 ID 为 300 的片片。

传说这三条规则能够覆盖 90% 以上的情景。不过我们马上就能造出一个打破规则的东西:/ms/rest/movie/{title : [ \\w]+}。经过测试,/ms/rest/movie/300 会匹配上 /ms/rest/movie/{id : \\d+}。如何解释?JAX-RS 规范文档 3.7.2 定义了完整的匹配规则,对于这两个简单的 URI 匹配模式,似乎一直进行到底都无法比较出优先级。莫非有另外的潜规则?或者是 JAX-RS 的实现(参考实现为 Jersey)自行规定?但无论如何,搞出这种怪物本身就是一个设计错误,所以也不必去深究原因。

posted @ 2011-12-07 15:10 蜀山兆孨龘 阅读(2285) | 评论 (0)编辑 收藏

以前我曾用两个类(ZipItemZipSystem)实现了一个简单的 ZIP 文件系统(以下简称 ZFS)。其实这两个小类挺好用的,而且支持嵌套的 ZIP 文件,但是,但是……JDK 7 丢下来一枚叫做 NIO2 的笑气炸弹,引入了一套标准的文件系统 API,我承认我中弹了,手痒了,又根据这套 API 重新实现了 ZIP 文件系统,终于在今天初步完工,哈。

话说,JDK 7 其实捆绑销售了一个 ZFS,demo 目录下还有源代码。可……它达不到我的奢求,而且 BUG 不少。随便逮两个:

        // com.sun.nio.zipfs.ZipFileSystemProvider 类中的方法
        @Override
        public Path getPath(URI uri) {

            String spec = uri.getSchemeSpecificPart();
            int sep = spec.indexOf("!/");
            if (sep == -1)
                throw new IllegalArgumentException("URI: "
                    + uri
                    + " does not contain path info ex. jar:file:/c:/foo.zip!/BAR");
            // 难怪该方法始终抛 IllegalArgumentException 异常,原来你小子把文件的 URI
            // 当成 ZFS 的 URI 在用……
            return getFileSystem(uri).getPath(spec.substring(sep + 1));
        }

        // com.sun.nio.zipfs.ZipFileSystem 类中的方法
        @Override
        public PathMatcher getPathMatcher(String syntaxAndInput) {
            int pos = syntaxAndInput.indexOf(':');
            // 丫的,pos == syntaxAndInput.length()?!谁写的?抓出来鞭尸。
            if (pos <= 0 || pos == syntaxAndInput.length()) {
                throw new IllegalArgumentException();
    

很明显,官方 ZFS 没有经过代码审阅、没有经过测试、没有经过……然后,@author Xueming Shen,真是丢咱华夏民族的脸……

下面列个表格详细比较官方 ZFS 和山寨 ZFS:

比较内容 官方 ZFS 山寨 ZFS
实现方式 另起炉灶,用纯 Java 重新实现了对 ZIP 文件格式的处理代码。 基于 ZipFileZipInputStream 这两个已经稳定多年的类,但涉及了大量本地代码调用,也许会影响性能。
读操作 支持,且通过解压到临时文件支持随机访问。 支持,但不支持随机访问。
写操作 通过解压到临时文件进行支持,但无法检测到其他进程对同一个 ZIP 文件的写操作,不适用于并发环境。 不支持。ZIP 文件事实上是一个整体,对内部条目的任何修改都可能导致重构整个文件,因此所谓的写操作必须通过临时文件来处理,效率低下,意义不大,而且难以处理嵌套 ZIP 文件。这也符合我的原则:不解压。
嵌套 ZIP 文件 不支持。 支持,当然读取嵌套 ZIP 文件会慢一些。
反斜线分隔符 不支持,直接瓜掉。 支持,且和标准的斜线分隔符区别对待。例如,/abc//abc\ 算不同的文件,实际上这两个能够并存于 ZIP 文件中。
空目录名 不支持,直接瓜掉。 支持。例如 /a/b/a//b 是两个可以并存且不同的文件。

山寨 ZFS 的用法示例:

        Map<String, Object> env = new HashMap<>();
        // 用于解码 ZIP 条目名。默认为 Charset.defaultCharset()。
        env.put("charset", StandardCharsets.UTF_8);
        // 指示是否自动探测嵌套的 ZIP 文件。默认为 false。
        env.put("autoDetect", true);
        // 默认目录,用于创建和解析相对路径。默认为“/”。
        env.put("defaultDirectory", "/dir/");

        // 从文件创建一个 ZFS。
        try (FileSystem zfs = FileSystems.newFileSystem(
                URI.create("zip:" + Paths.get("docs.zip").toUri()), env)) {
            Path path = zfs.getPath("app.jar");
            if ((Boolean) Files.getAttribute(path, "isZip")) {
                // 创建一个嵌套的 ZFS。
                try (FileSystem nestedZfs = zfs.provider().newFileSystem(path, env)) {
                    // 此处省略若干行。
                }
            }
        }
    

最后双手奉上源代码:请猛击此处!

posted @ 2011-12-01 13:12 蜀山兆孨龘 阅读(2415) | 评论 (1)编辑 收藏

先看出错的代码:

        public class Holder<T> {
            private T value;

            public Holder() {
            }

            public Holder(T value) {
                this.value = value;
            }

            public void setValue(T value) {
                this.value = value;
            }

            // 此处省略若干行。
        }

        Holder<Object> holder = new Holder<>("xxx");
    

看起来还好,但编译的时候却报错:

Uncompilable source code - incompatible types
  required: zhyi.test.Holder<java.lang.Object>
  found:    zhyi.test.Holder<java.lang.String>

老老实实把类型写出来就没问题:

        Holder<Object> holder = new Holder<Object>("xxx");
    

如果非要用钻石运算符的话,可以采取下列两种方式之一:

        // 使用默认构造器,再调用 setValue 方法。
        Holder<Object> holder = new Holder<>();
        holder.setValue("xxx");

        // 使用泛型通配符,但之后就不能调用 setValue 了,否则编译出错。
        Holder<? extends Object> holder = new Holder<>("xxx");
    

posted @ 2011-11-11 11:06 蜀山兆孨龘 阅读(1552) | 评论 (0)编辑 收藏

CyclicBarrier 的功能类似于前面说到的 CountDownLatch,用于让多个线程(子任务)互相等待,直到共同到达公共屏障点(common barrier point),在这个点上,所有的子任务都已完成,从而主任务完成。

该类构造的时候除了必须要指定线程数量,还可以传入一个 Runnable 对象,它的 run 方法将在到达公共屏障点后执行一次。子线程完成计算后,分别调用 CyclicBarrier#await 方法进入阻塞状态,直到其他所有子线程都调用了 await

下面仍然以运动员准备赛跑为例来说明 CyclicBarrier 的用法:

            final int count = 8;
            System.out.println("运动员开始就位。");

            final CyclicBarrier cb = new CyclicBarrier(count, new Runnable() {
                @Override
                public void run() {
                    System.out.println("比赛开始...");
                }
            });

            for (int i = 1; i <= count; i++) {
                final int number = i;
                new Thread() {
                    @Override
                    public void run() {
                        System.out.println(number + " 号运动员到场并开始准备...");
                        try {
                            // 准备 2~5 秒钟。
                            TimeUnit.SECONDS.sleep(new Random().nextInt(4) + 2);
                        } catch (InterruptedException ex) {
                        }
                        System.out.println(number + " 号运动员就位。");
                        try {
                            cb.await();
                        } catch (InterruptedException | BrokenBarrierException ex) {
                        }
                    }
                }.start();
            }
            System.out.println("等待所有运动员就位...");
    

运行输出(可能)为:

运动员开始就位。
1 号运动员到场并开始准备...
2 号运动员到场并开始准备...
等待所有运动员就位...
3 号运动员到场并开始准备...
4 号运动员到场并开始准备...
6 号运动员到场并开始准备...
8 号运动员到场并开始准备...
5 号运动员到场并开始准备...
7 号运动员到场并开始准备...
1 号运动员就位。
3 号运动员就位。
8 号运动员就位。
6 号运动员就位。
2 号运动员就位。
7 号运动员就位。
5 号运动员就位。
4 号运动员就位。
比赛开始...

最后看看 CyclicBarrierCountDownLatch 的主要异同:

  1. 两者在构造的时候都必须指定线程数量,而且该数量在构造后不可修改。
  2. 前者可以传入一个 Runnable 对象,在任务完成后自动调用,执行者为某个子线程;后者可在 await 方法后手动执行一段代码实现相同的功能,但执行者为主线程。
  3. 前者在每个子线程上都进行阻塞,然后一起放行;后者仅在主线程上阻塞一次。
  4. 前者可以重复使用;后者的倒计数器归零后就作废了。
  5. 两者的内部实现完全不同。

posted @ 2011-10-17 11:21 蜀山兆孨龘 阅读(1785) | 评论 (0)编辑 收藏

仅列出标题
共8页: 上一页 1 2 3 4 5 6 7 8 下一页