背景
在日常寫一些小工具或者小專案的時候,有依賴管理和依賴注入的需求,但是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)