在社区管理系统中,日志记录和数据可视化是两个核心需求。日志记录用于追踪系统操作,保障安全性;地图展示则能直观呈现小区分布。本文将详细介绍如何通过自定义注解 + AOP 实现声明式日志记录,并结合百度地图 API 实现小区地图功能。
一、声明式日志记录:注解 + AOP 的优雅实现
传统的日志记录方式需要在每个方法中手动编写日志代码,存在代码冗余、维护困难等问题。而通过自定义注解结合 AOP(面向切面编程),可以实现日志记录与业务逻辑的解耦,达到 "声明式" 记录日志的效果。
1. 自定义日志注解:@LogAnnotation
首先,我们需要定义一个用于标记需要记录日志的方法的注解。这个注解将作为 AOP 的切入点标识。
package com.qcby.community.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 仅用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可通过反射获取
@Documented // 生成API文档时包含该注解
public @interface LogAnnotation {
String value() default ""; // 用于描述操作名称,如"人脸采集"、"添加小区"
}
注解设计解析:
@Target(ElementType.METHOD)
:限制注解仅能作用于方法,符合日志记录的场景(通常记录方法级别的操作)。@Retention(RetentionPolicy.RUNTIME)
:指定注解在运行时可见,因为 AOP 需要在程序运行时通过反射获取注解信息。value()
属性:用于存储操作的描述信息,如 "人脸采集"、"删除小区" 等,使日志更具可读性。
2. AOP 切面实现:LogAspect
有了注解后,我们需要通过 AOP 切面捕获被@LogAnnotation
标记的方法,在方法执行前后自动记录日志。核心逻辑是:在方法执行前记录开始时间,执行后收集操作信息并保存日志。
package com.qcby.community.aspect;
import com.google.gson.Gson;
import com.qcby.community.annotation.LogAnnotation;
import com.qcby.community.entity.Log;
import com.qcby.community.entity.User;
import com.qcby.community.service.LogService;
import com.qcby.community.util.HttpContextUtil;
import com.qcby.community.util.IPUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Date;
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Autowired
private LogService logService; // 日志保存服务
// 切入点:所有被@LogAnnotation标记的方法
@Pointcut("@annotation(com.qcby.community.annotation.LogAnnotation)")
public void logPointCut() {}
// 环绕通知:在方法执行前后执行日志记录逻辑
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis(); // 记录开始时间
Object result = point.proceed(); // 执行目标方法
long time = System.currentTimeMillis() - beginTime; // 计算方法执行耗时
saveLog(point, (int) time); // 保存日志
return result;
}
// 日志保存逻辑
private void saveLog(ProceedingJoinPoint joinPoint, int time) {
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Log log = new Log(); // 日志实体类
// 1. 获取注解中的操作描述
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
if (logAnnotation != null) {
log.setOperation(logAnnotation.value()); // 如"人脸采集"
}
// 2. 记录调用的类和方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.setMethod(className + "." + methodName + "()"); // 如"com.qcby.controller.PersonController.addPerson()"
// 3. 记录请求参数
Object[] args = joinPoint.getArgs();
try {
if (args != null && args.length > 0) {
log.setParams(new Gson().toJson(args[0])); // 使用Gson将参数转为JSON
}
} catch (Exception e) {
logger.error("参数解析失败", e);
log.setParams("参数解析失败"); // 容错处理
}
// 4. 获取请求信息(IP、用户)
HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
if (request != null) {
log.setIp(IPUtil.getIpAddr(request)); // 获取客户端IP
// 从Session获取当前登录用户
HttpSession session = request.getSession();
if (session != null) {
User currentUser = (User) session.getAttribute("user");
log.setUsername(currentUser != null ? currentUser.getUsername() : "匿名用户");
}
}
// 5. 记录执行时间和操作时间
log.setTime(time); // 方法执行耗时(毫秒)
log.setCreateTime(new Date()); // 操作发生的时间
// 6. 保存日志到数据库
logService.save(log);
logger.info("日志保存成功: {}", log);
} catch (Exception e) {
logger.error("日志保存失败", e); // 日志记录失败不影响主业务
}
}
}
切面核心逻辑解析:
-
切入点定义:
@Pointcut("@annotation(com.qcby.community.annotation.LogAnnotation)")
表示所有被@LogAnnotation
标记的方法都会被该切面拦截。 -
环绕通知(@Around):
环绕通知是 AOP 中功能最强大的通知类型,它可以在方法执行前后插入逻辑。这里我们用它来:- 记录方法执行开始时间
- 调用
point.proceed()
执行目标方法(业务逻辑) - 计算执行耗时并触发日志保存
-
日志信息收集:
保存日志时需要收集的关键信息包括:- 操作描述(从注解
value
中获取) - 调用的类和方法名(通过
ProceedingJoinPoint
获取) - 请求参数(通过 Gson 转为 JSON 字符串,便于存储和查看)
- 客户端 IP(通过工具类从请求中获取)
- 操作用户(从 Session 中获取当前登录用户)
- 执行耗时和操作时间
- 操作描述(从注解
-
容错设计:
- 日志记录失败时(如数据库异常),通过
try-catch
捕获并仅记录错误日志,不影响主业务流程。 - 参数解析失败时,设置默认提示信息,避免日志记录中断。
- 日志记录失败时(如数据库异常),通过
3. 注解的使用:在业务方法中标记日志
定义好注解和切面后,使用方式非常简单:只需在需要记录日志的方法上添加@LogAnnotation
并指定操作描述即可。
// 人脸采集方法示例
@LogAnnotation("人脸采集")
@PostMapping("/addPerson")
public Result addPerson(@RequestBody PersonFaceForm personFaceForm) {
// 业务逻辑:处理人脸采集...
return Result.ok();
}
// 添加小区方法示例
@LogAnnotation("添加小区")
@PostMapping("/add")
public Result add(@RequestBody Community community, HttpSession session) {
// 业务逻辑:添加小区...
return Result.ok();
}
效果:当这些方法被调用时,AOP 切面会自动拦截并记录日志,无需在业务代码中编写任何日志相关逻辑。
二、小区地图功能实现:后端数据接口与前端集成
小区地图功能需要后端提供小区的地理位置数据(经纬度),前端通过百度地图 API 将小区标记在地图上。
1. 后端接口设计:提供小区地理数据
后端需要实现一个接口,查询所有小区的基本信息(包括经度lng
和纬度lat
),返回给前端用于地图标记。
/**
* 获取小区地图数据
* @return Result 包含小区列表的响应对象
*/
@GetMapping("/getCommunityMap")
public Result getCommunityMap() {
List<Community> data = communityService.list(); // 查询所有小区
if (data == null) {
return Result.error("没有小区数据");
}
return Result.ok().put("data", data); // 返回小区列表
}
返回数据格式:
{
"msg": "操作成功",
"code": 200,
"data": [
{
"communityId": 2,
"communityName": "栖海澐颂",
"termCount": 0,
"seq": 0,
"creater": "",
"createTime": "",
"lng": 116.2524, // 经度
"lat": 40.0961 // 纬度
}
// 更多小区...
]
}
实现说明:
- 接口通过
communityService.list()
查询所有小区数据,包含lng
(经度)和lat
(纬度)字段。 - 若查询结果为空,返回错误提示;否则将数据放入响应对象中返回。
2. 前端集成百度地图
前端使用 Vue 框架结合vue-baidu-map
组件库实现地图展示,步骤如下:
(1)安装依赖
cnpm i --save vue-baidu-map # 安装百度地图Vue组件
(2)配置百度地图 AK
在 Vue 项目入口文件(如main.js
)中配置百度地图开发者密钥(AK):
import Vue from 'vue'
import BaiduMap from 'vue-baidu-map'
Vue.use(BaiduMap, {
// AK需在百度地图开放平台申请
ak: '7eTaUxl9NY8RCMxCPm3oc8m2snTBOgbt'
})
(3)地图组件实现
在 Vue 组件中使用baidu-map
组件加载地图,并根据后端接口返回的小区数据添加标记:
<template>
<baidu-map
class="map"
center="北京" // 初始中心点
zoom="12" // 初始缩放级别
>
<!-- 遍历小区数据,添加标记 -->
<bm-marker
v-for="community in communityList"
:key="community.communityId"
:position="{lng: community.lng, lat: community.lat}" // 经纬度位置
:title="community.communityName" // 鼠标悬停提示
>
<!-- 信息窗口:点击标记显示小区名称 -->
<bm-info-window :show="false">
{{ community.communityName }}
</bm-info-window>
</bm-marker>
</baidu-map>
</template>
<script>
export default {
data() {
return {
communityList: [] // 小区数据列表
}
},
mounted() {
// 调用后端接口获取小区数据
this.axios.get('/sys/community/getCommunityMap')
.then(res => {
if (res.code === 200) {
this.communityList = res.data;
}
})
}
}
</script>
<style>
.map {
width: 100%;
height: 600px;
}
</style>
效果:地图加载后,会根据小区的经纬度在对应位置显示标记,点击标记可查看小区名称,实现小区分布的可视化展示。