背景
在日常寫一些小工具或者小項目的時候,有依賴管理和依賴注入的需求,但是Spring(Boot)
體系作為DI
框架過於重量級,於是需要調研一款微型的DI
框架。Guice
是Google
出品的一款輕量級的依賴注入框架,使用它有助於解決項目中的依賴注入問題,提高了可維護性和靈活性。相對於重量級的Spring(Boot)
體系,Guice
項目只有一個小於1MB
的核心模塊,如果核心需求是DI
(其實Guice
也提供了很低層次的AOP
實現),那么Guice
應該會是一個合適的候選方案。
在查找Guice相關資料的時候,見到不少介紹文章吐槽Guice過於簡陋,需要在Module中注冊接口和實現的鏈接關系,顯得十分簡陋。原因是:Guice是極度精簡的DI實現,沒有提供Class掃描和自動注冊的功能。下文會提供一些思路去實現ClassPath下的Bean自動掃描方案
依賴引入與入門示例
Guice
在5.x
版本后整合了低版本的擴展類庫,目前使用其所有功能只需要引入一個依賴即可,當前(2022-02
前后)最新版本依賴為:
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>5.1.0</version>
</dependency>
一個入門例子如下:
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() {
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() {
// bind(Greeter.class).in(Scopes.SINGLETON);
}
@Provides
@Count
public static Integer count() {
return 2;
}
@Provides
@Count
public static String message() {
return "vlts.cn";
}
}
}
執行main
方法控制台輸出:
first hashcode => 914507705
vlts.cn,count => 1
vlts.cn,count => 2
second hashcode => 914507705
vlts.cn,count => 1
vlts.cn,count => 2
Greeter
類需要注冊為單例,Guice
中注冊的實例如果不顯式指定為單例,默認都是原型(Prototype
,每次重新構造一個新的實例)。Guice
注冊一個單例目前來看主要有三種方式:
- 方式一:在類中使用注解
@Singleton
(使用Injector#getInstance()
會懶加載單例)
@Singleton
public static class Greeter {
......
}
- 方式二:注冊綁定關系的時候顯式指定
Scope
為Scopes.SINGLETON
public static class DemoModule extends AbstractModule {
@Override
public void configure() {
bind(Greeter.class).in(Scopes.SINGLETON);
// 如果Greeter已經使用了注解@Singleton可以無需指定in(Scopes.SINGLETON),僅bind(Greeter.class)即可
}
}
- 方式三:組合使用注解
@Provides
和@Singleton
,效果類似於Spring
中的@Bean
注解
public static class SecondModule extends AbstractModule {
@Override
public void configure() {
// config module
}
@Provides
@Singleton
public Foo foo() {
return new Foo();
}
}
public static class Foo {
}
上面的例子中,如果Greeter
類不使用@Singleton
,同時注釋掉bind(Greeter.class).in(Scopes.SINGLETON);
,那么執行main
方法會發現兩次從注入器中獲取到的實例的hashCode
不一致,也就是兩次從注入器中獲取到的都是重新創建的實例(hashCode
不相同):
Guice
中所有單例默認是懶加載的,理解為單例初始化使用了懶漢模式,可以通過ScopedBindingBuilder#asEagerSingleton()
標記單例為飢餓加載模式,可以理解為切換單例加載模式為餓漢模式。
Guice注入器初始化
Guice
注入器接口Injector
是其核心API
,類比為Spring
中的BeanFactory
。Injector
初始化依賴於一或多個模塊(com.google.inject.Module
)的實現。初始化Injector
的示例如下:
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() {
// config module
}
}
public static class SecondModule extends AbstractModule {
@Override
public void configure() {
// config module
}
}
}
Injector
支持基於當前實例創建子Injector
實例,類比於Spring
中的父子IOC
容器:
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() {
// config module
}
}
public static class SecondModule extends AbstractModule {
@Override
public void configure() {
// config module
}
}
}
子Injector
實例會繼承父Injector
實例的所有狀態(所有綁定、Scope
、攔截器和轉換器等)。
Guice心智模型
心智模型(Mental Model)的概念來自於認知心理學,心智模型指的是指認知主體運用概念對自身體驗進行判斷與分類的一種慣性化的心理機制或既定的認知框架
Guice
在認知上可以理解為一個map
(文檔中表示為map[^guice-map]
),應用程序代碼可以通過這個map
聲明和獲取應用程序內的依賴組件。這個Guice Map
每一個Map.Entry
有兩個部分:
Guice Key
:Guice Map
中的鍵,用於獲取該map
中特定的值Provider
:Guice Map
中的值,用於創建應用於應用程序內的(組件)對象
這個抽象的Guice Map
有點像下面這樣的結構:
// com.google.inject.Key => com.google.inject.Provider
private final ConcurrentMap<Key<?>, Provider<?>> guiceMap = new ConcurrentHashMap<>();
Guice Key
用於標識Guice Map
中的一個依賴組件,這個鍵是全局唯一的,由com.google.inject.Key
定義。鑒於Java
里面沒有形參(也就是方法的入參列表或者返回值只有順序和類型,沒有名稱),所以很多時候在構建Guice Key
的時候既需要依賴組件的類型,無法唯一確定組件類型的時候(例如一些定義常量的場景,只要滿足常量的場景,對於類實例也是可行的),需要額外增加一個自定義注解用於生成組合的唯一標識Type + Annotation(Type)
。例如:
@Message String
相當於Key<String>
@Count int
相當於Key<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() {
bind(EchoService.class).in(Scopes.SINGLETON);
}
@Provides
@Message
public String messageProvider() {
return "foo";
}
@Provides
@Count
public Integer countProvider() {
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;
}
}
}
Guice
注入器創建單例的處理邏輯類似於:
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);
這里的注解@Provides
在Guice
中的實現對應於Provider
接口,該接口的定義十分簡單:
interface Provider<T> {
/** Provides an instance of T.**/
T get();
}
Guice Map
中所有的值都可以理解為一個Provider
的實現,例如上面的例子可以理解為:
// messageProvider.get() => 'foo'
Provider<String> messageProvider = () -> EchoModule.messageProvider();
// countProvider.get() => 10087
Provider<Integer> countProvider = () -> EchoModule.countProvider();
依賴搜索和創建的過程也是根據條件創建Key
實例,然后在Guice Map
中定位唯一的於Provider
,然后通過該Provider
完成依賴組件的實例化,接着完成后續的依賴注入動作。這個過程在Guice
文檔中使用了一個具體的表格進行說明,這里貼一下這個表格:
Guice DSL 語法 |
對應的模型 |
---|---|
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) |
Key
實例的創建有很多衍生方法,可以滿足單具體類型、具體類型加注解等多種實例化方式。依賴注入使用@Inject
注解,支持成員變量和構造注入,一個接口由多個實現的場景可以通過內建@Named
注解或者自定義注解指定具體注入的實現,但是需要在構建綁定的時候通過@Named
注解或者自定義注解標記具體的實現。例如:
public class GuiceMentalModelDemo {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
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);
}
}
}
// 控制台輸出:SecondMessageProcessor process message => hello world
@Named
注解這里可以換成任意的自定義注解實現,不過注意自定義注解需要添加元注解@javax.inject.Qualifier
,最終的效果是一致的,內置的@Named
就能滿足大部分的場景。最后,每個組件注冊到Guice
中,該組件的所有依賴會形成一個有向圖,注入該組件的時候會遞歸注入該組件自身的所有依賴,這個遍歷注入流程遵循深度優先。Guice
會校驗組件的依賴有向圖的合法性,如果該有向圖是非法的,會拋出CreationException
異常。
Guice支持的綁定
Guice
提供AbstractModule
抽象模塊類給使用者繼承,覆蓋configure()
方法,通過bind()
相關API
創建綁定。
Guice中的Binding其實就是前面提到的Mental Model中Guice Map中的鍵和值的映射關系,Guice提供多種注冊這個綁定關系的API
這里僅介紹最常用的綁定類型:
Linked Binding
Instance Binding
Provider Binding
Constructor Binding
Untargeted Binding
Multi Binding
JIT Binding
Linked Binding
Linked Binding
用於映射一個類型和此類型的實現類型,使用起來如下:
bind(接口類型.class).to(實現類型.class);
具體例子:
public class GuiceLinkedBindingDemo {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
bind(Foo.class).to(Bar.class).in(Scopes.SINGLETON);
}
});
Foo foo = injector.getInstance(Foo.class);
}
interface Foo {
}
public static class Bar implements Foo {
}
}
Linked Binding
常用於這種一個接口一個實現的場景。目標類型上添加了@Singleton
注解,那么編程式注冊綁定時候可以無需調用in(Scopes.SINGLETON)
。
Instance Binding
Instance Binding
用於映射一個類型和此類型的實現類型實例,也包括常量的綁定。以前一小節的例子稍微改造成Instance Binding
的模式如下:
final Bar bar = new Bar();
bind(Foo.class).toInstance(bar);
# 或者添加Named注解
bind(Foo.class).annotatedWith(Names.named("bar")).toInstance(bar);
# 常量綁定
bindConstant().annotatedWith(Names.named("key")).to(value);
可以基於這種方式進行常量的綁定,例如:
public class GuiceInstanceBindingDemo {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
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();
}
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() {
System.out.printf("host => %s, port => %d, protocol => %s\n", host, port, protocol);
}
}
}
// 輸出結果:host => localhost, port => 8080, protocol => HTTPS
Provider Binding
Provider Binding
,可以指定某個類型和該類型的Provider
實現類型進行綁定,有點像設計模式中的簡單工廠模式,可以類比為Spring
中的FactoryBean
接口。舉個例子:
public class GuiceProviderBindingDemo {
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
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() {
System.out.println("Get Foo from FooProvider...");
return foo;
}
}
}
// Get Foo from FooProvider...
這里也要注意,如果標記Provider為單例,那么在Injector中獲取創建的實例,只會調用一次get()方法,也就是懶加載
@Provides
注解是Provider Binding
一種特化模式,可以在自定義的Module
實現中添加使用了@Provides
注解的返回對應類型實例的方法,這個用法跟Spring
里面的@Bean
注解十分相似。一個例子如下:
public class GuiceAnnotationProviderBindingDemo {
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
}
@Singleton
@Provides
public Foo fooProvider() {
System.out.println("init Foo from method fooProvider()...");
return new Foo();
}
});
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()...
Constructor Binding
Constructor Binding
需要顯式綁定某個類型到其實現類型的一個明確入參類型的構造函數,目標構造函數不需要使用@Inject
注解。例如:
public class GuiceConstructorBindingDemo {
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
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
這里需要使用者捕獲和處理獲取構造函數失敗拋出的NoSuchMethodException
異常。
Untargeted Binding
Untargeted Binding
用於注冊綁定沒有目標(實現)類型的特化場景,一般是沒有實現接口的普通類型,在沒有使用@Named
注解或者自定義注解綁定的前提下可以忽略to()
調用。但是如果使用了@Named
注解或者自定義注解進行綁定,to()
調用一定不能忽略。例如:
public class GuiceUnTargetedBindingDemo {
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
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 {
}
}
Multi Binding
Multi Binding
也就是多(實例)綁定,使用特化的Binder
代理完成,這三種Binder
代理分別是:
Multibinder
:可以簡單理解為Type => Set<TypeImpl>
,注入類型為Set<Type>
MapBinder
:可以簡單理解為(KeyType, ValueType) => Map<KeyType, ValueTypeImpl>
,注入類型為Map<KeyType, ValueType>
OptionalBinder
:可以簡單理解為Type => Optional.ofNullable(GuiceMap.get(Type)).or(DefaultImpl)
,注入類型為Optional<Type>
Multibinder
的使用例子:
public class GuiceMultiBinderDemo {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
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() {
Optional.ofNullable(processors).ifPresent(ps -> ps.forEach(Processor::process));
}
}
interface Processor {
void process();
}
public static class FirstProcessor implements Processor {
@Override
public void process() {
System.out.println("FirstProcessor process...");
}
}
public static class SecondProcessor implements Processor {
@Override
public void process() {
System.out.println("SecondProcessor process...");
}
}
}
// 輸出結果
FirstProcessor process...
SecondProcessor process...
MapBinder
的使用例子:
public class GuiceMapBinderDemo {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
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() {
Optional.ofNullable(processors).ifPresent(ps -> ps.forEach(((type, processor) -> processor.process())));
}
}
public enum Type {
/**
* 短信
*/
SMS,
/**
* 消息模板
*/
MESSAGE_TEMPLATE
}
interface Processor {
void process();
}
public static class SmsProcessor implements Processor {
@Override
public void process() {
System.out.println("SmsProcessor process...");
}
}
public static class MessageTemplateProcessor implements Processor {
@Override
public void process() {
System.out.println("MessageTemplateProcessor process...");
}
}
}
// 輸出結果
SmsProcessor process...
MessageTemplateProcessor process...
OptionalBinder
的使用例子:
public class GuiceOptionalBinderDemo {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
// 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);
}
}
}
JIT Binding
JIT Binding
也就是Just-In-Time Binding
,也可以稱為隱式綁定(Implicit Binding
)。隱式綁定需要滿足:
- 構造函數必須無參,並且非
private
修飾 - 沒有在
Module
實現中激活Binder#requireAtInjectRequired()
調用Binder#requireAtInjectRequired()
方法可以強制聲明Guice
只使用帶有@Inject
注解的構造器。調用Binder#requireExplicitBindings()
方法可以聲明Module
內必須顯式聲明所有綁定,也就是禁用隱式綁定,所有綁定必須在Module
的實現中聲明。下面是一個隱式綁定的例子:
public class GuiceJustInTimeBindingDemo {
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
}
});
Foo instance = injector.getInstance(Key.get(Foo.class));
}
public static class Foo {
public Foo() {
System.out.println("init Foo...");
}
}
}
// init Foo...
此外還有兩個運行時綁定注解:
@ImplementedBy
:特化的Linked Binding
,用於運行時綁定對應的目標類型
@ImplementedBy(MessageProcessor.class)
public interface Processor {
}
@ProvidedBy
:特化的Provider Binding
,用於運行時綁定對應的目標類型的Provider
實現
@ProvidedBy(DruidDataSource.class)
public interface DataSource {
}
AOP特性
Guice
提供了相對底層的AOP
特性,使用者需要自行實現org.aopalliance.intercept.MethodInterceptor
接口在方法執行點的前后插入自定義代碼,並且通過Binder#bindInterceptor()
注冊方法攔截器。這里只通過一個簡單的例子進行演示,模擬的場景是方法執行前和方法執行完成后分別打印日志,並且計算目標方法調用耗時:
public class GuiceAopDemo {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
bindInterceptor(Matchers.only(EchoService.class), Matchers.any(), new EchoMethodInterceptor());
}
});
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));
return result;
}
}
}
// 輸出結果
Before invoke method => [echo]
throwable echo
After invoke method => [echo], cost => 16013700 ns
自定義注入
通過TypeListener
和MembersInjector
可以實現目標類型實例的成員屬性自定義注入擴展。例如可以通過下面的方式實現目標實例的org.slf4j.Logger
屬性的自動注入:
public class GuiceCustomInjectionDemo {
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(new AbstractModule() {
@Override
public void configure() {
bindListener(Matchers.any(), new LoggingListener());
}
});
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(new LoggingMembersInjector<>(field));
}
}
clazz = clazz.getSuperclass();
}
}
}
}
// 輸出結果
[2022-02-22 00:51:33,516] [INFO] cn.vlts.guice.GuiceCustomInjectionDemo$LoggingClient [main] [] - Hello World
此例子需要引入logback
和slf4j-api
的依賴。
基於ClassGraph掃描和全自動注冊綁定
Guice
本身不提供類路徑或者Jar
文件的類掃描功能,要實現類路徑下的所有Bean
全自動注冊綁定,需要依賴第三方類掃描框架,這里選用了一個性能比較高社區比較活躍的類庫io.github.classgraph:classgraph
。引入ClassGraph
的最新依賴:
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.8.138</version>
</dependency>
編寫自動掃描Module
:
@RequiredArgsConstructor
public class GuiceAutoScanModule extends AbstractModule {
private final Set<Class<?>> bindClasses = new HashSet<>();
private final String[] acceptPackages;
private final String[] rejectClasses;
@Override
public void configure() {
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();
return isSingleton(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) {
return isSingleton(implClass) && Provider.class.isAssignableFrom(implClass);
}
}
使用方式:
GuiceAutoScanModule module = new GuiceAutoScanModule(new String[]{"cn.vlts"}, new String[]{"*Demo", "*Test"});
Injector injector = Guice.createInjector(module);
GuiceAutoScanModule
目前只是一個並不完善的示例,用於掃描cn.vlts
包下(排除類名以Demo
或者Test
結尾的類)所有的類並且按照不同情況進行綁定注冊,實際場景可能會更加復雜,可以基於類似的思路進行優化和調整。
小結
限於篇幅,本文只介紹了Guice
的基本使用、設計理念和不同類型的綁定方式注冊,更深入的實踐方案后面有機會應用在項目中的時候再基於案例詳細聊聊Guice
的應用。另外,Guice
不是過時的組件,相對於SpringBoot
一個最簡構建幾十MB
的Flat Jar
,如果僅僅想要輕量級DI
功能,Guice
會是一個十分合適的選擇。
參考資料:
(本文完 c-4-d e-a-20220221)