commit:同步1.4版本

This commit is contained in:
Jerry
2021-02-03 21:40:27 +08:00
parent 461b7a303b
commit 3a062ad619
290 changed files with 6973 additions and 4845 deletions

View File

@@ -0,0 +1,16 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记数据权限中基于DeptId进行过滤的字段。
*
* @author Jerry
* @date 2020-09-24
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeptFilterColumn {
}

View File

@@ -0,0 +1,17 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 作为DisableDataFilterAspect的切点。
* 该注解仅能标记在方法上方法内所有的查询语句均不会被Mybatis拦截器过滤数据。
*
* @author Jerry
* @date 2020-09-24
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DisableDataFilter {
}

View File

@@ -0,0 +1,28 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 仅用于微服务的多租户项目。
* 用于注解DAO层Mapper对象的租户过滤规则。被包含的方法将不会进行租户Id的过滤。
* 对于tk mapper和mybatis plus中的内置方法可以直接指定方法名即可selectOne。
* 需要说明的是在大多数场景下只要在实体对象中指定了租户Id字段基于该主表的绝大部分增删改操作
* 都需要经过租户Id过滤仅当查询非常复杂或者主表不在SQL语句之中的时候可以通过该注解禁用该SQL
* 并根据需求通过手动的方式实现租户过滤。
*
* @author Jerry
* @date 2020-09-24
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DisableTenantFilter {
/**
* 包含的方法名称数组。该值不能为空,因为如想取消所有方法的租户过滤,
* 可以通过在实体对象中不指定租户Id字段注解的方式实现。
*
* @return 被包括的方法名称数组。
*/
String[] includeMethodName();
}

View File

@@ -0,0 +1,27 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 用于注解DAO层Mapper对象的数据权限规则。
* 由于框架使用了tk.mapper所以并非所有的Mapper接口均在当前Mapper对象中定义有一部分被tk.mapper封装如selectAll等。
* 如果需要排除tk.mapper中的方法可以直接使用tk.mapper基类所声明的方法名称即可。
* 另外比较特殊的场景是因为tk.mapper是通用框架所以同样的selectAll方法可以获取不同的数据集合因此在service中如果
* 出现两个不同的方法调用Mapper的selectAll方法但是一个需要参与过滤另外一个不需要参与那么就需要修改当前类的Mapper方法
* 将其中一个方法重新定义一个具体的接口方法,并重新设定其是否参与数据过滤。
*
* @author Jerry
* @date 2020-09-24
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableDataPerm {
/**
* 排除的方法名称数组。如果为空所有的方法均会被Mybaits拦截注入权限过滤条件。
*
* @return 被排序的方法名称数据。
*/
String[] excluseMethodName() default {};
}

View File

@@ -0,0 +1,16 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记通过租户Id进行过滤的字段。
*
* @author Jerry
* @date 2020-09-24
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantFilterColumn {
}

View File

@@ -0,0 +1,16 @@
package com.orange.demo.common.core.annotation;
import java.lang.annotation.*;
/**
* 主要用于标记数据权限中基于UserId进行过滤的字段。
*
* @author Jerry
* @date 2020-09-24
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserFilterColumn {
}

View File

@@ -1,90 +0,0 @@
package com.orange.demo.common.core.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* 使用Caffeine作为本地缓存库
*
* @author Jerry
* @date 2020-09-24
*/
@Configuration
@EnableCaching
public class CacheConfig {
private static final int DEFAULT_MAXSIZE = 10000;
private static final int DEFAULT_TTL = 3600;
/**
* 定义cache名称、超时时长秒、最大个数
* 每个cache缺省3600秒过期最大个数1000
*/
public enum CacheEnum {
/**
* 专门存储用户权限的缓存。
*/
USER_PERMISSION_CACHE(1800, 10000),
/**
* session下上传文件名的缓存(时间是24小时)。
*/
UPLOAD_FILENAME_CACHE(86400, 20000),
/**
* 缺省全局缓存(时间是24小时)。
*/
GLOBAL_CACHE(86400, 20000);
CacheEnum() {
}
CacheEnum(int ttl, int maxSize) {
this.ttl = ttl;
this.maxSize = maxSize;
}
/**
* 缓存的最大数量。
*/
private int maxSize = DEFAULT_MAXSIZE;
/**
* 缓存的时长(单位:秒)
*/
private int ttl = DEFAULT_TTL;
public int getMaxSize() {
return maxSize;
}
public int getTtl() {
return ttl;
}
}
/**
* 初始化缓存配置。
*/
@Bean
public CacheManager cacheManager() {
SimpleCacheManager manager = new SimpleCacheManager();
// 把各个cache注册到cacheManager中CaffeineCache实现了org.springframework.cache.Cache接口
ArrayList<CaffeineCache> caches = new ArrayList<>();
for (CacheEnum c : CacheEnum.values()) {
caches.add(new CaffeineCache(c.name(),
Caffeine.newBuilder().recordStats()
.expireAfterAccess(c.getTtl(), TimeUnit.SECONDS)
.maximumSize(c.getMaxSize())
.build())
);
}
manager.setCaches(caches);
return manager;
}
}

View File

@@ -206,7 +206,7 @@ public class MyRequestArgumentResolver implements HandlerMethodArgumentResolver
}
}
} else if (parameterType == Boolean.class) {
return value.toString();
return value;
} else if (parameterType == Character.class) {
return value.toString().charAt(0);
}

View File

@@ -0,0 +1,52 @@
package com.orange.demo.common.core.object;
import cn.hutool.core.util.BooleanUtil;
/**
* 线程本地化数据管理的工具类。可根据需求自行添加更多的线程本地化变量及其操作方法。
*
* @author Jerry
* @date 2020-09-24
*/
public class GlobalThreadLocal {
/**
* 存储数据权限过滤是否启用的线程本地化对象。
* 目前的过滤条件,包括数据权限和租户过滤。
*/
private static final ThreadLocal<Boolean> DATA_FILTER_ENABLE = ThreadLocal.withInitial(() -> Boolean.TRUE);
/**
* 设置数据过滤是否打开。如果打开当前Servlet线程所执行的SQL操作均会进行数据过滤。
*
* @param enable 打开为true否则false。
* @return 返回之前的状态,便于恢复。
*/
public static boolean setDataFilter(boolean enable) {
boolean oldValue = DATA_FILTER_ENABLE.get();
DATA_FILTER_ENABLE.set(enable);
return oldValue;
}
/**
* 判断当前Servlet线程所执行的SQL操作是否进行数据过滤。
*
* @return true 进行数据权限过滤否则false。
*/
public static boolean enabledDataFilter() {
return BooleanUtil.isTrue(DATA_FILTER_ENABLE.get());
}
/**
* 清空该存储数据,主动释放线程本地化存储资源。
*/
public static void clearDataFilter() {
DATA_FILTER_ENABLE.remove();
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private GlobalThreadLocal() {
}
}

View File

@@ -24,6 +24,16 @@ public class TokenData {
* 用户Id。
*/
private Long userId;
/**
* 用户所在部门Id。
* 仅当系统支持uaa时可用否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。
*/
private Long deptId;
/**
* 租户Id。
* 仅当系统支持uaa时可用否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。
*/
private Long tenantId;
/**
* 是否为超级管理员。
*/
@@ -36,6 +46,11 @@ public class TokenData {
* 标识不同登录的会话Id。
*/
private String sessionId;
/**
* 访问uaa的授权token。
* 仅当系统支持uaa时可用否则可以直接忽略该字段。保留该字段是为了保持单体和微服务通用代码部分的兼容性。
*/
private String uaaAccessToken;
/**
* 将令牌对象添加到Http请求对象。

View File

@@ -0,0 +1,36 @@
package com.orange.demo.common.core.util;
/**
* Redis 键生成工具类。
*
* @author Jerry
* @date 2020-09-24
*/
public class RedisKeyUtil {
/**
* 计算SessionId关联的权限数据存储于Redis中的键。
*
* @param sessionId 会话Id。
* @return 会话关联的权限数据存储于Redis中的键值。
*/
public static String makeSessionPermIdKeyForRedis(String sessionId) {
return "PERM__" + sessionId;
}
/**
* 计算SessionId关联的数据权限数据存储于Redis中的键。
*
* @param sessionId 会话Id。
* @return 会话关联的数据权限数据存储于Redis中的键值。
*/
public static String makeSessionDataPermIdKeyForRedis(String sessionId) {
return "DATA_PERM__" + sessionId;
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
private RedisKeyUtil() {
}
}

View File

@@ -9,21 +9,26 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-swagger</artifactId>
<artifactId>common-redis</artifactId>
<version>1.0.0</version>
<name>common-swagger</name>
<name>common-redis</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>com.orange.demo</groupId>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,412 @@
package com.orange.demo.common.redis.cache;
import com.alibaba.fastjson.JSON;
import com.orange.demo.common.core.cache.DictionaryCache;
import com.orange.demo.common.core.constant.ApplicationConstant;
import com.orange.demo.common.core.exception.RedisCacheAccessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 字典数据Redis缓存对象。
*
* @param <K> 字典表主键类型。
* @param <V> 字典表对象类型。
* @author Jerry
* @date 2020-09-24
*/
@Slf4j
public class RedisDictionaryCache<K, V> implements DictionaryCache<K, V> {
/**
* redisson客户端。
*/
protected final RedissonClient redissonClient;
/**
* 数据存储对象。
*/
protected final RMap<K, String> dataMap;
/**
* 字典值对象类型。
*/
protected final Class<V> valueClazz;
/**
* 由于大部分场景是读取操作,所以使用读写锁提高并发的伸缩性。
*/
protected final ReadWriteLock lock;
/**
* 获取字典主键数据的函数对象。
*/
protected final Function<V, K> idGetter;
/**
* 超时时长。单位毫秒。
*/
protected static final long TIMEOUT = 2000L;
/**
* 当前对象的构造器函数。
*
* @param redissonClient Redisson的客户端对象。
* @param dictionaryName 字典表的名称。等同于redis hash对象的key。
* @param valueClazz 值对象的Class对象。
* @param idGetter 获取当前类主键字段值的函数对象。
* @param <K> 字典主键类型。
* @param <V> 字典对象类型
* @return 实例化后的字典内存缓存对象。
*/
public static <K, V> RedisDictionaryCache<K, V> create(
RedissonClient redissonClient,
String dictionaryName,
Class<V> valueClazz,
Function<V, K> idGetter) {
if (idGetter == null) {
throw new IllegalArgumentException("IdGetter can't be NULL.");
}
return new RedisDictionaryCache<>(redissonClient, dictionaryName, valueClazz, idGetter);
}
/**
* 构造函数。
*
* @param redissonClient Redisson的客户端对象。
* @param dictionaryName 字典表的名称。等同于redis hash对象的key。确保全局唯一。
* @param valueClazz 值对象的Class对象。
* @param idGetter 获取当前类主键字段值的函数对象。
*/
public RedisDictionaryCache(
RedissonClient redissonClient,
String dictionaryName,
Class<V> valueClazz,
Function<V, K> idGetter) {
this.redissonClient = redissonClient;
this.dataMap = redissonClient.getMap(dictionaryName + ApplicationConstant.DICT_CACHE_NAME_SUFFIX);
this.lock = new ReentrantReadWriteLock();
this.valueClazz = valueClazz;
this.idGetter = idGetter;
}
/**
* 按照数据插入的顺序返回全部字典对象的列表。
*
* @return 全部字段数据列表。
*/
@Override
public List<V> getAll() {
Collection<String> dataList;
String exceptionMessage;
try {
if (lock.readLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataList = dataMap.readAllValues();
} finally {
// 如果上面的操作时间超过redisson.lockWatchdogTimeout的时长
// redis会将与该锁关联的键删除此后调用unlock的时候就会抛出运行时异常。
lock.readLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::getAll] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
if (CollectionUtils.isEmpty(dataList)) {
return new LinkedList<>();
}
return dataList.stream()
.map(data -> JSON.parseObject(data, valueClazz))
.collect(Collectors.toCollection(LinkedList::new));
}
/**
* 获取缓存中与键列表对应的对象列表。
*
* @param keys 主键集合。
* @return 对象列表。
*/
@Override
public List<V> getInList(Set<K> keys) {
if (CollectionUtils.isEmpty(keys)) {
return new LinkedList<>();
}
Collection<String> dataList;
String exceptionMessage;
try {
if (lock.readLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataList = dataMap.getAll(keys).values();
} finally {
lock.readLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::getInList] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
if (dataList == null) {
return new LinkedList<>();
}
return dataList.stream()
.map(data -> JSON.parseObject(data, valueClazz))
.collect(Collectors.toCollection(LinkedList::new));
}
/**
* 从缓存中获取指定的数据。
*
* @param id 数据的key。
* @return 获取到的数据如果没有返回null。
*/
@Override
public V get(K id) {
if (id == null) {
return null;
}
String data;
String exceptionMessage;
try {
if (lock.readLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
data = dataMap.get(id);
} finally {
lock.readLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::get] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
if (data == null) {
return null;
}
return JSON.parseObject(data, valueClazz);
}
/**
* 获取缓存中数据条目的数量。
*
* @return 返回缓存的数据数量。
*/
@Override
public int getCount() {
return dataMap.size();
}
/**
* 将参数List中的数据保存到缓存中同时保证getAll返回的数据列表与参数列表中数据项的顺序保持一致。
*
* @param dataList 待缓存的数据列表。
*/
@Override
public void putAll(List<V> dataList) {
if (CollectionUtils.isEmpty(dataList)) {
return;
}
Map<K, String> map = dataList.stream()
.collect(Collectors.toMap(idGetter, JSON::toJSONString));
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataMap.putAll(map, 1000);
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::putAll] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
/**
* 将数据存入缓存。
*
* @param id 通常为字典数据的主键。
* @param data 字典数据对象。
*/
@Override
public void put(K id, V data) {
if (id == null || data == null) {
return;
}
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataMap.fastPut(id, JSON.toJSONString(data));
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::put] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
/**
* 重新加载先清空原有数据在执行putAll的操作。
*
* @param dataList 待缓存的数据列表。
* @param force true则强制刷新如果false当缓存中存在数据时不刷新。
*/
@Override
public void reload(List<V> dataList, boolean force) {
Map<K, String> map = null;
if (CollectionUtils.isNotEmpty(dataList)) {
map = dataList.stream().collect(Collectors.toMap(idGetter, JSON::toJSONString));
}
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
// 如果不强制刷新,需要先判断缓存中是否存在数据。
if (!force && this.getCount() > 0) {
return;
}
dataMap.clear();
if (map != null) {
dataMap.putAll(map, 1000);
}
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::reload] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
/**
* 删除缓存中指定的键。
*
* @param id 待删除数据的主键。
* @return 返回被删除的对象如果主键不存在返回null。
*/
@Override
public V invalidate(K id) {
if (id == null) {
return null;
}
String data;
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
data = dataMap.remove(id);
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::invalidate] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
if (data == null) {
return null;
}
return JSON.parseObject(data, valueClazz);
}
/**
* 删除缓存中,参数列表中包含的键。
*
* @param keys 待删除数据的主键集合。
*/
@SuppressWarnings("unchecked")
@Override
public void invalidateSet(Set<K> keys) {
if (CollectionUtils.isEmpty(keys)) {
return;
}
Object[] keyArray = keys.toArray(new Object[]{});
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataMap.fastRemove((K[]) keyArray);
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::invalidateSet] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
/**
* 清空缓存。
*/
@Override
public void invalidateAll() {
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataMap.clear();
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::invalidateAll] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
}

View File

@@ -0,0 +1,352 @@
package com.orange.demo.common.redis.cache;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import com.orange.demo.common.core.exception.RedisCacheAccessException;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
import org.apache.commons.collections4.CollectionUtils;
import org.redisson.api.RListMultimap;
import org.redisson.api.RedissonClient;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 树形字典数据Redis缓存对象。
*
* @param <K> 字典表主键类型。
* @param <V> 字典表对象类型。
* @author Jerry
* @date 2020-09-24
*/
@Slf4j
public class RedisTreeDictionaryCache<K, V> extends RedisDictionaryCache<K, V> {
/**
* 树形数据存储对象。
*/
private final RListMultimap<K, String> allTreeMap;
/**
* 获取字典父主键数据的函数对象。
*/
protected final Function<V, K> parentIdGetter;
/**
* 当前对象的构造器函数。
*
* @param redissonClient Redisson的客户端对象。
* @param dictionaryName 字典表的名称。等同于redis hash对象的key。
* @param valueClazz 值对象的Class对象。
* @param idGetter 获取当前类主键字段值的函数对象。
* @param parentIdGetter 获取当前类父主键字段值的函数对象。
* @param <K> 字典主键类型。
* @param <V> 字典对象类型
* @return 实例化后的树形字典内存缓存对象。
*/
public static <K, V> RedisTreeDictionaryCache<K, V> create(
RedissonClient redissonClient,
String dictionaryName,
Class<V> valueClazz,
Function<V, K> idGetter,
Function<V, K> parentIdGetter) {
if (idGetter == null) {
throw new IllegalArgumentException("IdGetter can't be NULL.");
}
if (parentIdGetter == null) {
throw new IllegalArgumentException("ParentIdGetter can't be NULL.");
}
return new RedisTreeDictionaryCache<>(
redissonClient, dictionaryName, valueClazz, idGetter, parentIdGetter);
}
/**
* 构造函数。
*
* @param redissonClient Redisson的客户端对象。
* @param dictionaryName 字典表的名称。等同于redis hash对象的key。
* @param valueClazz 值对象的Class对象。
* @param idGetter 获取当前类主键字段值的函数对象。
* @param parentIdGetter 获取当前类父主键字段值的函数对象。
*/
public RedisTreeDictionaryCache(
RedissonClient redissonClient,
String dictionaryName,
Class<V> valueClazz,
Function<V, K> idGetter,
Function<V, K> parentIdGetter) {
super(redissonClient, dictionaryName, valueClazz, idGetter);
this.allTreeMap = redissonClient.getListMultimap(dictionaryName + "-TREE-DICT");
this.parentIdGetter = parentIdGetter;
}
/**
* 获取该父主键的子数据列表。
*
* @param parentId 父主键Id。如果parentId为null则返回所有一级节点数据。
* @return 子数据列表。
*/
public List<V> getListByParentId(K parentId) {
List<String> dataList;
String exceptionMessage;
try {
if (lock.readLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataList = allTreeMap.get(parentId);
} finally {
lock.readLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisTreeDictionaryCache::getListByParentId] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
if (CollectionUtils.isEmpty(dataList)) {
return new LinkedList<>();
}
List<V> resultList = new LinkedList<>();
dataList.forEach(data -> resultList.add(JSON.parseObject(data, valueClazz)));
return resultList;
}
/**
* 将参数List中的数据保存到缓存中同时保证getAll返回的数据列表与参数列表中数据项的顺序保持一致。
*
* @param dataList 待缓存的数据列表。
*/
@Override
public void putAll(List<V> dataList) {
if (CollectionUtils.isEmpty(dataList)) {
return;
}
// 锁外执行数据结构组装,降低锁的粒度,提高并发性。
Map<K, String> map = dataList.stream()
.collect(Collectors.toMap(idGetter, JSON::toJSONString));
Multimap<K, String> treeMap = LinkedListMultimap.create();
for (V data : dataList) {
treeMap.put(parentIdGetter.apply(data), JSON.toJSONString(data));
}
Set<Map.Entry<K, Collection<String>>> entries = treeMap.asMap().entrySet();
String exceptionMessage;
try {
if (this.lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataMap.putAll(map, 1000);
for (Map.Entry<K, Collection<String>> entry : entries) {
allTreeMap.removeAll(entry.getKey());
allTreeMap.putAll(entry.getKey(), entry.getValue());
}
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisTreeDictionaryCache::putAll] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
/**
* 将数据存入缓存。
*
* @param id 通常为字典数据的主键。
* @param data 字典数据对象。
*/
@Override
public void put(K id, V data) {
if (id == null || data == null) {
return;
}
String stringData = JSON.toJSONString(data);
K parentId = parentIdGetter.apply(data);
String exceptionMessage;
try {
if (this.lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
String oldData = dataMap.put(id, stringData);
if (oldData != null) {
allTreeMap.remove(parentId, oldData);
}
allTreeMap.put(parentId, stringData);
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisTreeDictionaryCache::put] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
/**
* 行为等同于接口中的描述。这里之所以重写是因为不确定redisson的读写锁
* 是否为可重入锁。
*
* @param dataList 待缓存的数据列表。
* @param force true则强制刷新如果false当缓存中存在数据时不刷新。
*/
@Override
public void reload(List<V> dataList, boolean force) {
// 锁外执行数据结构组装,降低锁的粒度,提高并发性。
Map<K, String> map = null;
Set<Map.Entry<K, Collection<String>>> entries = null;
if (CollectionUtils.isNotEmpty(dataList)) {
map = dataList.stream().collect(Collectors.toMap(idGetter, JSON::toJSONString));
Multimap<K, String> treeMap = LinkedListMultimap.create();
for (V data : dataList) {
treeMap.put(parentIdGetter.apply(data), JSON.toJSONString(data));
}
entries = treeMap.asMap().entrySet();
}
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
// 如果不强制刷新,需要先判断缓存中是否存在数据。
if (!force && this.getCount() > 0) {
return;
}
dataMap.clear();
allTreeMap.clear();
if (map != null) {
dataMap.putAll(map, 1000);
for (Map.Entry<K, Collection<String>> entry : entries) {
allTreeMap.removeAll(entry.getKey());
allTreeMap.putAll(entry.getKey(), entry.getValue());
}
}
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisDictionaryCache::reload] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
/**
* 删除缓存中指定的键。
*
* @param id 待删除数据的主键。
* @return 返回被删除的对象如果主键不存在返回null。
*/
@Override
public V invalidate(K id) {
if (id == null) {
return null;
}
V data = null;
String exceptionMessage;
try {
if (this.lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
String stringData = dataMap.remove(id);
if (stringData != null) {
data = JSON.parseObject(stringData, valueClazz);
K parentId = parentIdGetter.apply(data);
allTreeMap.remove(parentId, stringData);
}
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisTreeDictionaryCache::invalidate] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
return data;
}
/**
* 删除缓存中,参数列表中包含的键。
*
* @param keys 待删除数据的主键集合。
*/
@Override
public void invalidateSet(Set<K> keys) {
if (CollectionUtils.isEmpty(keys)) {
return;
}
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
keys.forEach(id -> {
if (id != null) {
String stringData = dataMap.remove(id);
if (stringData != null) {
K parentId = parentIdGetter.apply(JSON.parseObject(stringData, valueClazz));
allTreeMap.remove(parentId, stringData);
}
}
});
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisTreeDictionaryCache::invalidateSet] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
/**
* 清空缓存。
*/
@Override
public void invalidateAll() {
String exceptionMessage;
try {
if (lock.writeLock().tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
try {
dataMap.clear();
allTreeMap.clear();
} finally {
lock.writeLock().unlock();
}
} else {
throw new TimeoutException();
}
} catch (Exception e) {
exceptionMessage = String.format(
"LOCK Operation of [RedisTreeDictionaryCache::invalidateAll] encountered EXCEPTION [%s] for DICT [%s].",
e.getClass().getSimpleName(), valueClazz.getSimpleName());
log.warn(exceptionMessage);
throw new RedisCacheAccessException(exceptionMessage, e);
}
}
}

View File

@@ -0,0 +1,67 @@
package com.orange.demo.common.redis.cache;
import com.google.common.collect.Maps;
import org.redisson.api.RedissonClient;
import org.redisson.spring.cache.CacheConfig;
import org.redisson.spring.cache.RedissonSpringCacheManager;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
/**
* 使用Redisson作为Redis的分布式缓存库。
*
* @author Jerry
* @date 2020-09-24
*/
@Configuration
@EnableCaching
public class RedissonCacheConfig {
private static final int DEFAULT_TTL = 3600000;
/**
* 定义cache名称、超时时长(毫秒)。
*/
public enum CacheEnum {
/**
* session下上传文件名的缓存(时间是24小时)。
*/
UPLOAD_FILENAME_CACHE(86400000),
/**
* 缺省全局缓存(时间是24小时)。
*/
GLOBAL_CACHE(86400000);
/**
* 缓存的时长(单位:毫秒)
*/
private int ttl = DEFAULT_TTL;
CacheEnum() {
}
CacheEnum(int ttl) {
this.ttl = ttl;
}
public int getTtl() {
return ttl;
}
}
/**
* 初始化缓存配置。
*/
@Bean
CacheManager cacheManager(RedissonClient redissonClient) {
Map<String, CacheConfig> config = Maps.newHashMap();
for (CacheEnum c : CacheEnum.values()) {
config.put(c.name(), new CacheConfig(c.getTtl(), 0));
}
return new RedissonSpringCacheManager(redissonClient, config);
}
}

View File

@@ -1,5 +1,7 @@
package com.orange.demo.common.core.cache;
package com.orange.demo.common.redis.cache;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.orange.demo.common.core.object.TokenData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
@@ -30,7 +32,7 @@ public class SessionCacheHelper {
public void putSessionUploadFile(String filename) {
if (filename != null) {
Set<String> sessionUploadFileSet = null;
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name());
Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name());
Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId());
if (valueWrapper != null) {
sessionUploadFileSet = (Set<String>) valueWrapper.get();
@@ -53,7 +55,7 @@ public class SessionCacheHelper {
if (filename == null) {
return false;
}
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name());
Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.UPLOAD_FILENAME_CACHE.name());
Cache.ValueWrapper valueWrapper = cache.get(TokenData.takeFromRequest().getSessionId());
if (valueWrapper == null) {
return false;
@@ -71,8 +73,8 @@ public class SessionCacheHelper {
if (sessionId == null || tokenData == null) {
return;
}
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.GLOBAL_CACHE.name());
cache.put(sessionId, tokenData);
Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.GLOBAL_CACHE.name());
cache.put(sessionId, JSON.toJSONString(tokenData));
}
/**
@@ -82,8 +84,9 @@ public class SessionCacheHelper {
* @return 当前会话的JWT Token对象
*/
public TokenData getTokenData(String sessionId) {
Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.GLOBAL_CACHE.name());
return cache.get(sessionId, TokenData.class);
Cache cache = cacheManager.getCache(RedissonCacheConfig.CacheEnum.GLOBAL_CACHE.name());
String tokenString = cache.get(sessionId, String.class);
return JSONObject.parseObject(tokenString, TokenData.class);
}
/**
@@ -92,7 +95,7 @@ public class SessionCacheHelper {
* @param sessionId 当前会话的SessionId
*/
public void removeAllSessionCache(String sessionId) {
for (CacheConfig.CacheEnum c : CacheConfig.CacheEnum.values()) {
for (RedissonCacheConfig.CacheEnum c : RedissonCacheConfig.CacheEnum.values()) {
cacheManager.getCache(c.name()).evict(sessionId);
}
}

View File

@@ -0,0 +1,52 @@
package com.orange.demo.common.redis.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* Redis配置类。
*
* @author Jerry
* @date 2020-09-24
*/
@Configuration
@ConditionalOnProperty(name = "redis.jedis.enabled", havingValue = "true")
public class JedisConfig {
@Value("${redis.jedis.port}")
private Integer port;
@Value("${redis.jedis.host}")
private String redisHost;
@Value("${redis.jedis.timeout}")
private int timeout;
@Value("${redis.jedis.pool.maxTotal}")
private Integer maxTotal;
@Value("${redis.jedis.pool.maxIdle}")
private Integer maxIdle;
@Value("${redis.jedis.pool.minIdle}")
private Integer minIdle;
@Value("${redis.jedis.pool.maxWait}")
private Integer maxWait;
@Bean
public JedisPool getJedisPool() {
// Jedis配置信息
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMinIdle(minIdle);
jedisPoolConfig.setMaxWaitMillis(maxWait);
jedisPoolConfig.setEvictorShutdownTimeoutMillis(2000);
return new JedisPool(jedisPoolConfig, redisHost, port);
}
}

View File

@@ -0,0 +1,50 @@
package com.orange.demo.common.redis.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Redisson配置类。和Jedis一样都是Redis客户端但是Redisson提供了更多的数据结构抽象。
* 这里我们只是使用了Redisson的分布式锁以及map等数据结构作为字典缓存使用。更多用法请参考其文档。
*
* @author Jerry
* @date 2020-09-24
*/
@Configuration
@ConditionalOnProperty(name = "redis.redisson.enabled", havingValue = "true")
public class RedissonConfig {
@Value("${redis.redisson.lockWatchdogTimeout}")
private Integer lockWatchdogTimeout;
@Value("${redis.redisson.address}")
private String address;
@Value("${redis.redisson.timeout}")
private Integer timeout;
@Value("${redis.redisson.pool.poolSize}")
private Integer poolSize;
@Value("${redis.redisson.pool.minIdle}")
private Integer minIdle;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 这里config还支持其他redis集群模式可根据实际需求更换。
// 比如useClusterServers()/useMasterSlaveServers()等。
config.setLockWatchdogTimeout(lockWatchdogTimeout)
.useSingleServer()
.setAddress("redis://" + address)
.setConnectionPoolSize(poolSize)
.setConnectionMinimumIdleSize(minIdle)
.setConnectTimeout(timeout);
return Redisson.create(config);
}
}

View File

@@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.orange.demo.common.redis.config.JedisConfig,\
com.orange.demo.common.redis.config.RedissonConfig

View File

@@ -1,56 +0,0 @@
package com.orange.demo.common.swagger.config;
import com.orange.demo.common.core.annotation.MyRequestBody;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* 自动加载bean的配置对象。
*
* @author Jerry
* @date 2020-09-24
*/
@EnableSwagger2
@EnableKnife4j
@EnableConfigurationProperties(SwaggerProperties.class)
@ConditionalOnProperty(prefix = "swagger", name = "enabled")
public class SwaggerAutoConfiguration {
@Bean
public Docket upmsDocket(SwaggerProperties properties) {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("1. 用户权限分组接口")
.ignoredParameterTypes(MyRequestBody.class)
.apiInfo(apiInfo(properties))
.select()
.apis(RequestHandlerSelectors.basePackage(properties.getBasePackage() + ".upms.controller"))
.paths(PathSelectors.any()).build();
}
@Bean
public Docket bizDocket(SwaggerProperties properties) {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("2. 业务应用分组接口")
.ignoredParameterTypes(MyRequestBody.class)
.apiInfo(apiInfo(properties))
.select()
.apis(RequestHandlerSelectors.basePackage(properties.getBasePackage() + ".app.controller"))
.paths(PathSelectors.any()).build();
}
private ApiInfo apiInfo(SwaggerProperties properties) {
return new ApiInfoBuilder()
.title(properties.getTitle())
.description(properties.getDescription())
.version(properties.getVersion()).build();
}
}

View File

@@ -1,40 +0,0 @@
package com.orange.demo.common.swagger.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 配置参数对象。
*
* @author Jerry
* @date 2020-09-24
*/
@Data
@ConfigurationProperties("swagger")
public class SwaggerProperties {
/**
* 是否开启Swagger。
*/
private Boolean enabled;
/**
* Swagger解析的基础包路径。
**/
private String basePackage = "";
/**
* ApiInfo中的标题。
**/
private String title = "";
/**
* ApiInfo中的描述信息。
**/
private String description = "";
/**
* ApiInfo中的版本信息。
**/
private String version = "";
}

View File

@@ -1,85 +0,0 @@
package com.orange.demo.common.swagger.plugin;
import cn.hutool.core.lang.Assert;
import com.orange.demo.common.core.annotation.MyRequestBody;
import com.github.xiaoymin.knife4j.core.conf.Consts;
import javassist.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import springfox.documentation.service.ResolvedMethodParameter;
import java.util.List;
/**
* 通过字节码方式动态创建接口参数封装对象。
*
* @author Jerry
* @date 2020-09-24
*/
@Slf4j
public class ByteBodyUtils {
private static final ClassPool CLASS_POOL = ClassPool.getDefault();
public static Class<?> createDynamicModelClass(String name, List<ResolvedMethodParameter> parameters) {
String clazzName = Consts.BASE_PACKAGE_PREFIX + name;
try {
CtClass tmp = CLASS_POOL.getCtClass(clazzName);
if (tmp != null) {
tmp.detach();
}
} catch (NotFoundException e) {
// 需要吃掉这个异常。
}
CtClass ctClass = CLASS_POOL.makeClass(clazzName);
try {
int fieldCount = 0;
for (ResolvedMethodParameter dynamicParameter : parameters) {
// 因为在调用这个方法之前这些参数都包含MyRequestBody注解。
MyRequestBody myRequestBody =
dynamicParameter.findAnnotation(MyRequestBody.class).orNull();
Assert.notNull(myRequestBody);
String fieldName = dynamicParameter.defaultName().isPresent()
? dynamicParameter.defaultName().get() : "parameter";
if (StringUtils.isNotBlank(myRequestBody.value())) {
fieldName = myRequestBody.value();
}
ctClass.addField(createField(dynamicParameter, fieldName, ctClass));
fieldCount++;
}
if (fieldCount > 0) {
return ctClass.toClass();
}
} catch (Throwable e) {
log.error(e.getMessage());
}
return null;
}
private static CtField createField(ResolvedMethodParameter parameter, String parameterName, CtClass ctClass)
throws NotFoundException, CannotCompileException {
CtField field = new CtField(getFieldType(parameter.getParameterType().getErasedType()), parameterName, ctClass);
field.setModifiers(Modifier.PUBLIC);
return field;
}
private static CtClass getFieldType(Class<?> propetyType) {
CtClass fieldType = null;
try {
if (!propetyType.isAssignableFrom(Void.class)) {
fieldType = CLASS_POOL.get(propetyType.getName());
} else {
fieldType = CLASS_POOL.get(String.class.getName());
}
} catch (NotFoundException e) {
// 抛异常
ClassClassPath path = new ClassClassPath(propetyType);
CLASS_POOL.insertClassPath(path);
try {
fieldType = CLASS_POOL.get(propetyType.getName());
} catch (NotFoundException e1) {
log.error(e1.getMessage(), e1);
}
}
return fieldType;
}
}

View File

@@ -1,61 +0,0 @@
package com.orange.demo.common.swagger.plugin;
import com.orange.demo.common.core.annotation.MyRequestBody;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.CaseFormat;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.OperationModelsProviderPlugin;
import springfox.documentation.spi.service.contexts.RequestMappingContext;
import java.util.List;
import java.util.stream.Collectors;
/**
* 生成参数包装类的插件。
*
* @author Jerry
* @date 2020-09-24
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 200)
@ConditionalOnProperty(prefix = "swagger", name = "enabled")
public class DynamicBodyModelPlugin implements OperationModelsProviderPlugin {
private final TypeResolver typeResolver;
public DynamicBodyModelPlugin(TypeResolver typeResolver) {
this.typeResolver = typeResolver;
}
@Override
public void apply(RequestMappingContext context) {
List<ResolvedMethodParameter> parameterTypes = context.getParameters();
if (CollectionUtils.isEmpty(parameterTypes)) {
return;
}
List<ResolvedMethodParameter> bodyParameter = parameterTypes.stream()
.filter(p -> p.hasParameterAnnotation(MyRequestBody.class)).collect(Collectors.toList());
if (CollectionUtils.isEmpty(bodyParameter)) {
return;
}
String groupName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, context.getGroupName());
String clazzName = groupName + StringUtils.capitalize(context.getName());
Class<?> clazz = ByteBodyUtils.createDynamicModelClass(clazzName, bodyParameter);
if (clazz != null) {
context.operationModelsBuilder().addInputParam(typeResolver.resolve(clazz));
}
}
@Override
public boolean supports(DocumentationType delimiter) {
// 支持2.0版本
return delimiter == DocumentationType.SWAGGER_2;
}
}

View File

@@ -1,64 +0,0 @@
package com.orange.demo.common.swagger.plugin;
import com.orange.demo.common.core.annotation.MyRequestBody;
import com.google.common.base.CaseFormat;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.Parameter;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.ParameterContext;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 构建操作接口参数对象的插件。
*
* @author Jerry
* @date 2020-09-24
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 102)
@ConditionalOnProperty(prefix = "swagger", name = "enabled")
public class DynamicBodyParameterBuilder implements OperationBuilderPlugin {
@Override
public void apply(OperationContext context) {
List<ResolvedMethodParameter> methodParameters = context.getParameters();
List<Parameter> parameters = new ArrayList<>();
if (CollectionUtils.isNotEmpty(methodParameters)) {
List<ResolvedMethodParameter> bodyParameter = methodParameters.stream()
.filter(p -> p.hasParameterAnnotation(MyRequestBody.class)).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(bodyParameter)) {
// 构造model
String groupName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, context.getGroupName());
String clazzName = groupName + StringUtils.capitalize(context.getName());
ResolvedMethodParameter methodParameter = bodyParameter.get(0);
ParameterContext parameterContext = new ParameterContext(methodParameter,
new ParameterBuilder(),
context.getDocumentationContext(),
context.getGenericsNamingStrategy(),
context);
Parameter parameter = parameterContext.parameterBuilder()
.parameterType("body").modelRef(new ModelRef(clazzName)).name(clazzName).build();
parameters.add(parameter);
}
}
context.operationBuilder().parameters(parameters);
}
@Override
public boolean supports(DocumentationType delimiter) {
return delimiter == DocumentationType.SWAGGER_2;
}
}

View File

@@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.orange.demo.common.swagger.config.SwaggerAutoConfiguration

View File

@@ -13,7 +13,7 @@
<modules>
<module>common-core</module>
<module>common-redis</module>
<module>common-sequence</module>
<module>common-swagger</module>
</modules>
</project>