Để nắm vững và sử dụng thành thạo Spring, chúng ta cần hiểu cách thức Spring IoC Container vận hành. Bài viết này trình bày về việc xây dựng một IoC Container đơn giản, có tên là Simple IoC Container, mô phỏng những nét cơ bản nhất của Spring IoC Container. Thông qua cấu trúc và quá trình xây dựng Simple IoC Container, chúng ta có thể đối chiếu và hình dung về cách thức mà Spring IoC Container làm việc.

Chúng ta minh họa bài viết bằng ví dụ đơn giản sau: lập trình đếm tổng số lượng hàng hóa và tổng số tiền hàng của một cửa hàng trực tuyến.

Hình 1. Sơ đồ tổng quan của Simple IoC Container

Các lớp đối tượng mô hình nghiệp vụ

Thông tin của một mặt hàng được lưu trong một đối tượng Product. Mỗi mặt hàng có 2 tính chất tương ứng với 2 thuộc tính của đối tượng Product liên quan đến tính toán của chúng ta: amount là số lượng của mặt hàng, price là đơn giá của mặt hàng này. Ngoài 2 thuộc tính trên, lớp đối tượng Product còn có thêm thuộc tính label chứa tên của mặt hàng, và sku là mã đánh số duy nhất của mặt hàng trong kho. Lớp đối tượng Product được khai báo như sau:

public class Product implements Serializable {

    private String label;
    private String sku;
    private int amount;
    private double price;

    public Product() {}

    public Product(String label, String sku, int amount, double price) {
        this.label = label;
        this.sku = sku;
        this.amount = amount;
        this.price = price;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public String getSku() {
        return sku;
    }

    public void setSku(String sku) {
        this.sku = sku;
    }
}

Tất cả mặt hàng được lưu trong danh sách có tên products, bên trong lớp đối tượng StoreRepository. Trong thực tế, các mặt hàng được lưu trữ trong cơ sở dữ liệu và được nạp lên đối tượng Product khi cần. Tuy nhiên, trong ví dụ này chúng ta tạo sẵn danh sách products trong bộ nhớ ngay khi tạo đối tượng StoreRepository bằng cách tạo ra một số dữ liệu giả cố định ngay trong cấu tử của lớp đối tượng StoreRepository.

public class StoreRepository {

    public StoreRepository() {
        products.add(new Product("Asus Zenfone 5",
                "ASZF-105", 50, 200.0));
        products.add(new Product("Samsung Galaxy V",
                "SSGL-PV", 10, 110.0));
        products.add(new Product("Sony Xperia Z Ultra C6802",
                "SXZU-C6802", 100, 450.0));
        products.add(new Product("Sony Xperia Z Ultra C6802",
                "IPAD-MW16", 30, 350.0));
        products.add(new Product("HTC Desire 210 Dual SIM",
                "HTC-210", 10, 115.0));
    }

    private List<Product> products = new ArrayList<Product>();

    public List<Product> getProducts() {
        return products;
    }
}

Để tính tổng số hàng hóa và tổng lượng tiền hàng của cửa hàng, chúng ta tạo lớp đối tượng StoreService có 2 phương thức countProducts() và totalAssets(). Phương thức countProducts() duyệt từng phần tử danh sách products, cộng dồn giá trị thuộc tính amount trong từng phần tử. Tương tự, phương thức totalAssets() tính tổng lượng tiền hàng bằng cách cộng dồn tích (amount x price) của tất cả phần tử trong danh sách. Để có thể truy cập và duyệt danh sách, lớp đối tượng StoreService định nghĩa thuộc tính repository thuộc kiểu StoreRepository, tham chiếu đến đối tượng thuộc lớp StoreRepository. Việc tạo các đối tượng StoreService, StoreRepository cũng như gán nó cho biến repository của đối tượng StoreService sẽ được IoC Container thực hiện. Các phương thức countProducts() và totalAssets() sử dụng biến repository để truy cập đến danh sách products.

public class StoreService {

    private StoreRepository repository;

    public void setRepository(StoreRepository storeRepository) {
        this.repository = storeRepository;
    }

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int countProducts() {
        int total = 0;
        List<Product> products = repository.getProducts();
        for(Product product:products) {
            total += product.getAmount();
        }
        return total;
    }

    public double totalAssets() {
        double assets = 0;
        List<Product> products = repository.getProducts();
        for(Product product:products) {
            assets += product.getPrice() * product.getAmount();
        }
        return assets;
    }
}

Các lớp đối tượng Product, StoreRepository, StoreService được tạo ra nhằm để thử nghiệm Simple IoC Container, do đó chúng ta lưu các tệp mã nguồn trong thư mục mã nguồn dành cho kiểm thử: src/test/java.

Ngoài các lớp đối tượng trên, chúng ta còn tạo một tệp cấu hình có định dạng XML nhằm khai báo với Simple IoC Container thông tin về các đối tượng cần tạo (storeService, storeRepository) và mối quan hệ giữa chúng (thuộc tính repository của đối tượng storeService trỏ đến đối tượng storeRepository):

<beans>
    <bean id="storeService"
            class="net.acegik.simpleioccontainer.StoreService">
        <property name="name" value="Foo and Bar Company" />
        <property name="repository" ref="storeRepository" />
    </bean>
    <bean id="storeRepository"
            class="net.acegik.simpleioccontainer.StoreRepository" />
</beans>

Các lớp đối tượng của simple-ioc-container

Chức năng của Simple IoC Container là tạo và móc nối các đối tượng nghiệp vụ với nhau dựa trên mô tả trong tệp cấu hình XML.

Hình 2. Cấu trúc bên trong của Simple IoC Container

Để lưu trữ thông tin mô tả một đối tượng được simple-ioc-container quản lý, chúng ta cần định nghĩa 2 lớp đối tượng: BeanObject và BeanProperty.

Lớp đối tượng BeanObject chứa thông tin của đối tượng được Simple IoC Container tạo ra và quản lý (để thuận tiện chúng ta tạm gọi đối tượng này là managed-object), có các thuộc tính cơ bản sau:

  • id: chứa định danh (tên duy nhất) của đối tượng managed-object, cho phép Simple IoC Container tìm và trả lại đối tượng cho ứng dụng khi được gọi đến;
  • clazz: tên lớp đầy đủ của lớp đối tượng dùng để tạo ra đối tượng;
  • properties: danh sách các đối tượng BeanProperty, mỗi đối tượng BeanProperty mô tả một thuộc tính của đối tượng managed-object.
public class BeanObject {
    private String id;
    private String clazz;
    private List<BeanProperty> properties = new ArrayList<BeanProperty>();

    public String getClazz() {
        return clazz;
    }

    public void setClazz(String clazz) {
        this.clazz = clazz;
    }

    public String getId() {
        return id;
    }

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

    public List<BeanProperty> getProperties() {
        return properties;
    }

    public void setProperties(List<BeanProperty> properties) {
        this.properties = properties;
    }
}

Lớp đối tượng BeanProperty chứa thông tin mô tả của một thuộc tính của managed-object. Mỗi thuộc tính của managed-object bao gồm các thông tin sau:

  • tên thuộc tính: khai báo trong thuộc tính name của BeanProperty;
  • giá trị thuộc tính: cung cấp bởi thuộc tính value hoặc ref của BeanProperty. Nếu giá trị của thuộc tính thuộc kiểu giá trị thông thường (kiểu int, float, double, boolean, String,…) thì được chứa trong value; Nếu giá trị của thuộc tính là một đối tượng khác thì chúng ta sử dụng ref để chứa định danh của đối tượng được tham chiếu đó.
public class BeanProperty {

    private String name;
    private String ref;
    private String value;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRef() {
        return ref;
    }

    public void setRef(String ref) {
        this.ref = ref;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

Sau khi đã có các lớp đối tượng BeanObject và BeanProperty, chúng ta định nghĩa lớp đối tượng BeanXmlParser dùng để chuyển đổi tệp cấu hình XML thành danh sách các đối tượng BeanObject. Lưu ý ở đây chúng ta sử dụng SAX để phân tách nội dung XML. Lớp đối tượng BeanXmlParser được định nghĩa như sau:

public class BeanXmlParser {

    private String xmlFilename = null;

    public BeanXmlParser(String fn) {
        xmlFilename = fn;
    }

    public List<BeanObject> getAllBeanObjects() {
        try {
            SAXParserFactory parserFactor = SAXParserFactory.newInstance();
            SAXParser parser = parserFactor.newSAXParser();
            SAXHandler handler = new SAXHandler();
            parser.parse(ClassLoader.getSystemResourceAsStream(xmlFilename),
                    handler);
            return handler.getResult();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    class SAXHandler extends DefaultHandler {

        List<BeanObject> beanList = new ArrayList<BeanObject>();
        BeanObject bean = null;
        BeanProperty property = null;
        String content = null;

        @Override
        public void startElement(String uri, String localName,
                String qName, Attributes attributes) throws SAXException {

            if ("bean".equals(qName)) {
                bean = new BeanObject();
                bean.setId(attributes.getValue("id"));
                bean.setClazz(attributes.getValue("class"));
            } else if ("property".equals(qName)) {
                property = new BeanProperty();
                property.setName(attributes.getValue("name"));
                property.setRef(attributes.getValue("ref"));
                property.setValue(attributes.getValue("value"));
            }
        }

        @Override
        public void endElement(String uri, String localName,
                String qName) throws SAXException {
            if ("bean".equals(qName)) {
                beanList.add(bean);
            } else if ("property".equals(qName)) {
                bean.getProperties().add(property);
            }
        }

        @Override
        public void characters(char[] ch, int start, int length)
                throws SAXException {
            content = String.copyValueOf(ch, start, length).trim();
        }

        public List<BeanObject> getResult() {
            return beanList;
        }
    }
}

Sau khi đã có lớp đối tượng BeanXmlParser phân tách tệp cấu hình XML thành danh sách các đối tượng BeanObject, chúng ta xem xét cách thức simple-ioc-container tạo và trả lại các đối tượng managed-object. Nhiệm vụ này được thực hiện thông qua phương thức getBean() định nghĩa trong lớp đối tượng BeanFactory.

public class BeanFactory {

    private List<BeanObject> beanList;
    private Map<String, Object> beanMap = new HashMap<String, Object>();
    
    public BeanFactory(String fn) {
        BeanXmlParser parser = new BeanXmlParser(fn);
        this.beanList = parser.getAllBeanObjects();
    }

    public Object getBean(String name) {
        Object result = null;

        if (this.beanMap.containsKey(name)) {
            result = this.beanMap.get(name);
        } else {
            for (BeanObject bean:beanList) {
                if (name != null && name.equals(bean.getId())) {
                    result = createBean(bean);
                    this.beanMap.put(name, result);
                }
            }
        }

        return result;
    }

    private Object createBean(BeanObject beanInfo) {
        try {
            String className = beanInfo.getClazz();
            Object beanInstance = Class.forName(className).newInstance();

            List<BeanProperty> propertyList = beanInfo.getProperties();
            if (propertyList.size() > 0) {
                Class beanClass = beanInstance.getClass();

                Map methodMap = new HashMap();
                Method[] methods = beanClass.getMethods();
                for (int n = 0; n < methods.length; n++) {
                    String methodName = methods[n].getName();
                    if (methodName.startsWith("set")) {
                        methodName = Character.toLowerCase(methodName.charAt(3))
                                + methodName.substring(4);
                        methodMap.put(methodName, methods[n]);
                    }
                }

                for (BeanProperty propertyElement:propertyList) {
                    String pName = propertyElement.getName();
                    String pRef = propertyElement.getRef();
                    String pValue = propertyElement.getValue();

                    if (methodMap.containsKey(pName)) {
                        Method method = (Method) methodMap.get(pName);

                        Class[] parameterTypes = method.getParameterTypes();
                        String parameterName = parameterTypes[0].getCanonicalName();
                        Object[] params = new Object[1];
                        if (parameterName.equals("java.lang.String")) {
                            params[0] = pValue;
                        } else if (parameterName.equals("boolean")) {
                            params[0] = Boolean.parseBoolean(pValue);
                        } else if (parameterName.equals("int")) {
                            params[0] = new Integer(pValue);
                        } else if (parameterName.equals("float")) {
                            params[0] = Float.parseFloat(pValue);
                        } else if (parameterName.equals("double")) {
                            params[0] = Double.parseDouble(pValue);
                        } else {
                            if (pRef != null) {
                                params[0] = getBean(pRef);
                            }
                        }
                        method.invoke(beanInstance, params);
                    }
                }
            }
            return beanInstance;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Lớp đối tượng BeanFactory có một hàm cấu tử (constructor) với tham số đầu vào là đường dẫn đến tệp cấu hình XML. Hàm cấu tử của BeanFactory sử dụng BeanXmlParser để phân tách tệp XML thành danh sách BeanObject. Khi chương trình gọi phương thức getBean() với tham số là định danh của managed-object, nó sẽ kiểm tra xem đối tượng managed-object này đã tồn tại chưa? nếu không tìm thấy, nó sẽ dựa trên thông tin mô tả trong BeanObject để tạo managed-object. Cuối cùng, getBean() trả về cho chương trình đối tượng managed-object mà nó đã tạo và quản lý.

Chương trình minh họa

Cũng giống như các lớp đối tượng mô hình nghiệp vụ (Product, StoreRepository, StoreService), lớp đối tượng AppTest chứa chương trình thử nghiệm cũng được tạo ra trong thư mục test của dự án (thư mục src/test/java). Lớp đối tượng này chỉ có một phương thức tên là testApp() chứa đoạn chương trình minh họa. Bên trong phương thức testApp(), chúng ta tạo một đối tượng có kiểu BeanFactory tham chiếu bởi biến factory. Đối tượng này phân tách tệp config.xml (trong thư mục src/test/resources) và tạo danh sách các đối tượng BeanObject mô tả các đối tượng managed-object. Chương trình gọi phương thức getBean() để lấy đối tượng storeService và lưu vào biến có tên myService. Cuối cùng, thông qua biến myService, chúng ta có thể gọi các phương thức countProducts() và totalAssets() để thực hiện yêu cầu bài toán.

public class AppTest {

    @Test
    public void testApp() {
        BeanFactory factory = new BeanFactory("config.xml");
        StoreService myService = (StoreService) factory.getBean("storeService");

        System.out.println("=================================================");
        System.out.println("Store Name:" + myService.getName());
        System.out.println("Total number of products:" + myService.countProducts());
        System.out.println("Total Assets:" + myService.totalAssets());
    }
}

Chương trình chạy cho kết quả như sau:

=================================================
Store Name:Foo and Bar Company
Total number of products:200
Total Assets:67750.0

Các bạn có thể tải mã nguồn đầy đủ của ví dụ minh họa để chạy thử.

Comments