libs - niubods/playframework-notes GitHub Wiki

Play的库

play.libs包下包含了很多有用的库,能帮助你完成常见的编程任务。

这些库大多数都是很简单的辅助类库,用起来简单明了:

  • Codec:对数据进行编码和解码的实用工具。
  • Crypto:用来加密的实用工具。
  • Expression:对动态表达式求值。
  • F:用Java进行函数式编程。
  • Files:操作文件系统的辅助工具。
  • I18N:国际化的辅助工具。
  • IO:流操作辅助工具。
  • Images:图像操作实用工具。
  • Mail:电子邮件功能。
  • MimeTypes:处理MIME类型。
  • OAuth:OAuth客户端协议。
  • OAuth2:OAuth2客户端协议。
  • OpenID:OpenID客户端协议。
  • Time:时间和持续时间的实用工具。
  • WS:强大的Web Service客户端。
  • XML:加载XML结构。
  • XPath:使用XPath解析XML。

下面的几个小节提供了关于重要库的详细信息。

用XPath解析XML

XPath 可能是不用代码生成工具解析XML文档的方法中最简单的一个了。 play.libs.XPath 库提供了有效完成这类任务所需的所有要素。

XPath 操作可以用来操纵所有 org.w3.dom.Node 类型。

org.w3.dom.Document xmlDoc = … // retrieve a Document somewhere
  
for(Node event: XPath.selectNodes("events//event", xmlDoc)) {
    
    String name = XPath.selectText("name", event);
    String data = XPath.selectText("@date", event);
    
    for(Node place: XPath.selectNodes("//place", event)) {
        String place = XPath.selectText("@city", place);
        …
    }
    
    …
}

Web Service客户端

play.libs.WS 提供了一个强大的HTTP客户端。在底层它使用了 Async HTTP client

发起一个请求很容易:

HttpResponse res = WS.url("http://www.google.com").get();

一旦你已经有了一个 HttpResponse 对象,你就可以访问响应中的所有属性了。

int status = res.getStatus();
String type = res.getContentType();

你也可以以多种内容类型取回内容体。

String content = res.getString();
Document xml = res.getXml();
JsonElement json = res.getJson();
InputStream is = res.getStream();

你还可以使用 异步 API以非阻塞的方式发起HTTP请求。然后你会收到一个 Promise<HttpResponse> 。响应完成之后你就可以照常使用 HttpResponse 了。

Promise<HttpResponse> futureResponse = WS.url(
    "http://www.google.com"
).getAsync();

用Java进行函数式编程

play.libs.F 库提供了很多来自函数式编程的结构。这些结构可以用来处理复杂抽象的案例。对于那些习惯使用函数式编程的人,我们提供了:

  • Option<T> (T值可以设置也可以不设置)
  • Either<A,B> (包含A值,或者B值)
  • Tuple<A,B> (既包含A值也包含B值)

Option<T>, Some<T>None<T>

在某种情况下当你要写一个可能不返回值的函数时(例如一个 find 方法),一种常见的(不好的)Java模式是在没有结果的时候返回 null 。这种实践是非常危险的,因为函数返回值类型并没有明确显示它可能不会返回一个对象,连空值引用的发明者也承认这是个 “数十亿美元的错误”
Option<T> 优雅地解决了这个问题:函数返回一个 Option<T> 类型,而不是 T 类型。如果如果函数成功执行,它会返回一个 Some<T> (包装了真正的结果)类型的对象,否则返回 None<T> 类型的对象,这两个都是 Option<T> 的子类。
这里有一个例子:

/* Safe division (will never throw a runtime ArithmeticException) */
public Option<Double> div(double a, double b) {
    if (b == 0)
        return None();
    else
        return Some(a / b);
}

这里是调用这个函数的方法:

Option<Double> q = div(42, 5);
if (q.isDefined()) {
    Logger.info("q = %s", q.get()); // "q = 8.4"
}

但是还有还有一种更方便的语法来使用它,借助 Option<T> 实现 Iterable<T>

for (double q : div(42, 5)) {
    Logger.info("q = %s", q); // "q = 8.4"
}

只有当 div 函数执行成功的时候,for循环的循环体值执行一次。

Tuple<A, B>

这个方便的 Tuple<A, B> 类包装了两个类型分别为 B 的对象。你可以使用 _1_2 字段分别取回这两个对象。例如:

public Option<Tuple<String, String>> parseEmail(String email) {
    final Matcher matcher = Pattern.compile("(\\w+)@(\\w+)").matcher(email);
    if (matcher.matches()) {
        return Some(Tuple(matcher.group(1), matcher.group(2)));
    }
    return None();
}

然后:

for (Tuple<String, String> email : parseEmail("[email protected]")) {
    Logger.info("name = %s", email._1); // "name = foo"
    Logger.info("server = %s", email._2); // "server = bar.com"
}

T2<A, B> 类是 Tuple<A, B> 的一个别名。要处理三个元素的元组就用 T3<A, B, C> 类,以此类推,最多到 T5<A, B, C, D, E>

模式匹配

有时候我们需要在Java中使用模式匹配。不幸的是Java并没有内置的模式匹配,而且因为没有函数式结构,很难以库的形式添加它。无论如何我们做出了一个不算很糟的解决方案。

我们的想法是使用最新的 “for循环” 语法来实现Java的基本模式匹配。模式匹配必须既能检查你的对象是不是符合要求的条件,又能提取感兴趣值。Play的模式匹配库是 play.libs.F 的一个组成部分。

我们来看一个简单的例子,你有一个某类型对象的引用,而你想要检查一下它是不是一个以“command”开头的字符串:

标准方法应该是这样:

Object o = anything();
 
if(o instanceof String && ((String)o).startsWith("command:")) {
    String s = (String)o;
    System.out.println(s.toUpperCase());
}

使用了Play的模式匹配库之后,你可以这样写:

for(String s: String.and(StartsWith("command:")).match(o)) {
    System.out.println(s.toUpperCase());
}

只有当符合条件的情况下,for循环会执行一次,而且它会自动提取字符串值而不需要类型转换。因为这里没有使用显示的类型转换,所有东西都是类型安全的,是由编译器检查过的。

Promises

Promise 是Play的自定义 Future 类型。事实上一个 Promise<T> 也是一个 Future<T> 所以你可以像标准的 Future 一样来使用它。

Promise 实例代替 Future 实例在Play中广泛使用 (在Jobs, WS.async, 等……)。

Promisek可以以多种方式组合,例如:

Promise p = Promise.waitAll(p1, p2, p3)
Promise p = Promise.waitAny(p1, p2, p3)
Promise p = Promise.waitEither(p1, p2, p3)

OAuth

OAuth 是一种用于安全API验证的开放协议,它采用了一种简单标准的方法,可用于桌面或网络应用。

目前存在两种不同的规范:OAuthor 1.0 和 OAuthor 2.0。Play提供了作为用户来连接服务端的库,两种规范都可以使用。

常规处理流程如下:

  • 重定向用户到提供商的验证页面
  • 当用户获得授权后,它会被重定向回你的服务器,并且携带有一个未被授权的token
  • 你的服务器拿针对当前用户的准入token来交换未授权的token,这个(未授权的token?)需要保存起来从而想服务发起请求。这一步是服务器对服务器通信。

Play框架会维护大部分处理过程。

OAuth 1.0

OAuth 1.0 的功能由 play.libs.OAuth 类提供,它基于 oauth-signpost 。使用它的服务有 Twitter 还有 Google

要连接到一个服务,你需要使用以下信息创建一个OAuth.ServiceInfo实例,这些信息取自服务提供商:

  • 请求token的URL
  • 准入token的URL
  • 授权URL
  • 用户的key
  • 用户的secret

准入token可以以这种方式取回:

public static void authenticate() {
    // TWITTER is a OAuth.ServiceInfo object
    // getUser() is a method returning the current user 
    if (OAuth.isVerifierResponse()) {
        // We got the verifier; 
        // now get the access tokens using the request tokens
        OAuth.Response resp = OAuth.service(TWITTER).retrieveAccessToken(
            getUser().token, getUser().secret
        );
        // let's store them and go back to index
        getUser().token = resp.token; getUser().secret = resp.secret;
        getUser().save()
        index();
    }
    OAuth twitt = OAuth.service(TWITTER);
    Response resp = twitt.retrieveRequestToken();
    // We received the unauthorized tokens 
    // we need to store them before continuing
    getUser().token = resp.token; getUser().secret = resp.secret;
    getUser().save()
    // Redirect the user to the authorization page
    redirect(twitt.redirectUrl(resp.token));
}

用这一对token给请求签名之后,调用就可以结束了。

mentions = WS.url(url).oauth(TWITTER, getUser().token, getUser().secret).get().getString();

虽然这个例子没有对错误进行检查,但在产品中你需要检查。OAuthor.Response对象持有一个error字段,当有错误产生时这个字段是非空的。很有可能用户没有获得你的准许,因为提供商服务挂掉了或者出了问题。

完整的用法示例在 samples-and-tests/twitter-oauth 中可以找到。

OAuth 2.0

OAuth 2.0比OAth 1.0简单得多,因为它不设计签名请求。使用它的服务有 Facebook37signals

这些功能由 play.libs.OAuth2 提供。

要连接一个服务,你需要使用下列信息创建一个OAuth2实例,这些信息取自服务提供商:

  • 准入token的URL
  • 授权URL
  • 客户ID
  • secret
public static void auth() {
    // FACEBOOK is a OAuth2 object
    if (OAuth2.isCodeResponse()) {
        // authUrl must be the same as the retrieveVerificationCode call
        OAuth2.Response response = FACEBOOK.retrieveAccessToken(authUrl);
        // null if an error occurred
        String accessToken = response.accessToken;
        // null if the call was a success
        OAuth2.Error = response.error;
        // Save accessToken, you will need it to request the service
        index();
    }
    // authUrl is a String containing an absolute URL where the service 
    // should redirect the user back
    // This will trigger a redirect
    FACEBOOK.requestVerificationCode(authUrl);
}

一旦准入token和当前用户关联,你就可以作为用户对服务进行查询了。

WS.url(
    "https://graph.facebook.com/me?access_token=%s", access_token
).get().getJson();

OpenID

OpenID 是一个开放的分布式身份认证系统。你可以很简单的在你的应用中接受新用户而不需要保存特定的用户信息。你只需要通过 OpenID 跟踪授权用户。

这个例子对于如何在Play应用中使用OpenID认证提供了一个高级视图:

  • 对每一次请求,检查用户是否已连接
  • 如果没有,显示一个用户可以提交他OpenID的页面
  • 重定向用户到OpenID提供商那里
  • 当用户返回的时候,获取已验证的OpenID并保存在HTTP session中

OpenID的功能由 play.libs.OpenID 类提供。

@Before(unless={"login", "authenticate"})
static void checkAuthenticated() {
    if(!session.contains("user")) {
        login();
    }
}
 
public static void index() {
    render("Hello %s!", session.get("user"));
}
     
public static void login() {
    render();
}
    
public static void authenticate(String user) {
    if(OpenID.isAuthenticationResponse()) {
        UserInfo verifiedUser = OpenID.getVerifiedID();
        if(verifiedUser == null) {
            flash.error("Oops. Authentication has failed");
            login();
        } 
        session.put("user", verifiedUser.id);
        index();
    } else {
        if(!OpenID.id(user).verify()) { // will redirect the user
            flash.error("Cannot verify your OpenID");
            login();
        } 
    }
}

添加 login.html 模板:

#{if flash.error}
<h1>${flash.error}</h1>
#{/if}
 
<form action="@{Application.authenticate()}" method="POST">
    <label for="user">What’s your OpenID?</label>
    <input type="text" name="user" id="user" />
    <input type="submit" value="login…" />
</form>

最后是 routes 定义:

GET   /                     Application.index
GET   /login                Application.login
*     /authenticate         Application.authenticate
⚠️ **GitHub.com Fallback** ⚠️