禁止轉載
重寫了之前博客寫的泛型相關內容,全部整合到這一篇文章里了,把坑都填了,后續不再糾結這些問題了,本文深度總結了函式式思想、泛型對在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)
從上面可以看出,雖然實作的是相同的語意,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");
常見的使用舉例
- 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);
- 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
- 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
根據之前講的函式的類關系“入參逆變,出參協變”來說,子類重寫父類方法只滿足了后半句,而 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 設計模式實作的基本思路是:
- builder方法創建builder物件
- 被創建物件的引數分多次方法傳入,回傳值為 builder 自己
- 最終 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”角色,
這個安全配置類配置了用戶登錄和訪問權限,允許公開和受保護的訪問,并驗證認證用戶的資訊,
我們來分析一下其原始碼,就會發現自限定型別經常出現,
從官網的這張圖可以看出,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的分析一樣,
總結
最后總結一下:
-
純函式沒有副作用
-
Java 中使用通配符在使用時確定繼承關系
-
協變使集合保留繼承關系
-
逆變常常應用于函式入參匹配
-
入參逆變,出參協變
-
集合類比較少使用 super 通配符,因為通常會失去型別資訊(當作Object使用)
-
自限定保證子類獲取自己,自己可以作為方法的引數或回傳值
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/550641.html
標籤:其他
上一篇:PHP 教程_編程入門自學教程_菜鳥教程-免費教程分享
下一篇:返回列表
