IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring-Aop简单步骤 -> 正文阅读

[Java知识库]Spring-Aop简单步骤

1.Spring Aop是什么?

AOP(Aspect Oriented Programming面向切面编程),是一种设计思想,它是面向编程(oop)的一种完善,通过编译期或运行期动态代理的方式为目标对象进行业务功能的增强,也就是为目标对象进行功能的扩展,如果使用传统的方式进行功能扩展的话有两种方式
????????方案一:基于继承方式实现其功能,设计如下:**

? ? ? ? ?假如有一个公告(通知)业务接口及实现:

? ? ? ? ?如何在不改变源码的前提下对对象进行功能扩展

pubic interface NoticeService{
 int deleteById(Integer…ids);
}

?

public class NoticeServiceImpl implements NoticeService{
 public int deleteById(Integer…ids){
 System.out.println(Arrays.toString(ids));
 return 0 ;
 } }

?需求:基于OCP(开闭原则-对扩展开放,对修改关闭)实际方式对NoticeServiceImpl类的功能进行扩展,例如在deleteById方法的前后输出以下系统时间

 public int deleteById(Integer…ids){
 System.out.println("Start:"+System.currentTimeMillis());
 Int rows=super.deleteById(ids);
 System.out.println("After:"+System.currentTimeMillis());
 return rows;
 } }

关键设计如下:

public class CglibLogNoticeService extends NoticeServiceImpl{
 public int deleteById(Integer…ids){
 System.out.println("Start:"+System.currentTimeMillis());
 Int rows=super.deleteById(ids);
 System.out.println("After:"+System.currentTimeMillis());
 return rows;
 } }

测试类如下:

public class NoticeServiceTests{
 public static void main(String[] args){
 NoticeService ns=new CglibLogNoticeService();
 ns.deleteById(10,20,30);
 } }

结论:基于继承方式实现功能扩展,代码简单,容易理解,但是不够灵活(java中只能单一继承,继承一个类就不能继承另一个类),耦合性太高。

? ? ? ? 方案二:基于组合方式实现功能扩展,代码如下:

public class JdkLogNoticeService implements NoticeService{
 private NoticeService noticeService;//has a 
 public JdkLogNoticeService(NoticeService noticeService){
 this.noticeService=noticeService;
}
 public int deleteById(Integer…ids){
 System.out.println("Start:"+System.currentTimeMillis());
 int rows=this.noticeService.deleteById(ids);
 System.out.println("After:"+System.currentTimeMillis());
 return rows;
 } }

测试类如下:

public class NoticeServiceTests{
 public static void main(String[] args){
 NoticeService ns=
 new JdkLogNoticeService(new NoticeServiceImpl());
 ns.deleteById(10,20);
 } }

结论:基于组合方式实现功能扩展,代码比较灵活,耦合低,稳定性强,但理解相对比较困

难。
? ? ? ? ? ?
? ? ? ? ? 总之,无论是继承, 还是组合都是基于 OCP 方式实现了对象功能扩展 , 都有相应的优缺点 ,
且我们都要自己去写这些子类或兄弟类,对于这样的模板代码我们能否进行简化呢 ?aop就实现了对象和两种方式的优化。
????????
????????
????????实际项目中我们通常将面向对象理解
为一个静态过程 ( 例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性 ) ,面
向切面理解为一个动态过程(在对象运行时动态织入一些扩展功能或控制对象执行)。如图
-1 所示
????????

实现原理:?? ? ?

????????AOP 可以在系统启动时为目标类型创建子类或兄弟类型对象 , 这样的对象我们通常会称
之为动态代理对象 . 如图所示 :
????????

?其中,为目标类型(XxxServiceImpl)创建其代理对象方式有两种:

  • 第一种方式 : 借助 JDK 官方 API 为目标对象类型创建其兄弟类型对象 , 但是目标对象类
    型需要实现相应接口。
  • 第二种方式 : 借助 CGLIB 库为目标对象类型创建其子类类型对象 , 但是目标对象类型不
    能使用 final 修饰 .
    注意:spring中默认使用jdk代理,如果目标对象没有实现接口会换成CGLIB代理方式;spring boot默认使用CGLIB方式代理 。

    相关术语分析

    • 切面 (aspect): 横切面对象 , 一般为一个具体类对象。、
    • 切入点 (pointcut): 定义了切入扩展业务逻辑的位置 ( 哪些方法运行时切入扩展业务 ),
      般会通过表达式进行相关定义 , 一个切面中可以定义多个切入点。
    • 通知 (Advice): 内部封装扩展业务逻辑的具体方法对象 , 一个切面中可以有多个通知 (
      切面的某个特定位置上执行的动作 ( 扩展功能 )
    • 连接点 (joinpoint): 程序执行过程中,封装了某个正在执行的目标方法信息的对象 , 可以
      通过此对象获取具体的目标方法信息 , 甚至去调用目标方法。连接点与切入点定义如图
      所示:

      ?

      说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全
      检查过程看成是 通知。总之,概念很晦涩难懂,多做例子,做完就会清晰。先可以按白话去理解。
入门步骤:
? ? ? ? ? ? ? ? ? ? ? ? 1.添加spring-boot-starter-aop依赖
????????????????????????
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
 </dependency>

? ?????????说明:基于此依赖 spring 可以整合 AspectJ 框架快速完成 AOP 的基本实现。AspectJ 是一个面向切面的框架,他定义了 AOP 的一些语法,有一个专门的字节码生成器来生成遵java 规范的 class 文件。

业务切面对象设计

????????第一步:创建注解类型,应用于切入点表达式的定义,关键代码如下:

package com.cy.pj.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
 String operation();
}

????????第二步:创建切面对象,用于做日志业务增强,关键代码如下:

@Aspect
@Component
public class SysLogAspect {
 private static final Logger log= 
LoggerFactory.getLogger(SysLogAspect.class);
 /**
 * @Pointcut 注解用于定义切入点
 * @annotation(注解)为切入点表达式,后续由此注解描述的方法为切入
 * 点方法
 */
 @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
 public void doLog(){}//此方法只负责承载切入点的定义
 /**
 * @Around 注解描述的方法,可以在切入点执行之前和之后进行业务拓展,
 * @param jp 连接点对象,此对象封装了要执行的目标方法信息.
 * 可以通过连接点对象调用目标方法
 * @return 目标方法的执行结果
 * @throws Throwable
 */
 @Around("doLog()")
 public Object doAround(ProceedingJoinPoint jp)throws Throwable{
     long t1=System.currentTimeMillis();
 try {
 //执行目标方法(切点方法中的某个方法)
 Object result = jp.proceed();
 long t2=System.currentTimeMillis();
 log.info("opertime:{}",t2-t1);
 return result;//目标业务方法的执行结果
 }catch(Throwable e){
 e.printStackTrace();
 long t2=System.currentTimeMillis();
log.info("exception:{}",e.getMessage());
 throw e;
 }
 }
????????第三步:通过注解 RequiredLog 注解描述通告业务 (@Service) 相关方法,此时这个方法为
日志切入点方法,例如:
@RequiredLog(operation="公告查询")
@Override
public List<SysNotice> findNotices(SysNotice notice) {
 
 //log.debug("start: {}",System.currentTimeMillis());
List<SysNotice> list=sysNoticeDao.selectNotices(notice);
 //log.debug("end: {}",System.currentTimeMillis());
 return list; }

????????第四步:测试通知业务方法,并检测日志输出以及了解其运行原理,如图所示

? ? ? ? 成功后再来修改切面对象:

@Aspect
@Component
public class SysLogAspect {
 private static final Logger log= 
LoggerFactory.getLogger(SysLogAspect.class);
 /**
 * @Pointcut 注解用于定义切入点
 * @annotation(注解)为切入点表达式,后续由此注解描述的方法为切入
 * 点方法
 */
 @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
 public void doLog(){}//此方法只负责承载切入点的定义
 /**
 * @Around 注解描述的方法,可以在切入点执行之前和之后进行业务拓展,
 * @param jp 连接点对象,此对象封装了要执行的目标方法信息.
 * 可以通过连接点对象调用目标方法.
 * @return 目标方法的执行结果
 * @throws Throwable
 */
 @Around("doLog()")
 public Object doAround(ProceedingJoinPoint jp)throws Throwable{
 long t1=System.currentTimeMillis();
 log.info("Start:{}",t1);
 try {
 //执行目标方法(切点方法中的某个方法)
 Object result = jp.proceed();
 long t2=System.currentTimeMillis();
 log.info("After:{}",t2);
 doLogInfo(jp,t2-t1,null);
 return result;//目标业务方法的执行结果
 }catch(Throwable e){
 e.printStackTrace();
 long t2=System.currentTimeMillis();
 doLogInfo(jp,t2-t1,e);
 throw e;
 }
 }
 //记录用户行为日志
13
 private void doLogInfo(ProceedingJoinPoint jp,
 long time,Throwable e)
 throws Exception {
 //1.获取用户行为日志
 //1.1 获取登录用户名(没做登录时,可以先给个固定值)
 String username="cgb";
 //1.2 获取 ip 地址
 String ip= "202.106.0.20";
 //1.3 获取操作名(operation)-@RequiredLog 注解中 value 属性的值
 //1.3.1 获取目标对象类型
 Class<?> targetCls=jp.getTarget().getClass();
 //1.3.2 获取目标方法
 MethodSignature ms=
 (MethodSignature) jp.getSignature();//方法签名
 Method targetMethod=targetCls.getMethod(
 ms.getName(),ms.getParameterTypes());
 //1.3.3 获取方法上 RequiredLog 注解
 RequiredLog annotation =
 targetMethod.getAnnotation(RequiredLog.class);
 //1.3.4 获取注解中定义操作名
 String operation=annotation.operation();
 //1.4 获取方法声明(类全名+方法名)
 String classMethodName=
 targetCls.getName()+"."+targetMethod.getName();
 //1.5 获取方法实际参数信息
 Object[]args=jp.getArgs();
 String params=new ObjectMapper().writeValueAsString(args);
 //2.封装用户行为日志
 SysLog sysLog=new SysLog();
 sysLog.setUsername(username);
 sysLog.setIp(ip);
 sysLog.setOperation(operation);
 sysLog.setMethod(classMethodName);
 sysLog.setParams(params);
 sysLog.setTime(time);
 if(e!=null) {
 sysLog.setStatus(0);
 sysLog.setError(e.getMessage());
 }
 //3.打印日志
 String userLog=new ObjectMapper()
 .writeValueAsString(sysLog);
 log.info("user.oper {}",userLog);
 } }

??Spring AOP 技术进阶

????????通知类型

  • @Around (优先级最高的通知,可以在目标方法执行之前,之后灵活进行业务拓展.)
  • @Before (目标方法执行之前调用)
  • @AfterReturning (目标方法正常结束时执行)
  • @AfterThrowing (目标方法异常结束时执行)
  • @After (目标方法结束时执行,正常结束和异常结束它都会执行)

? ? ? ??切面执行顺序??

切面的优先级需要借助 @Order 注解进行描述,数字越小优先级越高,默认优先级比较
低。例如:
@Order(1)
@Aspect
@Component
public class SysLogAspect {
}

定义缓存切面并指定优先级:

@Order(2)
@Aspect
@Component
public class SysCacheAspect {
...
}

?

说明:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤
器链、拦截器链,其执行分析如图所示:

?

注意:spring中提供的缓存在于第三方分页框架进行整合使用时,@Cacheable(cacheNames = "lk"),该注解如果放在service层查询结果时,第一次正常,第二次会发现没有数据,原因应该是拦截器的执行顺序问题...

? ? ? ? 分页底层会在sql访问数据前将原来sql语句进行拦截,而缓存是在第一次访问数据库后将返回的结果放到缓存区中一份,下次如果访问的是同一个方法,就从缓存区中取。如果将缓存置于service层的话,用户发送请求调用方法会先执行分页,所以这个时候数据没有被放到缓存中去,缓存中是空的,这就会导致页面第二次查询无结果。


                
        
    
 

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-10-06 12:04:30  更:2021-10-06 12:06:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 19:24:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码