一、AOP切面编程原理
AOP全称为Aspect-Oriented Programming,中文翻译为“面向切面编程”。AOP和OOP(Object-Oriented Programming)作为两种编程范式,都追求实现代码的模块化、可重用性和可维护性。AOP是通过将横切关注点抽象出来来实现这一点,将其称为“切面”(Aspect) 。AOP采用的是将这些关注点与原方法进行分离的方式来降低代码的耦合度的。
在Java中,AOP动态代理的实现,使得我们可以在执行目标方法之前和之后,插入一些特定的处理,而不是在主业务中添加大量冗余代码。AOP在实现中使用到了反射技术,对java层面的代码进行拦截和切入操作。
二、AOP切面编程是什么
AOP切面编程指的是一种面向切面的编程思想和设计模式,它将各个业务逻辑之间相同或类似的处理抽象成一个切面,达到了代码重用,简化程序设计的目的。
例如,当我们需要在应用程序中统计某个方法的运行时间,如果在每次调用该方法时都手动添加代码,代码将变得冗余且不易维护。但是,使用AOP切面编程,我们可以将计时的代码组织成一个切面,并将其织入到目标方法中,达到代码重用的目的。
三、AOP切面编程三种实现方式
1. 静态代理
静态代理是通过在编译期间,手动编写代理类的方式进行的,需要我们手动为每一个类编写代理类,存在大量的冗余代码。其原理是在代理类中生成一个目标类的对象并对其进行包装,通过调用代理类中的方法来实现对目标类方法的调用。
2. JDK动态代理
JDK动态代理是面向接口代理,实现InvocationHandler接口,重写invoke方法。在运行期间,使用Java反射机制,在内存中生成代理类,并且实现了目标类的接口,直接调用代理类中的方法会触发InvocationHandler的invoke方法,我们可以在这个方法中添加需要的操作。JDK动态代理可以通过proxy.newProxyInstance()方法来动态的生成代理类,支持多个目标类的代理。
3. CGLIB动态代理
CGLIB动态代理是直接生成目标类的子类,因而可以对类进行代理(而不仅仅是接口),不过,需要引入cglib-nodep依赖。CGLIB动态代理同样实现了InvocationHandler接口,通过创建Enhancer来动态生成目标类的子类,然后在子类中织入相关增强逻辑,最后创建子类对象。
四、切面编程是什么意思
切面编程是AOP编程的实现方式之一,它将通用或者常见的功能,像日志记录、性能分析、权限控制等,称为切面。这些切面可以纵向地贯穿应用的各层,比如Controller,Service,Dao等,也可以横向地贯穿应用的多个模块和组件。
切面可以看做是一组针对特定类某一类方法的函数,利用指定的切面可以更方便、更直观、更快捷地处理代码,它对代码的可读性和维护性都有很好的帮助。
五、AOP面向切面编程应用场景
通过AOP编程实现的切面,可以在任何地方被调用,可以方便地通过合适的实现方式,实现各种不同的应用场景,包括:
1. 日志记录
记录关键方法的执行情况,比如接口调用、运行时间、日志级别等,例如下面是一个简单的打印日志的AOP切面:
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Before("execution(* com.example.demo.service.*.*(..))")
public void doBefore() {
logger.info("method start...");
}
@After("execution(* com.example.demo.service.*.*(..))")
public void doAfter() {
logger.info("method end...");
}
}
2. 权限管理
处理用户权限相关问题,比如用户是否具有操作某项资源的权限、用户是否处于登录状态等。在使用Spring Security等安全框架时,会经常使用这种方式对请求进行认证和授权。
3. 数据缓存
对于一些数据的计算和处理非常耗费时间,可以将结果缓存起来,再次用到时直接从缓存中获取数据,提高效率。例如通过redis工具实现数据缓存:
@Around("execution(* com.example.demo.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
//生成redis的key
String key = RedisKeyUtils.getKey(methodName, Arrays.toString(args));
//先从redis中查找数据
Object obj = stringRedisTemplate.opsForValue().get(key);
if (obj != null) {
logger.info("Redis exists key - " + key);
return obj;
}
logger.info("Redis not exists key - " + key);
//如果redis中不存在数据则进行计算,然后添加到redis中
Object result = pjp.proceed();
if (result != null) {
stringRedisTemplate.opsForValue().set(key, result.toString(), 300, TimeUnit.SECONDS);
logger.info("Redis add key - " + key);
}
return result;
}
4. 声明式事务
通过AOP的方式来帮助我们完成事务的操作,比如管理数据库连接、开启或关闭事务、定义事务的边界等。在Spring整合Mybatis时,经常会用到这种方式来实现声明式事务,只需要在需要使用事务的方法上添加@Transactional注解即可:
@Transactional
public void insertUser(User user) {
userDao.insert(user);
}
六、AOP切面编程思想
AOP切面编程思想,是其在设计模式上的具体落地。通过将不同的功能划分为不同的切面或者模块,使其更具有可读性、可维护性和可扩展性。大大减少了重复代码和代码冗余,使得代码更加简介,减轻了程序员的工作量,提高开发效率。
七、AOP切面编程架构
AOP切面编程架构通常有三个核心模块,即切面模块、切入点模块和通知模块。
1. 切面模块
切面模块是AOP切面编程的核心模块,它由一个或多个切面组成。每一个切面都包含了一个或多个通知和一个或多个切点。
2. 切入点模块
切入点模块用于定位在目标类中的切点位置。在整个AOP过程中,先针对具体的方法进行拦截,拦截到方法后再根据用户定义的切面进行处理。
3. 通知模块
通知模块则包括了具体的增强代码,而这些增强代码都是用户自己定义的。通知可以包括Before Advice、After Advice、Around Advice、After Throwing Advice和After Returning Advice等。
八、AOP切面编程作用
AOP切面编程能够让我们的代码更加优雅、简洁、易于维护、扩展和复用,并且可以大大降低多个组件之间的耦合度,并且大大减少重复开发、代码冗余以及出错几率。性能方面也有所提高,例如Redis缓存、声明式事务等。
九、AOP切面编程代码示例
下面是一个简单的Spring Boot应用,使用AOP切面编程来记录用户新增和删除操作的日志。
1. 新增记录日志切面
@Aspect
@Component
public class UserAddAspect {
@Autowired
private HttpServletRequest request;
private static final Logger logger = LoggerFactory.getLogger(UserAddAspect.class);
@Before("@annotation(com.example.demo.annotation.UserAdd)")
public void addLog() {
String methodName = "新增用户";
String params = request.getParameter("username");
logger.info("执行 " + methodName + " 方法,参数为:" + params);
}
}
2. 删除记录日志切面
@Aspect
@Component
public class UserDeleteAspect {
@Autowired
private HttpServletRequest request;
private static final Logger logger = LoggerFactory.getLogger(UserDeleteAspect.class);
@Before("@annotation(com.example.demo.annotation.UserDelete)")
public void deleteLog() {
String methodName = "删除用户";
String params = request.getParameter("userId");
logger.info("执行 " + methodName + " 方法,参数为:" + params);
}
}
3. 控制器中使用
@RestController
public class UserController {
@Autowired
private UserService userService;
@UserAdd //记录新增用户日志
@PostMapping("/add")
public String addUser(@RequestParam String username, @RequestParam String password) {
userService.addUser(username, password);
return "success";
}
@UserDelete //记录删除用户日志
@DeleteMapping("/delete/{userId}")
public String deleteUser(@PathVariable String userId) {
userService.deleteUser(userId);
return "success";
}
}
将注解UserAdd和UserDelete,分别标记在新增用户和删除用户的方法上。当我们调用这两个方法时,将会在控制台上打印出相应的日志,从而实现了记录用户操作的目的。