全栈开发实战 | Spring框架快速入门第二篇

 一个人最好的状态:梦想藏在心里,行动落于腿脚。

目录

1、前言

2、代理

2.1 什么是代理?

2.2 简单理解代理

2.3 静态代理

2.4 CGLIB代理

2.5 AOP

2.6 AOP入门

2.7 AOP通知类型

2.8 语法详解


1、前言

        上一篇我们已经简单学习了Spring框架的Core模块,这一篇我们主要讲解一下Spring的Aop模块

        在开始之前,我们首先要开始了解一下静态代理和动态代理的相关知识

2、代理

2.1 什么是代理?

代理是设计模式的一种,其原理就是通过代理对象去访问目标对象,并且外部只能访问到代理对象

也就是说可以在目标对象实现的基础上,通过代理对象扩展目标对象的功能

2.2 简单理解代理

  • 假如我现在由一名程序员转行成为了一名家喻户晓的歌手(哈哈哈~~~),粉丝想听我唱歌,就得想办法找到我的经纪人,告诉经纪人想听我唱歌

  • 后来我开了演唱会,粉丝们想要听歌就需要买票,于是经纪人告诉粉丝:"只有花钱买上票,才能进去听歌"

  • 无论外界想要我干什么,都得经过我的经纪人

    这其中的经纪人就是代理,目标对象就是我,真正唱歌的人也是我

2.3 静态代理

  • 创建IUser的接口,并且拥有save( )方法

public interface IUser {
    void save();
}
  • 编写IUser的实现类UserDao,并且重写save( )方法

public class UserDao implements  IUser{
    public void save(){
        System.out.println("保存用户");
    }
}
  • 为了测试在save()方法前后添加开启事务和关闭事务的方法

public class UserDao implements  IUser{
    public void save(){
        System.out.println("开启事务");
        System.out.println("保存用户");
        System.out.println("关闭事务");
    }
}

思考:如果我有很多很多的业务方法都需要开启事务,关闭事务呢?

   这样就会导致非常多的重复代码,阅读代码非常混乱

   我们需要解决的就是当用户调用UserDao方法的时候,找到的是代理对象,是它在帮我解决这些繁琐的代理

  • 代理对象需要实现IUser接口,这样代理对象就和目标对象拥有相同的方法了

public class UserDaoProxy  implements  IUser{
    //接收保存目标对象
    private IUser target;
    public UserDaoProxy(IUser target) {
        this.target = target;
    }
    @Override
    public void save() {
        System.out.println("开启事务");
        System.out.println("保存用户");
        System.out.println("关闭事务");
    }
}

测试代码​​​​​​

public static void main(String[] args) {
   
   
        IUser userDao = (IUser) new UserDao();        IUser proxy = new UserDaoProxy(userDao);        proxy.save();    }

这样,我们的UserDao就不需要写那些繁琐的代码,静态代理是不是so easy!

注意:不能用接口的实现类(UserDao)来转换Proxy的实现类,它们是同级,应该用共同的接口来转换。

为什么要用动态代理?
先总结一下静态代理的不足

  • 接口如果被修改后,代理对象也要跟着去修改,大量改动的情况下可想而知

  • 静态代理需要代理对象去实现目标对象的接口,这就导致会出现大量的代理类,会造成代码混乱

态代理的优势

  • 代理对象,不需要实现目标对象接口

  • 代理对象的生成是利用JDKAPI,动态的在内存中构建代理对象

动态代理

  • 新建一个Person接口

public interface Person {
    void sing(String name);
    void dance(String name);
}
  • 编写Person的实现类Juzi,并且重写接口的所有方法

public class Juzi  implements Person{
    @Override
    public void sing(String name) {
        System.out.println("桔子唱"+name);
    }
    @Override
    public void dance(String name) {
        System.out.println("桔子跳"+name);
    }
}
  • 代理类代码如下:

public class JuziProxy {
    //目标对象
    Juzi juzi =  new Juzi();
    public Person getProxy(){
        /**
         * 参数一:代理类的类加载器
         * 参数二:目标对象的接口
         * 参数三:InvocationHandler实现类
         */
        return (Person) Proxy.newProxyInstance(JuziProxy.class.getClassLoader(), juzi.getClass().getInterfaces(), new InvocationHandler() {
            /**
             * @param o  目标对象
             * @param method  目标对象当前调用的方法
             * @param objects 方法的参数
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                if(method.getName().equals("sing")){
                    System.out.println("买票再来听歌...");
                    method.invoke(juzi,objects);
                }
                return null;
            }
        });
    }
}

Proxy是JAVA提供的一个类,调用它的newProxyInstance方法可以生成某个对象的代理对象,具体的参数在代码中已经注释说明

  • 测试代码如下:

public static void main(String[] args) {
        JuziProxy proxy = new JuziProxy();
        Person person = proxy.getProxy();
        person.sing("我爱你");
}
  • 输出结果如下:

 思考

上面我们介绍了JDK的静态代理、动态代理的特点和区别,他们有一个共同的限制条件,就是只能为有接口的类创建代理对象,而对于没有通过接口实现的类,如何创建代理对象呢?

答案就是我们接下来要介绍的CGLIB代理

2.4 CGLIB代理

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB通过继承方式实现代理,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。

简单的说也就是如果被代理的类不是一个实现类(没有接口实现),那么Spring会通过字节码底层继承要代理类来实现,因此也被称为子类代理。

  • 编写代理对象工具类

//需要实现MethodInterceptor接口
public class ProxyFactory implements MethodInterceptor{
    //目标对象
    private static  Object target;
    public ProxyFactory(Object target) {
        this.target = target;
    }
    //为目标对象创建代理对象(也就是创建它的子类来扩展)
    public  Object getProxyInstance(){
        // 1. 工具类
        Enhancer en = new Enhancer();
        // 2. 设置父类(父类就是目标对象)
        en.setSuperclass(target.getClass());
        // 3. 设置回调函数
        en.setCallback(this);
        // 4. 创建子类(代理对象)
        return en.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("开始事务...");
        //执行目标对象的方法
        methodProxy.invokeSuper(o,objects);
        System.out.println("提交事务...");
        return null;
    }
}
  • 测试代码

 public static void main(String[] args) {
        UserDao userDao = new UserDao();
        System.out.println(userDao.getClass());
     
        UserDao o = (UserDao) new ProxyFactory(userDao).getProxyInstance();
        System.out.println(o.getClass());
}

输出结果

 我们可以看到控制台输出的内容,第一行是目标对象的原始类型,第二行是目标对象的代理类型

上面我们对代理有了一定的认识,现在我们在这个基础上进入到Spring的Aop模块

2.5 AOP

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面理解为一个动态过程(在对象运行时动态织入一些扩展功能或控制对象执行)。

 AOP应用场景

在我们的开发过程中,实际项目通常会将系统分为两大部分,一部分是核心业务,一部分是非核心业务;

在编程中我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般是借助AOP来实现的

AOP就是基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以"控制"对象的执行。

AOP相关术语

  • 通知(Advice): 想要实现的功能,例如事务、日志等(说明要干什么和什么时候干)

  • 连接点(JoinPoint): 执行通知的地方,一般指被拦截到的方法

  • 切入点(PointCut):  通过切入点表达式来指定拦截哪些类的哪些方法(说明在哪干)

  • 切面(Aspect): 通知和切入点的集合,借助@Aspect声明

2.6 AOP入门

  • 在pom.xml文件中导入所需要的的jar包(之前第一篇文章已经导入过)

<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>5.1.5.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
    <dependency>
      <groupId>aopalliance</groupId>
      <artifactId>aopalliance</artifactId>
      <version>1.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>1.9.2</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.2</version>
      <scope>runtime</scope>
    </dependency>

Spring框架可以使用一下两种方法来实现AOP

  • 注解方式实现AOP编程

  • XML方式实现AOP编程

接下来我们分别介绍

第一种:注解方式实现AOP编程

  • 在配置文件中开启AOP注解方式

<!--开启注解扫描器-->
<context:component-scan base-package="com.cn"></context:component-scan>
 <!--第一种:注解方式实现aop编程-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
  • 编辑切面代码

@Component
@Aspect //指定为切面类
public class AOP {
    //里面值为切入点表达式
    @Before("execution(* com.cn.controller.*.*(..))")
    public void begin() {
        System.out.println("开始事务");
    }
    @After("execution(* com.cn.controller.*.*(..))")
    public void close() {
        System.out.println("关闭事务");
    }
}

我们先测试目标对象有接口的代码,看一下 输出结果

  • 编辑IUser接口代码

public interface IUser {
    void save();
}
  • UserDao实现了IUser接口(目标对象有接口)

@Component
public class UserDao implements IUser{
    public void save(){
        System.out.println("保存用户");
    }
}
  • 测试代码

 public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //目标对象有接口是动态代理
        IUser iUser = (IUser) context.getBean("userDao");
        System.out.println(iUser.getClass());
        iUser.save();
}
  • 输出结果

 接下来我们测试目标对象没有接口的代码,看一下 输出结果

  • OrderDao代码(目标对象没有接口)

@Component
public class OrderDao {
    public void save(){
        System.out.println("保存商品");
    }
}
  • 测试代码

 public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
       //目标对象没有接口是CGLIB动态代理
        OrderDao orderDao = (OrderDao) context.getBean("orderDao");
        System.out.println(orderDao.getClass());
        orderDao.save();
}
  • 输出结果

2.7 AOP通知类型

  • @Before    前置通知:目标方法之前执行

  • @After    后置通知:目标方法之后执行

  • @AfterReturning    返回通知:执行方法结束之前通知 

  • @AfterTrowing    异常通知:出现异常时候执行

  • @Around    环绕通知:环绕目标方法执行

执行顺序如下图:

 代码如下:

@Component
@Aspect //指定为切面类
public class AOP {
    // 前置通知 : 在执⾏⽬标⽅法之前执⾏
    @Before("execution(* com.cn.controller.*.*(..))")
    public void begin(){
        System.out.println("开始事务");
    }
    // 后置/最终通知:在执⾏⽬标⽅法之后执⾏ 【⽆论是否出现异常最终都会执⾏】
    @After("execution(* com.cn.controller.*.*(..))")
    public void close(){
        System.out.println("结束事务");
    }
    // 返回后通知:在调⽤⽬标⽅法结束后执⾏ 【出现异常不执⾏】
    @AfterReturning("execution(* com.cn.controller.*.*(..))")
    public void afterReturning() {
        System.out.println("afterReturning()");
    }
    // 异常通知:当⽬标⽅法执⾏异常时候执⾏
    @AfterThrowing("execution(* com.cn.controller.*.*(..))")
    public void afterThrowing(){
        System.out.println("afterThrowing()");
    }
    //环绕通知:环绕目标方法执行
    @Around("execution(* com.cn.controller.*.*(..))()")
    public  void  around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕前...");
        joinPoint.proceed();
        System.out.println("环绕后...");
    }
}

代码优化:

思考:每次在我们写通知类型的时候就要重新写一遍切入点表达式,有没有什么方法可以简便一些,让代码看起来更加的优雅?

这里可以用@Pointcut注解,来指定切入点表达式,这样我们在用到的时候直接引用就可以。

第二种:XML方式实现AOP编程

  • xml文件配置

<!--第二种:xml方式实现aop编程-->
    <!--对象实例-->
    <bean id="userDao" class="com.cn.controller.UserDao"></bean>
    <bean id="orderDao" class="com.cn.controller.OrderDao"></bean>
    <!--切面类-->
    <bean id="aop" class="com.cn.proxy.AOP"></bean>
    <!--aop配置-->
    <aop:config>
        <!--定义切入点表达式-->
        <aop:pointcut id="pointCut" expression="execution(* com.cn.controller.*.*(..))"></aop:pointcut>
        <!--指定切面类-->
        <aop:aspect ref="aop">
            <!--指定切面类的方法-->
            <aop:before method="begin" pointcut-ref="pointCut"></aop:before>
            <aop:after method="close" pointcut-ref="pointCut"></aop:after>
        </aop:aspect>
    </aop:config>

测试代码和第一种注解方式实现AOP编程的代码一样,这里就不再粘贴

大家可以试一下,输出结果一样就证明我们的配置没问题

切入点表达式

主要就是用来配置拦截哪些类的哪些方法

指示符 作用
bean 同于匹配指定bean对象的所有方法
within 用于匹配指定包下的所有类内的所有方法
execution 用于按指定语法规则匹配到具体方法
@annotation 用于匹配指定注解修饰的方法

上面我们的切入点表达式使用的是execution,应用于方法级别,实现细粒度的切入点表达式定义

2.8 语法详解

我们用代码中的表达式进行介绍,例如:

execution(* com.cn.controller.*.*(..))

execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?) 

除了返回类型模式、方法名模式和参数模式外,其它项都是可选的

整个表达式分为五个部分:

  • execution(): 表达式主体

  •  第一个 * 号 : 表示返回类型,* 号代表返回所有类型

  • 包名 : 表示要拦截的包名,后面的点表示当前包下的所有类

  •  第二个 * 号 : 表示类名,* 号代表所有类

  • *(..)  :  最后这个*号表示方法名,*代表所有方法,括号里面代表方法的参数,两个点表示任何参数

下面介绍一些常见的切入点表达式例子,代码如下:

execution(public **(..))

以public修饰的,方法返回值任意,方法名任意,参数任意

execution(* set*(..))

任意返回值,以set开头的方法,参数任意

execution(* com.cn.controller.*.*(..))

任意返回值,com.cn.controller包下的所有类的所有方法,参数任意

execution(* com.cn.controller..*.*(..))

任意返回值,com.cn.controller包或子包下的所有类的所有方法,参数任意

AOP的局限

在我们理解了表达式的含义的基础上,也自然能看到上面代码表达式的局限性

思考:当我们使用切面时,就要去事先写好切入点表达式,如果后面项目不断维护,代码量也随之增加,那么切入点表达式就会显的不易阅读,有什么好的方法呢?

这时我们使用自定义注解(@annotation())来配合AOP,就能很好的解决这个问题。

自定义注解表达式

  • 新建一个自定义注解

/**
 * 自定义注解,一个特殊的类,所有注解都默认继承Annotation接口
 * @Retention(RetentionPolicy.RUNTIME) 表示在什么级别保存该注解信息
 * @Target(ElementType.METHOD)  表示该注解用于什么地方
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DemoAnnotation {
}
  • 编辑切面类代码

@Component
@Aspect //指定为切面类
public class AOP {
    //注解形式的切入点表达式
    @Pointcut("@annotation(com.cn.anno.DemoAnnotation)")
    private void pointCut_() {
    }
    //环绕通知:环绕目标方法执行
    @Around("pointCut_()")
    public  void  around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕前...");
        joinPoint.proceed();
        System.out.println("环绕后...");
    }
}
  • 使用自定义的注解@DemoAnnotation对业务中的目标对象的方法进行描述

@Component
public class OrderDao {
    @DemoAnnotation
    public void save(){
        System.out.println("保存商品");
    }
}
  • 测试代码

 public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
       //目标对象没有接口是CGLIB动态代理
        OrderDao orderDao = (OrderDao) context.getBean("orderDao");
        System.out.println(orderDao.getClass());
        orderDao.save();
}
  • 输出结果

总结:这样做的好处在于控制的粒度更细,也更加灵活,方便切面编程的实现和细分

猜你喜欢

转载自blog.csdn.net/vx1271487114/article/details/125737332