概念
面向切面编程(也叫面向方面编程):Aspect Oriented Programming(AOP),是软件开发中的一个热点,也是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。
主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
image我们要做的,是定义一个切面,在切面的纵向定义处理方法,处理完成后,回到横向业务流(静态代理模式实现 proxy)。
因为Java 是一门静态的强类型语言, 代码一旦写好, 编译成 java class
以后 ,可以在运行时通过反射(Reflection)来查看类的信息, 但是对类进行修改的话很困难。有如下方式来实现:
- 在编译的时候, 根据AOP的配置信息,悄悄的把日志,安全,事务等“切面”代码 和业务类编译到一起去。【预编译】
- 在运行期,业务类加载以后, 通过Java动态代理技术为业务类生产一个代理类, 把“切面”代码放到代理类中, Java 动态代理要求业务类需要实现接口才行。【运行期动态代理】
- 在运行期, 业务类加载以后, 动态的使用字节码构建一个业务类的子类,将“切面”逻辑加入到子类当中去, CGLIB就是这么做的。
spring 采用(1)+(2)方式
实现方式
1. 实现 AOP 接口
2. 通过.xml
配置方式
<bean id="personDao" class="com.itheima12.spring.aop.xml.transaction.PersonDaoImpl"></bean>
<bean id="transaction" class="com.itheima12.spring.aop.xml.transaction.Transaction"></bean>
<aop:config>
<!-- 切入点表达式 确定目标类 -->
<expression="execution(* com.itheima12.spring.aop.xml.transaction.PersonDaoImpl.*(..))" id="perform"/>
<!-- ref指向的对象就是切面 -->
<aop:aspect ref="transaction">
<!--
前置通知
1、在目标方法执行之前
2、获取不到目标方法的返回值
-->
<!--
<aop:before method="beginTransaction" pointcut-ref="perform"/>
-->
<!--
后置通知
1、后置通知可以获取到目标方法的返回值
2、当目标方法抛出异常,后置通知将不再执行
-->
<!--
<aop:after-returning method="commit" pointcut-ref="perform" returning="val"/>
-->
<!--
最终通知
无论目标方法是否抛出异常都将执行
-->
<aop:after method="finallyMethod" pointcut-ref="perform"/>
<!--
异常通知(多个异常的处理)这个异常处理是完全独立于系统之外的,脱离业务逻辑
-->
<aop:after-throwing method="throwingMethod" throwing="ex" pointcut-ref="perform"/>
<!--
环绕通知(可以进行权限管理,比如 shiro 底层)
1. 能控制目标方法的执行
2. 前置通知和后置通知能在目标方法的前面和后面加一些代码,但是不能控制目标方法的执行
-->
<aop:around method="aroundMethod" pointcut-ref="perform"/>
</aop:aspect>
</aop:config>
一些概念
- 切面(aspect):用来切插业务方法的类。
- 连接点(joinpoint):是切面类和业务类的连接点,其实就是封装了业务方法的一些基本属性,作为通知的参数来解析。
- 通知(advice):在切面类中,声明对业务方法做额外处理的方法。
- 切入点(pointcut):业务类中指定的方法,作为切面切入的点。其实就是指定某个方法作为切面切的地方。
- 目标对象(target object):被代理对象。
- AOP代理(aop proxy):代理对象。
- 通知
- 前置通知(before advice):在切入点之前执行。
- 后置通知(after returning advice):在切入点执行完成后,执行通知。
- 环绕通知(around advice):包围切入点,调用方法前后完成自定义行为。
- 异常通知(after throwing advice):在切入点抛出异常后,执行通知。
切入点表达式
切入点表达式Spring AOP 的原理:
- 当spring启动的时候,加载两个bean,对两个bean进行实例化
- 当spring容器对配置文件解析到
<aop:config>
的时候,把切入点表达式解析出来,按照切入点表达式匹配spring容器内的bean。 - 如果匹配成功,则为该
bean
创建对象 - 当客户端利用context.getBean获取一个对象时,如果该对象有代理对象,则返回代理对象。如果没有,则返回本身
应用实例
1. AOP 权限管理(环绕通知)
自定义注解
@Target(ElementType.METHOD) //范围:在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface PrivlegeInfo {
String name() default "";
}
编写注解解析器
public class AnnotationParse {
/*
* targetClass 目标类的class形式
* methodName 在客户端调用哪个方法,methodName就代表哪个方法
*/
public static String parse(Class targetClass,String methodName) throws Exception{
String methodAccess = "";
/**
* 该方法没有参数
*/
Method method = targetClass.getMethod(methodName);
//判断方法上面是否存在PrivilegeInfo注解
if(method.isAnnotationPresent(PrivlegeInfo.class)){
//得到方法上面的注解
PrivlegeInfo privlegeInfo = method.getAnnotation(PrivlegeInfo.class);
methodAccess = privlegeInfo.name();
}
return methodAccess;
}
}
service方法
public class PersonServiceImpl implements PersonService{
@PrivlegeInfo(name="savePerson")
public void savePerson() {
System.out.println("save person");
}
@PrivlegeInfo(name="updatePerson")
public void updatePerson() {
System.out.println("update person");
}
}
用户权限的切面类
public class PrivilegeAspect {
/**
* 用户拥有的权限
*/
private List<Privilege> privileges = new ArrayList<Privilege>();
public List<Privilege> getPrivileges() {
return privileges;
}
public void setPrivileges(List<Privilege> privileges) {
this.privileges = privileges;
}
public void isAccessMethod(ProceedingJoinPoint joinPoint) throws Throwable{
/**
* 1、获取访问目标方法应该具备的权限
* 得到
* 1、目标类的class形式
* 2、方法的名称
*/
Class targetClass = joinPoint.getTarget().getClass();
String methodName = joinPoint.getSignature().getName();
//得到访问该方法的权限
String methodAccess = AnnotationParse.parse(targetClass, methodName);
boolean flag = false;
//遍历用户所有的权限,查看是否用访问该方法的权限
for (Privilege privilege : privileges) {
//该用户能够访问目标方法
if(methodAccess.equals(privilege.getName())){
flag = true;
}
}
if(flag){//访问目标方法
joinPoint.proceed();
}else{
System.out.println("对不起,您没有权限访问");
}
}
}
application 配置文件
<aop:config>
<aop:pointcut
expression="execution(* com.itheima12.spring.aop.xml.privilege.service.impl.*.*(..))" id="perform"/>
<aop:aspect ref="privilegeAspect">
<aop:around method="isAccessMethod" pointcut-ref="perform"/>
</aop:aspect>
</aop:config>
测试
public class PrivilegeTest {
@Test
public void testPrivilege(){
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
/**
* 初始化用户的权限
*/
PrivilegeAspect privilegeAspect = (PrivilegeAspect)context.getBean("privilegeAspect");
Privilege privilege1 = new Privilege();
privilege1.setName("savePerson");
Privilege privilege2 = new Privilege();
privilege2.setName("updatePerson");
privilegeAspect.getPrivileges().add(privilege2);
privilegeAspect.getPrivileges().add(privilege1);
PersonService personService = (PersonService)context.getBean("personService");
personService.savePerson();
personService.updatePerson();
}
}
2. AOP 缓存
application
<context:component-scan base-package="com.itheima12.spring.aop.xml.transaction">
</context:component-scan>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
切面类
/**
* @Aspect
* @Pointcut("execution(* com.itheima12.spring.aop.xml.transaction.PersonDaoImpl.*(..))")
* private void aa(){}
就相当于
* <aop:config>
<aop:pointcut
expression="execution(* com.itheima12.spring.aop.xml.transaction.PersonDaoImpl.*(..))"
id="aa()"/>
</aop:config>
* @author zd
*
*/
@Component("transaction") // 加入到spring容器中
@Aspect // 证明这个注解所在类是切面类
public class Transaction {
@Pointcut("execution(* com.itheima12.spring.aop.xml.transaction.PersonDaoImpl.*(..))")
private void aa(){} //方法签名
@Before("aa()") // 前置通知
public void beginTransaction(){
System.out.println("begin transaction");
}
@AfterReturning("aa()") // 后置通知
public void commit(){
System.out.println("commit");
}
}
3. AOP 日志管理
此项目是在 spring boot 环境下实现。
1、 添加maven依赖注解
<!--springBoot 的aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、添加数据库表
DROP TABLE IF EXISTS `journal`;
CREATE TABLE `journal` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '日志id',
`uid` int(11) NOT NULL COMMENT '用户id',
`modularType` int(2) NOT NULL COMMENT '模块类型',
`operationType` int(2) NOT NULL COMMENT '操作类型:0:增/1:删/2:改/3:关闭/4:移动',
`operationTime` datetime NOT NULL COMMENT '操作时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3、增加对应的实体类
4、日志添加 Mapper
/**
* 日志管理
* Created by 陈梓平 on 2017/8/12.
*/
public interface JournalMapper {
/**日志添加*/
int addJournalInfo(JournalInfo journalInfo);
}
<?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.chen.mapper.JournalMapper">
<!--添加日志信息-->
<insert id="addJournalInfo">
INSERT INTO journal (uid,modularType,operationType,operationTime)
VALUES (10086,#{modularType},#{operationType},NOW())
</insert>
</mapper>
5、日志工具类
@Component
@Transactional
public class JournalUtils {
@Autowired
private JournalMapper jouUtilsJournalMapper;
@Autowired
private JournalInfo journalInfo;
/**
* 添加日志
* @param modeularType
* @param operationType
*/
public void addJournalInfo(int modeularType,int operationType,int uid) {
journalInfo.setModularType(modeularType);
journalInfo.setOperationType(operationType);
journalInfo.setUid(uid);
jouUtilsJournalMapper.addJournalInfo(journalInfo);
}
}
6、静态类(包括模块和操作)
/**
* 静态信息
* Created by Administrator on 2017/8/12.
*/
public class StaticInfo {
/**-------------------- 模块类型 ----------------*/
//模块1
public static final int MODEULARTTYPE_FIRST= 1;
/**-------------------- 操作类别 ---------------*/
//增加
public static final int OPERATIONTYPE_ADD = 0;
//删除
public static final int OPERATIONTYPE_UPDATE = 1;
//修改
public static final int OPERATIONTYPE_DELETE = 2;
//开启
public static final int OPERATIONTYPE_OPEN = 3;
//关闭
public static final int OPERATIONTYPE_CLOSE = 4;
//移动
public static final int OPERATIONTYPE_MOVER = 5;
/**--------------- AOP代理 --------------------*/
public static final String AOP_OPERATION_TYPE_ADD = "add";
public static final String AOP_OPERATION_TYPE_EDIT = "edit";
public static final String AOP_OPERATION_TYPE_MOVE = "move";
public static final String AOP_OPERATION_TYPE_DELETE = "delete";
public static final String AOP_OPERATION_TYPE_OPENORCLOSE = "openOrClose";
public static final String AOP_MODULAR_TYPE_FIRST = "Journal";
public static final String AOP_SPIT_CLASSNAME = "impl.";
public static final String AOP_SPIT_MODULAR_TYPE= "ServiceImpl";
}
7、日志切面AOP
@Component
@Aspect
public class JournalAspect {
/**日志输出*/
private static final Logger logger = LoggerFactory.getLogger(JournalAspect.class);
/**日志工具类*/
@Autowired
private JournalUtils aspectJournalUtils;
/**service层切面*/
private final String POINT_CUT = "execution(* com.chen.service..*(..))";
@Pointcut(POINT_CUT)
private void pointcut(){}
/**
* 后置最终通知(目标方法只要执行完了就会执行后置通知方法)
* 日志管理
* @param joinPoint
*/
@After(value = "pointcut()")
@Transactional
public void doAfterAdvice(JoinPoint joinPoint) throws CustomException {
//用的最多 通知的签名
Signature signature = joinPoint.getSignature();
//1.获取模块类型
//AOP代理类的名字(包括包名)
String declaringTypeName = signature.getDeclaringTypeName();
logger.info("AOP代理类的名字"+declaringTypeName);
//获取代理类的类名
String[] split = declaringTypeName.split(StaticInfo.AOP_SPIT_CLASSNAME);
String className = split[1];
//获取模块名
String[] modularTypeNames = className.split(StaticInfo.AOP_SPIT_MODULAR_TYPE);
String modularTypeName = modularTypeNames[0];
int modulerType = -1;
//模块类型筛选
modulerType = this.getModularType(modularTypeName, modulerType);
//2.获取操作类型
//代理的是哪一个方法
String methodName = signature.getName();
logger.info("AOP代理方法的名字"+signature.getName());
int opreationType = -1;
opreationType = getOpreationType(joinPoint, signature, opreationType,methodName);
if (modulerType==-1&&opreationType==-1)
if (!StringUtils.isBlank(methodName)||!StringUtils.isBlank(modularTypeName))
throw new CustomException(ResultEnum.JOURNAL_LOG_ERROR);
//3.添加日志
if (modulerType!=-1&&opreationType!=-1)
//TODO 3.1 从请求获取用户id
aspectJournalUtils.addJournalInfo(modulerType,opreationType, 10086);
}
/**
* 模块类型筛选
* @param modularTypeName
* @param type
* @return
*/
private int getModularType(String modularTypeName, int type) {
//模块类型筛选
switch (modularTypeName){
case StaticInfo.AOP_MODULAR_TYPE_FIRST:
type = StaticInfo.MODEULARTTYPE_FIRST;
break;
//多模块添加
}
return type;
}
/**
* 获取操作类型
* @param joinPoint
* @param signature
* @param opreationType
* @return
*/
private int getOpreationType(JoinPoint joinPoint, Signature signature, int opreationType,String methodName ) {
switch (methodName){
case StaticInfo.AOP_OPERATION_TYPE_ADD:
opreationType = StaticInfo.OPERATIONTYPE_ADD;
break;
case StaticInfo.AOP_OPERATION_TYPE_EDIT:
opreationType = StaticInfo.OPERATIONTYPE_UPDATE;
break;
case StaticInfo.AOP_OPERATION_TYPE_MOVE:
opreationType = StaticInfo.OPERATIONTYPE_MOVER;
break;
case StaticInfo.AOP_OPERATION_TYPE_DELETE:
opreationType = StaticInfo.OPERATIONTYPE_DELETE;
break;
case StaticInfo.AOP_OPERATION_TYPE_OPENORCLOSE:
Object[] obj = joinPoint.getArgs();
int arg = (int) obj[1];
if (arg==1)
opreationType = StaticInfo.OPERATIONTYPE_OPEN;
else
opreationType = StaticInfo.OPERATIONTYPE_CLOSE;
break;
}
return opreationType;
}
}
8、添加Controller测试
@RestController
@RequestMapping
public class JournalController {
@Autowired
private JournalService journalService;
@PostMapping(value = "journalAdd")
public Result add(){
return journalService.add();
}
}
9、添加Service测试
@Service
public class JournalServiceImpl implements JournalService {
@Override
public Result add() {
return ResultUtils.success(ResultEnum.OK);
}
}
4. AOP 实现分布式锁
改造前:
所有应用分布式锁的地方都需要如下代码:
RLock redissonLock = redissonUtil.getRedisson().getLock("saveCourseApplyResource"+courseApplyResource.getUserId());
boolean res = false;
try {
//等待3秒,有效期5秒
res = redissonLock.tryLock(3, 5, TimeUnit.SECONDS);
if(res){
//执行业务操作
}
}catch (RuntimeException e){
throw e;
} catch (InterruptedException e) {
e.printStackTrace();
throw new RuntimeException("网络错误,请重试");
} finally {
if(res){
redissonLock.unlock();
}
}
改造后:
- 首先需要一个注解:
/**
* Description: 分布式锁应用注解<br>
*
* @author: name:yuxin <br>email: yuruixin@ixincheng.com <br>
* Create Time: 2018/3/4 0004-下午 8:48<br>
*/
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lock {
//分布式锁的key前缀
String lockName() default "";
//等待时长
int waitTime() default 3;
//有效期时长
int effectiveTime() default 5;
}
- 然后,需要一个切面服务类
/**
* Description: 分布式锁切面服务<br>
*
* @author: name:yuxin <br>email: yuruixin@ixincheng.com <br>
* Create Time: 2018/3/4 0004-下午 8:46<br>
*/
import com.xczhihui.bxg.online.common.utils.RedissonUtil;
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.redisson.api.RLock;
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.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
@Aspect
public class LockService {
public static String LOCK_NAME = "lockName";
public static String WAIT_TIME = "waitTime";
public static String EFFECTIVE_TIME = "effectiveTime";
@Autowired
private RedissonUtil redissonUtil;
protected Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("@annotation(com.xczhihui.common.Lock)")
public void lockPointcut() {
}
@Around("lockPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Map<String,Object> map = getLockParams(point);
String lockName = (String) map.get(LOCK_NAME);
int waitTime = (int) map.get(WAIT_TIME);
int effectiveTime = (int) map.get(EFFECTIVE_TIME);
Object[] methodParam = null;
Object object=null;
boolean resl = false;
//获取方法参数
methodParam = point.getArgs();
String lockKey = (String) methodParam[0];
// 获得锁对象实例
RLock redissonLock = redissonUtil.getRedisson().getLock(lockName+lockKey);
try {
//等待3秒 有效期8秒
resl = redissonLock.tryLock(waitTime, effectiveTime, TimeUnit.SECONDS);
if(resl){
object = point.proceed(point.getArgs());
}
}catch (RuntimeException e){
throw e;
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("网络错误,请重试");
}finally {
if(resl){
logger.info("开锁,{}",lockName+lockKey);
redissonLock.unlock();
}else{
logger.error("未获得锁,{}",lockName+lockKey);
throw new RuntimeException("网络错误,请重试");
}
}
return object;
}
/**
* Description:获取方法的中锁参数
* creed: Talk is cheap,show me the code
* @author name:yuxin <br>email: yuruixin@ixincheng.com
* @Date: 2018/3/4 0004 下午 8:59
**/
public Map<String,Object> getLockParams(ProceedingJoinPoint joinPoint) throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] method = targetClass.getMethods();
Map<String,Object> map = new HashMap<>();
for (Method m : method) {
if (m.getName().equals(methodName)) {
Class[] tmpCs = m.getParameterTypes();
if (tmpCs.length == arguments.length) {
Lock lock = m.getAnnotation(Lock.class);
if (lock != null) {
String lockName = lock.lockName();
int waitTime = lock.waitTime();
int effectiveTime = lock.effectiveTime();
map.put(LOCK_NAME,lockName);
map.put(WAIT_TIME,waitTime);
map.put(EFFECTIVE_TIME,effectiveTime);
}
break;
}
}
}
return map;
}
}
- 在需要加锁的方法上添加注解:
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Lock(lockName = "addCollectionApply",waitTime = 2,effectiveTime = 3)
public void saveCollectionApply4Lock(String lockKey,CourseApplyInfo courseApplyInfo){
//业务逻辑处理
}
网友评论