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

这篇具有很好参考价值的文章主要介绍了深入理解Java泛型、协变逆变、泛型通配符、自限定。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

禁止转载

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

深入理解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)

深入理解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”角色。

这个安全配置类配置了用户登录和访问权限,允许公开和受保护的访问,并验证认证用户的信息。

我们来分析一下其源码,就会发现自限定类型经常出现。

深入理解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. 自限定保证子类获取自己,自己可以作为方法的参数或返回值文章来源地址https://www.toymoban.com/news/detail-419647.html

到了这里,关于深入理解Java泛型、协变逆变、泛型通配符、自限定的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 了解java中的通配符“?“

    目录 通配符的作用         先看一段代码         用通配符\\\"?\\\"后,代码变化         结论 通配符上界  通配符下界 对通配符上下界的注释理解及其练习代码   简记: ?  用于在泛型的使用,即为通配符. 在Java中,通配符(wildcard)主要用于泛型编程,用于表示一个

    2024年02月10日
    浏览(54)
  • 数据结构(Java实现)-字符串常量池与通配符

    字符串常量池 在Java程序中,类似于:1, 2, 3,3.14,“hello”等字面类型的常量经常频繁使用,为了使程序的运行速度更快、更节省内存,Java为8种基本数据类型和String类都提供了常量池。 “池” 是编程中的一种常见的, 重要的提升效率的方式, 我们会在未来的学习中遇到各

    2024年02月10日
    浏览(50)
  • 【JavaSE】Java进阶知识一(泛型详解,包括泛型方法,协变,逆变,擦除机制)

    目录 泛型 1. 什么是泛型 2.泛型方法 3.通配符上界(泛型的协变) 4.通配符下界(泛型的逆变) 5.泛型的编译(擦除机制)         泛型:就是让一个类能适用于多个类型,就是在封装数据结构时能让封装的类型被各种类型使用所以引入了泛型的概念,虽然有了泛型,什么数

    2024年02月04日
    浏览(47)
  • Elasticsearch 通配符查询

    通配符查询(wildcard query) 匹配字段被通配符表达式(没有被分析)匹配的文档。支持的通配符为*(匹配任意字符序列,包括空字符序列)以及?(匹配任意单字符)。注意,此查询可能会很慢,它需要迭代许多字段值。为了防止极慢的通配符匹配,通配符字段值不能以一个

    2024年02月11日
    浏览(86)
  • Linux详解:通配符

    Linux是一款开源操作系统,其灵活性和可定制性一直受到开发者的喜爱和追捧。而且,Linux在文件管理方面提供了丰富的功能,例如通配符,它是一种用于匹配文件名的特殊字符。通配符在Linux中可以帮助我们更加方便和快捷地查找和操作文件。本文将介绍Linux中常用的通配符

    2024年02月09日
    浏览(61)
  • 【类型通配符】

    为了表示各种泛型List的父类,可以使用类型通配符 类型通配符:? List?:表示元素类型未知的List,它的元素可以匹配任何的类型 这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加到其中 如果不想让List?是任何泛型的父类,只想让它代表某一类泛型List的父

    2024年02月17日
    浏览(48)
  • 活用 命令行通配符

    本文是对 阮一峰老师 命令行通配符教程 [1] 的学习与记录 通配符早于正则表达式出现,可以看作是原始的正则表达式. 其功能没有正则那么强大灵活,而胜在简单和方便. - 字符 切回上一个路径/分支 如图: !! 代表上一个命令, 如图: [Linux中“!\\\"的神奇用法](https://www.cnblogs.com/bian

    2024年02月10日
    浏览(57)
  • 【算法题】44. 通配符匹配

    给你一个输入字符串 (s) 和一个字符模式 (p) ,请你实现一个支持 \\\'?\\\' 和 \\\'*\\\' 匹配规则的通配符匹配: \\\'?\\\' 可以匹配任何单个字符。 \\\'*\\\' 可以匹配任意字符序列(包括空字符序列)。 判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。  

    2024年01月25日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包