智慧社区(十)——声明式日志记录与小区地图功能实现
在社区管理系统中,日志记录和数据可视化是两个核心需求。日志记录用于追踪系统操作,保障安全性;地图展示则能直观呈现小区分布。本文将详细介绍如何通过自定义注解 + 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);@Autowiredprivate 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>
效果:地图加载后,会根据小区的经纬度在对应位置显示标记,点击标记可查看小区名称,实现小区分布的可视化展示。