Hiển thị các bài đăng có nhãn Java. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn Java. Hiển thị tất cả bài đăng

26 tháng 3, 2018

Xử lý tập dữ liệu lớn với Hibernate

Nếu bạn cần phải xử lý tập kết quả cơ sở dữ liệu lớn bằng Java, thì bạn có thể lựa chọn JDBC cho phép bạn khả năng kiểm soát ứng dụng ở tầng thấp. Mặt khác, nếu bạn đã lựa chọn sử dụng ORM trong ứng dụng của mình, nhưng sau đó thay đổi để sử dụng JDBC thì điều này lại khiến bạn mất khá nhiều tính năng thú vị như optimistic locking, caching,... Rất may là hầu hết các ORM framework, ví dụ như Hibernate, có vài lựa chọn có thể giúp bạn có thể xử lý tập kết quả cơ sở dữ liệu lớn.

Một ví dụ đơn giản như sau, giả định chúng ta có một bảng (được ánh xạ tới class DemoEntity) với một 100,000 bản ghi. Mỗi bản ghi bao gồm một cột đơn (được ánh xạ tới thuộc tính property trong lớp DemoEntity) giữ một số kí tự ngẫu nhiên với dung lượng ~ 2KB. JVM được chạy với -Xmx250m - giả sử chúng ta chỉ có tối đa 250MB cho bộ nhớ JVM trên hệ thống. Công việc của bạn là đọc toàn bộ bản ghi có trong bảng, thực hiện một vài xử lý và sau đó lưu lại kết quả. Ta giả định rằng kết quả từ việc xử lý tập dữ liệu lớn không bị thay đổi. Để bắt đầu ta sẽ thử xử lý đơn giản trước, thực hiện truy vấn lấy toàn bộ dữ liệu:

new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {
    @Override
    public Void doInTransaction(TransactionStatus status) {
        Session session = sessionFactory.getCurrentSession();
        List<DemoEntity> demoEntitities = (List<DemoEntity>) session.createQuery("from DemoEntity").list();
        for(DemoEntity demoEntity : demoEntitities) {
           // Process and write result
        }
        return null;
    }
});

Sau vài giây, ta nhận được message sau:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

Hình 1:

Hình 1

Rõ ràng điều này không giải quyết được vấn đề của chúng ta. Để khắc phục vấn đề này, chúng ta sẽ chuyển qua sử dụng scrollable trong Hibernate để thực thi câu truy vấn trên, ánh xạ toàn bộ kết quả vào các đối tượng entityreturn chúng. Khi sử dụng scrollable result set, các bản ghi được biến đổi sang các entity trong cùng một lần:

new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {

    @Override
    public Void doInTransaction(TransactionStatus status) {
        Session session = sessionFactory.getCurrentSession();
        ScrollableResults scrollableResults = session.createQuery("from DemoEntity").scroll(ScrollMode.FORWARD_ONLY);

        int count = 0;
        while (scrollableResults.next()) {
            if (++count > 0 && count % 100 == 0) {
                System.out.println("Fetched " + count + " entities");
            }
            DemoEntity demoEntity = (DemoEntity) scrollableResults.get()[0];
            // Process and write result
        }
        return null;
    }
});

Sau khi chạy đoạn code trên:

...
Fetched 49800 entities
Fetched 49900 entities
Fetched 50000 entities
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

Hình 2:

Hình 2

Mặc dù ta đang sử dụng một scrollable result set, nhưng mỗi đối tượng trả về là một attached object và trở thành một phần của persistence context (aka session). Kết quả này thực sự rất giống với ví dụ đầu tiên khi mà chúng ta sử dụng session.createQuery("from DemoEntity").list(). Tuy nhiên, với phương cách đầu tiên ta không phải điều khiển bằng tay, chúng ta sẽ có được kết quả khi Hibernate hoàn thành công việc của nó. Mặt khác, bằng cách sử dụng một scrollable result set sẽ cho phép chúng ta khả năng kiểm soát quá trình xử lý và giải phóng bộ nhớ khi cần. Như ta có thể thấy thì nó sẽ không tự động giải phóng bộ nhớ, bạn phải cho Hibernate biết khi nào sẽ thự hiện việc này. Bạn có thể tham khảo vài cách thực hiện như sau:

  • Thu hồi vùng nhớ của đối tượng trong persistent context sau khi xử lý xong
  • Xóa toàn bộ session

Chúng ta sẽ lựa chọn cách đầu tiên, ở ví dụ trên tại dòng số 13 (// Process and write result) ta sẽ thêm đoạn code sau:

session.evict(demoEntity);

Lưu ý:

  • Nếu bạn đã thực hiện bất kì thay đổi nào trên một hoặc nhiều đối tượng entity hãy chắc chắn rằng bạn đã thực hiện lệnh flush session trước khi thực hiện evict hoặc clear, nếu không thì câu truy vấn sẽ bị giữ lại vì nếu thực hiện sau thì Hibernate sẽ không gửi các thay đổi này đến cơ sở dữ liệu
  • Evict hoặc clear sẽ không xóa các đối tượng entity khỏi second level cache. Nếu bạn cho phép và đang sử dụng second level cache, bạn có thể phương thức sessionFactory.getCache().evictXxx() nếu muốn loại bỏ đối tượng khỏi second level cache
  • Sau thời điểm bạn sử dụng evict trên một entity, thì nó sẽ không liên kết với session nữa. Bất kì thay đổi nào được thực hiện trên đối tượng entity sẽ không được tự động phản ánh vào trong database. Nếu bạn đang sử dụng lazy loading, khi truy cập vào bất kì thuộc tính nào mà không được load trước khi thực hiện evict thì một ngoại lệ org.hibernate.LazyInitializationException sẽ được ném ra. Vì vậy, về cơ bản hãy đảm bảo xử lý xong các đối tượng entity (hoặc ít nhất khởi tạo những thứ cần thiết) trước khi bạn sử dụng evict hoặc clear

Sau khi chạy lại chương trình, ta sẽ thấy nó được thực thi thành công:

...
Fetched 99800 entities
Fetched 99900 entities
Fetched 100000 entities

Hình 3:

Hình 3

Bạn cũng có thể thiết lập câu truy vấn là read-only, điều này cho phép Hibernate thực hiện thêm một vài tối ưu.

ScrollableResults scrollableResults = session.createQuery("from DemoEntity").setReadOnly(true).scroll(ScrollMode.FORWARD_ONLY);

Thực hiện điều này chỉ mang lại sự khác biệt rất nhỏ trong việc sử dụng bộ nhớ.

Chúng ta đã có thể xử lý được 100,000 bản ghi, nhưng Hibernate có một tùy chọn khác để xử lý số lượng lớn: Stateless session. Bạn có thể lấy được scrollable result set từ một stateless session tương tự với cách làm với một session thông thường. Một stateless session nằm ngay bên trên của JDBC. Gần như, Hibernate sẽ chạy ở chế độ tắt toàn bộ các tính năng, nghĩa là sẽ không có persistent context, không có 2nd level caching, không có dirty detection, không có lazy loading, về cơ bản là sẽ không có gì. Javadoc có mô tả như sau:

/**
 * A command-oriented API for performing bulk operations against a database.
 * A stateless session does not implement a first-level cache nor interact with any 
 * second-level cache, nor does it implement transactional write-behind or automatic 
 * dirty checking, nor do operations cascade to associated instances. Collections are 
 * ignored by a stateless session. Operations performed via a stateless session bypass 
 * Hibernate's event model and interceptors.  Stateless sessions are vulnerable to data 
 * aliasing effects, due to the lack of a first-level cache. For certain kinds of 
 * transactions, a stateless session may perform slightly faster than a stateful session.
 *
 * @author Gavin King
 */

Điều duy nhất sẽ được thực hiện đó là chuyển đổi các bản ghi thành các đối tượng java. Đây có thể là một lựa chọn hấp dẫn bởi vì nó giúp bạn thoát khỏi ý nghĩ là khi nào sẽ cần phải evict/flush.

new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {

    @Override
    public Void doInTransaction(TransactionStatus status) {
        sessionFactory.getCurrentSession().doWork(new Work() {
            @Override
            public void execute(Connection connection) throws SQLException {
                StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);
                try {
                    ScrollableResults scrollableResults = statelessSession.createQuery("from DemoEntity").scroll(ScrollMode.FORWARD_ONLY);

                    int count = 0;
                    while (scrollableResults.next()) {
                        if (++count > 0 && count % 100 == 0) {
                            System.out.println("Fetched " + count + " entities");
                        }
                        DemoEntity demoEntity = (DemoEntity) scrollableResults.get()[0];
                        // Process and write result 
                    }
                } finally {
                    statelessSession.close();
                }
            }
        });
        return null;
    }
});

Hình 4:

Hình 4

Bên cạnh thực tế là sử dụng stateless session sẽ rất tối ưu bộ nhớ, thì khi sử nó cũng có một vài hạn chế, bạn có thể nhận thấy là ta phải thực hiện mở và đóng stateless session bằng tay một cách tường minh: sẽ không có bất kì phương thức nào giống như sessionFactory.getCurrentStatelessSession() hoặc bất kì cách thức tích hợp với Spring để quản lý stateless session (vào thời điểm hiện tại). Mặc định khi mở một stateless session sẽ cấp phát mới một đối tượng java.sql.Connection (nếu bạn sử dụng phương thức openStatelessSession()), do đó nó sẽ gián tiếp tạo ra một transaction thứ hai. Bạn có thể tránh được việc này bằng cách sử dụng API của Hibernate như trong ví dụ trên, ta sẽ lấy được current connection và truyền đối số qua phương thức openStatelessSession(Connection connection). Ta thực hiện đóng session trong khối finally sẽ không có tác động đến kết nối vật lý vì nó được quản lý bởi Spring: chỉ có connection logic bị đóng và một connection logic mới được tạo ra khi mở một stateless session.

Như đã nói ở trước đó, Hibernate sẽ chạy trong chế độ tất cả tính năng bị vô hiệu và các đối tượng entity được trả về ở trạng thái tách rời. Nghĩa là, mỗi entity mà bạn chỉnh sửa thì bạn sẽ phải gọi phương thức statelessSession.update(entity) trực tiếp. Đầu tiên, ta sẽ thử chỉnh sửa một đối tượng entity:

new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {
    @Override
    public Void doInTransaction(TransactionStatus status) {
        sessionFactory.getCurrentSession().doWork(new Work() {
            @Override
            public void execute(Connection connection) throws SQLException {
                StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);
                try {
                    DemoEntity demoEntity = (DemoEntity) statelessSession.createQuery("from DemoEntity where id = 1").uniqueResult();
                    demoEntity.setProperty("test");
                    statelessSession.update(demoEntity);
                } finally {
                    statelessSession.close();
                }
            }
        });
        return null;
    }
});

Ý tưởng là chúng ta sẽ thực hiện mở một stateless session với connection hiện tại, và như javadoc StatelessSession chỉ ra rằng sẽ không thực hiện ghi dữ liệu sau khi chỉnh sửa đổi dữ liệu, mỗi câu lệnh được thực hiện bởi stateless session sẽ được gửi trực tiếp đến database. Cuối cùng, khi transaction (được bắt đầu bởi TransactionTemplate) được commit thì kết quả sau khi được chỉnh sửa sẽ hiển thị trong database.

28 tháng 12, 2017

SQL Injection là gì? Ngăn chặn SQL Injection như thế nào?

1. SQL Injection là gì?

SQL Injection xảy ra khi kẻ tấn công có thể giả mạo các thao tác truy vấn để nhằm mục đích thực hiện một câu lệnh truy vấn SQL khác so với những gì mà nhà phát triển ứng dụng đã dự định ban đầu.

Khi thực thi câu lệnh truy vấn, chúng ta có hai tùy chọn cơ bản là:

  • Sử dụng statement (ví dụ, java.sql.Statement)
  • Hoặc sử dụng prepared statement (ví dụ, java.sql.PreparedStatement)

Khi xây dựng các câu truy vấn căn bản, nếu chúng ta thực hiện việc nối chuỗi thì cả hai loại java.sql.Statementjava.sql.PreparedStatement đều rất dễ bị tấn công SQL Injection.

Để có thể thực hiện một câu lệnh truy vấn thì trong hai lớp java.sql.Statementjava.sql.PreparedStatement có định nghĩa ra hai phương thức là:

  • executeQuery(String sql) để thực thi câu lệnh SQL SELECT
  • executeUpdate(String sql) để thực thi các câu lệnh SQL INSERT, UPDATE, DELETE

Tùy thuộc vào sự kết hợp của Statement/PreparedStatementexecuteQuery/executeUpdate, mà mục tiêu tấn công SQL Injection sẽ thay đổi, được thể hiện bằng các kịch bản dưới đây.

2. Statement và executeUpdate

Đây là sự kết hợp dễ bị tấn công nhất. Giả định, chúng ta có một phương thức dùng để cập nhật giá trị column review của một bản ghi thuộc bảng post_comment:

public void updatePostCommentReviewUsingStatement(Long id, String review) {
    doInJPA(entityManager -> {
        Session session = entityManager.unwrap(Session.class);
        session.doWork(connection -> {
            try(Statement statement = connection.createStatement()) {
                statement.executeUpdate(
                    "UPDATE post_comment " +
                    "SET review = '" + review + "' " +
                    "WHERE id = " + id);
            }
        });
    });
}

Và chúng ta sẽ thường gọi phương thức trên như sau:

updatePostCommentReviewUsingStatement(1L, "Awesome");

Một kẻ tấn công giả mạo chỉ đơn giả thực hiện cuộc tấn công SQL Injection như sau:

updatePostCommentReviewUsingStatement(1L, "'; DROP TABLE post_comment; -- '");

Và đay là những gì mà database sẽ thực thi:

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
  • Câu lệnh UPDATE sẽ được thực thi trước tiên
  • Sau đó sẽ là câu lệnh DROP
  • Cú pháp comment -- sẽ đảm bảo bỏ qua điều kiện trong mệnh đề WHERE trong phần còn lại của câu truy vấn.

Sau khi thực hiện tấn công SQL Injection trên, hãy thử truy vấn lại dữ liệu từ bảng post_comment để xem câu lệnh tấn công SQL Injection của chúng ta có thành công hay không.

ORACLE

Trên Oracle 11g, câu lệnh SQL Injection phía trên sẽ bị lỗi, vì JDBC driver không nhận ra dấu ;

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 911, SQLState: 22019
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - ORA-00911: invalid character
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]

SQL Server

Trên SQL Server 2014, câu lệnh SQL Injection sẽ được thực thi thành công và bảng post_comment đã bị xóa.

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]
 
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 208, SQLState: S0002
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - Invalid object name 'post_comment'.
INFO  [Alice]: o.h.e.i.DefaultLoadEventListener - HHH000327: Error performing load command : org.hibernate.exception.SQLGrammarException: could not extract ResultSet

PostgreSQL

Trên PostgreSQL 9.5, câu lệnh SQL Injection sẽ được thực thi thành công và bảng post_comment đã bị xóa.

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 0, SQLState: 42P01
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - ERROR: relation "post_comment" does not exist

MySQL

Trên MySQL 5.7, câu lệnh SQL Injection sẽ bị lỗi vì JDBC driver không biên dịch đúng nhiều câu lệnh DML

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 1064, SQLState: 42000
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DROP TABLE post_comment; -- '' WHERE id = 1' at line 1
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]

Mặc dù, lần tấn công SQL Injection đầu tiên không thành công trên tất cả các database, nhưng bạn sẽ nhận thấy rằng mọi cơ sở dữ liệu đều có ít nhất một biến thể của SQL Injection.

3. PreparedStatement và executeUpdate

Chúng ta sẽ thay đổi ví dụ trước để sử dụng PreparedStatement nhưng sẽ tránh sử dụng đến tham số ràng buộc

public void updatePostCommentReviewUsingPreparedStatement(Long id, String review) {
    doInJPA(entityManager -> {
        Session session = entityManager.unwrap(Session.class);
        session.doWork(connection -> {
            String sql = 
                "UPDATE post_comment " +
                "SET review = '" + review + "' " +
                "WHERE id = " + id;
            try(PreparedStatement statement = connection.prepareStatement(sql)) {
                statement.executeUpdate();
            }
        });
    });
}

Và thự hiện lại test case trước:

updatePostCommentReviewUsingPreparedStatement(
    1L, "'; DROP TABLE post_comment; -- '");
 
doInJPA(entityManager -> {
    PostComment comment = entityManager.find(
        PostComment.class, 1L);
    assertNotNull(comment);
});

Chúng ta cùng xem kết quả sau khi thực hiện tấn công SQL Injection

ORACLE

Trên Oracle 11g, câu lệnh SQL Injection phía trên sẽ bị lỗi, vì JDBC driver không nhận ra dấu ;

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 911, SQLState: 22019
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - ORA-00911: invalid character
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]

SQL Server

Trên SQL Server 2014, câu lệnh SQL Injection sẽ được thực thi thành công và bảng post_comment đã bị xóa.

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]
 
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 208, SQLState: S0002
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - Invalid object name 'post_comment'.
INFO  [Alice]: o.h.e.i.DefaultLoadEventListener - HHH000327: Error performing load command : org.hibernate.exception.SQLGrammarException: could not extract ResultSet

PostgreSQL

Trên PostgreSQL 9.5, câu lệnh SQL Injection sẽ được thực thi thành công, vì mặc định PreparedStatement chỉ mô phỏng câu lệnh trong giai đoạn chuẩn bị để chỉ phải thực thi câu truy vấn một lần.

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 0, SQLState: 42P01
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - ERROR: relation "post_comment" does not exist

MySQL

Trên MySQL 5.7, câu lệnh SQL Injection sẽ bị lỗi vì JDBC driver không biên dịch đúng nhiều câu lệnh DML

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 1064, SQLState: 42000
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DROP TABLE post_comment; -- '' WHERE id = 1' at line 1
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]

PreparedStatement không bảo vệ bạn khỏi một cuộc tấn công SQL Injection nếu bạn không sử dụng các tham số ràng buộc.

4. Ngăn chặn tấn công SQL Injection như thế nào?

Giải pháp rất đơn giản, bạn chỉ cần luôn chắc chắn sử dụng các tham số ràng buộc:

public PostComment getPostCommentByReview(String review) {
    return doInJPA(entityManager -> {
        return entityManager.createQuery(
            "select p " +
            "from PostComment p " +
            "where p.review = :review", PostComment.class)
        .setParameter("review", review)
        .getSingleResult();
    });
}

Bây giờ, khi cố thực hiện hack vào câu truy vấn trên:

getPostCommentByReview("1 AND 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) )");

tấn công SQL Injection sẽ bị ngăn chặn:

Time:1, Query:["select postcommen0_.id as id1_1_, postcommen0_.post_id as post_id3_1_, postcommen0_.review as review2_1_ from post_comment postcommen0_ where postcommen0_.review=?"], Params:[(1 AND 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ))]
 
javax.persistence.NoResultException: No entity found for query

28 tháng 9, 2017

Thực hiện tùy biến Spring AOP Annotation

1. Annotation AOP là gì?

Giới thiệu nhanh thì AOP là viết tắt của lập trình hướng khía cạnh (Aspect Oriented Programming), đây là kĩ thuật chèn thêm hành vi vào đoạn code đã tồn tại mà không cần phải sửa code trực tiếp.

Bài viết giả định bạn đọc đã có những kiến thức cơ bản về lập trình hướng khía cạnh. Nếu bạn chưa có kiến thức về AOP, hãy tìm hiểu qua về khái niệm của pointcutadvice trong lập trình hướng khía cạnh.

Loại AOP mà chúng ta sẽ thực hiện tùy biến trong bài viết này là annotation driven. Chắc hẳn mọi người đều cảm thấy rất quen thuộc nếu đã từng sử dụng qua Spring @Transaction annotaion.

@Transaction
public void saveOrder(Order order) {
    // Gọi một loạt các thao tác với database trong transaction
}

Điểm mấu chốt ở đây đó là không tác động trực tiếp vào code. Bằng cách sử dụng annotation meta-data, những logic nghiệp vụ cơ bản của ứng dụng của bạn sẽ không bị trộn lẫn với những đoạn code quản lí transaction. Việc này sẽ làm cho code của bạn dễ dàng được giải thích, tái cấu trúc và kiểm thử trong transaction isolation.

Đôi khi, những lập trình viên phát triển các ứng dụng của hệ sinh thái Spring lại coi đây là điều "vi diệu" của Framework này, mà không suy nghĩ nhiều về cách thức hoạt động chi tiết của nó. Thực tế, những gì đang xảy ra không thực sự quá phức tạp. Một khi hoàn thành các bước của bài viết này, bạn sẽ có thể tự tạo ra các annotation tùy biến của riêng mình, hiểu được cách thức hoạt động và tận dụng các lợi ích của AOP mang lại.

2. Maven Dependency

Đầu tiên, hãy thêm các Maven dependency cần thiết.

Trong ví dụ này, chúng ta sẽ sử dụng Spring Boot, vì cách tiếp cận cấu hình của module này sẽ giúp chúng ta bắt đầu ví dụ một cách nhanh nhất có thể:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.7.RELEASE</version>
</parent>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

Cần lưu ý là phải thêm AOP starter, nó sẽ kéo các thư viện mà ta cần để có thể triển khai AOP.

3. Tạo annotation tùy biến

Annotation sau đây mà tôi sẽ tạo ra là một annotation được sử dụng với mục đinh log khoảng thời gian thực thi của một phương thức:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {

}

Mặc dù cách thực hiện khá là đơn giản, nhưng bạn hãy chú ý đến hai meta-annotation được sử dụng.

Annotation @Target sẽ cho bạn biết nơi mà annotation tùy biến có thể áp dụng. Ở đây ta đang sử dụng ElementType.Method, điều này có nghĩa là annotation tùy biến sẽ chỉ làm việc trên các phương thức. Nếu bạn cố gắng thử sử dụng annotation đã tạo ra ở những nơi khác, thì đoạn code của chúng ta sẽ báo lỗi trong quá trình biên dịch.

Và annotation @Retention sẽ chỉ rõ liệu annotation tùy biến của chúng ta có sẵn dùng trong quá trình runtime hay không. Mặc định điều này sẽ là không, vì vậy Spring AOP sẽ không thể nhận biết được annotation mà chúng ta đã tạo ra. Đó là lí do tại sao nó được cấu hình lại.

4. Tạo Aspect

Bây giờ, chúng ta đã có một annotation, hãy tạo thêm aspect. Đây chính là điều chúng ta cần quan tâm nhất, tất cả những gì chúng ta cần là một class được đánh dấu bởi @Aspect annotation:

@Aspect
@Component
public class ExampleAspect {

}

Bạn cũng sẽ cần phải sử dụng đến annotation @Component vì class này cũng cần phải là một spring bean trong Spring container. Về cơ bản, thì đây là class mà ta sẽ thực hiện những logic mong muốn.

5. Tạo Pointcut và Advice

Bây giờ, hãy tạo ra các pointcutadvice. Đây sẽ là một phương thức chú thích được đặt bên trong class Aspect của chúng ta:

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}

Về mặt kĩ thuật, điều này sẽ không thay đổi hành vi của bất cứ điều gì, nhưng vẫn còn rất nhiều điều đang cần phân tích.

Đầu tiên, ta đánh dấu phương thức bằng annotation @Around. Đây là advice của chúng ta, và around advice nghĩa là chúng ta đang thêm code bổ sung trước và sau khi thực thi phương thức. Cũng có một số loại advice khác, ví dụ như before, after nhưng trong phạm vi bài viết này, ta sẽ không sử dụng đến chúng.

Tiếp theo, hãy chú ý đến annotation @Around của chúng ta có một đối số point cut. Pointcut của chúng ta nói rằng: 'Áp dụng advice này lên bất kì phương thức nào được đánh dấu với annotation @LogExecutionTime.'. Cũng có rất nhiều loại pointcut khác nữa, nhưng một lần nữa chúng ta sẽ không đề cập đến chúng trong phạm vi bài viết này.

Chính phương thức logExecutionTime() sẽ là một advice, phương thức này có một đối số là ProceedingJoinPoint. Trong trường hợp của chúng ta, thì đây sẽ là một phương thức thực thi mà đã được đánh dấu bằng annotation @LogExecutionTime.

Cuối cùng, bất cứ khi nào một phương thức đã được đánh dấu được gọi đến, thì advice của chúng ta sẽ được gọi đầu tiên. Sau đó, tùy thuộc vào advice của chúng ta quyết định làm gì tiếp theo. Trong trường hợp này, advice của chúng ta không làm gì khác ngoài việc gọi đến phương thức proceed(), điều này sẽ chỉ gọi đến phướng thức gốc đã được đánh dấu.

6. Log thời gian thực thi

Bây giờ chúng ta đã có một bộ khung, tất cả những gì chúng ta cần làm là thêm một vài logic vào trong advice. Chúng ta sẽ ghi lại thời gian thực thi của phương thức ban đầu.

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
 
    Object proceed = joinPoint.proceed();
 
    long executionTime = System.currentTimeMillis() - start;
 
    System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
    return proceed;
}

Một lần nữa, chúng ta không làm bất cứ điều gì quá phức tạp ở đây cả. Chúng ta chỉ lưu lại thời gian hiện lại, thực thi phương thức, sau đó in ra console khoảng thời gian đã tính toán được. Chúng ra cũng log cả tên của phương thức, api này được cung cấp bởi joinpoint instance. Ta cũng có thể truy cập được một tá các thông tin khác nếu muốn, ví dụ như các đối số của phương thức.

Bây giờ, hãy thử đánh dấu một phương thức bằng @LogExecutionTime, và sau đó thực thi nó để xem điều gì sẽ xảy ra. Chú ý rằng, phương thức mà chúng ta muốn đánh dấu phải thuộc về một Spring bean:

@LogExecutionTime
public void serve() throws InterruptedException {
    Thread.sleep(2000);
}

Sau khi thực thi, ta sẽ thấy kết quả trên console như sau:

void com.example.Service.serve() executed in 2030ms

26 tháng 8, 2017

Tùy biến Thymeleaf

Gần đây, tôi có dịp được tham gia một dự án Java có sử dụng đến Thymeleaf, một trong số các công việc mà đội phát triển chúng tôi thực hiện đó là phải tùy biến các thẻ hoặc thuộc tính của template khi triển khai dự án.

1. Tại sao phải tùy biến thư viện Thymeleaf?


Thymeleaf là một thư viện rất mở, hầu hết các tính năng hướng tới người dùng của nó đều không được trực tiếp phát triển bên trong các thành phần cốt lõi, mà chỉ là đóng gói và thành phần hóa các tính năng này thành bộ tính năng gọi là dialect.

Thư viện sẽ cung cấp cho người dùng hai dialect có thể trực tiếp sử dụng đó là: StandardSpringStandard, nhưng bạn cũng có thể dễ dàng mở rộng, tạo ra thêm các dialect của riêng mình. Hãy cùng xem qua một số lí do có thể khiến bạn phải thực hiện việc này:

Kịch bản 1: thêm các tính năng không tồn tại trong các dialect mặc định

Giả sử, ứng dụng của bạn sử dụng dialectSpringStandard, và cần hiển thị cho người dùng cuối một thông báo có màu nền là màu xanh lam hoặc màu đỏ dựa vào role của người dùng đã đăng nhập vào hệ thống (ví dụ, admin hoặc non-admin), trong khoảng thời gian từ thứ hai đến thứ bảy hàng tuần, nếu vào chủ nhật sẽ luôn là màu xanh lá. Bạn có thể làm được điều này bằng cách thực hiện tính toán với các biểu thức điều kiện trong html template, nhưng nếu có quá nhiều điều kiện sẽ khiến code của bạn trở lên rất khó đọc, và khó bảo trì về sau.

Giải pháp: hãy tạo ra một attribute gọi là alertClass, sử dụng Java code để tính toán giá trị của attribute này và trả về CSS class mong muốn, đóng gói code này bên trong dialect có tên là MyOwnDialect, thêm dialect này vào trong template engine với prefix là th (giống như SpringStandard) và bây giờ bạn sẽ có thể sử dụng th:alertClass="${user.role}".

Kịch bản 2: view-layer components

Giả sử, công ty của bạn sử dụng Thymeleaf cho rất nhiều dự án khác nhau, và bạn mong muốn tạo ra một repository cho tất cả các chức năng phổ biến, được sử dụng lại rất nhiều lần trong một số dự án (ví dụ, các tag và/hoặc các attribute) để không phải copy-paste những đoạn code tương tự nhau từ dự án này qua các dự án kế tiếp. Bạn mong muốn tạo ra các view-layers component tương tự như các taglib trong công nghệ JSP.

Giải pháp: tạo một Thymeleaf dialect cho mỗi bộ các chức năng có liên quan với nhau, và thêm các dialect này vào ứng dụng của bạn nếu điều đó cần thiết.

Kịch bản 3: Tự tạo một template riêng

Hãy tưởng tượng rằng, bạn có một trang web cộng đồng cho phép người dùng có thể tạo ra các mẫu thiết kế của riêng họ để hiển thị nội dung. Nhưng bạn không hề muốn người dùng có thể thực hiện được toàn bộ công việc trong template của họ, thậm chí là hạn chế một vài tính năng của Standard dialect (ví dụ, các biểu thức OGNL). Vì vậy, bạn cần phải cung cấp cho người dùng khả năng thêm vào template của họ một số tính năng trong tầm kiểm soát của bạn (ví dụ như, hiển thị ảnh cá nhân, nhập nội dung văn bản, ...).

Giải pháp: Bạn cần tạo ra một Thymeleaf dialect có các thẻ hoặc thuộc tính mà bạn cho phép người dùng có thể sử dụng, giống như <mysite:profilePhoto></mysite:profilePhoto> hoặc có thể là <mysite:blogentries fromDate="23/4/2011" />. Sau đó, hãy cho phép người dùng tạo ra các template riêng có thể sử dụng các tính năng này và chỉ cho Thymeleaf cách thực hiện chúng, đảm bảo rằng không một ai có thể thực hiện được những việc mà bạn không cung cấp.

2. Dialect và Processor


2.1 Dialect


Ngoài các thuộc tính th:x hoặc các thẻ <th:y> đã được cung cấp sẵn bởi Thymeleaf với các tính năng mặc định, thì bạn hoàn toàn có thể tự tạo ra các bộ thuộc tính hoặc thẻ của riêng mình bằng tên bạn muốn và sử dụng chúng để xử lý các template của bạn.

Các Dialect là những đối tượng được implement từ interface org.thymeleaf.dialect.IDialect:

public interface IDialect {

    public String getName();

}

Yêu cầu cốt lõi duy nhất của một dialect là nó phải có tên để có thể xác định được. Tuy nhiên, cách thực hiện chỉ implement duy nhất IDialect rất ít khi được lựa chọn, thay vào đó, chúng ta thường sẽ implement từ một hoặc một số các interface con của IDialect, việc này phụ thuộc vào những gì mà Thymeleaf engine cung cấp:

  • IProcessorDialect: dialect cung cấp các processor.
  • IPreProcessorDialect: dialect cung cấp các pre-processor.
  • IPostProcessorDialect: dialect cung cấp các post-processor.
  • IExpressionObjectDialect: dialect cung cấp các object biểu thức.
  • IExecutionAttributeDialect: dialect cung cấp các thuộc tính xử lý.

Processor dialects: IProcessorDialect

IProcessorDialect interface:

public interface IProcessorDialect extends IDialect {

    public String getPrefix();
    public int getDialectProcessorPrecedence();
    public Set<IProcessor> getProcessors(final String dialectPrefix);

}

Các Processor là những đối tượng phụ trách hầu hết các xử lý logic trong các Thymeleaf template, đây có thể được coi là thành phần quan trọng nhất trong Thymeleaf:

Trong diaclect này chỉ có ba mục được định nghĩa:

  • prefix: đây là phần tiền tố hoặc namespace sẽ được áp dụng mặc định cho các phần tử hoặc các thuộc tính phù hợp với các processor của dialect này. Do đó, một dialect có tiền tố là th giống với Standard Dialect sẽ có thể xác định được các processor phù hợp với các thuộc tính như th:text, th:if, hoặc th:whatever (hoặc bạn cũng có thể sử dụng cú pháp của HTML5: data-th-text, data-th-if, và data-th-whatever). Tiền tố có thể được thiết lập trong cấu hình template engine và chúng có thể nhận giá trị null nếu như bạn muốn các processor có thể được thực thi trên các tag/attribute không cố định.
  • dialect precedence: là độ ưu tiên khi sắp xếp các processor trong các dialect
  • processors: các processor được cung cấp bởi dialect. Chú ý là phương thức getProcessors(...) nhận dialectPrefix làm tham số đầu vào trong trường hợp dialect này đã được cấu hình trong Template engine với tiền tố khác mặc định. Hầu hết các trường hợp thì ```IProcessor sẽ cần thông tin này khi khởi tạo.

Pre-processor dialects: IPreProcessorDialect

Pre-processorpost-processor khác với các processor đó là thay vì chỉ thực thi trên một sự kiện hoặc mẫu sự kiện đơn lẻ (một fragment của một template), thì chúng sẽ được áp dụng lên toàn bộ quá trình xử lý template như là một bước bổ sung trong chuỗi xử lý của engine. Do vậy, chúng sẽ được thực thi theo API hoàn toàn khác so với các processor, sẽ hướng sự kiện nhiều hơn, và được xác định bởi interface tầng thấp là ITemplateHandler.

Trong trường hợp có sử dụng các pre-processor, chúng sẽ được áp dụng trước khi Thymeleaf engine bắt đầu xử lý các processor cho một template chỉ định.

IPreProcessorDialect interface:

public interface IPreProcessorDialect extends IDialect {

    public int getDialectPreProcessorPrecedence();
    public Set<IPreProcessor> getPreProcessors();

}

Interface này tương tự với IProcessorDialect, nhưng thiếu prefix vì nó không cần thiết cho các pre-processor (chúng sẽ xử lý tất cả sự kiện xảy ra, không riêng bất kì sự kiện cụ thể nào cả)

Post-processor dialects: IPostProcessorDialect

Như đã nêu ở trên, post-processor là một bước bổ sung vào dây chuyền thực thi template, nhưng lần này chúng sẽ được thực thi sau khi Thymeleaf engine đã áp dụng tất cả các processor cần thiết. Điều này nghĩa là post-processor sẽ được áp dụng ngay trước khi template có kết quả (và do đó có thể chỉnh sửa được kết quả trả về).

IPostProcessorDialect interface:

public interface IPostProcessorDialect extends IDialect {

    public int getDialectPostProcessorPrecedence();
    public Set<IPostProcessor> getPostProcessors();

}

IPostProcessorDialect có cấu trúc tương tự với IPreProcessorDialect.

Expression Object dialects: IExpressionObjectDialect

Những dialect implement interface này cung cấp thêm các đối tượng expression object hoặc các expression utility object, các đối tượng này có thể được sử dụng trong các biểu thức ở bất kì nơi nào của một template, ví dụ, Standard Dialect mặc định có cung cấp một số đối tượng sau #strings, #numbers, #dates, #list,...

Còn đây là interface IExpressionObjectDialect:

public interface IExpressionObjectDialect extends IDialect {

    public IExpressionObjectFactory getExpressionObjectFactory();

}

Như chúng ta thấy, interface này chỉ có một phương thức duy nhất và nó không hề trả về chính các đối tượng expression, mà lại là một factory. Lí do là một vài đối tượng expression sẽ phải cần dữ liệu từ ngữ cảnh xử lý để có thể build được, do đó, nó sẽ không thể tự build cho đến khi chúng ta thực sự đang trong quá trình xử lý template... Bên cạnh đó, hầu hết các biểu thức đều không cần dùng đến các đối tượng expression, vì vậy sẽ tốt hơn nếu các đối tượng này chỉ được build khi thực sự cần thiết đối với các biểu thức cụ thể (và cũng chỉ build những gì thật cần thiết).

Đây là interface IExpressionObjectFactory:

public interface IExpressionObjectFactory {

    public Map<String,ExpressionObjectDefinition> getObjectDefinitions();

    public Object buildObject(final IProcessingContext processingContext, final String expressionObjectName);

}

Dialect xử lý thuộc tính: IExecutionAttributeDialect

Các Dialect implement interface này được cung cấp khả năng xử lý các thuộc tính.

Ví dụ, Standard Dialect implement interface này để cung cấp cho mọi processor:

  • Thymeleaf Standard Expression parser: các Standard Expression trong bất kì thuộc tính đều có thể được phân tích và thực thi.
  • Variable Expression Evaluator: các biểu thức ${...} được thực thi trong OGNL hoặc SpringEL (nếu tích hợp với Spring module).
  • Conversion Service: thực hiện các tính toán chuyển đổi trong biểu thức ${{...}}.

Lưu ý rằng, những đối tượng này không có sẵn trong ngữ cảnh, vì vậy, chúng không thể được sử dụng từ các biểu thức template. Tính khả dụng của chúng bị giới hạn trong việc triển khai mở rộng, như là các processor, pre-processor,...

Interface này đơn giản chỉ là:

public interface IExecutionAttributeDialect extends IDialect {

    public Map<String,Object> getExecutionAttributes();

}

2.2. Processor


Processor là những đối tượng implement interface org.thymeleaf.processor.IProcessor, và chúng chứa các logic thật sự để áp dụng cho các phần khác nhau của một template.

Interface này có cấu trúc như sau:

public interface IProcessor {

    public TemplateMode getTemplateMode();
    public int getPrecedence();

}

Giống với các dialect, đây là một interface rất đơn giản, chỉ xác định mode của templateprocessor có thể được sử dụng và độ ưu tiên của nó.

Nhưng có một vài loại processor tương ứng với mỗi loại sự kiện có thể xảy ra:

  • Template start/end
  • Element Tags
  • Texts
  • Comments
  • CDATA Sections
  • DOCTYPE Clauses
  • XML Declarations
  • Processing Instructions

Tùy biến Thymeleaf


(by @dangquando)

26 tháng 7, 2017

Quản lý transaction trong Spring Framework - Spring @Transaction

Spring là một framework Java được sử dụng phổ biến nhất hiện nay, nó mang đến rất nhiều tính năng và nhiều phần bổ trợ cho các ứng dụng Java. Tuy nhiên, hầu hết mọi người đều có khuynh hướng sử dụng những tính năng này mà không thực sự hiểu cơ chế bên dưới của chúng.

1. Cách sử dụng và trường hợp sử dụng

@Transaction(value = "myTransactionManager", propagation = Propagation.REQUIRED)
public void myMethod() {
...
}

Thuộc tính value của annotation @Transaction không bắt buộc phải được khai báo. Nếu không khai báo thuộc tính này thì mặc định Spring sẽ tìm kiếm một bean bất kì được khai báo bên trong context có tên là "transactionManager" (đây là convention mặc định trong Spring).

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
  <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

2. Cấu hình sử dụng transaction trong Spring context

Muốn annotation @Transaction có thể hoạt động được, ta sẽ phải khai báo thẻ <tx:annotation-driven> (trong đó tx là rút gọn của namespace "http://www.springframework.org/schema/tx" hoặc cũng có thể tạm hiểu đây là một alias cho namespace này).

3. Phân tích code

3.1. Khai báo spring bean

Ở phần này, chúng ta sẽ xem xét cách mà Spring context xử lý khi khai báo sử dụng thẻ <tx:annotation-driven>

1, org.springframework.transaction.config.AnnotationDrivenBeanDefinitionParser

/**
* Parses the '<code></code>' tag. Will
* {@link AopNamespaceUtils#registerAutoProxyCreatorIfNecessary register an AutoProxyCreator}
* with the container as necessary.
*/
public BeanDefinition parse(Element element, ParserContext parserContext) {
  String mode = element.getAttribute("mode");
  if ("aspectj".equals(mode)) {
    // mode="aspectj"
    registerTransactionAspect(element, parserContext); 
  } else {
    // mode="proxy"
    // DEFAULT MODE
    AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);
  }
  return null;
}

Hầu hết, chúng ta sẽ đều đi vào block else của block điều kiện if-else trong đoạn code bên trên (mode="proxy"), vì vậy chúng ta sẽ gọi AopAutoProxyConfigurer.configureAutoProxyCreator()

29 tháng 4, 2017

Hạn chế sai lầm tỉ đô khi sử dụng ngôn ngữ Java

Trong ngành điện toán, khái niệm con trỏ rỗng chỉ một biến con trỏ có một giá trị định sẵn khiến cho nó không trỏ được tới bất kì một đối tượng chính tắc nào. Charles Antony Richard Hoare tác giả của ngôn ngữ Algol W từng nói rằng việc phát minh ra tham chiếu rỗng là một sai lầm tỉ đô của ông. Vậy tại sao ông lại gọi đó là sai lầm tỉ đô? Trong ngôn ngữ Java, tham chiếu đến một đối tượng rỗng sẽ sinh ra ngoại lệ NullPointerException, đây là một unchecked exception. Việc xác định exception này có xảy ra hay không, chỉ có thể thực hiện được ở thời điểm runtime và compiler trong quá trình biên dịch không thể giúp lập trình viên xác định được chuyện đó. Chính điều này dẫn đến việc cho dù sản phẩm của bạn đã vượt qua được cơ số các bài test của đội QA tại môi trường test, nhưng đến một thời điểm nào đó sau khi đã lên production thì hệ thống đột ngột lại ngừng hoạt động do phát sinh lỗi, hệ thống sụp đổ dẫn đến thiệt hại có thể lên đến cả tỉ đô la. Dưới đây là một số biện pháp có thể áp dụng để hạn chế điều tồi tệ trên có thể xảy ra trong quá trình phát triển phần mềm khi sử dụng ngôn ngữ Java:

1, Gọi phương thức equals()equalsIgnoreCase() trên các đối tượng String đã biết sẽ tốt hơn là gọi trên các Object chưa xác định


Hãy nhớ luôn gọi phương thức equals() trên một đối tượng String không null đã biết. Vì phương thức equals() là phương thức đối xứng, nên khi gọi a.equals(b) sẽ tương tự như khi gọi b.equals(a), và đây là lí do mà nhiều lập trình viên không chú ý đến hai đối tượng a và b. Hệ quả của việc gọi phương thức bất cẩn như thế có thể dẫn đến việc chương trình ném ra NullPointerException nếu như đối tượng gọi phương thức equals()null.

Object unknownObject = null;

// Cách thực hiện sai - có thể gây ra NullPointerException
if(unknownObject.equals("knownObject")) {
    System.err.println("This may result in NullPointerException if unknownObject is null");
}

// Cách thực hiện đúng - tránh được NullPointerException thậm chí nếu unknownObject là một đối tượng null
if("knownObject".equals(unknownObject)) {
    System.err.println("better coding avoided NullPointerException");
}

Đây là một trong các cách thực hiện dễ dàng nhất khi sử dụng ngôn ngữ Java để tránh được NullPointerException, nhưng kết quả mà nó mang lại được cải thiện vô cùng lớn vì phương thức equals() là một trong những phương thức được sử dụng phổ biến nhất.

2, Nên sử dụng valueOf() thay vì toString() khi cả hai phương thức trả về cùng một giá trị như nhau


Vì khi gọi phương thức toString() trên một đối tượng null, thì nó sẽ ném ra NullPointerException. Nếu sử dụng phương thức valueOf() mà cũng có thể trả về giá trị giống với khi sử dụng phương thức toString() thì ta nên sử dụng valueOf(), vì gọi phương thức valueOf() trên một đối tượng null nó sẽ trả về chuỗi "null", chứ không ném ra NullPointerException, đặc biệt là trong trường hợp các lớp Wrapper như Integer, Float, Double hoặc BigDecimal.

BigDecimal bd = getPrice();

/* Không ném ra NullPointerException */
System.out.println(String.valueOf(bd));

/* Ném ra "Exception trong "main" thread java.lang.NullPointerExxception" */
System.out.println(bd.toString());

Sử dụng cách này nếu bạn không chắc chắn một đối tượng là null hay không null.

3, Nên sử dụng các phương thức và thư viện null safe


Trong cộng đồng mã nguồn mở có rất nhiều thư viện có thể giúp bạn dễ dàng hơn trong việc kiểm tra null. Phổ biến nhất trong số này là lớp StringUtils từ thư viện Apache commons. Bạn có thể sử dụng StringUtils.íBlank(), isNumeric(), isWhitSpace() và các phương thức hữu ích khác mà không phải lo lắng về NullPointerException.

// Các phương thức trong lớp StringUtils là null safe, chúng không ném ra NullPointerExcetion
System.out.println(StringUtils.isEmpty(null));
System.out.println(StringUtils.isBlank(null));
System.out.println(StringUtils.isNumeric(null));
System.out.println(StringUtils.isAllUpperCase(null));

Output:
true
true
false
false

4, Không nên viết một phương thức trả về tập hợp là null, thay vào đó hãy trả về một empty collection hoặc empty array


Bằng việc trả về một empty collection hoặc empty array, bạn có thể chắc chắn đươc rằng khi gọi các phương thức cơ bản như size() hoặc length() chương trình sẽ xảy ra lỗi vì NullPointerException. Lớp Collections trong Java cung cấp tập hợp các đối tượng List, Set, Map rỗng thuận tiện sử dụng trong các trường hợp phù hợp, ví dụ như: Collections.EMPTY_LIST, Collections.EMPTY_SETCollections.EMPTY_MAP

Ví dụ:

@Service
public class EmployeeService {

    private EmployeeDao employeeDao;

    public List<Contact> getContacts(Employee employee) {
        List<Contact> contacts;
        try {
            contacts = this.employeeDao.getContacts(employee); 
        } catch (Exception e) {
            contacts = Collections.EMPTY_LIST;
        }
        return contacts;
    }

    @Autowired
    public void setEmployeeDao(EmployeeDao employeeDao) {
        Assert.notNull(employeeDao, "EmployeeDao must not be null!");
        this.employeeDao = employeeDao;
    }
}

Tương tự, bạn có thể sử dụng Collections.EMPTY_SETCollections.EMPTY_MAP thay vì trả về null.

5, Hãy sử dụng annotation @NotNull@Nullable


Trong khi viết code, ta có thể đánh dấu một đoạn code có khả năng gây ra null bằng cách sử dụng annotation @NotNull@Nullable, hai annotation này sẽ đánh dấu liệu một phương thức có phải là null safe hay không. Nhiều trình biên dịch, IDE hoặc tool hiện nay đều có thể đọc được hai annotation này và nó sẽ gợi ý cho bạn khi có thiếu sót trong việc kiểm tra null, hoặc những công cụ này cũng có thể sẽ thông báo cho bạn nếu như việc kiểm tra null tại một đoạn code nào đó là không cần thiết. IntelliJ IDE và plugin findbugs là một trong những công cụ có hỗ trợ tính năng này. Hai annotation này là một phần trong JSR 305, nhưng ngay cả khi không có công cụ hoặc IDE nào hỗ trợ thì việc đánh dấu code bằng các annotaiton này thì điều này cũng giống như một tài liệu cho source code, giúp cho các lập trình viên khi nhìn vào có thể đưa ra quyết định có kiểm tra null hay không.

6, Tránh sử dụng autoboxing và unboxing không cần thiết trong code


Mặc dù cách thực hiện này gây ra những bất lợi khác như là tạo ra object tạm thời, nhưng vì nếu lớp Wrappernull thì autoboxing rất dễ gây ra NullPointerException. Ví dụ, khi thực hiện đoạn code sau sẽ bị lỗi và ném ra NullPointerException nếu đối tượng Person không có phone number và chương trình sẽ trả về null nếu bạn cố gắng thực hiện phương thức getPhone().

Person person = new Person("Psyduck");
int phone = person.getPhone();

Không chỉ khi so sánh tương đương mà khi sử dụng các toán tử <, > cũng có thể gây ra NullPointerException nếu như được sử dụng cùng với autoboxingunboxing.

7, Theo sát Contract đã thiết kế và xác định các giá trị mặc định hợp lí


Trong Java, một cách đơn giản khác để tránh được NullPointerException đó là định nghĩa ra Contract và thực hiện theo đúng như thiết kế. Nhiều trường hợp dẫn đến NullPointerException đó là do Object được tạo ra có dữ liệu hoặc các dependency bắt buộc chưa được cung cấp đầy đủ. Nếu bạn định nghĩa ra các Contract không cho phép tạo đối tượng bị thiếu thông tin như vậy thì bạn sẽ có thể ngăn chặn sớm được việc chương trình bị lỗi do NullPointerException. Bạn cũng nên định nghĩa ra các giá trị mặc định hợp lí khi viết code, ví dụ như một đối tượng Employee không thể được tạo nếu thiếu các thuộc tính id, name, nhưng số điện thoại là tùy chọn. Nếu Employee không có số điện thoại thì thay vì trả về null, ta sẽ trả về giá trị mặc định, ví dụ như trả về 0, nhưng lựa chọn này cần phải được thực hiện một cách cẩn trọng vì trong một số trường hợp thì việc kiểm tra null sẽ dễ dàng hơn là gọi một số không hợp lệ.

8, Tạo ràng buộc trong database


Nếu bạn đang sử dụng cơ sở dữ liệu cho việc lưu trữ các domain object giống như Customers, Orders, Contacts, ... thì hãy nên định nghĩa ra các ràng buộc xác định rõ một trường dữ liệu có khả năng được nhận giá trị null hay không. Vì cơ sở dữ liệu có thể thu được dữ liệu từ nhiều nguồn khác nhau, vậy nên sử dụng các ràng buộc để kiểm tra dữ liệu đầu vào trước khi insert hay update sẽ đảm bảo được sự toàn vẹn của dữ liệu. Việc cung cấp các ràng buộc kiểm tra giá trị null cũng giảm số lần phải kiểm tra null khi thực hiện viết code Java. Khi thực hiện lấy dữ liệu về từ cơ sở dữ liệu để truyền vào các object, bạn đã nắm rõ được thuộc tính nào của object có thể là null, và thuộc tính nào sẽ không bao giờ có thể nhận giá trị null nên điều này sẽ giảm số lần kiểm tra != null không cần thiết trong code của bạn tới mức tối thiểu.

9, Sử dụng Null Object Pattern


Trong Java thì đây cũng là một cách để đảm bảo chương trình của bạn sẽ không ném ra NullPointerException.

Hạn chế sai lầm tỉ đô khi sử dụng ngôn ngữ Java


(by @dangquando)

15 tháng 9, 2016

Kiểm soát quyền truy cập tới các thuộc tính của một Class trong Java

            | Class | Package | Subclass | Subclass | World  |
            |       |         |(same pkg)|(diff pkg)|        |
————————————+———————+—————————+——————————+——————————+————————+
public      |   +   |    +    |    +     |     +    |   +    |
————————————+———————+—————————+——————————+——————————+————————+
protected   |   +   |    +    |    +     |     +    |   o    | 
————————————+———————+—————————+——————————+——————————+————————+
no modifier |   +   |    +    |    +     |     o    |   o    |
————————————+———————+—————————+——————————+——————————+————————+
private     |   +   |    o    |    o     |     o    |   o    |
--------------------------------------------------------------
+ : accessible
o : not accessible

1 tháng 4, 2016

Cài đặt Oracle JDK trong Ubuntu

  1. Thông báo cho hệ thống biết nơi chứa jdk
  2. $ sudo update-alternatives --install "/usr/bin/java" java "/opt/java/oracle_jdk/bin/java" 1
    $ sudo update-alternatives --install "/usr/bin/javac" javac "/opt/java/oracle_jdk/bin/javac" 1
    $ sudo update-alternatives --install "/usr/bin/jar" jar "/opt/java/oracle_jdk/bin/jar" 1
    $ sudo update-alternatives --install "/usr/bin/javaws" javaws "/opt/java/oracle_jdk/bin/javaws" 1

  3. Mở file PATH của hệ thống:
  4. Mở Terminal và gõ lệnh sau:

    $ sudo gedit /etc/profile.d/jdk.sh

    hoặc:

    $ sudo vi /etc/profile.d/jdk.sh

  5. Sửa file PATH của hệ thống:
  6. Thêm vào các dòng lệnh sau:

    #!/bin/bash
    export JAVA_HOME=/opt/java/oracle_jdk
    export PATH=$PATH:$JAVA_HOME/bin

  7. Logout hoặc khởi động lại.
Lưu ý: Cài đặt Maven tương tự nhưng chỉ cần sửa PATH của hệ thống là được.