神奇好望角 The Magical Cape of Good Hope

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

JAX-RS(JSR 311 - Java™ API for RESTful Web Services,用于 REST 风格的 Web 服务的 Java™ API)是 Java EE 6 规范的一部分,其目标在于简化和标准化用 Java 开发 REST 风格的 Web 服务。虽然 Java EE 6 刚出炉的时候,楼主也从头到尾看过这份规范,但苦于没有实际的项目练手,看过又忘了,现在最多算达到大成傻逼的境界。这次边看边写,期望完成后至少能破入小成牛逼。先从 REST 本身开始。


REST(REpresentational State Transfer,代表性状态传输)自称是一种风格而非标准,这在楼主看来有炒作的嫌疑。如果仅仅是一种风格,那么不同的框架如何兼容?所以才有 JAX-RS 的诞生。REST 最大的贡献是带来了 HTTP 协议的复兴。为什么叫复兴呢?本来 HTTP 的功能挺丰富的,可惜长期以来只被用作传输数据,大好青年被埋没了。楼主记得刚开始学 Servlet 的时候,一向是把 doGetdoPost 两个方法一视同仁的,因为书上这么教,很多 Web 框架也这么搞,以至于弄了很久才搞清楚 GETPOST 是两种不同的请求。现在 REST 拍砖说道,HTTP 早就定义好了一堆操作,以前一直被混淆使用,现在应该重新赋予它们本来的意义了,而且如果充分发挥 HTTP 的功能,完全能够胜任分布式应用的开发(传说中的 SOA)。


SOA 的理念在于将系统设计为一系列可重用的、解耦的、分布式的服务。这也不是新鲜玩意儿了,早期有 CORBA,稍晚有 SOAP 等等。REST 作为后起之秀,能够快速崛起,也必有其非同寻常的特色。下面一一列举。

可寻址性(Addressability)

系统中的每个资源都可以通过唯一标识符来访问。小插一句,“标识”的正确读音是 biāozhì。REST 使用 URI(统一资源标识符)管理资源的地址。URI 的概念不解释。一个 URI 可以指向一个或者多个资源。

统一的受限接口(The Uniform, Constrained Interface)

实际上强调了 HTTP 操作的原意。REST 主要使用了 GET、PUT、DELETE、POST、HEAD 和 OPTIONS 这 6 种操作。此处有两个曾经被忽略的 HTTP 概念:幂等(idempotent)和安全(safe)。幂等应该是 HTTP 从数学借来的一个术语(原始的数学意义楼主也不懂),意味着若干次请求的副作用与单次请求相同,或者根本没有副作用。GET、PUT、DELETE、HEAD 和 OPTIONS 都是幂等的:GET、HEAD 和 OPTIONS 都是读操作,容易理解;PUT 用于创建或更新已知 URI 的资源,多次创建或更新同一个资源显然和一次的效果相同;DELETE 删除资源,亦然。安全表示操作不会影响服务器的状态。GET、HEAD 和 OPTIONS 是安全的。POST 既不幂等又不安全,因为和 PUT 不同,POST 创建资源的时候并不知道资源的 URI,所以多个 POST 请求将会创建多个资源。

面向表象(Representation-Oriented)

表象这个词有点拗口,传闻在一个 REST 风格的系统中,服务端和客户端之间传输的咚咚就是表象……表象可以是纯文本、XML、JSON……或者自编的山寨格式。唉,不就是数据么?只不过可以用任意的格式来传输,因为 HTTP 正文里面啥都能放。Content-Type 头用来声明格式,一般是 MIME(多用途因特网邮件扩展),像 text/plaintext/htmlapplication/pdf 这些。MIME 可以带属性,例如 text/html; charset=UTF-8

无态通信(Communicate Statelessly)

REST 服务器只管理资源,而不会像 Web 服务器一样记录客户的会话状态,这些应该由客户端来管理,如此就能增强 REST 服务器的伸缩性。此处的客户端可以是客户端程序、浏览器,甚至一个 Web 应用。总之,REST 只负责库管啦!

HATEOAS

猛词砸来了!HATEOAS = Hypermedia As The Engine Of Application State,超媒体作为应用状态的引擎,怎么看起来比 SaaS(Software as a Service,软件作为服务)还要吓人呢?超文本不过是一只纸老虎,超媒体也瞒不过楼主的天眼:超媒体就是是由文字、图像、图形、视频、音频……链成一体的大杂烩!很简单的一个例子,有些坑爹的电影网站经常发布一些内嵌了广告的电影,播放的时候会弹出广告窗口,里面很多链接,你去点两下就中招了:这个电影文件就算是超媒体。

其实这个词最关键的地方是“状态引擎”。例如楼主在去网购,先选了几个东西,接下来可以干啥呢?可以继续选、可以把购物车清空、可以结账……楼主可以从现状“转换”到其他某些状态,而控制状态转换的那个咚咚就被冠名为状态引擎。多么聪明的词汇啊!楼主发现凡是高手都是造词砖家呀!用超媒体来控制状态转换,就是 HATEOAS:你是要继续看电影还是看广告?看哪个广告?自己慢慢考虑……


REST 相比 CORBA、SOAP、WS-* 之流确实独树一帜,但也难逃玩弄概念的嫌疑。记得大学里讲数据库的老师说过:“你们现在学了这么多理论,其实以后在工作中未必管用。在大街上随便找一个软件培训学校出来的小伙子,他哪儿懂什么第二第三范式啊?但却能把数据库玩儿得飞转!”

posted @ 2011-09-16 15:31 蜀山兆孨龘 阅读(5452) | 评论 (3)编辑 收藏

考虑两个具有一对一关联的实体类,受控方除了有一个自己的主键,还有一个引用主控方的外键。如果主控方和受控方是同生同灭的关系,换句话说,双方的一对一关联一旦确立就不可更改,就可以考虑让双方共享相同的主键,简化受控方的表结构。下面就让楼主通过实例来说明如何用 JPA 2.0 来实现这种映射。

盯着眼前的电脑,楼主想到了一个也许不太贴切的例子:员工和公司配的电脑的关系。员工的主键就是工号。虽然电脑可能被换掉,但电脑实体完全可以用工号做主键,只是把电脑配置的详细信息改掉而已。此处的难点就在与如何将电脑的主键字段同时映射一个员工,请看楼主是怎么一步步推导出来的。

一开始是最想当然的写法:

        public class Computer implements Serializable {
            @Id
            @OneToOne
            private Employee employee;
            // 此处省略若干行
        }
    

然而根据规范,只有这些类型可以作为主键:Java 原始类型(例如 int)、原始包装类型(例如 Integer)、Stringjava.util.Datejava.sql.Datejava.math.BigDecimaljava.math.BigInteger。所以直接拿 Employee 做主键是不行的。顺便提一下,也许某些 JPA 实现自己做了扩展,使得可以直接拿实体类做主键,这已经超出了 JPA 规范的范畴,此处不讨论。

直接映射是行不通了,那有什么间接的方式吗?这时楼主想起了一个特殊的注解:EmbeddedId。该注解的本意是用于联合主键,不过变通一下,是否可以将 Employee 包装进一个嵌入式主键,然后再将这个嵌入式主键作为 Computer 的主键以达到目的?带着这种想法,楼主有了下面的代码:

        @Embeddable
        public class ComputerId implements Serializable {
            @OneToOne
            private Employee employee;
            // 此处省略若干行
        }
    
        public class Computer implements Serializable {
            @EmbeddedId
            private ComputerId id;
            // 此处省略若干行
        }
    

现在又出现了新的问题:JPA 不支持定义在嵌入式主键类中的关联映射。好在天无绝人之路,EmbeddedId 的文档中直接指出,可以使用 MapsId 注解来间接指定嵌入式主键类中的关联映射,而且还附带了一个例子!于是最终的成品就出炉了:

        @Embeddable
        public class ComputerId implements Serializable {
            private String employeeId;
            // 此处省略若干行
        }
    
        public class Computer implements Serializable {
            @EmbeddedId
            private Employee employee;
            @MapsId("employeeId")
            @OneToOne
            private Employee employee;
            // 此处省略若干行
        }
    

唯一的遗憾是,逻辑上的主控方 Employee 现在不得不成为受控方了,好在可以定义级联操作来达到差不多的效果:

        public class Employee implements Serializable {
            @Id
            private String id;
            @OneToOne(mappedBy = "employee", cascade = CascadeType.ALL)
            private Computer computer;
            // 此处省略若干行
        }
    

虽然做到了,但确实挺绕的。希望未来版本的 JPA 能直接支持将实体类作为主键,楼主个人觉得不是一个技术问题。

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

JSF 都 2.0 了,尼玛居然还是无法识别 multipart/form-data(至少参考实现 Mojarra 如此),绑定的属性一个都读不出来,坑爹啊!!!既然官方不支持,咱就自己搞一个补丁,让它不从也得从。

说到底,JSF 的属性绑定功能不外乎是利用 FacesServlet 帮我们把参数进行转换和校验,然后拼装成受管 Bean。而 FacesServlet 必定是通过 HttpServletRequest 的相关方法来读取请求参数,因此只需要在 FacesServlet 之前增加一个过滤器,把文本类型的 Part 参数转换为普通参数就可以了。至于文件类型的 Part,则可以使用一些第三方工具来绑定,例如使用 PrimeFaces 将文件绑定到 File 对象。下图是这种思路的流程:

multipart/form-data 的处理流程

第二步的过滤器就是给 JSF 打的“补丁”:

        /**
         * 该过滤器帮助 {@link FacesServlet} 识别 {@code multipart/form-data} 格式的 POST 请求。
         */
        @WebFilter("*.xhtml")
        public class MultipartFilter implements Filter {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {
            }

            @Override
            public void doFilter(ServletRequest request, ServletResponse response,
                    FilterChain chain) throws IOException, ServletException {
                String contentType = request.getContentType();
                // 判断请求的格式是否为 multipart/form-data。
                if (contentType != null && contentType.startsWith("multipart/form-data")) {
                    MultipartRequest req = new MultipartRequest((HttpServletRequest) request);
                    for (Part part : req.getParts()) {
                        // 如果该 Part 的内容类型不为 null, 那它是一个文件,忽略。
                        if (part.getContentType() == null) {
                            req.addParameter(part.getName(), decode(part));
                        }
                    }
                    chain.doFilter(req, response);
                } else {
                    chain.doFilter(request, response);
                }
            }

            @Override
            public void destroy() {
            }

            /**
             * 将 {@link Part} 对象解码为字符串。
             */
            private String decode(Part part) throws IOException {
                try (InputStreamReader in = new InputStreamReader(
                        part.getInputStream(), StandardCharsets.UTF_8)) {
                    char[] buffer = new char[64];
                    int nread = 0;
                    StringBuilder sb = new StringBuilder();
                    while ((nread = in.read(buffer)) != -1) {
                        sb.append(buffer, 0, nread);
                    }
                    return sb.toString();
                }
            }

            /**
             * {@link HttpServletRequest} 中的请求参数映射是只读的,所以自己封装一个。
             */
            private static class MultipartRequest extends HttpServletRequestWrapper {
                private Map<String, String[]> parameters;

                public MultipartRequest(HttpServletRequest request) {
                    super(request);
                    parameters = new HashMap<>();
                }

                private void addParameter(String name, String value) {
                    String[] oldValues = parameters.get(name);
                    if (oldValues == null) {
                        parameters.put(name, new String[] {value});
                    } else {
                        int size = oldValues.length;
                        String[] values = new String[size + 1];
                        System.arraycopy(oldValues, 0, values, 0, size);
                        values[size] = value;
                        parameters.put(name, values);
                    }
                }

                @Override
                public String getParameter(String name) {
                    String[] values = getParameterValues(name);
                    return values == null ? null : values[0];
                }

                @Override
                public Map<String, String[]> getParameterMap() {
                    return parameters;
                }

                @Override
                public Enumeration<String> getParameterNames() {
                    final Iterator<String> it = parameters.keySet().iterator();
                    return new Enumeration<String>() {
                        @Override
                        public boolean hasMoreElements() {
                            return it.hasNext();
                        }

                        @Override
                        public String nextElement() {
                            return it.next();
                        }
                    };
                }

                @Override
                public String[] getParameterValues(String name) {
                    return parameters.get(name);
                }
            }
        }
    

这儿喷一下,为什么 HttpServletRequest 里面的请求参数映射是只读的,非得要通过继承 HttpServletRequestWrapper 这种蛋疼的弯路来黑?

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

最近闲来无事(楼主确实太懒了),重翻旧账,捣鼓了下 JPA 2.0,通过不断地写代码和谷歌,又有了一些旧瓶装新酒的发现和吐槽。楼主将在这一系列文章中慢慢道来。本次开篇带来的是两个模板类:用作实体类基础框架的 AbstractEntity, 以及实现了对实体的基本 CRUD 操作的 BasicEntityDao

一个实体类必须实现 java.io.Serializable 接口,必须有一个 ID 字段作为主键,且最好覆盖 equalshashCode 方法。因为实体类和数据表有对应关系,所以往往根据 ID 来实现 equalshashCode。这很自然地可以引出一个模板类,所有的实体类都可以从它继承:

        /**
         * 该类可作为实体类的模板,其 {@link #equals(Object)} 和 {@link hashCode()} 方法基于主键实现。
         * 子类只需要实现 {@link #getId()} 方法。
         */
        public abstract class AbstractEntity implements Serializable {
            /**
             * 返回主键。
             */
            public abstract Object getId();

            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (obj == null || getClass() != obj.getClass()) {
                    return false;
                }
                return getId() == null ? false
                        : getId().equals(((AbstractEntity) obj).getId());
            }

            @Override
            public int hashCode() {
                return Objects.hashCode(getId());
            }
        }
    

针对主键的类型,AbstractEntity 可以进一步扩展。例如,可以扩展出一个 UuidEntity,它使用随机生成的 UUID 作为主键:

        @MappedSuperclass
        public class UuidEntity extends AbstractEntity {
            @Id
            private String id;

            @Override
            public String getId() {
                return id;
            }

            @PrePersist
            private void generateId() {
                // 仅在持久化前生成 ID,提升一点性能。
                id = UUID.randomUUID().toString();
            }
        }
    

继续发挥想象,让它支持乐观锁:

        @MappedSuperclass
        public class VersionedUuidEntity extends UuidEntity {
            @Version
            private int version;
        }
    

这儿顺便插嘴吐槽下主键的类型。用整数还是 UUID 好呢?这个问题在网上也是争论纷纷。在楼主看来,两者各有优劣:整数主键性能高,可读性也好,但会对数据迁移,例如合并两个数据库,造成不小的麻烦,因为可能出现一大堆重复的主键;UUID 性能差些,看起来晃眼,虽然据说有些数据库针对性地做了优化,想来也不大可能优于整数,不过好处就是理论上出现重复主键的概率比中彩票还小(福彩除外)。说这么一大堆,其实还是蛮纠结啊……楼主一般倾向于用 UUID,只要服务器的配置够劲,想来不会出现明显的性能问题。

接下来说说 BasicEntityDao,它提供了基本的 CRUD 实现,可以用来为会话 Bean 做模板:

        /**
         * 提供了对实体进行基本 CRUD 操作的实现,可作为会话 Bean 的模板。
         */
        public abstract class BasicEntityDao<T> {
            private Class<T> entityClass;
            private String entityClassName;
            private String findAllQuery;
            private String countQuery;

            protected BasicEntityDao(Class<T> entityClass) {
                this.entityClass = Objects.requireNonNull(entityClass);
                entityClassName = entityClass.getSimpleName();
                findAllQuery = "select e from " + entityClassName + " e";
                countQuery = "select count(e) from " + entityClassName + " e";
            }

            /**
             * 返回用于数据库操作的 {@link EntityManager} 实例。
             */
            protected abstract EntityManager getEntityManager();

            public void persist(T entity) {
                getEntityManager().persist(entity);
            }

            public T find(Object id) {
                return getEntityManager().find(entityClass, id);
            }

            public List<T> findAll() {
                return getEntityManager().createQuery(findAllQuery, entityClass).getResultList();
            }

            public List<T> findRange(int first, int max) {
                return getEntityManager().createQuery(findAllQuery, entityClass)
                        .setFirstResult(first).setMaxResults(max).getResultList();
            }

            public long count() {
                return (Long) getEntityManager().createQuery(countQuery).getSingleResult();
            }

            public T merge(T entity) {
                return getEntityManager().merge(entity);
            }

            public void remove(T entity) {
                getEntityManager().remove(merge(entity));
            }
        }
    

子类只需要提供 getEntityManager() 的实现即可。假设楼主要做一个养鸡场管理系统,对鸡圈进行操作的会话 Bean 就可以简单地写成:

        @Stateless
        public class CoopDao extends BasicEntityDao<Coop> {
            @Persistence
            private EntityManager em;

            public CoopDao() {
                super(Coop.class);
            }

            @Override
            protected EntityManager getEntityManager() {
                return em;
            }

            // 更多方法……
        }
    

posted @ 2011-09-07 17:40 蜀山兆孨龘 阅读(3531) | 评论 (8)编辑 收藏

     摘要: JSF 2.0 大量采用标注,从而使 web/WEB-INF/faces-config.xml 不再必需。本文介绍并比较了三种途径来定义可从页面上的 EL 表达式中引用的受管 Bean。  阅读全文

posted @ 2010-05-15 19:10 蜀山兆孨龘 阅读(4064) | 评论 (0)编辑 收藏

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