基于Springboot的Spring AOP学习记录
前段时间各种面试,aop问到就蒙逼,所以结合新学的springboot重新理一下这玩意儿,很重要啊
一、AOP概述
AOP(面对切面编程)是对OOP(面向对象编程)的补充,总体来说,编程范式包含:面向过程编程、面向对象编程、函数式编程、事件驱动编程、面向切面编程。
AOP的出现主要是为了解决如下的几个问题:
1.代码重复性的问题
2.关注点的分离(包含了水平分离:展示层->服务层->持久层;垂直分离:模块划分(订单、库存等);切面分离:分离功能性需求与非功能性需求)
AOP使用优势:
1.集中处理某一关注点/横切逻辑
2.可以方便的添加/删除关注点
3.侵入性少,增强代码的可读性和可维护性
AOP的应用场景:
权限控制、缓存控制、事务控制、审计日志、性能监控、分布式追踪、异常处理
所以这么优秀的东西绝不仅仅局限于Java,AOP是支持多语言开发的。
二、SpringAOP的使用
Spring AOP的使用方式包含XML配置和注解方式(比较方便),下面看一下基于注解方式的AOP。
AOP的注解主要包括@Aspect、@Pointcut、Advice三种。在详细记录前先给出一个AOP的简单使用代码(流程:引入依赖–>定义切面类–>在切面类中定义Advice以及前后逻辑),如果要获取方法的一些属性(比如方法名,返回值、参数等等),除了环绕通知需要传入ProceedingJoinPoint对象,其他的Advice则是需要传入JoinPoint对象,两类不同!
<!-- 添加AOP的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
package com.imooc.aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect//表明它是一个切面类
@Component//交给Spring管理
public class HttpAspect {
// //定义拦截GirlController中所有public方法
// @Before("execution(public * com.imooc.controller.GirlController.*(..))")
// public void log() {
// System.out.println("******拦截前的逻辑******");
// }
//
// @After("execution(public * com.imooc.controller.GirlController.*(..))")
// public void doAfter() {
// System.out.println("******拦截后的逻辑******");
// }
/*
* 上面拦截的方式比较繁琐,因为Before和After的匹配规则有重复代码
*
* 可以先定义一个Pointcut,然后直接拦截这个方法即可
*
*/
//这里就定义了一个总的匹配规则,以后拦截的时候直接拦截log()方法即可,无须去重复写execution表达式
@Pointcut("execution(public * com.imooc.controller.GirlController.*(..))")
public void log() {
}
@Before("log()")
public void doBefore() {
System.out.println("******拦截前的逻辑******");
}
@After("log()")
public void doAfter() {
System.out.println("******拦截后的逻辑******");
}
}
aaa@qq.com
主要用来标注Java类,表明它是一个切面配置的类,通常下面也会加上@Component注解来表明它由Spring管理
aaa@qq.com
主要有pointCutExpression(切面表达式)来表达,用来描述你要在哪些类的哪些方法上注入代码。其中切面表达式包含了designators(指示器,主要描述通过哪些方式去匹配Java类的哪些方法:如execution()等)和wildcards(通配符:如*)以及operators(运算符:如&&、||、!),具体如下所示
designators指示器
表示想要通过什么样的方式匹配想要的方法,具体组成如下图,重点是execution()
1.匹配包/类型within()
//匹配ProductService类种的所有方法
@Poincut("within(com.hhu.service.ProductService)")
public void matchType(){
...
}
//匹配com.hhu包及其子包下所有类的方法
@Pointcut("within(com.hhu..*)")
public void matchPackage(){
...
}
2.匹配对象(主要有this和target以及bean)
//匹配AOP对象的目标对象为指定类型的方法,即DemoDao的aop的代理对象
@Pointcut("this(com.hhu.DemaoDao)")
public void thisDemo() {
...
}
//匹配实现IDao接口的目标对象(而不是aop代理后的对象,这里即DemoDao的方法)
@Pointcut("target(com.hhu.Idao)")
public void targetDemo() {
...
}
//匹配所有以Service结尾的bean中的方法
@Pointcut("bean(*Service)")
public void beanDemo() {
...
}
3.参数匹配(主要有execution()和args()方法)
//过滤出第一个参数是long类型的并且在com.hhu.service包下的方法,如果是第一个参数是Long,第二个参数是String则可以写成args(Long,String),如果匹配第一个为Long,其它任意的话则可以写成args(Long..)
@Pointcut("args(Long) && within(com.hhu.service.*)")
public void matchArgs() {
...
}
@Before("mathArgs()")
public void befor() {
System.out.println("");
System.out.println("###before");
}
4.匹配注解
//匹配方法标注有AdminOnly注解的方法
@Pointcut("@annotation(com.hhu.demo.security.AdminOnly)")
public void annoDemo() {
...
}
//匹配标注有Beta的类下的方法,要求annotation的RetentionPplicy级别为CLASS
@Pointcut("@within(com.google.common.annotations.Beta)")
public void annoWithinDemo() {
...
}
//匹配标注有Repository类下的方法,要求的annotation的RetentionPolicy级别为RUNTIME
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void annoTargetDemo() {
...
}
//匹配传入的参数类型标注有Repository注解的方法
@Pointcut("@args(org.springframework.stereotype.Repository)")
public void annoArgsDemo() {
...
}
5.execution()
标准的execution表达式如下,共有5个参数:
execution(
//权限修饰符(如public、private)
[modifier-pattern],
//返回值(如*表示任意返回值,void表示无返回值)
ret-type-pattern,
//包名(如com.hhu.service.*Service.*(..)表示对应包com.hhu.service中以Service结尾的类中的任意方法(该方法可以带任意参数),如果匹配无参的则可以写成(),如果像拦截产生异常的方法,则可以写成(..) throws+具体异常 )
[declaring-type-pattern],
name-pattern(或者是param-pattern),
[throws-pattern]
)
其中带有方括号的参数表示可以缺省。比如
@Pointcut("execution(* *..find*(Long)) && within(com.imooc..*) ")
wildcard主要包括常用的三种:
*表示匹配任意数量的字符
+表示匹配制定类及其子类
..表示一般用于匹配任意数的子包或参数。
operators主要包括如下的三种:
&&表示与操作
||表示或操作
!表示非操作
3.Advice注解
表示代码织入的时机,如执行之前、执行之后等等,主要有5种,通常是进行注解切面注解后会紧接着写Advice,格式为Advice(“切面拦截方法”)。比如下面的
//切面注解
@Pointcut("@annotation(com.imooc.anno.AdminOnly) && within(com.imooc..*)")
public void matchAnno(){}
//Advice注解
@After("matchAnno()")
public void after() {
System.out.println("###after");
}
- @Before,前置通知
- @After,后置通知,不管代码是成功还是抛出异常,都会织入
- @AfterReturning,返回通知,当且仅当方法成功执行
- @AfterThrowing,异常通知,当且仅当方法抛出异常
- @Around,环绕通知,基本包含了上述所有的植入位置
获取参数的话一般跟Before的Advice结合在一起的,代码如下
@Before("matchLongArg() && args(productId)")
//这里是因为知道参数的类型是Long
public void before(Long productId) {
System.out.println("###before,get args:" + productId);
}
其中@AfterReturning注解可以获取方法的返回值,比如下面的
//切面注解
@Pointcut("@annotation(com.imooc.anno.AdminOnly) && within(com.imooc..*)")
public void matchAnno(){}
//Advice注解,returning的值result代表的就是返回值,形参Object类表示
@AfterReturning(value="matchAnno()",returning="result")
public void after(java.lang.Object result) {
System.out.println("###after");
}
@Around注解比较强大,一般有它的存在就可以不用Before或者After这类注解了,如下
//由于这里可能有返回值所以必须有Object,而且必须获取Proceeding的上下文才能让方法得以继续,所以会有此形参
@Around("matchAnno()")
public java.lang.Object after(ProceedingJoinPoint joinPoint) {
System.out.println("###before");
//定义返回值
java.lang.Object result = null;
try {
//获取返回值
result = joinPoint.proceed(joinPoint.getArgs());
System.out.println("###after returning");
} catch(Throwable e) {
System.out.println("###ex");
e.printStackTrace();
} finally {
System.out.println("###finally");
}
return result;
}
三、Spring AOP的原理
AOP的设计主要用到的设计模式包含了代理模式、责任链模式;AOP的实现方式包含了两种:基于JDK实现、基于CGlib实现。
织入时机:主要有以下的3种
1. 编译期(如AspectJ)
2. 类加载时(如AspectJ5以上版本)
3. 运行时(如Spring AOP)
Spring AOP属于运行时织入,这种织入方式是基于代理实现的,这样代理才能在实际方法执行前后进行一些逻辑处理,可以有两种:从静态代理到动态代理(动态代理包含基于接口代理与基于继承代理)。
1.代理模式(简单AOP的模拟)–静态代理
//1.首先定义一个接口
package com.imooc.pattern;
public interface Subject {
void request();
}
//2.让实际对象和代理对象都实现这个接口
//2.1实际对象
package com.imooc.pattern;
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("real subject execute request");
}
}
//2.2织入逻辑对象,持有实际对象
package com.imooc.pattern;
public class Proxy implements Subject{
private RealSubject realSubject;
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
System.out.println("before");
try {
realSubject.request();
} catch (Exception e) {
System.out.println("ex:" + e.getMessage());
throw e;
} finally {
System.out.println("after");
}
}
}
//3.客户端调用
package com.imooc.pattern;
public class Client {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
Proxy proxy = new Proxy(realSubject);
realSubject.request();
proxy.request();
}
}
静态代理方便易懂,但是当目标类中需要逻辑的处理方法很多,由于逻辑处理的前后大部分都是相差不大的,那么此时就需要写很多重复的代码,从而向动态代理过渡,动态代理分为基于接口的动态代理(典型代表是JDK代理)和基于继承的动态代理(典型代表是CGlib代理)。
2.JDK动态代理(只能基于接口)
jdk动态着重理解动态,可以翻一下其源码应该就知道了。主要有两个步骤:
通过java.lang.reflect.Proxy类来动态生成代理类,其次这个代理类需要实现的织入逻辑必须实现InvocationHandler接口(需要重写其中的invoke()方法),代码如下
//1.Subject接口同上
//2.jdk动态代理
package com.imooc.dynamic;
import com.imooc.pattern.RealSubject;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* 就是需要织入的逻辑代码,相当与AOP中aspect
*/
//必须实现InvocationHandler接口
public class JdkProxySubject implements InvocationHandler{
//jdk动态代理仍然需要引用目标类对象
private RealSubject realSubject;
public JdkProxySubject(RealSubject realSubject) {
this.realSubject = realSubject;
}
//重写invoke()
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//执行动态代理逻辑
System.out.println("before");
//初始化返回值
Object result = null;
try {
//通过反射执行目标类的所有方法
result = method.invoke(realSubject,args);
} catch (Exception e) {
System.out.println("ex:" + e.getMessage());
//异常一定要抛出,因为上面的catch只是指针的是代理类,这里的throw是指目标类的异常抛出
throw e;
}finally {
System.out.println("after");
}
return result;
}
}
//3.客户端调用
package com.imooc.pattern;
import com.imooc.dynamic.JdkProxySubject;
import java.lang.reflect.Proxy;
public class Client {
public static void main(String[] args) {
//通过java.lang.reflect.Proxy类来动态生成代理类,注意参数
Subject subject = (Subject)Proxy.newProxyInstance(Client.class.getClassLoader(),
new Class[]{Subject.class}, new JdkProxySubject(new RealSubject()));
subject.request();
//这里在接口里增加一个hello(),如果用静态代理的话就必须在代理类中重写该方法,而jdk代理则不需要,直接调用即可
subject.hello();
}
}
3.Cglib动态代理(通过继承实现代理类)
主要两点:
1.Cglib通过继承的方式实现代理类
2.Cglib通过Callback方式来织入代码
步骤两不:通过org.springframework.cglib.proxy.Enhancer来创建代理对象,织入的逻辑代码需要实现org.springframework.cglib.proxy.MethodInterceptor接口(重写intercept()方法),代码如下
//1.Subject接口同上
//2.织入的逻辑代码
package com.imooc.cglib;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class DemoMethodInterceptor implements MethodInterceptor{
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before in cglib");
//定义返回的结果
Object result = null;
try {
//获取结果
result = methodProxy.invokeSuper(o,objects);
}catch (Exception e) {
System.out.println("ex:" + e.getMessage());
throw e;
}finally {
System.out.println("after in cglib");
}
return result;
}
}
//3.客户端调用
package com.imooc.pattern;
import com.imooc.cglib.DemoMethodInterceptor;
import org.springframework.cglib.proxy.Enhancer;
public class Client {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
//Superclass就是目标类
enhancer.setSuperclass(RealSubject.class);
//Callback就是要织入的代码
enhancer.setCallback(new DemoMethodInterceptor());
//通过Enhancer创建代理类
Subject subject = (Subject)enhancer.create();
subject.request();
subject.hello();
}
}
cglib和jdk的主要区别在于:
1.JDK动态代理只能针对有接口的类的接口方法进行动态代理
2.Cglib基于继承来是实现代理,无法对static、final类以及private、static方法进行代理
4.Spring AOP代理方式的选择
主要依据以下的原则:
1.如果目标对象实现了接口,则默认使用JDK动态代理
2.如果目标对象没有实现接口,则采用Cglib动态代理
3.如果目标对象实现了接口,且强制cglib代理,则使用cglib动态代理
关于第3强制使用cglib代理设置如下:
@SpringBootApplication
//强制使用cglib代理
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ExecutionDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ExecutionDemoApplication.class, args);
}
}
5.多个AOP的叠加–责任链模式
责任链的原理图如下:
代码演示如下:
//1.定义Handler
package com.imooc.chain;
public abstract class Handler {
protected Handler successor;
public Handler getSuccessor() {
return successor;
}
public void setSuccessor(Handler successor) {
this.successor = successor;
}
//对外暴露
public void execute() {
//先执行自己的handlerProcess
handlerProcess();
//递归执行handler
if(successor!=null) {
successor.execute();
}
}
protected abstract void handlerProcess();
}
//2.客户端多AOP的作用
package com.imooc.chain;
public class Client {
static class HandlerA extends Handler {
@Override
protected void handlerProcess() {
System.out.println("handler by a");
}
}
static class HandlerB extends Handler {
@Override
protected void handlerProcess() {
System.out.println("handler by b");
}
}
static class HandlerC extends Handler {
@Override
protected void handlerProcess() {
System.out.println("handler by c");
}
}
public static void main(String[] args) {
Handler handlerA = new HandlerA();
Handler handlerB = new HandlerB();
Handler handlerC = new HandlerC();
//设置连接关系
handlerA.setSuccessor(handlerB);
handlerB.setSuccessor(handlerC);
handlerA.execute();
}
}
上述代码之间的多个AOP需要进行设置之间的连接,下面将这个步骤封装,无需设置他们之间的连接,更具灵活性,AOP内部也是这种原理实现。
//1.设置Handler
package com.imooc.chain;
public abstract class ChainHandler {
public void execute(Chain chain) {
handlerProcess();
chain.proceed();
}
protected abstract void handlerProcess();
}
//2.封装多个AOP
package com.imooc.chain;
import java.util.List;
public class Chain {
private List<ChainHandler> handlers;
private int index = 0;
public Chain(List<ChainHandler> handlers) {
this.handlers = handlers;
}
public void proceed() {
if(index>=handlers.size()) {
return ;
}
handlers.get(index++).execute(this);
}
}
//3.客户端调用
package com.imooc.chain;
import java.util.Arrays;
import java.util.List;
public class Client2 {
static class ChainHandlerA extends ChainHandler {
@Override
protected void handlerProcess() {
System.out.println("handle by chain a");
}
}
static class ChainHandlerB extends ChainHandler {
@Override
protected void handlerProcess() {
System.out.println("handle by chain b");
}
}
static class ChainHandlerC extends ChainHandler {
@Override
protected void handlerProcess() {
System.out.println("handle by chain c");
}
}
public static void main(String[] args) {
//利用封装的方式将chain的方式声明出来,并且每个chain之间是独立存在的
//Spring AOP内部也是使用的这种方式来实现的,原理类似,从-1开始,这里从0开始
List<ChainHandler> handlers = Arrays.asList(
new ChainHandlerA(),
new ChainHandlerB(),
new ChainHandlerC()
);
Chain chain = new Chain(handlers);
chain.proceed();
}
}
四、AOP应用控制的三大注解
AOP在整个Spring框架中占非常重要的作用,这里主要结合三个注解理解它的控制应用:@Transactional、@PreAuthorize、@Cacheable.
1. Spring利用@Transactional注解进行事务控制
关于事务控制已经是老生常谈了,这里简单的利用Springboot和jpa两者结合给出比较简单的代码来演示(如对springboot不了解的话可以参看springboot的学习记录)
//插入用户同时记录一个日志
@Transactional
public void addUser(String name){
OperationLog log = new OperationLog();
log.setContent("create user:"+name);
operationLogDao.save(log);
User user = new User();
user.setName(name);
userDao.save(user);
}
即插入用户时同时插入记录,只要有一个操作不成功,那么整个事物进行回滚操作。
2. SpringSecurity利用@preAuthorize进行安全控制
SpringSecurity安全校验同样使用了AOP,首先使用MethodSecurityInterceptor进行拦截,然后这个Interceptor又是调用的Voter,这个Voter是处理InvocationAuthorizationAdvice这个类型的,再然后这个Voter又是调用的ExpressionBasePreInvocationAdvice进行判断,工作图如下
这里以一个简单的用户登陆为例,代码如下
//1.配置文件
package com.imooc.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Created by cat on 2017-03-12.
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/index").permitAll()//这是不需要校验的,其他的都需要校验
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
//默认在内存中创建两个用户,一个是USER角色的用户demo,另一个是ADMIN角色的用户admin
.withUser("demo").password("demo").roles("USER")
.and()
//
.withUser("admin").password("admin").roles("ADMIN");
}
}
//2.控制器
package com.imooc.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by cat on 2017-03-12.
*/
@RestController
public class DemoController {
//这个前面已经设置过了,是不需要拦截的
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/common")
public String commonAccess(){
return "only login can view";
}
@RequestMapping("/admin")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String admin(){
return "only admin can access";
}
}
这里index页面是每个人都可以访问的,common页面允许demo用户访问,admin页面只允许admin用户访问。
3. SpringCache如何利用@Cacheable进行缓存控制
首先前面有一个AnnotationCacheAspect的定义,然后通过CacheInterceptor来执行逻辑,最后这个Interceptor又是托给CacheAspectSupport来进行缓存控制。时序图如下
代码如下
//1.缓存配置文件
package com.imooc.service;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* Created by cat on 2017-03-12.
*/
@Component
public class MenuService {
//@Cacheable是用来声明方法返回值是可缓存的。将结果存储到缓存中以便后续使用相同参数调用时不需执行实际的方法。直接从缓存中取值。最简单的格式只需要需要制定缓存名称即可使用。
@Cacheable(cacheNames = {"menu"})
public List<String> getMenuList(){
System.out.println("");
System.out.println("mock:get from db");
return Arrays.asList("article","comment","admin");
}
}
//2.主文件
package com.imooc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching//这个注解不能漏
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class, args);
}
}
//3.测试文件
package com.hhu;
import com.hhu.service.MenuService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class CacheDemoApplicationTests {
@Autowired
MenuService menuService;
@Test
public void testCache() {
//首次调用getMenuList,缓存中没有东西,所以完整执行了 System.out.println("call:"+menuService.getMenuList());
//第二次调用时,由于注解了缓存,所以并没有直接调用方法,而是直接去缓存中获取了第一次调用的返回值List,并没有执行之前的打印文本 System.out.println("call:"+menuService.getMenuList());
}
}
//4.pom文件
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hhu.config</groupId>
<artifactId>SpringAOPTest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringAOPTest</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
运行结果为
mock:get from db
call:[article, comment, admin]
call:[article, comment, admin]
第一次出现了mock标志,第二次没有出现mock标志。
五、综合案例
以商家产品管理系统为例,需要实现以下功能(非功能性需求)
1.记录产品修改的操作记录
2.记录什么人在什么时间修改了哪些产品的哪些字段以及修改的具体值
思路:
1.利用qspect拦截增删改的方法
2.利用反射获取对象的新旧值
3.利用@Around注解的Advice去记录操作记录
环境:Eclipse + jdk1.8 数据库:MySQL+MongoDB
主要代码片如下:
1.maven的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hhu</groupId>
<artifactId>datalog</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>datalog</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mongodb主要用来存储用户的操作记录 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- 主要用于获取对象bean描述时候用到的 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.28</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.切面类
package com.hhu.datalog;
import java.util.Date;
import java.util.List;
import org.apache.commons.beanutils.PropertyUtils;
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.assertj.core.internal.Diff;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.hhu.domain.Action;
import com.hhu.domain.ActionType;
import com.hhu.domain.ChangeItem;
import com.hhu.util.DiffUtil;
@Aspect//标志他是个切面的类
@Component
public class DatalogAspect {
private static final Logger logger = LoggerFactory.getLogger(DatalogAspect.class);
@Autowired
ActionDao actionDao;
//这里定义的save方法实际上就是Point拦截下来的public * com.hhu.dao.*.save*(..)方法的总称
@Pointcut("execution(public * com.hhu.dao.*.save*(..))")
public void save() {
}
@Pointcut("execution(public * com.hhu.dao.*.delete*(..))")
public void delete() {
}
/*
* 定义完Pointcut,再定义Advice,即前后逻辑代码
* 这里使用的是@Around
* 它经典的写法就是try-catch-finally
*
* 1.判断是什么类型的操作,增加\删除\还是更新
* 增加/更新 save(Product),通过id区分是增加还是更新
* 删除delete(id)
* 2.获取changeitem
* (1)新增操作,before直接获取,after记录下新增之后的id
* (2)更新操作,before获取操作之前的记录,after获取操作之后的记录,然后diff获取ChangeItem
* (3)删除操作,before根据id取记录
* 3.保存操作记录
* actionType
* objectId
* objectClass
*/
@Around("save() || delete()")
public Object addOperateLog(ProceedingJoinPoint pjp) throws Throwable{
Object returnObj = null;
//获取操作方法名
String method = pjp.getSignature().getName();
System.out.println("***************用户当前再进行" + method + "操作***************");
//初始化操作类型
ActionType actionType = null;
//初始化id,用户区分更新还是新增的行为
Long id = null;
Action action = new Action();
Object oldObj = null;
try {
//判断方法是增、删、改中的哪一类
if("save".equals(method)) {//如果方法有“save”则可能是save或者update
//获取操作对象
Object obj = pjp.getArgs()[0];
System.out.println("***************操作对象为:" + obj + "***************");
//获取对象的id属性
Object idObj = PropertyUtils.getProperty(obj,"id");
System.out.println("获取的idObj为: " + idObj);
//注意:这里不能直接对idObj进行toString然后强转包装类,主要如果获取对象为null那么在进行toString就会报空指针异常
if(idObj==null) {
//如果没有id参数那么就是新增操作
actionType = ActionType.INSERT;
//在Insert是获取ChangeItems
List<ChangeItem> changeItems = DiffUtil.getInsertChangeItems(obj);
//存储在change里
action.getChanges().addAll(changeItems);
//这里存在疑问,新增操作前是不存在旧对象的,就为null,这里注意数据库是否接受的问题
// action.setObjectClass(oldObj.getClass().getName());
} else {
//这里注意getProperty()获取的是Object对象
id = Long.valueOf(idObj.toString());
System.out.println("***************操作对象的id为:" + id + "***************");
//如果出现id就是更新操作
System.out.println("***************执行更新操作,id为:" + id + "***************");
actionType = ActionType.UPDATE;
action.setObjectId(id);
//将旧的对象保存起来(即更新前的对象)
oldObj = DiffUtil.getObjectById(pjp.getTarget(), id);
//更新操作时是存在旧对象的
action.setObjectClass(oldObj.getClass().getName());
}
} else if ("delete".equals(method)){
//保存删除擦操作前对象的id,这里既然进行删除操作,那么id就不可能为空,所以这里可以安全转换
id = Long.valueOf(pjp.getArgs()[0].toString());
System.out.println("***************执行删除操作,id为:" + id + "***************");
actionType = ActionType.DELETE;
//保存删除前的对象
oldObj = DiffUtil.getObjectById(pjp.getTarget(), id);
ChangeItem changeItem = DiffUtil.getDeleteChangeItem(oldObj);
action.getChanges().add(changeItem);
action.setObjectClass(oldObj.getClass().getName());
action.setObjectId(id);
}
/*执行proceed()方法获取返回值,这里即方法的执行,这里要做的
* 就是在该方法前后加上非功能性代码
*/
returnObj = pjp.proceed(pjp.getArgs());
//After之后的逻辑,即保存操记录
action.setActionType(actionType);
if(actionType.INSERT == actionType) {
//获取新增后的id,通过反射获取returnObj中的id
Object newId = PropertyUtils.getProperty(returnObj, "id");
action.setObjectId(Long.valueOf(newId.toString()));
} else if(ActionType.UPDATE ==actionType) {
//获取新的对象
Object newObj = DiffUtil.getObjectById(pjp.getTarget(), id);
System.out.println("***************更新操作时获取的即将更新的对象为" + newObj + "***************");
List<ChangeItem> changeItems = DiffUtil.getChangeItems(oldObj, newObj);
//保存ChangeItems
action.getChanges().addAll(changeItems);
}
action.setOperator("admin");//具体根据登录用户来设定
action.setOperatorTime(new Date());
actionDao.save(action);
} catch (Exception e) {
// TODO: handle exception
logger.error(e.getMessage(),e);
}
//返回返回值
return returnObj;
}
}
3.工具类
package com.hhu.util;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.beanutils.PropertyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.hhu.datalog.Datalog;
import com.hhu.domain.ChangeItem;
/**
* 获取ChangeItem的方法
* @author Weiguo Liu
*
*/
public class DiffUtil {
private static final Logger logger = LoggerFactory.getLogger(DiffUtil.class);
public static Object getObjectById(Object target,Object id) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method findMethod = target.getClass().getDeclaredMethod("findById", Long.class);
Object oldObj = findMethod.invoke(target, id);
return oldObj;
}
/**
* 获取新增擦欧总的change Item
*
* @param obj
* @return
*/
public static List<ChangeItem> getInsertChangeItems(Object obj) {
Map<String, String> valueMap = getBeanSimpleFieldValueMap(obj, true);
//获取中文名
Map<String,String> fieldCnNameMap = getFieldNameMap(obj.getClass());
List<ChangeItem> items = new ArrayList<>();
for (Map.Entry<String, String> entry : valueMap.entrySet()) {
String fieldName = entry.getKey();
String value = entry.getValue();
ChangeItem changeItem = new ChangeItem();
changeItem.setOldValue("");
changeItem.setNewValue(value);
changeItem.setField(fieldName);
String cnName = fieldCnNameMap.get(fieldName);
changeItem.setFieldShowName(StringUtils.isEmpty(cnName) ? fieldName : cnName);
items.add(changeItem);
}
return items;
}
/*
* 获取删除操作的Change Item
*/
public static ChangeItem getDeleteChangeItem(Object obj) {
ChangeItem changeItem = new ChangeItem();
changeItem.setOldValue(JSON.toJSONString(obj));
changeItem.setNewValue("");
return changeItem;
}
public static List<ChangeItem> getChangeItems(Object oldObj, Object newObj) {
Class cl = oldObj.getClass();
List<ChangeItem> changeItems = new ArrayList<ChangeItem>();
// 获取字段的中文名称
Map<String, String> fieldCnNameMap = getFieldNameMap(cl);
try {
BeanInfo beanInfo = Introspector.getBeanInfo(cl, Object.class);
for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
String field = propertyDescriptor.getName();
// 获取字段旧值
String oldProp = getValue(PropertyUtils.getProperty(oldObj, field));
// 获取字段新值
String newProp = getValue(PropertyUtils.getProperty(newObj, field));
// 对比新旧值
if (!oldProp.equals(newProp)) {
ChangeItem changeItem = new ChangeItem();
changeItem.setField(field);
String cnName = fieldCnNameMap.get(field);
changeItem.setFieldShowName(StringUtils.isEmpty(cnName) ? field : cnName);
changeItem.setNewValue(newProp);
changeItem.setOldValue(oldProp);
changeItems.add(changeItem);
}
}
} catch (Exception e) {
// TODO: handle exception
logger.error("This is error when convert change item", e);
}
return changeItems;
}
/**
* 不同类型转字符串的处理
*/
public static String getValue(Object obj) {
if (obj != null) {
if (obj instanceof Date) {
return formateDateW3C((Date) obj);
} else {
return obj.toString();
}
} else {
return "";
}
}
/**
* 从注解中读取中文名
*/
public static Map<String, String> getFieldNameMap(Class<?> clz) {
Map<String, String> map = new HashMap<>();
for (Field field : clz.getDeclaredFields()) {
if (field.isAnnotationPresent(Datalog.class)) {
Datalog datalog = field.getAnnotation(Datalog.class);
map.put(field.getName(), datalog.name());
}
}
return map;
}
/**
* 将data类型转换为字符串形式
*/
public static String formateDateW3C(Date date) {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
String text = df.format(date);
String result = text.substring(0, 22) + ":" + text.substring(22);
return result;
}
/**
* 获取bean的fieldname和value 只获取简单类型,不获取复杂类型,包括集合
*/
public static Map<String, String> getBeanSimpleFieldValueMap(Object bean, boolean filterNull) {
Map<String, String> map = new HashMap<String, String>();
if (bean == null) {
return map;
}
Class<?> clazz = bean.getClass();
try {
// 不获取父类的字段
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Class<?> fieldType = fields[i].getType();
String name = fields[i].getName();
Method method = clazz.getMethod("get" + name.substring(0, 1).toUpperCase() + name.substring(1));
Object value = method.invoke(bean);
if (filterNull && value == null) {
continue;
}
if (isBaseDataType(fieldType)) {
String strValue = getFieldStringValue(fieldType, value);
map.put(name, strValue);
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return map;
}
/**
* 自定义不同类型的String类型
*/
public static String getFieldStringValue(Class type, Object value) {
if (type.equals(Date.class)) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return formatter.format((Date) value);
}
return value.toString();
}
/**
* 判断一个类是否为基本数据类型或包装类,或日期。
*
* @param clazz
* 要判断的类。
* @return true 表示为基本数据类型。
*/
public static boolean isBaseDataType(Class clazz) throws Exception {
return (clazz.equals(String.class) || clazz.equals(Integer.class) || clazz.equals(Byte.class)
|| clazz.equals(Long.class) || clazz.equals(Double.class) || clazz.equals(Float.class)
|| clazz.equals(Character.class) || clazz.equals(Short.class) || clazz.equals(BigDecimal.class)
|| clazz.equals(BigInteger.class) || clazz.equals(Boolean.class) || clazz.equals(Date.class)
|| clazz.isPrimitive());
}
}
完整项目代码转:https://github.com/Jacksonary/CodeRepository.git其中的datalog项目
推荐阅读
-
Spring学习教程之AOP模块的概述
-
利用spring AOP记录用户操作日志的方法示例
-
基于spring中的aop简单实例讲解
-
Spring学习教程之AOP模块的概述
-
Springboot基于assembly的服务化打包方案及spring boot部署方式
-
利用spring AOP记录用户操作日志的方法示例
-
基于spring中的aop简单实例讲解
-
Spring中的JDBCTemplate、Spring基于AOP的事务控制、Spring中的事务控制
-
Spring学习笔记第二天,Spring基于xml或注解的IOC以及IOC的案例
-
基于SpringBoot的操作日志管理(AOP+自定义注解方式)