Phát triển Java 2.0: Khai phá Twitter với Objectify-Appengine, Phần 2

Bắt đầu tiến nhanh đến các ứng dụng web của GAE với Gaelyk, JSON và Ajax

Google App Engine không chỉ mở rộng quy mô các ứng dụng: nó có thể giúp bạn xây dựng ứng dụng nhanh, bằng cách sử dụng các công cụ mà bạn yêu thích. Andrew Glover gói ghém mô hình miền domain dành cho ứng dụng khai phá Twitter của mình, thêm các móc nối tới việc lập chỉ mục và lưu dữ liệu vào một bộ nhớ đệm. Sau đó ông nối nó với cơ chế cấp phép OAuth của Twitter, các hàng đợi của GAE, một chút JSON và Ajax thông qua thư viện JavaScript được nhiều người ưa thích là JQuery.

Andrew Glover, Tác giả và Nhà phát triển, Beacon50

 Andrew GloverAndrew Glover là một nhà phát triển, tác giả, diễn giả và là người điều hành doanh nghiệp với niềm đam mê phát triển các hệ thống điều khiển hành vi, tích hợp liên tục và phát triển phần mềm dựa trên quy trình Agile. Ông đã sáng lập ra nền tảng phát triển điều khiển hành vi easyb và là đồng tác giả của 3 cuốn sách: Continuous Integration (Tích hợp liên tục), Groovy in Action (Thực hành ngôn ngữ Groovy), và Java Testing Patterns (Các mẫu kiểm thử Java). Bạn có thể theo dõi và tìm hiểu về ông qua trang blogTwitter.



02 05 2013

Tương tự như Hibernate, Objectify-Appengine giúp dễ dàng hơn trong việc ánh xạ và sử dụng các POJO (Plain Old Java Object - Đối tượng Java cũ đơn giản) dựa vào kho dữ liệu của Google App Engine, còn được gọi là Bigtable. Trong phần đầu của bài này, tôi đã giới thiệu Objectify và một số tính năng chính của nó, bao gồm các chú thích về JPA (nó cũng chỉ được sử dụng một phần) và các chú giải tùy chỉnh để tích hợp với kho dữ liệu GAE. Tôi cũng đã giải thích cách tiếp cận của Objectify đến các mối quan hệ và cũng đã trình bày về giao diện truy vấn, để hỗ trợ các khái niệm của GAE về sắp xếp và lọc dữ liệu.

Phần 2 thay đổi trọng tâm từ mô hình hóa miền domain đến việc hoàn thành một ứng dụng web để chuẩn bị cho việc triển khai nó trên GAE. Tuy nhiên, trước tiên tôi chỉ muốn dành thêm một vài phút cho mô hình miền domain để tôi có thể giới thiệu các tính năng lập chỉ mục và lưu dữ liệu vào một bộ nhớ đệm của Objectify. Cả hai tính năng này đều khá quan trọng khi triển khai các ứng dụng vào GAE, đặc biệt là nếu bạn có kế hoạch di chuyển một lượng lớn dữ liệu.

Đôi điều về loạt bài này

Bối cảnh phát triển Java đã thay đổi hoàn toàn kể từ khi công nghệ Java đã xuất hiện lần đầu tiên. Nhờ các khung công tác nguồn mở hoàn thiện và cơ sở hạ tầng triển khai cho thuê tin cậy mà ngày nay chúng ta có thể lắp ráp, thử nghiệm, chạy và duy trì các ứng dụng Java một cách nhanh chóng và không tốn kém. Trong loạt bài này, Andrew Glover khám phá một loạt các công nghệ và các công cụ làm cho hình mẫu phát triển Java mới này trở nên khả thi.

Lập chỉ mục và lưu dữ liệu vào một bộ nhớ đệm với Objectify-Appengine

Theo mặc định, tất cả các thuộc tính được định nghĩa trong một đối tượng miền domain (và do đó, cả với API Entity mức thấp của Google) đều được lập chỉ mục. Đúng như trong thế giới quan hệ, lập chỉ mục giúp cho việc tìm kiếm kho dữ liệu bên dưới dễ dàng hơn và nhanh hơn đối với người dùng đầu cuối. Tuy nhiên, việc lập chỉ mục cũng tốn chi phí: khi đối tượng mới được tạo ra — chẳng hạn như một hàng mới, theo thuật ngữ quan hệ hoặc Entity (Thực thể) mới trong Bigtable — bạn phải cập nhật các chỉ mục của mình để tính đến dữ liệu mới thêm vào này.

Khi bạn bắt đầu sử dụng GAE để lưu trữ và lấy ra dữ liệu từ đó, việc lập chỉ mục trở thành một mối quan tâm trong thế giới thực (mối quan tâm này cũng giống như với đồng đô la vậy). Khi bạn đăng ký một tài khoản GAE, bạn sẽ tự động nhận được 200 chỉ mục miễn phí; ngoài số này ra thì bạn sẽ phải trả thêm tiền. Giống như vậy, nếu bạn vượt quá 6,50 giờ CPU mỗi ngày, bạn sẽ bắt đầu nhận một khoản nợ của Google. Vì vậy, lập chỉ mục có chọn lọc là việc nên làm. Nếu bạn không cần lập chỉ mục cho một thuộc tính (tức là bạn không có kế hoạch tìm kiếm một đối tượng miền domain theo thuộc tính riêng lẻ đó), thì việc tắt chỉ mục đó là điều hợp lý. Trong Liệt kê 1, tôi cho bạn thấy cách sử dụng chú giải @Unindexed của Objectify để tắt một chỉ mục cụ thể.

Tôi đã mở rộng đối tượng Retweet từ Phần 1, bằng cách thêm một số chức năng lập chỉ mục. Bạn có thể thấy những thay đổi này trong gói mã ví dụ mẫu. Trong Liệt kê 1, tôi đã sử dụng chú thích @Unindexed để không lập chỉ mục thuộc tính tên thực của người dùng của đối tượng Retweet, thuộc tính đó được đặt tên là userName chứ không phải screenName. Tôi cũng đã không lập chỉ mục URL hình ảnh của người dùng, một tính năng mới cho phép tôi tạo báo cáo tốt hơn trên giao diện người dùng.

Tôi không lập kế hoạch tìm kiếm các đối tượng Retweet theo các thuộc tính này, do đó rõ ràng không cần lập chỉ mục cho chúng. Tất nhiên, tôi có thể loại bỏ chú giải @Unindexed nếu có thứ gì đó thay đổi. Liệt kê 1 cho thấy tôi đã cải tiến các thuộc tính của đối tượng Retweet như thế nào .

Liệt kê 1. Đối tượng Retweet được nạp lại
public class Retweet
 @Id private String id;
 private String screenName;
 private Date date;
 private String tweet;
 private Long influence;
 private Key<User> owner;
 private Long tweetId;
 @Unindexed private String userName;
 @Unindexed private String userPicture;
 //...
}

Hai thuộc tính in đậm ở cuối danh sách —userNameuserPicture— là các thuộc tính mới và cả hai đều không được lập chỉ mục. Tôi đã đổi tên userName thành screenName để tích hợp tốt hơn với Twitter. (Trong Twitter, tên người dùng chính là tên chính thức của bạn — "Andrew Glover" — còn tên hiển thị trên màn hình là tên làm việc của bạn — "aglover".)

Lưu dữ liệu trong memcache

Giống như việc lập chỉ mục, việc lưu dữ liệu vào một bộ nhớ đệm cải thiện trải nghiệm của người dùng đầu cuối: nó loại bỏ yêu cầu truyền dữ liệu khứ hồi đến kho dữ liệu và quay về, do đó giúp cho việc đọc dữ liệu nhanh hơn. Google App Engine sử dụng bộ nhớ đệm trên web (memcache) để lưu dữ liệu và Objectify sử dụng chú thích @Cached để cắm vào nó. Chỉ cần thêm @Cached vào các đối tượng miền của bạn và —hô biến!— dữ liệu (không phải là đối tượng Java™ tương ứng) được lưu trữ trong memcache và có thể được lấy ra khỏi bộ nhớ khi ứng dụng đọc.

Tôi đã thêm chú giải @Cached cho cả hai đối tượng miền của tôi —UserRetweet. Liệt kê 2 cho thấy đối tượng User đã được cập nhật và thêm vào vài thuôc tính @Unindexed:

Liệt kê 2. Lưu dữ liệu vào một bộ nhớ đệm thật là đơn giản!
@Cached
public class User {
 @Id private String name;	
 @Unindexed private String token;
 @Unindexed private String tokenSecret;
	//...
}

Lưu ý rằng các lời gọi query của Objectify vẫn truy cập vào kho dữ liệu; tất cả các cuộc gọi tương tác kho dữ liệu khác, như get, đều sử dụng memcache. Ví dụ, trong đối tượng User trong Liệt kê 2, phương thức findByName sử dụng lời gọi get của Objectify để tải đối tượng miền domain tương ứng. (Xin nhắc lại rằng, name chính là khóa). Nếu đối tượng User đã được tạo ra rồi thì dữ liệu của nó sẽ phải được lưu vào một bộ nhớ đệm, với các lời gọi tiếp theo tới findByName dữ liệu của đối tượng đó được lấy ra từ bộ nhớ đệm chứ không phải từ kho dữ liệu. Hạn ngạch của GAE về các lời gọi memcache cao hơn so với các lời gọi vào datastore (kho lưu trữ), do đó, nó giúp sử dụng memcache hợp lý bất cứ lúc nào.


Triển khai vào GAE

Tôi đã cập nhật xong các đối tượng miền domain của mình, ít nhất là những gì tôi có thể làm với Objectify. Bước tiếp theo là triển khai các ứng dụng vào GAE. Như tôi đã giải thích ở bài trước, ý định của tôi là xây dựng một ứng dụng web để cho phép những người dùng khai phá các retweet (N.D.: việc chuyển tiếp các mẩu tin nhắn nhận được tới những người khác - sau đây sẽ gọi tắt là chuyển tiếp tin nhắn) của họ, để có thể xem những người nào được chuyển tiếp tin nhắn bởi những người theo dõi (followers) có ảnh hưởng nhất của họ.

Ở một mức cao, ứng dụng web, gồm đối tượng UserRetweet, sẽ làm một số việc: Trước tiên, người dùng phải xác thực mình với Twitter thông qua OAuth. Tiếp theo, ứng dụng sẽ yêu cầu một tiến trình chạy nền để thu thập được dữ liệu chuyển tiếp tin nhắn của người dùng. Đồng thời, ứng dụng sẽ hiển thị một bảng điều khiển và thực hiện một lời gọi Ajax để lấy ra dữ liệu chuyển tiếp tin nhắn, vẫn còn lưu giữ lâu bền trong kho dữ liệu GAE (được gọi là Bigtable).

Tôi đã xây dựng các khía cạnh miền của ứng dụng vào tháng trước, nên những gì còn lại là tích hợp nó với hệ thống cấp phép của Twitter và sau đó hiển thị dữ liệu tương ứng được lấy ra.


Cấp phép người dùng với OAuth

Như bạn đã biết ở Phần 1, Twitter sử dụng OAuth để cấp phép cho người dùng. Biểu mẫu OAuth cần thiết phụ thuộc vào việc ứng dụng được triển khai trên một máy tính để bàn, trên thiết bị di động hay trong môi trường web. Ngoài ra, có hai điều bạn thực sự cần biết về cấp phép cho người dùng với OAuth:

  • Ứng dụng không cần lưu trữ một tên đăng nhập và mật khẩu.
  • Việc cấp phép được giao cho một nhà cung cấp đáng tin cậy, như Twitter.

Việc cấp phép được thực hiện bằng các thẻ xác thực khác nhau, trao đổi được. Do ứng dụng Retweet được triển khai vào một môi trường web, bước đầu tiên là đăng ký ứng dụng đó với Twitter. Khi tôi đăng ký, tôi cung cấp một số thông tin, trong đó có một URL gọi lại sẽ được sử dụng như một khóa. Tôi có thể thay đổi URL trong thời gian chạy. Đổi lại, tôi nhận được một khóa và một bí mật của người dùng, tất nhiên tôi cần phải giữ những thứ đó cho riêng mình.

Tiếp theo, tôi cần giấy phép của người dùng để đăng nhập vào Twitter và lấy dữ liệu chuyển tiếp tin nhắn của họ. Người dùng cấp phép bằng cách gửi một số thông tin đến Twitter, sau đó Twitter sẽ truy vấn URL gọi lại của tôi. Khi làm như vậy, Twitter cũng sẽ chuyển kèm theo một số thẻ xác thực (như các tham số URL, có thể nhận biết được qua việc lập trình).

Quá trình cấp phép

Tôi sẽ sử dụng hai servlet để xử lý quá trình cấp phép. Servlet đầu tiên sẽ tạo ra một đối tượng Session (Phiên làm việc), đăng ký một URL gọi lại với Twitter và hướng người dùng đến Twitter để xác thực. Quá trình xác thực cũng sẽ cho phép ứng dụng của tôi đại diện cho người dùng, như bạn sẽ thấy ngay sau đây. Servlet thứ hai sẽ là cuộc gọi lại của tôi; nó sẽ xử lý đáp ứng từ Twitter và tạo ra một đối tượng User tương ứng.

Gaelyk là một framwork Groovy, tôi sẽ sử dụng cú pháp ngắn gọn và các mã viết tắt của nó để nhanh chóng xây dựng ứng dụng web GAE của mình. Điều đầu tiên tôi cần làm với Gaelyk là định nghĩa các URL tùy chỉnh của ứng dụng mình, để tôi có thể thực hiện các URL tùy chỉnh đó bằng cách sử dụng các định tuyến URL. Cấu hình định tuyến URL của Gaelyk về cơ bản là một DSL cho phép tôi (1) xây dựng các URL dễ đọc và (2) che giấu công nghệ bên dưới. Liệt kê 3 cho thấy giai đoạn đầu tiên của ánh xạ này:

Liệt kê 3. Bắt đầu một tệp các định tuyến
get "/login",  forward: "/login.groovy"
get "/tcallback",  forward: "/tcallback.groovy"

Trong Liệt kê 3, tôi đã định nghĩa hai URL tuỳ chỉnh. Hãy lưu ý cách /login che giấu khía cạnh .groovy, giúp cho URL dễ đọc hơn. DSL mô tả một chuỗi đơn giản: Nếu một HTTP GET được áp dụng cho một URL như là your_domain/login, thì chuyển tiếp yêu cầu đó tới /login.groovy, đó là một servlet (hoặc đúng ra là groovlet) trong trường hợp Gaelyk.

Groovlet đăng nhập của tôi cũng rất đơn giản, bạn có thể thấy trong Liệt kê 4:

Liệt kê 4. Groovlet đăng nhập
import twitter4j.TwitterFactory

def twitter = new TwitterFactory().getOAuthAuthorizedInstance("...", "...")

def requestToken = twitter.getOAuthRequestToken("http://b-50-1.appspot.com/tcallback")

if(!session){
	session = request.getSession(true)
}

session.setAttribute("requestToken_token", requestToken.token)
session.setAttribute("requestToken_secret", requestToken.tokenSecret)
redirect(requestToken.getAuthorizationURL())

Trong Liệt kê 4, tôi sử dụng khóa và bí mật (secret) người dùng để tạo ra một cá thể TwitterFactory. TwitterFactory là một phần của thư viện Twitter4J. Sau đó tôi thu được một cá thể RequestToken, trong quá trình chuyển đi URL gọi lại riêng của mình (http://b-50-1.appspot.com/tcallback, đã định nghĩa trong Liệt kê 3), để Twitter sẽ gọi URL gọi lại đó sau khi có người dùng cấp quyền truy cập. Cuối cùng, tôi đặt hai mẩu thông tin vào một đối tượng HttpSession. Thông tin đó sẽ cần đến khi dữ liệu được truyền trở lại ứng dụng web. Sau đó một trình duyệt được chuyển hướng đến đến một URL ủy quyền trên trang web của Twitter.

Trình xử lý gọi lại

Trình xử lý gọi lại của tôi, đã định nghĩa trong Liệt kê 5, chỉ cần nhận thông tin cần thiết từ Twitter và liên kết dữ liệu đó với những gì được tìm thấy trong đối tượng HttpSession. Sau đó nó tạo ra một đối tượng User mới. Một khả năng khác là, nếu kho dữ liệu này đã chứa một đối tượng User rồi, thì groovlet chỉ cần cập nhật nó với dữ liệu thông tin đăng nhập mới.

Liệt kê 5. Một trình xử lý gọi lại
import java.util.Date
import twitter4j.TwitterFactory
import com.b50.gretweet.User

def twitter = new TwitterFactory().getOAuthAuthorizedInstance("...", "...")

def accTok = twitter.getOAuthAccessToken(
  session.getAttribute("requestToken_token"), 
  session.getAttribute("requestToken_secret"), 
  request.getParameter("oauth_verifier"))

def scrname = twitter.getScreenName()

session.setAttribute("screenname", scrname)

new User(scrname, accTok.getToken(), accTok.getTokenSecret()).save()

defaultQueue <<  [
  countdownMillis: 1, url: "/retweetpull",
  taskName: "twitter-pull-${new Date().time}",
  method: 'POST', params: [id: scrname]
  ]	

redirect "/dashboard/${scrname}"

Khi được gọi, groovlet nhận được một tham số khóa từ Twitter: oauth_verifier. Tham số này, được kết hợp với một khóa và bí mật, sẽ được dùng làm giấy phép để đại diện cho người dùng tiếp tục làm việc. Groovlet cũng cập nhật đối tượng session với việc xử lý Twitter của người dùng, rồi đối tượng User hoặc được tạo ra hoặc được cập nhật.

Mã xử lý defaultQueue là phép thuật của Gaelyk để đặt một yêu cầu vào một hàng đợi GAE và tôi sẽ thảo luận thêm về nó trong phần tiếp theo. Ở lệnh cuối cùng trong Liệt kê 5, trình duyệt của người dùng được chuyển hướng đến một URL mới, có ánh xạ get "/dashboard/@name", forward: "/dashboard.gtpl?tname=@name". URL này được nối với một trang web để nhận một tham số có tên là tname, là tên màn hình Twitter của người dùng.


Vào/ra và hiệu năng của GAE

Nếu bạn muốn xây dựng các ứng dụng của mình trên GAE, bạn phải tuân theo một số quy tắc. Google định nghĩa những thư viện nào mà bạn có thể và không thể sử dụng và cũng đặt ra một giới hạn thời gian phản hồi của servlet. Giới hạn thời gian lớn nhất mà một servlet dùng để xử lý một yêu cầu gửi đến là khoảng 30 giây. Nếu một servlet mất nhiều thời gian hơn lượng thời gian nói trên để đưa ra một phản hồi, thì GAE sinh ra một lỗi dưới dạng một mã HTTP 500.

Max Ross thảo luận về GAE

Các nguồn tin bên trong của Google nói rằng quy tắc yêu cầu-phản hồi trong 30-giây có thể biến mất vào một ngày nào đó. Xem phần Tài nguyên để tìm hiểu thêm về hiệu năng GAE và kho dữ liệu của Google.

Google đã có lý do chính đáng để áp đặt thời hạn này: nó cần có khả năng mở rộng quy mô ứng dụng của bạn khi cần thiết, mà không có sự trợ giúp của bạn. Vì vậy, nếu bạn thấy rằng đoạn mã mà bạn đã viết đòi hỏi quá nhiều thời gian để xử lý — có nghĩa là, nó tương tác với các hệ thống khác mà bạn không có quyền kiểm soát (hoặc chỉ đơn giản là mã code đã được viết không tốt) — thì có lẽ bạn cần xem xét lại ứng dụng của mình.

Là một nhà phát triển GAE, bạn không có quyền truy cập vào các máy tính bên dưới đang chạy ứng dụng của mình; trên thực tế, bạn sẽ không biết cấu hình của chúng hoặc thậm chí chúng có bao nhiêu bộ nhớ. Nhưng có lẽ đây không phải là nơi các vấn đề hiệu năng sẽ xuất đầu lộ diện. Vấn đề Vào/ra là thủ phạm phổ biến của hiệu năng kém và trong GAE, các vấn đề Vào/ra thường sẽ bất ngờ xảy ra khi bạn tương tác với các hệ thống khác, như Twitter.

Việc thực hiện các lời gọi API tới Twitter có thể không đáng tin cậy vì (như mọi người dùng Twitter đều biết) Twitter thỉnh thoảng cũng bị "sập". Thời gian phản hồi cũng có thể chậm ổ đĩa cứng bị quá tải. Vì vậy, việc xây dựng một ứng dụng dựa vào dữ liệu của Twitter cần phải lập kế hoạch một chút. Trong GAE, còn cần phải xếp hàng nữa.

Bộ đếm thời gian phản hồi của GAE

Google App Engine cung cấp một cơ chế hàng đợi để cho phép các tác vụ được xử lý không đồng bộ khi chạy ngầm. Thật thú vị, bản thân mỗi tác vụ là một servlet và cái bạn đưa vào một hàng đợi là một URL, cùng với các tham số nếu có. Bạn có thể thấy tất cả điều này tại Liệt kê 5. Các hàng đợi GAE cho phép bạn chọn ra các tiến trình có khả năng chạy lâu dài, chia tách chúng thành các tác vụ (các servlet) ngắn gọn và đưa các tác vụ đó vào các hàng đợi để được thực hiện trong nền sau. Tất nhiên, nếu một tác vụ làm một việc gì đó thú vị, bạn có thể lưu dữ liệu này vào kho dữ liệu và lấy nó ra tại một thời điểm nào đó sau này — đó là những gì tôi sắp làm với các dữ liệu chuyển tiếp tin nhắn trên Twitter của người dùng.

Một khi người dùng đã xác thực với Twitter, tôi sẽ tạo ra một URL dẫn tới servlet khác để nhận được các dữ liệu chuyển tiếp tin nhắn thông qua API của Twitter và đưa URL đó vào một hàng đợi. Tôi sẽ sử dụng hàng đợi mặc định của GAE (có tên là "default"), mặc dù tôi có thể tạo ra một hàng đợi duy nhất nếu tôi muốn. Liệt kê 6 cho thấy mã để đặt một yêu cầu vào hàng đợi mặc định của GAE, cũng như bạn đã thấy lần đầu trong Liệt kê 5:

Liệt kê 6. Đặt một yêu cầu vào một hàng đợi
defaultQueue << [
  countdownMillis: 1, url: "/retweetpull",
  taskName: "twitter-pull-${new Date().time}",
  method: 'POST', params: [id: scrname]
]

Tôi đã đặt một yêu cầu cho URL /retweetpull, được ánh xạ đúng là post "/retweetpull", forward: "/retweetpull.groovy". Lưu ý rằng đây là một yêu cầu HTTP POST. Tôi cũng đã chuyển kèm theo tham số id, là tên màn hình của người dùng. Lưu ý rằng tên của các tác vụ hàng đợi phải là duy nhất. Tôi nhận ra rằng sẽ thật thuận tiện nếu lấy thời gian hiện hành nối với tên hàng đợi đợi để tạo ra một tên duy nhất. Một tùy chọn khác được trình bày trong Liệt kê 6 là định nghĩa một thời gian đếm ngược, sẽ chạy cho đến khi tác vụ được thực hiện.

Một tác vụ trong một hàng đợi GAE chính là một servlet. Do đó, một "trình đọc" hàng đợi sẽ gọi ra URL và servlet cụ thể mà tôi muốn thực hiện thể hiện trong Liệt kê 7:

Liệt kê 7. Một servlet được thực hiện không đồng bộ
import twitter4j.TwitterFactory
import twitter4j.http.AccessToken
import com.b50.gretweet.User
import com.b50.gretweet.Retweet

def user =  User.findByName(params['id']) 

def twitter = getTwitter(user)

def retweets = []

def statuses = twitter.getRetweetsOfMe()

statuses.each { status ->
	def tid = status.getId()
	def dt = status.getCreatedAt()
	def txt = status.getText()

	users = twitter.getRetweetedBy(status.getId())

	users.each { usr ->		
                  retweets << new Retweet(usr.getScreenName(), tid, dt, txt,
                    usr.getFollowersCount(), usr.getProfileImageURL().toString(), 
                    usr.getName())
	}
}

user.addRetweets(retweets)

def getTwitter(user){
	return new TwitterFactory().getOAuthAuthorizedInstance("...", 
			"...", new AccessToken(user.token, user.tokenSecret))	
}

Việc đầu tiên mà servlet trong Liệt kê 7 làm là tìm một cá thể User, trên thực tế nó lấy cá thể đó từ memcache. Tiếp theo, vì được ủy quyền để làm, nên servlet thực hiện vai trò của người dùng để thiết lập một phiên làm việc Twitter. Dùng API của Twitter4J, nó nhận được một danh sách các dữ liệu chuyển tiếp tin nhắn — ví dụ, twitter.getRetweetsOfMe(). Nó xử lý từng cuộc chuyển tiếp tin nhắn thành một đối tượng Retweet và đặt tất cả các đối tượng Retweet vào một List ((bằng cách sử dụng toán tử << tiện dụng của Groovy, thực ra chỉ là một phương thức add). Khi đối tượng đó được thêm vào cá thể User thông qua phương thức addRetweets) nó sẽ được lưu.

Như vậy, tôi đã viết xong vài đoạn mã trọng yếu trong ứng dụng web của mình. Tiếp theo là tìm hiểu xem cách giao tiếp như thế nào — có nghĩa là, tôi sẽ sử dụng định dạng nào khi phát hành một yêu cầu để lấy dữ liệu ứng dụng.


Định dạng yêu cầu: JSON

Một số người nói rằng JSON đã thay thế XML làm ngôn ngữ chung mới để trao đổi dữ liệu Internet. JSON gọn nhẹ hơn so với XML, làm cho việc đọc và phân tích cú pháp dễ dàng hơn. Bạn có thể tự mình nhận thấy sự khác biệt bằng cách xem xét hai liệt kê dưới đây. Liệt kê đầu tiên cho thấy dữ liệu chuyển tiếp tin nhắn được biểu diễn bằng XML:

<retweet>
  <screenname>BarackObama</screenname>
  <influence>5901913</influence>
  <date>2010-11-11</date>
  <tweet>"I want our veterans to know: We remember..."</tweet>
  <tweetid>2758011831459840</tweetid>
  <picture>http://a3.twimg.com/profile_images/784...</picture>
  <username>"Barack Obama"</username> 
</retweet>

Liệt kê dưới đây cho thấy cũng dữ liệu đó nhưng được biểu diễn bằng JSON:

{
 "screenname":"BarackObama",
 "influence":5901913,
 "date":"2010-11-11"
 "tweet":"I want our veterans to know: We remember...",
 "tweetid":2758011831459840,
 "picture":"http://a3.twimg.com/profile_images/784...",
 "username":"Barack Obama"
}

Lưu ý rằng tài liệu JSON trông rất giống như một ánh xạ. Có các tên cùng với các giá trị. Điều này giúp cho việc biểu diễn một đối tượng miền domain trở nên khá đơn giản, nhưng đừng để cho tính đơn giản của JSON đánh lừa bạn. JSON có thể biểu diễn các danh sách, có thể cũng là các giá trị. Cách đánh dấu của JSON ít dài dòng hơn so với XML và dữ liệu tương ứng dễ hiểu bằng trực quan hơn. Tính ngắn gọn của JSON cũng có thể là một ưu điểm trong các hệ thống Ajax, do hệ thống Ajax thường có xu hướng đòi hỏi xử lý trình duyệt và băng thông mạng nhiều hơn.

Vì những lý do này, tôi thích JSON hơn XML để truyền thông từ trình duyệt đến máy chủ. Điều này đặc biệt đúng cho các ứng dụng sử dụng Ajax ở mặt trước, như ứng dụng này. Cũng như phương thức toString của java.lang.Object cung cấp một cơ chế thuận tiện để mã hóa một đối tượng, tôi thấy rằng làm cho các đối tượng miền domain trong một ứng dụng web có khả năng JSON thông qua một phương thức toJSON là có ích. Bước đầu tiên của tôi là thực thi một hợp đồng có khả năng JSON thông qua giao diện trong Liệt kê 8:

Liệt kê 8. Một hợp đồng toJSON
public interface JSONable {
  String toJSON();
}

Tiếp theo, tôi cho các đối tượng miền domain của mình thực hiện giao diện này. Tôi cung cấp hành vi thông qua một thư viện tiện dụng gọi là JSON-lib, trong đó có một API mức thấp, đơn giản, để tạo ra các cấu trúc JSON. Ví dụ, để biểu diễn một đối tượng Retweet, phương thức toJSON của tôi sẽ trông giống như Liệt kê 9:

Liệt kê 9. Phương thức toJSON của đối tượng Retweet
public String toJSON() {
  JSONObject json = new JSONObject();
  json.put("screenname", this.screenName);
  json.put("influence", this.influence);
  json.put("date", this.getFormattedDate());
  json.put("tweet", this.tweet);
  json.put("tweetid", this.tweetId);
  json.put("picture", this.userPicture);
  json.put("username", this.userName);
  return json.toString();
}

Lưu ý là tôi đã chọn có quyền điều khiển chi tiết các tài liệu JSON của tôi trông sẽ như thế nào. Tôi không quan tâm đến cách viết hoa chữ cái đầu tiên của các từ ở giữa, mặc dù tôi đã tuân thủ nó theo quy ước trong mã Java của mình. Vì vậy userName là một thuộc tính trong đối tượng Retweet, nhưng tôi vẫn để nguyên nó là username trong tài liệu JSON của mình. Trong Liệt kê 9, tôi đã sử dụng JSONObject của JSON-lib và lời gọi put của nó để định nghĩa từng thuộc tính mà tôi muốn biểu diễn. Tôi cũng đã bỏ qua thuộc tính owner.

Biểu diễn các collections

Mẫu pattern cho phép các đối tượng riêng lẻ tự biểu diễn mình dưới dạng JSON, điều này rất có ích khi xây dựng các tài liệu JSON tổng hợp. Ví dụ, nếu tôi muốn biểu diễn một tập hợp các đối tượng retweet (giả dụ ba đối tượng retweet đứng đầu xếp theo ảnh hưởng), tôi có thể làm như vậy. Dựa vào bộ sưu tập ba đối tượng Retweet, việc tạo ra một tài liệu JSON lớn để chứa chúng cũng dễ dàng như việc viết mã một servlet để làm điều này, như trong Liệt kê 10:

Liệt kê 10. Phân tích Servlet
import com.b50.gretweet.User
import com.b50.gretweet.Retweet
import net.sf.json.JSONObject

response.contentType = "application/json"

def user =  User.findByName(params['name']) 
def tweets = user.listAllRetweetsByInfluence() 
def topthree = tweets.unique(Retweet.getScreenNameComparator())
def justthree = ((topthree.size() >=3) ? topthree[0..2] : topthree[0..topthree.size()])
def jsonr = new JSONObject() jsonr.put("influencers", justthree*.toJSON())

println jsonr.toString()

Trong Liệt kê 10, kiểu phản hồi (response type) được thiết lập là application/json, để cảnh báo trình duyệt rằng một số nội dung JSON sắp đến. Tiếp theo, tìm cá thể User mong muốn. Tham số được chuyển tới servlet này là tên màn hình của người dùng. Tất cả các đối tượng Retweet liên quan sau đó được lấy ra.

Việc sử dụng một Comparator (bộ so sánh) bảo đảm rằng chỉ trả về các đối tượng Retweet duy nhất. Nếu một người có mức ảnh hưởng cao cần chuyển tiếp nhiều tin nhắn tweet khác nhau cho một người dùng cụ thể, thì chúng sẽ chỉ được liệt kê một lần. Comparator được hiển thị trong Liệt kê 11:

Liệt kê 11. Một Comparator của đối tượng Retweet
public static Comparator<Retweet> getScreenNameComparator(){
 return new Comparator<Retweet>() {
  @Override
  public int compare(Retweet arg0, Retweet arg1) {
   if(arg0.screenName.equals(arg1.screenName)){
    return 0;
   }else{
    return (arg0.influence > arg1.influence) ? 1 : -1; 
   }
  }
 };
}

Biến justthree trong Liệt kê 10 là một List có chứa ba đối tượng Retweet. Cách viết mã tắt tiện dụng của Groovy cho phép tôi gọi ra phương thức toJSON trên tất cả các đối tượng trong một bộ sưu tập chỉ bằng việc chạy *..

Đoạn mã này dẫn đến một tài liệu JSON trông giống như Liệt kê 12 (Tôi đã thêm các khoảng trống và các dòng mới để dễ đọc hơn):

Liệt kê 12. Một danh sách các đối tượng Retweet trong JSON
{
 "influencers":
   [ 
     {
      "screenname":"stuarthalloway",
      "influence":2310,
      "date":"2010-08-17",
      "tweet":"Podcast w/@stuarthalloway about Clojure http://bit.ly/bFBRND...",
      "tweetid":21451426508,
      "picture":"http://a0.twimg.com/profile_images/51921564/stu-small_norm...",
      "username":"stuarthalloway"
     },
     {
      "screenname":"aalmiray",
      "influence":1023,
      "date":"2010-08-10",
      "tweet":"New weekly podcast series airs today @ devWorks! @matthewmcc...",
      "tweetid":20812977124,
      "picture":"http://a3.twimg.com/profile_images/584851991/twitterProfil...",
      "username":"Andres Almiray"
     },
     {
      "screenname":"wakaleo",
      "influence":1020,
      "date":"2010-09-28",
      "tweet":"new article published: \"MongoDB: A NoSQL datastore with (all...",
      "tweetid":25796403122,
      "picture":"http://a2.twimg.com/profile_images/1164962298/jfsmart_norma...",
      "username":"John Ferguson Smart"
      }
     ]
}

Bây giờ tôi đã có một servlet để lấy ra một danh sách các đối tượng Retweet theo định dạng JSON, tôi đã sẵn sàng kết nối dữ liệu đó vào trang web bảng điều khiển của mình.


Xử lý không đồng bộ trong GAE

Việc xử lý nền mà tôi thiết lập trong mấy đoạn ở trên là khá gọn gàng theo quan điểm ứng dụng: sau khi người dùng đã xác thực một phiên làm việc với Twitter, một công việc đi vào hàng đợi và chuyển sang làm việc để lấy được một danh sách tất cả các cuộc chuyển tiếp tin nhắn. Nhiệm vụ tiếp theo của tôi là ghép các tương tác ứng dụng này với trải nghiệm người dùng, cũng cần phải gọn gàng như thế. Thay vì yêu cầu người dùng nhấn vào các liên kết bổ sung hoặc nạp các trang bổ sung, tôi sẽ sử dụng một chút Ajax để tuyến xử lý thông suốt.

Khi người dùng đã cấp phép cho ứng dụng web của tôi để đại diện cho người dùng trong Twitter, thì người dùng sẽ được chuyển tiếp đến một bảng điều khiển phân loại. Trên bảng điều khiển, người dùng sẽ được trình bày cùng với một báo cáo cuối cùng sẽ liệt kê các đối tượng retweet của mình, được xếp hạng theo ảnh hưởng. Dĩ nhiên, khi người dùng lần đầu nhấn chuột lên trang này, báo cáo vẫn chưa có, nên tôi sẽ phải tải nó lên theo phương thức không đồng bộ.

Yêu cầu và phản hồi

Tôi đã tạo ra một servlet để xử lý "phép thuật" không đồng bộ và trả về JSON, như bạn đã thấy trong Liệt kê 10 (có tên là tanalysis.groovy). Việc gọi servlet từ một trang web thông qua Ajax không phải là quá khó; thực vậy, tôi có thể đơn giản hóa việc này bằng một thư viện như JQuery. Để đạt được mục tiêu của mình là cập nhật không đồng bộ bảng điều khiển với một báo cáo phân tích các chuyển tiếp tin nhắn, tôi sẽ làm một vài việc: thứ nhất, khi trang được tải xong, tôi sẽ ban hành một lời gọi Ajax thông qua phương thức getJSON của JQuery. Phương thức này sẽ gọi servlet phân tích trong Liệt kê 10. Tôi cũng sẽ chuyển kèm theo người dùng hiện tại làm một tham số cho cuộc gọi của servlet. Nhưng ngay trước khi servlet được truy cập, một hàm JavaScript khác sẽ tạo ra biểu tượng "quay tròn" (spinner) quen thuộc, để cho biết rằng hoạt động đang diễn ra.

Khi servlet đáp ứng với một tài liệu JSON, phương thức getJSON sẽ cập nhật một đoạn trong HTML DOM với một danh sách của ba cuộc chuyển tiếp tin nhắn. Cuối cùng, một sự kiện JavaScript sẽ bắt đầu, tắt biểu tượng quay tròn.

Liệt kê 13 là mã Ajax có trong tệp dashboard.gptl của tôi:

Liệt kê 13. Tệp dashboard.gptl
<script type="text/javascript">
$(document).ready(function() {
 $.getJSON('../tanalysis', {name: "${params['tname']}" }, function(data) {
   $.each(data.influencers, function(){
    $('.statuses').append('<li class="hentry status"> <span class="thumb"
      > <img width="48" height="48" src="' +
	 this.picture +
     '" class="photo fn" alt="Chris Burns"></span><span class="status-body"
       > <span class="status-content">' +
     '<strong><a class="tweet-url screen-name" href="https://twitter.com/' +
	 this.screenname +
	 '">' +
	 this.username +
	 '</a></strong>' +
	 ' Followers: ' +
	 this.influence +
	 '<br/><span class="entry-content">Tweet ' +
	 '<a target="_blank" rel="nofollow" href="https://twitter.com/' +
	 this.screenname +
	 '/status/' +
	 this.tweetid +
	 '">' +
	 this.tweetid +
	 '</a> <img width="13" height="11" src="../images/new_window_icon.gif"/>' +
	 ':   ' +
	 this.tweet +
	 '</span>'+
	 '</span></span></li>')
	});
  });
 });

$('.log').ajaxStart(function() {
 $(this).text('Loading data...');
 $(".statuses").text('');
 $(".spinnericon").css('display', 'block');
});

$('.log').ajaxStop(function() {
 $(this).text('');
 $(".spinnericon").css('display', 'none');
});
</script>

Hãy lưu ý phản hồi JSON được phân tích cú pháp thông qua JQuery và JavaScript dễ dàng ra sao. Phương thức getJSON về cơ bản gọi một bao đóng với JSON của tôi, mà tôi đã đặt tên cho nó là data. Tôi có thể truy cập các phần tử trong tài liệu JSON khá dễ dàng — thực vậy, chỉ cần dùng tên. Trong trường hợp này, data đầu tiên trỏ tới một sưu tập, data.influencers. Tôi gọi phương thức each theo các mục trong sưu tập đó và sau đó lấy các giá trị riêng lẻ bằng lệnh —it.tweet, chẳng hạn.

Bạn mới bắt đầu dùng JavaScript?

Nếu bạn là người mới bắt đầu với JavaScript, một số mã trong một vài đoạn vừa qua sẽ không quen thuộc với bạn. Tuy nhiên, nếu bạn nghiên cứu nó, bạn sẽ nhận thấy rằng nó rất giống với Groovy: dễ đọc và dễ chọn đối với một nhà phát triển Java. JavaScript phổ biến trên web như là cơ sở của các công cụ có năng suất cao như JSON và JQuery và nó là ngôn ngữ cơ sở bên dưới cho Ajax. Mọi nhà phát triển Java 2.0 nên làm quen với JavaScript.

Lưu ý các sự kiện mà tôi đã định nghĩa ở dưới phương thức getJSON trong Liệt kê 13, như ajaxStartajaxStop. Những phương thức này tương ứng sẽ đặt và sẽ gỡ bỏ một biểu tượng quay tròn, bên trong một phần tử HTML div.

Bây giờ về cơ bản tôi đã có một ứng dụng web chạy được để xác thực một người dùng với OAuth của Twitter, lấy dữ liệu của người dùng theo cách không đồng bộ và sau đó cập nhật một trang web theo cách không đồng bộ.

Mối quan tâm còn lại duy nhất của tôi là bảo mật.

Xác thực người dùng — vâng!

Các độc giả thông minh có thể đã nhận thấy một vấn đề nhỏ với các mã trong Liệt kê 13: Một báo cáo về cuộc chuyển tiếp tin nhắn được tạo ra cho một người dùng và tên của người dùng đó là bất cứ ai trên Twitter (hoặc ít nhất là bất cứ ai mà các ứng dụng web có dữ liệu của họ). Có thể có một người không cẩn thận bắt đầu đưa vào các tên người dùng một cách ngẫu nhiên không? Đúng, họ có thể — nhưng chẳng có báo cáo nào sẽ được hiển thị cả, trừ phi họ đã đăng nhập vào Twitter. Khi viết mã chi tiết bên trong, tôi đã thiết lập một cái bẫy bảo mật nhỏ: một ServletFilter sẽ kiểm tra xem liệu tham số name đã cho có khớp với tên đang có trong phiên làm việc của người dùng hiện tại không. Nếu không khớp, một yêu cầu sẽ được chuyển hướng quay trở về màn hình đăng nhập.

Liệt kê 14. Xác thực người dùng bằng ServletFilter
package com.b50.gretweet;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class UserFilter implements Filter {

 private ServletContext context;

 @Override
 public void destroy() {}

 @Override
 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
  throws IOException, ServletException {

  String uri = ((HttpServletRequest)req).getRequestURI();
  HttpSession sess = ((HttpServletRequest)req).getSession();
  String username = uri.replace("/dashboard/", "");

  if(sess != null){
   String sessname = (String) sess.getAttribute("screenname");
    if(!sessname.equals(username)){
     ((HttpServletResponse)res).sendRedirect("/");
    }
  }
  chain.doFilter(req, res);
 }

 @Override
 public void init(FilterConfig arg0) throws ServletException {
 this.context = arg0.getServletContext();
 }
}

Cơ chế bảo mật này không đẹp cho lắm, nhưng nó sẽ làm việc được cho mục đích ứng dụng của tôi và bài này.


Kết luận

Trong bài viết hai phần này, trước tiên tôi đã giới thiệu Objectify-Appengine, là một tầng ánh xạ dành cho kho dữ liệu của GAE, giống như Hibernate dành cho các kho dữ liệu quan hệ. Tôi đã giới thiệu cho bạn cách sử dụng Objectify để nhanh chóng mô hình hóa một ứng dụng, rồi tôi đã nối ứng dụng đó với hệ thống cấp phép của Twitter, là OAuth. Tôi đã sử dụng Gaelyk để xây dựng các chức năng tầng sau của ứng dụng, như Vào/ra và các hàng đợi GAE và JSON (chứ không phải là XML) để làm cho việc truyền thông từ trình duyệt đến máy chủ dễ dàng. Cuối cùng, tôi đã đưa một chút Ajax vào phần mặt trước để có một trải nghiệm người dùng cuối mượt mà hơn. Tôi thực sự đã không phải làm nhiều việc khác nữa, vì GAE quản lý nhiều việc như quản lý cơ sở dữ liệu và lưu dữ liệu vào bộ nhớ đệm.

Mặc dù GAE có một số quy tắc mà bạn có thể chưa quen với chúng, kiểu như hướng dẫn yêu cầu-phản hồi 30-giây, GAE cũng cung cấp các cơ chế để xử lý chúng, ví dụ như các hàng đợi không đồng bộ. Trên nhiều mặt, GAE là sự biểu hiện hoàn hảo về phát triển Java 2.0: nó tạo điều kiện thuận lơi cho sự phát triển nhanh chóng với giá rẻ trong khi vẫn cung cấp khả năng mở rộng quy mô và độ tin cậy mà những người dùng vẫn mong đợi.


Tải về

Mô tảTênKích thước
Sample code for this articlej-javadev2-14.zip137KB

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

  • Tải về Objectify: Giao diện thuận tiện đơn giản nhất cho kho dữ liệu Google App Engine.

Thảo luận

  • Hãy tham gia vào cộng đồng My developerWorks. Kết nối với những người dùng developerWorks khác trong khi khám phá các blog, các diễn đàn, các nhóm và các wiki theo hướng nhà phát triển.

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java
ArticleID=928561
ArticleTitle=Phát triển Java 2.0: Khai phá Twitter với Objectify-Appengine, Phần 2
publish-date=05022013