SpringBoot插件开发实战和原理

为什么要编写springBoot插件

我们在开发的时候需要提供一些通用的功能,所以我们将重复代码抽取成一个jar包。开发人员在使用jar包的时候不用考虑jar包的内容,直接使用具体的功能即可,但是可能由于包路径的不同,你所编写的bean没有被初始化到spring容器中。不应该让开发人员主动的去扫描通用jar包中的路径去初始化bean。所以我们要自己动手去把bean初始化到bean容器中,这也是spring扩展能力的由来(spring.factories)

DEMO

用户登录是一个通用的功能,各个服务都需要和用户中心服务进行交互,进行用户的登录和登出。

用户中心侧

在resources下创建META-INF/spring.factories 编写spring.factories

spring.factories

ini 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.jiahui.ucs.sdk.config.UcsClientSDKConfig

configuration类

kotlin 复制代码
package com.jiahui.ucs.sdk.config;

import com.jiahui.ucs.facade.IUCSAccountInfoAPI;
import com.jiahui.ucs.facade.IUCSLoginAPI;
import com.jiahui.ucs.facade.IUCSUserInfoAPI;
import com.jiahui.ucs.facade.IUCSWechatOpenAPI;
import com.jiahui.ucs.sdk.IUcsInnerClientAPI;
import com.jiahui.ucs.sdk.client.UcsInnerClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * @author yatao.xu
 * @version 1.0.0
 * @date 2020-11-03
 **/

@Configuration
public class UcsClientSDKConfig {

    @Resource
    private IUCSLoginAPI ucsLoginAPI;

    @Resource
    private IUCSAccountInfoAPI accountInfoAPI;

    @Resource
    private IUCSUserInfoAPI userInfoAPI;

    @Resource
    private IUCSWechatOpenAPI ucsWechatOpenAPI;

    @Bean
    @ConditionalOnMissingBean
    public IUcsInnerClientAPI ucsInnerClient() {
        return new UcsInnerClient(ucsLoginAPI, accountInfoAPI, userInfoAPI, ucsWechatOpenAPI);
    }

}
java 复制代码
package com.jiahui.ucs.sdk;

import com.jiahui.ucs.facade.dto.*;
import com.jiahui.ucs.facade.vo.UCSLoginVO;
import com.jiahui.ucs.facade.vo.UCSUserInfoVO;

/**
 * @author yatao.xu
 * @version 1.0.0
 * @date 2020-11-04
 **/
public interface IUcsInnerClientAPI {

    /**
     * 手机验证码登录(支持国际手机号码登录)
     *
     * @param areaCode 区号 
     * @param phone    手机号
     * @param lang     语言
     * @return 登录信息
     */
    UCSLoginVO loginOfValidation(String areaCode, String phone, Integer userType, Integer lang, String systemCode, String terminal);

    /**
     * 微信授权登录
     *
     * @param code code
     * @return 登录信息
     */
    UCSLoginVO loginOfWeChatOpen(String code, String systemCod);

    /**
     * token 校验
     *
     * @param token token
     * @return ture:有效
     */
    Boolean checkToken(String token);

    /**
     * 支付宝授权
     *
     * @param code code
     * @param systemCod 设备信息
     * @return 用户信息
     */
    UCSLoginVO alipayLogin(String code, String systemCod);

    /**
     * apple授权登录
     *
     * @param systemCode        系统编号
     * @param user              user(授权使用)
     * @param authorizationCode authorizationCode(授权使用)
     * @param identityToken     identityToken(授权使用)
     * @return 登录信息
     */
    UCSLoginVO appleLogin(String systemCode, String user, String authorizationCode, String identityToken);

    /**
     * 游客登录
     *
     * @param deviceId
     * @param deviceType
     * @param ip
     * @return 用户信息
     */
    UCSLoginVO touristLogin(String deviceId, String deviceType, String ip);

    /**
     * 查询用户信息
     *
     * @param token token
     * @return 用户信息
     */
    UCSALLUserInfoDTO getUserInfo(Long userId, String token, String systemCod);

    /**
     * 修改用户信息
     *
     * @param userInfoDTO 用户信息
     */
    void modifyUserInfo(UCSUserInfoDTO userInfoDTO);


    /**
     * 修改手机号
     *
     * @param ucsModifyPhoneDTO 用户信息
     */
    void updatePhone(UCSModifyPhoneDTO ucsModifyPhoneDTO);

    /**
     * 绑定手机号
     *
     * @param bindingPhoneDTO 手机信息
     * @return 登录信息
     */
    UCSLoginVO bindingPhone(UCSBindingPhoneDTO bindingPhoneDTO, String systemCod);

    /**
     * 解绑微信
     *
     * @param token token
     * @return 是否成功
     */
    Boolean unBindWeChat(String token);

    /**
     * 退出登录
     *
     * @param token token
     */
    void logout(String token);

    /**
     * 获取用户信息
     *
     * @param userId userid
     * @return ucs 用户信息
     */
    UCSUserInfoVO getUserInfoByUserId(Long userId);

    /**
     * 根据token获取用户信息
     *
     * @param token token
     * @return 用户系统id
     */
    Long getUserId(String token);

    /**
     * 手机号登录后绑定微信号
     *
     * @param bindingWeChatDTO 绑定手机对象
     * @return 登录信息
     */
    Boolean bindWeChat(UCSBindingWeChatDTO bindingWeChatDTO, String systemCod);

    /**
     * 创建token
     *
     * @param useType 用户类型
     * @param userId  用户id
     * @return
     */
    UCSLoginVO createToken(Integer useType, Long userId, String systemCod);

    /**
     * 添加小程序游客记录
     *
     * @param
     * @param openId
     * @param o
     * @param o1
     * @param lang
     * @return
     */
    void addWechatUser(String openId, Object o, Object o1, Integer lang);
}
java 复制代码
package com.jiahui.ucs.sdk.client;

import com.alibaba.fastjson.JSON;
import com.jiahui.ucs.facade.IUCSAccountInfoAPI;
import com.jiahui.ucs.facade.IUCSLoginAPI;
import com.jiahui.ucs.facade.IUCSUserInfoAPI;
import com.jiahui.ucs.facade.IUCSWechatOpenAPI;
import com.jiahui.ucs.facade.dto.*;
import com.jiahui.ucs.facade.vo.UCSLoginVO;
import com.jiahui.ucs.facade.vo.UCSUserInfoVO;
import com.jiahui.ucs.sdk.IUcsInnerClientAPI;
import lombok.extern.slf4j.Slf4j;

/**
 * @author yatao.xu
 * @version 1.0.0
 * @date 2020-10-28
 **/
@Slf4j
public class UcsInnerClient implements IUcsInnerClientAPI {

    private IUCSLoginAPI ucsLoginAPI;

    private IUCSUserInfoAPI userInfoAPI;

    private IUCSAccountInfoAPI accountInfoAPI;

    private IUCSWechatOpenAPI ucsWechatOpenAPI;

    public UcsInnerClient(IUCSLoginAPI ucsLoginAPI, IUCSAccountInfoAPI accountInfoAPI, IUCSUserInfoAPI userInfoAPI, IUCSWechatOpenAPI ucsWechatOpenAPI) {
        this.ucsLoginAPI = ucsLoginAPI;
        this.accountInfoAPI = accountInfoAPI;
        this.userInfoAPI = userInfoAPI;
        this.ucsWechatOpenAPI = ucsWechatOpenAPI;
    }


    @Override
    public UCSLoginVO loginOfValidation(String areaCode, String phone, Integer userType, Integer lang, String systemCode, String terminal) {
        log.info("验证码登录 :phone=[{}],lang=[{}]", phone, lang);
        return ucsLoginAPI.loginOfValidation(areaCode, phone, userType, systemCode, terminal);
    }

    @Override
    public Boolean checkToken(String token) {
        log.info("token校验:token=[{}]", token);
        return ucsLoginAPI.checkToken(token);

    }

    @Override
    public UCSLoginVO loginOfWeChatOpen(String code, String systemCode) {
        log.info("微信账号登录 :  code=[{}]", code);
        return ucsLoginAPI.loginOfWeChatOpen(code, systemCode);
    }

    @Override
    public Boolean unBindWeChat(String token) {
        log.info("解绑微信,token:[{}]", token);
        return ucsLoginAPI.unBindWeChat(token);
    }

    @Override
    public UCSLoginVO alipayLogin(String code, String systemCod) {
        return ucsLoginAPI.alipayLogin(systemCod, code);
    }

    @Override
    public UCSLoginVO appleLogin(String systemCode, String user, String authorizationCode, String identityToken) {
        return ucsLoginAPI.appleLogin(systemCode, user, authorizationCode, identityToken);
    }

    @Override
    public UCSLoginVO touristLogin(String deviceId, String deviceType, String ip) {
        return ucsLoginAPI.touristLogin(deviceId, deviceType, ip);
    }

    @Override
    public void logout(String token) {
        ucsLoginAPI.logout(token);
    }

    @Override
    public UCSALLUserInfoDTO getUserInfo(Long userId, String token, String systemCod) {
        return userInfoAPI.getUserInfo(userId, token, systemCod);
    }

    @Override
    public void modifyUserInfo(UCSUserInfoDTO userInfoDTO) {
        userInfoAPI.modifyUserInfo(userInfoDTO);
    }

    @Override
    public void updatePhone(UCSModifyPhoneDTO ucsModifyPhoneDTO) {
        userInfoAPI.updatePhone(ucsModifyPhoneDTO);
    }

    @Override
    public UCSLoginVO bindingPhone(UCSBindingPhoneDTO bindingPhoneDTO, String systemCod) {
        log.info("绑定手机号 :  bindingPhoneDTO=[{}]", JSON.toJSONString(bindingPhoneDTO));

        // 设置业务系统systemCode
        bindingPhoneDTO.setSystemCode(systemCod);
        return accountInfoAPI.bindPhone(bindingPhoneDTO);
    }

    @Override
    public UCSUserInfoVO getUserInfoByUserId(Long userId) {
        return userInfoAPI.getUserInfoByUserId(userId);
    }

    @Override
    public Long getUserId(String token) {
        return userInfoAPI.getUserId(token);
    }

    @Override
    public Boolean bindWeChat(UCSBindingWeChatDTO bindingWeChatDTO, String systemCod) {
        log.info("绑定微信号 :  bindingPhoneDTO=[{}]", JSON.toJSONString(bindingWeChatDTO));
        // 设置业务系统systemCode
        bindingWeChatDTO.setSystemCode(systemCod);
        return accountInfoAPI.bindWeChat(bindingWeChatDTO);
    }

    @Override
    public UCSLoginVO createToken(Integer useType, Long userId, String systemCod) {
        log.info("生成token useType=[{}],userId=[{}]", useType, userId);
        return ucsLoginAPI.createToken(useType, userId, systemCod);
    }

    @Override
    public void addWechatUser(String openId, Object o, Object o1, Integer lang) {
        ucsWechatOpenAPI.addWechatGuestUser(openId, lang);
    }
}

通用代码使用侧

pom

xml 复制代码
<dependency>
    <artifactId>ucs-sdk-http-ucs</artifactId>
    <groupId>com.jiahui.ucs</groupId>
    <version>${ucs-facade.version}</version>
</dependency>

在pom中添加依赖

业务代码

less 复制代码
/**
 * App 登录服务
 *
 * @author yatao.xu
 * @version 1.0.0
 * @date 2020-10-29
 **/
@Slf4j
@Api(value = "LoginServiceRest", tags = "App 登录服务")
@RestController("LoginServiceRest")
@Validated
@RequestMapping("/app/login")
public class LoginServiceRest extends AppSystemRest {

    @Autowired
    private IUcsInnerClientAPI ucsClient;
    @Resource
    private IMASmsSendAPI smsSendAPI;


    @ApiOperation(value = "发送验证码", notes = "发送验证码")
    @PostMapping("/sendValidation")
    public void sendValidation(HttpServletRequest request, @RequestParam(name = "areaCode", defaultValue = "86") String areaCode,
                               @RequestParam("phone") String phone) {
        String ip = HttpUtil.getIP(request);
        smsSendAPI.sendGeneralVerificationCode(areaCode, phone, MASendMessageSceneEnum.VERIFICATION_CODE_SIGN_IN.getSceneCode(), MASMSTypeEnum.SMS.getType(), getLanguage(), ip);
    }

    @ApiOperation(value = "发送语音验证码", notes = "发送语音验证码")
    @PostMapping("/send-validation-voice")
    public void sendValidationOfVoice(HttpServletRequest request, @RequestParam(name = "areaCode", defaultValue = "86") String areaCode,
                                      @RequestParam("phone") String phone) {
        String ip = HttpUtil.getIP(request);
        smsSendAPI.sendGeneralVerificationCode(areaCode, phone, MASendMessageSceneEnum.VERIFICATION_CODE_SIGN_IN.getSceneCode(), MASMSTypeEnum.VOICE_MESSAGE_CHINA.getType(), getLanguage(), ip);
    }
    

    @ApiOperation(value = "游客身份登录", notes = "游客身份")
    @PostMapping("/touristLogin")
    public UCSLoginVO touristLogin(HttpServletRequest request, @RequestParam("deviceId") String deviceId) {
        String deviceType = request.getHeader("terminal");
        String ip = HttpUtil.getIP(request);
        return ucsClient.touristLogin(deviceId, deviceType, ip);
    }
    
    @ApiOperation(value = "解绑微信", notes = "解绑微信")
    @NeedUserLogin
    @GetMapping("/unBindWeChat")
    public Boolean unBindWeChat() {
        return ucsClient.unBindWeChat(getToken());
    }

    @ApiOperation(value = "退出登录", notes = "退出登录")
    @NeedUserLogin
    @PostMapping("/logout")
    public void loginOut() {
        ucsClient.logout(getToken());
    }
}

spring.factories 常用配置接口

1. org.springframework.boot.SpringApplicationRunListener

SpringApplicationRunListener来监听Spring Boot的启动流程,并且在各个流程中处理自己的逻辑。在应用启动时,在Spring容器初始化的各个阶段回调对应的方法。

2. org.springframework.context.ApplicationContextInitializer

ApplicationContextInitializer是在springboot启动过程上下文 ConfigurableApplicationContext刷新方法前(refresh)调用,对ConfigurableApplicationContext的实例做进一步的设置或者处理。

3.org.springframework.boot.autoconfigure.EnableAutoConfiguration

定义系统自动装配的类。

4.org.springframework.boot.env.EnvironmentPostProcessor

配置环境的集中管理。比如扩展去做排除加载系统默认的哪些配置类,方便自定义扩展。

5.org.springframework.boot.autoconfigure.AutoConfigurationImportFilter

自动装配类排除

spring factories 原理

获取配置流程

@EnableAutoConfiguration

在启动类注解@SpringBootApplication中可以看到引用了@EnableAutoConfiguration。

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

}

@EnableAutoConfiguration注解:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

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

	String[] excludeName() default {};
}

可以看到EnableAutoConfiguration中包含@Import(AutoConfigurationImportSelector.class),这段代码表示,使用 @Import 注解将 AutoConfigurationImportSelector 类导入到 Spring 容器中。AutoConfigurationImportSelector 类是 Spring Boot 自动配置的核心类,它负责扫描类路径中的 JAR 文件,并根据 Spring Boot 启动器的依赖关系导入相应的自动配置类。

具体来说,@Import 注解会将 AutoConfigurationImportSelector 类作为一个 bean 注册到 Spring 容器中。AutoConfigurationImportSelector 类继承ImportSelect接口,告诉Spring它是一个配置类,可以用于配置 Spring 容器。

AutoConfigurationImportSelector 类的 @ImportResource 注解会导入 spring.factories 文件中定义的自动配置类。

因此,@Import(AutoConfigurationImportSelector.class) 这段代码会启用 Spring Boot 的自动配置功能,并根据 Spring Boot 启动器的依赖关系自动配置 Spring 容器中的 bean。

核心方法:selectImports

java 复制代码
	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
				autoConfigurationMetadata, annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

其中getAutoConfigurationEntry方法

scss 复制代码
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
			AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    //
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = filter(configurations, autoConfigurationMetadata);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

其中getCandidateConfigurations

java 复制代码
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
            getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
            + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

调用了SpringFactoriesLoader.loadFactoryNames

java 复制代码
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    String factoryTypeName = factoryType.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

loadSpringFactories方法

java 复制代码
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryTypeName = ((String) entry.getKey()).trim();
                for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryTypeName, factoryImplementationName.trim());
                }
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

加载配置流程

在main方法启动的时候我们会调用SpringApplication.run方法 run方法中调用了getSpringFactoriesInstances 调用createSpringFactoriesInstances

java 复制代码
public ConfigurableApplicationContext run(String... args) {
    //此处调用getRunListeners()方法
    SpringApplicationRunListeners listeners = getRunListeners(args);
    //..
}


private SpringApplicationRunListeners getRunListeners(String[] args) {
   Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    
    //此处调用getSpringFactoriesInstances()方法
   return new SpringApplicationRunListeners(logger,
         getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
         this.applicationStartup);
}


private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
   ClassLoader classLoader = getClassLoader();
   Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
   //此处调用了createSpringFactoriesInstances
   List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
   AnnotationAwareOrderComparator.sort(instances);
   return instances;
}

private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
			ClassLoader classLoader, Object[] args, Set<String> names) {
    List<T> instances = new ArrayList<>(names.size());
    for (String name : names) {
        try {
            Class<?> instanceClass = ClassUtils.forName(name, classLoader);
            Assert.isAssignable(type, instanceClass);
            Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
            T instance = (T) BeanUtils.instantiateClass(constructor, args);
            instances.add(instance);
        }
        catch (Throwable ex) {
            throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
        }
    }
    return instances;
}

总结

这是一种类似插件的设计方式,只要引入对应的jar包,就会扫描到jar里的spring.factories,对应的实现类也就会被实例化

相关推荐
2401_857610038 分钟前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
背水2 小时前
初识Spring
java·后端·spring
晴天飛 雪3 小时前
Spring Boot MySQL 分库分表
spring boot·后端·mysql
weixin_537590453 小时前
《Spring boot从入门到实战》第七章习题答案
数据库·spring boot·后端
AskHarries3 小时前
Spring Cloud Gateway快速入门Demo
java·后端·spring cloud
Qi妙代码4 小时前
MyBatisPlus(Spring Boot版)的基本使用
java·spring boot·后端
宇宙超级勇猛无敌暴龙战神4 小时前
Springboot整合xxl-job
java·spring boot·后端·xxl-job·定时任务