神奇好望角 The Magical Cape of Good Hope

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

顾名思义,CountDownLatch 是一个用来倒计数的咚咚。如果某项任务可以拆分成若干个子任务同时进行,然后等待所有的子任务完成,可以考虑使用它。

该类的用法非常简单。首先构造一个 CountDownLatch,唯一的参数是任务数量,一旦构造完毕就不能修改。接着启动所有的子任务(线程),且每个子任务在完成自己的计算后,调用 CountDownLatch#countDown 方法将倒计数减一。最后在主线程中调用 CountDownLatch#await 方法等待计数器归零。

例如赛跑的准备阶段,八名运动员先后到达起点做好准备,然后裁判打响发令枪,准备工作就结束了,比赛开始。如果把从运动员就位到发令枪响看做赛跑准备任务,那么每个运动员的准备过程就是其子任务,可以用 CountDownLatch 模拟如下:

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

        // 构造 CountDownLatch。
        final CountDownLatch cdl = new CountDownLatch(count);
        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 + " 号运动员就位。");
                    // 倒计数减一。
                    cdl.countDown();
                }
            }.start();
        }

        System.out.println("等待所有运动员就位...");
        try {
            // 等待倒计数变为 0。
            cdl.await();
            System.out.println("比赛开始。");
        } catch (InterruptedException ex) {
        }
    

运行输出(可能)为:

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

从上面的例子还可以看出 CountDownLatch 的局限性和 CompletionService 类似,在于无法处理子任务数量不确定的情况,例如统计某个文件夹中的文件数量。另外,如果某个子任务在调用 countDown 之前就挂掉了,倒计数就永远不会归零。对于这种情况,要么用 finally 之类的手段保证 countDown 一定会被调用,要么用带参数的 await 方法指定超时时间。

posted @ 2011-10-14 14:22 蜀山兆孨龘 阅读(1705) | 评论 (1)编辑 收藏

JAX-RS 的核心功能是处理向 URI 发送的请求,所以它提供了一些匹配模式以便简化对 URI 的解析。楼主在本系列的上一篇文章中已经使用了最简单的路径参数,本文将介绍一些稍微高级点的咚咚。

模板参数

前面已经见过用 @Path("{id}")@PathParam("id") 来匹配路径参数 id。这种匹配方式可以被嵌入到 @Path 注解中的任何地方,从而匹配多个参数,例如下面的代码用来查找 ID 在某一范围内的电影:

        @GET
        @Path("{min}~{max}")
        @Produces(MediaType.APPLICATION_JSON)
        public List<Movie> findMovies(@PathParam("min") int min, @PathParam("max") int max) {
    

于是,GET /ms/rest/movie/5~16 就将返回 ID 为 5 到 16 的电影。此处的 minmax 已被自动转换为 int 类型。JAX-RS 支持多种类型的自动转换,详见 @PathParam 的文档。

根据 HTTP 规范,参数可能会编码。默认情况下,JAX-RS 会自动解码。如果希望得到未解码的参数,只需在参数上再加个 @Encoded 注解。该注解适用于大多数 JAX-RS 注入类型,但并不常用。

模板参数虽然灵活,也可能会带来歧义。例如想用 {firstName}-{lastName} 匹配一个人的姓名,但恰好某人的名(lastName)含有“-”字符,像 O-live K 这种,匹配后就会变成姓 live-K,名 O。这种场景很难避免,一种简单的解决方法就是对参数值进行两次编码,然后在服务端代码解码一次,因为 JAX-RS 默认会进行一次解码,或者加上 @Encoded 注解,自己进行两次解码。

另外,在一个复杂系统中,多个 @Path 可能会造成路径混淆,例如 {a}-{b}{a}-z 都能匹配路径 a-z。虽然 JAX-RS 定义了一些规则来指定匹配的优先级,但这些规则本身就比较复杂,并且也不能完全消除混淆。楼主认为,设计一个 REST 系统的核心就是对 URI 的设计,应当小心处理 URI 的结构,合理分类,尽量保证匹配的唯一性,而不要过度使用晦涩的优先级规则。楼主将在下一篇文章介绍优先级规则。

正则表达式

模板参数可以用一个正则表达式进行验证,写法是在模板参数的标识符后面加一个冒号,然后跟上正则表达式字符串。例如在根据 ID 查询电影信息的代码中,模板参数 {id} 只能是整数,于是代码可以改进为:

        @GET
        @Path("{id : \\d+}")
        @Produces(MediaType.APPLICATION_JSON)
        public List<Movie> findMovies(@PathParam("min") int min, @PathParam("max") int max) {
    

冒号左右的空格将被忽略。用正则表达式验证数据很有局限性,可惜 JAX-RS 目前并不能直接集成 Bean 验证框架,因此复杂的验证只能靠自己写代码。

查询参数

查询参数很常见,就是在 URI 的末尾跟上一个问号和一系列由“&”分隔的键值对,例如查询 ID 为 5 到 16 的电影也可以设计为 /ms/rest/movie?min=5&max=16。JAX-RS 提供了 QueryParam 来注入查询参数:

        @GET
        @Produces(MediaType.APPLICATION_JSON)
        public List<Movie> findMovies(@DefaultValue("0") @QueryParam("min") int min,
                @DefaultValue("0") @QueryParam("max") int max) {
    

查询参数是可选的。如果 URI 没有设定某个查询参数,JAX-RS 就会根据情况为其生成 0、空字符串之类的默认值。如果要手动设定默认值,需要像上面的代码一样用 @DefaultValue 注解来指定。另外还可以加上 Encoded 注解来得到编码的原始参数。

有的查询参数是一对多的键值对,例如 /xyz?a=def&a=pqr,这种情况只需将注入的参数类型改为 List 即可。

矩阵参数

矩阵参数应该属于 URI 规范中的非主流类型,但它实际上比查询参数更灵活,因为它可以嵌入到 URI 路径中的任何一段末尾(用分号隔开),用来标识该段的某些属性。例如 GET /ms/rest/movie;year=2011/title;initial=A 表示在 2011 年出品的电影中查找首字母为 A 的标题。year 是电影的属性,而 initial 是标题的属性,这比把它们都作为查询参数放在末尾更直观可读。匹配 URI 的时候,矩阵参数将被忽略,因此前面的 URI 匹配为 /ms/rest/movie/title。矩阵参数可以用 @MatrixParam 来注入:

        @GET
        @Path("title")
        @Produces(MediaType.APPLICATION_JSON)
        public List<String> findTitles(@MatrixParam("year") int year,
                @MatrixParam("initial") String initial) {
    

如果 URI 的多个段中含有相同名称的矩阵参数,例如 /abc;name=XXX/xyz;name=OOO,这种直接注入就失效了,只能用下面要讲的编程式访问来取得。

编程式访问

如果简单的注入不能达到目的,就需要通过注入 PathSegmentUriInfo 对象来直接编程访问 URI 的信息。

一个 PathSegment 对象代表 URI 中的一个路径段,可以从它得到矩阵参数。它可以通过 @PathParam 来注入,这要求该路径段必须整个被定义为一个模板参数。例如下面的代码也可以用来处理 GET /ms/rest/movie/{id}

        @GET
        @Path("{id}")
        @Produces(MediaType.APPLICATION_JSON)
        public Movie findMovie(@PathParam("id") PathSegment ps) {
    

@PathParam 也可以注入多个段,如果想把 /a/b/c/d 匹配到 /a/{segments}/d,直接注入一个字符串显然不行,因为 b/c 是两个路径段。唯一的选择是把注入的类型改为 List<PathSegment>。楼主严重不推荐用一个模板参数匹配多个路径段,因为这很容易干扰其他匹配的设计,最后搞成一团乱麻。URI 路径段应当尽量设计得简单明晰,再辅以矩阵参数或查询参数就能应付大多数场景。不论对服务端还是客户端开发人员来说,简洁的 URI 既便于管理,又便于使用。网上有不少关于 URI 设计指南的文章,此处不再赘述。

如果想完全手动解析路径,则可以用 @Context 注入一个 UriInfo 对象,通过此对象可以得到 URI 的全部信息,详见 API 文档。例如:

        @GET
        @Path("{id}/{segments}")
        @Produces(MediaType.PLAIN_TEXT)
        public String getInfo(@PathParam("id") int id, @Context UriInfo uriInfo) {
    

UriInfo 主要用在某些特殊场合下起辅助作用,设计良好的 URI 用普通的注入就能完成大部分匹配。


工欲善其事必先利其器,为此 JAX-RS 提供了这些利器来解析 URI。至于如何用这些器来做出一个好系统,则还是依赖于 URI 本身的设计。

posted @ 2011-10-09 12:43 蜀山兆孨龘 阅读(4243) | 评论 (1)编辑 收藏

CompletionService 接口的实例可以充当生产者和消费者的中间处理引擎,从而达到将提交任务和处理结果的代码进行解耦的目的。生产者调用 submit 方法提交任务,而消费者调用 poll(非阻塞)或 take(阻塞)方法获取下一个结果:这一特征看起来和阻塞队列(BlockingQueue)类似,两者的区别在于 CompletionService 要负责任务的处理,而阻塞队列则不会。

在 JDK 中,该接口只有一个实现类 ExecutorCompletionService,该类使用创建时提供的 Executor 对象(通常是线程池)来执行任务,然后将结果放入一个阻塞队列中:果然本就是一家亲啊!ExecutorCompletionService 将线程池和阻塞队列糅合在一起,仅仅通过三个方法,就实现了任务的异步处理,可谓并发编程初学者的神兵利器!

接下来看一个例子。楼主有一大堆 *.java 文件,需要计算它们的代码总行数。利用 ExecutorCompletionService 可以写出很简单的多线程处理代码:

        public int countLines(List<Path> javaFiles) throws Exception {
            // 根据处理器数量创建线程池。虽然多线程并不保证能够提升性能,但适量地
            // 开线程一般可以从系统骗取更多资源。
            ExecutorService es = Executors.newFixedThreadPool(
                    Runtime.getRuntime().availableProcessors() * 2);
            // 使用 ExecutorCompletionService 内建的阻塞队列。
            CompletionService cs = new ExecutorCompletionService(es);

            // 按文件向 CompletionService 提交任务。
            for (final Path javaFile : javaFiles) {
                cs.submit(new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        // 略去计算单个文件行数的代码。
                        return countLines(javaFile);
                    }
                });
            }

            try {
                int loc = 0;
                int size = javaFiles.size();
                for (int i = 0; i < size; i++) {
                    // take 方法等待下一个结果并返回 Future 对象。不直接返回计算结果是为了
                    // 捕获计算时可能抛出的异常。
                    // poll 不等待,有结果就返回一个 Future 对象,否则返回 null。
                    loc += cs.take().get();
                }
                return loc;
            } finally {
                // 关闭线程池。也可以将线程池提升为字段以便重用。
                // 如果任务线程(Callable#call)能响应中断,用 shutdownNow 更好。
                es.shutdown();
            }
        }
    

最后,CompletionService 也不是到处都能用,它不适合处理任务数量有限但个数不可知的场景。例如,要统计某个文件夹中的文件个数,在遍历子文件夹的时候也会“递归地”提交新的任务,但最后到底提交了多少,以及在什么时候提交完了所有任务,都是未知数,无论 CompletionService 还是线程池都无法进行判断。这种情况只能直接用线程池来处理。

posted @ 2011-09-29 13:37 蜀山兆孨龘 阅读(2051) | 评论 (0)编辑 收藏

实体间的多对多的关联需要一张关联表。如果直接使用 ManyToMany 来映射,JPA 就会隐式地帮我们自动管理关联表,代码写出来和其他类型的关联差别不大。例如,某州炒房团需要一个炒房跟踪系统,那么该系统中的炒房客和房子就是多对多的关系:

        public class Speculator implements Serializable {
            @Id
            private Integer id;
            @ManyToMany
            @JoinTable(joinColumns = @JoinColumn(name = "speculator_id"),
                    inverseJoinColumns = @JoinColumn(name = "house_id"))
            private List<House> houses;
            // 此处省略若干行
        }

        public class House implements Serializable {
            @Id
            private Integer id;
            @ManyToMany(mappedBy = "houses")
            private List<Speculator> speculators;
            // 此处省略若干行
        }
    

如果炒房客 s 要卖掉房子 h(严格点说是卖掉房子的产权部分),那么系统执行的代码差不多就是 s.getHouses().remove(h)。看似简单,然而底层的操作却性能低下:JPA 会先从数据库中取出炒房客的所有房产(s.getHouses()),然后再删除指定的那套房子;从数据库层面上看,这将先从关联表(speculator_house)中找到该炒房客的所有房子的外键,然后从 house 表载入这些 House 对象,最后才从 speculator_house 删除关联。在 ORM 出现前,这种操作只需要改关联表,根本不用关心其他房子。这种简单的多对多映射写法将关联表隐藏起来,虽然简化了代码,却也可能带来性能隐患。

很自然地可以想到,如果把关联表也映射成实体类,就能解决这个问题。speculator_house 包含两个外键,可用作联合主键。如果把它映射为 SpeculatorHouse 类,则该类与 SpeculatorHouse 都是多对一的关系。关联表实体类的代码如下(EmbeddedId 的映射技巧见《JPA 应用技巧 2:主键外键合体映射》):

        @Embeddable
        public class SpeculatorHouseId implements Serializable {
            private Integer speculatorId;
            private Integer houseId;
            // 此处省略若干行
        }

        @Entity
        @Table(name = "speculator_house")
        public class SpeculatorHouse implements Serializable {
            @EmbeddedId
            private SpeculatorHouseId id;
            @MapsId("speculatorId")
            @ManyToOne
            private Speculator speculator;
            @MapsId("houseId")
            @ManyToOne
            private House house;
            // 此处省略若干行
        }
    

SpeculatorHouse 也要增加相应的关联信息:

        public class Speculator implements Serializable {
            @Id
            private Integer id;
            @ManyToMany
            @JoinTable(joinColumns = @JoinColumn(name = "speculator_id"),
                    inverseJoinColumns = @JoinColumn(name = "house_id"))
            private List<House> houses;
            @OneToMany(mappedBy = "speculator")
            private List<SpeculatorHouse> speculatorHouses;
            // 此处省略若干行
        }

        public class House implements Serializable {
            @Id
            private Integer id;
            @ManyToMany(mappedBy = "houses")
            private List<Speculator> speculators;
            @OneToMany(mappedBy = "house")
            private List<SpeculatorHouse> speculatorHouses;
            // 此处省略若干行
        }
    

这样既保留了多对多关系,又映射了关联表,然后就可以根据实际情况选择隐式或显示的关联表管理。例如,要得到一个炒房客的全部房子,就使用隐式管理:s.getHouses();而要删除炒房客和某套房子的关联,则用显示管理:delete from SpeculatorHouse sh where sh.speculator = :s and sh.house = :h

posted @ 2011-09-27 11:04 蜀山兆孨龘 阅读(3252) | 评论 (2)编辑 收藏

JAX-RS 使用注解进行配置,所以用它开发 REST 风格的服务非常简单。楼主在本文用一个小例子来说明 JAX-RS 的基本用法。


假设楼主要开发一个小电影服务,客户端可以通过请求 URI 对电影进行 CRUD 操作。为简明起见,这儿不使用数据库,只在内存中模拟。先用一个非常简单的 Movie 类,在后续的文章中根据情况逐步扩充:

        public class Movie {
            private int id;
            private String title;
            // 此处省略若干行
        }
    

嗯,就是一个很普通的 JavaBean,实际项目中可以根据需要加上 @Entity 等注解。接下来看看如何编写 JAX-RS 服务。


一个 JAX-RS 服务就是一个使用了 JAX-RS 注解来将 HTTP 请求绑定到方法的 Java 类,一共支持两种类型:单请求对象或单例对象。单请求对象意味着每来一个请求,就创建一个服务对象,在请求结束时销毁。单例对象则意味着只有一个服务对象处理所有的请求,从而可以在多个请求间维持服务状态。JAX-RS 服务可通过继承 javax.ws.rs.core.Application 来定义,其中的 getClasses 方法返回单请求对象的类型,getSingletons 方法返回单例对象的类型。这两个方法是可选的。在 Java EE 6 环境中,如果这两个方法都返回 null 或者空集合,那么应用程序中的所有 JAX-RS 都将被部署。这时可以用 CDI 的 @javax.inject.Singleton 或者 EJB 的 @javax.ejb.Singleton 注解来指定单例对象。

如果电影服务的上下文根路径为 http://localhost/ms,而楼主希望将服务部署到 http://localhost/ms/rest 下面,只需要写一个类:

        @ApplicationPath("rest")
        public class RestApplication extends Application {
        }
    

@ApplicationPath 注解指定所有服务的相对基址,如果为空字符串,则直接使用上下文根路径。另一种配置方式是在 web.xml 文件中进行声明,那是为了使 JAX-RS 能在 Servlet 容器(例如 Tomcat)中运行,此处略过。这项配置必不可少,否则无法部署服务。


很好很强大,现在开始编写电影服务类 MovieService,先看看声明和初始化:

        @Singleton
        @Path("movie")
        public class MovieService {
            private AtomicInteger ai;
            private ConcurrentMap<Integer, Movie> movieMap;

            @PostConstruct
            private void init() {
                ai = new AtomicInteger();
                movieMap = new ConcurrentHashMap<>();
                int id = ai.getAndIncrement();
                movieMap.put(id, new Movie().setId(id).setTitle("Avatar"));
            }
    

因为楼主只需要一个“内存数据库”,所以用单例对象即可,此处使用 CDI 的 @javax.inject.Singleton 来声明单例。@Path 声明了一个服务,它指示 MovieService 负责处理发送到 http://localhost/ms/rest/movie 的请求。路径的拼接方式非常直观。init 方法带有 @PostConstruct 注解,因此将在 MovieService 构造完成后立即调用,它向 movieMap 中存入了一个 ID 为 0 的 Movie 对象。为简化代码,Movie 的设置方法都返回 this,有点伪造构建者模式的味道。


接下来看看如何处理 HTTP 请求。

GET

GET 请求用于获取一个或多个资源。在本例中用来获取一部电影的信息:

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

该方法标注了 @GET,表示用来处理向 http://localhost/ms/rest/movie/{id} 发送的 GET 请求。@Path 再次用来绑定路径,注意其参数 {id},它带有花括号,对应 URI 的最后一段,也正好和方法参数 id@PathParam 的值相对应。这种参数还有很多高级用法,以后再介绍。@Produces 注解指定输出格式为 JSON。JAX-RS 内置了很多格式,详见 MediaType 的文档。如果找到了相应 ID 的对象,则将其返回,JAX-RS 会自动加上响应码 200 OK;否则抛出异常,错误码为 404 Not Found。

例如,通过浏览器访问 http://localhost/ms/rest/movie/0,得到的结果为 {"@id":"0","@title":"Avatar"}。

POST

POST 请求用于创建一个资源。在本例中用来创建一部电影:

        @POST
        @Consumes(MediaType.APPLICATION_JSON)
        public Response create(Movie movie) {
            int id = ai.getAndIncrement();
            movieMap.put(id, movie.setId(id));
            return Response.created(URI.create(String.valueOf(id))).build();
        }
    

由于没有 @Path 注解,所以 POST 请求的目标就直接是 http://localhost/ms/rest/movie。Consumes@Produces 相反,表示接受的数据类型,此处 JAX-RS 会自动把 JSON 数据转换为 Movie 对象。返回的响应码为 201 Created,并且带有所创建资源的 URI。

例如,向 http://localhost/ms/rest/movie 发送 POST 请求,正文为 {"@title": "007"},则可以从 FireBug 的网络监控中看到返回的响应码,以及头部中 Location 的值为 http://localhost:8080/rest/service/movie/1。多次发送该 POST 请求,将会创建多个资源,以保证 POST 不是幂等的。

PUT

PUT 请求用于创建或更新一个资源。与 POST 不同,PUT 请求要指定某个特定资源的地址。在本例中用来更新一部电影的信息:

        @PUT
        @Path("{id}")
        @Consumes(MediaType.APPLICATION_JSON)
        public Response update(@PathParam("id") int id, Movie movie) {
            movie.setId(id);
            if (movieMap.replace(id, movie) != null) {
                return Response.ok().build();
            } else {
                throw new WebApplicationException(Response.Status.NOT_FOUND);
            }
        }
    

更新成功就返回 200 OK,否则返回 404 Not Found。这儿先把 movie 对象的 ID 强制改为 URI 所指定的,以免出现不一致。也可以根据需求,将不一致作为异常处理,给客户端返回一个错误码。

顺便啰嗦一句,反正代码在自己手中,楼主也可以把 PUT 搞成非幂等的,例如将 PUT 当成 POST 来处理,就像以前把 GET 和 POST 一视同仁那样。不过咱既然在搞 JAX-RS,就还是要沾染一点 REST 风格,严格遵守 HTTP 才是。

DELETE

DELETE 请求用于删除一个资源。在本例中用来删除一部电影:

        @DELETE
        @Path("{id}")
        public Response delete(@PathParam("id") int id) {
            if (movieMap.remove(id) != null) {
                return Response.ok().build();
            } else {
                throw new WebApplicationException(Response.Status.NOT_FOUND);
            }
        }
    

没什么特别的,该说的前面都说了。

HEAD 和 OPTIONS 请求就忽略吧,用得不太多,也同样挺简单的。


JAX-RS 服务的部署和部署常规 Web 程序一样,打包成 war 文件就可以了。最后赞一下 NetBeans 可以为 REST 风格的服务自动生成测试页面,很好用,虽然在 Firefox 下页面显示不正常(对此我已经提了一个 bug),但 IE 是可以的。

posted @ 2011-09-20 17:22 蜀山兆孨龘 阅读(9730) | 评论 (3)编辑 收藏

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