简单的移动企业应用程序






4.70/5 (6投票s)
如何创建一个简单的、端到端的、移动的、Java企业应用程序,包括一个RESTful Web服务和一个Android客户端。
引言
本文面向刚开始学习企业应用程序开发或正在学习如何将此处描述的组件组合应用于应用程序的开发人员,展示了如何构建一个简单的、端到端的移动企业应用程序。我们的示例包括一个RESTful Web服务,该服务提供对Oracle Glassfish应用程序服务器上的Java实体Bean的访问,以及一个简单的Android客户端来消费它。
实体Bean在Android和Glassfish之间的序列化通过JSON编码。除非另有说明,所有代码均使用Java编写。
背景
今年早些时候(2012年),在加拿大多伦多汉伯学院的一个学生作业中,我突然想到一个绝妙的主意:可以同时演示如何通过Android应用程序消费我们的RESTful Web服务,以及如何通过JSON序列化Java实体Bean。几周后,令我沮丧的是,我的Java讲师告诉我,这个想法并非原创,甚至他自己过去的班级也未曾有过。尽管如此,我仍然没有在网上找到另一篇文章能完全复制本文的所有内容(尽管我的搜索可能不够彻底);因此,我将我辛苦得来的宝贵见解,作为一盏温暖的灯,为全人类分享。(此处包含自嘲且受《复仇者联盟》启发的讽刺意味。)
本文展示的代码已从我的作业中提取并简化。
使用代码
我们假设我们的读者知道如何创建数据库、EJB托管的RESTful Web服务以及Android Activity的简单示例。然而,即使只有一点编程经验,您也能理解代码的大致意思。
对于NetBeans Web服务项目,我添加了来自jettison.codehaus.org的免费、最新版本的jettison.jar。它包含了JSON STaX实现,其中包含BadgerFish将JSON映射到XML的功能。
Data Model
数据库
底层数据库表包含本文目的所需的足够字段:一个标识符、一个标签和一个用于更新的信息字段。以下是描述它的MySQL DDL脚本片段:
CREATE TABLE `simpleuser` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
实体
我使用NetBeans中的“新建 > 从数据库生成实体类”命令生成了相应的实体类。
public class SimpleUser implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@NotNull
@Column(name = "id")
private Integer id;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 255)
@Column(name = "name")
private String name;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 255)
@Column(name = "email")
private String email;
...
}
业务逻辑
RESTful Web服务提供了对实体Bean的基本CRUD访问操作,在REST范式中,实体Bean就是资源:创建、检索、更新、删除。
Glassfish包含了Jersey,它是用于在Java中构建RESTful Web服务的参考实现。我们还添加了Jettison库中提供的JSON/XML映射,该库在jettison.codehaus.org上可用,并使用了BadgerFish。
我们的Web服务由一个无状态的Enterprise JavaBean SimpleUserResource实现。我们还将REST ApplicationPath属性设置为“rest”。
Create
我们采用的REST范式要求通过HTTP POST请求创建资源。此代码片段包含了我们实现类的开头。
import java.net.URI;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.xml.bind.JAXBElement;
import org.codehaus.jettison.json.JSONArray;
@Path("/appuser")
@Produces(MediaType.WILDCARD)
@Consumes(MediaType.WILDCARD)
@Stateless
public class SimpleUserResource {
@PersistenceContext(unitName = "SimpleUserRESTService-warPU")
private EntityManager entityManager;
@Context
private UriInfo uriInfo;
@POST
@Consumes({MediaType.APPLICATION_JSON})
public Response createSimpleUser(JAXBElement<SimpleUser> userJAXB) {
// Save the new user to the db.
SimpleUser user = userJAXB.getValue();
user.setId(0); // set the id to nonesense to generate a new id
entityManager.persist(user);
// Refresh the persistence context for the newly generated id.
// Can't just use EntityManager.merge() because the managed instance
// that it returns does not contain the newly generated id, either.
// (Pesist() makes the argument instance managed.)
entityManager.flush();
// Return the HTTP 201 Created response with the new user id.
URI userURI = uriInfo.getAbsolutePathBuilder().path(user.getId().
toString()).build();
return Response.created(userURI).build();
}
...
@Path
属性指定了URL路径组件,它指示Glassfish将包含的HTTP请求定向到此EJB。
@Consumes
和@Produces
属性分别指定了接收到的HTTP请求内容和发送的响应的可接受MIME类型。(当然,WILDCARD表示所有类型。)为方法指定的属性会覆盖类上的相应属性。
JAXBElement参数包装了POST的实体Bean,此时BadgerFish已将Bean的HTTP JSON表示转换为XML,然后Project JAXB将XML表示转换为SimpleUser
类实例。
检索
我们采用的REST范式要求通过HTTP GET请求检索资源。
有两种基本的检索方式。一种是检索特定资源。
@GET
@Path("{id}/")
@Produces({MediaType.APPLICATION_JSON})
public SimpleUser retrieveSimpleUser(@PathParam("id") int id) {
URI userURI = uriInfo.getAbsolutePathBuilder().build();
if (id < 1) {
ResponseBuilder rBuild = Response.status(
Response.Status.NOT_FOUND).entity(
userURI.toASCIIString());
throw new WebApplicationException(rBuild.build());
}
SimpleUser user = entityManager.find(SimpleUser.class, id);
if (user == null) {
ResponseBuilder rBuild = Response.status(
Response.Status.NOT_FOUND).entity(
userURI.toASCIIString());
throw new WebApplicationException(rBuild.build());
}
return user;
}
@Path
中的{id}
模板指定,当HTTP GET请求的URL在“appuser”之后带有附加路径组件时,将调用retrieveSimpleUser()
来处理该请求,并将该附加组件的文本作为整数传递给名为id
的参数。
返回的SimpleUser实例将首先由JAXB转换为XML,然后由BadgerFish转换为JSON,最后在生成的HTTP响应中发送给客户端。
另一种检索方式是获取位于特定目录的资源列表。
@GET
@Produces(MediaType.APPLICATION_JSON)
public JSONArray retrieveSimpleUsers() {
// Perform query.
Query query = entityManager.createNamedQuery("SimpleUser.findAll");
List<SimpleUser> userList = query.getResultList();
// Translate result list to JSON array.
JSONArray resUriArray = new JSONArray();
for (SimpleUser user : userList) {
UriBuilder ub = uriInfo.getAbsolutePathBuilder();
URI resUri = ub.path(user.getId().toString()).build();
resUriArray.put(resUri.toASCIIString());
}
return resUriArray;
}
返回的数组将包含实体的URI作为JSON字符串,而不是实体本身。
更新
我们采用的REST范式要求通过HTTP PUT请求更新资源。
@PUT
@Consumes({MediaType.APPLICATION_JSON})
public Response updateSimpleUser(JAXBElement<SimpleUser> userJAXB) {
SimpleUser user = userJAXB.getValue();
URI userURI = uriInfo.getAbsolutePathBuilder().
path(user.getId().toString()).build();
try {
// Ensure that this is an update, not an insert, for merge()
// can create a new entity, too.
SimpleUser matchU = (SimpleUser) entityManager.find(
SimpleUser.class, user.getId());
if (matchU == null) {
String msg = "PUT is the wrong HTTP request for creating
new user " + user.getId() + ".";
ResponseBuilder rBuild = Response.status(
Response.Status.BAD_REQUEST).entity(msg);
throw new WebApplicationException(rBuild.build());
}
// Save the current properties of the specified user to the db.
entityManager.merge(user);
return Response.ok().build();
} catch (IllegalArgumentException ex) {
ResponseBuilder rBuild = Response.status(
Response.Status.NOT_FOUND).entity(
userURI.toASCIIString());
return rBuild.build();
}
}
删除
我们采用的REST范式要求通过HTTP DELETE请求删除资源。
@DELETE
@Path("{id}/")
public Response deleteSimpleUser(@PathParam("id") int id) {
URI userURI = uriInfo.getAbsolutePathBuilder().build();
if (id < 1) {
ResponseBuilder rBuild = Response.status(
Response.Status.NOT_FOUND).entity(
userURI.toASCIIString());
return rBuild.build();
}
// Find the specified user to remove.
SimpleUser user = entityManager.find(SimpleUser.class, id);
if (user == null) {
ResponseBuilder rBuild = Response.status(
Response.Status.NOT_FOUND).entity(
userURI.toASCIIString());
return rBuild.build();
}
entityManager.remove(user);
// Status 204 No Content means that deletion has occurred.
ResponseBuilder rBuild = Response.status(Response.Status.NO_CONTENT);
return rBuild.build();
}
@Path
中的{id}
模板与retrieveSimpleUser()
中的用法相同。
Android客户端
现在,请准备好迎接这款Android客户端应用程序的惊人魅力。
此UI中显示的是实体Bean,其id
=3,name
=”Dustin Penner”,email
="dustin.penner@simpleuser.org"。
现在,构建此应用程序的一个考虑因素是如何序列化Bean。典型的现代软件开发者,习惯于使用应用程序框架,会本能地从互联网上寻找一个能够自动处理JSON序列化的库,例如,jackson at jackson.codehaus.org。您知道这种思路——不要重复造轮子;不要因为害怕编程自己的代码而产生错误;将责任外包给库的开发者。然而,我认为在这种情况下,为客户端添加一个库有点大材小用。SimpleUser
实体是*简单的*——我们可以自己处理。我们可以在实体类中包含自己的转换方法,而这个实体类最终需要作为库导入到Android客户端项目中。我们不必承担在多个客户端应用中包含和更新额外库的开销。而且,为了启发读者,我们还可以在本文中展示JSON序列化是如何工作的。
让我们创建一个实体来表示这位伟大的、年轻的加拿大冰球后卫 Drew Doughty。客户端中的方法通过原始的HttpURLConnection
发出HTTP POST请求,如下所示:
private void createUser(SimpleUser user) {
URL url;
HttpURLConnection connection = null;
try {
// Create connection.
String wsUri = getBaseContext().getResources().getString(
R.string.rest_web_service_uri);
url = new URL(wsUri);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
String userSer = user.getJSONSerialization();
connection.setRequestProperty("Content-Length", Integer
.toString(userSer.getBytes().length));
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
// Send request.
DataOutputStream wr = new DataOutputStream(connection
.getOutputStream());
wr.writeBytes(userSer);
wr.flush();
wr.close();
...
此方法还会通过Toast
弹出窗口显示服务器的HTTP响应,但为了本文的重点,我们将忽略这一点。
现在我们来看看SimpleUser
的序列化方法。
public String getJSONSerialization() {
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append(serializeJSONField("id", Integer.toString(id)) + ",");
sb.append(serializeJSONField("name", name) + ",");
sb.append(serializeJSONField("email", email) + ",");
sb.append("}");
return sb.toString();
}
private String serializeJSONField(String name, String value) {
StringBuilder sb = new StringBuilder();
sb.append("\"");
sb.append(name);
sb.append("\":\"");
sb.append(value);
sb.append("\"");
return sb.toString();
}
优雅,不是吗?这就是JSON序列化相比于JAXB XML序列化的优点——开销很小。
因此,当一位喜爱冰球的Android粉丝想永远记录下一位冰球明星的名字和电子邮件时,他按下Create User
按钮,就会绕过任何典型的防火墙,直接向我们的Web服务发送如下文本请求:
POST /SimpleUserRESTService-war/rest/appuser HTTP/1.1
Host: localhost:8080
Accept: */*
Content-Type: application/json
Content-Length: 70
{"id":"0","name":"Drew Doughty","email":"drew.doughty@simpleuser.org"}
点击Retrieve All Users
后,粉丝可以在下拉列表中选择最后一个用户ID,然后点击Retrieve User Details
来检查他新增的用户。
private void retrieveUserDetails() {
URL url;
HttpURLConnection connection = null;
try {
// Create connection with the selected user.
String wsUri = getBaseContext().
getResources().getString(
R.string.rest_web_service_uri);
Spinner spinner = (Spinner) findViewById(
R.id.retrieveAllSpinner);
String userId = (String) spinner.getSelectedItem();
wsUri += userId;
url = new URL(wsUri);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
// Send request.
connection.connect();
int rspCode = connection.getResponseCode();
// Load in the response body.
BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String rspBody = sb.toString();
if (rspCode == HttpURLConnection.HTTP_OK) {
// Deserialize the SimpleUser.
currentUser = new SimpleUser(rspBody);
// Load the detail text view.
TextView detailsTextView = (TextView) findViewById(
R.id.detailsTextView);
String userLabel = currentUser.getName() + " (" +
currentUser.getId() + ")";
detailsTextView.setText(userLabel);
// Load the email edit view.
EditText emailEditText = (EditText) findViewById(
R.id.emailEditText);
emailEditText.setText(currentUser.getEmail());
}
...
这会导致服务器收到如下HTTP请求和响应:
GET /SimpleUserRESTService-war/rest/appuser/4 HTTP/1.1
Host: localhost:8080
Accept: application/json
HTTP/1.1 200 OK
X-Powered-By: Servlet/3.0 JSP/2.2 (GlassFish Server Open Source Edition 3.1.1 Java/Oracle Corporation/1.7)
Server: GlassFish Server Open Source Edition 3.1.1
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 29 May 2012 19:28:14 GMT
{"email":"drew.doughty@simpleuser.org","id":"4","name":"Drew Doughty"}*
此响应在SimpleUser
构造函数中被反序列化,如下所示:
public SimpleUser(String jSONSerialization) {
try {
String valStr = extractJSONFieldValue(jSONSerialization, "id");
this.id = Integer.parseInt(valStr);
this.name = extractJSONFieldValue(jSONSerialization, "name");
this.email = extractJSONFieldValue(jSONSerialization, "email");
} catch (UnsupportedEncodingException ex) {
Logger.getLogger(SimpleUser.class.getName()).
log(Level.SEVERE, null, ex);
ex.printStackTrace();
}
}
private String extractJSONFieldValue(String jSONSerialization,
String field) throws UnsupportedEncodingException {
String fieldLab = '"' + field + '"' + ':';
int i = jSONSerialization.indexOf(fieldLab);
if (i < 0)
throw new IllegalArgumentException(
"The JSON serialization is missing field label:" +
fieldLab);
i = jSONSerialization.indexOf('"', i + fieldLab.length());
if (i < 0)
throw new IllegalArgumentException("The JSON serialization " +
"is missing the opening quote for the value for field " +
field);
int j = jSONSerialization.indexOf('"', ++i);
if (j < 0)
throw new IllegalArgumentException("The JSON serialization " +
"is missing the closing quote for the value for field " +
field);
String valStr = jSONSerialization.substring(i, j);
return URLDecoder.decode(valStr, "UTF-8");
}
您应该能够根据此处提供的信息推断出检索所有实体、更新实体和删除实体的必要方法,因此我将不再提供更多代码。
这是粉丝检查完新条目后,Android界面显示的样子:
关注点
实现您自己的JSON序列化,通过HTTP传输数据库对象,非常有趣,因为它很简单,并且几乎可以穿透您的客户端和服务器之间的任何网络障碍。