组件地址
<dependency>
<groupId>org.opengoofy.index12306</groupId>
<artifactId>index12306-base-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
组件功能
全局变量 & 组件执行顺序
全局用户常量
package org.opengoofy.index12306.framework.starter.bases.constant;
/**
* 用户常量
*
*/
public final class UserConstant {
/**
* 用户 ID Key
*/
public static final String USER_ID_KEY = "userId";
/**
* 用户名 Key
*/
public static final String USER_NAME_KEY = "username";
/**
* 用户真实名称 Key
*/
public static final String REAL_NAME_KEY = "realName";
}
为什么要封装这个常量类,这个常量肯定不是只在用户组件库中使用。因为在网关中,将用户 Token 进行解析,并放到 HTTP Header 中,最终放到用户请求上下文,也需要用到这些用户常量。(即网关需要解析token,并将获取的用户信息放入HTTP Header中传递给下游服务)
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String requestPath = request.getPath().toString();
if (isPathInBlackPreList(requestPath, config.getBlackPathPre())) {
String token = request.getHeaders().getFirst("Authorization");
UserInfoDTO userInfo = JWTUtil.parseJwtToken(token);
if (!validateToken(userInfo)) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
ServerHttpRequest.Builder builder = exchange.getRequest().mutate().headers(httpHeaders -> {
// 通过 HTTP Token 获取用户 ID 放入 HTTP Header 中
httpHeaders.set(UserConstant.USER_ID_KEY, userInfo.getUserId());
// 同上
httpHeaders.set(UserConstant.USER_NAME_KEY, userInfo.getUsername());
try {
// 同上
httpHeaders.set(UserConstant.REAL_NAME_KEY, URLEncoder.encode(userInfo.getRealName(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
});
return chain.filter(exchange.mutate().request(builder.build()).build());
}
return chain.filter(exchange);
};
}
全局过滤器顺序执行常量类
package org.opengoofy.index12306.framework.starter.bases.constant;
/**
* 全局过滤器顺序执行常量类
*
*/
public final class FilterOrderConstant {
/**
* 用户信息传递过滤器执行顺序排序
*/
public static final int USER_TRANSMIT_FILTER_ORDER = 100;
}
这个常量类主要限制过滤器、拦截器或 AOP 等组件的执行顺序,为什么要封装这个常量类:假设你的应用依赖了日志打印和幂等组件,方法执行时,需要打印日志和避免请求幂等,如下场景
@Log
@Idempotent
private boolean validateToken(UserInfoDTO userInfo) {
return userInfo != null;
}
如果不定义执行顺序,则有可能先触发幂等,一旦触发,日志组件将不再执行,对于生产排查不友好,因此每个组件开发者要考虑好自己开发的组件与全局组件的执行顺序问题
Spring 容器上下文
很多时候,我们也需要在非 Spring Bean 中使用到 Spring Bean。在 Spring Bean 中获取 Spring Bean,只需要通过构造器 或者 @Autowired[] 或者 @Autowired 就可以,但非Spring Bean则不行,比如大家定义线程池任务实现类时,可能需要获取到 Spring Bean,在这种场景下,我们依赖 Spring 提供的 ApplicationContextAware,这样就可以通过自定义容器访问 Spring IOC 容器获取 Spring Bean
package org.opengoofy.index12306.framework.starter.bases;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import java.lang.annotation.Annotation;
import java.util.Map;
/**
* Application context holder.
*
*/
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext CONTEXT;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextHolder.CONTEXT = applicationContext;
}
/**
* Get ioc container bean by type.
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return CONTEXT.getBean(clazz);
}
/**
* Get ioc container bean by name and type.
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return CONTEXT.getBean(name, clazz);
}
/**
* Get a set of ioc container beans by type.
*
* @param clazz
* @param <T>
* @return
*/
public static <T> Map<String, T> getBeansOfType(Class<T> clazz) {
return CONTEXT.getBeansOfType(clazz);
}
/**
* Find whether the bean has annotations.
*
* @param beanName
* @param annotationType
* @param <A>
* @return
*/
public static <A extends Annotation> A findAnnotationOnBean(String beanName, Class<A> annotationType) {
return CONTEXT.findAnnotationOnBean(beanName, annotationType);
}
/**
* Get ApplicationContext.
*
* @return
*/
public static ApplicationContext getInstance() {
return CONTEXT;
}
}
FastJson安全模式
我们封装的FastJson安全模式会关闭autoType特性
package org.opengoofy.index12306.framework.starter.bases.safa;
import org.springframework.beans.factory.InitializingBean;
/**
* FastJson安全模式,开启后关闭类型隐式传递
*
*/
public class FastJsonSafeMode implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.setProperty("fastjson2.parser.safeMode", "true");
}
}
如果配置文件中配置了 framework.fastjson.safa-mode=true 那么则开启安全模式,反之无任何变化
package org.opengoofy.index12306.framework.starter.bases.config;
import org.opengoofy.index12306.framework.starter.bases.ApplicationContextHolder;
import org.opengoofy.index12306.framework.starter.bases.init.ApplicationContentPostProcessor;
import org.opengoofy.index12306.framework.starter.bases.safa.FastJsonSafeMode;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
/**
* 应用基础自动装配
*
*/
public class ApplicationBaseAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "framework.fastjson.safa-mode", havingValue = "true")
public FastJsonSafeMode congoFastJsonSafeMode() {
return new FastJsonSafeMode();
}
}
安全初始化事件
有些场景是依赖 Spring 容器初始化完成后调用的,ContextRefreshedEvent 这个时间就比较合适。但是它除了初始化调用,容器刷新也会调用。
为了避免容器刷新造成二次调用初始化逻辑,我们对一些比较常用的事件简单封装了一层逻辑。
首先定义初始化事件对象类
package org.opengoofy.index12306.framework.starter.bases.init;
import org.springframework.context.ApplicationEvent;
/**
* 应用初始化事件
*
* <p> 规约事件,通过此事件可以查看业务系统所有初始化行为
*
*/
public class ApplicationInitializingEvent extends ApplicationEvent {
/**
* Create a new {@code ApplicationEvent}.
*
* @param source the object on which the event initially occurred or with
* which the event is associated (never {@code null})
*/
public ApplicationInitializingEvent(Object source) {
super(source);
}
}
其次,定义应用初始化后置处理器,防止 Spring 事件被多次执行
通过锁来保证同一时间只有一个事件进行初始化。初始化后设置一个标识,下次如果再触发,就不再执行,类似于幂等处理的逻辑
package org.opengoofy.index12306.framework.starter.bases.init;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
/**
* 应用初始化后置处理器,防止Spring事件被多次执行
*
* @公众号:马丁玩编程,回复:加群,添加马哥微信(备注:12306)获取项目资料
*/
@RequiredArgsConstructor
public class ApplicationContentPostProcessor implements ApplicationListener<ApplicationReadyEvent> {
private final ApplicationContext applicationContext;
/**
* 执行标识,确保Spring事件 {@link ApplicationReadyEvent} 有且执行一次
*/
private boolean executeOnlyOnce = true;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
synchronized (ApplicationContentPostProcessor.class) {
if (executeOnlyOnce) {
applicationContext.publishEvent(new ApplicationInitializingEvent(this));
executeOnlyOnce = false;
}
}
}
}
单例对象容器
package org.opengoofy.index12306.framework.starter.bases;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
* 单例对象容器
*
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class Singleton {
private static final ConcurrentHashMap<String, Object> SINGLE_OBJECT_POOL = new ConcurrentHashMap();
/**
* 根据 key 获取单例对象
*/
public static <T> T get(String key) {
Object result = SINGLE_OBJECT_POOL.get(key);
return result == null ? null : (T) result;
}
/**
* 根据 key 获取单例对象
*
* <p> 为空时,通过 supplier 构建单例对象并放入容器
*/
public static <T> T get(String key, Supplier<T> supplier) {
Object result = SINGLE_OBJECT_POOL.get(key);
if (result == null && (result = supplier.get()) != null) {
SINGLE_OBJECT_POOL.put(key, result);
}
return result != null ? (T) result : null;
}
/**
* 对象放入容器
*/
public static void put(Object value) {
put(value.getClass().getName(), value);
}
/**
* 对象放入容器
*/
public static void put(String key, Object value) {
SINGLE_OBJECT_POOL.put(key, value);
}
}