后台管理系统-知识梳理
后台管理系统知识梳理
此处只记录部分知识点 , 具体所有知识梳理内容均在源码以注解形式保存
项目介绍 : https://www.cnblogs.com/Leo-Heng/p/14989398.html
目录
- 目录结构
- 后端知识点
- cn / common / exception / GlobalExceptionHandler
- cn / common / perm / PermAop
- cn / components / DemoScheduling
- cn / components / TokenService
- cn / config / CorsConfig
- cn / controller / CommonController
- cn / controller / ScheduleController
- cn / utils / ReflectionUtils
- cn / utils / TreeUtils
- cn / utils / UploadService
目录结构
myproject
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── myproject
│ └── cn
│ ├── common
│ │ ├── domin //登录验证
│ │ │ └── LoginAdmin.java
│ │ ├── exception //异常处理
│ │ │ ├── ApiException.java
│ │ │ ├── FormValidException.java
│ │ │ └── GlobalExceptionHandler.java
│ │ ├── http //返回信息
│ │ │ ├── AxiosResult.java
│ │ │ └── AxiosStatus.java
│ │ ├── page //分页查询
│ │ │ └── PageBean.java
│ │ ├── perm //权限校验
│ │ │ ├── HasPerm.java
│ │ │ └── PermAop.java
│ │ ├── props //相关属性
│ │ │ └── UploadProperties.java
│ │ └── valid //分组校验
│ │ ├── anno
│ │ │ ├── InList.java
│ │ │ └── InListHandler.java
│ │ └── group
│ │ ├── AddGroup.java
│ │ ├── BtnGroup.java
│ │ ├── DirectoryGrop.java
│ │ ├── MenuGroup.java
│ │ └── UpdateGroup.java
│ ├── components //相关组件
│ │ ├── CacheService.java //redis缓存
│ │ ├── DemoScheduling.java //定时任务测试demo
│ │ ├── EmailService.java //email发送
│ │ └── TokenService.java //token生成解析
│ ├── config //相关配置
│ │ ├── CorsConfig.java //跨域请求配置
│ │ ├── MyWebConfig.java //登录拦截
│ │ └── SwaggerConfig.java //swagger2文档配置
│ ├── constants //常量
│ │ └── RedisKey.java //redis分组常量
│ ├── controller //相关业务
│ │ ├── AdminController.java
│ │ ├── base
│ │ │ └── BaseController.java
│ │ ├── BrandController.java
│ │ ├── CommonController.java
│ │ ├── MenuController.java
│ │ ├── RoleController.java
│ │ └── ScheduleController.java
│ ├── dto //返回前端的封装数据
│ │ ├── AdminDTO.java
│ │ ├── base
│ │ │ └── BaseDTO.java
│ │ ├── BrandDTO.java
│ │ ├── MenuDTO.java
│ │ ├── RoleDTO.java
│ │ └── ScheduleBeanDTO.java
│ ├── entity //实体类
│ │ ├── Admin.java
│ │ ├── AdminRole.java
│ │ ├── base
│ │ │ └── BaseEntity.java
│ │ ├── Brand.java
│ │ ├── Menu.java
│ │ ├── Role.java
│ │ ├── RoleMenu.java
│ │ └── ScheduleBean.java
│ ├── interceptor //登录拦截
│ │ └── LoginInterceptor.java
│ ├── mapper //mapper查询
│ │ ├── AdminMapper.java
│ │ ├── AdminRoleMapper.java
│ │ ├── base
│ │ │ └── MyMapper.java
│ │ ├── BrandMapper.java
│ │ ├── MenuMapper.java
│ │ ├── RoleMapper.java
│ │ ├── RoleMenuMapper.java
│ │ └── ScheduleMapper.java
│ ├── MyApplication.java
│ ├── query //查询封装
│ │ ├── AdminQuery.java
│ │ ├── base
│ │ │ └── BaseQuery.java
│ │ ├── BrandQuery.java
│ │ ├── MenuQuery.java
│ │ └── RoleQuery.java
│ ├── service //service层
│ │ ├── AdminService.java
│ │ ├── base
│ │ │ ├── BaseService.java //所有service接口的父类
│ │ │ └── impl
│ │ │ └── BaseServiceImpl.java //所有serviceImpl的父类
│ │ ├── BrandService.java
│ │ ├── impl
│ │ │ ├── AdminServiceImpl.java
│ │ │ ├── BrandServiceImpl.java
│ │ │ ├── MenuServiceImpl.java
│ │ │ ├── RoleServiceImpl.java
│ │ │ └── ScheduleServiceImpl.java
│ │ ├── MenuService.java
│ │ ├── RoleService.java
│ │ └── ScheduleService.java
│ ├── transfer //类型转换 entity转DTO
│ │ ├── AdminTransfer.java
│ │ ├── base
│ │ │ └── BaseTransfer.java
│ │ ├── BrandTransfer.java
│ │ ├── MenuTransfer.java
│ │ ├── RoleTransfer.java
│ │ └── ScheduleBeanTransfer.java
│ └── utils //工具类
│ ├── FormValidUtils.java //表单校验
│ ├── JsonUtils.java //json转换
│ ├── ReflectionUtils.java //反射工具
│ ├── TreeUtils.java //树状数据
│ └── UploadService.java //下载配置
└── resources //配置文件
├── application.yml
└── templates //freemark模板引擎默认加载文档
└── salary.ftl //工资邮件模板
后端知识点
直接对源码进行分析 , 一些没必要分析的类直接跳过
cn / common / exception / GlobalExceptionHandler
spring异常处理
/**
* @className: GlobalExceptionHandler
* @description: spring异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
//操作失败异常的异常处理器
@ExceptionHandler(ApiException.class)
public AxiosResult<Void> handlerFormValidException(ApiException e) {
return AxiosResult.error(e.getAxiosStatus());
}
//全局异常的异常处理器
@ExceptionHandler(Throwable.class)
public AxiosResult<Void> handlerThrowable(Throwable e) {
System.out.println(e);
AxiosStatus uploadError = AxiosStatus.UNKNOWN_ERROR;
uploadError.setMessage(e.getMessage());
return AxiosResult.error(uploadError);
}
@ExceptionHandler(FormValidException.class)
public AxiosResult<Map<String, String>> handlerFormValidException(FormValidException e) {
AxiosStatus axiosStatus = e.getAxiosStatus();
return AxiosResult.error(axiosStatus, e.getMap());
}
//表单校验异常的异常处理器 ,
// 当表单校验失败时会抛出一个携带错误信息的MethodArgumentNotValidException异常
// 此时使用异常处理器处理此异常向前端返回携带的错误信息
@ExceptionHandler(MethodArgumentNotValidException.class)
public AxiosResult<String> handlerFormValidException(MethodArgumentNotValidException e) {
//获取抛出要异常中存储的异常信息
BindingResult bindingResult = e.getBindingResult();
AxiosStatus formValidError = AxiosStatus.FORM_VALID_ERROR;
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
//当携带的内容不为空时 , 返回携带的错误
if (!CollectionUtils.isEmpty(fieldErrors)) {
FieldError fieldError = fieldErrors.get(0);
String defaultMessage = fieldError.getDefaultMessage();
formValidError.setMessage(defaultMessage);
return AxiosResult.error(formValidError);
}
formValidError.setMessage("表单校验错误");
return AxiosResult.error(formValidError);
}
}
cn / common / perm / PermAop
/**
* @className: PermAop
* @description: Aop切面 , 用于权限校验 , 如果登录角色没有对应权限则报错
*/
@Aspect //切面声明,标注在类、接口(包括注解类型)或枚举上
@Component
public class PermAop {
@Autowired
private TokenService tokenService;
//@Before 前置通知 , 先于pointcut设置的切入点执行
@Before("pointCut()")
public void checkBtnPerm(JoinPoint joinPoint) {
//获取当前执行的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//获取当前方法是否含有HasPerm注解
HasPerm annotation = method.getAnnotation(HasPerm.class);
//不为null说明含有注解
if (annotation != null) {
//获取当前注解设置的权限标识
String s = annotation.perSign();
System.out.println(s);
//获取当前登录用户的所有权限
List<Menu> menus = tokenService.getLoginAdminCache().getMenus();
//使用anyMatch判断是否含有对应权限 , 如果含有则放行 , 否则抛出异常
boolean b = menus.stream().anyMatch(menu -> s.equalsIgnoreCase(menu.getPermSign()));
System.out.println(b);
if (!b) {
throw new ApiException(AxiosStatus.NO_PERM);
}
}
}
//@Pointcut 设置切入点表达式 , 选择controller包下的所有类的所有方法 方法的参数可以是任何东西 , 都需要经过切面
@Pointcut("execution(* com.myproject.cn.controller.*.*(..))")
public void pointCut() {
}
}
cn / components / DemoScheduling
package com.myproject.cn.components;
import org.springframework.scheduling.annotation.Scheduled;
/**
* @className: DemoScheduling
* @description: StringTask定时任务相关知识点笔记 , 邮件发送相关笔记 ,
*/
//@Component
public class DemoScheduling {
/**
* stringTask的使用方式 :
* 1. 手写
* Crontrigger trigger = new CronTrigger(* * * * * *);
* ThreadPoolTaskScheduler
* 好处 : 手动指定线程池大小 , 手动初始化
*
* 2. 使用Spring封装的注解
* 1. 开启任务调度 @EnableScheduling
* 2. 在容器中的某个类 , 定义一个方法 , 在方法中做任务调度做的事情
* 这个事情什么时候做 , 需要在这个方法上添加一个注解@Scheduled ,
* 通过这个注解中的配置cron表达式等 , 确定这个方法什么时候执行
* 好处 : 使用起来简单 , 只需要2个注解
* 缺点 : 默认的线程池只有一个
*
* 解决方案 :
* 解决方案一 : 调度就使用一个线程 , 但是这个线程所做的事情放到子线程中去做 , 此时这个线程不会阻塞
* 解决方案二 : 修改调度线程次大小
*/
/**
* 发送邮件 :
* 导包 : spring-context-support-javaxmail
* 相关配置 :
* javaMailSender
* from
* 邮箱授权码
* smtp服务器
*
* freemarker模板引擎使用方法 :
* 1. 导包freemarker
* 2. 配置freemarkerConfiger
* 配置模板地址
* 配置字符编码集
* 3. 在配置的默认地址文件夹(templates)中 , 定义xxx.ftl格式文件
* 4. 在这个格式文件中 , 可变的值使用${key}定义
* 5. 编码读取模板 , 获取字符串 , 发送邮件
*/
// @Autowired
// private AdminMapper adminMapper;
// @Autowired
// private EmailService emailService;
/**
* 定时任务 :
* 定时任务的功能如果是一个耗时操作 , 应该放到线程池中
* 使用@Scheduled注解创建定时任务 , 会在系统启动后自动执行
* @Scheduled 线程池默认只有一个线程 ,
* corn表示执行频率或者执行时机
* *从前到后依次表示 秒 分 时 日 月 年
* 0 30 16 15 * * 表示每个月15日 下午4时 30分执行任务
* 可以通过扩大线程池容量 或 开辟新线程来实现操作
* Scheduled注解特点 : 自动触发 , 表达式无法修改
* 可以动态的从数据库中获取表达式后 , 手动触发 , 如果想做到动态触发需要使用@EnableScheduling
*
*/
// @Scheduled(cron = "0 30 16 15 * *")
// public void doSomeThing() throws Exception {
// System.out.println(Thread.currentThread().getName());
// List<KafkaProperties.Admin> admins = adminMapper.selectList(null);
// admins.forEach(admin -> {
// //使用异步线程池来做的
// emailService.sendMail(admin);
// });
// //要给这里面的员工发邮件
//
// }
//@Scheduled的特点 : 自动触发执行 , 表达式无法修改
//解决方案 : 动态从数据库中获取数据后 , 手动触发
//如果想要做到动态需要使用@EnableScheduling + @Scheduled(cron=)
/*
@Scheduled(cron = "0/5 * * * * *")
public void doSomeThing(){
System.out.println("xxxxx");
}
*/
}
cn / components / TokenService
Token的用途 :
- 前端登录
- 获取前端用户信息
前端登录操作流程 :
- 用户登录
- 传入后台用户信息的map集合
- 用户信息输入正确 , 生成uuid , loginAdmin对象 , 并将uuid作为key , LoginAdmin转为json字符串作为value存入redis
- 将uuid封装为token返回前端
**前端获取登录用户信息流程 : **
- 前端发送请求获取用户信息
- 将token封装入请求头
- 从请求中获取携带的token
- 解析token携带的uuid
- 根据uuid从redis中找出LoginAdmin对应的json字符串并转为对象 , 将对象返回前端
问 : 为什么不把uuid直接返回前端 , 而是将uuid封装入token , 将token返回前端
答 : 使用token是规范 并且token有检测机制 比如过期等 如果直接返回uuid功能可以实现 但是没有过期机制 并且不符合规范
/**
* 由于前端设置了每次请求都会携带token , 所以可以从请求中头中获取token ,
* 根据token可以获取loginAdmin对象在redis中对应的uuid
* loginAdmin对象中页存储了这个个uuid
*
* @className: TokenService
* @description: 将对象转为token
*/
@Component
public class TokenService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
//token加密
private static final String secret = "ljkaidop";
//将admin封装为一个token
public String createTokenAndCacheLoginAdmin(Admin admin) {
//生成随机uuid
String s = UUID.randomUUID().toString();
//创建一个loginAdmin对象封装登录信息
LoginAdmin loginAdmin = new LoginAdmin();
loginAdmin.setUuid(s);
loginAdmin.setAdmin(admin);
//uuid作为key , loginAdmin对象转为json放入redis
setLoginAdminCache(loginAdmin);
//将uuid封装入token传回前端
return createToken(s);
}
/**
* 使用redis缓存登录用户
*
* @param loginAdmin
*/
public void setLoginAdminCache(LoginAdmin loginAdmin) {
//将loginAdmin的uuid作为key , loginAdmin对象转为json作为value存入redis
stringRedisTemplate.opsForValue().set(loginAdmin.getUuid(), JsonUtils.obj2Str(loginAdmin));
}
/**
* 生成token
*/
public String createToken(String uuid) {
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withIssuer("Leo")//设置签发者
.withSubject("登录token")//设置主题
.withClaim("uuid", uuid)
.sign(algorithm);
return token;
}
/**
* 解析request内的token
* 解析失败时会抛出异常
*
* @param request
* @return
*/
public DecodedJWT verifyToken(HttpServletRequest request) {
String authentication = request.getHeader("Authentication"); //从request中获取token
String token = authentication.split(" ")[1]; // 去除token的前缀 获取token内容
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("Leo")
.withSubject("登录token")
.build();
DecodedJWT jwt = verifier.verify(token);
return jwt;
}
/**
* 解析token中的uuid
*
* @return
*/
public String getUUID() {
DecodedJWT jwt = verifyToken(getRequest());
String uuid = jwt.getClaim("uuid").asString();
return uuid;
}
/**
* 从redis缓存中获取loginAdmin的json格式并转为java对象
*/
public LoginAdmin getLoginAdminCache() {
String uuId = getUUID();
String s = stringRedisTemplate.opsForValue().get(uuId);
LoginAdmin loginAdmin = JsonUtils.str2Obj(s, LoginAdmin.class);
return loginAdmin;
}
public Long adminI() {
Long id = getLoginAdminCache().getAdmin().getId();
return id;
}
public boolean isAdmin() {
Boolean isAdmin = getLoginAdminCache().getAdmin().getIsAdmin();
return isAdmin;
}
/**
* 判断token是否正确 , 用于登录校验
* @param httpServletRequest
* @return
*/
public boolean checkTokenIsSure(HttpServletRequest httpServletRequest) {
//从请求头中获取token
String authentication = httpServletRequest.getHeader("Authentication");
//没有传递token
if (StringUtils.isEmpty(authentication))
throw new ApiException(AxiosStatus.NO_LOGIN);
//token没有携带前缀 , token格式错误
if (!authentication.startsWith("Bearer "))
throw new ApiException(AxiosStatus.NO_LOGIN);
//token式错误
String[] s = authentication.split(" ");
if (s.length != 2)
throw new ApiException(AxiosStatus.NO_LOGIN);
//如果没有解析出异常 , 则token正确
try {
verifyToken(httpServletRequest);
} catch (Exception e) {
throw new ApiException(AxiosStatus.NO_LOGIN);
}
return true;
}
/**
* 获取全局request
*
* @return
*/
public HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
System.out.println("request = " + request);
return request;
}
/**
* 获取全局response
*
* @return
*/
public HttpServletResponse getResponse() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return requestAttributes.getResponse();
}
}
cn / config / CorsConfig
/**
* @className: CorsConfig
* @description: 跨域过滤器
*/
@Configuration
public class CorsConfig {
/**
* 设置跨域请求相关配置
* @return
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许所有访问源地址
corsConfiguration.addAllowedOrigin("*");
// 允许所有请求方法
corsConfiguration.addAllowedMethod("*");
// 允许所有请求头
corsConfiguration.addAllowedHeader("*");
// spring boot2.4版本后configuration.setAllowCredentials(true)的时候对addAllowedOrigin设置为"*",
// 而要使用 allowedOriginPatterns这个字段来设置origin。(本项目使用2.3.3)
// corsConfiguration用于是否发送cookie信息
corsConfiguration.setAllowCredentials(false);
//设置跨域过滤器的过滤内容
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
CorsFilter corsFilter = new CorsFilter(urlBasedCorsConfigurationSource);
return corsFilter;
}
}
cn / controller / CommonController
/**
* @className: CommonController
* @description:
*/
@RestController
@RequestMapping("common")
public class CommonController {
/**
* 文件上传 :
* 以前 : ssm整合 , 文件上传需要开启multipart , 如果不限制文件上传大小则可随意上传
* 现在 : springBoot , 文件上传默认开启multipart , 默认的tomcat上传的文件大小为1M
* 设置文件上传的限制:
* 1. 格式限制 jpg,png
* 2. 最大不超过200kb
* 3. 宽高为100*100
* 4. 上传的文件必须是图片
* <p>
* ssm中redis如何使用
* redis
* 需要配置jedis或者lettuce
* slf4j
* 配置RedisTemplate
* StringRedisTemplate
* 配置工程 , 数据库连接池
* <p>
* 登录拦截 :
* 登录时查询数据库如果用户存在 , 密码匹配成功则登录
* 给前端返回一个token , token携带了缓存redis中的用户key
*/
@Autowired
private UploadService uploadService;
@Autowired
private AdminService adminService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
@Autowired
private MenuTransfer menuTransfer;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private TokenService tokenService;
@PostMapping("upload")
@ApiOperation(value = "文件上传")
public AxiosResult<String> upload(@RequestPart Part part) throws IOException {
return AxiosResult.success(uploadService.upload(part));
}
@GetMapping("getCode/{uuid}")
@ApiOperation(value = "获取验证码")
public AxiosResult<String> getCode(@PathVariable String uuid) {
//使用gif验证码 , 设置宽高和字符数
GifCaptcha gifCaptcha = new GifCaptcha(130, 48, 4);
//获取验证码的对应文字
String text = gifCaptcha.text();
//将前端传回的uuid作为key , 验证码的值作为value存入redis
stringRedisTemplate.opsForValue().set(RedisKey.CODE_KEY + uuid, text);
//将验证码图片转为base64
String s = gifCaptcha.toBase64();
return AxiosResult.success(s);
}
@PostMapping("doLogin")
@ApiOperation(value = "登录")
public AxiosResult<String> deLogin(@RequestBody Map<String, String> map) {
String code = map.get("code");
String uuid = map.get("uuid");
String username = map.get("username");
String password = map.get("password");
String value = stringRedisTemplate.opsForValue().get(RedisKey.CODE_KEY + uuid);
//判断验证码是否正确
if (StringUtils.isEmpty(value) || !value.equalsIgnoreCase(code))
throw new ApiException(AxiosStatus.ERROR);
Admin admin = adminService.getAdminByAccount(username);
//判断用户名是否正确
if (admin == null)
throw new ApiException(AxiosStatus.ERROR);
//判断验证码是否正确
if (!bCryptPasswordEncoder.matches(password, admin.getAdminPassword()))
throw new ApiException(AxiosStatus.ERROR);
//校验成功
//清除redis中的验证码数据
Boolean delete = stringRedisTemplate.delete(RedisKey.CODE_KEY + uuid);
String token = tokenService.createTokenAndCacheLoginAdmin(admin);
return AxiosResult.success(token);
}
@GetMapping("getUserInfo")
@ApiOperation(value = "获取用户信息")
public AxiosResult<Map<String, Object>> getUserInfo(HttpServletRequest httpServletRequest) {
//从redis中获取存储的登录用户
LoginAdmin loginAdmin = tokenService.getLoginAdminCache();
System.out.println("登录用户 : " + loginAdmin);
Admin admin = loginAdmin.getAdmin();
System.out.println("admin : " + admin);
Map<String, Object> map = new HashMap<>();
map.put("admin", admin);
//如果登录用户是超级管理员则赋予所有权限
if (admin.getIsActive()) {
//查询所有权限
List<Menu> list = menuService.list();
//将用户所有权限放入loginAdmin中
loginAdmin.setMenus(list);
List<MenuDTO> menuDTOS = menuTransfer.toDTO(list);
//获取除按钮权限外的其他权限
List<MenuDTO> collect = menuDTOS.stream().filter(menuDTO -> !menuDTO.getMenuType().equals(3)).collect(Collectors.toList());
//获取所有按钮权限
List<MenuDTO> btnMenus = menuDTOS.stream().filter(menuDTO -> menuDTO.getMenuType().equals(3)).collect(Collectors.toList());
//获取所有根级权限
List<MenuDTO> root = collect.stream().filter(menuDTO -> menuDTO.getParentId().equals(0L)).collect(Collectors.toList());
//将列表排序并转为树形结构
TreeUtils.buildTreeData(root, collect);
map.put("menuList", root);
map.put("btnList", btnMenus);
//将loginAdmin存入存入redis
tokenService.setLoginAdminCache(loginAdmin);
return AxiosResult.success(map);
} else {
//如果登录用户不是超级管理员返回admin的用户权限
//获取admin对应角色
List<Long> roleIds = adminService.getRolesByAdminId(admin.getId());
//获取角色对应权限
List<MenuDTO> menuDTOs = roleService.getMenusByRoleIds(roleIds);
List<Menu> menus = menuTransfer.toEntity(menuDTOs);
//设置用户对应权限
loginAdmin.setMenus(menus);
System.out.println("所有权限为 : " + menuDTOs);
//获取除按钮权限外的其他权限
List<MenuDTO> collect = menuDTOs.stream().filter(menuDTO -> !menuDTO.getMenuType().equals(3)).collect(Collectors.toList());
//获取所有按钮权限
List<MenuDTO> btnMenus = menuDTOs.stream().filter(menuDTO -> menuDTO.getMenuType().equals(3)).collect(Collectors.toList());
System.out.println("菜单 : " + collect);
//获取所有根级权限
List<MenuDTO> root = collect.stream().filter(menuDTO -> menuDTO.getParentId().equals(0L)).collect(Collectors.toList());
//将列表排序并转为树形结构
TreeUtils.buildTreeData(root, collect);
map.put("menuList", root);
map.put("btnList", btnMenus);
//将loginAdmin存入存入redis
tokenService.setLoginAdminCache(loginAdmin);
return AxiosResult.success(map);
}
}
/*
以前登录拦截的实现方式 :
1. 登录 , 登录成功后把用户信息存入session
2. 定义拦截器 , 从session总获取登录信息
如果能获取说明登录了
获取不到说明未登录
产生的问题 :
1. 前端端分离时 , 使用axios请求时 , 默认不携带cookie ,
所以每次访问的session都不同 , 登录拦截失效
2. 解决方案 : 携带cookie
3. 实际开发中项目为了做到分流的效果 , 需要部署集群
1个tomcat的并发量默认值为200 , 可以调整 ,
最多调整到500左右 , 性能还行 ,如果调整过高 , 性能就会变差
因此在实际生产中 : 要把项目部署多次 , 启动多个tomcat , 占用不同的端口 , 使用nginx做负载均衡
此时想要使用session做登录状态 , 就需要共享session
但是session是有状态的
因此产生的思想 :
1. 在登录成功时 , 给前端返回一个字符串 , 前端接收到字符串保存起来
2. 前端每次请求都需要在请求头中添加这个字符串 , 后台拦截器获取这个字符串
如果得到字符串说明登录 , 没得到说明没登录
这个字符串不能是明文 , 不能轻易让人识别出来 因此需要使用token , jwt
token是一个字符串 , 这个字符串中间有2个"." , 将字符串分为3部分
*/
// @GetMapping("loginStatus")
// @ApiOperation(value = "获取登录状态")
// public String loginStatus(HttpSession httpSession) {
// RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// System.out.println(requestAttributes.getClass().getName());
// return "success";
// }
}
cn / controller / ScheduleController
手动设置定时任务的暂停 重启 删除 添加
/**
* @className: ScheduleController
* @description:
*/
@RestController
@RequestMapping("schedule")
@Api(tags = "定时管理", description = "定时管理")
public class ScheduleController extends BaseController {
@Autowired
private ScheduleService scheduleService;
@Autowired
private ScheduleBeanTransfer scheduleBeanTransfer;
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
@Autowired
private AdminMapper adminMapper;
@Autowired
private EmailService emailService;
private Map<Long, ScheduledFuture> map = new HashMap<>();
//手动执行定时任务
@PostConstruct//java自带注解 , 被修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次
public void initSchedule() {
System.out.println("PostConstruct方法执行");
List<ScheduleBean> list = scheduleService.list();
list.forEach(scheduleBean -> {
//获取cron表达式
String cornExpress = scheduleBean.getCronExpress();
//创建CronTrigger
CronTrigger cronTrigger = new CronTrigger(cornExpress);
//设置定时任务 , 将任务放到子线程执行
ScheduledFuture<?> scheduledFuture = threadPoolTaskScheduler.schedule(new Runnable() {
@Override
public void run() {
//发送邮件
List<Admin> admins = adminMapper.selectList(null);
admins.forEach(admin -> emailService.sendMail(admin));
}
}, cronTrigger);
//存入map , 方便切换
map.put(scheduleBean.getCronId(), scheduledFuture);
});
}
@ApiOperation(value = "查询所有cron表达式")
@GetMapping
public AxiosResult<List<ScheduleBeanDTO>> list() {
return AxiosResult.success(scheduleBeanTransfer.toDTO(scheduleService.list()));
}
@ApiOperation(value = "根据id查询cron表达式")
@GetMapping("{id}")
public AxiosResult<ScheduleBeanDTO> findById(@PathVariable Long id) {
return AxiosResult.success(scheduleBeanTransfer.toDTO(scheduleService.findById(id)));
}
@ApiOperation(value = "添加cron表达式")
@PostMapping
public AxiosResult<Void> add(@RequestBody ScheduleBean scheduleBean) {
return toAxios(scheduleService.addSchedule(scheduleBean));
}
@PutMapping
@ApiOperation(value = "修改定时任务")
@HasPerm(perSign = "task:time:edit")
public AxiosResult<Void> update(@RequestBody ScheduleBean scheduleBean) {
//表单校验
long cronId = scheduleBean.getCronId();
ScheduledFuture scheduledFuture = map.get(cronId);
//判断定时任务存在并且没有被取消
if (scheduledFuture != null && !scheduledFuture.isCancelled())
scheduledFuture.cancel(false);//先将定时任务取消
map.remove(cronId);//将scheduledFuture从map中移除
int i = scheduleService.updateSchedule(scheduleBean);
//重新设置一个新的定时任务
ScheduledFuture<?> schedule = threadPoolTaskScheduler.schedule(new Runnable() {
@Override
public void run() {
List<Admin> admins = adminMapper.selectList(null);
admins.forEach(admin -> emailService.sendMail(admin));
}
}, new CronTrigger(scheduleBean.getCronExpress()));
//将定时任务存入map
map.put(cronId, schedule);
return toAxios(i);
}
@DeleteMapping("{id}")
@ApiOperation(value = "通过id删除")
@HasPerm(perSign = "task:time:delete")
public AxiosResult<Void> deleteById(@PathVariable Long id) {
int i = scheduleService.deleteById(id);
ScheduledFuture scheduledFuture = map.get(i);
if (scheduledFuture != null && !scheduledFuture.isCancelled())
scheduledFuture.cancel(true);//取消定时任务
map.remove(i);
return toAxios(i);
}
@PutMapping("{id}")
@ApiOperation(value = "暂停定时任务")
@HasPerm(perSign = "task:time:pause")
public AxiosResult<Void> pause(@PathVariable Long id) {
ScheduledFuture scheduledFuture = map.get(id);
if (scheduledFuture != null && !scheduledFuture.isCancelled())
scheduledFuture.cancel(false);//停止定时任务
map.remove(id);
return AxiosResult.success();
}
@PostMapping("{id}")
@ApiOperation(value = "重启")
@HasPerm(perSign = "task:time:resume")
public AxiosResult<Void> resume(@PathVariable Long id) {
//从数据库中获取schedule对象
ScheduleBean byId = scheduleService.findById(id);
//根据cron表达式创建cronTrigger对象 , 并开启定时任务
ScheduledFuture<?> schedule = threadPoolTaskScheduler.schedule(new Runnable() {
@Override
public void run() {
List<Admin> admins = adminMapper.selectList(null);
admins.forEach(admin -> emailService.sendMail(admin));
}
}, new CronTrigger(byId.getCronExpress()));
map.put(byId.getCronId(), schedule);
return AxiosResult.success();
}
}
cn / utils / ReflectionUtils
/**
* @className: ReflectionUtils
* @description:
*/
@Slf4j //lombok提供 , 可以使用log来记录日志
public class ReflectionUtils {
/**
* 直接读取对象的属性值, 忽略 private/protected 修饰符, 也不经过 getter
*/
public static Object getFieldValue(Object object, String fieldName) {
Field field = getDeclaredField(object, fieldName);
if (field == null) {
throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
}
makeAccessible(field);
Object result = null;
try {
result = field.get(object);
} catch (IllegalAccessException e) {
log.error("ReflectionUtils类中 获取属性值错误");
}
return result;
}
/**
* 直接设置对象属性值, 忽略 private/protected 修饰符, 也不经过 setter
*/
public static void setFieldValue(Object object, String fieldName, Object value) {
Field field = getDeclaredField(object, fieldName);
if (field == null) {
throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
}
makeAccessible(field);
try {
field.set(object, value);
} catch (IllegalAccessException e) {
log.error("ReflectionUtils类中 设置属性值错误");
}
}
/**
* 通过反射, 获得定义 Class 时声明的父类的泛型参数的类型
* 如: public EmployeeDao extends BaseDao<Employee, String>
*/
@SuppressWarnings("unchecked")
public static Class getSuperClassGenricType(Class clazz, int index) {
Type genType = clazz.getGenericSuperclass();
if (!(genType instanceof ParameterizedType)) {
return Object.class;
}
Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
if (index >= params.length || index < 0) {
return Object.class;
}
if (!(params[index] instanceof Class)) {
return Object.class;
}
return (Class) params[index];
}
/**
* 通过反射, 获得 Class 定义中声明的父类的泛型参数类型
* 如: public EmployeeDao extends BaseDao<Employee, String>
*/
@SuppressWarnings("unchecked")
public static <T> Class<T> getSuperGenericType(Class clazz) {
return getSuperClassGenricType(clazz, 0);
}
/**
* 循环向上转型, 获取对象的 DeclaredMethod
*/
public static Method getDeclaredMethod(Object object, String methodName, Class<?>[] parameterTypes) {
for (Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
try {
return superClass.getDeclaredMethod(methodName, parameterTypes);
} catch (NoSuchMethodException e) {
//Method 不在当前类定义, 继续向上转型
log.error("ReflectionUtils类中 目标对象中没有该方法");
}
}
return null;
}
/**
* 使 filed 变为可访问
*/
public static void makeAccessible(Field field) {
if (!Modifier.isPublic(field.getModifiers())) {
field.setAccessible(true);
}
}
/**
* 循环向上转型, 获取对象的 DeclaredField
*/
public static Field getDeclaredField(Object object, String filedName) {
for (Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
try {
return superClass.getDeclaredField(filedName);
} catch (NoSuchFieldException e) {
//Field 不在当前类定义, 继续向上转型
log.error("目标对象中没有这个属性");
}
}
return null;
}
/**
* 直接调用对象方法, 而忽略修饰符(private, protected)
*/
public static Object invokeMethod(Object object, String methodName, Class<?>[] parameterTypes,
Object[] parameters) {
Method method = getDeclaredMethod(object, methodName, parameterTypes);
if (method == null) {
throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + object + "]");
}
method.setAccessible(true);
try {
return method.invoke(object, parameters);
} catch (Exception e) {
log.error("方法执行失败");
}
return null;
}
}
cn / utils / TreeUtils
菜单工具类
/**
* @className: TreeUtils
* @description: 动态生成菜单
*/
public class TreeUtils {
/*
参数一 : 最顶级节点的集合
参数二 : 所有节点的集合
目的 : 从所有数据中查询出顶级节点的子节点 , 递归查询 , 并按顺序输出
流程 :
1. 先对由由根节点组成的list进行排序
2. 将排序后的list覆盖到排序前的
3. 遍历根节点的list , 将子节点封装入根节点
*/
public static <T> void buildTreeData(List<T> root, List<T> allList) {
//将根节点进行排序
List<T> sortToot = root.stream().sorted((t1, t2) -> {
Integer sort1 = (Integer) ReflectionUtils.getFieldValue(t1, "sort");
Integer sort2 = (Integer) ReflectionUtils.getFieldValue(t2, "sort");
return sort1 - sort2;
}).collect(Collectors.toList());
//将排序后的list覆盖掉原list
root.clear();
root.addAll(sortToot);
//从allList中找出跟节点的所有子节点 , 并封装入根节点
root.forEach(t -> getChildren(t, allList));
}
/*
方法作用 :
将传入参数对象的 所有子级节点 和 子级节点的子级节点 以及更下级的节点 找出并封装入参数对象并排序
流程 :
1. 使用反射获取传入对象的id和list内所有对象的parentId (ReflectionUtils为反射工具类)
2. 当list内对象的parentId与传入对象的id相等时 , 说明这个list内的对象是传入对象的下级对象
使用list.stream().filter()将list内所有t的子级对象抽出单独生成一个list并返回
3. 如果子级节点的list不为空 , 则根据子级节点的sort属性值对子级节点进行排序 , 使用递归寻找子级节点的子级节点
*/
private static <T> void getChildren(T t, List<T> allList) {
//找出所有的子级节点
List<T> children = allList.stream().filter((t1) -> {
Long id = (Long) ReflectionUtils.getFieldValue(t, "id");
Long parentId = (Long) ReflectionUtils.getFieldValue(t1, "parentId");
return id.equals(parentId);
}).collect(Collectors.toList());
//对子级节点排序 , 并使用递归找出更下级节点
if (!CollectionUtils.isEmpty(children)) {
//子节点排序
List<T> sortedChildren = children.stream().sorted((t1, t2) -> {
Integer sort1 = (Integer) ReflectionUtils.getFieldValue(t1, "sort");
Integer sort2 = (Integer) ReflectionUtils.getFieldValue(t2, "sort");
return sort1 - sort2;
}).collect(Collectors.toList());
//将子节点list封装入上级节点
ReflectionUtils.setFieldValue(t, "children", sortedChildren);
//递归寻找更下级节点
sortedChildren.forEach(t1 -> getChildren(t1, allList));
}
}
}
cn / utils / UploadService
/**
* @className: UploadService
* @description: 头像上传
*/
@Component
public class UploadService {
@Autowired
private UploadProperties uploadProperties;
public String upload(Part part) throws IOException {
BufferedImage read = ImageIO.read(part.getInputStream());
//判断上传的文件是不是图片
if (read == null)
throw new ApiException(AxiosStatus.NOT_IMG);
//判断上传文件是否以jpg或png结尾
if (uploadProperties.getUploadExt().contains(StringUtils.getFilenameExtension(part.getSubmittedFileName())))
throw new ApiException(AxiosStatus.IMG_TYPE_ERROR);
//获取文件大小 , 单位KB
long size = part.getSize() / 1024;
//判断文件上传大小是否合规
if (size > uploadProperties.getUploadSize())
throw new ApiException(AxiosStatus.IMG_TOO_LARGE);
int width = read.getWidth();
int height = read.getHeight();
//判断图片长宽是否合规
if (width != uploadProperties.getImgWidth() || height != uploadProperties.getImgHeight())
throw new ApiException(AxiosStatus.IMG_SIZE_ERROR);
//设置文件名
String fileName = UUID.randomUUID().toString() + "." + StringUtils.getFilenameExtension(part.getSubmittedFileName());
//获取服务器地址
String endPoint = uploadProperties.getEndPoint();
//获取登录id
String accessKeyId = uploadProperties.getAccessKeyId();
//获取登录密码
String accessKeySecret = uploadProperties.getAccessKeySecret();
//设置oss
OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
ossClient.putObject(uploadProperties.getBucket(), fileName, part.getInputStream());
//关闭oss
ossClient.shutdown();
String url = uploadProperties.getBaseUrl() + fileName;
return url;
}
}