制止转载

重写了之前博客写的泛型相关内容,全部整合到这一篇文章里了,把坑都填了,后续不再纠结这些问题了。本文深度总结了函数式思维、泛型对在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是承继复用的柱石,只要当子类能够替换掉父类,软件单位的功用不受到影响时,父类才干真实被复用,而子类也能够在父类的基础上添加新的行为。里氏代换准则是对“开-闭”准则的补充。完成“开-闭”准则的关键步骤便是笼统化。而子类与父类的承继联系便是笼统化的具体完成,所以里氏代换准则是对完成笼统化的具体步骤的标准。

【Final】深入理解Java泛型、协变逆变、泛型通配符、自限定

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)

【Final】深入理解Java泛型、协变逆变、泛型通配符、自限定

从上面能够看出,尽管完成的是相同的语义,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”人物。

这个安全装备类装备了用户登录和拜访权限,答应公开和受维护的拜访,并验证认证用户的信息。

咱们来剖析一下其源码,就会发现自限制类型经常呈现。

【Final】深入理解Java泛型、协变逆变、泛型通配符、自限定

从官网的这张图能够看出,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. 自限制确保子类获取自己,自己能够作为办法的参数或回来值