这是Project Student的一部分。 其他职位包括带有Jersey的 Web服务 客户端,带有Jersey的 Web服务服务器 , 业务层 , 具有Spring数据的持久性 ,分片集成测试数据 , Webservice集成和JPA标准查询 。
当我开始这个项目时,我有四个目标。 他们没有特别的次序:
- 了解jQuery和其他AJAX技术。 为此,我需要一个了解的REST服务器,
- 捕获最近获得的有关球衣和挂毯的知识,
- 创建一个我可以用来了解其他技术(例如spring MVC,restlet,netty)的框架,以及
- 在工作面试中有什么要讨论的
如果对其他人有用–太好了! 这就是为什么它在Apache许可下可用。
(不言而喻,可接受的用途不包括在没有适当归因的情况下将“ Project Student”变成学生项目!)
学习AJAX的问题在于,我一开始会不确定问题的根源。 jQuery不好吗? 不良的REST服务? 还有吗 广泛的单元和集成测试是一个好的开始,但始终会存在一些不确定性。
另一个考虑因素是,在现实世界中,我们经常需要对数据库的基本视图。 它不会供公众使用,而是当我们遇到WTF时刻时供内部使用。 它也可以用于维护我们不想通过公共界面管理的信息,例如下拉菜单中的值。
对此可以稍作改动,以提供适度的可伸缩性。 将大型服务器用于数据库和REST服务,然后让N台前端服务器运行常规的Web应用程序,充当用户和REST服务之间的中介。 前端服务器可以是相当轻量的,并且可以根据需要旋转。 将缓存服务器放置在前端和REST服务器之间的加分点,因为将读取压倒性的点击量。
这种方法无法扩展到Amazon或Facebook的规模,但是对于许多站点来说已经足够了。
维护Webapp
这将我们带到了webapp onion的可选层上-传统的webapp充当REST服务的前端。 由于各种原因,我在应用程序中使用Tapestry 5 ,但这是一个任意决定,我不会花很多时间研究Tapestry特定的代码。
您可以使用创建新的挂毯项目
$ mvn archetype:generate -DarchetypeCatalog=http://tapestry.apache.org
我已经在http://jumpstart.doublenegative.com.au/jumpstart/examples/找到了有价值的示例。 在适当的地方,我会注明出处。
稍后,我还将创建webapp的第二个可选层-使用Selenium和WebDriver (即Selenium 2.0)进行功能和回归测试。
局限性
只读 –分解webapp需要大量工作,因此初始版本将仅提供对简单表的只读访问。 没有更新,没有一对多映射。
用户身份验证 –尚未进行身份验证的工作。
加密 –尚未对通信进行加密。
数据库锁 –我们在休眠版本中使用机会锁定,而不是显式数据库锁定。 安全说明:根据最少公开的原则,除非需要,否则我们不想使该版本可见。 一个好的规则是,您将看到它是否请求一个特定的对象,但看不到列表中的对象。
REST客户端 -每种类型的默认GET处理程序非常粗糙-它仅返回对象列表。 我们需要更复杂的响应(例如,记录数,起始索引和结束索引,状态码等),并将让UI驱动它。 现在,我们真正需要的只是一个计数,我们可以只请求列表并计数元素的数量。
目标
我们需要一个列出数据库中所有课程的页面。 它不必担心分页,排序等问题。它应该具有用于编辑和删除记录的链接(可能是无效的)。 它不需要添加新课程的链接。
该页面应如下所示:
课程模板
Tapestry页面上列出的课程很简单–基本上只是修饰的值网格 。
(有关Layout.tml等信息,请参见挂毯原型。)
<html t:type="layout" title="Course List"
t:sidebarTitle="Framework Version"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:p="tapestry:parameter">
<!-- Most of the page content, including <head>, <body>, etc. tags, comes from Layout.tml -->
<t:zone t:id="zone">
<p>
"Course" page
</p>
<t:grid source="courses" row="course" include="uuid,name,creationdate" add="edit,delete">
<p:name>
<t:pagelink page="CourseEditor" context="course.uuid">${course.name}</t:pagelink>
</p:name>
<p:editcell>
<t:actionlink t:id="edit" context="course.uuid">Edit</t:actionlink>
</p:editcell>
<p:deletecell>
<t:actionlink t:id="delete" context="course.uuid">Delete</t:actionlink>
</p:deletecell>
<p:empty>
<p>There are no courses to display; you can <t:pagelink page="Course/Editor" parameters="{ 'mode':'create', 'courseUuid':null }">add some</t:pagelink>.</p>
</p:empty>
</t:grid>
</t:zone>
<p:sidebar>
<p>
[
<t:pagelink page="Index">Index</t:pagelink>
]<br/>
[
<t:pagelink page="Course/List">Courses</t:pagelink>
]
</p>
</p:sidebar>
</html>
具有的属性文件
title=Courses
delete-course=Delete course?
我已经包括了用于编辑和删除的操作链接,但是它们不起作用。
GridDataSources
我们的页面需要显示值的来源。 这需要两个类。 第一个定义了上面使用的课程属性。
package com.invariantproperties.sandbox.student.maintenance.web.tables;
import com.invariantproperties.sandbox.student.business.CourseFinderService;
public class GridDataSources {
// Screen fields
@Property
private GridDataSource courses;
// Generally useful bits and pieces
@Inject
private CourseFinderService courseFinderService;
@InjectComponent
private Grid grid;
// The code
void setupRender() {
courses = new CoursePagedDataSource(courseFinderService);
}
}
实际的实现是
package com.invariantproperties.sandbox.student.maintenance.web.tables;
import com.invariantproperties.sandbox.student.business.CourseFinderService;
import com.invariantproperties.sandbox.student.domain.Course;
import com.invariantproperties.sandbox.student.maintenance.query.SortCriterion;
import com.invariantproperties.sandbox.student.maintenance.query.SortDirection;
public class CoursePagedDataSource implements GridDataSource {
private int startIndex;
private List<Course> preparedResults;
private final CourseFinderService courseFinderService;
public CoursePagedDataSource(CourseFinderService courseFinderService) {
this.courseFinderService = courseFinderService;
}
@Override
public int getAvailableRows() {
long count = courseFinderService.count();
return (int) count;
}
@Override
public void prepare(final int startIndex, final int endIndex, final List<SortConstraint> sortConstraints) {
// Get a page of courses - ask business service to find them (from the
// database)
// List<SortCriterion> sortCriteria = toSortCriteria(sortConstraints);
// preparedResults = courseFinderService.findCourses(startIndex,
// endIndex - startIndex + 1, sortCriteria);
preparedResults = courseFinderService.findAllCourses();
this.startIndex = startIndex;
}
@Override
public Object getRowValue(final int index) {
return preparedResults.get(index - startIndex);
}
@Override
public Class<Course> getRowType() {
return Course.class;
}
/**
* Converts a list of Tapestry's SortConstraint to a list of our business
* tier's SortCriterion. The business tier does not use SortConstraint
* because that would create a dependency on Tapestry.
*/
private List<SortCriterion> toSortCriteria(List<SortConstraint> sortConstraints) {
List<SortCriterion> sortCriteria = new ArrayList<>();
for (SortConstraint sortConstraint : sortConstraints) {
String propertyName = sortConstraint.getPropertyModel().getPropertyName();
SortDirection sortDirection = SortDirection.UNSORTED;
switch (sortConstraint.getColumnSort()) {
case ASCENDING:
sortDirection = SortDirection.ASCENDING;
break;
case DESCENDING:
sortDirection = SortDirection.DESCENDING;
break;
default:
}
SortCriterion sortCriterion = new SortCriterion(propertyName, sortDirection);
sortCriteria.add(sortCriterion);
}
return sortCriteria;
}
}
应用模块
现在有了GridDataSource,我们可以看到它的需求– CourseFinderService。 虽然有Tapestry-Spring集成,但我们希望保持维护Webapp尽可能的薄,所以现在我们使用标准的Tapestry注入。
package com.invariantproperties.sandbox.student.maintenance.web.services;
import com.invariantproperties.sandbox.student.business.CourseFinderService;
import com.invariantproperties.sandbox.student.business.CourseManagerService;
import com.invariantproperties.sandbox.student.maintenance.service.impl.CourseFinderServiceTapestryImpl;
import com.invariantproperties.sandbox.student.maintenance.service.impl.CourseManagerServiceTapestryImpl;
/**
* This module is automatically included as part of the Tapestry IoC Registry,
* it's a good place to configure and extend Tapestry, or to place your own
* service definitions.
*/
public class AppModule {
public static void bind(ServiceBinder binder) {
binder.bind(CourseFinderService.class, CourseFinderServiceTapestryImpl.class);
binder.bind(CourseManagerService.class, CourseManagerServiceTapestryImpl.class);
}
....
}
请注意,我们正在使用标准的CourseFinderService接口以及特定于挂毯的实现。 这意味着我们可以直接使用标准实现,只需要对配置文件进行一点改动即可!
CourseFinderServiceTapestryImpl
CourseFinderService接口的本地实现必须使用REST客户端而不是Spring Data实现。 使用早期使用的由外而内的方法,Tapestry模板的需求应推动服务实现的需求,进而驱动REST客户端和服务器的需求。
package com.invariantproperties.sandbox.student.maintenance.service.impl;
public class CourseFinderServiceTapestryImpl implements CourseFinderService {
private final CourseFinderRestClient finder;
public CourseFinderServiceTapestryImpl() {
// resource should be loaded as tapestry resource
final String resource = "http://localhost:8080/student-ws-webapp/rest/course/";
finder = new CourseFinderRestClientImpl(resource);
// load some initial data
initCache(new CourseManagerRestClientImpl(resource));
}
@Override
public long count() {
// FIXME: grossly inefficient but good enough for now.
return finder.getAllCourses().length;
}
@Override
public long countByTestRun(TestRun testRun) {
// FIXME: grossly inefficient but good enough for now.
return finder.getAllCourses().length;
}
@Override
public Course findCourseById(Integer id) {
// unsupported operation!
throw new ObjectNotFoundException(id);
}
@Override
public Course findCourseByUuid(String uuid) {
return finder.getCourse(uuid);
}
@Override
public List<Course> findAllCourses() {
return Arrays.asList(finder.getAllCourses());
}
@Override
public List<Course> findCoursesByTestRun(TestRun testRun) {
return Collections.emptyList();
}
// method to load some test data into the database.
private void initCache(CourseManagerRestClient manager) {
manager.createCourse("physics 101");
manager.createCourse("physics 201");
manager.createCourse("physics 202");
}
}
我们的JPA Criteria查询可以快速计数,但是我们的REST客户端尚不支持。
包起来
完成艰苦的工作后,我们将获得一个维护文件.war。 我们可以使用webservice .war将其部署在我们的应用服务器上,也可以不部署。 除了Web服务的临时硬编码URL外,这两个.war文件没有理由必须位于同一系统上。
我们首先应该转到http:// localhost:8080 / student-maintenance-webapp / course / list。 我们应该看到如上所示的简短课程清单。 (在那种情况下,我已经重新启动了webapp三次,因此每个条目都会重复三倍。)
现在,我们应该访问位于http:// localhost:8080 / student-ws-webapp / rest / course的webservice webapp,并验证我们是否也可以通过浏览器获取数据。 经过一点清理后,我们应该看到:
{"course":
[
{
"creationDate":"2013-12-28T14:40:21.369-07:00",
"uuid":"500069e4-444d-49bc-80f0-4894c2d13f6a",
"version":"0",
"name":"physics 101"
},
{
"creationDate":"2013-12-28T14:40:21.777-07:00",
"uuid":"54001b2a-abbb-4a75-a289-e1f09173fa04",
"version":"0",
"name":"physics 201"
},
{
"creationDate":"2013-12-28T14:40:21.938-07:00",
"uuid":"cfaf892b-7ead-4d64-8659-8f87756bed62",
"version":"0",
"name":"physics 202"
},
{
"creationDate":"2013-12-28T16:17:54.608-07:00",
"uuid":"d29735ff-f614-4979-a0de-e1d134e859f4",
"version":"0",
"name":"physics 101"
},
....
]
}
源代码
- 源代码位于https://github.com/beargiles/project-student [github]和http://beargiles.github.io/project-student/ [github页面]。
翻译自: https://www.javacodegeeks.com/2014/01/project-student-maintenance-webapp-read-only.html