1. liugh-parent源码研究参考
1.1. 前言
- 这也是个开源的springboot脚手架项目,这里研究记录一些该框架写的比较好的代码段和功能
- 脚手架地址
1.2. 功能
1.2.1. 当前用户
- 这里它用了注解切面进行登录用户的统一注入入口参数,这个做法可以进行参考,不需要在需要使用到登录用户的地方用对象去取了
import com.liugh.annotation.CurrentUser;
import com.liugh.exception.UnauthorizedException;
import com.liugh.entity.User;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* 增加方法注入,将含有 @CurrentUser 注解的方法参数注入当前登录用户
* @author liugh
* @since 2018-05-03
*/
public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(User.class)
&& parameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
User user = (User) webRequest.getAttribute("currentUser", RequestAttributes.SCOPE_REQUEST);
if (user == null) {
throw new UnauthorizedException("获取用户信息失败");
}
return user;
}
}
/**
* 身份认证异常
* @author liugh
* @since 2018-05-06
*/
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String msg) {
super(msg);
}
public UnauthorizedException() {
super();
}
}
/**
* 在Controller的方法参数中使用此注解,该方法在映射时会注入当前登录的User对象
* @author : liugh
* @date : 2018/05/08
*/
@Target(ElementType.PARAMETER) // 可用在方法的参数上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
public @interface CurrentUser {
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
/**
* @author liugh
* @since 2018-05-03
*/
@Configuration
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(currentUserMethodArgumentResolver());
super.addArgumentResolvers(argumentResolvers);
}
@Bean
public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() {
return new CurrentUserMethodArgumentResolver();
}
}
1.2.2. 令牌桶
- 这是一种限流思路,令牌桶算法请自行百度,这里也不是从零实现,用到了
guava
中的RateLimiter
限流器
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import com.liugh.annotation.AccessLimit;
import com.liugh.base.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 限流切面
* Created by liugh on 2018/10/12.
*/
@Slf4j
public class AccessLimitAspect extends AbstractAspectManager{
public AccessLimitAspect(AspectApi aspectApi){
super(aspectApi);
}
@Override
public Object doHandlerAspect(ProceedingJoinPoint pjp, Method method)throws Throwable {
super.doHandlerAspect(pjp,method);
execute(pjp,method);
return null;
}
//添加速率.保证是单例的
private static RateLimiter rateLimiter = RateLimiter.create(1000);
//使用url做为key,存放令牌桶 防止每次重新创建令牌桶
private static Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
@Override
public Object execute(ProceedingJoinPoint pjp,Method method) throws Throwable{
AccessLimit lxRateLimit = method.getAnnotation(AccessLimit.class);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 或者url(存在map集合的key)
String url = request.getRequestURI();
if (!limitMap.containsKey(url)) {
// 创建令牌桶
rateLimiter = RateLimiter.create(lxRateLimit.perSecond());
limitMap.put(url, rateLimiter);
log.info("<<================= 请求{},创建令牌桶,容量{} 成功!!!",url,lxRateLimit.perSecond());
}
rateLimiter = limitMap.get(url);
if (!rateLimiter.tryAcquire(lxRateLimit.timeOut(), lxRateLimit.timeOutUnit())) {//获取令牌
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("Error ---时间:{},获取令牌失败.", sdf.format(new Date()));
throw new BusinessException("服务器繁忙,请稍后再试!");
}
return null;
}
}
import com.liugh.annotation.AccessLimit;
import com.liugh.annotation.Log;
import com.liugh.annotation.ParamXssPass;
import com.liugh.annotation.ValidationParam;
import com.liugh.aspect.*;
import com.liugh.util.ComUtil;
import com.liugh.util.StringUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
/**
* 切面:防止xss攻击 记录log 参数验证
* @author liugh
* @since 2018-05-03
*/
@Aspect
@Configuration
public class ControllerAspect {
@Pointcut("execution(* com.liugh.controller..*(..)) ")
public void aspect() {
}
@Around(value = "aspect()")
public Object validationPoint(ProceedingJoinPoint pjp)throws Throwable{
Method method = currentMethod(pjp,pjp.getSignature().getName());
//创建被装饰者
AspectApiImpl aspectApi = new AspectApiImpl();
//是否需要验证参数
if (!ComUtil.isEmpty(StringUtil.getMethodAnnotationOne(method, ValidationParam.class.getSimpleName()))) {
new ValidationParamAspect(aspectApi).doHandlerAspect(pjp,method);
}
//是否需要限流
if (method.isAnnotationPresent(AccessLimit.class)) {
new AccessLimitAspect(aspectApi).doHandlerAspect(pjp,method);
}
//是否需要拦截xss攻击
if(method.isAnnotationPresent( ParamXssPass.class )){
new ParamXssPassAspect(aspectApi).doHandlerAspect(pjp,method);
}
//是否需要记录日志
if(method.isAnnotationPresent(Log.class)){
return new RecordLogAspect(aspectApi).doHandlerAspect(pjp,method);
}
return pjp.proceed(pjp.getArgs());
}
/**
* 获取目标类的所有方法,找到当前要执行的方法
*/
private Method currentMethod ( ProceedingJoinPoint joinPoint , String methodName ) {
Method[] methods = joinPoint.getTarget().getClass().getMethods();
Method resultMethod = null;
for ( Method method : methods ) {
if ( method.getName().equals( methodName ) ) {
resultMethod = method;
break;
}
}
return resultMethod;
}
}
- 上面的代码还包含了参数验证,xss攻击拦截,日志记录,这些比较常用功能,感兴趣的自行下载代码浏览细节
1.2.3. 异步日志记录
- 日志的异步记录,这是个好思路,日志记录不影响主任务,可以改成异步加快速度
/**
* 线程池配置、启用异步
*
* @author liugh
*
*/
//开启异步
@EnableAsync(proxyTargetClass = true)
@Configuration
public class AsycTaskExecutorConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心线程数
taskExecutor.setCorePoolSize(50);
//最大线程数
taskExecutor.setMaxPoolSize(100);
return taskExecutor;
}
}
import java.lang.reflect.Method;
import java.util.Map;
import com.alibaba.fastjson.JSONObject;
import com.liugh.annotation.Log;
import com.liugh.service.SpringContextBeanService;
import com.liugh.entity.OperationLog;
import com.liugh.service.IOperationLogService;
import com.liugh.util.ComUtil;
import com.liugh.util.JWTUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 记录日志切面
* @author liugh
* @since on 2018/5/10.
*/
public class RecordLogAspect extends AbstractAspectManager {
public RecordLogAspect(AspectApi aspectApi){
super(aspectApi);
}
@Override
public Object doHandlerAspect(ProceedingJoinPoint pjp, Method method) throws Throwable{
super.doHandlerAspect(pjp,method);
return execute(pjp,method);
}
private Logger logger = LoggerFactory.getLogger(RecordLogAspect.class);
@Override
@Async
protected Object execute(ProceedingJoinPoint pjp, Method method) throws Throwable{
Log log = method.getAnnotation( Log.class );
// 异常日志信息
String actionLog = null;
StackTraceElement[] stackTrace =null;
// 是否执行异常
boolean isException = false;
// 接收时间戳
long endTime;
// 开始时间戳
long operationTime = System.currentTimeMillis();
try {
return pjp.proceed(pjp.getArgs());
} catch ( Throwable throwable ) {
isException = true;
actionLog = throwable.getMessage();
stackTrace = throwable.getStackTrace();
throw throwable;
} finally {
// 日志处理
logHandle( pjp , method , log , actionLog , operationTime , isException,stackTrace );
}
}
private void logHandle (ProceedingJoinPoint joinPoint ,
Method method ,
Log log ,
String actionLog ,
long startTime ,
boolean isException,
StackTraceElement[] stackTrace) {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
IOperationLogService operationLogService = SpringContextBeanService.getBean(IOperationLogService.class);
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
String authorization = request.getHeader("Authorization");
OperationLog operationLog = new OperationLog();
if(!ComUtil.isEmpty(authorization)){
String userNo = JWTUtil.getUserNo(authorization);
operationLog.setUserNo(userNo);
}
operationLog.setIp(getIpAddress(request));
operationLog.setClassName(joinPoint.getTarget().getClass().getName() );
operationLog.setCreateTime(startTime);
operationLog.setLogDescription(log.description());
operationLog.setModelName(log.modelName());
operationLog.setAction(log.action());
if(isException){
StringBuilder sb = new StringBuilder();
sb.append(actionLog+" ");
for (int i = 0; i < stackTrace.length; i++) {
sb.append(stackTrace[i]+" ");
}
operationLog.setMessage(sb.toString());
}
operationLog.setMethodName(method.getName());
operationLog.setSucceed(isException == true ? 2:1);
Object[] args = joinPoint.getArgs();
StringBuilder sb = new StringBuilder();
boolean isJoint = false;
for (int i = 0; i < args.length; i++) {
if(args[i] instanceof JSONObject){
JSONObject parse = (JSONObject)JSONObject.parse(args[i].toString());
if(!ComUtil.isEmpty(parse.getString("password"))){
parse.put("password","*******");
}
if(!ComUtil.isEmpty(parse.getString("rePassword"))){
parse.put("rePassword","*******");
}
if(!ComUtil.isEmpty(parse.getString("oldPassword"))){
parse.put("oldPassword","*******");
}
operationLog.setActionArgs(parse.toString());
}else if(args[i] instanceof String
|| args[i] instanceof Long
|| args[i] instanceof Integer
|| args[i] instanceof Double
|| args[i] instanceof Float
|| args[i] instanceof Byte
|| args[i] instanceof Short
|| args[i] instanceof Character){
isJoint=true;
}
else if(args[i] instanceof String []
|| args[i] instanceof Long []
|| args[i] instanceof Integer []
|| args[i] instanceof Double []
|| args[i] instanceof Float []
|| args[i] instanceof Byte []
|| args[i] instanceof Short []
|| args[i] instanceof Character []){
Object[] strs = (Object[])args[i];
StringBuilder sbArray =new StringBuilder();
sbArray.append("[");
for (Object str:strs) {
sbArray.append(str.toString()+",");
}
sbArray.deleteCharAt(sbArray.length()-1);
sbArray.append("]");
operationLog.setActionArgs(sbArray.toString());
}else {
continue;
}
}
if(isJoint){
Map<String, String[]> parameterMap = request.getParameterMap();
for (String key:parameterMap.keySet()) {
String[] strings = parameterMap.get(key);
for (String str:strings) {
sb.append(key+"="+str+"&");
}
}
operationLog.setActionArgs(sb.deleteCharAt(sb.length()-1).toString());
}
logger.info("执行方法信息:"+JSONObject.toJSON(operationLog));
operationLogService.insert(operationLog);
}
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip+":"+request.getRemotePort();
}
}
1.2.4. 启动初始化扫描
- 启动时扫描对应包,然后做相应处理,这里是把对应接口记录后用于后续
pass
,也就是通过@Pass
不认证处理
import com.liugh.annotation.Pass;
import com.liugh.base.Constant;
import com.liugh.util.ComUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author liugh
* @Since 2018-05-10
*/
@Component
//日志打印 log.info
@Slf4j
public class MyCommandLineRunner implements CommandLineRunner {
@Value("${controller.scanPackage}")
private String scanPackage;
@Override
public void run(String... args) throws Exception {
doScanner(scanPackage);
Set<String> urlAndMethodSet =new HashSet<>();
for (String aClassName:Constant.METHOD_URL_SET) {
Class<?> clazz = Class.forName(aClassName);
String baseUrl = "";
String[] classUrl ={};
if(!ComUtil.isEmpty(clazz.getAnnotation(RequestMapping.class))){
classUrl=clazz.getAnnotation(RequestMapping.class).value();
}
Method[] methods = clazz.getMethods();
for (Method method:methods) {
if(method.isAnnotationPresent(Pass.class)){
String [] methodUrl = null;
StringBuilder sb =new StringBuilder();
if(!ComUtil.isEmpty(method.getAnnotation(PostMapping.class))){
methodUrl=method.getAnnotation(PostMapping.class).value();
if(ComUtil.isEmpty(methodUrl)){
methodUrl=method.getAnnotation(PostMapping.class).path();
}
baseUrl=getRequestUrl(classUrl, methodUrl, sb,"POST");
}else if(!ComUtil.isEmpty(method.getAnnotation(GetMapping.class))){
methodUrl=method.getAnnotation(GetMapping.class).value();
if(ComUtil.isEmpty(methodUrl)){
methodUrl=method.getAnnotation(GetMapping.class).path();
}
baseUrl=getRequestUrl(classUrl, methodUrl, sb,"GET");
}else if(!ComUtil.isEmpty(method.getAnnotation(DeleteMapping.class))){
methodUrl=method.getAnnotation(DeleteMapping.class).value();
if(ComUtil.isEmpty(methodUrl)){
methodUrl=method.getAnnotation(DeleteMapping.class).path();
}
baseUrl=getRequestUrl(classUrl, methodUrl, sb,"DELETE");
}else if(!ComUtil.isEmpty(method.getAnnotation(PutMapping.class))){
methodUrl=method.getAnnotation(PutMapping.class).value();
if(ComUtil.isEmpty(methodUrl)){
methodUrl=method.getAnnotation(PutMapping.class).path();
}
baseUrl=getRequestUrl(classUrl, methodUrl, sb,"PUT");
}else {
methodUrl=method.getAnnotation(RequestMapping.class).value();
baseUrl=getRequestUrl(classUrl, methodUrl, sb,RequestMapping.class.getSimpleName());
}
if(!ComUtil.isEmpty(baseUrl)){
urlAndMethodSet.add(baseUrl);
}
}
}
}
Constant.METHOD_URL_SET=urlAndMethodSet;
log.info("@Pass:"+urlAndMethodSet);
}
private String getRequestUrl(String[] classUrl, String[] methodUrl, StringBuilder sb,String requestName) {
sb.append("/api/v1");
if(!ComUtil.isEmpty(classUrl)){
for (String url:classUrl) {
sb.append(url+"/");
}
}
for (String url:methodUrl) {
sb.append(url);
}
if(sb.toString().endsWith("/")){
sb.deleteCharAt(sb.length()-1);
}
return sb.toString().replaceAll("/+", "/")+":--:"+requestName;
}
private void doScanner(String packageName) {
//把所有的.替换成/
URL url =this.getClass().getClassLoader().getResource(packageName.replaceAll("\\.", "/"));
// 是否循环迭代
if(StringUtils.countMatches(url.getFile(), ".jar")>0){
boolean recursive=true;
JarFile jar;
// 获取jar
try {
jar = ((JarURLConnection) url.openConnection())
.getJarFile();
// 从此jar包 得到一个枚举类
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
JarEntry entry = entries.nextElement();
String name = entry.getName();
// 如果是以/开头的
if (name.charAt(0) == '/') {
// 获取后面的字符串
name = name.substring(1);
}
// 如果前半部分和定义的包名相同
if (name.startsWith(packageName.replaceAll("\\.","/"))) {
int idx = name.lastIndexOf('/');
// 如果以"/"结尾 是一个包
if (idx != -1) {
// 获取包名 把"/"替换成"."
packageName = name.substring(0, idx)
.replace('/', '.');
}
// 如果可以迭代下去 并且是一个包
if ((idx != -1) || recursive) {
// 如果是一个.class文件 而且不是目录
if (name.endsWith(".class")
&& !entry.isDirectory()) {
// 去掉后面的".class" 获取真正的类名
String className = name.substring(
packageName.length() + 1, name
.length() - 6);
try {
// 添加到classes
Constant.METHOD_URL_SET.add(Class
.forName(packageName + '.'
+ className).getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
return;
} catch (IOException e) {
e.printStackTrace();
}
}
File dir = new File(url.getFile());
for (File file : dir.listFiles()) {
if(file.isDirectory()){
//递归读取包
doScanner(packageName+"."+file.getName());
}else{
String className =packageName +"." +file.getName().replace(".class", "");
Constant.METHOD_URL_SET.add(className);
}
}
}
}
1.2.5. redis缓存用法
- 缓存
wiselyKeyGenerator
具体用法,之前没了解到这个的具体用法,这次知道了
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import java.lang.reflect.Method;
/**
* @author liugh
* @since on 2018/5/11.
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/*定义缓存数据 key 生成策略的bean
包名+类名+方法名+所有参数
*/
@Bean("wiselyKeyGenerator")
public KeyGenerator wiselyKeyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName()+":");
sb.append(method.getName()+":");
for (Object obj : params) {
sb.append(obj.toString()+":");
}
return sb.deleteCharAt(sb.length()-1).toString();
}
};
}
/*要启用spring缓存支持,需创建一个 CacheManager的 bean,CacheManager 接口有很多实现,这里Redis 的集成,用 RedisCacheManager这个实现类
Redis 不是应用的共享内存,它只是一个内存服务器,就像 MySql 似的,
我们需要将应用连接到它并使用某种“语言”进行交互,因此我们还需要一个连接工厂以及一个 Spring 和 Redis 对话要用的 RedisTemplate,
这些都是 Redis 缓存所必需的配置,把它们都放在自定义的 CachingConfigurerSupport 中
*/
@Bean
public CacheManager cacheManager(
@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
// cacheManager.setDefaultExpiration(60);//设置缓存保留时间(seconds)
return cacheManager;
}
// @Bean springboot 2.0
// public CacheManager cacheManager(
// @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
// // 初始化缓存管理器,在这里我们可以缓存的整体过期时间什么的,我这里默认没有配置
// RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
// .RedisCacheManagerBuilder
// .fromConnectionFactory(jedisConnectionFactory);
// return builder.build();
// }
//1.项目启动时此方法先被注册成bean被spring管理
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
@Override
//redis方法级别的缓存,需要做缓存打开改注解即可
@Cacheable(value = "UserToRole",keyGenerator="wiselyKeyGenerator")
public List<Menu> selectByIds(List<Integer> permissionIds) {
EntityWrapper<Menu> ew = new EntityWrapper<>();
ew.in("menu_id", permissionIds);
return this.selectList(ew);
}