前言
在做
RBAC
权限控制的时候,通常会在权限表中存有接口的对应信息,手动添加如此多的接口会很麻烦,也很容易出错。有没有办法在Spring项目启动时就可以扫描所有接口并自动加入数据库呢的方法呢?
借助ApplicationListener接口
ApplicationListener
是org.springframework.context
包下的接口,它能监听容器中发布的事件。这里不做过多介绍,具体可查看->
理解 Spring ApplicationListener
实现ApplicationListener接口
- 新建
ApiScan
实现ApplicationListener<ApplicationReadyEvent>
接口,重写void onApplicationEvent(ApplicationReadyEvent event)
方法
@Slf4j
@Component // 注入到容器中
@ConditionalOnWebApplication // 判断是否为Web应用,如果不是则不注入
public class ApiScan implements ApplicationListener<ApplicationReadyEvent> {
private static final AntPathMatcher pathMatch = new AntPathMatcher();
private ExecutorService executorService = Executors.newFixedThreadPool(2);
// @Autowired
// private BaseApiService baseApiService;
/**
* 初始化方法
*
* @param event
*/
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
ConfigurableApplicationContext applicationContext = event.getApplicationContext();
Environment env = applicationContext.getEnvironment();
// 服务名称
String serviceId = env.getProperty("spring.application.name", "application");
// 所有接口映射
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
// 获取url与类和方法的对应信息
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
List<RequestMatcher> permitAll = Lists.newArrayList();
// Spring Security相关信息
try {
// 获取所有安全配置适配器
Map<String, WebSecurityConfigurerAdapter> securityConfigurerAdapterMap = applicationContext.getBeansOfType(WebSecurityConfigurerAdapter.class);
Iterator<Map.Entry<String, WebSecurityConfigurerAdapter>> iterable = securityConfigurerAdapterMap.entrySet().iterator();
while (iterable.hasNext()) {
WebSecurityConfigurerAdapter configurer = iterable.next().getValue();
HttpSecurity httpSecurity = (HttpSecurity) ReflectionUtils.getFieldValue(configurer, "http");
FilterSecurityInterceptor filterSecurityInterceptor = httpSecurity.getSharedObject(FilterSecurityInterceptor.class);
FilterInvocationSecurityMetadataSource metadataSource = filterSecurityInterceptor.getSecurityMetadataSource();
Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = (Map) ReflectionUtils.getFieldValue(metadataSource, "requestMap");
Iterator<Map.Entry<RequestMatcher, Collection<ConfigAttribute>>> requestIterable = requestMap.entrySet().iterator();
while (requestIterable.hasNext()) {
Map.Entry<RequestMatcher, Collection<ConfigAttribute>> match = requestIterable.next();
if (match.getValue().toString().contains("permitAll")) {
permitAll.add(match.getKey());
}
}
}
} catch (Exception e) {
log.error("error:{}", e);
}
for (Map.Entry<RequestMappingInfo, HandlerMethod> m : map.entrySet()) {
RequestMappingInfo info = m.getKey();
HandlerMethod method = m.getValue();
/*if (method.getMethod().getDeclaringClass().getAnnotation(RestController.class) == null) {
// 只扫描RestController
continue;
}*/
if (method.getMethodAnnotation(ApiIgnore.class) != null) {
// 忽略的接口不扫描
continue;
}
Set<MediaType> mediaTypeSet = info.getProducesCondition().getProducibleMediaTypes();
for (MethodParameter params : method.getMethodParameters()) {
if (params.hasParameterAnnotation(RequestBody.class)) {
mediaTypeSet.add(MediaType.APPLICATION_JSON_UTF8);
break;
}
}
String mediaTypes = getMediaTypes(mediaTypeSet);
// 请求类型
RequestMethodsRequestCondition methodsCondition = info.getMethodsCondition();
String methods = getMethods(methodsCondition.getMethods());
// 请求路径
PatternsRequestCondition p = info.getPatternsCondition();
// 请求路径可能会有多个,用‘,’分开
String urls = getUrls(p.getPatterns());
// 类名
String className = method.getMethod().getDeclaringClass().getName();
// 方法名
String methodName = method.getMethod().getName();
String fullName = className + "." + methodName;
// md5码,根据服务id,请求路径,方法生成,用于判断唯一性
String md5 = EncryptUtils.md5Hex(serviceId + urls + methods);
String name = "";
String desc = "";
// 是否需要安全认证 默认:1-是 0-否
Integer isAuth = 1;
// 匹配项目中.permitAll()配置
for (String url : p.getPatterns()) {
for (RequestMatcher requestMatcher : permitAll) {
if (requestMatcher instanceof AntPathRequestMatcher) {
AntPathRequestMatcher pathRequestMatcher = (AntPathRequestMatcher) requestMatcher;
if (pathMatch.match(pathRequestMatcher.getPattern(), url)) {
// 忽略验证
isAuth = 0;
}
}
}
}
ApiOperation apiOperation = method.getMethodAnnotation(ApiOperation.class);
if (apiOperation != null) {
name = apiOperation.value();
desc = apiOperation.notes();
}
name = StringUtils.isBlank(name) ? methodName : name;
BaseApi baseApi = new BaseApi();
baseApi.setApiCode(md5);
baseApi.setApiName(name);
baseApi.setApiDesc(desc);
baseApi.setRequestMethod(methods);
baseApi.setContentType(mediaTypes);
baseApi.setServiceId(serviceId);
baseApi.setPath(urls);
baseApi.setIsAuth(isAuth);
baseApi.setClassName(className);
baseApi.setMethodName(methodName);
// 多线程减轻压力
executorService.submit(new Runnable() {
@Override
public void run() {
try {
// 根据md5码从数据库查找接口
// BaseApi save = baseApiService.getApi(baseApi.getApiCode());
// 判断接口数据是否存在
// if (save == null) {
// 不存在则初始化并添加
// baseApi.setIsOpen(0);
// baseApi.setIsPersist(1);
// baseApiService.addApi(baseApi);
// } else {
// 存在则更新信息
// baseApi.setApiId(save.getApiId());
// baseApiService.updateApi(baseApi);
// }
} catch (Exception e) {
log.error("接口存储失败:{}", e);
}
}
});
}
log.info("ApplicationReadyEvent:[{}]", serviceId);
}
/**
* 功能描述: 获取媒体类型,即content-type
*
* @param mediaTypes
* @return java.lang.String
*/
private String getMediaTypes(Set<MediaType> mediaTypes) {
StringBuilder sbf = new StringBuilder();
for (MediaType mediaType : mediaTypes) {
sbf.append(mediaType.toString()).append(",");
}
if (mediaTypes.size() > 0) {
sbf.deleteCharAt(sbf.length() - 1);
}
return sbf.toString();
}
/**
* 功能描述: 获取请求方法
*
* @param requestMethods
* @return java.lang.String
*/
private String getMethods(Set<RequestMethod> requestMethods) {
StringBuilder sbf = new StringBuilder();
for (RequestMethod requestMethod : requestMethods) {
sbf.append(requestMethod.toString()).append(",");
}
if (requestMethods.size() > 0) {
sbf.deleteCharAt(sbf.length() - 1);
}
return sbf.toString();
}
/**
* 功能描述: 获取请求路径列表,使用','分割
*
* @param urls
* @return java.lang.String
*/
private String getUrls(Set<String> urls) {
StringBuilder sbf = new StringBuilder();
for (String url : urls) {
sbf.append(url).append(",");
}
if (urls.size() > 0) {
sbf.deleteCharAt(sbf.length() - 1);
}
return sbf.toString();
}
}
- 接口数据库映射
public class BaseApi implements Serializable {
/**
* 接口ID
*/
private Long apiId;
/**
* 接口编码
*/
private String apiCode;
/**
* 接口名称
*/
private String apiName;
/**
* 接口分类:default-默认分类
*/
private String apiCategory;
/**
* 资源描述
*/
private String apiDesc;
/**
* 请求方式
*/
private String requestMethod;
/**
* 响应类型
*/
private String contentType;
/**
* 服务ID
*/
private String serviceId;
/**
* 请求路径
*/
private String path;
/**
* 优先级
*/
private Integer priority;
/**
* 状态:0-无效 1-有效
*/
private Integer status;
/**
* 保留数据0-否 1-是 不允许删除
*/
private Integer isPersist;
/**
* 是否需要认证: 0-无认证 1-身份认证 默认:1
*/
private Integer isAuth;
/**
* 是否公开: 0-内部的 1-公开的
*/
private Integer isOpen;
/**
* 类名
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
别忘了注入,否则不生效
- Spring Boot通过
@Component
注入,即上面所示 - Spring Boot通过
@Configuration
加@Bean
注入,更加灵活 - Spring可通过
<bean>
标签注入
问题
- 上述涵盖了一个接口的所有基本属性,可根据项目情况添加修改或删除
- 如没有
Swagger
和Spring Security
也可删除相应内容 - 用于判断接口唯一性的
md5码
根据服务id(及项目名)
,请求路径列表
和请求方法列表
生成,也可根据实际情况选择生成
网友评论