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

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

2023-04-21 07:18:22 後端開發

禁止轉載

重寫了之前博客寫的泛型相關內容,全部整合到這一篇文章里了,把坑都填了,后續不再糾結這些問題了,本文深度總結了函式式思想、泛型對在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/550619.html

標籤:Java

上一篇:java -- 函式式編程

下一篇:返回列表

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17978) 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:18:22 more
  • java -- 函式式編程

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

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

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

    uj5u.com 2023-04-21 07:14:54 more
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more