Spring AOP 完成日志记录
1、技术目标
掌握Spring AOP基本用法
使用Spring AOP完成日志记录功能
提示:本文所用项目为"影片管理",参看
http://hotstrong.iteye.com/blog/1160153
本文基于"影片管理"项目进行了日志记录功能扩充
注意:本文所实现的项目(MyEclipse工程)已提供下载,数据库
脚本可参看《MyBatis 1章 入门(使用MyBatis完成CRUD)》
2、什么是AOP
AOP是Aspect Oriented Programming的缩写,意思是面向方面编程,AOP实际是GoF设计模式的延续
注意:关于AOP的详细介绍不是本文重点
3、关于Spring AOP的一些术语
切面(Aspect):在Spring AOP中,切面可以使用通用类或者在普通类中以@Aspect 注解(@AspectJ风格)来实现
连接点(Joinpoint):在Spring AOP中一个连接点代表一个方法的执行
通知(Advice):在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括"around"、"before”和"after"等通知。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链
切入点(Pointcut):定义出一个或一组方法,当执行这些方法时可产生通知,Spring缺省使用AspectJ切入点语法。
4、通知类型
前置通知(@Before):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)
返回后通知(@AfterReturning):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
后通知(@After):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
环绕通知(@Around):包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型,环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行
5、@AspectJ风格的AOP配置
Spring AOP配置有两种风格:
XML风格 = 采用声明形式实现Spring AOP
AspectJ风格 = 采用注解形式实现Spring AOP
注意:本文采用AspectJ风格
6、使用准备
闲话少说,下面开始日志记录的准备工作
6.1)创建日志记录表(MySQL),
CREATE TABLE `t_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`userid` bigint(20) unsigned NOT NULL,
`createdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建日期',
`content` varchar(8000) NOT NULL DEFAULT '' COMMENT '日志内容',
`operation` varchar(250) NOT NULL DEFAULT '' COMMENT '用户所做的操作',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
6.2)在经过了Spring Security的权限验证后,可以从Security中获取到
登录管理员的帐号,而日志记录表t_log中存储的是管理员id,所以需要通
过管理员的帐号查询出管理员id,创建管理员POJO、Mapper、Service,
代码及配置如下:
管理员POJO类:
package com.xxx.pojo;
public class Admin extends BaseDomain {
private String nickname;//管理员帐号
private String passwd;//管理员密码
private String phoneno;//联系电话
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public String getPhoneno() {
return phoneno;
}
public void setPhoneno(String phoneno) {
this.phoneno = phoneno;
}
}
管理员Mapper接口与XML配置文件:
package com.xxx.dao;
import com.xxx.pojo.Admin;
/**
* 管理员Mapper接口
*/
public interface AdminMapper {
/**
* 获取指定帐号名的管理员
*/
public Admin findAdminByNickname(String userName);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.dao.AdminMapper">
<!-- 通过账号名称查询管理员 -->
<select id="findAdminByNickname" parameterType="string" resultType="Admin">
select * from t_admin where nickname=#{userName}
</select>
</mapper>
管理员Service接口与实现类:
package com.xxx.service;
import com.xxx.pojo.Admin;
/**
* 管理员信息业务逻辑接口
*/
public interface AdminService {
/**
* 获取指定帐号名的管理员
*/
public Admin findAdminByNickname(String userName);
}
package com.xxx.service;
import org.springframework.beans.factory.annotation.Autowired;
import com.xxx.dao.AdminMapper;
import com.xxx.pojo.Admin;
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminMapper adminMapper;//Mapper接口
public Admin findAdminByNickname(String userName) {
return adminMapper.findAdminByNickname(userName);
}
}
6.3)创建日志记录POJO、Mapper、Service,代码及配置如下:
日志记录POJO类:
package com.xxx.pojo;
import java.io.Serializable;
import java.util.Date;
/**
* 日志记录POJO
*/
public class Log extends BaseDomain implements Serializable{
private static final long serialVersionUID = 1024792477652984770L;
private Long userid;//管理员id
private Date createdate;//日期
private String content;//日志内容
private String operation;//操作(主要是"添加"、"修改"、"删除")
//getter、setter,此处省略N字(你懂的)
}
日志记录Mapper接口与XML配置文件:
package com.xxx.dao;
import com.xxx.pojo.Log;
/**
* 日志记录Mapper
*/
public interface LogMapper {
public void insert(Log log);//添加日志记录
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.dao.LogMapper">
<!-- 添加日志记录 -->
<insert id="insert" parameterType="Log">
INSERT INTO t_log(userid,createdate,operation,content)
VALUES(#{userid},NOW(),#{operation},#{content});
</insert>
</mapper>
日志记录Service接口与实现类:
package com.xxx.service;
import org.springframework.transaction.annotation.Transactional;
import com.xxx.pojo.Log;
/**
* 日志记录业务逻辑接口
*/
public interface LogService {
/**
* 日志记录
* @param log
*/
@Transactional
public void log(Log log);
/**
* 获取登录管理员ID
*/
public Long loginUserId();
}
package com.xxx.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import com.xxx.dao.LogMapper;
import com.xxx.pojo.Admin;
import com.xxx.pojo.Log;
/**
* 日志记录业务逻辑接口实现类
* @author HotStrong
*/
public class LogServiceImpl implements LogService{
@Autowired
private AdminService adminService;
@Autowired
private LogMapper logMapper;
public void log(Log log) {
logMapper.insert(log);
}
/**
* 获取登录管理员ID
*
* @return
*/
public Long loginUserId() {
if(SecurityContextHolder.getContext() == null){
return null;
}
if(SecurityContextHolder.getContext().getAuthentication() == null){
return null;
}
UserDetails userDetails = (UserDetails) SecurityContextHolder
.getContext().getAuthentication().getPrincipal();
if(userDetails == null){
return null;
}
//获取登录管理员帐号名
String userName = userDetails.getUsername();
if(userName == null || userName.equals("")){
return null;
}
// 根据管理员帐号名获取帐号ID
Admin admin = this.adminService.findAdminByNickname(userName);
if(admin == null){
return null;
}
return admin.getId();
}
}
7、在MyBatis配置文件mybatis-config.xml中配置POJO,如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- changes from the defaults -->
<setting name="lazyLoadingEnabled" value="false" />
</settings>
<typeAliases>
<typeAlias alias="Film" type="com.xxx.pojo.Film"/>
<typeAlias alias="Admin" type="com.xxx.pojo.Admin"/>
<typeAlias alias="Log" type="com.xxx.pojo.Log"/>
</typeAliases>
</configuration>
8、创建aop包,在aop包下创建切面类LogAspect
package com.xxx.aop;
import java.lang.reflect.Method;
import java.util.Date;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
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 com.xxx.pojo.Film;
import com.xxx.pojo.Log;
import com.xxx.service.FilmService;
import com.xxx.service.LogService;
/**
* 日志记录,添加、删除、修改方法AOP
* @author HotStrong
*
*/
@Aspect
public class LogAspect {
@Autowired
private LogService logService;//日志记录Service
@Autowired
private FilmService filmService;//影片Service
/**
* 添加业务逻辑方法切入点
*/
@Pointcut("execution(* com.xxx.service.*.insert*(..))")
public void insertServiceCall() { }
/**
* 修改业务逻辑方法切入点
*/
@Pointcut("execution(* com.xxx.service.*.update*(..))")
public void updateServiceCall() { }
/**
* 删除影片业务逻辑方法切入点
*/
@Pointcut("execution(* com.xxx.service.FilmService.deleteFilm(..))")
public void deleteFilmCall() { }
/**
* 管理员添加操作日志(后置通知)
* @param joinPoint
* @param rtv
* @throws Throwable
*/
@AfterReturning(value="insertServiceCall()", argNames="rtv", returning="rtv")
public void insertServiceCallCalls(JoinPoint joinPoint, Object rtv) throws Throwable{
//获取登录管理员id
Long adminUserId = logService.loginUserId();
if(adminUserId == null){//没有管理员登录
return;
}
//判断参数
if(joinPoint.getArgs() == null){//没有参数
return;
}
//获取方法名
String methodName = joinPoint.getSignature().getName();
//获取操作内容
String opContent = adminOptionContent(joinPoint.getArgs(), methodName);
//创建日志对象
Log log = new Log();
log.setUserid(logService.loginUserId());//设置管理员id
log.setCreatedate(new Date());//操作时间
log.setContent(opContent);//操作内容
log.setOperation("添加");//操作
logService.log(log);//添加日志
}
/**
* 管理员修改操作日志(后置通知)
* @param joinPoint
* @param rtv
* @throws Throwable
*/
@AfterReturning(value="updateServiceCall()", argNames="rtv", returning="rtv")
public void updateServiceCallCalls(JoinPoint joinPoint, Object rtv) throws Throwable{
//获取登录管理员id
Long adminUserId = logService.loginUserId();
if(adminUserId == null){//没有管理员登录
return;
}
//判断参数
if(joinPoint.getArgs() == null){//没有参数
return;
}
//获取方法名
String methodName = joinPoint.getSignature().getName();
//获取操作内容
String opContent = adminOptionContent(joinPoint.getArgs(), methodName);
//创建日志对象
Log log = new Log();
log.setUserid(logService.loginUserId());//设置管理员id
log.setCreatedate(new Date());//操作时间
log.setContent(opContent);//操作内容
log.setOperation("修改");//操作
logService.log(log);//添加日志
}
/**
* 管理员删除影片操作(环绕通知),使用环绕通知的目的是
* 在影片被删除前可以先查询出影片信息用于日志记录
* @param joinPoint
* @param rtv
* @throws Throwable
*/
@Around(value="deleteFilmCall()", argNames="rtv")
public Object deleteFilmCallCalls(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
//环绕通知处理方法
try {
//获取方法参数(被删除的影片id)
Integer id = (Integer)pjp.getArgs()[0];
Film obj = null;//影片对象
if(id != null){
//删除前先查询出影片对象
obj = filmService.getFilmById(id);
}
//执行删除影片操作
result = pjp.proceed();
if(obj != null){
//创建日志对象
Log log = new Log();
log.setUserid(logService.loginUserId());//用户编号
log.setCreatedate(new Date());//操作时间
StringBuffer msg = new StringBuffer("影片名 : ");
msg.append(obj.getFname());
log.setContent(msg.toString());//操作内容
log.setOperation("删除");//操作
logService.log(log);//添加日志
}
}
catch(Exception ex) {
ex.printStackTrace();
}
return result;
}
/**
* 使用Java反射来获取被拦截方法(insert、update)的参数值,
* 将参数值拼接为操作内容
*/
public String adminOptionContent(Object[] args, String mName) throws Exception{
if (args == null) {
return null;
}
StringBuffer rs = new StringBuffer();
rs.append(mName);
String className = null;
int index = 1;
// 遍历参数对象
for (Object info : args) {
//获取对象类型
className = info.getClass().getName();
className = className.substring(className.lastIndexOf(".") + 1);
rs.append("[参数" + index + ",类型:" + className + ",值:");
// 获取对象的所有方法
Method[] methods = info.getClass().getDeclaredMethods();
// 遍历方法,判断get方法
for (Method method : methods) {
String methodName = method.getName();
// 判断是不是get方法
if (methodName.indexOf("get") == -1) {// 不是get方法
continue;// 不处理
}
Object rsValue = null;
try {
// 调用get方法,获取返回值
rsValue = method.invoke(info);
if (rsValue == null) {//没有返回值
continue;
}
} catch (Exception e) {
continue;
}
//将值加入内容中
rs.append("(" + methodName + " : " + rsValue + ")");
}
rs.append("]");
index++;
}
return rs.toString();
}
}
9、对管理员登录操作进行日志记录
还记得《使用Spring Security实现权限管理》一文中第7步提到的两个类吗?其中LoginSuccessHandler类中可以记录管理员的登录操作,代码如下:
package com.xxx.security;
import java.io.IOException;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import com.xxx.pojo.Log;
import com.xxx.service.LogService;
/**
* 处理管理登录日志
*
*/
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
@Autowired
private LogService logService;//日志记录Service
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException,
ServletException {
UserDetails userDetails = (UserDetails)authentication.getPrincipal();
//创建日志对象
Log log = new Log();
log.setUserid(logService.loginUserId());//设置管理员id
log.setCreatedate(new Date());//操作时间
log.setContent("管理员 " + userDetails.getUsername());//操作内容
log.setOperation("登录");//操作
logService.log(log);//添加日志
super.onAuthenticationSuccess(request, response, authentication);
}
}
10、在applicationContext-services.xml中加入新的配置
applicationContext-services.xml中加入了Aspectj配置以及新增的管理员Service、日志记录Service配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- 加入Aspectj配置 -->
<aop:aspectj-autoproxy />
<bean id="logAspect" class="com.xxx.aop.LogAspect" />
<!-- 电影业务逻辑对象 -->
<bean id="filmService" class="com.xxx.service.FilmServiceImpl"></bean>
<!-- 管理员业务逻辑对象 -->
<bean id="adminService" class="com.xxx.service.AdminServiceImpl"></bean>
<!-- 日志记录业务逻辑对象 -->
<bean id="logService" class="com.xxx.service.LogServiceImpl"></bean>
</beans>
11、配置成功后分别进行登录、添加、修改、删除影片操作,日志记录表的内容如下:
关于mybatis、Spring的security权限管理的博客地址:http://hotstrong.iteye.com/blog/1330046