本文章是基于我的另一篇博客所写的相关代码,如果还没看过的可以先看看我这篇文章:
https://blog.csdn.net/qq_56769991/article/details/123915587
核心代码
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VisitLogger {
/**
* 访问行为枚举
*/
VisitBehavior value() default VisitBehavior.UNKNOWN;
}
切面配置:
@Component
@Aspect //该类用作切面类
public class VisitLogAspect {
@Autowired
VisitLogService visitLogService;
@Autowired
VisitorService visitorService;
@Autowired
RedisService redisService;
ThreadLocal<Long> currentTime = new ThreadLocal<>();
/**
* 配置切入点:表示被VisitLogger注解的方法都可以设置为切入点。
* 下面的没有内容的方法,其实就是代表被视为切入点的那个方法,在形参中我们传入了参数visitLogger,因为我们要使用被
* 切入点方法注解中的相关参数
*/
@Pointcut("@annotation(visitLogger)")
public void logPointcut(VisitLogger visitLogger) {
}
/**
* 配置环绕通知
*
* @param joinPoint
* @return
* @throws Throwable
* 在切入点的前后进行操作
* 这里的ProceedingJoinPoint表示被切入点的那个方法里面的逻辑内容
* visitLogger表示我们注解的内容
* 这些参数系统会帮我们自动传入
* 在环绕通知的返回值应该与被环绕的方法的返回值一致
*/
@Around("logPointcut(visitLogger)")
public Object logAround(ProceedingJoinPoint joinPoint, VisitLogger visitLogger) throws Throwable {
currentTime.set(System.currentTimeMillis());
Result result = (Result) joinPoint.proceed();
int times = (int) (System.currentTimeMillis() - currentTime.get());
currentTime.remove();
//获取请求对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//校验访客标识码
String identification = checkIdentification(request);
//记录访问日志
VisitLog visitLog = handleLog(joinPoint, visitLogger, request, result, times, identification);
//存到数据库
visitLogService.saveVisitLog(visitLog);
return result;
}
/**
* 校验访客标识码
*
* @param request
* @return
*/
private String checkIdentification(HttpServletRequest request) {
String identification = request.getHeader("identification");
if (identification == null) {
//请求头没有uuid,签发uuid并保存到数据库和Redis
identification = saveUUID(request);
} else {
//校验Redis中是否存在uuid
boolean redisHas = redisService.hasValueInSet(RedisKeyConstants.IDENTIFICATION_SET, identification);
//Redis中不存在uuid
if (!redisHas) {
//校验数据库中是否存在uuid
boolean mysqlHas = visitorService.hasUUID(identification);
if (mysqlHas) {
//数据库存在,保存至Redis
redisService.saveValueToSet(RedisKeyConstants.IDENTIFICATION_SET, identification);
} else {
//数据库不存在,签发新的uuid
identification = saveUUID(request);
}
}
}
return identification;
}
/**
* 签发UUID,并保存至数据库和Redis
*
* @param request
* @return
*/
private String saveUUID(HttpServletRequest request) {
//获取响应对象
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
//获取当前时间戳,精确到小时,防刷访客数据
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
String timestamp = Long.toString(calendar.getTimeInMillis() / 1000);
//获取访问者基本信息
String ip = IpAddressUtils.getIpAddress(request);
String userAgent = request.getHeader("User-Agent");
//根据时间戳、ip、userAgent生成UUID
String nameUUID = timestamp + ip + userAgent;
String uuid = UUID.nameUUIDFromBytes(nameUUID.getBytes()).toString();
//添加访客标识码UUID至响应头
response.addHeader("identification", uuid);
//暴露自定义header供页面资源使用
response.addHeader("Access-Control-Expose-Headers", "identification");
//校验Redis中是否存在uuid
boolean redisHas = redisService.hasValueInSet(RedisKeyConstants.IDENTIFICATION_SET, uuid);
if (!redisHas) {
//保存至Redis
redisService.saveValueToSet(RedisKeyConstants.IDENTIFICATION_SET, uuid);
//保存至数据库
Visitor visitor = new Visitor(uuid, ip, userAgent);
visitorService.saveVisitor(visitor);
}
return uuid;
}
/**
* 设置VisitLogger对象属性
*
* @param joinPoint
* @param visitLogger
* @param result
* @param times
* @return
*/
private VisitLog handleLog(ProceedingJoinPoint joinPoint, VisitLogger visitLogger, HttpServletRequest request, Result result,
int times, String identification) {
String uri = request.getRequestURI();
String method = request.getMethod(); //获取请求方法
String ip = IpAddressUtils.getIpAddress(request);
String userAgent = request.getHeader("User-Agent");
//通过切面的ProceedingJoinPoint获取相关信息的工具类,在这里主要是获得请求参数
Map<String, Object> requestParams = AopUtils.getRequestParams(joinPoint);
//这里visitLogRemark是visitlog表中的一小部分
VisitLogRemark visitLogRemark = judgeBehavior(visitLogger.value(), requestParams, result);
VisitLog log = new VisitLog(identification, uri, method, visitLogger.value().getBehavior(),
visitLogRemark.getContent(), visitLogRemark.getRemark(), ip, times, userAgent);
log.setParam(StringUtils.substring(JacksonUtils.writeValueAsString(requestParams), 0, 2000));
// 将visitLog相关信息查询到后放到对象中并返回
return log;
}
/**
* 根据访问行为,设置对应的访问内容或备注
*
* @param behavior
* @param requestParams
* @param result
* @return
*/
private VisitLogRemark judgeBehavior(VisitBehavior behavior, Map<String, Object> requestParams, Result result) {
String remark = "";
String content = behavior.getContent();
switch (behavior) {
case INDEX:
case MOMENT:
remark = "第" + requestParams.get("pageNum") + "页";
break;
case BLOG:
if (result.getCode() == 200) {
BlogDetail blog = (BlogDetail) result.getData();
String title = blog.getTitle();
content = title;
remark = "文章标题:" + title;
}
break;
case SEARCH:
if (result.getCode() == 200) {
String query = (String) requestParams.get("query");
content = query;
remark = "搜索内容:" + query;
}
break;
case CATEGORY:
String categoryName = (String) requestParams.get("categoryName");
content = categoryName;
remark = "分类名称:" + categoryName + ",第" + requestParams.get("pageNum") + "页";
break;
case TAG:
String tagName = (String) requestParams.get("tagName");
content = tagName;
remark = "标签名称:" + tagName + ",第" + requestParams.get("pageNum") + "页";
break;
case CLICK_FRIEND:
String nickname = (String) requestParams.get("nickname");
content = nickname;
remark = "友链名称:" + nickname;
break;
}
return new VisitLogRemark(content, remark);
}
}
工具类:
@Slf4j
@Component
public class IpAddressUtils {
/**
* 在Nginx等代理之后获取用户真实IP地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Real-IP");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
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();
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
//根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
log.error("getIpAddress exception:", e);
}
ip = inet.getHostAddress();
}
}
return StringUtils.substringBefore(ip, ",");
}
private static DbSearcher searcher;
private static Method method;
/**
* 在服务启动时加载 ip2region.db 到内存中
* 解决打包jar后找不到 ip2region.db 的问题
*
* @throws Exception 出现异常应该直接抛出终止程序启动,避免后续invoke时出现更多错误
*/
@PostConstruct
private void initIp2regionResource() throws Exception {
InputStream inputStream = new ClassPathResource("/ipdb/ip2region.db").getInputStream();
//将 ip2region.db 转为 ByteArray
byte[] dbBinStr = FileCopyUtils.copyToByteArray(inputStream);
DbConfig dbConfig = new DbConfig();
searcher = new DbSearcher(dbConfig, dbBinStr);
//二进制方式初始化 DBSearcher,需要使用基于内存的查找算法 memorySearch
method = searcher.getClass().getMethod("memorySearch", String.class);
}
/**
* 根据ip从 ip2region.db 中获取地理位置
*
* @param ip
* @return
*/
public static String getCityInfo(String ip) {
if (ip == null || !Util.isIpAddress(ip)) {
log.error("Error: Invalid ip address");
return "";
}
try {
DataBlock dataBlock = (DataBlock) method.invoke(searcher, ip);
String ipInfo = dataBlock.getRegion();
if (!StringUtils.isEmpty(ipInfo)) {
ipInfo = ipInfo.replace("|0", "");
ipInfo = ipInfo.replace("0|", "");
return ipInfo;
}
} catch (Exception e) {
log.error("getCityInfo exception:", e);
}
return "";
}
}
@Component
public class UserAgentUtils {
private UserAgentAnalyzer uaa;
public UserAgentUtils() {
this.uaa = UserAgentAnalyzer
.newBuilder()
.hideMatcherLoadStats()
.withField(UserAgent.OPERATING_SYSTEM_NAME_VERSION_MAJOR)
.withField(UserAgent.AGENT_NAME_VERSION)
.build();
}
/**
* 从User-Agent解析客户端操作系统和浏览器版本
*
* @param userAgent
* @return
*/
public Map<String, String> parseOsAndBrowser(String userAgent) {
UserAgent agent = uaa.parse(userAgent);
String os = agent.getValue(UserAgent.OPERATING_SYSTEM_NAME_VERSION_MAJOR);
String browser = agent.getValue(UserAgent.AGENT_NAME_VERSION);
Map<String, String> map = new HashMap<>();
map.put("os", os);
map.put("browser", browser);
return map;
}
}
public class AopUtils {
private static Set<String> ignoreParams = new HashSet<String>() {
{
add("jwt");
}
};
/**
* 获取请求参数
*
* @param joinPoint
* @return
*/
public static Map<String, Object> getRequestParams(JoinPoint joinPoint) {
Map<String, Object> map = new LinkedHashMap<>();
String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames(); //获得切入点方法的参数的名字
Object[] args = joinPoint.getArgs(); //获得参数对应的值
for (int i = 0; i < args.length; i++) {
if (!isIgnoreParams(parameterNames[i]) && !isFilterObject(args[i])) {
map.put(parameterNames[i], args[i]);
}
}
return map;
}
/**
* consider if the data is file, httpRequest or response
*
* @param o the data
* @return if match return true, else return false
*/
private static boolean isFilterObject(final Object o) {
return o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof MultipartFile;
}
/**
* 判断是否忽略参数
*
* @param params
* @return
*/
private static boolean isIgnoreParams(String params) {
return ignoreParams.contains(params);
}
}
数据库
visitor
DROP TABLE IF EXISTS `visitor`;
CREATE TABLE `visitor` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`uuid` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '访客标识码',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip',
`ip_source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip来源',
`os` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统',
`browser` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '浏览器',
`create_time` datetime(0) NOT NULL COMMENT '首次访问时间',
`last_time` datetime(0) NOT NULL COMMENT '最后访问时间',
`pv` int(0) NULL DEFAULT NULL COMMENT '访问页数统计',
`user_agent` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'user-agent用户代理',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `idx_uuid`(`uuid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
visit_log
DROP TABLE IF EXISTS `visit_log`;
CREATE TABLE `visit_log` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`uuid` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '访客标识码',
`uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '请求接口',
`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '请求方式',
`param` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '请求参数',
`behavior` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '访问行为',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '访问内容',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip',
`ip_source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ip来源',
`os` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统',
`browser` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '浏览器',
`times` int(0) NOT NULL COMMENT '请求耗时(毫秒)',
`create_time` datetime(0) NOT NULL COMMENT '访问时间',
`user_agent` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'user-agent用户代理',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
result
@NoArgsConstructor
@Getter
@Setter
@ToString
public class Result {
private Integer code;
private String msg;
private Object data;
private Result(Integer code, String msg) {
this.code = code;
this.msg = msg;
this.data = null;
}
private Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static Result ok(String msg, Object data) {
return new Result(200, msg, data);
}
public static Result ok(String msg) {
return new Result(200, msg);
}
public static Result error(String msg) {
return new Result(500, msg);
}
public static Result error() {
return new Result(500, "异常错误");
}
public static Result create(Integer code, String msg, Object data) {
return new Result(code, msg, data);
}
public static Result create(Integer code, String msg) {
return new Result(code, msg);
}
}
public enum VisitBehavior {
UNKNOWN("UNKNOWN", "UNKNOWN"),
INDEX("访问页面", "首页"),
ARCHIVE("访问页面", "归档"),
MOMENT("访问页面", "动态"),
FRIEND("访问页面", "友链"),
ABOUT("访问页面", "关于我"),
BLOG("查看博客", ""),
CATEGORY("查看分类", ""),
TAG("查看标签", ""),
SEARCH("搜索博客", ""),
CLICK_FRIEND("点击友链", ""),
LIKE_MOMENT("点赞动态", ""),
CHECK_PASSWORD("校验博客密码", ""),
;
/**
* 访问行为
*/
private String behavior;
/**
* 访问内容
*/
private String content;
VisitBehavior(String behavior, String content) {
this.behavior = behavior;
this.content = content;
}
public String getBehavior() {
return behavior;
}
public String getContent() {
return content;
}
}
Controller
@VisitLogger(VisitBehavior.INDEX)
@GetMapping("/blogs")
public Result blogs(@RequestParam(defaultValue = "1") Integer pageNum) {
PageResult<BlogInfo> pageResult = blogService.getBlogInfoListByIsPublished(pageNum);
return Result.ok("请求成功", pageResult);
}
关于service层的代码就不给出了,自己进行实现就行