background

In daily writing some small tools or small projects, there is a need for dependency management and dependency injection, but Spring(Boot) system as a DI framework is too heavy, so we need to investigate a mini DI framework. Guice is a lightweight dependency injection framework from Google that helps solve dependency injection problems in projects and improves maintainability and flexibility. The Guice project has a core module of less than 1MB compared to the heavyweight Spring(Boot) architecture, so if the core requirement is DI (and Guice also provides a low-level AOP implementation), Guice should be a good candidate.

When I was looking for Guice related materials, I saw a lot of introduction articles teasing Guice for being too simple. The interface and link relationship of implementation need to be registered in Module, which seemed very simple. The reason: Guice is an extremely lean DI implementation that does not provide Class scanning and automatic registration. The following sections provide some ideas for implementing automatic Bean scanning schemes under ClassPath

Dependency introduction and getting started examples

The Guice extension library has been integrated since the 5.x release, and currently only one dependency is required to use all of its functionality. The dependencies in the current (around 2022-02) version are:

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>5.1.0</version>
</dependency>
Copy the code

Here’s a primer:

public class GuiceDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new DemoModule());
        Greeter first = injector.getInstance(Greeter.class);
        Greeter second = injector.getInstance(Greeter.class);
        System.out.printf("first hashcode => %s\n", first.hashCode());
        first.sayHello();
        System.out.printf("second hashcode => %s\n", second.hashCode());
        second.sayHello();
    }

    @Retention(RUNTIME)
    public @interface Count {

    }

    @Retention(RUNTIME)
    public @interface Message {

    }

    @Singleton
    public static class Greeter {

        private final String message;

        private final Integer count;

        @Inject
        public Greeter(@Message String message,
                       @Count Integer count) {
            this.message = message;
            this.count = count;
        }

        public void sayHello(a) {
            for (int i = 1; i <= count; i++) {
                System.out.printf("%s,count => %d\n", message, i); }}}public static class DemoModule extends AbstractModule {

        @Override
        public void configure(a) {
// bind(Greeter.class).in(Scopes.SINGLETON);
        }

        @Provides
        @Count
        public static Integer count(a) {
            return 2;
        }

        @Provides
        @Count
        public static String message(a) {
            return "vlts.cn"; }}}Copy the code

Console output from executing the main method:

first hashcode => 914507705
vlts.cn,count => 1
vlts.cn,count => 2
second hashcode => 914507705
vlts.cn,count => 1
vlts.cn,count => 2
Copy the code

Greeter classes need to be registered as singletons. Instances registered in Guice that are not explicitly specified as singletons default to prototypes. Guice registers a singleton in three main ways:

  • Method 1: Use annotations in classes@Singleton(using theInjector#getInstance()Lazy singleton loading)
@Singleton
public static class Greeter {... }Copy the code
  • Method 2: Specify it explicitly when registering bindingsScopeforScopes.SINGLETON
public static class DemoModule extends AbstractModule {

    @Override
    public void configure(a) {
        bind(Greeter.class).in(Scopes.SINGLETON);
        // If Greeter is already annotated @singleton, you don't need to specify in(Scopes. Singleton), just bind(greeter.class)}}Copy the code
  • Method 3: Use annotations in combination@Providesand@SingletonThe effect is similar toSpringIn the@Beanannotations
public static class SecondModule extends AbstractModule {

    @Override
    public void configure(a) {
        // config module
    }

    @Provides
    @Singleton
    public Foo foo(a) {
        return newFoo(); }}public static class Foo {}Copy the code

In the example above, if the Greeter class does not use @singleton, comment bind(greeter.class).in(Scopes. Singleton); If the main method is used, the hashCode of the instance retrieved from the injector is not the same as that of the instance retrieved from the injector.

All singletons in Guice are lazily loaded by default, which means the singletons are initialized in lazy mode. You can mark the singletons for hungry mode by ScopedBindingBuilder#asEagerSingleton(), which means you can switch the singletons to hungry mode.

The Guice injector is initialized

Guice Injector interface Injector is its core API, analogous to Spring’s BeanFactory. Injector initialization depends on the implementation of one or more modules (com.google.inject.Module). An example of initializing an Injector is as follows:

public class GuiceInjectorDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(
                new FirstModule(),
                new SecondModule()
        );
        // injector.getInstance(Foo.class);
    }

    public static class FirstModule extends AbstractModule {

        @Override
        public void configure(a) {
            // config module}}public static class SecondModule extends AbstractModule {

        @Override
        public void configure(a) {
            // config module}}}Copy the code

Injector supports creating a child Injector instance based on the current instance, similar to the parent-child IOC container in Spring:

public class GuiceChildInjectorDemo {

    public static void main(String[] args) throws Exception {
        Injector parent = Guice.createInjector(
                new FirstModule()
        );
        Injector childInjector = parent.createChildInjector(new SecondModule());
    }

    public static class FirstModule extends AbstractModule {

        @Override
        public void configure(a) {
            // config module}}public static class SecondModule extends AbstractModule {

        @Override
        public void configure(a) {
            // config module}}}Copy the code

The child Injector instance inherits all the state of the parent Injector instance (all bindings, scopes, interceptors, converters, etc.).

Guice mental model

The concept of Mental Model comes from cognitive psychology. Mental Model refers to an inertial psychological mechanism or established cognitive framework that cognitive subjects use concepts to judge and classify their own experience

Guice is cognitively understood as a map (represented in the documentation as a map[^guice-map]) through which application code can declare and retrieve dependent components within the application. Each map. Entry in this Guice Map has two parts:

  • Guice Key:Guice MapKey in to get themapThe specified value in
  • Provider:Guice MapIs used to create (component) objects to be applied within the application

The abstract Guice Map looks something like this:

// com.google.inject.Key => com.google.inject.Provider
private finalConcurrentMap<Key<? >, Provider<? >> guiceMap =new ConcurrentHashMap<>();
Copy the code

Guice Key identifies a dependent component in a Guice Map. This Key is globally unique and is defined by com.google.inject.Key. Since there are no parameters in Java (i.e., a method’s entry list or return value has only the order and type, but no name), there are many times when Guice keys need to be built depending on the type of the component and cannot be uniquely determined. An additional custom Annotation is required to generate a unique identification Type + Annotation(Type) for the composition. Such as:

  • @Message StringThe equivalent ofKey<String>
  • @Count intThe equivalent ofKey<Integer>
public class GuiceMentalModelDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new EchoModule());
        EchoService echoService = injector.getInstance(EchoService.class);
    }

    @Qualifier
    @Retention(RUNTIME)
    public @interface Count {

    }

    @Qualifier
    @Retention(RUNTIME)
    public @interface Message {

    }

    public static class EchoModule extends AbstractModule {

        @Override
        public void configure(a) {
            bind(EchoService.class).in(Scopes.SINGLETON);
        }

        @Provides
        @Message
        public String messageProvider(a) {
            return "foo";
        }

        @Provides
        @Count
        public Integer countProvider(a) {
            return 10087; }}public static class EchoService {

        private final String messageValue;

        private final Integer countValue;

        @Inject
        public EchoService(@Message String messageValue, @Count Integer countValue) {
            this.messageValue = messageValue;
            this.countValue = countValue; }}}Copy the code

The processing logic for the Guice injector to create a singleton is similar to:

String messageValue = injector.getInstance(Key.get(String.class, Message.class));
Integer countValue = injector.getInstance(Key.get(Integer.class, Count.class));
EchoService echoService = new EchoService(messageValue, countValue);
Copy the code

The @provides annotation implemented in Guice corresponds to the Provider interface, which is defined very simply:

interface Provider<T> {

    /** Provides an instance of T.**/
    T get(a);
}
Copy the code

All values in a Guice Map can be interpreted as a Provider implementation, such as the above example:

// messageProvider.get() => 'foo'
Provider<String> messageProvider = () -> EchoModule.messageProvider();
// countProvider.get() => 10087
Provider<Integer> countProvider = () -> EchoModule.countProvider();
Copy the code

The process of dependency search and creation is also to create Key instances based on criteria, locate the unique Provider in the Guice Map, and then instantiate the dependent component through that Provider, followed by subsequent dependency injection actions. This process is illustrated in the Guice documentation using a specific table, which is posted here:

Guice DSLgrammar Corresponding model
bind(key).toInstance(value) instance bindingmap.put(key,() -> value)
bind(key).toProvider(provider) provider bindingmap.put(key, provider)
bind(key).to(anotherKey) linked bindingmap.put(key, map.get(anotherKey))
@Provides Foo provideFoo(){... } provider method bindingmap.put(Key.get(Foo.class), module::provideFoo)

There are many derivative methods for creating Key instances, which can meet the requirements of single concrete type, concrete type with annotations and other instantiation methods. Dependency injection uses the @inject annotation to support member variable and construct injection. Scenarios with multiple implementations of an interface can specify the implementation of the injection through the @named annotation or custom annotation, but need to mark the implementation with the @named annotation or custom annotation when building the binding. Such as:

public class GuiceMentalModelDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {
                bind(MessageProcessor.class)
                        .annotatedWith(Names.named("firstMessageProcessor"))
                        .to(FirstMessageProcessor.class)
                        .in(Scopes.SINGLETON);
                bind(MessageProcessor.class)
                        .annotatedWith(Names.named("secondMessageProcessor")) .to(SecondMessageProcessor.class) .in(Scopes.SINGLETON); }}); MessageClient messageClient = injector.getInstance(MessageClient.class); messageClient.invoke("hello world");
    }

    interface MessageProcessor {

        void process(String message);
    }

    public static class FirstMessageProcessor implements MessageProcessor {

        @Override
        public void process(String message) {
            System.out.println("FirstMessageProcessor process message => "+ message); }}public static class SecondMessageProcessor implements MessageProcessor {

        @Override
        public void process(String message) {
            System.out.println("SecondMessageProcessor process message => "+ message); }}@Singleton
    public static class MessageClient {

        @Inject
        @Named("secondMessageProcessor")
        private MessageProcessor messageProcessor;

        public void invoke(String message) { messageProcessor.process(message); }}}// Console output: SecondMessageProcessor Process Message => Hello World
Copy the code

The @named annotation can be implemented as any custom annotation, but note that the custom annotation requires the addition of the meta annotation @javax.inject.Qualifier. The final effect is consistent, and the built-in @named annotation will satisfy most scenarios. Finally, each component is registered with Guice, and all the dependencies of that component form a directed graph. When the component is injected, all the dependencies of the component itself are injected recursively. This traversal injection process follows depth-first. Guice verifies the validity of a component’s dependent directed graph and throws a CreationException if the graph is illegal.

Guice supported bindings

Guice provides AbstractModule AbstractModule classes for consumers to inherit, overriding configure() methods and creating bindings through the bind() related API.

Binding in Guice is the mapping between keys and values in the Guice Map of Mental Model. Guice provides various apis for registering this Binding

Only the most common binding types are described here:

  • Linked Binding
  • Instance Binding
  • Provider Binding
  • Constructor Binding
  • Untargeted Binding
  • Multi Binding
  • JIT Binding

Linked Binding

Linked Binding is used to map a type to its implementation type and is used as follows:

Bind (interface type.class).to(implementation type.class);Copy the code

Specific examples:

public class GuiceLinkedBindingDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) { bind(Foo.class).to(Bar.class).in(Scopes.SINGLETON); }}); Foo foo = injector.getInstance(Foo.class); }interface Foo {}public static class Bar implements Foo {}}Copy the code

Linked Binding is often used in this one interface one implementation scenario. The @Singleton annotation is added to the target type so that the binding can be programmatically registered without calling in(Scopes. Singleton).

Instance Binding

Instance Binding is used to map a type to an Instance of its implementation type, including Binding of constants. The example in the previous section is slightly modified to the Instance Binding mode as follows:

final Bar bar = newBar(); bind(Foo.class).toInstance(bar); Bind (foo.class).annotatedwith (names.named ("bar")).toInstance(bar); # constant bind bindConstant().annotatedwith (names.named ("key")).to(value);
Copy the code

Constants can be bound in this way, for example:

public class GuiceInstanceBindingDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {
                bind(String.class).annotatedWith(Names.named("host")).toInstance("localhost");
                bind(Integer.class).annotatedWith(Names.named("port")).toInstance(8080);
                bindConstant().annotatedWith(Protocol.class).to("HTTPS"); bind(HttpClient.class).to(DefaultHttpClient.class).in(Scopes.SINGLETON); }}); HttpClient httpClient = injector.getInstance(HttpClient.class); httpClient.print(); }@Qualifier
    @Retention(RUNTIME)
    public @interface Protocol {

    }

    interface HttpClient {

        void print(a);
    }

    public static class DefaultHttpClient implements HttpClient {

        @Inject
        @Named("host")
        private String host;

        @Inject
        @Named("port")
        private Integer port;

        @Inject
        @Protocol
        private String protocol;

        @Override
        public void print(a) {
            System.out.printf("host => %s, port => %d, protocol => %s\n", host, port, protocol); }}}// Host => localhost, port => 8080, protocol => HTTPS
Copy the code

Provider Binding

Provider Binding, which specifies that a type is bound to the Provider implementation type of that type, is a bit like a simple factory pattern in design mode, analogous to the FactoryBean interface in Spring. Here’s an example:

public class GuiceProviderBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) { bind(Key.get(Foo.class)).toProvider(FooProvider.class).in(Scopes.SINGLETON); }}); Foo s1 = injector.getInstance(Key.get(Foo.class)); Foo s2 = injector.getInstance(Key.get(Foo.class)); }public static class Foo {}public static class FooProvider implements Provider<Foo> {

        private final Foo foo = new Foo();

        @Override
        public Foo get(a) {
            System.out.println("Get Foo from FooProvider...");
            returnfoo; }}}// Get Foo from FooProvider...
Copy the code

Note also here that if the marked Provider is a singleton, then the get() method will be called once when the Injector gets the instance created, which is lazy loading

The @provides annotation is a Provider Binding that allows you to add a method to your custom Module implementation that returns an instance of the corresponding type using the @Provides annotation, much like the @Bean annotation in Spring. Here’s an example:

public class GuiceAnnotationProviderBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {}@Singleton
            @Provides
            public Foo fooProvider(a) {
                System.out.println("init Foo from method fooProvider()...");
                return newFoo(); }}); Foo s1 = injector.getInstance(Key.get(Foo.class)); Foo s2 = injector.getInstance(Key.get(Foo.class)); }public static class Foo {}}// init Foo from method fooProvider()...
Copy the code

Constructor Binding

Constructor Binding A Constructor that needs to explicitly bind a type to an explicit input type of its implementation type. The target Constructor does not need the @Inject annotation. Such as:

public class GuiceConstructorBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {
                try {
                    bind(Key.get(JdbcTemplate.class))
                            .toConstructor(DefaultJdbcTemplate.class.getConstructor(DataSource.class))
                            .in(Scopes.SINGLETON);
                } catch(NoSuchMethodException e) { addError(e); }}}); JdbcTemplate instance = injector.getInstance(JdbcTemplate.class); }interface JdbcTemplate {}public static class DefaultJdbcTemplate implements JdbcTemplate {

        public DefaultJdbcTemplate(DataSource dataSource) {
            System.out.println("init JdbcTemplate,ds => "+ dataSource.hashCode()); }}public static class DataSource {}}// init JdbcTemplate,ds => 1420232606
Copy the code

The consumer is required to catch and handle the NoSuchMethodException thrown when the constructor fails to get.

Untargeted Binding

Untargeted Binding is used to register specialized scenarios where the Binding has no target (implementation) type, which is generally a common type that does not implement an interface. The to() call can be ignored unless the @named annotation or custom annotation Binding is used. But if you bind with @named or custom annotations, the to() call must not be ignored. Such as:

public class GuiceUnTargetedBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {
                bind(Foo.class).in(Scopes.SINGLETON);
                bind(Bar.class).annotatedWith(Names.named("bar")).to(Bar.class).in(Scopes.SINGLETON); }}); }public static class Foo {}public static class Bar {}}Copy the code

Multi Binding

Multi Binding Multi Binding is a multi-instance Binding with specialized Binder agents:

  • MultibinderCan be simply understood asType => Set<TypeImpl>, the injection type isSet<Type>
  • MapBinderCan be simply understood as(KeyType, ValueType) => Map<KeyType, ValueTypeImpl>, the injection type isMap<KeyType, ValueType>
  • OptionalBinderCan be simply understood asType => Optional.ofNullable(GuiceMap.get(Type)).or(DefaultImpl), the injection type isOptional<Type>

Examples of Multibinder use:

public class GuiceMultiBinderDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) { Multibinder<Processor> multiBinder = Multibinder.newSetBinder(binder(), Processor.class); multiBinder.permitDuplicates().addBinding().to(FirstProcessor.class).in(Scopes.SINGLETON); multiBinder.permitDuplicates().addBinding().to(SecondProcessor.class).in(Scopes.SINGLETON); }}); injector.getInstance(Client.class).process(); }@Singleton
    public static class Client {

        @Inject
        private Set<Processor> processors;

        public void process(a) { Optional.ofNullable(processors).ifPresent(ps -> ps.forEach(Processor::process)); }}interface Processor {

        void process(a);
    }

    public static class FirstProcessor implements Processor {

        @Override
        public void process(a) {
            System.out.println("FirstProcessor process..."); }}public static class SecondProcessor implements Processor {

        @Override
        public void process(a) {
            System.out.println("SecondProcessor process..."); }}}// Output the result
FirstProcessor process...
SecondProcessor process...
Copy the code

Examples of MapBinder use:

public class GuiceMapBinderDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) { MapBinder<Type, Processor> mapBinder = MapBinder.newMapBinder(binder(), Type.class, Processor.class); mapBinder.addBinding(Type.SMS).to(SmsProcessor.class).in(Scopes.SINGLETON); mapBinder.addBinding(Type.MESSAGE_TEMPLATE).to(MessageTemplateProcessor.class).in(Scopes.SINGLETON); }}); injector.getInstance(Client.class).process(); }@Singleton
    public static class Client {

        @Inject
        private Map<Type, Processor> processors;

        public void process(a) { Optional.ofNullable(processors).ifPresent(ps -> ps.forEach(((type, processor) -> processor.process()))); }}public enum Type {

        /** * SMS */
        SMS,

        /** * Message template */
        MESSAGE_TEMPLATE
    }

    interface Processor {

        void process(a);
    }

    public static class SmsProcessor implements Processor {

        @Override
        public void process(a) {
            System.out.println("SmsProcessor process..."); }}public static class MessageTemplateProcessor implements Processor {

        @Override
        public void process(a) {
            System.out.println("MessageTemplateProcessor process..."); }}}// Output the result
SmsProcessor process...
MessageTemplateProcessor process...
Copy the code

OptionalBinder examples:

public class GuiceOptionalBinderDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {
// bind(Logger.class).to(LogbackLogger.class).in(Scopes.SINGLETON);OptionalBinder.newOptionalBinder(binder(), Logger.class) .setDefault() .to(StdLogger.class) .in(Scopes.SINGLETON); }}); injector.getInstance(Client.class).log("Hello World");
    }

    @Singleton
    public static class Client {

        @Inject
        private Optional<Logger> logger;

        public void log(String content) { logger.ifPresent(l -> l.log(content)); }}interface Logger {

        void log(String content);
    }

    public static class StdLogger implements Logger {

        @Override
        public void log(String content) { System.out.println(content); }}}Copy the code

JIT Binding

JIT Binding is also known as just-in-time Binding and can also be called Implicit Binding. Implicit binding needs to satisfy:

  • The constructor must have no parameters and is notprivatemodified
  • Not in theModuleActivation in implementationBinder#requireAtInjectRequired()

Calling the Binder#requireAtInjectRequired() method forces Guice to use only the constructor annotated with @inject. Calling the Binder#requireExplicitBindings() method states that all bindings must be explicitly declared within the Module. Implicit bindings are disabled. All bindings must be declared in the implementation of the Module. Here is an example of an implicit binding:

public class GuiceJustInTimeBindingDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {}}); Foo instance = injector.getInstance(Key.get(Foo.class)); }public static class Foo {

        public Foo(a) {
            System.out.println("init Foo..."); }}}// init Foo...
Copy the code

There are also two runtime binding annotations:

  • @ImplementedBy: specializedLinked BindingFor the runtime binding of the corresponding target type
@ImplementedBy(MessageProcessor.class)
public interface Processor {}Copy the code
  • @ProvidedBy: specializedProvider BindingFor the runtime binding of the corresponding target typeProviderimplementation
@ProvidedBy(DruidDataSource.class)
public interface DataSource {}Copy the code

AOP features

Guice provides relative to the underlying characteristics of AOP, users need to implement org. Aopalliance. Intercept. The MethodInterceptor interface insert custom code before and after the point of execution of the method, And register the method interceptor with Binder#bindInterceptor(). Here is a simple example, simulating a scenario where logs are printed before and after method execution, and the target method call time is calculated:

public class GuiceAopDemo {

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {
                bindInterceptor(Matchers.only(EchoService.class), Matchers.any(), newEchoMethodInterceptor()); }}); EchoService instance = injector.getInstance(Key.get(EchoService.class)); instance.echo("throwable");
    }

    public static class EchoService {

        public void echo(String name) {
            System.out.println(name + " echo"); }}public static class EchoMethodInterceptor implements MethodInterceptor {

        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            Method method = methodInvocation.getMethod();
            String methodName = method.getName();
            long start = System.nanoTime();
            System.out.printf("Before invoke method => [%s]\n", methodName);
            Object result = methodInvocation.proceed();
            long end = System.nanoTime();
            System.out.printf("After invoke method => [%s], cost => %d ns\n", methodName, (end - start));
            returnresult; }}}// Output the result
Before invoke method => [echo]
throwable echo
After invoke method => [echo], cost => 16013700 ns
Copy the code

Custom injection

TypeListener and MembersInjector allow custom injection extensions for member attributes of target type instances. For example, the org.slf4j.Logger attribute of the target instance can be automatically injected as follows:

public class GuiceCustomInjectionDemo {

    public static void main(String[] args) throws Exception {
        Injector injector = Guice.createInjector(new AbstractModule() {
            @Override
            public void configure(a) {
                bindListener(Matchers.any(), newLoggingListener()); }}); injector.getInstance(LoggingClient.class).doLogging("Hello World");
    }

    public static class LoggingClient {

        @Logging
        private Logger logger;

        public void doLogging(String content) { Optional.ofNullable(logger).ifPresent(l -> l.info(content)); }}@Qualifier
    @Retention(RUNTIME)
    @interface Logging {

    }

    public static class LoggingMembersInjector<T> implements MembersInjector<T> {

        private final Field field;
        private final Logger logger;

        public LoggingMembersInjector(Field field) {
            this.field = field;
            this.logger = LoggerFactory.getLogger(field.getDeclaringClass());
            field.setAccessible(true);
        }

        @Override
        public void injectMembers(T instance) {
            try {
                field.set(instance, logger);
            } catch (IllegalAccessException e) {
                throw new IllegalStateException(e);
            } finally {
                field.setAccessible(false); }}}public static class LoggingListener implements TypeListener {

        @Override
        public <I> void hear(TypeLiteral<I> typeLiteral, TypeEncounter<I> typeEncounter) { Class<? > clazz = typeLiteral.getRawType();while (Objects.nonNull(clazz)) {
                for (Field field : clazz.getDeclaredFields()) {
                    if (field.getType() == Logger.class && field.isAnnotationPresent(Logging.class)) {
                        typeEncounter.register(newLoggingMembersInjector<>(field)); } } clazz = clazz.getSuperclass(); }}}}// Output the result
[2022-02-22 00:51:33.516] [INFO] cn.vlts.guice.GuiceCustomInjectionDemo$LoggingClient [main] [] - Hello World
Copy the code

This example requires the introduction of logback and SLf4J-API dependencies.

ClassGraph based scanning and automatic registration binding

Guice itself does not provide the class path or Jar file scanning function, to realize all classpath Bean automatic registration binding, need to rely on third-party framework class scan, chose a higher performance community here more active class library IO. Making. Classgraph: classgraph. New dependencies introduced to ClassGraph:

<dependency>
    <groupId>io.github.classgraph</groupId>
    <artifactId>classgraph</artifactId>
    <version>4.8.138</version>
</dependency>
Copy the code

Write automatic scan Module:

@RequiredArgsConstructor
public class GuiceAutoScanModule extends AbstractModule {

    private finalSet<Class<? >> bindClasses =new HashSet<>();

    private final String[] acceptPackages;

    private final String[] rejectClasses;

    @Override
    public void configure(a) {
        ClassGraph classGraph = new ClassGraph();
        ScanResult scanResult = classGraph
                .enableClassInfo()
                .acceptPackages(acceptPackages)
                .rejectClasses(rejectClasses)
                .scan();
        ClassInfoList allInterfaces = scanResult.getAllInterfaces();
        for (ClassInfo i : allInterfaces) {
            ClassInfoList impl = scanResult.getClassesImplementing(i.getName());
            if(Objects.nonNull(impl)) { Class<? > ic = i.loadClass();int size = impl.size();
                if (size > 1) {
                    for(ClassInfo im : impl) { Class<? > implClass = im.loadClass();if (isSingleton(implClass)) {
                            String simpleName = im.getSimpleName();
                            String name = Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1); bindNamedSingleInterface(ic, name, implClass); }}}else {
                    for(ClassInfo im : impl) { Class<? > implClass = im.loadClass();if (isProvider(implClass)) {
                            bindProvider(ic, implClass);
                        }
                        if (isSingleton(implClass)) {
                            bindSingleInterface(ic, implClass);
                        }
                    }
                }
            }
        }
        ClassInfoList standardClasses = scanResult.getAllStandardClasses();
        for(ClassInfo ci : standardClasses) { Class<? > implClass = ci.loadClass();if(! bindClasses.contains(implClass) && shouldBindSingleton(implClass)) { bindSingleton(implClass); } } bindClasses.clear(); ScanResult.closeAll(); }private boolean shouldBindSingleton(Class
        implClass) {
        int modifiers = implClass.getModifiers();
        returnisSingleton(implClass) && ! Modifier.isAbstract(modifiers) && ! implClass.isEnum(); }private void bindSingleton(Class
        implClass) {
        bindClasses.add(implClass);
        bind(implClass).in(Scopes.SINGLETON);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void bindSingleInterface(Class
        ic, Class
        implClass) {
        bindClasses.add(implClass);
        bind((Class) ic).to(implClass).in(Scopes.SINGLETON);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void bindNamedSingleInterface(Class
        ic, String name, Class
        implClass) {
        bindClasses.add(implClass);
        bind((Class) ic).annotatedWith(Names.named(name)).to(implClass).in(Scopes.SINGLETON);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private <T> void bindProvider(Class
        ic, Class
        provider) {
        bindClasses.add(provider);
        Type type = ic.getGenericInterfaces()[0];
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Class target = (Class) parameterizedType.getActualTypeArguments()[0];
        bind(target).toProvider(provider).in(Scopes.SINGLETON);
    }

    private boolean isSingleton(Class
        implClass) {
        return Objects.nonNull(implClass) && implClass.isAnnotationPresent(Singleton.class);
    }

    private boolean isProvider(Class
        implClass) {
        returnisSingleton(implClass) && Provider.class.isAssignableFrom(implClass); }}Copy the code

Usage:

GuiceAutoScanModule module = new GuiceAutoScanModule(new String[]{"cn.vlts"}, new String[]{"*Demo"."*Test"});
Injector injector = Guice.createInjector(module);
Copy the code

GuiceAutoScanModule is currently only an incomplete example. It is used to scan all classes in the CN. VLTS package (excluding classes whose class names end in Demo or Test) and bind and register them according to different situations.

summary

Limited by space, this paper only introduces the basic usage, design concept and different types of binding registration of Guice, and then discusses the application of Guice in detail based on cases when more in-depth practical schemes are applied in projects later. In addition, Guice is not an outdated component, as compared to SpringBoot, which is a Flat Jar with a minimum build of tens of megabytes, Guice is a good choice if you only want lightweight DI functionality.

References:

  • Guice Wiki

(C-4-D E-A-20220221)