EzyFox Bean: Bean Creation

Updated at 1685686808000
The most difficult point in the EzyFox Bean is how to create beans, because bean dependencies is complicated between multi layers and sometimes we have classes depend on each other.

1. Sequence of Bean Creation

  1. Firstly, EzyFox Bean will load configuration classes that be annotated by EzyConfigurationBefore, you should create and register which singletons that other classes need to use.
  2. Secondly, EzyFox Bean will load configuration classes that be annotated by EzyConfiguration, if there is a configuration class depend on a singleton class, it will create that singleton.
  3. Thirdly, EzyFox will create beans that be annotated by EzySingleton, EzyPrototype and EzyPropertiesBean. If there is any missing dependent bean, EzyFox Bean will print warning logs (by default) or throw exception by your configuration.
  4. Finally, EzyFox Bean will load configuration classes that be annotated by EzyConfigurationAfter, you should use created beans and should not create any beans here.

2. Example

Let's we need create a web service to get data from a book service via API Gateway and provide Rest API to client. Firstly, to call to the API Gateway we need create a HTTP client with ezyhttp-client. Becase HttpClient is not annotated by EzySingleton annotation, so we need a singleton for it. Because it's not depending on any another beans, so we can use EzyConfigurationBefore for it:

    @Setter
    @EzyConfigurationBefore
    public class HttpClientConfiguration {
        @Property("http.connect.timeout")
        private int connectTimeout;
        @EzySingleton
        public HttpClient httpClient() {
            return HttpClient
                .builder()
                .connectTimeout(connectTimeout)
                .build();
        }
    }

The HttpClientConfiguration class will be loaded first and the singleton of HttpClient will be created first. Now, let's say we don't want to use HttpClient directly, we want to use an BookGatewayProxy like this:

    @AllArgsConstructor
    public class BookGatewayProxy {
        private final String gatewayUrl;
        private final HttpClient httpClient;
        public Book getBookById(long bookId) throws Exception {
            return httpClient.call(
                new GetRequest()
                    .setURL(gatewayUrl + "/api/v1/books" + bookId)
                    .setResponseType(Book.class)
            );
        }
    }

We can create singleton for the BookGatewayProxy with EzyConfiguration like this:

    @Setter
    @EzyConfiguration
    public class BookGatewayConfiguration {
        @Property("book.gateway.url")
        private String gatewayUrl;
        @EzyAutoBind
        private HttpClient httpClient;
        @EzySingleton
        public BookGatewayProxy bookGatewayProxy() {
            return new BookGatewayProxy(
                gatewayUrl,
                httpClient
            );
        }
    }

The class BookGatewayConfiguration will be created after the HttpClientConfiguration class, and the instance of BookGatewayProxy will be created after HttpClient. Now, we will create a service class named BookDataService call to BookGatewayProxy to get book data like this:

    @EzySingleton
    @AllArgsConstructor
    public class BookDataService {
        private final BookGatewayProxy bookGatewayProxy;
        public Book getBookById(long bookId) {
            try {
                return bookGatewayProxy.getBookById(bookId);
            } catch (Exception e) {
                throw new NotFoundException("book not found", e);
            }
        }
    }

The singleton of BookDataService will be created after the BookGatewayConfiguration configuration and BookGatewayProxy. Next step, we can create a controller named BookDataController to return Book data in json format like this:

    @EzySingleton
    @AllArgsConstructor
    public class BookDataController {
        private final BookDataService bookDataService;
        public String getBookData(long bookId) {
            return bookDataService
                .getBookById(bookId)
                .toJson();
        }
    }

The singleton of this class will be created after BookDataController. Now, let's say we want to cache some books to highlight them, we can create a configuration class named HighlightBookConfiguration and annotated it with EzyConfigurationAfter annotation like this:

    @EzyConfigurationAfter
    public class HighlightBookConfiguration implements EzyBeanConfig {
        @Setter
        @EzyAutoBind
        private BookDataService bookDataService;
        @Getter
        private final List highlightBooks = new ArrayList<>();
        @Override
        public void config() {
            highlightBooks.add(
                bookDataService.getBookById(1)
            );
        }
    }

This class will load after the all above class and EzyFox Bean will call config method automatically.

3. Circular dependency

Circular dependency happend when we have some classes depend on each other like above image. With circular dependency, EzyFox Bean will throw an exception. Example, if you update BookDataService like this:

    @EzySingleton
    @AllArgsConstructor
    public class BookDataService {
        private final BookGatewayProxy bookGatewayProxy;
        private final BookDataController bookDataController;
    }

And then, you run the application, you will get an exception like this:

Exception in thread "main" java.lang.IllegalStateException: can not create singleton of class class com.tvd12.ezyfox.example.bean.service.BookDataService
	at com.tvd12.ezyfox.bean.impl.EzySimpleSingletonLoader.load(EzySimpleSingletonLoader.java:53)
	at com.tvd12.ezyfox.bean.impl.EzySimpleBeanContext$Builder.createAndLoadSingleton(EzySimpleBeanContext.java:959)
	at com.tvd12.ezyfox.bean.impl.EzySimpleBeanContext$Builder.createAndLoadSingleton(EzySimpleBeanContext.java:946)
	at com.tvd12.ezyfox.bean.impl.EzySimpleBeanContext$Builder.addScannedSingletonsToFactory(EzySimpleBeanContext.java:935)
	at com.tvd12.ezyfox.bean.impl.EzySimpleBeanContext$Builder.build(EzySimpleBeanContext.java:853)
	at com.tvd12.ezyfox.bean.impl.EzySimpleBeanContext$Builder.build(EzySimpleBeanContext.java:207)
	at com.tvd12.ezyfox.example.bean.EzyFoxBeanExample.main(EzyFoxBeanExample.java:13)
Caused by: java.lang.IllegalStateException: circular dependency detected, class com.tvd12.ezyfox.example.bean.service.BookDataService => class com.tvd12.ezyfox.example.bean.http.BookGatewayProxy => class java.lang.String => class com.tvd12.ezyhttp.client.HttpClient => class com.tvd12.ezyhttp.client.HttpClient$Builder => class com.tvd12.ezyfox.example.bean.controller.BookDataController => class com.tvd12.ezyfox.example.bean.service.BookDataService
	at com.tvd12.ezyfox.bean.impl.EzySimpleSingletonLoader.detectCircularDependency(EzySimpleSingletonLoader.java:251)

So, to avoid the circular dependency make your application broken, please organize your source in multi layers and don't add dependency from higer layer to lower layer.