你期望月薪4萬,出門右拐,不送,這幾個點,你也就是個初級的水平

路人甲Java發表於2020-03-27

先來看幾個問題

  1. 通過註解的方式注入依賴物件,介紹一下你知道的幾種方式

  2. @Autowired和@Resource有何區別

  3. 說一下@Autowired查詢候選者的過程

  4. 說一下@Resource查詢候選者的過程

  5. @Qulifier有哪些用法?

  6. @Qulifier加在類上面是幹什麼用的?

  7. @Primary是做什麼的?

  8. 泛型注入用過麼?

這些問題如果你都ok,那麼恭喜你,很厲害。

本文內容

  1. 介紹spring中通過註解實現依賴注入的所有方式

  • @Autowired註解

  • @Qualifier註解

  • @Resource註解

  • @Primary註解

  • @Bean中注入的幾種方式

  1. 將指定型別的所有bean,注入到集合中

  2. 將指定型別的所有bean,注入到map中

  3. 注入泛型

  4. 依賴注入原始碼方面的一些介紹

本文內容比較多,所有知識點均有詳細案例,大家一定要敲一遍,加深理解。

@Autowired:注入依賴物件

作用

實現依賴注入,spring容器會對bean中所有欄位、方法進行遍歷,標註有@Autowired註解的,都會進行注入。

看一下其定義:

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

    /**
     * Declares whether the annotated dependency is required.
     * <p>Defaults to {@code true}.
     */
    boolean required() default true;

}

可以用在構造器、方法、方法引數、欄位、註解上。

引數:

required:標註的物件是否必須注入,可能這個物件在容器中不存在,如果為true的時候,找不到匹配的候選者就會報錯,為false的時候,找不到也沒關係 。

@Autowire查詢候選者的過程

查詢過程有點複雜,看不懂的可以先跳過,先看後面案例,本文看完之後,可以回頭再來看這個過程。

@Autowired標註在欄位上面:假定欄位型別為一個自定義的普通的型別,候選者查詢過程如下

你期望月薪4萬,出門右拐,不送,這幾個點,你也就是個初級的水平

@Autowired標註在方法上或者方法引數上面:假定引數型別為為一個自定義的普通的型別,候選者查詢過程如下:

你期望月薪4萬,出門右拐,不送,這幾個點,你也就是個初級的水平

上圖中深色的表示方法注入和欄位注入查詢過程的不同點。

上圖中展示的是方法中只有一個引數的情況,如果有多個引數,就重複上面的過程,直到找到所有需要注入的引數。

將指定型別的所有bean注入到Collection中

如果被注入的物件是Collection型別的,可以指定泛型的型別,然後會按照上面的方式查詢所有滿足泛型型別所有的bean

將指定型別的所有bean注入到Map中

如果被注入的物件是Map型別的,可以指定泛型的型別,key通常為String型別,value為需要查詢的bean的型別,然後會按照上面方式查詢所有注入value型別的bean,將bean的name作為key,bean物件作為value,放在HashMap中,然後注入。

@Autowired查詢候選者可以簡化為下面這樣

按型別找->通過限定符@Qualifier過濾->@Primary->@Priority->根據名稱找(欄位名稱或者方法名稱)

概括為:先按型別找,然後按名稱找

案例1:@Autowired標註在構造器上,通過構造器注入依賴物件

Service1

package com.javacode2018.lesson001.demo26.test0;

import org.springframework.stereotype.Component;

@Component
public class Service1 {
}

Service2

package com.javacode2018.lesson001.demo26.test0;

import org.springframework.stereotype.Component;

@Component
public class Service2 {
    private Service1 service1;

    public Service2() { //@1
        System.out.println(this.getClass() + "無參構造器");
    }

    public Service2(Service1 service1) { //@2
        System.out.println(this.getClass() + "有參構造器");
        this.service1 = service1;
    }

    @Override
    public String toString() { //@2
        return "Service2{" +
                "service1=" + service1 +
                '}';
    }
}

Service2中依賴於Service1,有2個構造方法

@1:無參構造器

@2:有參構造器,可以通過這個傳入依賴的Service1

@3:重寫了toString方法,一會列印測試的時候方便檢視

來個總的配置檔案

package com.javacode2018.lesson001.demo26.test0;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan //@1
public class MainConfig0 {
}

@1:會自動掃描當前類所在的包,會將Service1和Service2註冊到容器。

來個測試用例

package com.javacode2018.lesson001.demo26;

import com.javacode2018.lesson001.demo26.test0.MainConfig0;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class InjectTest {

    @Test
    public void test0() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig0.class);
        for (String beanName : context.getBeanDefinitionNames()) {
            System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
        }
    }

}

main方法中啟動容器,載入MainConfig0配置類,然後輸出容器中所有的bean

執行部分輸出

class com.javacode2018.lesson001.demo26.test0.Service2無參構造器
service1->com.javacode2018.lesson001.demo26.test0.Service1@4a94ee4
service2->Service2{service1=null}

輸出中可以看出呼叫了Service2的無參構造器,service2中的service1為null

通過@Autowired指定注入的構造器

在Service2有參有參構造器上面加上@Autowired註解,如下:

@Autowired
public Service2(Service1 service1) {
    System.out.println(this.getClass() + "有參構造器");
    this.service1 = service1;
}

再次執行test0()

class com.javacode2018.lesson001.demo26.test0.Service2有參構造器
service1->com.javacode2018.lesson001.demo26.test0.Service1@4ec4f3a0
service2->Service2{service1=com.javacode2018.lesson001.demo26.test0.Service1@4ec4f3a0}

Service2有參構造器被呼叫了,service2中的service1有值了。

案例2:@Autowired標註在方法上,通過方法注入依賴的物件

Service1

package com.javacode2018.lesson001.demo26.test1;

import org.springframework.stereotype.Component;

@Component
public class Service1 {
}

Service2

package com.javacode2018.lesson001.demo26.test1;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Service2 {
    private Service1 service1;

    @Autowired
    public void injectService1(Service1 service1) { //@1
        System.out.println(this.getClass().getName() + ".injectService1()");
        this.service1 = service1;
    }

    @Override
    public String toString() {
        return "Service2{" +
                "service1=" + service1 +
                '}';
    }
}

@1:方法上標註了@Autowired,spring容器會呼叫這個方法,從容器中查詢Service1型別的bean,然後注入。

來個總的配置檔案

package com.javacode2018.lesson001.demo26.test1;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig1 {
}

來個測試用例

InjectTest中加個方法

@Test
public void test1() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig1.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

com.javacode2018.lesson001.demo26.test1.Service2.injectService1()
service1->com.javacode2018.lesson001.demo26.test1.Service1@9597028
service2->Service2{service1=com.javacode2018.lesson001.demo26.test1.Service1@9597028}

通過injectService1方法成功注入service1

案例3:@Autowired標註在setter方法上,通過setter方法注入

上面2種通過構造器,和通過普通的一個方法注入,不是很常見,可以將@Autowired標註在set方法上面,來注入指定的物件

Service1

package com.javacode2018.lesson001.demo26.test2;

import org.springframework.stereotype.Component;

@Component
public class Service1 {
}

Service2

package com.javacode2018.lesson001.demo26.test2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Service2 {

    private Service1 service1;

    @Autowired
    public void setService1(Service1 service1) { //@1
        System.out.println(this.getClass().getName() + ".setService1方法");
        this.service1 = service1;
    }

    @Override
    public String toString() {
        return "Service2{" +
                "service1=" + service1 +
                '}';
    }
}

@1:標準的set方法,方法上使用了 @Autowired,會通過這個方法注入Service1型別的bean物件。

來個總的配置檔案

package com.javacode2018.lesson001.demo26.test2;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig2 {
}

來個測試用例

@Test
public void test2() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig2.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

com.javacode2018.lesson001.demo26.test2.Service2.setService1方法
service1->com.javacode2018.lesson001.demo26.test2.Service1@6069db50
service2->Service2{service1=com.javacode2018.lesson001.demo26.test2.Service1@6069db50}

案例4:@Autowired標註在方法引數上

Service1

package com.javacode2018.lesson001.demo26.test3;

import org.springframework.stereotype.Component;

@Component
public class Service1 {
}

Service2

package com.javacode2018.lesson001.demo26.test3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Service2 {

    private Service1 service1;

    @Autowired
    public void injectService1(Service1 service1, String name) { //@1
        System.out.println(String.format("%s.injectService1(),{service1=%s,name=%s}", this.getClass().getName(), service1, name));
        this.service1 = service1;
    }

    @Override
    public String toString() {
        return "Service2{" +
                "service1=" + service1 +
                '}';
    }
}

@1:方法上標註了@Autowired,表示會將這個方法作為注入方法,這個方法有2個引數,spring查詢這2個引數對應的bean,然後注入。

第一個引數對應的bean是存在的,第二個是一個String型別的,我們並沒有定義String型別bean,一會看看效果

來個總的配置檔案

package com.javacode2018.lesson001.demo26.test3;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig3 {
}

來個測試用例

@Test
public void test3() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig3.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'service2': Unsatisfied dependency expressed through method 'injectService1' parameter 1; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

報錯了,從錯誤資訊中可以看出,通過injectService1方法注入的時候,第二個引數為String型別,spring從容器中沒有找到String型別的候選bean,所以報錯了。

我們可以這麼做

多個引數的時候,方法上面的@Autowire預設對方法中所有引數起效,如果我們想對某個引數進行特定的配置,可以在引數上加上@Autowired,這個配置會覆蓋方法上面的@Autowired配置。

在第二個引數上面加上@Autowired,設定required為false:表示這個bean不是強制注入的,能找到就注入,找不到就注入一個null物件,調整一下程式碼,如下:

@Autowired
public void injectService1(Service1 service1, @Autowired(required = false) String name) { //@1
    System.out.println(String.format("%s.injectService1(),{service1=%s,name=%s}", this.getClass().getName(), service1, name));
    this.service1 = service1;
}

此時方法的第一個引數被方法上面的@Autowired約束

第二個引數受@Autowired(required = false)約束

再次執行輸出

com.javacode2018.lesson001.demo26.test3.Service2.injectService1(),{service1=com.javacode2018.lesson001.demo26.test3.Service1@59309333,name=null}
service1->com.javacode2018.lesson001.demo26.test3.Service1@59309333
service2->Service2{service1=com.javacode2018.lesson001.demo26.test3.Service1@59309333}

注入成功了,service1有值,name為null

案例5:@Autowired用在欄位上

Service1

package com.javacode2018.lesson001.demo26.test4;

import org.springframework.stereotype.Component;

@Component
public class Service1 {
}

Service2

package com.javacode2018.lesson001.demo26.test4;

import org.springframework.stereotype.Component;

@Component
public class Service2 {
}

Service3

package com.javacode2018.lesson001.demo26.test4;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Service3 {

    @Autowired
    private Service1 service1;//@1

    @Autowired
    private Service2 service2;//@2

    @Override
    public String toString() {
        return "Service3{" +
                "service1=" + service1 +
                ", service2=" + service2 +
                '}';
    }
}

@1和@2:定義了2個欄位,上面都標註了@Autowired,spring會去容器中按照型別查詢這2種型別的bean,然後設定給這2個屬性。

來個總的配置檔案

package com.javacode2018.lesson001.demo26.test4;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig4 {
}

來個測試用例

@Test
public void test4() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig4.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

service1->com.javacode2018.lesson001.demo26.test4.Service1@7e07db1f
service2->com.javacode2018.lesson001.demo26.test4.Service2@1189dd52
service3->Service3{service1=com.javacode2018.lesson001.demo26.test4.Service1@7e07db1f, service2=com.javacode2018.lesson001.demo26.test4.Service2@1189dd52}

service3中標註@Autowired的2個屬性都有值了,都被注入成功了。

案例6:@Autowire標註欄位,多個候選者的時候,按欄位名稱注入

IService介面

package com.javacode2018.lesson001.demo26.test5;

public interface IService {
}

介面來2個實現

2個實現類上都標註了@Component註解,都會被註冊到容器中。

Service0
package com.javacode2018.lesson001.demo26.test5;

import org.springframework.stereotype.Component;

@Component
public class Service0 implements IService {
}
Service1
package com.javacode2018.lesson001.demo26.test5;

import org.springframework.stereotype.Component;

@Component
public class Service1 implements IService {
}

來個Service2

package com.javacode2018.lesson001.demo26.test5;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Service2 {
    @Autowired
    private IService service1; //@1

    @Override
    public String toString() {
        return "Service2{" +
                "service1=" + service1 +
                '}';
    }
}

@1:標註了@Autowired註解,需要注入型別為IService型別的bean,滿足這種型別的有2個:service0和service1

按照上面介紹的候選者查詢過程,最後會注入和欄位名稱一樣的bean,即:service1

來個總的配置類,負責掃描當前包中的元件

package com.javacode2018.lesson001.demo26.test5;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig5 {
}

來個測試用例

@Test
public void test5() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig5.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

service0->com.javacode2018.lesson001.demo26.test5.Service0@36902638
service1->com.javacode2018.lesson001.demo26.test5.Service1@223d2c72
service2->Service2{service1=com.javacode2018.lesson001.demo26.test5.Service1@223d2c72}

注意最後一行,service2中的service1被注入了bean:service1

案例7:將指定型別的所有bean,注入到Collection、Map中

注入到Collection中

被注入的型別為Collection型別或者Collection子介面型別,注意必須是介面型別,如:

Collection<IService>
List<IService>
Set<IService>

會在容器中找到所有IService型別的bean,放到這個集合中

注入到Map中

被注入的型別為Map型別或者Map子介面型別,注意必須是介面型別,如:

Map<String,IService>

會在容器中找到所有IService型別的bean,放到這個Map中,key為bean的名稱,value為bean物件

來看案例程式碼。

來個介面

package com.javacode2018.lesson001.demo26.test6;

public interface IService {
}

來2個實現類,標註@Component註解

Service0
package com.javacode2018.lesson001.demo26.test6;

import org.springframework.stereotype.Component;

@Component
public class Service0 implements IService {
}
Service1
package com.javacode2018.lesson001.demo26.test6;

import org.springframework.stereotype.Component;

@Component
public class Service1 implements IService {
}

再來個類Service2

package com.javacode2018.lesson001.demo26.test6;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

@Component
public class Service2 {

    @Autowired
    private List<IService> services;

    @Autowired
    private Map<String, IService> serviceMap;

    @Override
    public String toString() {
        return "Service2{\n" +
                "services=" + services +
                ", \n serviceMap=" + serviceMap +
                '}';
    }
}

@1:注入IService型別的所有bean

@2:注入一個map

來個總的配置類

package com.javacode2018.lesson001.demo26.test6;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig6 {
}

來個測試用例

@Test
public void test6() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig6.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

service0->com.javacode2018.lesson001.demo26.test6.Service0@1189dd52
service1->com.javacode2018.lesson001.demo26.test6.Service1@36bc55de
service2->Service2{
services=[com.javacode2018.lesson001.demo26.test6.Service0@1189dd52, com.javacode2018.lesson001.demo26.test6.Service1@36bc55de], 
 serviceMap={service0=com.javacode2018.lesson001.demo26.test6.Service0@1189dd52, service1=com.javacode2018.lesson001.demo26.test6.Service1@36bc55de}}

注意看一下上面services和serviceMap的值。

@Autowired原始碼

spring使用下面這個類處理@Autowired註解

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor

@Resource:注意依賴物件

作用

和@Autowired註解類似,也是用來注入依賴的物件的,spring容器會對bean中所有欄位、方法進行遍歷,標註有@Resource註解的,都會進行注入。

看一下這個註解定義:

javax.annotation.Resource

@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
    String name() default "";
    ..其他不常用的引數省略
}

這個註解是javax中定義的,並不是spring中定義的註解。

從定義上可以見,這個註解可以用在任何型別上面、欄位、方法上面。

注意點:

用在方法上的時候,方法引數只能有一個。

@Resource查詢候選者的過程

查詢過程有點複雜,看不懂的可以先跳過,先看後面案例,本文看完之後,可以回頭再來看這個過程。

@Resource標註在欄位上面:假定欄位型別為一個自定義的普通的型別,候選者查詢過程如下

你期望月薪4萬,出門右拐,不送,這幾個點,你也就是個初級的水平

@Autowired標註在方法上或者方法引數上面:假定引數型別為為一個自定義的普通的型別,候選者查詢過程如下:

你期望月薪4萬,出門右拐,不送,這幾個點,你也就是個初級的水平

將指定型別的所有bean注入到Collection中

如果被注入的物件是Collection型別的,可以指定泛型的型別,然後會按照上面的方式查詢所有滿足泛型型別所有的bean

將指定型別的所有bean注入到Map中

如果被注入的物件是Map型別的,可以指定泛型的型別,key通常為String型別,value為需要查詢的bean的型別,然後會按照上面方式查詢所有注入value型別的bean,將bean的name作為key,bean物件作為value,放在HashMap中,然後注入。

@Resource查詢候選者可以簡化為

先按Resource的name值作為bean名稱找->按名稱(欄位名稱、方法名稱、set屬性名稱)找->按型別找->通過限定符@Qualifier過濾->@Primary->@Priority->根據名稱找(欄位名稱或者方法引數名稱)

概括為:先按名稱找,然後按型別找

案例1:將@Resource標註在欄位上

IService介面

package com.javacode2018.lesson001.demo26.test7;

public interface IService {
}

2個實現類

Service0
package com.javacode2018.lesson001.demo26.test7;

import org.springframework.stereotype.Component;

@Component
public class Service0 implements IService {
}

@Component標註的bean名稱預設為service0

Service1
package com.javacode2018.lesson001.demo26.test7;

import org.springframework.stereotype.Component;

@Component
public class Service1 implements IService {
}

@Component標註的bean名稱預設為service1

再來一個類

package com.javacode2018.lesson001.demo26.test7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

@Component
public class Service2 {

    @Resource
    private IService service1;//@1

    @Override
    public String toString() {
        return "Service2{" +
                "service1=" + service1 +
                '}';
    }
}

@1:欄位名稱為service1,按照欄位名稱查詢bean,會找到Service1

來個配置類

package com.javacode2018.lesson001.demo26.test7;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig7 {
}

測試用例

@Test
public void test7() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig7.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

service0->com.javacode2018.lesson001.demo26.test7.Service0@222545dc
service1->com.javacode2018.lesson001.demo26.test7.Service1@5c5eefef
service2->Service2{service1=com.javacode2018.lesson001.demo26.test7.Service1@5c5eefef}

最後一行可以看出注入了service1

如果將Service2中的程式碼調整一下

@Resource
private IService service0;

此時會注入service0這個bean

同樣@Resource可以用在方法上,也可以將所有型別的bean注入到Collection、Map中,這裡就不演示了,重點了解一下候選者查詢的過程,使用上就比較簡單了,@Resource的其他案例,大家可以自己寫寫練練。

下面來說另外幾個註解,也是比較重要的。

@Resource原始碼

spring使用下面這個類處理@Resource註解

org.springframework.context.annotation.CommonAnnotationBeanPostProcessor

@Qualifier:限定符

作用

這個單詞的意思是:限定符。

可以在依賴注入查詢候選者的過程中對候選者進行過濾。

看一下其定義:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {

    String value() default "";

}

可以用在欄位、方法、引數、任意型別、註解上面

有一個引數value

還是來看案例,通過案例理解更容易。

案例1:用在類上

用在類上,你可以理解為給通過@Qulifier給這個bean打了一個標籤。

先來一個介面

package com.javacode2018.lesson001.demo26.test8;

public interface IService {
}

來3個實現類

前2個@Qulifier的value為tag1,第3個實現類為tag2

Service1

package com.javacode2018.lesson001.demo26.test8;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
@Qualifier("tag1") //@1
public class Service1 implements IService {
}

@1:tag1

Service2

package com.javacode2018.lesson001.demo26.test8;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
@Qualifier("tag1")
public class Service2 implements IService {
}

@1:tag1

Service3

package com.javacode2018.lesson001.demo26.test8;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
@Qualifier("tag2")//@1
public class Service3 implements IService {
}

@1:tag2

來一個類,來注入上面幾個bean

package com.javacode2018.lesson001.demo26.test8;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class InjectService {
    @Autowired
    @Qualifier("tag1") //@1
    private Map<String, IService> serviceMap1;

    @Autowired
    @Qualifier("tag2") //@2
    private Map<String, IService> serviceMap2;

    @Override
    public String toString() {
        return "InjectService{" +
                "serviceMap1=" + serviceMap1 +
                ", serviceMap2=" + serviceMap2 +
                '}';
    }
}

@1:限定符的值為tag1,此時會將類上限定符為tag1的所有bean注入進來

@2:限定符的值為tag2,此時會將類上限定符為tag2的所有bean注入進來

來個配置類

package com.javacode2018.lesson001.demo26.test8;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig8 {
}

測試用例

@Test
public void test8() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig8.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

injectService->InjectService{serviceMap1={service1=com.javacode2018.lesson001.demo26.test8.Service1@9597028, service2=com.javacode2018.lesson001.demo26.test8.Service2@6069db50}, serviceMap2={service3=com.javacode2018.lesson001.demo26.test8.Service3@4efbca5a}}
service1->com.javacode2018.lesson001.demo26.test8.Service1@9597028
service2->com.javacode2018.lesson001.demo26.test8.Service2@6069db50
service3->com.javacode2018.lesson001.demo26.test8.Service3@4efbca5a

注意第一行的輸出,看一下serviceMap1和serviceMap2的值。

serviceMap1注入了@Qulifier的value為tag1的所有IService型別的bean

serviceMap1注入了@Qulifier的value為tag2的所有IService型別的bean

實現了bean分組的效果。

案例2:@Autowired結合@Qulifier指定注入的bean

被注入的型別有多個的時候,可以使用@Qulifier來指定需要注入那個bean,將@Qulifier的value設定為需要注入bean的名稱

看案例程式碼

來個介面

package com.javacode2018.lesson001.demo26.test9;

public interface IService {
}

有2個實現類

2個實現類上面沒有使用@Qulifier註解了

Service1
package com.javacode2018.lesson001.demo26.test9;

import org.springframework.stereotype.Component;

@Component
public class Service1 implements IService {
}
Service2
package com.javacode2018.lesson001.demo26.test9;

import org.springframework.stereotype.Component;

@Component
public class Service2 implements IService {
}

我們可以知道上面2個bean的名稱分別為:service1、service2

來個類,注入IService型別的bean

package com.javacode2018.lesson001.demo26.test9;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class InjectService {
    @Autowired
    @Qualifier("service2") //@1
    private IService service;

    @Override
    public String toString() {
        return "InjectService{" +
                "service=" + service +
                '}';
    }
}

@1:這裡限定符的值為service2,容器中IService型別的bean有2個[service1和service2],當類上沒有標註@Qualifier註解的時候,可以理解為:bean的名稱就是限定符的值,所以@1這裡會匹配到service2

來個配置類

package com.javacode2018.lesson001.demo26.test9;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;

@ComponentScan
public class MainConfig9 {
}

來個測試用例

@Test
public void test9() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig9.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

injectService->InjectService{service=com.javacode2018.lesson001.demo26.test9.Service2@223d2c72}
service1->com.javacode2018.lesson001.demo26.test9.Service1@8f4ea7c
service2->com.javacode2018.lesson001.demo26.test9.Service2@223d2c72

從第一行可以看出注入了service1

案例3:用在方法引數上

程式碼

package com.javacode2018.lesson001.demo26.test10;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class InjectService {

    private IService s1;
    private IService s2;

    @Autowired
    public void injectBean(@Qualifier("service2") IService s1, @Qualifier("service1") IService s2) { //@1
        this.s1 = s1;
        this.s2 = s2;
    }

    @Override
    public String toString() {
        return "InjectService{" +
                "s1=" + s1 +
                ", s2=" + s2 +
                '}';
    }
}

@1:方法上標註了@Autowired註解,說明會被注入依賴,2個引數上分別使用了限定符來指定具體需要注入哪個bean

測試用例

@Test
public void test10() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig10.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

injectService->InjectService{s1=com.javacode2018.lesson001.demo26.test10.Service2@55183b20, s2=com.javacode2018.lesson001.demo26.test10.Service1@4f83df68}
service1->com.javacode2018.lesson001.demo26.test10.Service1@4f83df68
service2->com.javacode2018.lesson001.demo26.test10.Service2@55183b20

第一行中的

s1:service2

s2:service1

案例4:用在setter方法上

不管是用在setter方法還是普通方法上面,都是一樣的效果

程式碼

package com.javacode2018.lesson001.demo26.test11;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class InjectService {

    private IService s1;
    private IService s2;

    @Autowired
    @Qualifier("service2")
    public void setS1(IService s1) {
        this.s1 = s1;
    }

    @Autowired
    @Qualifier("service2")
    public void setS2(IService s2) {
        this.s2 = s2;
    }

    @Override
    public String toString() {
        return "InjectService{" +
                "s1=" + s1 +
                ", s2=" + s2 +
                '}';
    }
}

上面2個setter方法上都有@Autowired註解,並且結合了@Qulifier註解來限定需要注入哪個bean

測試用例

@Test
public void test11() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig11.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

injectService->InjectService{s1=com.javacode2018.lesson001.demo26.test11.Service2@35e2d654, s2=com.javacode2018.lesson001.demo26.test11.Service2@35e2d654}
service1->com.javacode2018.lesson001.demo26.test11.Service1@1bd4fdd
service2->com.javacode2018.lesson001.demo26.test11.Service2@35e2d654

輸出中可以看出:s1為service2,s2為service1

@Primary:設定為主要候選者

注入依賴的過程中,當有多個候選者的時候,可以指定哪個候選者為主要的候選者。

看一下其定義

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Primary {

}

可以用在類上或者方法上面。

通常定義bean常見的有2種方式:

方式1:在類上標註@Component註解,此時可以配合@Primary,標註這個bean為主要候選者

方式2:在配置檔案中使用@Bean註解標註方法,來註冊bean,可以在@Bean標註的方法上加上@Primary,標註這個bean為主要候選bean。

看案例。

案例1:用在類上

來個介面

package com.javacode2018.lesson001.demo26.test12;

public interface IService {
}

2個實現類

Service1
package com.javacode2018.lesson001.demo26.test12;

import org.springframework.stereotype.Component;

@Component
public class Service1 implements IService {
}
Service2
package com.javacode2018.lesson001.demo26.test12;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class Service2 implements IService {
}

Service2上面使用了@Primary,表示這是個主要的候選者

再來個類,注入IService型別的bean

package com.javacode2018.lesson001.demo26.test12;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class InjectService {

    @Autowired
    private IService service1; //@1

    @Override
    public String toString() {
        return "InjectService{" +
                "service1=" + service1 +
                '}';
    }
}

@1:容器中IService型別的bean有2個,但是service2為主要的候選者,所以此處會注入service2

總的配置類

package com.javacode2018.lesson001.demo26.test12;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig12 {
}

測試用例

@Test
public void test12() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig12.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

injectService->InjectService{service1=com.javacode2018.lesson001.demo26.test12.Service2@49ec71f8}
service1->com.javacode2018.lesson001.demo26.test12.Service1@1d2adfbe
service2->com.javacode2018.lesson001.demo26.test12.Service2@49ec71f8

案例2:用在方法上,結合@Bean使用

來個介面

package com.javacode2018.lesson001.demo26.test13;

public interface IService {
}

2個實現類

Service1
package com.javacode2018.lesson001.demo26.test13;

public class Service1 implements IService {
}

Service2

package com.javacode2018.lesson001.demo26.test13;

public class Service2 implements IService {
}

InjectService

package com.javacode2018.lesson001.demo26.test13;

import org.springframework.beans.factory.annotation.Autowired;

public class InjectService {

    @Autowired
    private IService service1;//@1

    @Override
    public String toString() {
        return "InjectService{" +
                "service1=" + service1 +
                '}';
    }
}

使用了@Autowired,需要注入

來個配置類,通過@Bean定義上面3個型別的bean

package com.javacode2018.lesson001.demo26.test13;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class MainConfig13 {

    @Bean
    public IService service1() {
        return new Service1();
    }

    @Bean
    @Primary //@1
    public IService service2() {
        return new Service2();
    }

    @Bean
    public InjectService injectService() {
        return new InjectService();
    }
}

上面是一個配置類,定義了3個bean

@1:這個bean被標註為主要的候選者

來個測試用例

@Test
public void test13() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig13.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

service1->com.javacode2018.lesson001.demo26.test13.Service1@6913c1fb
service2->com.javacode2018.lesson001.demo26.test13.Service2@66d18979
injectService->InjectService{service1=com.javacode2018.lesson001.demo26.test13.Service2@66d18979}

注意最後一行,service1注入的是service2這個bean

@Bean定義bean時注入依賴的幾種方式

常見3種方式

  1. 硬編碼方式

  2. @Autowired、@Resource的方式

  3. @Bean標註的方法引數的方式

方式1:硬編碼方式

來3個類

Service1

package com.javacode2018.lesson001.demo26.test14;

public class Service1 {
}

Service2

package com.javacode2018.lesson001.demo26.test14;

public class Service2 {
}

Service3

package com.javacode2018.lesson001.demo26.test14;

public class Service3 {
    private Service1 service1;
    private Service2 service2;

    public Service1 getService1() {
        return service1;
    }

    public void setService1(Service1 service1) {
        this.service1 = service1;
    }

    public Service2 getService2() {
        return service2;
    }

    public void setService2(Service2 service2) {
        this.service2 = service2;
    }

    @Override
    public String toString() {
        return "Service3{" +
                "service1=" + service1 +
                ", service2=" + service2 +
                '}';
    }
}

上面類中會用到service1和service2,提供了對應的setter方法,一會我們通過setter方法注入依賴物件

來個配置類,通過@Bean的方式建立上面物件

package com.javacode2018.lesson001.demo26.test14;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MainConfig14 {
    @Bean
    public Service1 service1() {
        return new Service1();
    }

    @Bean
    public Service2 service2() {
        return new Service2();
    }

    @Bean
    public Service3 service3() {
        Service3 service3 = new Service3(); //@0
        service3.setService1(this.service1()); //@1
        service3.setService2(this.service2()); //@2
        return service3;
    }
}

上面程式碼中通過@Bean定義了3個bean

Service3中需要用到Service1和Service2,注意@1和@2直接呼叫當前方法獲取另外2個bean,注入到service3中

測試用例

@Test
public void test14() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig14.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

service1->com.javacode2018.lesson001.demo26.test14.Service1@41a2befb
service2->com.javacode2018.lesson001.demo26.test14.Service2@6c40365c
service3->Service3{service1=com.javacode2018.lesson001.demo26.test14.Service1@41a2befb, service2=com.javacode2018.lesson001.demo26.test14.Service2@6c40365c}

方式2:@Autowired、@Resource的方式

這種方式就不講了直接在需要注入的物件上面加上這2個註解的任意一個就行了,可以參考文章前面的部分。

方式3:@Bean標註的方法使用引數來進行注入

package com.javacode2018.lesson001.demo26.test15;

import com.javacode2018.lesson001.demo26.test14.Service1;
import com.javacode2018.lesson001.demo26.test14.Service2;
import com.javacode2018.lesson001.demo26.test14.Service3;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MainConfig15 {
    @Bean
    public Service1 service1() {
        return new Service1();
    }

    @Bean
    public Service2 service2() {
        return new Service2();
    }

    @Bean
    public Service3 service3(Service1 s1, Service2 s2) { //@0
        Service3 service3 = new Service3();
        service3.setService1(s1); //@1
        service3.setService2(s2); //@2
        return service3;
    }
}

@0:這個地方是關鍵,方法上標註了@Bean,並且方法中是有引數的,spring呼叫這個方法建立bean的時候,會將引數中的兩個引數注入進來。

注入物件的查詢邏輯可以參考上面@Autowired標註方法時查詢候選者的邏輯。

來個測試用例

@Test
public void test15() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig15.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

service1->com.javacode2018.lesson001.demo26.test14.Service1@4009e306
service2->com.javacode2018.lesson001.demo26.test14.Service2@43c1b556
service3->Service3{service1=com.javacode2018.lesson001.demo26.test14.Service1@4009e306, service2=com.javacode2018.lesson001.demo26.test14.Service2@43c1b556}

同樣注入成功了。

其他

@Bean標註的方法引數上使用@Autowired註解

@Bean
public Service3 service3_0(Service1 s1, @Autowired(required = false) Service2 s2) { //@0
    Service3 service3 = new Service3();
    service3.setService1(s1); //@1
    service3.setService2(s2); //@2
    return service3;
}

@0:方法由2個引數,第二個引數上標註了@Autowired(required = false),說明第二個引數候選者不是必須的,找不到會注入一個null物件;第一個引數候選者是必須的,找不到會丟擲異常

@Bean結合@Qualifier

package com.javacode2018.lesson001.demo26.test17;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

@Configuration
public class MainConfig17 {

    @Bean
    @Qualifier("tag1") //@1
    public Service1 service1() {
        return new Service1();
    }

    @Bean
    @Qualifier("tag1") //@2
    public Service2 service2() {
        return new Service2();
    }

    @Bean
    @Qualifier("tag2") //@3
    public Service3 service3() {
        return new Service3();
    }

    @Bean
    public InjectService injectService(@Qualifier("tag1") Map<String, IService> map1) { //@4
        InjectService injectService = new InjectService();
        injectService.setServiceMap1(map1);
        return injectService;
    }
}

Service1,Service2,Service3都實現了IService介面

@1,@2,@3這3個方法上面使用了@Bean註解,用來定義3個bean,這3個方法上還是用了@Qualifier註解,用來給這些bean定義標籤,service1()方法類似於下面的寫法:

@Compontent
@Qualifier("tag1")
public class Service1 implements IService{
}

再回到MainConfig17中的@4:引數中需要注入Map<String, IService>,會查詢IService型別的bean,容器中有3個,但是這個引數前面加上了@Qualifier限定符,值為tag1,所以會通過這個過濾,最後滿足的候選者為:[service1,service]

對應測試用例

@Test
public void test17() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig17.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

執行輸出

service1->com.javacode2018.lesson001.demo26.test17.Service1@1190200a
service2->com.javacode2018.lesson001.demo26.test17.Service2@6a2f6f80
service3->com.javacode2018.lesson001.demo26.test17.Service3@45b4c3a9
injectService->InjectService{serviceMap1={service1=com.javacode2018.lesson001.demo26.test17.Service1@1190200a, service2=com.javacode2018.lesson001.demo26.test17.Service2@6a2f6f80}, serviceMap2=null}

注意最後一行serviceMap1,注入了service1和service2

泛型注入

先來2個普通的類

UserModel

package com.javacode2018.lesson001.demo26.test18;

public class UserModel {
}

OrderModel

package com.javacode2018.lesson001.demo26.test18;

public class OrderModel {
}

記住上面2個普通的類UserModel和OrderModel,一會下面會用到。

來個泛型介面

package com.javacode2018.lesson001.demo26.test18;

public interface IDao<T> {
}

上面是個泛型類,類名後面後尖括號

來2個實現類

兩個實現類都會標註@Compontent,交給spring容器管理

UserDao

package com.javacode2018.lesson001.demo26.test18;

import org.springframework.stereotype.Component;

@Component
public class UserDao implements IDao<UserModel> { //@1
}

@1:指定了IDao後面泛型的型別為UserModel

OrderDao

package com.javacode2018.lesson001.demo26.test18;

import org.springframework.stereotype.Component;

@Component
public class OrderDao implements IDao<OrderModel> {//@1
}

@1:指定了IDao後面泛型的型別為OrderModel

在來個泛型型別

package com.javacode2018.lesson001.demo26.test18;

import org.springframework.beans.factory.annotation.Autowired;

public class BaseService<T> {
    @Autowired
    private IDao<T> dao; //@1

    public IDao<T> getDao() {
        return dao;
    }

    public void setDao(IDao<T> dao) {
        this.dao = dao;
    }
}

BaseService同樣是個泛型類

@1:這個地方要注意了,上面使用了@Autowired,來注入IDao物件

BaseService來2個子類

兩個子類都會標註@Compontent,交給spring容器管理

UserService

package com.javacode2018.lesson001.demo26.test18;

import org.springframework.stereotype.Component;

@Component
public class UserService extends BaseService<UserModel> {//@1
}

@1:指定了BaseService後面泛型的型別為UserModel

OrderService

package com.javacode2018.lesson001.demo26.test18;

import org.springframework.stereotype.Component;

@Component
public class OrderService extends BaseService<OrderModel> {//@1
}

@1:指定了BaseService後面泛型的型別為OrderModel

UserService和OrderService繼承了BaseService,所以一會BaseService中的dao屬性會被注入,一會我們關注一下dao這個屬性的值,會是什麼樣的

來個總的配置類

package com.javacode2018.lesson001.demo26.test18;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class MainConfig18 {
}

上面有@CompontentScan註解,會自動掃描當前包中的所有類,並進行自動注入

來個測試用例

@Test
public void test18() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig18.class);
    System.out.println(context.getBean(UserService.class).getDao());
    System.out.println(context.getBean(OrderService.class).getDao());
}

上面程式碼中會將兩個service中的dao輸出,我們來看一下效果

執行輸出

com.javacode2018.lesson001.demo26.test18.UserDao@6adbc9d
com.javacode2018.lesson001.demo26.test18.OrderDao@4550bb58

結果就是重點了,dao屬性並沒有指定具體需要注入那個bean,此時是根據尖括號中的泛型型別來匹配的,這個功能也是相當厲害的。

總結

這篇文中內容比較多,每個案例大家都要去敲一遍,不清楚的,可以留言,或者直接微信中@我

  1. 需要掌握@Autowired註解和@Resource註解中候選者查詢的過程

  2. @Autowired:先通過型別找,然後通過名稱找

  3. @Resource:先通過名稱找,然後通過型別找

  4. @Autowired和@Resource,建議開發中使用@Autowired來實現依賴注入,spring的註解用起來更名正言順一些

  5. @Qulifier:限定符,可以用在類上;也可以用在依賴注入的地方,可以對候選者的查詢進行過濾

  6. @Primary:多個候選者的時候,可以標註某個候選者為主要的候選者

  7. @Bean中注入依賴的3種方式需要掌握

  8. 掌握泛型注入的使用

  9. 主要還是掌握候選者的查詢過程,過程熟悉了,其他的都是小意思,回頭再去看看上面的幾個查詢的流程圖。

案例原始碼

https://gitee.com/javacode2018/spring-series

路人甲java所有案例程式碼以後都會放到這個上面,大家watch一下,可以持續關注動態。

Spring系列

  1. Spring系列第1篇:為何要學spring?

  2. Spring系列第2篇:控制反轉(IoC)與依賴注入(DI)

  3. Spring系列第3篇:Spring容器基本使用及原理

  4. Spring系列第4篇:xml中bean定義詳解(-)

  5. Spring系列第5篇:建立bean例項這些方式你們都知道?

  6. Spring系列第6篇:玩轉bean scope,避免跳坑裡!

  7. Spring系列第7篇:依賴注入之手動注入

  8. Spring系列第8篇:自動注入(autowire)詳解,高手在於堅持

  9. Spring系列第9篇:depend-on到底是幹什麼的?

  10. Spring系列第10篇:primary可以解決什麼問題?

  11. Spring系列第11篇:bean中的autowire-candidate又是幹什麼的?

  12. Spring系列第12篇:lazy-init:bean延遲初始化

  13. Spring系列第13篇:使用繼承簡化bean配置(abstract & parent)

  14. Spring系列第14篇:lookup-method和replaced-method比較陌生,怎麼玩的?

  15. Spring系列第15篇:代理詳解(Java動態代理&cglib代理)?

  16. Spring系列第16篇:深入理解java註解及spring對註解的增強(預備知識)

  17. Spring系列第17篇:@Configration和@Bean註解詳解(bean批量註冊)

  18. Spring系列第18篇:@ComponentScan、@ComponentScans詳解(bean批量註冊)

  19. Spring系列第18篇:@import詳解(bean批量註冊)

  20. Spring系列第20篇:@Conditional通過條件來控制bean的註冊

更多好文章

  1. Java高併發系列(共34篇)

  2. MySql高手系列(共27篇)

  3. Maven高手系列(共10篇)

  4. Mybatis系列(共12篇)

  5. 聊聊db和快取一致性常見的實現方式

  6. 介面冪等性這麼重要,它是什麼?怎麼實現?

  7. 泛型,有點難度,會讓很多人懵逼,那是因為你沒有看這篇文章!

感謝大家的閱讀,也歡迎您把這篇文章分享給更多的朋友一起閱讀!謝謝!

路人甲java

你期望月薪4萬,出門右拐,不送,這幾個點,你也就是個初級的水平

▲長按圖片識別二維碼關注

路人甲Java:工作10年的前阿里P7分享Java、演算法、資料庫方面的技術乾貨!堅信用技術改變命運,讓家人過上更體面的生活!

相關文章