日志管理删除操作实现
1.1数据架构分析
当用户执行日志删除操作时,客户端与服务端交互时的基本数据架构,如图所示
1.2删除业务时序分析
客户端提交删除请求,服务端对象的工作时序分析,如图所示:
1.3服务端关键业务及代码实现
1.3.1Dao接口实现
业务描述及设计实现
持久层基于业务层提交的日志记录id,进行日志删除操作。
关键代码及实现
在SysLogDao中添加基于id执行日志删除的方法。代码参考如下:
int deleteObjects(@Param("ids")Integer… ids);
1.3.2 Mapper文件实现
业务描述及设计实现
在SysLogDao接口中对应的映射文件添加用于执行删除业务的delete元素,此元素内部定义具体的SQL实现
关键代码的实际与实现
在SysLogMapper.xml文件添加delete元素,关键代码如下:
<delete id="deleteObjects">
delete from sys_Logs
where id in
<foreach collection="ids" open="(" close=")" separator="," ="id">
#{id}
</foreach>
</delete>
FAQ分析:如上SQL实现可能会存在什么问题?(可靠性问题,性能问题)
从可靠性的角度分析,假如ids的值为null或长度为0时,SQL构建可能会出现语法问题,可参考如下代码进行改进(先对ids的值进行判定):
<delete id="deleteObjects">
delete from sys_logs
<if test="ids!=null and ids.length>0">
where id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</if>
<if test="ids==null or ids.length==0">
where 1=2
</if>
</delete>
从SQL执行性能角度分析,一般在SQL语句中不建议使用in表达式,可以参考如下代码进行实现(重点是forearch中or运算符的应用)
<delete id="deleteObjects">
delete from sys_logs
<choose>
<when test="ids!=null and ids.length>0">
<where>
<foreach collection="ids" item="id" separator="or">
id=#{id}
</foreach>
</where>
</when>
<otherwise>
where 1=2
</otherwise>
</choose>
</delete>
说明:这里的choose元素也为一种选择结构,when元素相当于if,otherwise相当于elese的语法。
1.3. 3 Service接口及实现类
业务描述与设计实现
在日志业务层定义用于执行删除业务的方法,首先通过方法参数接受控制层传递的多个记录的id,并对参数id进行校验,然后基于日志记录id执行删除业务实现。最后返回业务执行结果
关键代码实现
第一步:在SysLogService接口中,添加基于多个id进行日志删除的方法。关键代码如下:
int deleteObjects(Integer… ids) {}
第二步:在SysLogServiceImpl接口中,添加基于多个id进行日志删除的方法。关键代码如下:
@Override
public int deleteObjects(Integer… ids) {
//1.判定参数合法性
if(ids==null||ids.length==0)
throw new IllegalArgumentException("请选择一个");
//2.执行删除操作
int rows;
try{
rows=sysLogDao.deleteObjects(ids);
}catch(Throwable e){
e.printStackTrace();
//发出报警信息(例如给运维人员发短信)
throw new ServiceException("系统故障,正在恢复中...");
}
//4.对结果进行验证
if(rows==0)
throw new ServiceException("记录可能已经不存在");
//5.返回结果
return rows;
}
1.3.4Controller类实现
业务描述与设计实现
第一步:在SysLogController中添加用于执行删除业务的方法。代码如下:
@RequestMapping("doDeleteObjects")
@ResponseBody
public JsonResult doDeleteObjects(Integer… ids){
sysLogService.deleteObjects(ids);
return new JsonResult("delete ok");
}
第二步:启动tomcat进行访问测试,打开浏览器输入如下网址:
http://localhost/log/doDeleteObjects?ids=1,2,3
二、日志管理添加实现
2.1服务端关键业务及代码实现
2.1.1 Dao接口实现
业务描述设计实现
持久层基于业务层的持久化请求,将业务层提交的用户行为日志信息写入到数据库。
关键代码实现
在SysLogDao接口中添加用于实现日志信息持久化的方法代码如下:
int insertObject(SysLog entity);
2.1.2Mapper映射文件
业务描述与设计实现
基于SysLogDao中方法的定义,编写用于数据持久化的sql元素。
关键代码设计与实现
int insertObject(SysLog entity);
2.2.1 Mapper映射文件
业务描述与设计实现
基于SysLogDao方法中的定义,编写用于数据持久化的SQL元素。
关键代码设计与实现
在SysLogMapper.xml中添加insertObject元素,用于像日志表写入用户行为日志。关键代码如下:
<insert id="insertObject">
insert into sys_logs
(username,operation,method,params,time,ip,createdTime)
values
(#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
</insert>
2.1.3 Service接口及实现类
业务描述与设计实现
将日志切面中抓取到的用户行为日志信息,通过业务层对象方法持久化到数据库
关键代码实现
第一步:在SysLogService接口中添加保存日志信息的方法,关键代码如下:
void saveObject(SysLog entity)
第二步:在SysLogServiceImpl类中添加,保存日志的方法实现。关键代码如下:
@Override
public void saveObject(SysLog entity) {
sysLogDao.insertObject(entity);
}
2.1.4 日志切面Aspect实现
业务描述与设计实现
在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业务层对象对日志信息做进一步处理。此部分内容后序结合AOP进行实现(暂时先了解不做具体实现)。
关键代码设计与实现
定义日志切面类对象,通过环绕通知处理日志操作。关键代码如下:
@Aspect
@Component
public class SysLogAspect {
private Logger log=LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private SysLogService sysLogService;
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
public void logPointCut(){}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint //连接点
jointPoint) throws Throwable{
long startTime=System.currentTimeMillis();
//执行目标方法(result为目标方法的执行结果)
Object result=jointPoint.proceed();
long endTime=System.currentTimeMillis();
long totalTime=endTime-startTime;
log.info("方法执行的总时长为:"+totalTime);
saveSysLog(jointPoint,totalTime);
return result;
}
private void saveSysLog(ProceedingJoinPoint point,
long totleTime) throws NoSuchMethodException, SecurityException, JsonProcessingException{
//1.获取日志信息
MethodSignature ms= (MethodSignature)point.getSignature();
Class<?> targetClass=point.getTarget().getClass();
String className=targetClass.getName();
//获取接口声明的方法
String methodName=ms.getMethod().getName();
Class<?>[] parameterTypes=ms.getMethod().getParameterTypes();
//获取目标对象方法(AOP版本不同,可能获取方法对象方式也不同)
Method targetMethod=targetClass.getDeclaredMethod(
methodName,parameterTypes);
//获取用户名,学完shiro再进行自定义实现,没有就先给固定值
String username=ShiroUtils.getPrincipal().getUsername();
//获取方法参数
Object[] paramsObj=point.getArgs();
System.out.println("paramsObj="+paramsObj);
//将参数转换为字符串
String params=new ObjectMapper()
.writeValueAsString(paramsObj);
//2.封装日志信息
SysLog log=new SysLog();
log.setUsername(username);//登陆的用户
//假如目标方法对象上有注解,我们获取注解定义的操作值
RequiredLog requestLog=
targetMethod.getDeclaredAnnotation(RequiredLog.class);
if(requestLog!=null){
log.setOperation(requestLog.value());
}
log.setMethod(className+"."+methodName);//className.methodName()
log.setParams(params);//method params
log.setIp(IPUtils.getIpAddr());//ip 地址
log.setTime(totleTime);//
log.setCreateDate(new Date());
//3.保存日志信息
sysLogService.saveObject(log);
}
}
方法中用到的ip地址获取需要提供一个如下的工具类:(不用自己实现,直接用)
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
public static String getIpAddr() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
return ip;
}
}
原理分析,如图所示:
三、总结
日志管理整体业务分析与实现。
(1)分层架构(应用层MVC:基于spring的mvc模块)。
(2)API架构(SysLogDao,SysLogService,SysLogController)。
(3)业务架构(查询,删除,添加用户行为日志)。
(4)数据架构(SysLog,PageObject,JsonResult,…)。
日志管理持久层映射文件中SQL元素的定义及编写。
(1)定义在映射文件”mapper/sys/SysLogMapper.xml”(必须在加载范围内)。
(2)每个SQL元素必须提供一个唯一ID,对于select必须指定结果映射(resultType)。
(3)系统底层运行时会将每个SQL元素的对象封装一个值对象(MappedStatement)。
日志管理模块数据查询操作中的数据封装。
(1)数据层(数据逻辑)的SysLog对象应用(一行记录一个log对象)。
(2)业务层(业务逻辑)PageObject对象应用(封装每页记录以及对应的分页信息)。
(3)控制层(控制逻辑)的JsonResult对象应用(对业务数据添加状态信息)。
日志管理控制层请求数据映射,响应数据的封装及转换(转换为json 串)。
(1)请求路径映射,请求方式映射(GET,POST),请求参数映射(直接量,POJO)。
(2)响应数据两种(页面,JSON串)。
日志管理模块异常处理如何实现的。
(1)请求处理层(控制层)定义统一(全局)异常处理类。
(2)使用注解@ControllerAdvice描述类,使用@ExceptionHandler描述方法.
(3)异常处理规则:能处理则处理,不能处理则抛出。
3.2 FAQ分析
用户行为日志表中都有哪些字段?(面试时有时会问)
用户行为日志是如何实现分页查询的?---- limit
用户行为数据的封装过程?---- 数据层,业务层,控制层
项目中的异常是如何处理的?
页面中数据乱码,如何解决? ---- 数据来源,请求数据,响应数据
说说的日志删除业务是如何实现?
Spring MVC 响应数据处理?---- view,json
项目你常用的JS函数说几个?---- data,prop,ajax,each,…
MyBatis中的@Params注解的作用? ---- 为参数变量指定其其别名
Jquery中data函数用于做什么?
---- 可以借助data函数将数据绑定到指定对象,语法为data(key[,value]),key和value为自己业务中的任意数据,假如只有key表示取值。
Jquery中的prop函数用于获取html标签对象中”标准属性”的值或为属性赋值,其语法为prop(propertyName[,propertyValue]),假如只有属性名则为获取属性值。
Jquery中attr函数为用户获取html标签中任意属性值或为属性赋值的一个方法,其语法为attr(propertyName[,propertyValue]),假如只有属性名则为获取属性值。
日志写操作事务的传播特性如何配置?(每次开启新事务)?
日志写操作为什么应该是异步的?
Spring 中的异步操作如何实现?
Spring 中的@Async如何应用?
项目中的BUG分析及解决套路?(排除法,打桩,断点)