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 bindings
Scope
forScopes.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
@Provides
and@Singleton
The effect is similar toSpring
In the@Bean
annotations
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 Map
Key in to get themap
The specified value inProvider
:Guice Map
Is 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 String
The equivalent ofKey<String>
@Count int
The 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 DSL grammar |
Corresponding model |
---|---|
bind(key).toInstance(value) |
【instance binding 】map.put(key,() -> value) |
bind(key).toProvider(provider) |
【provider binding 】map.put(key, provider) |
bind(key).to(anotherKey) |
【linked binding 】map.put(key, map.get(anotherKey)) |
@Provides Foo provideFoo(){... } |
【provider method binding 】map.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:
Multibinder
Can be simply understood asType => Set<TypeImpl>
, the injection type isSet<Type>
MapBinder
Can be simply understood as(KeyType, ValueType) => Map<KeyType, ValueTypeImpl>
, the injection type isMap<KeyType, ValueType>
OptionalBinder
Can 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 not
private
modified - Not in the
Module
Activation 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 Binding
For the runtime binding of the corresponding target type
@ImplementedBy(MessageProcessor.class)
public interface Processor {}Copy the code
@ProvidedBy
: specializedProvider Binding
For the runtime binding of the corresponding target typeProvider
implementation
@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)