给Java开发者的Play Framework(2.4)介绍 Part2:使用Play,Spring,JPA进行开发

来源:互联网 发布:项羽彭城之战 知乎 编辑:程序博客网 时间:2024/04/25 02:28

原文地址:http://skaka.me/blog/2015/08/17/play2/

1. 介绍

这篇文章会使用Play,Spring,JPA(hibernate)开发一个简单的CRUD功能,主要是为了介绍如何使用Play进行开发。

2. 界面截图

很简单的新增和查询功能。我们来看看代码如何实现。

3. 代码实现

1. Model

代码架构使用典型的MVC,分层为Controller-Service-Dao-Model。首先来看Model。

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
//省略了部分字段@Table(name = "test_object")@Entitypublic class TestObject implements EntityClass<Integer>, OperableData {    private Integer id;    private String orderNo;    /**     * 状态     */    private TestObjectStatus status;    //...    /**     * 下单时间     */    private DateTime buyTime;    //...    private List<TestObjectItem> testObjectItemList = new ArrayList<>(0);    @OneToMany(fetch = FetchType.LAZY, mappedBy = "testObject")    public List<TestObjectItem> getTestObjectItemList() {        return testObjectItemList;    }    public void setTestObjectItemList(List<TestObjectItem> testObjectItemList) {        this.testObjectItemList = testObjectItemList;    }    @GeneratedValue(strategy = GenerationType.AUTO)    @Id    public Integer getId() {        return id;    }    public void setId(Integer id) {        this.id = id;    }    @Column(name = "status")    @Enumerated(EnumType.STRING)    public TestObjectStatus getStatus() {        return status;    }    public void setStatus(TestObjectStatus status) {        this.status = status;    }    @Column(name = "buy_time")    @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")    public DateTime getBuyTime() {        return buyTime;    }    public void setBuyTime(DateTime buyTime) {        this.buyTime = buyTime;    }}

很普通的Entity,使用JPA注解,唯一需要注意的是在处理枚举类型和Joda DateTime类型的时候用到了不同的类型注解。

2. Dao

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
//省略了部分方法@Repositorypublic class GeneralDao {    @PersistenceContext    EntityManager em;    public GeneralDao(){}    public GeneralDao(EntityManager em) {this.em = em;}    public EntityManager getEm() {        return em;    }    /**     * 使用jpql进行查询     * @param ql jpql     * @param page 分页对象,可选     * @param queryParams 查询参数     * @param <T>     * @return     */    public <T> List<T> query(String ql, Optional<Page<T>> page, Map<String, Object> queryParams) {      //...    }    /**     * 使用jpql进行数据更新操作     * @param ql     * @param queryParams     * @return     */    public int update(String ql, Map<String, Object> queryParams) {      //...    }    public <T extends EntityClass<Integer>> void persist(T t) {        setOperableDataIfNecessary(t, t.getId() == null || t.getId() == 0);        em.persist(t);    }    public <T extends EntityClass<Integer>> T merge(T t) {        setOperableDataIfNecessary(t, t.getId() == null || t.getId() == 0);        return em.merge(t);    }    public <T extends EntityClass<Integer>> boolean remove(T t) {        if(t != null) {            em.remove(t);            return true;        } else {            return false;        }    }    public <T extends EntityClass<Integer>> boolean removeById(Class<T> type, Integer id) {        T t = get(type, id);        return remove(t);    }    public <T extends EntityClass<Integer>> T get(Class<T> type, Integer id) {        return em.find(type, id);    }    public void flush() {        em.flush();    }    public <T extends EntityClass<Integer>> void refresh(T t) {        em.refresh(t);    }    public <T extends EntityClass<Integer>> void detach(T t) {        em.detach(t);    }}

这里使用的是通用Dao,一般的增删改查操作可以直接通过该Dao完成。可以看出这个Dao只是对JPA的EntityManager一个简单封装, 大部分操作还是委派给EntityManager完成。代码中也可以直接取得EntityManager进行操作。

3. Service

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
//省略了部分方法@Servicepublic class TestObjectService {    @PersistenceContext    EntityManager em;    @Autowired    GeneralDao generalDao;    @Transactional(readOnly = true)    public List<TestObject> findByKey(Optional<Page<TestObject>> page, Optional<String> orderNo, Optional<TestObjectStatus> status,            Optional<DateTime> createTimeStart, Optional<DateTime> createTimeEnd) {        CriteriaBuilder cb = em.getCriteriaBuilder();        CriteriaQuery<TestObject> cq = cb.createQuery(TestObject.class);        Root<TestObject> order = cq.from(TestObject.class);        List<Predicate> predicateList = new ArrayList<>();        if(orderNo.isPresent()) {            predicateList.add(cb.equal(order.get("orderNo"), orderNo.get()));        }        if(createTimeStart.isPresent()) {            predicateList.add(cb.greaterThanOrEqualTo(order.get("createTime"), createTimeStart.get()));        }        if(createTimeEnd.isPresent()) {            predicateList.add(cb.lessThanOrEqualTo(order.get("createTime"), createTimeEnd.get()));        }        if(status.isPresent()) {            predicateList.add(cb.equal(order.get("status"), status.get()));        }        cq.select(order).where(predicateList.toArray(new Predicate[predicateList.size()])).orderBy(cb.desc(order.get("updateTime")));        TypedQuery<TestObject> query = em.createQuery(cq);        if(page.isPresent()) {            CriteriaQuery<Long> countCq = cb.createQuery(Long.class);            countCq.select(cb.count(countCq.from(TestObject.class))).where(predicateList.toArray(new Predicate[predicateList.size()]));            Long count = em.createQuery(countCq).getSingleResult();            page.get().setTotalCount(count.intValue());            query.setFirstResult(page.get().getStart());            query.setMaxResults(page.get().getLimit());        }        List<TestObject> results = query.getResultList();        if(page.isPresent()) {            page.get().setResult(results);        }        return results;    }    @Transactional(readOnly = true)    public TestObject get(Integer id) {        return generalDao.get(TestObject.class, id);    }}

findByKey方法是一个查询方法,这里使用的是JPA的Criteria查询。Service类没有使用接口,只有实现类。Service就是一个Spring管理的Bean, 事务边界在Service层。

4. Controller

1234567891011121314151617181920212223242526
//省略了部分方法@org.springframework.stereotype.Controllerpublic class TestObjectController extends Controller {    @Autowired    private TestObjectService testObjectService;    public Result list(String status, String orderNo) {        List<TestObject> testObjectList = testObjectService.findByKey(of(PageFactory.getPage(request())), ofNullable(orderNo),                ofNullable(status).map(TestObjectStatus::valueOf), empty(), empty());        return ok(list.render(testObjectList));    }    public Result addPage() {        return ok(add.render(Form.form(TestObject.class)));    }    public Result updatePage(Integer id) {        return ok(update.render(Form.form(TestObject.class).fill(testObjectService.get(id))));    }    //...}

Controller继承play.mvc.Controller。和SpringMVC一样,在Play中,Controller就是一系列Action的集合。例如我开发用户有关的功能, 那么我就建一个UserController,然后把用户的CRUD方法都放在UserController里,每个方法都有自己的路由规则。这里我们先来看list方法:

1234567
public Result list(String status, String orderNo) {    List<TestObject> testObjectList = testObjectService.findByKey(of(PageFactory.getPage(request())), ofNullable(orderNo),            ofNullable(status).map(TestObjectStatus::valueOf), empty(), empty());    return ok(list.render(testObjectList));}

这里list的功能是查询出所有满足条件的测试对象(TestObject对象)。首先来看参数,这里声明了两个查询参数status和orderNo,用来匹配Http请求QueryString中的参数, 这和SpringMVC中声明了RequestParam(“status”)的参数类似。注意有一点区别,这里的status和orderNo不能捕捉通过Http Body提交的参数,只能匹配QueryString中的参数。

Controller中调用service方法来完成查询,然后将结果返回。方法的返回值声明是play.mvc.Result接口,你可以理解Result的实现类只需要包含两个值: ResponseHeader和ResponseBody。对于ResponseHeader的设置,这里调用父类的ok方法设置返回的Http状态码为200,对应的还有created 201, notFound 404等方法。 ok方法参数需要传入的就是ResponseBody,参数类型声明为play.twirl.api.Content特质(Scala中的特质类似于Java的接口),你基本上永远不需要去手动构造这个特质的实现, 而是使用Play提供的模板。这里我在views.html.test目录下有一个list.scala.html模板,这个模板文件会被IDE自动编译成views.html.test.list类,类里面有一个render方法来完成模板的渲染, render方法返回值就是play.twirl.api.Content的子类。所以我在这里可以直接调用list.render(testObjectList)方法来完成模板的渲染。

好了,现在来对照SpringMVC,如果是用SpringMVC的话,这个方法应该是这样的

1234567891011
@RequestMapping(value = "/test/objects", method = RequestMethod.GET)@ResponseStatus(HttpStatus.OK)  //可省略public String list(String status, String orderNo, Model model) {    List<TestObject> testObjectList = testObjectService.findByKey(of(PageFactory.getPage(request())), ofNullable(orderNo),            ofNullable(status).map(TestObjectStatus::valueOf), empty(), empty());    model.addAttribute("testObjectList", testObjectList);    return "views/html/test/list";}

细心的你可能已经发现了,Play的版本与SpringMVC的对照,少了一个路由的信息,那么在Play中怎么配置路由呢,请看下节

5. routes文件(路由)

在Play中,所有的路由信息都是统一放在一个文件里,即conf/routes文件。上面的list方法路由在routes中对应如下:

1
GET         /test/objects                     @controllers.test.TestObjectController.list(status ?= null, orderNo ?= null)

最左边的GET声明的是Http Method,在Play中每个路由都要明确写出对应的Http Method,中间是路由的URI,最右边是映射的Controller方法。参数status ?= null代表参数是可选的, 如果请求参数中没有status则默认值是null。routes文件的一大好处是在写映射Controller方法的时候IDE能帮助自动补全,并且编译器在编译的时候也能校验声明的参数个数与类型是否一致, 这能有效的帮助开发者减少错误。路由也配好了,剩下的工作就是模板的编写。

6. 模板

list.scala.html

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
@(testObjects: List[ordercenter.models.TestObject])@import common.utils.DateUtils._@main()() {    <div class="breadcrumbs" id="breadcrumbs">        <script type="text/javascript">        try{ace.settings.check('breadcrumbs' , 'fixed')}catch(e){}        </script>        <ul class="breadcrumb">            <li>                <i class="icon-home home-icon"></i>测试对象管理            </li>            <li class="active"><a href="@controllers.test.routes.TestObjectController.list()">测试对象查询</a></li>        </ul><!-- .breadcrumb -->    </div>    <div class="page-content">        <div class="page-header">            <h1>                测试对象管理                <small>                    <i class="icon-double-angle-right"></i>测试对象查询                </small>            </h1>        </div>            <!-- /.page-header -->        <div class="row">            <div class="col-xs-12">                    <!-- PAGE CONTENT BEGINS -->                <div class="row">                    <div class="col-xs-12">                        <div class="table-responsive">                            <table id="sample-table-1" class="table table-striped table-bordered table-hover">                                <thead>                                    <tr>                                        <th>状态</th>                                        <th>买家</th>                                        <th class="hidden-480">金额</th>                                        <th>下单时间</th>                                        <th></th>                                    </tr>                                </thead>                                <tbody>                                @for(testObject <- testObjects) {                                    <tr>                                        <td>@testObject.getStatus.value</td>                                        <td>@testObject.getBuyerId</td>                                        <td>@testObject.getActualFee</td>                                        <td>@printDateTime(testObject.getCreateTime) </td>                                        <td>                                            <div class="visible-md visible-lg hidden-sm hidden-xs btn-group">                                                <button class="btn btn-xs btn-info" onclick="location.href='@controllers.test.routes.TestObjectController.updatePage(testObject.getId)'">                                                    <i class="icon-edit bigger-120"></i>                                                </button>                                                <button class="btn btn-xs btn-success" onclick="location.href='@controllers.test.routes.TestObjectController.list()'">                                                    <i class="icon-info bigger-120"></i>                                                </button>                                            </div>                                        </td>                                    </tr>                                }                                </tbody>                            </table>                        </div><!-- /.table-responsive -->                    </div><!-- /span -->                </div>            </div>        </div>    </div>}

Play的模板引擎是Twirl,关于这个引擎的介绍可以参考上一篇文章。

@(testObjects: List[ordercenter.models.TestObject])是参数声明,写Play模板的时候,建议参数都通过这种声明的形式传入,而不是使用页面隐藏对象。 因为编译器能够自动帮我们校验类型和个数,重构起来也会更方便。下面一行@import common.utils.DateUtils._引入了定义好的一个工具类, 下面的@printDateTime(testObject.getCreateTime)用来格式化显示时间。

@main()() {...}的形式是调用main模板完成渲染,main模板前两个参数可以省略,第三个参数需要传入html代码。在Scala中,方法调用既可以用小括号,比如println("ok"), 也可以用大括号println{"ok"}。而这里第三个参数使用的是大括号。@for(testObject <- testObjects){}是循环的写法,这里循环testObjects,取出每一条记录用来显示。

onclick="location.href='@controllers.test.routes.TestObjectController.list()'"绑定了onClick事件,用户在点击的时候会跳转到测试对象的编辑页面。 这里没有硬编码uri,而是使用routes反向路由的写法。之所以能这样写,是因为IDE在编译的时候会根据routes文件自动生成一个routes对象, 对象里面的方法对应的就是我们配置好的controller方法映射。这里写的@controllers.test.routes.TestObjectController.list()在模板渲染的时候就会被替换成/test/objects这个URI。

4. 总结

这一篇主要介绍了Play在整合Spring和JPA之后是如何进行开发的。可以看出,开发Play应用与开发SSH应用没有太大区别,只是Controller和模板的写法有所不同, 但是我们能很快享受到Play的便利:简单易用的模板,修改代码无需重启服务器,不需要配置外部服务器,etc。随着业务和技术的扩展,使用Play的项目更容易整合其他服务。 例如整合监控工具StatsD+Graphite+Grafana+Kamon,Docker化,服务化。

这篇文章我没有介绍如何启动应用,因为这需要一些开发环境的准备,以及了解SBT的基本用法。这些内容我会在下一篇博客介绍。这篇文章相关的代码已经提交到Github, 项目地址。 这个项目整合了Play,Spring,JPA,数据存储使用MySQL和Redis,使用Bootstrap作为页面框架,可以作为脚手架项目给有兴趣的朋友进行研究。

除此之外,感兴趣的朋友还可以下载Typesafe Reactive Platform进行学习。这这上面有很多关于Play,Akka的项目模板, 并且你可以通过浏览器查看编辑这些代码,还可以直接运行。另外要进一步学习可以读这本书,网上有电子版的。

下篇我会介绍如何搭建开发环境,以及如何调试应用。掌握了之后,你会发现开发和调试过程原来还能这样直观和简单!


0 0
原创粉丝点击