目录
1 项目信息
1.1 项目模板地址
2.特殊功能与实
2.1 日志记录功能
看过网上很多的SpringBoot项目记录日志的功能,使用SysLog注解,同时将日志写入数据库,写入数据库字段名如下:
即是记录什么用户,什么时间,什么ip,操作了什么接口,耗时多长,请求的什么参数,其实我也是这样记录的,但是这里存在一个问题,请求的参数多数情况下是一个 主键id,如删除用户接口是一个userId
@ApiOperation("删除用户")
@DeleteMapping(value = "/delete")
@SysLog(msg = "删除用户")
public ResponseEntity<ResponseMessage> deleteUser(Integer userId) {
ResponseMessage response = sysUserService.deleteUser(userId);
return ResponseEntity.status(response.getStatus()).body(response);
}
最后记录参数时记录的是userId,这样导致前端使用时不知道对应的用户名是谁,不方便展示:
在将改进前,我们先回顾一下原始日志代码实现流程
步骤一:新建SysLog注解类,用于Controller的接口上
package com.swt.agy.annotation;
import com.swt.agy.aspect.ServiceEnum;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 系统日志注解,从方法上中提取参数,用于查、增、改操作
*
* @author Bleeth
* @version 1.0
* @date 2020-01-08 16:12
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
String msg() default "";
}
步骤二:Controller接口上添加SysLog注解
@ApiOperation("用户登陆")
@PostMapping(value = "/login")
@SysLog(msg = "用户登陆")
public ResponseEntity<ResponseMessage> login(@RequestBody SysLoginForm form) {
ResponseMessage response = sysLoginService.login(form);
return ResponseEntity.status(response.getStatus()).body(response);
}
步骤三:AOP进行处理SysLog的注解(此代码仅供参考,有修改)
package com.swt.agy.aspect;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import com.swt.agy.annotation.SysLog;
import com.swt.agy.config.ResponseBean;
import com.swt.agy.config.ResponseMessage;
import com.swt.agy.module.sys.entity.SysLogEntity;
import com.swt.agy.module.sys.service.SysLogService;
import com.swt.agy.module.sys.service.SysMsgService;
import com.swt.agy.module.sys.service.SysUserService;
import com.swt.agy.shiro.ShiroUtils;
import com.swt.agy.util.IpUtil;
import com.swt.agy.util.RedisUtil;
import com.swt.agy.util.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
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.util.HashMap;
import java.util.Map;
/**
* 系统日志,切面处理类
*
* @author Bleeth
* @version 1.0
* @date 2020-01-08 16:12
*/
@Slf4j
@Aspect
@Component
public class SysLogAspect {
@Autowired
private SysLogService sysLogService;
@Autowired
private SysMsgService sysMsgService;
@Autowired
private SysUserService sysUserService;
@Pointcut("@annotation(com.swt.agy.annotation.SysLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
String methodParam = null;
String methodMsg = null;
DateTime current = DateUtil.date();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Method method = getMethod(point);
String methodName = method.getName();
//想办法获取参数
SysLog sysLog = method.getAnnotation(SysLog.class);
methodParam = parseKey(paramField, method, point.getArgs());
methodMsg = sysLog.msg();
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
Integer userId = ShiroUtils.getUserId();
if (userId == null) {
return result;
}
SysLogEntity entity = new SysLogEntity();
entity.setUserId(userId);
entity.setParams(methodParam);
entity.setCreatedTime(current);
entity.setIp(IpUtil.getIpAddr(request));
entity.setMethod(methodName);
entity.setTime(time);
entity.setOperation(methodMsg);
entity.setRemark("");
if (result instanceof ResponseEntity) {
ResponseEntity responseEntity = (ResponseEntity) result;
Object body = responseEntity.getBody();
if (body instanceof ResponseMessage) {
ResponseMessage responseMessage = (ResponseMessage) body;
entity.setRemark(responseMessage.getMessage());
}
if (body instanceof ResponseBean) {
ResponseBean responseBean = (ResponseBean) body;
entity.setRemark(responseBean.getMessage());
}
}
sysLogService.save(entity);
return result;
}
/**
* 获取被拦截方法对象
* <p>
* MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象
* 而缓存的注解在实现类的方法上
* 所以应该使用反射获取当前对象的方法对象
*/
public Method getMethod(ProceedingJoinPoint point) {
//获取参数的类型
Object[] args = point.getArgs();
Class[] argTypes = new Class[point.getArgs().length];
for (int i = 0; i < args.length; i++) {
argTypes[i] = args[i].getClass();
}
Method method = null;
try {
method = point.getTarget().getClass().getMethod(point.getSignature().getName(), argTypes);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
return method;
}
private String parseKey(String key, Method method, Object[] args) {
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
//把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
String value = parser.parseExpression(key).getValue(context, String.class);
return value;
}
}
改进思路:针对Controller中接口的参数形式,我们得知大概几种情况
1.增,改接口一般都会对应表中实体的名称,此时可以从参数列表中获取我们需要的参数,如创建用户,我们关注的是创建的用户名是什么,此时可以从form表单中获取得到,同类,修改用户接口可从修改表单中获取
@ApiOperation("创建用户")
@PostMapping(value = "/create")
@SysLog(msg = "创建用户", field = "#form.username")
public ResponseEntity<ResponseBean> createUser(AddUserForm form) {
ResponseBean response = sysUserService.createUser(form);
return ResponseEntity.status(response.getStatus()).body(response);
}
2.删除接口都是提供对应实体的id,此时需要从数据库中查询记录实体名是什么,如删除用户,我们需要用SysUserService查询userId得到SysUserEntity,后获取username,如下
@ApiOperation("删除用户")
@DeleteMapping(value = "/delete")
@SysLog(msg = "删除用户", field = "#userId",serviceEnum = ServiceEnum.SYS_USER,queryKey = "username")
public ResponseEntity<ResponseMessage> deleteUser(Integer userId) {
ResponseMessage response = sysUserService.deleteUser(userId);
return ResponseEntity.status(response.getStatus()).body(response);
}
SysLog 中 field 记录的是主键id,serviceEnum 是用哪个Service进行查询(配合Mybatis使用Service中的selectById方法),queryKey 指查询后需要获取哪个字段的值
步骤说明如下:
步骤一:扩展SysLog注解类,用于Controller的接口上
package com.swt.agy.annotation;
import com.swt.agy.aspect.ServiceEnum;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 系统日志注解,从方法上中提取参数,用于查、增、改操作
*
* @author Bleeth
* @version 1.0
* @date 2020-01-08 16:12
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
String msg() default "";
String field() default "";
ServiceEnum serviceEnum() default ServiceEnum.EMPTY;
String queryKey() default "";
}
步骤二:每个Service上添加一个Bean名称,Bean名称不允许重复
/**
* @author BleethNie
* @version 1.0
* @date 2020-01-08 17:48
**/
@Service("SysLoginService")
public class SysLoginServiceImpl implements SysLoginService {
private static DateTime LAST_DELETE_CAPTION_TIME;
@Value(value = "${swt.login.mac}")
private boolean swtLoginMac;
@Autowired
private SwtSsoConfigProperties swtSsoConfigProperties;
...
}
步骤三:添加辅助枚举类
package com.swt.agy.aspect;
import lombok.Getter;
/**
* @author BleethNie
* @version 1.0
* @date 2020-09-28 18:42
**/
@Getter
public enum ServiceEnum {
EMPTY(""),
SYS_USER("SysUserService"),
private String serviceName;
ServiceEnum(String serviceName) {
this.serviceName = serviceName;
}
}
步骤四:AOP解析
package com.swt.agy.aspect;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import com.swt.agy.annotation.SysLog;
import com.swt.agy.config.ResponseBean;
import com.swt.agy.config.ResponseMessage;
import com.swt.agy.module.sys.entity.SysLogEntity;
import com.swt.agy.module.sys.service.SysLogService;
import com.swt.agy.module.sys.service.SysMsgService;
import com.swt.agy.module.sys.service.SysUserService;
import com.swt.agy.shiro.ShiroUtils;
import com.swt.agy.util.IpUtil;
import com.swt.agy.util.RedisUtil;
import com.swt.agy.util.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
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.util.HashMap;
import java.util.Map;
/**
* 系统日志,切面处理类
*
* @author Bleeth
* @version 1.0
* @date 2020-01-08 16:12
*/
@Slf4j
@Aspect
@Component
public class SysLogAspect {
@Autowired
private SysLogService sysLogService;
@Autowired
private SysUserService sysUserService;
@Pointcut("@annotation(com.swt.agy.annotation.SysLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
String methodParam = null;
String methodMsg = null;
DateTime current = DateUtil.date();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Method method = getMethod(point);
String methodName = method.getName();
//想办法获取参数
SysLog sysLog = method.getAnnotation(SysLog.class);
ServiceEnum serviceEnum = sysLog.serviceEnum();
String paramField = sysLog.field();
String queryKey = sysLog.queryKey();
methodParam = parseKey(paramField, method, point.getArgs());
methodMsg = sysLog.msg();
if (!serviceEnum.equals(ServiceEnum.EMPTY)) {
String serviceName = serviceEnum.getServiceName();
IService iService = (IService) SpringContextUtil.getBean(serviceName);
boolean isNum = NumberUtil.isNumber(methodParam);
if (isNum && iService != null) {
Integer id = Convert.toInt(methodParam);
Object entity = iService.getById(id);
if (entity != null) {
JSONObject jsonObject = new JSONObject(entity);
methodParam = jsonObject.getStr(queryKey);
}
}
}
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
Integer userId = ShiroUtils.getUserId();
if (userId == null) {
return result;
}
SysLogEntity entity = new SysLogEntity();
entity.setUserId(userId);
entity.setParams(methodParam);
entity.setCreatedTime(current);
entity.setIp(IpUtil.getIpAddr(request));
entity.setMethod(methodName);
entity.setTime(time);
entity.setOperation(methodMsg);
entity.setRemark("");
if (result instanceof ResponseEntity) {
ResponseEntity responseEntity = (ResponseEntity) result;
Object body = responseEntity.getBody();
if (body instanceof ResponseMessage) {
ResponseMessage responseMessage = (ResponseMessage) body;
entity.setRemark(responseMessage.getMessage());
}
if (body instanceof ResponseBean) {
ResponseBean responseBean = (ResponseBean) body;
entity.setRemark(responseBean.getMessage());
}
}
sysLogService.save(entity);
return result;
}
/**
* 获取被拦截方法对象
* <p>
* MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象
* 而缓存的注解在实现类的方法上
* 所以应该使用反射获取当前对象的方法对象
*/
public Method getMethod(ProceedingJoinPoint point) {
//获取参数的类型
Object[] args = point.getArgs();
Class[] argTypes = new Class[point.getArgs().length];
for (int i = 0; i < args.length; i++) {
argTypes[i] = args[i].getClass();
}
Method method = null;
try {
method = point.getTarget().getClass().getMethod(point.getSignature().getName(), argTypes);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
return method;
}
private String parseKey(String key, Method method, Object[] args) {
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
//把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
String value = parser.parseExpression(key).getValue(context, String.class);
return value;
}
}
2.2 单点登录问题
2.3 整合Redis
整合Redis不是啥难事,网上教程很多,自己在网上找的RedisUtil工具,在此基础上进行了稍微的修改,使其可以直接set和get对象
package com.swt.agy.util;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* redisTemplate封装
*/
@Component
public class RedisUtil {
public static final long ONE_MIN_EXPIRE_TIME = 1 * 60;
public static final long DEFAULT_EXPIRE_TIME = 5 * 60;
public static final long TEN_MIN_EXPIRE_TIME = 10 * 60;
public static final long ONE_HOUR_EXPIRE_TIME = 60 * 60;
public static final long ONE_DAY_EXPIRE_TIME = 24 * 60 * 60;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
public <T> T getObject(String key, Class<T> clazz) {
if (key == null) {
return null;
}
String str = (String) (redisTemplate.opsForValue().get(key));
if (str == null) {
return null;
}
JSONObject jsonObject = JSONUtil.parseObj(str);
if (jsonObject == null) {
return null;
}
return jsonObject.toBean(clazz);
}
public <E> List<E> getArray(String key, Class<E> clazz) {
if (key == null) {
return null;
}
String str = (String) (redisTemplate.opsForValue().get(key));
if (str == null) {
return null;
}
JSONArray array = JSONUtil.parseArray(str);
if (array == null) {
return null;
}
return array.toList(clazz);
}
public JSONArray getJSONArray(String key) {
if (key == null) {
return null;
}
String str = (String) (redisTemplate.opsForValue().get(key));
if (str == null) {
return null;
}
JSONArray jsonArray = JSONUtil.parseArray(str);
return jsonArray;
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean setObject(String key, Object value, long time) {
JSONObject jsonObject = JSONUtil.parseObj(value);
try {
redisTemplate.opsForValue().set(key, jsonObject.toStringPretty());
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean setArray(String key, Object value, long time) {
JSONArray array = JSONUtil.parseArray(value);
try {
redisTemplate.opsForValue().set(key, array.toStringPretty());
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
HashOperations<String, Object, Object> opsHash = redisTemplate.opsForHash();
return opsHash.entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
2.4 消息管理模块
2.5 意见反馈模块
2.6 登录失败锁定功能
2.7 初始化时接口定义问题
2.8 验证码登录
实现验证码登录流程:
步骤一:前端调用获取验证码接口,调用接口时提供一个唯一UUID,提供的目的是将登录用户和验证码进行绑定
获取验证码Controller层
@ApiOperation("获取验证码")
@GetMapping(value = "/captcha", produces = "image/png")
@ApiImplicitParam(name = "uuid", value = "随机码", required = true, paramType = "query", dataTypeClass = String.class, defaultValue = "8e04634d-3420-47f8-a1af-392d77301bb9")
public byte[] captcha(String uuid) {
return sysLoginService.captcha(uuid);
}
获取验证码Service实现层
@Override
public byte[] captcha(String uuid) {
//生成captcha
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(150, 50);
String code = captcha.getCode();
SysCaptchaEntity sysCaptchaEntity = new SysCaptchaEntity();
sysCaptchaEntity.setCaptcha(code);
sysCaptchaEntity.setUuid(uuid);
sysCaptchaEntity.setCreatedTime(current);
sysCaptchaEntity.setExpireTime(expireDay);
sysCaptchaEntity.setStatus(SysCaptchaEntity.STATUS_VALID);
int row = sysCaptchaDao.insert(sysCaptchaEntity);
if (row == 0) {
throw new AgyException("获取验证码失败");
}
return captcha.getImageBytes();
}
验证码生成来源于Hutool中的工具类
步骤二:前端调用登录接口,登录时提供用户名,密码,上一步得到的验证码还有uuid
登录接口Controller层
@ApiOperationSupport(order = 3)
@ApiOperation("用户登陆")
@PostMapping(value = "/login")
@SysLog(msg = "用户登陆", field = "#form.username")
public ResponseEntity<ResponseMessage> login(@RequestBody SysLoginForm form) {
ResponseMessage response = sysLoginService.login(form);
return ResponseEntity.status(response.getStatus()).body(response);
}
登录接口Service实现层
@Override
public ResponseMessage login(SysLoginForm form) {
HttpStatus httpStatus = HttpStatus.OK;
//参数校验
ValidatorUtil.validateEntity(form);
//验证码校验
String uuid = form.getUuid();
String captcha = form.getCaptcha();
String username = form.getUsername();
SysCaptchaEntity sysCaptchaEntity = sysCaptchaDao.queryEntityByUuid(uuid);
if (sysCaptchaEntity == null) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
return new ResponseMessage(httpStatus.value(), "验证码校验失败");
}
String sysCaptcha = sysCaptchaEntity.getCaptcha();
sysCaptchaDao.useEntity(uuid);
if (StrUtil.isEmpty(sysCaptcha) || !StrUtil.equalsIgnoreCase(sysCaptcha, captcha)) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
return new ResponseMessage(httpStatus.value(), "验证码校验失败");
}
String message = "登陆成功";
Subject subject = ShiroUtils.getSubject();
try {
UsernamePasswordToken token = null;
if (swtLoginMac) {
token = new UsernamePasswordToken(username, form.getPassword());
} else {
token = new UsernamePasswordToken(username, form.getPassword());
}
subject.login(token);
//登录成功,失败次数重置
sysUserDao.updateUserLoginFail("1", username, 0);
} catch (UnknownAccountException e) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
message = e.getMessage();
} catch (IncorrectCredentialsException e) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
Integer failCount = sysUserDao.selectUserLoginFail(username);
if (failCount == null) {
} else if (failCount >= 4) {
message = "账号已被锁定,请联系管理员";
sysUserDao.updateUserLoginFail("0", username, 0);
} else {
failCount++;
message = "密码错误,剩余重试次数" + (5 - failCount) + "次";
sysUserDao.updateUserLoginFail("1", username, failCount);
}
} catch (LockedAccountException e) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
message = e.getMessage();
} catch (AccountException e) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
message = e.getMessage();
} catch (AuthenticationException e) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
message = e.getMessage();
}
return new ResponseMessage(httpStatus.value(), message);
}
3.0 安装部署
3.1 maven打包
springboot项目打包,一般存在的问题是如何打瘦包,即lib依赖和主程序jar包分开,Fat Jar的主要问题是打出的包太大,部署安装不太方便,所以本节主要讲SpringBoot如何打瘦包。
步骤一:在maven的pom中增加插件maven-assembly-plugin和maven-jar-plugin,代码如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>install</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<!-- 打包生成的文件名 -->
<finalName>${project.artifactId}</finalName>
<!-- jar 等压缩文件在被打包进入 zip、tar.gz 时是否压缩,设置为 false 可加快打包速度 -->
<recompressZippedFiles>false</recompressZippedFiles>
<!-- 打包生成的文件是否要追加 release.xml 中定义的 id 值 -->
<appendAssemblyId>true</appendAssemblyId>
<!-- 指向打包描述文件 package.xml -->
<descriptors>
<descriptor>package.xml</descriptor>
</descriptors>
<!-- 打包结果输出的基础目录 -->
<outputDirectory>${project.build.directory}/</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<excludes>
<exclude>*.yml</exclude>
<exclude>application.properties</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
maven-assembly-plugin:该插件可以使用户根据自己的需求自定义打包规则和打包类型,自定义的信息在package.xml中编写
maven-jar-plugin:将源码进行打包,打出的包不含依赖包
步骤二:项目根目录下添加package.xml,内容如下:
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>release</id>
<!--
设置打包格式,可同时设置多种格式,常用格式有:dir、zip、tar、tar.gz
dir 格式便于在本地测试打包结果
zip 格式便于 windows 系统下解压运行
tar、tar.gz 格式便于 linux 系统下解压运行
-->
<formats>
<format>dir</format>
</formats>
<!-- 打 zip 设置为 true 时,会在 zip 包中生成一个根目录,打 dir 时设置为 false 少层目录 -->
<includeBaseDirectory>true</includeBaseDirectory>
<fileSets>
<!-- src/main/resources 全部 copy 到 config 目录下 -->
<fileSet>
<directory>${basedir}/src/main/resources</directory>
<outputDirectory>config</outputDirectory>
<includes>
<include>application*.yml</include>
<include>logback*.xml</include>
</includes>
</fileSet>
<!-- 项目根下面的脚本文件 copy 到根目录下 -->
<fileSet>
<directory>${basedir}</directory>
<!-- 脚本文件在 linux 下的权限设为 755,无需 chmod 可直接运行 -->
<fileMode>755</fileMode>
<includes>
<include>*.sh</include>
<include>*.bat</include>
<include>*.md</include>
</includes>
</fileSet>
</fileSets>
<!-- 依赖的 jar 包 copy 到 lib 目录下 -->
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
</dependencySet>
</dependencySets>
</assembly>
配置完成,运行mvn package 则仅打包,为thin jar,运行mvn install则打完整包,会按照package.xml中描述进行打包,将依赖包,配置文件等整合在一起
示例项目结构:
打包后的结构,每次项目更新只需要mvn package打包,然后将该包放入最终结构的lib目录下,执行stop.sh 和 start.sh脚本启动停止;当第一次安装部署时用mvn install生成完整目录结构
3.2 脚本启动
上面的打包部署我们讲完了,但是启动的时候不可能用java命令执行程序,不够方便,下面则提供脚本,方便大家启动程序
start.sh内容如下:
#!/usr/bin/env bash
#!/bin/bash
# ----------------------------------------------------------------------
# name: start.sh
# version: 1.0
#
# 使用说明:
# 1: 该脚本使用前需要首先修改 MAIN_CLASS 值,使其指向实际的启动类
#
# 2:使用命令行 ./run.sh 启动项目
#
# 3: JAVA_OPTS 可传入标准的 java 命令行参数,例如 -Xms256m -Xmx1024m 这类常用参数
#
#
# ----------------------------------------------------------------------
# 启动入口类,该脚本文件用于别的项目时要改这里
MAIN_CLASS=com.swt.agy.AgyApp
COMMAND="$1"
# Java 命令行参数,根据需要开启下面的配置,改成自己需要的,注意等号前后不能有空格
# JAVA_OPTS="-Xms256m -Xmx1024m -Dundertow.port=80 -Dundertow.host=0.0.0.0"
# JAVA_OPTS="-Dundertow.port=80 -Dundertow.host=0.0.0.0"
# 生成 class path 值
APP_BASE_PATH=$(cd `dirname $0`; pwd)
CP=${APP_BASE_PATH}/config:${APP_BASE_PATH}/lib/*
function backendStart()
{
echo "backend 运行为后台进程,并且不在控制台输出信息"
# 运行为后台进程,并在控制台输出信息
# java -Xverify:none ${JAVA_OPTS} -cp ${CP} ${MAIN_CLASS} &
# 运行为后台进程,并且不在控制台输出信息
nohup java -Xverify:none ${JAVA_OPTS} -cp ${CP} ${MAIN_CLASS} >/dev/null 2>&1 &
# 运行为后台进程,并且将信息输出到 output.log 文件
# nohup java -Xverify:none ${JAVA_OPTS} -cp ${CP} ${MAIN_CLASS} > output.log &
# 运行为非后台进程,多用于开发阶段,快捷键 ctrl + c 可停止服务
# java -Xverify:none ${JAVA_OPTS} -cp ${CP} ${MAIN_CLASS}
}
function nohupStart()
{
echo "nohup 运行为后台进程,并且将信息输出到 output.log 文件"
nohup java -Xverify:none ${JAVA_OPTS} -cp ${CP} ${MAIN_CLASS} > output.log &
}
function start()
{
echo "运行为非后台进程,多用于开发阶段,快捷键 ctrl + c 可停止服务"
java -Xverify:none ${JAVA_OPTS} -cp ${CP} ${MAIN_CLASS}
}
function debugStart()
{
echo "运行调试模式,多用于开发阶段"
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Xverify:none ${JAVA_OPTS} -cp ${CP} ${MAIN_CLASS}
}
if [[ "$COMMAND" == "backend" ]]; then
backendStart
elif [[ "$COMMAND" == "nohup" ]]; then
nohupStart
elif [[ "$COMMAND" == "debug" ]]; then
debugStart
else
start
fi
需要注意修改的是 MAIN_CLASS,将该项修改为自己项目运行的main类,stop.sh中的同理
stop.sh内容如下:
#!/usr/bin/env bash
#!/bin/bash
# ----------------------------------------------------------------------
# name: run.sh
# version: 1.0
# author: yangfuhai
# email: fuhai999@gmail.com
#
# 使用说明:
# 1: 该脚本使用前需要首先修改 MAIN_CLASS 值,使其指向实际的启动类
#
# 2:使用命令行 ./run.sh start | stop | restart 可启动/关闭/重启项目
#
# 3: JAVA_OPTS 可传入标准的 java 命令行参数,例如 -Xms256m -Xmx1024m 这类常用参数
#
# ----------------------------------------------------------------------
# 启动入口类,该脚本文件用于别的项目时要改这里
MAIN_CLASS=com.swt.agy.AgyApp
# Java 命令行参数,根据需要开启下面的配置,改成自己需要的,注意等号前后不能有空格
# JAVA_OPTS="-Xms256m -Xmx1024m -Dundertow.port=80 -Dundertow.host=0.0.0.0"
# JAVA_OPTS="-Dundertow.port=80 -Dundertow.host=0.0.0.0"
# 生成 class path 值
APP_BASE_PATH=$(cd `dirname $0`; pwd)
CP=${APP_BASE_PATH}/config:${APP_BASE_PATH}/lib/*
function stop()
{
# 支持集群部署
kill `pgrep -f ${APP_BASE_PATH}` 2>/dev/null
# kill 命令不使用 -9 参数时,会回调 onStop() 方法,确定不需要此回调建议使用 -9 参数
# kill `pgrep -f ${MAIN_CLASS}` 2>/dev/null
# 以下代码与上述代码等价
# kill $(pgrep -f ${MAIN_CLASS}) 2>/dev/null
}
stop
需要注意的:
- start.sh和stop.sh脚本来源于JFinal项目中的脚本,再此进行了一些修改
- start.sh启动方法./start.sh debug 配合 idea进行远程调试 ./start.sh nohup 后台启动 ./start.sh 前端启动,关闭shell则停止运行
- start.sh脚本用于linux时需要赋予可执行权限和用vim修改为\n换行符(set ff=unix)
3.3 项目开机自启动配置
项目开机自启动,利用 systemctl 系统服务管理器指令,因为前面一节我们介绍了脚本启动程序,此时配置开机启动脚本相对简单,并且网上参考资料较多,在此贴上一份Elasticsearch的自启动脚本配置,仅供参考
[Unit]
Description= elasticsearch全文检索服务器
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
ExecStart=/home/shuwei/gapenv/elasticsearch-6.4.3/bin/elasticsearch -d -p /home/shuwei/gapenv/elasticsearch-6.4.3/esearch.pid
ExecStop=kill -INT `cat /home/shuwei/gapenv/elasticsearch-6.4.3/esearch.pid`
User=elsearch
Group=elsearch
LimitCORE=infinity
LimitNOFILE=65536
LimitNPROC=65536
[Install]
WantedBy=multi-user.target