menu

VanThuong.Com

Blog học lập trình Java và SEO miễn phí.

Kết hợp Java Reflection và Java Annotations - Bạn đã biết chưa?

Posted by on
Mục lục
Kết hợp Reflection và Annotation
Class Annotations
Field Annotations
Method Annotations
Parameter Annotations
Thực hành
Bạn có thể sử dụng Reflection để truy cập (access) các annotations được áp dụng trong các class tại thời điểm Runtime. Và theo tôi thì, Java Reflection + Annotations = bí kíp võ công đầu tiên mà bạn cần phải luyện nếu muốn trở thành cao thủ. Các bạn có thể xem lại hai bài viết mà tôi đã hướng dẫn cách sử dụng Java Annotation cũng như giới thiệu về Java Reflection trước khi luyện công.

Kết hợp Reflection và Annotation

Nếu như bạn mới chập chững bước vào tìm hiểu về ngôn ngữ Java thì tôi xin cam đoan đây là phần cực kỳ thú vị mà bạn không nên bỏ qua. Bên cạnh đó, nếu như bạn muốn sử dụng JDBC để làm việc với Database mà không cần đến sự hỗ trợ của Hibernate thì đây là cách hiệu quả nhất cho bạn. Với sự kết hợp của Java Reflection và Annotation, bạn có thể ứng dụng nó trong nhiều trường hợp như cần Generic, validate dữ liệu, cấu hình,...
Kết hợp Java Reflection và Java Annotations - Bạn đã biết chưa?

Trường hợp đặc biệt, nếu như bạn đã sử dụng qua Hibernate và nắm bắt được cách mà các Annotation được Hibernate hỗ trợ hoạt động như thế nào. Trong khi đó bạn chỉ cần một vài chức năng cơ bản giống như Hibernate thì bạn cũng có thể tự mình viết ra các lớp Util để hỗ trợ công việc dựa trên ý tưởng đó thông qua Java Annotation và Java Reflection

Để giúp các bạn dễ hiểu hơn khi đọc code example, tôi sẽ tạo một dự án Java demo nho nhỏ với structure như sau:
cấu trúc dự án Java Reflection & Annotation

Tiếp theo, các bạn copy dán code vào các file tương ứng như sau:
com.vanthuong.example.annotations.Column
package com.vanthuong.example.annotations;

import java.lang.annotation.*;

@Documented
@Target(ElementType.FIELD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
  String name() default "";
}

com.vanthuong.example.annotations.Table
package com.vanthuong.example.annotations;

import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
  String name() default "";
}

com.vanthuong.example.annotations.Permission
package com.vanthuong.example.annotations;

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
  String[] value();
}

Tạo file Customer.java trong package com.vanthuong.example.model và chép đoạn code bên dưới đây vào
package com.vanthuong.example.model;

import com.vanthuong.example.annotations.Column;
import com.vanthuong.example.annotations.Table;

@Table(name = "customers")
public class Customer {

  @Column(name = "id")
  private Long id;

  @Column(name = "name")
  private String contactName;

  @Column(name = "title")
  private String contactTitle;

  @Column(name = "address")
  private String address;

  @Column(name = "phoneNumber")
  private String phoneNumber;

  public Customer() {
  }

  public Customer(Long id, String contactName, String contactTitle, String address, String phoneNumber) {
    this.id = id;
    this.contactName = contactName;
    this.contactTitle = contactTitle;
    this.address = address;
    this.phoneNumber = phoneNumber;
  }

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getContactName() {
    return contactName;
  }

  public void setContactName(String contactName) {
    this.contactName = contactName;
  }

  public String getContactTitle() {
    return contactTitle;
  }

  public void setContactTitle(String contactTitle) {
    this.contactTitle = contactTitle;
  }

  public String getAddress() {
    return address;
  }

  public void setAddress(String address) {
    this.address = address;
  }

  public String getPhoneNumber() {
    return phoneNumber;
  }

  public void setPhoneNumber(String phoneNumber) {
    this.phoneNumber = phoneNumber;
  }

  @Override
  public String toString() {
    return "Customer{" +
        "id=" + id +
        ", contactName='" + contactName + '\'' +
        ", contactTitle='" + contactTitle + '\'' +
        ", address='" + address + '\'' +
        ", phoneNumber='" + phoneNumber + '\'' +
        '}';
  }
}

Giải thích chút xíu, ở đây các annotation được tạo ra mới mục đích như sau:
- @Table: Được sử dụng cho class với mục đích đánh dấu model (Customer, Employee...) map với 1 table cụ thể nào đó dưới database thông qua method name().
- @Column: Sử dụng cho các field trong Model - map tên các field xuống tên Column trong database
- @Permission: Annotation dùng để chú thích rằng chỉ có những ai có quyền được liệt kê mới cho phép sử dụng phương thức đã apply nó.

Class Annotations

Đầu tiên, chúng ta sẽ tìm hiểu cách lấy các annotation được áp dụng cho class. Như các bạn thấy, tôi đã áp dụng annotation @Table(name = "customers") cho lớp com.vanthuong.example.model.Customer.

Tại thời điểm Runtime tôi có thể get được một mảng các Annotation đã áp dụng cho Customer class bằng cách đơn giản nhất là gọi phương thức getAnnotations() của đối tượng Class.

Class<Customer> customerClass = Customer.class; // obtain class object
Annotation[] annotations = customerClass.getAnnotations();

// Lọc ra các annotations là thể hiện của Table
for (Annotation annotation : annotations) {
  if (annotation instanceof Table) {
    Table tableAnnotation = (Table) annotation;
    System.out.println("name: " + tableAnnotation.name());
  }
}

Bên cạnh đó, bạn có thể sử dụng một phương thức khác có tên là getAnnotation() để lấy chính xác Annotation mà mình mong muốn như sau:
Class<Customer> customerClass = Customer.class;

Annotation tableAnnotation = customerClass.getAnnotation(Table.class);
if (tableAnnotation != null) {
  System.out.println(((Table) tableAnnotation).name());
} else {
  System.out.println("Table annotation is not applied to Customer class");
}

Field Annotations

Cũng từ lớp Customer ở trên, bạn thấy rằng tôi đang áp dụng annotation @Column cho các field được khai báo như: id, contactName, contactTitle,... Vậy làm sao để access được các Annotation mà bạn đã áp dụng cho field?

Đầu tiên, các bạn cần xem lại bài viết về cách làm việc với các Field trong Class với Java Reflection mà tôi đã giới thiệu trước đó để ôn tập nhé. Dưới đây là đoạn code mà tôi dùng để lấy được một mảng các Annotations đã áp dụng cho một field.

Field field = // ... obtain Field object 
Annotation[] annotations = field.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
  if (annotation instanceof Column) {
    Column columnAnnotation = (Column) annotation;
    System.out.println(columnAnnotation.name());
  }
}

Cũng tương tự như ở Class level, bạn hoàn toàn có thể lấy được Annotation cụ thể mà bạn biết chắc đã áp dụng cho Field như sau:
Field field = // ... obtain Field object
Annotation annotation = field.getDeclaredAnnotation(Column.class);
if (annotation instanceof Column) {
  Column columnAnnotation = (Column) annotation;
  System.out.println(columnAnnotation.name());
}

Method Annotations

Annotation còn được áp dụng cho các phương thức trong một Class. Ví dụ như bên dưới đây, tôi đã áp dụng Annotation @Permission cho phương thức public void add(Customer customer).
@Permission(value = {"Administration", "Manager"})
public void add(Customer customer) {
  if (customer != null) {
    customerPropsByIds.put(customer.getId(), convert(customer));
  }
}
OK bây giờ tôi có thể lấy ra mảng các Annotation đã áp dụng cho method như sau:
Method method = //...obtain method object
Annotation[] annotations = method.getDeclaredAnnotations();

for (Annotation annotation : annotations) {
  if (annotation instanceof Permission) {
    Permission permissionAnnotation = (Permission) annotation;
    String[] permissions = permissionAnnotation.value();
  }
}

Tương tự như vậy, bạn cũng có thể lấy được đối tượng Annotation cụ thể đã áp dụng cho một phương thức nếu như bạn biết trước.

Method method = // ...obtain method object
Annotation annotation = method.getAnnotation(Permission.class);

if (annotation instanceof Permission) {
  Permission permissionAnnotation = (Permission) annotation;
  System.out.println(permissionAnnotation.value());
}

Parameter Annotations

Các parameter truyền vào một method cũng có thể dùng Annotation. Với Java Reflection chúng ta cũng có thể access được các Annotation này.
Ví dụ chúng ta có doSomething() method và parameter của nó được đánh dấu chú thích như sau:
public class DemoClass {
  public static void doSomething (
        @MyAnnotation(name = "aName", value = "aValue") String parameter){
  }
}

Chúng ta sẽ lấy được các Annotation đã áp dụng cho các parameter từ đối tượng Method như sau:
Method method = //...obtain method object
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class[] parameterTypes = method.getParameterTypes();

int i = 0;
for (Annotation[] annotations : parameterAnnotations){
  Class parameterType = parameterTypes[i++];

  for (Annotation annotation : annotations){
    if (annotation instanceof MyAnnotation){
        MyAnnotation myAnnotation = (MyAnnotation) annotation;
        System.out.println("param: " + parameterType.getName());
        System.out.println("name : " + myAnnotation.name());
        System.out.println("value: " + myAnnotation.value());
    }
  }
}

Các bạn lưu ý, phương thức Method.getParameterAnnotations() sẽ trả về một mảng Annotation 2 chiều. Mỗi array tương ứng với danh sách các Annotation được dùng cho một parameter của method.

Thực hành

Từ dự án mà các bạn đã tạo ở đầu bài viết, hãy tạo một package mới và đặt tên nó là enums. Sau đó, tạo ra các enum Action, UserRole với nội dung như sau:
com.vanthuong.example.enums.Action
package com.vanthuong.example.enums;

/**
 * Official blog: http://www.vanthuong.com
 * Learn and share your java knowledge
 */
public enum Action {
  READ("get"),
  ADD("add"),
  UPDATE("update"),
  DELETE("delete");

  private final String value;

  Action(String value) {
    this.value = value;
  }

  public String value() {
    return this.value;
  }
}

com.vanthuong.example.enums.UserRole
package com.vanthuong.example.enums;

/**
 * Official blog: http://www.vanthuong.com
 * Learn and share your java knowledge
 */
public enum UserRole {
  ADMINISTRATION,
  MANAGER,
  USER;
}

Tiếp theo, hãy tạo mới một package khác và đặt tên nó là dao. Trong package này, chúng ta sẽ có các lớp giao tiếp với database. Tuy nhiên, trong bài viết này chỉ dừng lại mức giới thiệu về cách sử dụng Java Reflection + Annotation nên tôi sẽ không làm trực tiếp với database mà sẽ tạo một đối tượng Store như 1 DB giả để lưu trữ dự liệu. Dưới đây là nội dung của các tập tin này.
com.vanthuong.example.dao.Store
package com.vanthuong.example.dao;

/*
 * Official blog: http://www.vanthuong.com
 * Learn and share your java knowledge
 */

import com.vanthuong.example.annotations.Column;
import com.vanthuong.example.annotations.Permission;
import com.vanthuong.example.model.Customer;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.vanthuong.example.enums.UserRole.ADMINISTRATION;
import static com.vanthuong.example.enums.UserRole.MANAGER;

public class Store {
  private final Map<Long, Map<String, Object>> customerPropsByIds = new HashMap<>();

  public Store() {
    Map<String, Object> valuesByProperties = new HashMap<>();

    System.out.println("--- Init mock database ---");
    valuesByProperties.put("id", 1L);
    valuesByProperties.put("name", "Nguyen Van A");
    valuesByProperties.put("title", "MANAGER");
    valuesByProperties.put("address", "Somme where");
    valuesByProperties.put("phoneNumber", "123456789");

    customerPropsByIds.put(1L, valuesByProperties);
  }

  public List<Customer> getCustomers() {
    List<Customer> customers = new ArrayList<>();

    customerPropsByIds.values()
        .forEach(customerPropsByKeys -> {
          Customer customer = new Customer();
          customer.setId((Long) customerPropsByKeys.get("id"));
          customer.setContactName((String) customerPropsByKeys.get("name"));
          customer.setContactTitle((String) customerPropsByKeys.get("title"));
          customer.setAddress((String) customerPropsByKeys.get("address"));
          customer.setPhoneNumber((String) customerPropsByKeys.get("phoneNumber"));

          customers.add(customer);
        });

    return customers;
  }

  @Permission(value = {ADMINISTRATION, MANAGER})
  public void add(Customer customer) {
    if (customer != null) {
      customerPropsByIds.put(customer.getId(), convert(customer));
    }
  }

  public void select(String columnName) {
    // TODO: add your code here to implement this action
  }

  @Permission(value = {ADMINISTRATION, MANAGER})
  public void delete() {
    // TODO: add your code here to implement this action
  }

  @Permission(value = {ADMINISTRATION, MANAGER})
  public void update() {
    // TODO: add your code here to implement this action
  }

  private Map<String, Object> convert(Customer customer) {
    Map<String, Object> valuesByProperties = new HashMap<>();
    Class<?> customerClass = customer.getClass();
    Field[] fields = customerClass.getDeclaredFields();

    for (Field field : fields) {
      getFieldValueByColumn(customer, field, valuesByProperties);
    }

    return valuesByProperties;
  }

  private void getFieldValueByColumn(Customer customer,
                                     Field field,
                                     Map<String, Object> valuesByProperties) {
    field.setAccessible(true);
    try {
      Annotation[] annotations = field.getDeclaredAnnotations();
      for (Annotation annotation : annotations) {
        if (annotation instanceof Column) {
          String propertyName = ((Column) annotation).name();
          Object value = field.get(customer);
          valuesByProperties.put(propertyName, value);
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
Trong lớp này các bạn thấy rằng, tôi đã tạo ra biến customerPropsByIds để mô phỏng như là một Database để lưu trữ giá trị của các thuộc tính có trong model Customer như là một column.

- Phương thức getCustomers() được dùng để tất cả dữ liệu có trong customerPropsByIds để trả về. Giả sử nếu tôi dùng database thì đoạn code bên trong sẽ thay thế bằng việc parse đối tượng SQL sang Customer Object.
- Phương thức add() được dùng để thêm mới một customer vào database.
- Phương thức convert() được sử dụng để convert một đối tượng Customer sang dữ liệu có thể lưu vào customerPropsByIds. Tuy nhiên, nếu như các bạn sử dụng Database thì phương thức này sẽ được dùng để chuyển thành câu lệnh SQL chẳng hạn.

Tóm lại, lớp mục đích của tôi là muốn cho các bạn thấy cách mà tôi đang thực hiện reflection với các annotation của các field trong một class, cụ thể là @Column và lớp Customer.

com.vanthuong.example.dao.CustomerDAO
package com.vanthuong.example.dao;

import com.vanthuong.example.annotations.Permission;
import com.vanthuong.example.enums.Action;
import com.vanthuong.example.enums.UserRole;
import com.vanthuong.example.exceptions.CustomException;
import com.vanthuong.example.model.Customer;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.vanthuong.example.enums.Action.ADD;
import static com.vanthuong.example.enums.UserRole.ADMINISTRATION;

/**
 * Official blog: http://www.vanthuong.com
 * Learn and share your java knowledge
 */
public class CustomerDAO {
  private final Store store;

  public CustomerDAO() {
    store = new Store();
  }

  public List<Customer> getAll() {
    return store.getCustomers();
  }

  public void add(Customer customer) throws CustomException {
    // Hard code to execute method with Administration right
    if (hasPermission(ADD, ADMINISTRATION)) {
      store.add(customer);
    } else {
      throw new CustomException("You don't have permission to execute this action");
    }
  }

  private boolean hasPermission(Action action, UserRole userRole) throws CustomException {
    Class<?> storeClass = store.getClass();
    List<UserRole> permissions = new ArrayList<>();

    try {
      Method method = storeClass.getMethod(action.value());
      Annotation[] annotations = method.getDeclaredAnnotations();
      permissions.addAll(getPermissions(annotations));

      return permissions.isEmpty() || permissions.contains(userRole);
    } catch (NoSuchMethodException e) {
      throw new CustomException(
          String.format("Action %s is not existing in %s class", action.value(), storeClass.getName()));
    }
  }

  private List<UserRole> getPermissions(Annotation[] annotations) {
    List<UserRole> roles = new ArrayList<>();
    for (Annotation annotation : annotations) {
      if (annotation instanceof Permission) {
        Permission permission = (Permission) annotation;
        UserRole[] permissionRoles = permission.value();
        if (permissionRoles.length > 0) {
          roles.addAll(Arrays.asList(permissionRoles));
        }
      }
    }
    return roles;
  }
}

Ở lớp CustomerDAO này chúng ta chủ yếu tập trung vào phương thức hasPermission(). Và đây là cách mà tôi sử dụng Reflection cho các annotation được áp dụng cho một phương thức.

OK, bây giờ chúng ta lại tạo thêm một package mới là exceptions và một lớp mới có tên là CustomException.

com.vanthuong.example.exceptions.CustomException
package com.vanthuong.example.exceptions;

/**
 * Official blog: http://www.vanthuong.com
 * Learn and share your java knowledge
 */
public class CustomException extends Exception {
  public CustomException() {
    super();
  }

  public CustomException(String message) {
    super(message);
  }

  public CustomException(String message, Throwable cause) {
    super(message, cause);
  }

  public CustomException(Throwable cause) {
    super(cause);
  }
}

Cuối cùng, tạo mới class Application và chép nội dung bên dưới đây vào để chạy thử chương trình.
com.vanthuong.example.Application
package com.vanthuong.example;


/*
 * Official blog: http://www.vanthuong.com
 * Learn and share your java knowledge
 */

import com.vanthuong.example.dao.Store;
import com.vanthuong.example.model.Customer;

import java.util.List;

public class Application {
  public static void main(String[] args) {
    Store store = new Store();

    List<Customer> customers = store.getCustomers();
    System.out.println("--- LIST OF CUSTOMERS ---");
    customers.forEach(System.out::println);

    System.out.println("--- ADD NEW CUSTOMER ---");
    Customer customer = new Customer();
    customer.setId(2L);
    customer.setContactName("Thuong Van Nguyen");
    customer.setContactTitle("Software Engineer");
    customer.setPhoneNumber("123123123123");
    customer.setAddress("HCM");
    store.add(customer);

    List<Customer> customerList = store.getCustomers();
    System.out.println("--- LIST OF CUSTOMERS ---");
    customerList.forEach(System.out::println);
  }
}
Chương trình sẽ in ra kết quả như hình bên dưới
Kết quả chạy chương trình

It's your turn

Yeah, nãy giờ xem xong bài viết này các bạn cũng thấy rằng trong các lớp Store và CustomerDAO tôi vẫn còn chưa implement các phương thức như select(), update(), delete(). Đây chính là phần để các bạn practice. Ngoài ra, các bạn có thể thay đổi cách tiếp cận sang dùng Database để dễ dàng hơn trong việc thực hành. Thay vì convert chúng thành Map<String, Object> các bạn có thể viết câu Query để giao tiếp với database thật sự.

Dưới đây là liên kết để các bạn tải về dự án này đầy đủ và tiếp tục thực hành.
https://github.com/vanthuongkb/reflectionexample1
Trong bài viết sắp tới, tôi sẽ có gắng cho ra một dự án kết hợp sử dụng Reflection + Annotation + MySQL để cho nội dung hay hơn. Các bạn đón đọc nhé.
Chúc các bạn học tốt.
Đăng bình luận

menu