为什么要编写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,对应的实现类也就会被实例化