Jackson hay Google Gson

Cả JacksonGoogle Gson đều là hai thư viện rất tốt để thao tác với JSON trên Java (xem thêm một so sánh hơi cũ), và nói chung không tốn công sức lắm bạn có thể đạt được mục đích tương tự nhau với cả Jackson và Gson. Tuy nhiên, có một số điểm khác biệt mà bạn cần cân nhắc khi lựa chọn Jackson hay Gson.

Thứ nhất, Jackson có hai kiểu serialize Date khác nhau là kiểu chuỗi (như “yyyy-MM-dd”) và kiểu epoch timestamp (số mili giây tính từ ngày 01/01/1970, UTC). Gson mặc định chỉ hỗ trợ kiểu chuỗi, nếu bạn cần kiểu khác phải tự viết Serializer và Deserializer. Không nói đến performance, với kiểu chuỗi, bạn phải đảm bảo định dạng của bạn có thông tin về múi giờ, gửi dữ liệu có kích thước lớn hơn, và phải code một cách rất cẩn thận nếu muốn thay đổi định dạng ngày giờ trong tương lai. Hơn nữa, epoch timestamp luôn là cách serialize đầu tiên được hỗ trợ trong các API liên quan đến ngày giờ.

Thứ hai, Spring MVC hỗ trợ Jackson với JSON viewmessage converter. Bạn có thể tự implement view và message converter dùng Gson, nhưng việc configuration, register, v.v sẽ khá phiền toái. (Bản thân việc Spring hỗ trợ Jackson cũng đã là một sự đảm bảo.)

Thứ ba, Jackson chỉ serialize các field có getter/setter hoặc annotation, còn Gson mặc định serialize tất cả các field. Trong trường hợp bạn không thể tiếp cận với mã nguồn của một class để thêm getter/setter hoặc annotation, với Jackson bạn phải dùng mix-in annotation, còn Gson sẽ giúp bạn đạt được mục đích một cách nhanh chóng (đây cũng là một mục tiêu thiết kế của Gson). Tất nhiên, việc thao tác trực tiếp hay gián tiếp với các field hoàn toàn private của một class có vẻ là một điều gì đó không ổn.

Sau cùng, trong các tình huống sử dụng mặc định, Jackson vẫn có performance tốt hơn Gson (xem benchmark , mới). Vài chục hay vài trăm mili giây trong nhiều trường hợp không phải là điều gì quá quan trọng. Nhưng dù sao thì a penny saved is a penny earned, nhanh tí nào hay tí ấy (và bạn phải tự benchmark cho tình huống sử dụng của bạn).

Advertisements

SLF4J và logback

Mặc dù unit test có thể giúp hạn chế lỗi khi phát triển phần mềm, với những chương trình phức tạp, sự cố là không thể tránh khỏi, nghĩa là vẫn phải có debug. Và việc này sẽ đơn giản hơn rất nhiều nếu trạng thái của chương trình tại các thời điểm khác nhau được ghi lại. Ngoài ra, các thông tin này có thể giúp cải tiến chương trình về giao diện sử dụng, tốc độ, v.v. Đó là lí do chúng ta có các logging framework.

Tại sao không dùng println cho việc logging? Bởi vì chúng quá thô sơ. Một giải pháp logging, có lẽ phải đáp ứng các yêu cầu sau:

  • Tùy biến:
    • Bật, tắt logging (toàn bộ hoặc theo loại thông điệp.
    • Vị trí xuất log (màn hình, tập tin, database, v.v)
    • Định dạng, lượng thông tin cần log
  • Thông tin có thể log:
    • Vị trí gọi log
    • Thời gian
    • Thông điệp, stack trace, v.v

Có nhiều logging framework trong Java, trong đó nổi tiếng nhất là Log4J. Để thuận tiện khi chuyển đổi giữa các logging framework, người ta phát triển facade chung cho chúng. Không may là cũng lại có hơn một facade 🙂 (Java là thế). Hiện nay, các dự án có vẻ đang ưa chuộng facade SLF4J. API của SLF4J được framework logback hiện thực trực tiếp (native implementation). Ngoài tính dễ sử dụng, SLF4J còn có một lợi thế là được phát triển bởi tác giả của Log4J—tất nhiên là người hiểu rõ các hạn chế của Log4J.

Dưới đây là một ví dụ nhỏ về SLF4J. Tài liệu hướng dẫn và cả javadoc của SLF4J và logback đã rất cụ thể và đầy đủ, nên nếu bạn muốn tìm hiểu sâu hơn thì đó là nơi tốt nhất để bắt đầu.

Mặc định của logback là log ra console:

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
public class HelloWorld { 
  public static void main(String[] args) { 
    Logger logger = LoggerFactory.getLogger("HelloWorld"); 
    logger.debug("Hello world."); 
  } 
}

Kết quả (console):

22:12:35.261 [main] DEBUG HelloWorld - Hello world.

Sửa đổi tập tin cấu hình (thường là logback.xml) và ta sẽ log được ra tập tin:

<configuration> 
  <appender name="FILE" class="ch.qos.logback.core.FileAppender"> 
    <file>hw.log</file> 
    <encoder> 
      <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern> 
    </encoder> 
  </appender> 
  <root level="debug"> 
    <appender-ref ref="FILE" /> 
  </root> 
</configuration>

Kết quả (trong tập tin hw.log):

2010-08-21 21:35:57,583 DEBUG [main] HelloWorld [HelloWorld.java:7] Hello world.

JUnit

Có lẽ đa số sinh viên chúng ta đều quen với việc hoàn tất chương trình, chạy thử, thấy sai, tìm và sửa lỗi, chạy thử, v.v. Điều này không có gì là sai trái, tuy nhiên nếu đợi đến khi hoàn tất chương trình rồi mới chạy thử, thì việc tìm và sửa lỗi sẽ tương đối vất vả. Quá trình này sẽ đơn giản hơn, nếu bạn có thể chia chương trình thành từng phần nhỏ, và test chúng một cách riêng rẽ. Đó là ý nghĩa của unit test.

Tuy nhiên, để unit test hiệu quả, chúng ta cần có các công cụ để tiến hành nó một cách tự động. Các công cụ này gọi là unit test framework. Với Java, framework nổi tiếng nhất là JUnit.

Nếu bạn muốn tìm hiểu về JUnit, thì bạn nên đọc “JUnit in action”. Dưới đây là một ví dụ nhỏ về JUnit để bạn tham khảo.

Kiểm tra với một test case

Giả sử chúng ta có một class Number như dưới đây. Mỗi object của class đại diện cho một số nguyên. Method isEven kiểm tra tính chẵn lẻ của số nguyên đó. Mục tiêu của chúng ta là kiểm tra tính đúng đắn của method isEvent.

public class Number { 
    private int number; 
    public Number(int number) { this.number = number; } 
    public boolean isEven() { return (number % 2 == 0)?true:false; } 
}

Việc đầu tiên là tải tập tin junit-4.x.x.jar và cho nó vào CLASSPATH. Đoạn mã dưới đây sẽ kiểm tra isEvent với 2.

import static org.junit.Assert.*; 
import org.junit.Test; 
public class NumberTest { 
    @Test public void testIsEven() { 
        Number number = new Number(2); 
        assertTrue(number.isEven()); 
    } 
}

Dịch hai đoạn mã trên và chạy lệnh java org.junit.runner.JUnitCore NumberTest. Khi đó JUnit sẽ chạy các method được đánh dấu @Test. Kết quả là:

JUnit version 4.8.2 
. 
Time: 0.008 
OK (1 test)

Kiểm tra với nhiều test case

Bây giờ ta muốn kiếm tra với nhiều số hơn. Chúng ta sẽ không chèn thêm assertTrue vào testIsEven (vì như vậy sẽ không thể biết được số nào làm cho test bị sai). Thay vào đó, chúng ta sẽ tham số hóa method testIsEven. Các tham số được lấy từ bộ test tạo ra bởi method getTestParameters.

import static org.junit.Assert.*; 
import org.junit.Test; 
import org.junit.runner.RunWith; 
import org.junit.runners.Parameterized; 
import org.junit.runners.Parameterized.Parameters; 
import java.util.Arrays; 
import java.util.Collection;  

@RunWith(value=Parameterized.class) 
public class NumberParameterizedTest { 
    private int value; 
    private boolean expected; 
    public NumberParameterizedTest(int value, boolean expected) { 
        this.value = value; 
        this.expected = expected; 
    } 
    @Parameters public static Collection&lt;Object[]&gt; getTestParameters() { 
        return Arrays.asList(new Object[][] { {2, true}, {3, false}, {5, true}, }); 
    } 
    @Test public void testIsEven() { 
        Number number = new Number(value); 
        assertEquals(number.isEven(), expected); 
    } 
}

Lần này, Junit sẽ lần lượt lấy từng mẫu ra để kiểm tra. Kết quả là (trích):

JUnit version 4.8.2 
...E 
Time: 0.022 
There was 1 failure: 
1) testIsEven[2](NumberParameterizedTest)...

Như vậy là test sai với mẫu thứ ba (5, true) (vì đếm từ 0).

Kiểm tra Exception

Trong một số trường hợp, một method sẽ throw Exception thay vì trả về kết quả tính toán. Để đảm bảo hành vi của method đúng như mong đợi, chúng ta phải test cả những trường hợp này để xem chúng có throw đúng Exception cần throw không. @Test(expected=YourException.class) là annotation phục vụ cho mục đích này.

Dùng test làm hướng dẫn sử dụng

Hãy nhìn lại đoạn code trên. Khi đọc test (nếu bạn hiểu JUnit), bạn sẽ suy ra ngay cách sử dụng method isEven: đầu tiên tạo một object Number với constructor nhận vào giá trị int, sau đó gọi isEven(). Vậy là, test code còn một tác dụng khác là làm hướng dẫn sử dụng (tương tự như các đoạn code trong tutorial trên mạng). Ưu điểm của việc lấy test làm hướng dẫn sử dụng là nhanh gọn và bám sát theo thay đổi của chương trình.

Local class trong Java

Local class là class được khai báo bên trong một method. Các object của class này có đặc điểm là chỉ truy xuất được variable của method nếu chúng là final. Để giải thích điều này, hãy xét đoạn mã dưới đây.

class Outer {
	SomeClass o;
	public void OuterMethod() {
		int a;
		class Inner {
			public void InnerMethod() {
				// Do something
			}
		}
		Inner i = new Inner;
		o = new SomeClass(i);
	}
}

Ở đây, i và a là các local variable của method OuterMethod. Khi method này kết thúc, i và a sẽ bị xóa khỏi stack. Thế còn object mà i trỏ tới liệu có bị Garbage Collector xóa? Câu trả lời là tùy từng trường hợp. Vì i đã được pass cho object o, nếu o lưu lại một reference để trỏ tới object mà i đang trỏ tới, thì object này chưa đủ điều kiện để bị thu hồi vùng nhớ. Điều đáng nói ở đây là object mà i trỏ tới có thể tồn tại lâu hơn method OuterMethod. Bây giờ, nếu trong phần mã của InnerMethod có một method nào đó sử dụng i, và method này được chạy khi method OuterMethod đã kết thúc (nghĩa là i đã bị xóa), thì nó sẽ lấy i ở đâu ra? Tất nhiên, nó không thể lấy được i ở đâu cả. Để tránh tình huống này, Java không cho mã của method-local inner class truy xuất variable của method chứa nó, trừ khi variable là final.

Xét tiếp phần “final” của vấn đề. Vì final variable không thể bị thay đổi một khi đã được gán giá trị, cho nên object của inner class có thể copy lại. Vậy tại sao không copy các variable còn lại? Rất đơn giản, nếu biến int i được copy, thì khi đó i của object và i của method không liên hệ gì với nhau, mọi thay đổi của i trong object không ảnh hưởng gì đến i của method, nghĩa là việc copy i cũng chỉ tương đương với việc chúng ta truyền i vào trong object thông qua constructor hoặc method bình thường, không hơn không kém. Hoàn toàn không cần thiết phải đưa thêm một tính năng khi nó không thực sự giúp giải quyết vấn đề gì.

Overriding trong Java là single dispatch

Không có gì đặc biệt, nhưng có lẽ cũng phải ghi lại.

Trước hết, cho hai class Animal và Cat:

class Animal {} 
class Cat extends Animal {}

Tiếp theo cho class Hunter và BetterHunter:

class Hunter { 
    public void Kill(Animal a) { System.out.println("Hunter killed Animal"); } 
    public void Kill(Cat c) { System.out.println("Hunter killed Cat"); } 
} 
class BetterHunter extends Hunter { 
    public void Kill(Animal a) { System.out.println("BetterHunter killed Animal"); } 
    public void Kill(Cat c) { System.out.println("BetterHunter killed Cat"); } 
}

Bây giờ xét tình huống sau, kết quả in ra là gì?

Animal a = new Cat(); 
Hunter h = new BetterHunter(); 
h.Kill(a);

Vì h trỏ đến BetterHunter, JVM phân giải h.Kill thành Kill của BetterHunter. Sau đó, vì a trỏ đến Cat, JVM chọn Kill(Cat). Kết quả là “BetterHunter killed Cat”.

Sai. Java chỉ hỗ trợ Single dispatch, vì overriding được hiện thực nhờ vtable. Nói cách khác JVM chỉ phân giải method dựa vào tham số đầu tiên (this). Bước thứ hai (chọn Kill nào) được thực hiện trong lúc biên dịch (overload). Vì vậy Kill của BetterHunter vẫn được gọi, nhưng là Kill(Animal). Kết quả là “BetterHunter killed Animal”.

Để có Double dispatch, tức là gọi Kill(Cat), xem Thinking in Java, hoặc pattern Visitor.

Memory leak trong Java

Có hai lỗi quản lí bộ nhớ mà ai học C/C++ cũng từng mắc phải là memory leak và dangling pointer. Memory leak xảy ra khi chương trình không thể thu hồi (hay truy cập) một vùng nhớ đã cấp phát mặc dù không còn sử dụng. Nó xảy ra khi tất cả con trỏ trỏ đến vùng nhớ đó bị thay đổi giá trị trước khi vùng nhớ được giải phóng. Dangling pointer xảy ra khi tồn tại con trỏ trỏ tới một vùng nhớ chưa được cấp phát, hoặc đã bị thu hồi.

Khác với C/C++, việc thu hồi vùng nhớ trong Java được thực hiện bởi Gabage Collector (GC). Các vùng nhớ không còn được trỏ tới sẽ có khả năng bị thu hồi (mặc dù ta không thể đảm bảo được thời điểm mà GC sẽ thực hiện điều đó) (không còn memory leak). Và tất nhiên, các vùng nhớ vẫn còn được trỏ tới bởi ít nhất một reference sẽ không bị thu hồi (không còn dangling pointer).

Vùng nhớ chỉ thỏa mãn tiêu chuẩn thu hồi khi không còn reference nào trỏ tới. Điều này nghĩa là ta phải nhớ xóa tất cả các reference tới vùng nhớ không còn dùng. Và thông thường, chuyện này không có gì phức tạp, chỉ cần trỏ reference tới vùng nhớ khác, tới null, hoặc để reference tự biến mất khi ra khỏi block chứa nó (chẳng hạn, khi method khai báo reference đó kết thúc). Nói chung là đa số được thực hiện một cách “vô tình”, tức là ta thậm chí không để ý là mình đang làm điều đó.

Nhưng có những tình huống mà chúng ta phải chủ động xóa reference, ví dụ như khi làm việc với Listener. Để A có thể nhận thông báo về các event mới trong B, nó phải được thêm vào danh sách Listener của B, giả sử bằng method addActionListener. Khi muốn kết thúc sự tồn tại của A, ta cần để đưa A ra khỏi danh sách, giả sử bằng removeActionListener. Đây là điều bắt buộc khi chúng ta muốn vùng nhớ của A có thể được thu hồi, vì chừng nào A còn nằm trong danh sách Listener thì vẫn còn reference trỏ đến nó. Tất nhiên, triển vọng là ta sẽ quên làm việc đó, như cái cách chúng ta quên giải phóng bộ nhớ trong C/C++. Chưa kể đến các tình huống đau đầu như có vài vị trí (ví dụ như vài nhánh if) có thể kết thúc A, đồng nghĩa với việc có vài vị trí cần gọi removeActionListener ;).

Như vậy, mặc dù phần mềm viết bằng Java không có kiểu memory leak như C/C++, nhưng vẫn có thể có những vùng nhớ không còn sử dụng, song cũng không thể bị thu hồi, đó chính là memory leak “kiểu Java”.

Chính vì lí do trên, Java cho chúng ta thêm một số dạng reference đặc biệt trong package java.lang.ref, là WeakReference (WR) và SoftReference (SR). Các reference này cũng được dùng để trỏ tới các vùng nhớ, như reference mà chúng ta vẫn quen dùng (giờ gọi là strong reference). Tuy nhiên, nếu vùng nhớ được trỏ tới bởi cả strong reference lẫn WR, thì khi strong reference cuối cùng bị xóa bỏ, vùng nhớ này có thể được thu hồi bởi GC, và WR không có khả năng ngăn cản điều đó. SR “strong” hơn WR một chút, nghĩa là vùng nhớ được trỏ tới bởi WR sẽ được GC ưu tiên thu hồi hơn (ưu tiên tới đâu thì phụ thuộc vào hiện thực của từng máy ảo Java). Bây giờ, nếu ta đưa A vào danh sách Listener qua một WR chứ không phải là strong reference, ta sẽ không cần bận tâm tới việc gọi removeActionListener khi muốn kết thúc A nữa. Chi tiết cách sử dụng WR và SR, tham khảo JDK documentation và Thinking in Java (ví dụ khá rõ, dù không nói nhiều về ý nghĩa của WR và SR).

Chú ý là với các ứng dụng thông thường, thì hiện tượng này nếu có cũng không làm máy cạn bộ nhớ được, vì vậy cũng không nhất thiết phải dùng đến SR và WR.

Checked exception trong Java

Tiếp chuyện hôm trước, nhưng hôm nay không có C#.

Không quan tâm tới chuyện checked exception trong Java hay dở thế nào nữa, tại vì chúng ta không phải là Sun. Họ làm sao là quyền của họ. Việc của chúng ta là làm theo ý họ theo cách tốt nhất có thể.

Sun có một bài dài về checked exception, và họ kết luận:

Generally speaking, do not throw a RuntimeException or create a subclass of RuntimeException simply because you don’t want to be bothered with specifying the exceptions your methods can throw.

Here’s the bottom line guideline: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception.

Tức là với Sun, lập trình viên phải ưu tiên xem xét checked exception trước. Trong quyển Effective Java: Programming Language Guide, Josh Bloch (một trong những người tham gia thiết kế Java) cụ thể hóa các chỉ dẫn trên một chút:

  • Item 39: Use exceptions only for exceptional conditions. That is, do not use exceptions for control flow, such as catching NoSuchElementException  when calling Iterator.next() instead of first checking Iterator.hasNext().
  • Item 40: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors. Here, Bloch echoes the conventional Sun wisdom — that runtime exceptions should be used only to indicate programming errors, such as precondition violations.
  • Item 41: Avoid unnecessary use of checked exceptions. In other words, don’t use checked exceptions for conditions from which the caller could not possibly recover, or for which the only foreseeable response would be for the program to exit.
  • Item 43: Throw exceptions appropriate to the abstraction. In other words, exceptions thrown by a method should be defined at an abstraction level consistent with what the method does, not necessarily with the low-level details of how it is implemented. For example, a method that loads resources from files, databases, or JNDI should throw some sort of ResourceNotFound exception when it cannot find a resource (generally using exception chaining to preserve the underlying cause), rather than the lower-level IOException, SQLException, or NamingException.

Tuy nhiên, như đã nói, các ông trùm đều cho rằng checked exception theo kiểu Java phá vỡ tính bao đóng, làm code dài, khó bảo trì, v.v cho nên một hướng dẫn chung chung thế kia có lẽ là chưa đủ. Có hai lựa chọn có vẻ đáng chú ý.

Thứ nhất là quan điểm “cực đoan” của Bruce Eckel (người viết mấy quyển Thinking in), chỉ nên có checked exception. Bruce Eckel đề xuất việc mở rộng RuntimeException thành ExceptionAdapter có tác dụng:

Here, you’re still able to catch the specific type of exception but you’re not forced to put in all the exception specifications and try-catch clauses everywhere between the origin of the exception and the place that it’s caught. An even more importantly, no one writing code is tempted to swallow the exception and thus erase it. If you forget to catch some exception, it will show up at the top level. If you want to catch exceptions somewhere in between, you can.

Thứ hai là quan điểm “ôn hòa” của Rod Johnson trong J2EE Design and Development. Giống như Anders Hejlsberg, ông này cũng cho là checked exception là cần thiết, vấn đề là dùng sao cho hợp lí (trong điều kiện vẫn chưa tìm ra giải pháp nào phù hợp về mặt ngôn ngữ):

Checked exceptions are much superior to error return codes (as used in many older languages). Sooner or later (probably sooner) someone will fail to check an error return value; it’s good to use the compiler to enforce correct error handling. Such checked exceptions are as integral to an object’s API as parameters and return values.

However, I don’t recommend using checked exceptions unless callers are likely to be able to handle them. In particular, checked exceptions shouldn’t be used to indicate that something went horribly wrong, which the caller can’t be expected to handle.

Rod Johnson còn đề xuất cả một hướng dẫn cụ thể:

Question Example Recommendation if the answer is yes
Should all callers handle this problem? Is the exception essentially a second return value for the method? Spending limit exceeded in a processInvoice() method Define and used a checked exception and take advantage of Java’s compile-time support.
Will only a minority of callers want to handle this problem? JDO exceptions Extend RuntimeException. This leaves callers the choice of catching the exception, but doesn’t force all callers to catch it.
Did something go horribly wrong? Is the problem unrecoverable? A business method fails because it can’t connect to the application database Extend RuntimeException. We know that callers can’t do anything useful besides inform the user of the error.
Still not clear?   Extend RuntimeException. Document the exceptions that may be thrown and let callers decide which, if any, they wish to catch.

Decide at a package level how each package will use checked or unchecked exceptions. Document the decision to use unchecked exceptions, as many developers will not expect it.

The only danger in using unchecked exceptions is that the exceptions may be inadequately documented. When using unchecked exceptions, be sure to document all exceptions that may be thrown from each method, allowing calling code to choose to catch even exceptions that you expect will be fatal. Ideally, the compiler should enforce Javdoc-ing of all exceptions, checked and unchecked.

If allocating resources such as JDBC connections that must be released under all circumstances, remember to use a finally block to ensure cleanup, whether or not you need to catch checked exceptions. Remember that a finally block can be used even without a catch block.

Quyển J2EE Design and Development còn trình bày rất nhiều về exception và các vấn đề đáng quan tâm khác trong Java.