最开始蘑菇博客收集用户访问日志,是直接在请求接口里面进行编写的,比如像下面这样
很显然这种方法是非常笨的一种方法,因为它直接侵入了我们的业务代码,引入无关的操作,因此这次主要就是通过spring aop + 自定义接口,来收集用户的访问日志
首先我们需要创建一个自定义接口
package com.moxi.mogublog.web.log;
import com.moxi.mougblog.base.enums.EBehavior;
import com.moxi.mougblog.base.enums.PlatformEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 日志记录、自定义注解
*
* @author 陌溪
* @date 2020年2月27日08:55:02
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BussinessLog {
/**
* 业务名称
*
* @return
*/
String value() default "";
/**
* 用户行为
*
* @return
*/
EBehavior behavior();
/**
* 平台,默认为WEB端
*/
PlatformEnum platform() default PlatformEnum.WEB;
/**
* 是否将当前日志记录到数据库中
*/
boolean save() default true;
}
这里的用户行为使用了枚举类,方便扩展,目前共有15中行为
package com.moxi.mougblog.base.enums;
import com.moxi.mogublog.utils.JsonUtils;
import com.moxi.mougblog.base.global.BaseSysConf;
import java.util.HashMap;
import java.util.Map;
public enum EBehavior {
BLOG_TAG("点击标签", "blog_tag"),
BLOG_SORT("点击博客分类", "blog_sort"),
BLOG_CONTNET("点击博客", "blog_content"),
BLOG_PRAISE("点赞", "blog_praise"),
FRIENDSHIP_LINK("点击友情链接", "friendship_link"),
BLOG_SEARCH("点击搜索", "blog_search"),
STUDY_VIDEO("点击学习视频", "study_video"),
VISIT_PAGE("访问页面", "visit_page"),
VISIT_SORT("点击归档", "visit_sort"),
BLOG_AUTHOR("点击作者", "blog_author"),
PUBLISH_COMMENT("发表评论", "publish_comment"),
DELETE_COMMENT("删除评论", "delete_comment"),
REPORT_COMMENT("举报评论", "report_comment"),
VISIT_CLASSIFY("点击分类", "visit_classify");
private String content;
private String behavior;
private EBehavior(String content, String behavior) {
this.content = content;
this.behavior = behavior;
}
/**
* 根据value返回枚举类型,主要在switch中使用
* @param value
* @return
*/
public static EBehavior getByValue(String value) {
for(EBehavior behavior: values()) {
if(behavior.getBehavior() == value) {
return behavior;
}
}
return null;
}
public static Map<String, String> getModuleAndOtherData(EBehavior behavior, Map<String, Object> nameAndArgsMap, String bussinessName) {
String otherData = "";
String moduleUid = "";
switch (behavior) {
case BLOG_AUTHOR: {
// 判断是否是点击作者
if(nameAndArgsMap.get(BaseSysConf.AUTHOR) != null) {
otherData = nameAndArgsMap.get(BaseSysConf.AUTHOR).toString();
}
};break;
case BLOG_SORT: {
// 判断是否点击博客分类
if(nameAndArgsMap.get(BaseSysConf.BLOG_SORT_UID) != null) {
moduleUid = nameAndArgsMap.get(BaseSysConf.BLOG_SORT_UID).toString();
}
};break;
case BLOG_TAG: {
// 判断是否点击博客标签
if(nameAndArgsMap.get(BaseSysConf.TAG_UID) != null) {
moduleUid = nameAndArgsMap.get(BaseSysConf.TAG_UID).toString();
}
};break;
case BLOG_SEARCH: {
// 判断是否进行搜索
if(nameAndArgsMap.get(BaseSysConf.KEYWORDS) != null) {
otherData = nameAndArgsMap.get(BaseSysConf.KEYWORDS).toString();
}
};break;
case VISIT_CLASSIFY: {
// 判断是否点击分类
if(nameAndArgsMap.get(BaseSysConf.BLOG_SORT_UID) != null) {
moduleUid = nameAndArgsMap.get(BaseSysConf.BLOG_SORT_UID).toString();
}
};break;
case VISIT_SORT: {
// 判断是否点击归档
if(nameAndArgsMap.get(BaseSysConf.MONTH_DATE) != null) {
otherData = nameAndArgsMap.get(BaseSysConf.MONTH_DATE).toString();
}
};break;
case BLOG_CONTNET: {
// 判断是否博客详情
if(nameAndArgsMap.get(BaseSysConf.UID) != null) {
moduleUid = nameAndArgsMap.get(BaseSysConf.UID).toString();
}
};break;
case BLOG_PRAISE: {
// 判断是否给博客点赞
if(nameAndArgsMap.get(BaseSysConf.UID) != null) {
moduleUid = nameAndArgsMap.get(BaseSysConf.UID).toString();
}
};break;
case VISIT_PAGE: {
// 访问页面
otherData = bussinessName;
};break;
case PUBLISH_COMMENT: {
Object object = nameAndArgsMap.get(BaseSysConf.COMMENT_VO);
Map<String, Object> map = JsonUtils.objectToMap(object);
if(map.get(BaseSysConf.CONTENT) != null) {
otherData = map.get(BaseSysConf.CONTENT).toString();
}
};break;
case REPORT_COMMENT: {
// 举报评论
Object object = nameAndArgsMap.get(BaseSysConf.COMMENT_VO);
Map<String, Object> map = JsonUtils.objectToMap(object);
if(map.get(BaseSysConf.CONTENT) != null) {
otherData = map.get(BaseSysConf.CONTENT).toString();
}
};break;
case DELETE_COMMENT: {
// 删除评论
Object object = nameAndArgsMap.get(BaseSysConf.COMMENT_VO);
Map<String, Object> map = JsonUtils.objectToMap(object);
if(map.get(BaseSysConf.CONTENT) != null) {
otherData = map.get(BaseSysConf.CONTENT).toString();
}
};break;
}
Map<String, String> result = new HashMap<>();
result.put(BaseSysConf.MODULE_UID, moduleUid);
result.put(BaseSysConf.OTHER_DATA, otherData);
return result;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getBehavior() {
return behavior;
}
public void setBehavior(String behavior) {
this.behavior = behavior;
}
}
在AOP中,我们使用环绕通知的方式,来收集用户的访问日志
package com.moxi.mogublog.web.log;
import com.moxi.mogublog.utils.AopUtils;
import com.moxi.mogublog.utils.AspectUtil;
import com.moxi.mogublog.utils.IpUtils;
import com.moxi.mogublog.web.global.SysConf;
import com.moxi.mogublog.xo.entity.ExceptionLog;
import com.moxi.mogublog.xo.entity.SysLog;
import com.moxi.mogublog.xo.service.WebVisitService;
import com.moxi.mougblog.base.enums.EBehavior;
import com.moxi.mougblog.base.holder.RequestHolder;
import com.moxi.mougblog.base.util.RequestUtil;
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.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;
/**
* 日志切面
*/
@Aspect
@Component
@Slf4j
public class LoggerAspect {
private SysLog sysLog;
private ExceptionLog exceptionLog;
@Autowired
private WebVisitService webVisitService;
@Pointcut(value = "@annotation(bussinessLog)")
public void pointcut(BussinessLog bussinessLog) {
}
@Around(value = "pointcut(bussinessLog)")
public Object doAround(ProceedingJoinPoint joinPoint, BussinessLog bussinessLog) throws Throwable {
//先执行业务
Object result = joinPoint.proceed();
try {
// 日志收集
handle(joinPoint);
} catch (Exception e) {
log.error("日志记录出错!", e);
}
return result;
}
private void handle(ProceedingJoinPoint point) throws Exception {
HttpServletRequest request = RequestHolder.getRequest();
Method currentMethod = AspectUtil.INSTANCE.getMethod(point);
//获取操作名称
BussinessLog annotation = currentMethod.getAnnotation(BussinessLog.class);
boolean save = annotation.save();
EBehavior behavior = annotation.behavior();
String bussinessName = AspectUtil.INSTANCE.parseParams(point.getArgs(), annotation.value());
String ua = RequestUtil.getUa();
log.info("{} | {} - {} {} - {}", bussinessName, IpUtils.getIpAddr(request), RequestUtil.getMethod(), RequestUtil.getRequestUrl(), ua);
if (!save) {
return;
}
// 获取参数名称和值
Map<String, Object> nameAndArgsMap = AopUtils.getFieldsName(point);
Map<String, String> result = EBehavior.getModuleAndOtherData(behavior, nameAndArgsMap, bussinessName);
AopUtils.getFieldsName(point);
if (result != null) {
String userUid = "";
if (request.getAttribute(SysConf.USER_UID) != null) {
userUid = request.getAttribute(SysConf.USER_UID).toString();
}
webVisitService.addWebVisit(userUid, request, behavior.getBehavior(), result.get(SysConf.MODULE_UID), result.get(SysConf.OTHER_DATA));
}
}
}
这里使用了一个AspectUtils工具类
package com.moxi.mogublog.utils;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.List;
/**
* AOP相关的工具
* @author 陌溪
* @date 2020年2月27日08:44:28
*/
public enum AspectUtil {
INSTANCE;
/**
* 获取以类路径为前缀的键
*
* @param point 当前切面执行的方法
*/
public String getKey(JoinPoint point, String prefix) {
String keyPrefix = "";
if (!StringUtils.isEmpty(prefix)) {
keyPrefix += prefix;
}
keyPrefix += getClassName(point);
return keyPrefix;
}
/**
* 获取当前切面执行的方法所在的class
*
* @param point 当前切面执行的方法
*/
public String getClassName(JoinPoint point) {
return point.getTarget().getClass().getName().replaceAll("\\.", "_");
}
/**
* 获取当前切面执行的方法的方法名
*
* @param point 当前切面执行的方法
*/
public Method getMethod(JoinPoint point) throws NoSuchMethodException {
Signature sig = point.getSignature();
MethodSignature msig = (MethodSignature) sig;
Object target = point.getTarget();
return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
}
public String parseParams(Object[] params, String bussinessName) {
if (bussinessName.contains("{") && bussinessName.contains("}")) {
List<String> result = RegexUtils.match(bussinessName, "(?<=\\{)(\\d+)");
for (String s : result) {
int index = Integer.parseInt(s);
bussinessName = bussinessName.replaceAll("\\{" + index + "}", JSON.toJSONString(params[index - 1]));
}
}
return bussinessName;
}
}
以及AOPUtils
package com.moxi.mogublog.utils;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.javassist.*;
import org.apache.ibatis.javassist.bytecode.CodeAttribute;
import org.apache.ibatis.javassist.bytecode.LocalVariableAttribute;
import org.apache.ibatis.javassist.bytecode.MethodInfo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 切面相关工具类
*
* @author: 陌溪
* @create: 2020-01-21-12:34
*/
@Slf4j
public class AopUtils {
/**
* 获取参数名和值
* @param joinPoint
* @return
*/
public static Map getFieldsName(ProceedingJoinPoint joinPoint) throws ClassNotFoundException, NoSuchMethodException {
// 参数值
Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] parameterNames = methodSignature.getParameterNames();
// 通过map封装参数和参数值
HashMap<String, Object> paramMap = new HashMap();
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
return paramMap;
}
}
需要注意的是,我们在进行日志收集的时候,采用的是@Async注解修饰,也就是异步调用
在Spring中,基于@Async标注的方法,称之为异步方法;这些方法将在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。
@Async
@Override
public void addWebVisit(String userUid, HttpServletRequest request, String behavior, String moduleUid, String otherData) {
//增加记录(可以考虑使用AOP)
Map<String, String> map = IpUtils.getOsAndBrowserInfo(request);
String os = map.get("OS");
String browser = map.get("BROWSER");
WebVisit webVisit = new WebVisit();
String ip = IpUtils.getIpAddr(request);
webVisit.setIp(ip);
//从Redis中获取IP来源
String jsonResult = stringRedisTemplate.opsForValue().get("IP_SOURCE:" + ip);
if (StringUtils.isEmpty(jsonResult)) {
String addresses = IpUtils.getAddresses("ip=" + ip, "utf-8");
if (StringUtils.isNotEmpty(addresses)) {
webVisit.setIpSource(addresses);
stringRedisTemplate.opsForValue().set("IP_SOURCE" + BaseSysConf.REDIS_SEGMENTATION + ip, addresses, 24, TimeUnit.HOURS);
}
} else {
webVisit.setIpSource(jsonResult);
}
webVisit.setOs(os);
webVisit.setBrowser(browser);
webVisit.setUserUid(userUid);
webVisit.setBehavior(behavior);
webVisit.setModuleUid(moduleUid);
webVisit.setOtherData(otherData);
webVisit.insert();
}
tip:在使用@Async注解时候,需要在启动类中加入 @EnableAsync 才能够开启异步功能
最后我们使用 @BussinessLog 在我们需要收集的日志出进行标记,标记后AOP的环绕通知 就会获取该接口的相关参数,将其实例化到数据库中
示例代码如下:
@BussinessLog(value = "发表评论", behavior = EBehavior.PUBLISH_COMMENT)
@ApiOperation(value = "增加评论", notes = "增加评论")
@PostMapping("/add")
public String add(@Validated({Insert.class}) @RequestBody CommentVO commentVO, BindingResult result) {
QueryWrapper<WebConfig> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(SysConf.STATUS, EStatus.ENABLE);
WebConfig webConfig = webConfigService.getOne(queryWrapper);
if (SysConf.CAN_NOT_COMMENT.equals(webConfig.getStartComment())) {
return ResultUtil.result(SysConf.ERROR, MessageConf.NO_COMMENTS_OPEN);
}
ThrowableUtils.checkParamArgument(result);
if (commentVO.getContent().length() > SysConf.TWO_TWO_FIVE) {
return ResultUtil.result(SysConf.ERROR, MessageConf.COMMENT_CAN_NOT_MORE_THAN_225);
}
Comment comment = new Comment();
comment.setSource(commentVO.getSource());
comment.setBlogUid(commentVO.getBlogUid());
comment.setContent(commentVO.getContent());
comment.setUserUid(commentVO.getUserUid());
comment.setToUid(commentVO.getToUid());
comment.setToUserUid(commentVO.getToUserUid());
comment.setStatus(EStatus.ENABLE);
comment.insert();
User user = userService.getById(commentVO.getUserUid());
//获取图片
if (StringUtils.isNotEmpty(user.getAvatar())) {
String pictureList = this.pictureFeignClient.getPicture(user.getAvatar(), SysConf.FILE_SEGMENTATION);
if (webUtils.getPicture(pictureList).size() > 0) {
user.setPhotoUrl(webUtils.getPicture(pictureList).get(0));
}
}
comment.setUser(user);
return ResultUtil.result(SysConf.SUCCESS, comment);
}
最后用户的日志记录也能够成功记录下来了