????撸源代码破冰杀手锏(三):SPI机制 | 自定制starter自动配置组件|SpringSecurity安全框架认知

2023-12-14 20:45:05

一: 禅悟人生,码砖破冰感悟





二: Spring Security安全阐述

? ? ? 2.1:?Spring 是非常流行和Java应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括两个核心部分:

? ? ? ? ?(1)Authentication(认证):认证主要是验证某个用户是否为系统中的合法主体,从而进一步明确该主体在该系统中能够访问的资源(服务|接口|功能版块等系统事先预设好的资源)

? ? ? ? ? (2)Authorization(授权):授权就是用户认证通过后,拥有访问该系统那些资源,认证是授权的前提,只有认证通过后,才能明确该用户拥有访问系统的那些资源;

? ? ? 2.2 首先来看下Authentication认证接口的源代码:

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

public interface Authentication extends Principal, Serializable {
?   // 权限集合
    Collection<? extends GrantedAuthority> getAuthorities();  
    // 凭证
    Object getCredentials();  
    // 详情
    Object getDetails(); 
    // 认证主体
    Object getPrincipal(); 
    // 认证是否通过
    boolean isAuthenticated();  
    // 设置认证方法
    void setAuthenticated(boolean var1) throws IllegalArgumentException; 
}

2.3 引入Sping Security 三脚架依赖,搭建项目体验下Spring Security框架默认配置项带来的便利

引入依赖:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!---SpringSecurity权限认证相关 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

编写一个HTTP服务暴露端点:


package org.jd.auth.data.security.server.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Spring Security默认权限配置
 */
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String getHello(){
        return "你好,欢迎来到Spring Security安全控制框架!";
    }
}

2.4 访问该f服务端点:http://127.0.0.1:9911/auth/hello? 自动跳转到下面的默认登录页面,该页面是Spring Security自带的登录页;


可见引入该Spring Security依赖,会自动给项目中的端点服务添加认证机制; 默认的登录名时user,登录密码是随机生成的UUID字符串,在项目启动日志中可以看到该密码(这也意味着每次启动项目都会重新生成);输入用户及密码即可登录,认证通过后就会看到访问/hello端点服务信息:


这就是Spring Security 强大之处,只需要引入一个依赖,所有的接口就会自动被保护起来;

下面我们在配置文件中来配置定制化的用户名及固定密码,如下图所示:
?


?我们来重点关注下面红色标注的代码,配置user.name及user.password这两个属性,框架时怎样使用定制化的属性覆盖默认的配置的呢,首先从源代码来着手;首先我们来看与配置文件对应的属性文件SecurityProperties,如下,@ConfigurationProperties 主键标注的类,会自动被Spring framework框架识别注册为IOC容器的bean,根据配置文件的相关属性给对应实体设置对应的值; 在SecurityProperties类中的内部静态内部类的user和password属性,该属性user的默认初始值为user,password为默认随机字符串; 这就是我们在启动控制台看到的默认输出密码;?


@ConfigurationProperties(
    prefix = "spring.security"
)
public class SecurityProperties {
    public static final int BASIC_AUTH_ORDER = 2147483642;
    public static final int IGNORED_ORDER = -2147483648;
    public static final int DEFAULT_FILTER_ORDER = -100;
    private final SecurityProperties.Filter filter = new SecurityProperties.Filter();
     
    private SecurityProperties.User user = new SecurityProperties.User();

    public SecurityProperties() {
    }

    public SecurityProperties.User getUser() {
        return this.user;
    }

    public SecurityProperties.Filter getFilter() {
        return this.filter;
    }

    public static class User {
        private String name = "user";
        private String password = UUID.randomUUID().toString();
        private List<String> roles = new ArrayList();
        private boolean passwordGenerated = true;

        public User() {
        }

        public String getName() {
            return this.name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getPassword() {
            return this.password;
        }
        // 当我们在配置文件中配置了该属性时,就会去覆盖自动生成的密码,使用我们定制的密码
        public void setPassword(String password) {
            if (StringUtils.hasLength(password)) {
                this.passwordGenerated = false;
                this.password = password;
            }
        }

        ............................省略部分非关键代码..........................
    }

    public static class Filter {
        private int order = -100;
        private Set<DispatcherType> dispatcherTypes;
        ............................省略部分非关键代码..........................
}

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
/** 该注解注意是标注配置类与配置文件对应前缀,被该注解标注的类会被识别Spring Framework框架识别属性配置类,IOC容器会自动读取配置文件中的对应前缀对应的配置项,被解析出配置项的值,给配置类赋值
*/
public @interface ConfigurationProperties { 
    @AliasFor("prefix")
    String value() default ""; 

    @AliasFor("value")
    String prefix() default ""; 

    boolean ignoreInvalidFields() default false;

    boolean ignoreUnknownFields() default true;
}

?配置密码后,我们就可以使用自定制的用户名及密码登录;


三: Spring Security安全框架与Spring Boot整合核心类


我们知道Spring Boot与三方组件整合的是通过Spring Boot框架的SPI(Service Provider Interface)机制[服务提供规范],识别三方组件的自动装载配置类,我们来看下Spring Security与Spring Boot整合的自动装配类: 三方框架整合的套路或者说规范;该Spring-boot-configuration-xxxx.jar組件是Spring boot与三方框架整合的核心类, 当我们要定制化自己的组件,并让Spring Boot框架加载识别我们的组件,必须引入该依赖;

?Spring boot框架会解析META-INF目录下面的spring.factories配置文件, 我们可以在该配置文件中找到Spring Security与Spring Boot框架自动装配的配置类(

SecurityAutoConfiguration   这是Security框架与Spring Boot整合最主要的配置类

):

?我们可以大致看下该配置类的源代码: 该类上的多个注解,作用值得我们细细品;者对应我们自定制自己业务组件很有用;


package org.springframework.boot.autoconfigure.security.servlet;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityDataConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({DefaultAuthenticationEventPublisher.class})
@EnableConfigurationProperties({SecurityProperties.class})
@Import({SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class, ErrorPageSecurityFilterConfiguration.class})
public class SecurityAutoConfiguration {
    public SecurityAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean({AuthenticationEventPublisher.class})
    public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
        return new DefaultAuthenticationEventPublisher(publisher);
    }
}


我们来看下@EnableConfigurationProperties 注解, 该组件中就一个成员value,其值为对应配置文件的相关属性的配置类

package org.springframework.boot.context.properties;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EnableConfigurationPropertiesRegistrar.class})
public @interface EnableConfigurationProperties {
    String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

    Class<?>[] value() default {};
}

我们来看@Import注解中的属性EnableConfigurationPropertiesRegistrar,我们可以跟踪该类的作用,就是解析配置文件,并被根据对应的配置文件的属性项赋值给对应的配置类并把该配置类注册为IOC容器管理的组件,?核心代码如下;

class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
    private static final String METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME = Conventions.getQualifiedAttributeName(EnableConfigurationPropertiesRegistrar.class, "methodValidationExcludeFilter");

    EnableConfigurationPropertiesRegistrar() {
    }

    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        registerInfrastructureBeans(registry);
        registerMethodValidationExcludeFilter(registry);
        ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
        this.getTypes(metadata).forEach(beanRegistrar::register);
    }

    private Set<Class<?>> getTypes(AnnotationMetadata metadata) {
        return (Set)metadata.getAnnotations().stream(EnableConfigurationProperties.class).flatMap((annotation) -> {
            return Arrays.stream(annotation.getClassArray("value"));
        }).filter((type) -> {
            return Void.TYPE != type;
        }).collect(Collectors.toSet());
    }

    static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
        ConfigurationPropertiesBindingPostProcessor.register(registry);
        BoundConfigurationProperties.register(registry);
    }

    static void registerMethodValidationExcludeFilter(BeanDefinitionRegistry registry) {
        if (!registry.containsBeanDefinition(METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME)) {
            BeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition(MethodValidationExcludeFilter.class, () -> {
                return MethodValidationExcludeFilter.byAnnotation(ConfigurationProperties.class);
            }).setRole(2).getBeanDefinition();
            registry.registerBeanDefinition(METHOD_VALIDATION_EXCLUDE_FILTER_BEAN_NAME, definition);
        }

    }
}

上述的各个标红色的代码对应的类需要我们细细琢磨,体会自定制与组件与Spring Boot整合需要那些加工部件,值得思考;

四: 定制自定义Spring-boot-starter自动配置组件?

下面我们通过自定义一个starter自动配置项目,来了解及引出Spring Boot的核心运作原理,Spring Boot默认实现了许多starter,可以在项目中快速集成。但是当我们所需的starter并不在其中时,又想借鉴Spring Boot的starter的创建机制来创建自己的框架starter,下面我们通过一个实例来阐述自定制starter自动配置的项目Demo,来深入了解Spring Boot的自动配置的机制。


下面我们来自定制Spring boot自动装配短信发送项目组件:ShortLetter-spring-boot-starter;并在其他项目中引用该组件,实现自动配置;

4.1 引入Spring Boot 自动化配置依赖spring-boot-authconfigure;

此处使用的是2.2.6.RELEASE版本

<!--引入Spring Boot 自动化配置依赖spring-boot-autoconfigure -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

本ShortLetter-spring-boot-starter组件的pom.xml配置如下


<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.jd.auth.data.security.server</groupId>
    <artifactId>ShortLetter-Spring-boot-starter</artifactId>
    <version>1.0.0.RELEASE</version>
    <name>ShortLetter-Spring-boot-starter</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source.version>1.8</maven.compiler.source.version>
        <maven.compiler.target.version>1.8</maven.compiler.target.version>
    </properties>

    <!-- 设定主仓库,按设定顺序进行查找。 -->
    <repositories>
        <!-- 会优先查找lib目录下的依赖的本地jar包-->
        <repository>
            <id>in-project</id>
            <name>In Project Repo</name>
            <url>file://${project.basedir}/lib</url>
        </repository>
        <repository>
            <id>repos</id>
            <name>Repository</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public</url>
        </repository>
    </repositories>
    <!-- 设定插件仓库 -->
    <pluginRepositories>
        <pluginRepository>
            <id>repos</id>
            <name>Repository</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public</url>
        </pluginRepository>
    </pluginRepositories>

    <dependencies>
        <!--引入Spring Boot 自动化配置依赖spring-boot-autoconfigure -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource> <!-- 配置需要被替换的资源文件路径 -->
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                    <include>**/*.yml</include>
                    <include>**/*.txt</include>
                    <include>**/*.gif</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.2.6.RELEASE</version>
                <configuration>
                    <!--跳过测试-->
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>


4.2:编写属性配置类?
package org.jd.auth.data.security.server.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 属性配置类
 */

@ConfigurationProperties(prefix = "short.msg") // 通过该注解来进行对应属性的装配
public class ShortMessageProperties {

    // 访问发送短信的url地址
    private String Url;
    // 短信服务商提供的请求keyId
    private String accessKeyId;
    // 短信服务提供商KeySecret
    private String accessKeySecret;

    public String getUrl() {
        return Url;
    }

    public void setUrl(String url) {
        Url = url;
    }

    public String getAccessKeyId() {
        return accessKeyId;
    }

    public void setAccessKeyId(String accessKeyId) {
        this.accessKeyId = accessKeyId;
    }

    public String getAccessKeySecret() {
        return accessKeySecret;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }
}

4.3 消息载体领域模型

package org.jd.auth.data.security.server.entity;

import java.io.Serializable;
import java.util.Date;

/**
 * 消息领域模型
 */
public class ShortMessage implements Serializable {
    private static final long serialVersionUID = 4677292720893513983L;

    private String shortMsgId; // 消息ID
    private String content; // 消息内容
    private Date publishTime; //发布时间

    public ShortMessage() {
    }

    public String getShortMsgId() {
        return shortMsgId;
    }

    public void setShortMsgId(String shortMsgId) {
        this.shortMsgId = shortMsgId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getPublishTime() {
        return publishTime;
    }

    public void setPublishTime(Date publishTime) {
        this.publishTime = publishTime;
    }

    @Override
    public String toString() {
        return "ShortMessage{" +
                "shortMsgId='" + shortMsgId + '\'' +
                ", content='" + content + '\'' +
                ", publishTime=" + publishTime +
                '}';
    }
}

4.4 编写模拟调用短信营运商的短信发送接口工具类

package org.jd.auth.data.security.server.util;

import org.jd.auth.data.security.server.entity.ShortMessage;

/**
 * 模拟发送短信服务的工具类
 */
public class HttpClientUtils {
    public static int sendShortMessage(String url, String accessKeyId, String accessKeySecret, ShortMessage shortMessage) {
        // 调用指定url进行请求的业务逻辑
        try {
            System.out.println("http 请求,url=" + url + " ; accessKeyId=" + accessKeyId
                    + " ;accessKeySecret=" + accessKeySecret + " ; message = " + shortMessage.toString());
        } catch (Exception e) {
            return -1;
        }
        return 0;
    }
}

4.5? 编写自动配置类

* 有了属性配置类(ShortMessageProperties)和服务类(ShortMessageService) * 就可以通过自动配置类将其整合,并在特定条件下进行实例化操作 * 自动配置本质上就是一个普通的Java类,通过不同的注解来对其赋予不同的功能


package org.jd.auth.data.security.server.config;

import org.jd.auth.data.security.server.properties.ShortMessageProperties;
import org.jd.auth.data.security.server.service.ShortMessageService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * 有了属性配置类(ShortMessageProperties)和服务类(ShortMessageService)
 * 就可以通过自动配置类将其整合,并在特定条件下进行实例化操作
 * 自动配置本质上就是一个普通的Java类,通过不同的注解来对其赋予不同的功能
 */
@Configuration
@ConditionalOnClass(ShortMessageService.class)
@EnableConfigurationProperties(ShortMessageProperties.class)
public class ShortMessageAutoConfiguration {
    /**
     * 注入属性配置类
     */
    @Resource
    private ShortMessageProperties shortMessageProperties;

    @Bean
    @ConditionalOnMissingBean(ShortMessageService.class)
    @ConditionalOnProperty(prefix = "short.msg",value="enabled",havingValue = "true")
    public ShortMessageService shortMessageService(){
        // 如果提供了其他的set方法,在此也可以调用对应方法对其进行相应的设置或初始化
        return new ShortMessageService(shortMessageProperties.getUrl(),
                shortMessageProperties.getAccessKeyId(),shortMessageProperties.getAccessKeySecret());
    }

}


4.6? 创建META-INF目录|spring.factories文件并配置短信发送服务的自动配置类

在本项目的resources目录下面创建META-INF及spring.factories文件,并且该spring.factories配置内容如下:


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.jd.auth.data.security.server.config.ShortMessageAutoConfiguration

?4.7 把该项目组件打包上传值私服便于其他项目中引用该依赖


4.8 在其他项目中引入该组件三角坐标

<!-- 引入发送短信服务依赖-->
<dependency>
    <groupId>org.jd.auth.data.security.server</groupId>
    <artifactId>ShortLetter-Spring-boot-starter</artifactId>
    <version>1.0.0.RELEASE</version>
</dependency>

4.9 在引入依赖的项目中resource目录下配置属性

在application.yml或者application.properties文件中配置对应的属性



4.10 编写发送短信服务暴露端点并测试
package org.jd.auth.data.security.server.controller;

import org.jd.auth.data.security.server.service.ShortMessageService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * Spring Security默认权限配置
 */
@RestController
public class HelloController {
    /**
     * 引入自动配置短信服务组件
     */
    @Resource
    private ShortMessageService shortMessageService;

    @RequestMapping("/hello")
    public String getHello() {
        return "你好,欢迎来到Spring Security安全控制框架!";
    }

    @RequestMapping("/send")
    public String sendShortMsg() {
        try {
            String message = "共享您,已经支付成功!";
            int result = shortMessageService.sendShortMessage(message);
            return result == 0 ? "发送成功" : "发送失败";
        } catch (Exception e) {
            return "发送失败";
        }
    }
}


?访问http://127.0.0.1:9911/auth/send

输入admin/admin123登录后,即可看到如下信息,说明自定制短信服务的starter自动配置组件整合成功,至此自定义自动化配置组件完毕;

IDEA控制台打印信息如下:


http 请求,url=www.lingcaibao.com ; accessKeyId=123456emsces ;accessKeySecret=86384638433484863483 ; message = ShortMessage{shortMsgId='07f9ce19-8281-4521-8a08-367fa4329fcc', content='共享您,已经支付成功!', publishTime=Thu Dec 14 10:52:37 CST 2023}
http 请求,url=www.lingcaibao.com ; accessKeyId=123456emsces ;accessKeySecret=86384638433484863483 ; message = ShortMessage{shortMsgId='2f560915-4e54-4aec-bce9-fc4368219e2c', content='共享您,已经支付成功!', publishTime=Thu Dec 14 12:36:25 CST 2023}
http 请求,url=www.lingcaibao.com ; accessKeyId=123456emsces ;accessKeySecret=86384638433484863483 ; message = ShortMessage{shortMsgId='3e63bbb7-c65c-4803-b09e-58ed5d79e59c', content='共享您,已经支付成功!', publishTime=Thu Dec 14 12:36:55 CST 2023}


?

五:Spring Boot自动装配(SPI机制核心)


SPI机制:
拓展与自动装配相关的服务提供方接口(Service Provider Interface,SPI)机制;关于SPI的来源,需要熟系设计模式中的依赖颠倒原则.依赖颠倒原则中提到,应该依赖接口而不是实现类;但是接口最终都是需要实现类来落地。如果因为业务调整,需要替换某个接口的实现类,就不得不改动实现类的源代码。SPI机制的出现解决了这个问题,它通过一种"服务寻找"的机制,动态地加载接口/抽象类对应的具体实现类,这有点类似IOC的"味道“,它把接口的具体实现类的定义和声明全交给了”外部化的配置文件“。可以通过下图来简单理解SPI机制。一个接口可以有多个实现类,通过SPI机制,可以将一个接口需要创建的实现类的对象都罗列在一个特殊的文件中,SPI机制会依次将这些实现类的对象进行创建并返回。

原生JDK的SPI机制可以通过一个指定的接口/抽象类找到预先设置好的实现类(并创建实现类的对象)。JDK1.6中新增了SPI的实现,Spring Framework 3.2也引入了SPI的实现,而且比JDK的实现更加强大; 下面我们介绍JDK与Spring Framework中的SPI及它们的区别;


5.1 JDK原生的SPI

SPI的全名为Service Provider Interface.大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java spi机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不使用实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候动态指定具体实现类,这就需要一种服务发现机制。 java spi就是提供这种功能的机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。?

java SPI应用场景很广泛,在Java底层和一些框架中都很常用,比如java数据驱动加载和Dubbo。Java底层定义加载接口后,由不同的厂商提供驱动加载的实现方式,当我们需要加载不同的数据库的时候,只需要替换数据库对应的驱动加载jar包,就可以进行使用。

要使用Java SPI,需要遵循如下约定:

  • 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;

  • 2、接口实现类所在的jar包放在主程序的classpath中;

  • 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;

  • 4、SPI的实现类必须携带一个不带参数的构造方法;


?5.2 模拟不同服务产商对数据源的不同实现

定义数据源规范:

package org.jd.auth.data.security.server.reponsitory;

/**
 * 定义数据源接口: 旨在演示JDK与Spring Framework框架的SPI机制
 */
public interface DataSourceDao {
}

5.2.1 Mysql数据库厂商实现该规范?
package org.jd.auth.data.security.server.reponsitory.impl;

import org.jd.auth.data.security.server.reponsitory.DataSourceDao;

/**
 * Mysql产商实现数据源接口的类
 */
public class DemoDataSourceMysqlDaoImpl implements DataSourceDao {
}
5.2.2 Oracle数据库厂商实现该规范

package org.jd.auth.data.security.server.reponsitory.impl;

import org.jd.auth.data.security.server.reponsitory.DataSourceDao;

/**
 * Oracle数据库厂商实现数据源接口实现类
 */
public class DemoDataSourceOracleDaoImpl implements DataSourceDao {
}
?5.2.3 在组件项目的resources资源目录下创建services目录并"创建规范接口全路径名的文本文件"

在该规范文件中添加mysql与oracle厂商的实现类,文件内容如下所示:

org.jd.auth.data.security.server.reponsitory.impl.DemoDataSourceMysqlDaoImpl
org.jd.auth.data.security.server.reponsitory.impl.DemoDataSourceOracleDaoImpl


?5.2.4 然后打包该项目供其他项目使用并测试

?测试代码如下:


package org.jd.auth.data.security.server;



import org.jd.auth.data.security.server.reponsitory.DataSourceDao;
import org.junit.Test;


import java.util.ServiceLoader;


public class SystemAuthenticationAppTest {
   @Test
   public  void testJDKSpiProvider(){
       ServiceLoader<DataSourceDao> serviceLoader = ServiceLoader.load(DataSourceDao.class);
       serviceLoader.iterator().forEachRemaining(dataSourceDao->{
           System.out.println(dataSourceDao);
       });
   }
}

5.2.5? JDK的SPI机制的优缺点

优点:

使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

缺点:

  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。

  • 多个并发多线程使用ServiceLoader类的实例是不安全的。


?5.2.6 JDK的SPI源代码

由源代码可以看出,java会根据定义的路径去扫描可能存在的接口的实现。放在config中,然后使用parse方法将配置文件中的接口实现全路径放在pending中,并取得第一个实现类(变量nextName),

然后使用类加载器加载,加载需要调用的类,然后调用实现的方法


5.3?Spring Framework框架提供的SPI

Spring Framework 3.2 中提供的SPI

(1)Spring Framework3.2中的SPI相对于JDK中的原生的SPI更加高级使用,因为它不仅仅限于接口和抽象类,而可以是任何一个类,接口或者注解;也这是因为Spring Framework的SPI支持注解作为索引,所以在Spring Boot中大量用到SPI机制加载自动配置类和特殊组件等

(2)申明SPI文件,Spring Framework的SPI文件也是有相应的规范,需要将SPI文件放在项目的META-INFO目录下,且文件名必须时spring.factories。该文件 为properties类型文件 (3)spring.properties文件定义的规则是:被检索的类/接口/注解的全限定名作为properties的key,具体检索的类(注意不是实现类)的全限定名作为value, 多个类之间用英文逗号分隔.

(4)注意:spring.factories文件中定义的key和value可以毫五关联,仅凭这一点就比JDK的SPI要灵活得多

(5)使用Spring Framework框架的SPI机制时,加载spring.factories文件的API是SpringFactoriesLoader,它不仅可以加载声明的类的对象 ,而且可以直接把预先定义好的全限定名都提取出来;


5.3.1 编写Mysql与Oracle实现数据源DataSource规范实现类

package org.jd.auth.data.security.server.reponsitory.impl;

import org.jd.auth.data.security.server.reponsitory.DataSourceDao;

/**
 * Spring Framework 的Mysql厂商实现数据源DataSource规范实现类
 */
public class DemoSpringSPIDataSourceMysqlDaoImpl implements DataSourceDao {
}

package org.jd.auth.data.security.server.reponsitory.impl;

import org.jd.auth.data.security.server.reponsitory.DataSourceDao;

/**
 * Spring Framework 的Oracle厂商实现数据源DataSource规范实现类
 */
public class DemoSpringSPIDataSourceOracleDaoImpl implements DataSourceDao {
}

5.3.2 在spring.factories配置文件配置如下内容

org.jd.auth.data.security.server.reponsitory.DataSourceDao=\
org.jd.auth.data.security.server.reponsitory.impl.DemoSpringSPIDataSourceOracleDaoImpl,\
org.jd.auth.data.security.server.reponsitory.impl.DemoSpringSPIDataSourceMysqlDaoImpl

?5.3.3 在另外一个项目中引入该依赖并测试

引入依赖三角坐标组件

<!-- 引入发送短信服务依赖-->
<dependency>
    <groupId>org.jd.auth.data.security.server</groupId>
    <artifactId>ShortLetter-Spring-boot-starter</artifactId>
    <version>1.0.0.RELEASE</version>
</dependency>

编写测试:


package org.jd.auth.data.security.server;



import org.jd.auth.data.security.server.reponsitory.DataSourceDao;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.core.io.support.SpringFactoriesLoader;


import java.util.List;
import java.util.ServiceLoader;


public class SystemAuthenticationAppTest {
   @Test
   public  void testJDKProviderSPI(){
       ServiceLoader<DataSourceDao> serviceLoader = ServiceLoader.load(DataSourceDao.class);
       serviceLoader.iterator().forEachRemaining(dataSourceDao->{
           System.out.println(dataSourceDao);
       });
   }

    @Test
    public  void testSpringFrameworkProviderSPI(){
        List<DataSourceDao> resultList = SpringFactoriesLoader.loadFactories(DataSourceDao.class, SystemAuthenticationAppTest.class.getClassLoader());
        resultList.forEach(dao ->{
            System.out.println(dao);
        });
        System.out.println("--------------------------------------------------------------------------------------------");
        List<String> daoClassNames = SpringFactoriesLoader.loadFactoryNames(DataSourceDao.class,SystemAuthenticationAppTest.class.getClassLoader());
        daoClassNames.forEach(className->{
            System.out.println(className);
        });
    }
}


?从控制台输出的内容可以看到,Spring Framework的SPI机制已经生效

?

文章来源:https://blog.csdn.net/u014635374/article/details/134999592
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。