Spring IOC和AOP的原理和实例

系列文章目录

文章目录

  • 系列文章目录
  • 前言
  • 一、什么Spring
    • 1.1、Spring核心结构
  • 二、Spring核心思想
    • 2.1、IOC
      • 2.1.1、IOC优点
      • 2.2.2、IOC与DI的区别
      • 2.2.3、实战
        • 2.2.3.1、 策略工厂模式
        • 2.2.3.2、ApplicationContent上下文获取Service
        • 2.2.3.3、双数据库
    • 2.2、AOP
      • 2.2.1、AOP术语
      • 2.2.2、Advice增强
      • 2.2.3、实现原理
      • 2.2.4、扩展:动态代理
        • 2.2.4.1、jdk动态代理
        • 2.2.4.2、cglib动态代理
      • 2.2.3、实战
        • 2.2.3.1、事务注解@Transactions
        • 2.2.3.2、异步注解@Async
        • 2.2.3.3、 本地缓存
        • 2.2.3.4、入参去空格拦截器


前言

本文主要介绍了Spring IOC和aop的原理及实例详解,文中的示例代码非常详细,需要的朋友可以参考下。


一、什么Spring

Spring框架是一个开放源代码的J2EE应用程序框架,是针对bean的生命周期进行管理的轻量级容器。 Spring解决了开发者在J2EE开发中遇到的许多常见的问题,提供了功能强大IOC、AOP及Web MVC等功能。


1.1、Spring核心结构

Spring是⼀个分层⾮常清晰并且依赖关系、职责定位⾮常明确的轻量级框架,主要包括⼏个⼤模块:数据处理模块、Web模块、AOP(Aspect Oriented Programming)/Aspects模块、Core Container模块和 Test 模块,如下图所示:
Spring IOC和AOP的原理和实例_第1张图片

Spring依靠这些基本模块,实现了⼀个令⼈愉悦的融合了现有解决⽅案的零侵⼊的轻量级框架。

模块 名称 功能
Spring core 核心容器 是Spring框架最核⼼的部分,负责bean的生命周期管理
Spring context 应用上下文 拓展了核心容器,提供事件处理、国际化等功能
Spring AOP 面向切面编程 Spring管理的任何对象都支持AOP,AOP可以帮助应⽤对象解耦
Data Access 数据访问与集成 Spring的JDBC和DAO模块封装了⼤量样板代码,这样可以使得数据库代码变得简洁,也可以更专注于我们的业务,还可以避免数据库资源释放失败⽽引起的问题。
web web模块 提供了多种构建和其它应⽤交互的远程调⽤⽅案。例如:SpringMVC框架,提供了Web应用上下文,对Web开发提供功能上的支持。
Spring test 测试模块 使得开发者能够很⽅便的进⾏测试.

二、Spring核心思想

众所周知,Spring拥有两大特性:IOC和AOP。IOC,英文全称Inversion of Control,意为控制反转。AOP,英文全称Aspect-Oriented Programming,意为面向切面编程。

下面,我们简要说明下这两大特性。


2.1、IOC

IoC Inversion of Control (控制反转/反转控制),注意它是⼀个技术思想,不是⼀个技术实现。

为什么叫做控制反转?
控制:指的是对象创建(实例化、管理)的权利
反转:控制权交给外部环境了(spring框架、IoC容器)

2.1.1、IOC优点

举个例子: 传统调用一个接口:一个请求进来需要创建一个userDao

Spring IOC和AOP的原理和实例_第2张图片

相比于传统的开发:需要什么new对象,避免了重复创建相同的对象,解决了对象之间的耦合问题。


2.2.2、IOC与DI的区别

DI:Dependancy Injection(依赖注⼊)

IOC和DI描述的是同⼀件事情,只不过⻆度不⼀样罢了

Spring IOC和AOP的原理和实例_第3张图片


2.2.3、实战

2.2.3.1、 策略工厂模式


2.2.3.2、ApplicationContent上下文获取Service


2.2.3.3、双数据库


2.2、AOP

AOP,面向切面编程。

在开发中,为了给业务方法中增加日志记录,权限检查,事务控制等功能,此时我们需要在修改业务方法内添加这些零散的功能代码(横切面关注点)。

由于这些功能的关注点不属于业务范围,应该 业务代码中剥离出来.这时候就可以考虑AOP了。
我们可以把这些零散的功能代码放到某个模块中,简称切面。 例如:日志切面就是关注日志的模块,所以说 切面的目的是:功能的增强。

Spring IOC和AOP的原理和实例_第4张图片


2.2.1、AOP术语

Spring IOC和AOP的原理和实例_第5张图片


2.2.2、Advice增强

Spring IOC和AOP的原理和实例_第6张图片

import org.checkerframework.checker.units.qual.A;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 模拟记录⽇志
 */
@Component
@Aspect
public class LogUtil {
    /**
     * 我们在xml中已经使⽤了通⽤切⼊点表达式,供多个切⾯使⽤,那么在注解中如何使⽤呢?
     * 第⼀步:编写⼀个⽅法
     * 第⼆步:在⽅法使⽤@Pointcut注解
     * 第三步:给注解的value属性提供切⼊点表达式
     * 细节:
     * 1.在引⽤切⼊点表达式时,必须是⽅法名+(),例如"pointcut()"。
     * 2.在当前切⾯中使⽤,可以直接写⽅法名。在其他切⾯中使⽤必须是全限定⽅法名。
     */
    @Pointcut("execution(* com.lagou.service.impl.*.*(..))")
    public void pointcut(){
    }
    
    @Before("pointcut()")
    public void beforePrintLog(JoinPoint jp){
        Object[] args = jp.getArgs();
        System.out.println("前置通知:beforePrintLog,参数是:"+
                Arrays.toString(args));
    }
    @AfterReturning(value = "pointcut()",returning = "rtValue")
    public void afterReturningPrintLog(Object rtValue){
        System.out.println("后置通知:afterReturningPrintLog,返回值
                是:"+rtValue);
    }
    @AfterThrowing(value = "pointcut()",throwing = "e")
    public void afterThrowingPrintLog(Throwable e){
        System.out.println("异常通知:afterThrowingPrintLog,异常是:"+e);
    }
    @After("pointcut()")
    public void afterPrintLog(){
        System.out.println("最终通知:afterPrintLog");
    }
    /**
     * 环绕通知
     * @param pjp
     * @return
     */
    @Around("pointcut()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        //定义返回值
        Object rtValue = null;
        try{
            //前置通知
            System.out.println("前置通知");
            //1.获取参数
            Object[] args = pjp.getArgs();

            //2.执⾏切⼊点⽅法
            rtValue = pjp.proceed(args);
            //后置通知
            System.out.println("后置通知");
        }catch (Throwable t){
            //异常通知
            System.out.println("异常通知");
            t.printStackTrace();
        }finally {
            //最终通知
            System.out.println("最终通知");
        }
        return rtValue;
    }
}

2.2.3、实现原理

AOP的底层是通过反射创建代理对象的方式实现的。

类型 介绍
静态代理 由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了
动态代理 在程序运行时,运用反射机制动态创建而成,无需手动编写代码

代理对象可以分为静态代理和动态代理,由上可知,AOP是通过动态代理的方式实现的。

java动态代理又分为jdk动态代理和cglib动态代理。

代理方式 描述
JDK动态代理 要求目标对象实现一个接口
CGLIB动态代理 它是在内存中构建构建一个子类对象从而实现对目标对象功能的扩展

区别:

  • JDK动态代理是自带的,CGLIB需要引入第三方包
  • CGLIB动态代理基于继承来实现代理,所有无法对final类、private和static方法实现代理

AOP动态代理默认策略:

  • 目标对象实现了接口,则默认使用JDK动态代理
  • 目标对象没有实现接口,则默认使用CGLIB动态代理
  • 目标对象实现了接口,程序里面依旧可以指定使用CGLIB动态代理
@SpringBootApplication
//强制使用cglib代理
@EnableAspectJAutoProxy(proxyTargetClass = tree)
public class AopDemoApplication{
    public static void main(String[] args){
        SpringApplication.run(AopDemoApplication.class,args);
    }
}

2.2.4、扩展:动态代理

2.2.4.1、jdk动态代理


2.2.4.2、cglib动态代理


2.2.3、实战

2.2.3.1、事务注解@Transactions

不恰当的使用此注解,导致不起作用,异常事务无法回滚

  • 1.注解@transaction应用在非public修饰的方法上

  • 2.注解@Transactional 注解属性 propagation 设置错误,若是错误的配置以下三种propagation,事务将不会发生回滚。
    a、 TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    b、 TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    c、 TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

  • 3.注解@Transactional 注解属性 rollbackFor 设置错误.rollbackFor 可以指定能够触发事务回滚的异常类型。Spring 默认抛出了未检查 unchecked 异常(继承自 RuntimeException 的异常)或者 Error 才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor 属性。

  • 4.同一个类中方法调用.

  • 5.异常被 catch捕获了.

  • 6.数据库引擎不支持事务。(仅InnoDB支持)

  • 7.被final、static关键字修饰的类或方法。CGLIB是通过生成目标类子类的方式生成代理类的,被final、static修饰后,无法继承父类与父类的方法。

  • 8.多线程环境下,事务的信息都是独立的.


多线程事务bug 举例:

主线程A调用线程B保存Id为1的数据,然后主线程A等待线程B执行完成再通过线程A查询id为1的数据。

这时你会发现在主线程A中无法查询到id为1的数据。因为这两个线程在不同的Spring事务中,本质上会导致它们在Mysql中存在不同的事务中。

Mysql中通过MVCC保证了线程在快照读时只读取小于当前事务号的数据,在线程B显然事务号是大于线程A的,因此查询不到数据。


正确使用事务代码示例:

/** 
* 异步代码写到别的地方,不要在同一个类中
*/
@RestController
@Slf4j
public class ResolveServiceImpl {

    @Autowired
    private MemberServiceManage memberServiceManage;

    @GetMapping("/resolve2")
    public String addUser() {
        log.info(">>>流程1");
        memberServiceManage.addUserLog();
        log.info(">>>流程3");
        return "success";
    }
}

-----------------------------------------------------------------------------------

@Component
@Slf4j
public class MemberServiceManage {
    @Transactions
    public String addUserLog() {
        try {
            Thread.sleep(1000);
        //自定义异常捕获,其他使用事务处理
        } catch (BizException e) {

        }
        log.info(">>>流程2");
        return "success";
    }
}

2.2.3.2、异步注解@Async

不恰当的使用此注解,导致不起作用:

  • 1.注解@Async的方法不是public方法
  • 2.注解@Async的返回值只能为void或Future
  • 3.注解@Async方法使用static修饰也会失效
  • 4.spring无法扫描到异步类,没加注解@Async或@EnableAsync注解
  • 5.调用方与被调用方不能在同一个类
  • 6.类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
  • 7.在Async方法上标注@Transactional是没用的.但在Async方法调用的方法上标注@Transcational是有效的。

代码示例:

/** 
* 异步代码写到别的地方,不要在同一个类中
*/
@RestController
@Slf4j
public class ResolveServiceImpl {

    @Autowired
    private MemberServiceManage memberServiceManage;

    @GetMapping("/resolve2")
    public String addUser() {
        log.info(">>>流程1");
        memberServiceManage.addUserLog();
        log.info(">>>流程3");
        return "success";
    }
}

-----------------------------------------------------------------------------------

@Component
@Slf4j
public class MemberServiceManage {
    @Async
    public String addUserLog() {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {

        }
        log.info(">>>流程2");
        return "success";
    }
}

2.2.3.3、 本地缓存

多个地方需要更新本地缓存,为避免代码重复,功能解耦,使用切面更新本地缓存。

定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredCache {}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClearCache {}


模拟缓存

@Component
public class SimpleCache {
	  //封装Map对象
	  private Map<Object,Object> cache=new ConcurrentHashMap<>();
	  //添加构造方法调用此方法并传入查询到的数据,将此数据存入Cache
	  public boolean putObject(Object key,Object value) {
		  cache.put(key, value);
		  return true;
	  }
	  //传入key取出Cache中的值
	  public Object getObject(Object key) {
		  return cache.get(key);
	  }
	  //清空Cache
	  public void clearObject() {
		  cache.clear();
	  }


切面

@Aspect
@Component
public class SysCacheAspect {
	@Autowired
	private SimpleCache simpleCache;
	
  // --------------------------------写缓存注解的方法--------------------------------
  
   @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)")
   public void doCachePointCut() {}

   @Around("doCachePointCut()")
   public Object around(ProceedingJoinPoint jp)throws Throwable{
	   Object obj=simpleCache.getObject("deptCache");//这个key的名字先自己写个固定值
	   if(obj!=null)return obj;
	   Object result=jp.proceed();//最终要执行目标方法
	   simpleCache.putObject("deptCache", result);
	   return result;
   }
   
  // --------------------------------清缓存注解的方法--------------------------------
  
   @Pointcut("@annotation(com.cy.pj.common.annotation.ClearCache)")
   public void doClearCachePointCut() {}

   /**
    * 定义@AfterReturning通知,原方法执行完以后执行,如果有After则先执行AfterReturning
    * 此处切入点要定义在更新 新增 删除 方法上,因为缓存内数据需要更新
    */
   @AfterReturning("doClearCachePointCut()")
   public void doAfterReturning() {//目标方法正常结束以后执行
		   simpleCache.clearObject();
   }

}

使用

public interface ResolveService {

/**
 * 缓存
 */
 @RequiredCache 
 String writeCache();
}



@Service
@Slf4j
public class Resolve1ServiceImpl implements ResolveService {

	
    public String writeCache() {
        log.info(">>>流程1");
        // doSomething 业务逻辑
        // 写缓存
        log.info(">>>流程3");
        return "success";
    }

}



@Service
@Slf4j
public class WorkServiceImpl implements ResolveService, WorkService {

	
    public String work() {
        log.info(">>>流程1");
        // doSomething 业务逻辑
        // 写缓存
        this.writeCache();
        log.info(">>>流程2");
        return "success";
    }
	
	public String writeCache() {
        log.info(">>>流程3");
        // doOtherthing 业务逻辑
        log.info(">>>流程4");
        return "success";
    }
}

由于数据量比较大,多节点处理时,不仅需要分页处理数据,为保证数据的一致性,还需要在切面里增加分布式锁,需要使用@before。

为啥使用切面?
1、更新本地缓存业务逻辑,高内聚。
2、本地缓存业务逻辑,存在并发问题,存在环绕业务逻辑的现象,代码冗余,单中间存在差异。


2.2.3.4、入参去空格拦截器


参考资料:
1、@Async注解的失效之谜
2、@Transactional 注解的失效场景(简述)
3、AOP缓存实现

你可能感兴趣的:(java基础,spring,java)