主頁 > 後端開發 > 深入理解Java泛型、協變逆變、泛型通配符、自限定

深入理解Java泛型、協變逆變、泛型通配符、自限定

2023-04-21 07:40:51 後端開發

禁止轉載

重寫了之前博客寫的泛型相關內容,全部整合到這一篇文章里了,把坑都填了,后續不再糾結這些問題了,本文深度總結了函式式思想、泛型對在Java中的應用,解答了許多比較難的問題,

  • 純函式
  • 協變
  • 逆變
  • 泛型通配符
  • PECS法則
  • 自限定

Part 1: 協變與逆變

Java8 引入了函式式介面,從此方法傳參可以傳遞函式了,有人說這是語法糖,

實際上,這是編程范式的轉換,思想體系的變化,

一、純函式—沒有副作用

純函式的執行不會帶來物件內部引數、方法引數、資料庫等的改變,這些改變都是副作用,比如Integer::sum是一個純函式,輸入為兩個int,輸出為兩數之和,兩個輸入量不會改變,在Java 中可以申明為final int型別,

副作用的執行

Java對于不變類的約束明顯不足,比如final array只能保證參考的指向不變,array內部的值還是可以改變的,如果存在第二個參考指向相同的array,那么將無法保證array不可變;標準庫中的collection常用的還是屬于可變mutable型別,可變型別在使用時很便利,

在函式式思想下,函式是一等公民,函式是有值的,比如Integer::sum就是函式型別BiFunction<Integer, Integer, Integer>的一個值,沒有副作用的函式保證了函式可以看做一個黑盒,一個固定的輸入便有固定的輸出,

那么Java中物件的方法是純函式嗎?

大多數時候不是,物件的方法受到物件的狀態影響,如果物件的狀態不發生改變,同時不對外部產生影響(比如列印字串),可以看做純函式,

本文之后討論的函式都默認為純函式,

二、協變—更抽象的繼承關系

協變和逆變描述了繼承關系的傳遞特性,協變比逆變更好理解,

協變的簡單定義:如果A是B的子類,那么F(A)是F(B) 的子類,F表示的是一種型別變換,

比如:貓是動物,表示為Cat < Animal,那么一群貓是一群動物,表示為List[Cat] < List[Aniaml],

上面的關系很好理解,在面向物件語言中,is-a表示為繼承關系,即貓是動物的子類(subtype),

所以,協變可以這樣表示:

A < B ? F(A) < F(B)

在貓的例子中,F表示集合,

那么如果F是函式呢?

我們定義函式F=Provider,函式的型別定義包括入參和出參,簡單地考慮入參為空,出參為Animal和Cat的情況,簡單理解為方法F定義為獲取貓或動物,

那么Supplier作用Cat和Animal上,原來的型別關系保持嗎?

答案是保持,Supplier[Cat] < Supplier[Animal],也就是說獲取一只貓就是獲取一只動物,轉換成面向物件的語言,Supplier[Cat]是Supplier[Animal]的子類,

在面向物件語言中,子類關系常常表現為不同型別之間的兼容,也就是說傳值的型別必須為宣告的型別的子類,如下面的代碼是好的

List[User] users = List(user1, user2)
List[Animal] animals = cats
Supplier[Animal] supplierWithAnimal = supplierWithCat
// 使用Supplier[Animal],實際上得到的是Cat
Animal animal = supplierWithAnimal.get()

我們來看下某百科對于里氏替換原則(LSP)的定義:

里氏代換原則(Liskov Substitution Principle LSP)面向物件設計的基本原則之一, 里氏代換原則中說,任何父類可以出現的地方,子類一定可以出現, LSP是繼承復用的基石,只有當子類可以替換掉父類,軟體單位的功能不受到影響時,父類才能真正被復用,而子類也能夠在父類的基礎上增加新的行為,里氏代換原則是對“開-閉”原則的補充,實作“開-閉”原則的關鍵步驟就是抽象化,而子類與父類的繼承關系就是抽象化的具體實作,所以里氏代換原則是對實作抽象化的具體步驟的規范,

Animal animal = new Cat(”kitty”);

在UML圖中,一般父類在上,子類在下,因此,子類賦值到父類宣告的程序可以形象地稱為向上轉型,

總結一下:協變是LSP的體現,形象的理解為向上轉型,

三、逆變—難以理解的概念

與協變的定義相反,逆變可以這樣表示:

A < B ? F(B) < F(A)

最簡單的逆變類是Consumer[T],考慮Consumer[Fruit] 和 Consumer[Apple],榨汁機就是一類Consumer,接受的是水果,輸出的是果汁,我定義的函式accpt為了避免副作用,回傳字串,然后再列印,

下面我用scala寫的示例,其比Java簡潔一些,也是靜態強型別語言,你可以使用網路上的 playground 運行(eg: scastie.scala-lang.org),

// scala 變數名在前,型別在后,函式回傳型別在括號后,可以省略
class Fruit(val name: String) {}

class Apple extends Fruit("蘋果") {}

class Orange extends Fruit("橙子") {}

// 榨汁機,T表示泛型,<:表示匹配上界(榨汁機只能榨果汁),-T 表示T支持逆變
class Juicer[-T <: Fruit] {
  def accept(fruit: T) = s"${fruit.name}汁"
}

val appleJuicer: Juicer[Apple] = Juicer[Fruit]()
println(appleJuicer.accept(Apple()))

// 編譯不通過,因為appleJuicer的型別是Juicer[Apple]
// 雖然宣告appleJuicer時傳遞的值是水果榨汁機,但是編譯器只做型別檢查,Juicer[Apple]型別不能接受其他水果
println(appleJuicer.accept(Orange()))

榨汁機 is-a 榨蘋果汁機,因為榨汁機可以榨蘋果,

逆變難以理解的點就在于逆變考慮的是函式的功能,而不是函式具體的引數,

引數傳參原則上都可以支持逆變,因為對于純函式而言,引數值并不可變,

再舉一個例子,Java8 中stream的map方法需要的引數就是一個函式:

// map方法宣告
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

// 此時方法的引數就是T,我們傳遞的mapper的入參可以為T的父類, 因為mapper支持引數逆變
// 如下程式可以運行
// 你可以對任意一個Stream<T>流使用map(Object::toString),因為在Java中所有類都繼承自Object,
Stream.of(1, 2, 3).map(Object::toString).forEach(System.out::println);

問題可以再復雜一點,如果函式的引數為集合型別,還可以支持逆變嗎?

當然可以,如前所述,逆變考慮的是函式的功能,傳入一個更為一般的函式也可以處理具體的問題,

// Scala中可以使用 ::: 運算子合并兩個List, 下一行是List中對方法:::的宣告
// def ::: [B >: A](prefix: List[B]): List[B]
// 這個方法在Java很難實作,你可以看看ArrayList::addAll的引數, 然后想想曲線救國的方案,下一篇文章我會詳細討論

// usage
val list: List[Fruit] = List(Apple()) ::: (List(Fruit("水果")))
println(list)
// output: List(Playground$Apple@74046e99, Playground$Fruit@8f0fecd)

總結一下:函式的入參可以支持逆變,即引數的繼承關系和函式的繼承關系相反,逆變的函式更通用,

Part 2: 深入理解泛型

上次說到函式入參支持協變,出參支持逆變,那么Java中是如何實作支持的?

一切都可以歸因于Java的前向兼容,Java泛型是一個殘缺品,不過也可以解決大量的泛型問題,

Java中物件宣告并不支持協變和逆變,所以我們看到的函式介面宣告如下:

// R - Result
@FunctionalInterface
public interface Function<T, R> {
    // 1. 函式式介面
    R apply(T t);

    // 2. compose 和 andThen 實作函式復合
    // compose 的入參函式 before 支持入參逆變,出參協變
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    // Java9 支持的靜態方法
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

Java中僅在使用時支持逆變與協變的匹配,可以在方法上使用通配符,也就是說,andThen方法接受的引數支持入參逆變、出參協變,不使用通配符則為不變,在IDEA中可以開啟通配符的提示,很有用,一般情況下,撰寫時可以考慮不變,然后再考慮增加逆變與協變的支持,

但是Java中通配符使用了和繼承相關的super、 extends 關鍵字,而實際協變與逆變和繼承沒有關系,在scala中協變和逆變可以簡單地寫作+和-,比如宣告List[+T],

通配符繼承了Java一貫的繁瑣,函式宣告更甚,函式的入參和出參都在泛型引數中,Function<T, R> 和 T → R 相比誰更簡潔一目了然,特別是定義高階函式(入參或出參為函式的函式)更為麻煩,比如一個簡單的加法:

// Java 中的宣告,可以這樣考慮:Function泛型引數的右邊為回傳值
Function<Integer, Function<Integer, Integer>> add;
// 使用時連續傳入兩個引數
add.apply(1).apply(2);

// 其他語言
val add : Int -> Int -> Int = x -> y -> x + y
add(1)(2)

// 傳入 tuple 的等價形式 Java
Function<Tuple<Integer, Integer>, Integer> add = (x, y) -> x + y;
add.apply(new Tuple(1, 2));

BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
add.apply(1, 2);

// 其他語言
val add: (Int, Int) -> Int = x + y
add(1, 2)

image

從上面可以看出,雖然實作的是相同的語意,Java對函式的支持還是有明顯不足的,沒有原生的Tuple型別,但是在使用時又可以使用 (x, y),

話雖如此,畢竟可以實作相同的功能,豐富的類別庫加之方法參考、lambda運算式等的存在,Java中使用函式式編程思想可以說是如虎添翼,

三人成虎

理解函式式思想實際上只需要了解三種函式式介面,生產者、函式、消費者,只有生產者和消費者可以有副作用,函式就是純函式,

Function<T, R>

public interface Supplier<T> {
    T get();
}

public interface Consumer<T> {
    void accept(T t);
		// 多次消費合并為一次
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

函式式編程將操作都通過鏈式呼叫連接起來,

Supplier → Func1 → … → Funcn → Consumer

比如stream流的整個生命周期,只消費一次,

// Stream
Stream.of("apple", "orange")
		.map(String::toUpperCase)
		.forEach(System.out::println);

// reactor, 簡單理解為stream++, 支持異步 + 背壓
Flux.just(1, 2, 3, 4)
	  .log()
	  .map(i -> i * 2)
	  .zipWith(Flux.range(0, Integer.MAX_VALUE), 
	    (one, two) -> String.format("First Flux: %d, Second Flux: %d", one, two))
	  .subscribe(elements::add);

assertThat(elements).containsExactly(
	  "First Flux: 2, Second Flux: 0",
	  "First Flux: 4, Second Flux: 1",
	  "First Flux: 6, Second Flux: 2",
	  "First Flux: 8, Second Flux: 3");

常見的使用舉例

  1. Comparable

舉例來說,實作 集合類的sort方法,方法簽名如下:

// 最簡單的宣告
public static <T> void sort(Collection<T> col);

// 加入可比較約束,編譯器檢查:如果沒有實作Comparable,則編譯不通過
public static <T extends Comparable<T>> void sort(Collection<T> col);

// 使用通配符匹配更多使用場景,大多數類別庫都是這樣宣告的,缺點是看起了比較繁瑣
// 其實只需要理解了函式的入參逆變,出參協變的準則,關注extends、super后面的型別即可理解
public static <T extends Comparable<? super T>> void sort(Collection<T> col);
  1. Stream

這個方法宣告在Stream介面中,可以把Stream<Stream>展開,

public interface Stream<T> extends BaseStream<T, Stream<T>> {

		Stream<T> filter(Predicate<? super T> predicate);

		<R> Stream<R> map(Function<? super T, ? extends R> mapper);

		// flatMap 把 Stream<Stream<T>> 展開,也有叫 bind 的,
		<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
}

可以看看flatMap中mapper的回傳型別,完美遵循出參協變和集合類支持協變的特性,

你看,本來Stream就應該支持協變,現在只能在使用時(方法宣告時)使用通配符表示??,

  1. Collections工具類
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key)

和例子1相比,通配符在List里,也可以放在靜態方法的泛型引數宣告上?


還可以觀察到方法宣告的回傳值一般都不使用通配符,而是指定的型別(T,R,U),這樣方便user使用,同時避免誤用,在使用時,只需要考慮方法和引數之間的匹配,不需要考慮宣告集合類對于協變和逆變的支持:

// 盡量不要有這樣的代碼,基本沒啥卵用,還把問題復雜化了,這些復雜化的問題盡量放到方法中,
List<? extends User> userList = ...

PECS法則

說了這么多,好像也沒有提到PECS法則(provider- extends, consumer-super ),也有叫 The Get and Put Principle 的,其實,函式式思想中所有的類都是不可變的,對于一個不可變的類remove,add等操作并不會改變原有物件的值,而是回傳一個新物件,所以就沒有PECS這樣的約束,

那么在Java中是怎樣的?

Java集合類的設計大多都是以命令式編程的角度,實作了集合類的增刪改查,這種類物件天生就是有狀態的,Stream可以簡單理解為函式式中不變的集合,沒有內部狀態,或者說只有一種狀態,再比如String就是沒有狀態的,“apple”永遠是”apple”,

對于可變集合物件的增刪改查,適用于PECS法則,

PECS法則可以理解為extends通配符的Collection為provider,只讀;super通配符下的Collection為consumer,只寫,

// 只讀,求和
public static double sum(Collection<? extends Number> nums) {
		double s = 0.0;
		for (Number num : nums) s += num.doubleValue();
		return s;
}

// 只寫,陣列 --> 集合類
@SafeVarargs
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
    boolean result = false;
    for (T element : elements)
        result |= c.add(element);
    return result;
}

// 讀 + 寫,參見 Collections
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

上面三個例子,只有第一個方法是純函式,

總之,要注意區分兩種編程范式,命令式和函式式思想的差別,它們在泛型中都有具體的應用,Java對于函式式思想有很多借鑒的地方,特別是從Java8之后,如果想學好泛型,不妨了解一下其他語言對于泛型的實作,這樣可以有一個整體的認識,而不是學習許多corner case(eg: 泛型擦除、物化、varargs、@SuppressWarnning),

從使用者的角度,泛型可以很容易,但是從撰寫者的角度,泛型可以很復雜,特別是在Java中,

Part 3: 自限定與協變override

class A<T extends A> {}
class B extends A<B> {}

自限定類的泛型引數包含自己,類A就是一個自限定類(SelfBound),類B繼承自A,也是自限定類,

這樣的好處是一個方法的入參和出參都可以支持自限定型別,

interface ServiceA {
    Iterable<String> selectAll();
}

class ServiceAImpl implements ServiceA{
    @Override
    public List<String> selectAll() {
        return new ArrayList<>();
    }
}

class ComplicateServiceA extends ServiceAImpl {
    @Override
    public CopyOnWriteArrayList<String> selectAll() {
        // 父類回傳父類方法 selectAll 對應的型別 List<String>
        List<String> all = super.selectAll();
        return new CopyOnWriteArrayList<>(all);
    }
}

interface ServiceB {
    Collection<String> selectAll();
}

class ServiceBImpl implements ServiceB {
    @Override
    public List<String> selectAll() {
        return Collections.emptyList();
    }
}

型別繼承中方法支持重寫,回傳型別支持協變,從以上代碼可以看出,ServiceA 定義獲取所有資料的方法,其子類(不管是子類還是子介面)都可以重寫方法回傳型別:

ServiceA 定義的方法回傳型別是 Iterable,而 ComplicateServiceA 重寫了該方法并回傳了 CopyOnWriteArrayList,是 Iterable 的子型別,符合協變的規定,同樣的,ServiceB 定義的方法回傳型別為 Collection,而 ServiceBImpl 重寫該方法并回傳了 List,也是 Collection 的子型別,同樣符合協變的規定,

根據之前講的函式的類關系“入參逆變,出參協變”來說,子類重寫父類方法只滿足了后半句,而 Java 對于入參逆變這一條并不支持,會被當做多載(overload)處理,

有時甚至相反,我們希望“支持協變”,實際上入參可以為型別引數(Type Parameter: T),對于自限定型別,T也就是自己,請看下例:

SupperBuilder

// lombok
@Builder
@Data
class POJO1 {
    String id; 
    // 僅做示例,省略其他欄位
}

@Builder
@Data
class POJO2 extends POJO1 {
    String note;
}

class App {
    public static void main(String[] args) {
        // 簡單的 builder 實作
        POJO2 b = POJO2.builder()
                // 不包含id(String id)方法
                .note("this is pojo b").build();
        b.setId("001");
        // ...
        // 還有個問題:沒有默認構造器了,解決方法:重寫構造器
    }
}

復雜物件的創建常常使用 builder 模式,builder 設計模式實作的基本思路是:

  1. builder方法創建builder物件
  2. 被創建物件的引數分多次方法傳入,回傳值為 builder 自己
  3. 最終 build 方法呼叫全引數構造方法獲取實體,

以上代碼使用 @Builder 注解無法與繼承體系良好兼容,若想良好地將未最終定義的引數傳遞下去,builder的回傳引數就應該是可拓展的builder自己,自己可拓展自己,不就是我們說的自限定泛型嗎,請看 @SuperBuilder 實作

@Data
@SuperBuilder
class POJO1 {
    String id;

    public POJO1() {
    }
}

@Data
@SuperBuilder
class POJO2 extends POJO1 {
    String note;

    public POJO2(){
    }
}

class App {
    public static void main(String[] args) {
        // 正常運行,不過此處僅做示例,@SuperBuilder 還在 experimental 階段,生產勿用
        POJO2 b = POJO2.builder()
                .id("001")
                .note("this is pojo b")
                .build();
        POJO1 a1 = POJO1.builder()
                .id("a")
                .build();
        POJO1 a2 = new POJO1();
    }
}

這個實作沒有問題,且看delombok @SuperBuilder 的結果:

@Data
class POJO1 {
    String id;

    public POJO1() {
    }

    // builder -> build() -> 獲取示例
    protected POJO1(POJO1Builder<?, ?> b) {
        this.id = b.id;
    }

    // 獲取自限定 builder
    public static POJO1Builder<?, ?> builder() {
        return new POJO1BuilderImpl();
    }

    // 請仔細理解此處泛型引數的含義
    public static abstract class POJO1Builder<C extends POJO1, B extends POJO1Builder<C, B>> {
        private String id;

        public B id(String id) {
            this.id = id;
            return self();
        }

        // 以下兩個方法理解為builder生命周期下的回呼函式/鉤子
        // 獲取自己,只因在繼承體系下使用 self 獲取的型別不完全
        protected abstract B self();

        public abstract C build();

        public String toString() {
            return "POJO1.POJO1Builder(id=" + this.id + ")";
        }
    }

    // 實作類:指定泛型引數+回呼實作
    private static final class POJO1BuilderImpl extends POJO1Builder<POJO1, POJO1BuilderImpl> {
        private POJO1BuilderImpl() {
        }

        protected POJO1BuilderImpl self() {
            return this;
        }

        public POJO1 build() {
            return new POJO1(this);
        }
    }
}

分析一下型別宣告 class POJO1Builder<C extends POJO1, B extends POJO1Builder<C, B>>, 第一個泛型引數為C,表示被創建物件型別,這里似乎叫自限定也不太合適,畢竟創建者和被創建者不一樣,其為繼承鏈上的被創建物件;第二個泛型引數為B,表示創建者型別,是自限定型別,

再來看子類的實作:

@Data
class POJO2 extends POJO1 {
    String note;

    public POJO2(){
    }

    protected POJO2(POJO2Builder<?, ?> b) {
        super(b);
        this.note = b.note;
    }

    public static POJO2Builder<?, ?> builder() {
        return new POJO2BuilderImpl();
    }

    public static abstract class POJO2Builder<C extends POJO2, B extends POJO2Builder<C, B>> extends POJO1Builder<C, B> {
        private String note;

        public B note(String note) {
            this.note = note;
            return self();
        }

        // 這兩個方法實際上不需要重新定義了
        protected abstract B self();

        public abstract C build();

        public String toString() {
            return "POJO2.POJO2Builder(super=" + super.toString() + ", note=" + this.note + ")";
        }
    }

    private static final class POJO2BuilderImpl extends POJO2Builder<POJO2, POJO2BuilderImpl> {
        private POJO2BuilderImpl() {
        }

        protected POJO2BuilderImpl self() {
            return this;
        }

        public POJO2 build() {
            return new POJO2(this);
        }
    }

}

子類實作實際上和父類基本一致,

有一些需要注意的點:

  • 子類并不需要父類的 BuilderImpl

  • builder 組成的繼承鏈可以無限延長,每一個鏈子節點都包含對應被創建物件對應類的欄位值,

  • 在構造器方法中需要呼叫父類構造方法

SpringSecurity配置

對于初學者,SpringSecurity 的配置可能有些繁瑣,其支持多種驗證、鑒權及網路安全相關的配置,其提供的 DSL 配置對于初學者可能過于復雜,如果你覺得太難,可以跳過這一部分,

以下為一個配置示例:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/secured/**").authenticated()
                .and()
            .httpBasic()
                .and()
            .csrf().disable();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user")
                .password("{noop}password")
                .roles("USER");
    }
}

這是一個安全配置類,使用了@EnableWebSecurity注解,繼承了WebSecurityConfigurerAdapter抽象類,并復寫了其中的兩個方法,這種配置的缺點是沒有集成到 Spring bean 的注入,所以還需要額外的自動配置類,5.7.0-M2推薦使用基于bean的配置,

在configure(HttpSecurity http)方法中,配置了訪問的權限規則,對于以“/public/”開頭的請求路徑,允許所有訪問,對于以“/secured/”開頭的請求路徑,需要認證通過才能訪問,同時,啟用了HTTP Basic方式的認證方式,并禁用了跨站請求偽造(CSRF)保護,

在configureGlobal(AuthenticationManagerBuilder auth)方法中,配置了一個基于記憶體的認證用戶,這個用戶的用戶名為“user”,密碼為“password”,擁有“USER”角色,

這個安全配置類配置了用戶登錄和訪問權限,允許公開和受保護的訪問,并驗證認證用戶的資訊,

我們來分析一下其原始碼,就會發現自限定型別經常出現,

image.png

從官網的這張圖可以看出,Spring Security 基于 Servlet Filter 實作,

簡單來說,我們的配置實際上就是配置了各個過濾器及其內部組件,

上面我們提到的configure方法就是配置了過濾器及其組件,http物件可以配置一條過濾器鏈,對于過濾器上單獨的節點(過濾器),可以單獨配置,也可以回到http上再去配置其他的節點(and方法),

從代碼功能抽象的角度考慮,這時的節點具有builder的功能(叫做configurer),同時支持鏈式配置,具有 and 方法回傳鏈路 builder,最終的build功能由鏈路實作,

可以看出,節點依賴于鏈路,鏈路由節點組成,為了拓展性,定義的節點builder支持自限定,如果鏈路支持繼承,and方法回傳物件必須支持協變,鏈路支持協變,所以在設計上鏈路也要支持自限定,

節點builder在 Spring Security 實作中叫做 Configurer, 鏈路builder叫做 Builder,

下面來看下builer和configurer如何關聯:

// builder類
public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>> extends AbstractSecurityBuilder<O> {
    // 持有多個configurers,待遍歷呼叫
    private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers;
    // 略去其他
    // build()方法流程(生命周期)
    protected final O doBuild() throws Exception {  
        synchronized (this.configurers) {  
        this.buildState = BuildState.INITIALIZING;  
        beforeInit();  
        init();  
        this.buildState = BuildState.CONFIGURING;  
        beforeConfigure();  
        configure();  
        this.buildState = BuildState.BUILDING;  
        O result = performBuild();  
        this.buildState = BuildState.BUILT;  
        return result;  
        }
    }
    
    // 你可以把這里的 apply 理解為 register
    public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
        configurer.addObjectPostProcessor(this.objectPostProcessor);  
        configurer.setBuilder((B) this);  
        add(configurer);  
        return configurer;  
    }
}

// configurer 介面定義,用來配置 builder
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {  
    void init(B builder) throws Exception;  
    void configure(B builder) throws Exception;  
}

// configurer 基本實作,為子類提供實作便利
public abstract class SecurityConfigurerAdapter<O, B extends SecurityBuilder<O>> implements SecurityConfigurer<O, B> {  
    private B securityBuilder;  

    // 默認實作為空
    @Override  
    public void init(B builder) throws Exception {  
    }  

    @Override  
    public void configure(B builder) throws Exception {  
    }  

    public B and() {  
        return getBuilder();  
    }  
    protected final B getBuilder() {  
        Assert.state(this.securityBuilder != null, "securityBuilder cannot be null");  
        return this.securityBuilder;  
    }
    // 忽略其他代碼:ObjectPostProcessor支持
}

以上原理基本分析完畢,我們來看一些實作:

// configurer
// 配置了驗證邏輯(用戶登錄)
// 自限定,雖然型別宣告看上去復雜,第二個泛型引數即定義了自限定
public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter>  
extends AbstractHttpConfigurer<T, B>{
    // 大部分內容略
    // 配置,回傳self
    public final T defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) {  
        SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();  
        handler.setDefaultTargetUrl(defaultSuccessUrl);  
        handler.setAlwaysUseDefaultTargetUrl(alwaysUse);  
        this.defaultSuccessHandler = handler;  
        return successHandler(handler);  
    }
    public final T successHandler(AuthenticationSuccessHandler successHandler) {  
        this.successHandler = successHandler;  
        return getSelf();  
    }
}

// builder: http
// HttpSecurityBuilder 定義了自限定
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>  
implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity>{
    // 略,這個類有3000多行??
} 
public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>  
extends SecurityBuilder<DefaultSecurityFilterChain> {  
    <C extends SecurityConfigurer<DefaultSecurityFilterChain, H>> C getConfigurer(Class<C> clazz); 
    <C extends SecurityConfigurer<DefaultSecurityFilterChain, H>> C removeConfigurer(Class<C> clazz);  
    <C> void setSharedObject(Class<C> sharedType, C object);  
    <C> C getSharedObject(Class<C> sharedType);  
    H authenticationProvider(AuthenticationProvider authenticationProvider);  
    H userDetailsService(UserDetailsService userDetailsService) throws Exception;
    H addFilterAfter(Filter filter, Class<? extends Filter> afterFilter);  
    H addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter);  
    H addFilter(Filter filter);   
}

除了這條鏈路之外,還有用戶登錄驗證鏈路和 WebSecurity,基本思路大同小異,缺點是又有很多類,

// 配置類
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
            .contextSource() // 又進入了一個 configurer
                .url(ldapProperties.getUrls()[0] + StrUtil.SLASH + ldapProperties.getBase())
                .managerDn(ldapProperties.getUsername())
                .managerPassword(ldapProperties.getPassword())
            .and() // 這個and回傳到了 LDAP configurer
            .userDetailsContextMapper(customLdapUserDetailsMapper)
            .userSearchBase(extendLdapProperties.getSearchBase())
            .userSearchFilter(extendLdapProperties.getSearchFilter())
            .and()
            .userDetailsService(userDetailsService())
            .passwordEncoder(new BCryptPasswordEncoder());
    }
}
// 自限定Builder, Provider 這個名字起得不好,應該叫做 Authenticator
public interface ProviderManagerBuilder<B extends ProviderManagerBuilder<B>> extends
      SecurityBuilder<AuthenticationManager> {
   B authenticationProvider(AuthenticationProvider authenticationProvider);
}
// 上面介面的實作
public class AuthenticationManagerBuilder
      extends
      AbstractConfiguredSecurityBuilder<AuthenticationManager, AuthenticationManagerBuilder>
      implements ProviderManagerBuilder<AuthenticationManagerBuilder> {
    // 略去實作 + 部分介面
    public AuthenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor){}
    // 感覺這里有些復雜,可能有特殊考慮,大概思路是:遍歷authencator處理,都沒有成功的話,就使用parentAuthenticator處理,
    // 詳細可參看官方檔案:Spring Security # Servlet Authentication Architecture
    public AuthenticationManagerBuilder parentAuthenticationManager(
      AuthenticationManager authenticationManager){}
    // 回傳值為configurer
    public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication(){}
    public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication(){}
}

// 還有子類繼承上面的類,又形成了一條繼承鏈,這里就不分析了,同http的分析一樣,

總結

最后總結一下:

  1. 純函式沒有副作用

  2. Java 中使用通配符在使用時確定繼承關系

  3. 協變使集合保留繼承關系

  4. 逆變常常應用于函式入參匹配

  5. 入參逆變,出參協變

  6. 集合類比較少使用 super 通配符,因為通常會失去型別資訊(當作Object使用)

  7. 自限定保證子類獲取自己,自己可以作為方法的引數或回傳值

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/550641.html

標籤:其他

上一篇:PHP 教程_編程入門自學教程_菜鳥教程-免費教程分享

下一篇:返回列表

標籤雲
其他(157684) Python(38083) JavaScript(25376) Java(17984) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • 深入理解Java泛型、協變逆變、泛型通配符、自限定

    禁止轉載 重寫了之前博客寫的泛型相關內容,全部整合到這一篇文章里了,把坑都填了,后續不再糾結這些問題了。本文深度總結了函式式思想、泛型對在Java中的應用,解答了許多比較難的問題。 純函式 協變 逆變 泛型通配符 PECS法則 自限定 Part 1: 協變與逆變 Java8 引入了函式式介面,從此方 ......

    uj5u.com 2023-04-21 07:40:51 more
  • PHP 教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 PHP開發入門教程 - 一個簡單而簡短的PHP教程和所有內置PHP函式的完整參考手冊。本教程是為初學者和高級開發人員設計的。您將了解PHP內置函式,預定義變數示例,面向物件的PHP,數字,標量,陣列,散列檔案I / O,IF,ELSEIF,執行,回圈,運算子,正則運算式,GET,POST, ......

    uj5u.com 2023-04-21 07:40:46 more
  • java -- 函式式編程

    函式式編程 面向物件過分強調“必須通過物件的形式來做事情”,而函式式思想則盡量忽略面向物件的復雜語法——強調做什么,而不是怎么做。 有時只是為了做某事情而不得不創建一個物件,而傳遞一段代碼才是我們真正的目的。 Lambda Lambda是一個匿名函式,可以理解為一段可以傳遞的代碼。 當需要啟動一個線 ......

    uj5u.com 2023-04-21 07:40:40 more
  • UML類圖

    UML類圖介紹 概念 UML中的類圖(Class Diagram)用于表示類、介面、實體等之間相互的靜態關系。雖然名字叫作類圖,但是圖中并不僅僅只有類。 類結構 繼承 該圖展示了Parentclass和Childclass兩個類之間的關系,其中的空心箭頭表明了兩者之間的層次關系。箭頭由子類指向父類, ......

    uj5u.com 2023-04-21 07:40:27 more
  • 關于Java中物件的向上轉型和向下轉型

    什么是多型? 同一個類呼叫同一個方法會產生不同的影響/結果 這就是多型 public class Pet{ public void eat(){ System.out.println("Pet eat...") } } class Dog extends Pet{ public void eat() ......

    uj5u.com 2023-04-21 07:40:19 more
  • Flask 背景關系是什么 ?

    哈嘍大家好,我是咸魚。今天我們來聊聊什么是 Flask 背景關系 咸魚在剛接觸到這個概念的時候腦子里蹦出的第一個詞是 CPU 背景關系 今天咸魚希望通過這篇文章,讓大家能夠對 Flask 背景關系設計的初衷以及應用有一個基本的了解 Flask 背景關系 我們在使用 Flask 開發 web 程式的時候,通常 ......

    uj5u.com 2023-04-21 07:40:08 more
  • 【0基礎學爬蟲】爬蟲基礎之自動化工具 Selenium 的使用

    大資料時代,各行各業對資料采集的需求日益增多,網路爬蟲的運用也更為廣泛,越來越多的人開始學習網路爬蟲這項技術,K哥爬蟲此前已經推出不少爬蟲進階、逆向相關文章,為實作從易到難全方位覆寫,特設【0基礎學爬蟲】專欄,幫助小白快速入門爬蟲,本期為自動化工具 Selenium 的使用。 概述 目前,很多網站都 ......

    uj5u.com 2023-04-21 07:39:47 more
  • 關于Java中方法多載和方法重寫

    方法重寫是子類繼承父類(默認繼承Object類)后覆寫父類的方法 需要保證同名 同參 同回傳值 且訪問權限范圍不能縮小(public>protected>default>private) public class Father{ public int method(){ return -1; } } ......

    uj5u.com 2023-04-21 07:39:34 more
  • Go語言入門8(匿名函式 閉包)

    匿名函式 閉包 匿名函式 ? 顧名思義,就是沒有名字的函式。。。 func(){ fmt.Println("我就是匿名函式") } 匿名函式的兩種執行方法 將匿名函式賦給一個變數 定義后立即執行匿名函式 // 講匿名函式賦給一個變數 tmp := func(){ fmt.Println("我是匿名函 ......

    uj5u.com 2023-04-21 07:39:30 more
  • Opencv在VS2022中的配置(Python)

    下載Opencv 先去官網https://opencv.org/opencv-4-7-0/下載, 找到適合你設備的版本下載Windows就是Win pack,完成后進行安裝即可,一路同意默認就行,可以更改安裝位置,但路徑上盡可能以英文,以防止后面不必要的問題。 2.下載Python 首先是版本 發文 ......

    uj5u.com 2023-04-21 07:38:40 more