Spring(二)-Spring AOP

作者: sixleaves | 来源:发表于2018-08-24 23:54 被阅读45次

    1. Spring AOP

    回顾

    Spring(一)中,我分享了

    • Spring的发展
    • IoC容器相关
      • IoC思想和其要解决的问题
      • 我们如何使用Spring中的IoC容器
      • 如何交出创建对象的控制权给Spring
      • 基于IoC容器的四种对象的实例化方式、对象的生命周期(初始化、销毁)、作用域研究.(这里的对象指的是JavaBean)
    • DI相关
      • DI的概念和以及DI和IoC的关系
      • DI注入的几种方式、属性注入、构造器注入
      • 如何引入属性文件,转交Druid数据库连接池给容器

    今天分享啥

    今天要分享的主要是关于Spring AOP, 即Spring的面向切面编程。再聊Spring AOP之前, 我会先分享一下AOP思想、由AOP思想我们再来聊聊如何实现AOP,也就是实现AOP的三种方式。了解完AOP思想和其实现,我们再来聊实际开发中我们如何使用Spring实现AOP,即Spring中实现AOP的方式。下面列下这片博客要分享的内容。

    • DI相关-基于注解的自动装配
    • AOP思想
    • 实现AOP的三种方式
    • Spring中实现AOP的方式

    上一篇博客中,主要是使用xml进行转交控制权,告诉IoC容器如何注入
    还有一种基于注解的方式进行DI,也是现在开发中最常用的方式,所以我们先来了解下DI相关-基于注解的自动装配

    1.1 DI相关-基于注解的自动装配

    首先我们要知道基于注解的自动装配其实是一种依赖注入。依赖注入我们即可以使用xml配置文件方式来告诉容器,如何去注入。也可以通过更简便的注解方式来告诉IoC容器,如何取注入。

    1.1.1 如何使用注解转交控制权并告诉容器如何注入

    关于这个问题,我们需要拆分成两个子问题。

    • 首先我们得告诉容器取哪些包下扫描注解
    • 其次我们的给我们要转交出控制权的类打上标记

    告诉Spring IoC容器去哪些包下扫描注解

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
        <context:component-scan base-package="com.sweetcs._09autowise_with_annotation"></context:component-scan>
        
    </beans>
    

    给要转交控制权的类打上标记

    既然要大标记,在Java中即可以使用注解技术实现.Spring中根据J2EE的分层开发规范,提供了四个注解用来表示类对应的层,而且一旦被打上注解,意味着这些类一旦被Spring IoC容器扫描到,Spring IoC容器就会接管这它们

    四个转交控制权注解

    这四个注解的共同作用,都是我们用来将转交控制权给Spring IoC容器。不同的是他们的含义(语义), 不同的名称用来标注不同层的组件, @Component可以用来标注所有层的组件。

    @Component注解

    四个注解中的第一个是Componet注解,表示组件的意思, 所以对于daoservice等web组件都可以打上@Component注解。
    表示这是一个组件的意思,并且将会托管给Spring IoC容器。

    @Repository注解

    该注解表示的是 数据仓库的意思,主要表示DAO层的对象。

    @Controller注解

    该注解表示的是控制器。例如Servlet就是控制器,其主要用来解析请求,处理请求,跳转。

    @Service注解

    该注解表示的是Service即业务逻辑层。

    案例

    下图为案例的项目代码结构图

    • 步骤一.告诉Spring IoC容器去哪些包下扫描注解
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
        <context:component-scan base-package="com.sweetcs._09autowise_with_annotation"></context:component-scan>
        
    </beans>
    
    • 步骤二.给要转交控制权的类打上标记
      UserDAOImpl和数据库直接交互做持久化,故而打上@Repository
    @Repository
    public class UserDAOImpl implements IUserDAO {
    
        @Override
        public void save() {
            // TODO Auto-generated method stub
            System.out.println("保存User对象到数据库");
        }
    }
    

    UserServiceImpl.业务逻辑层,故而打上@Service

    @Service
    @ToString
    public class UserServiceImpl implements IUserService {
    
        @Autowired
        @Setter
        private IUserDAO userDAO;
        
        @Override
        public void save() {
            // TODO Auto-generated method stub
            userDAO.save();
        }
    }
    

    测试代码
    告诉容器我们要什么对象,让它返回,我们测试十分容器给我们完成了注入。

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration
    public class AutoWiseWithAnnotationTest {
        
        @Autowired
        ApplicationContext ctx;
        
        @Test
        public void testDIWithAnnotation() {
            
            UserServiceImpl userServiceImpl = ctx.getBean("userServiceImpl", UserServiceImpl.class);
            userServiceImpl.save();
        }
    }
    

    输出

    可以看到我们使用注解的方式转交控制权是有效的,我们可以在测试用例中成功的通过Spring IoC获取到对应的对象,而不是我们自己创建的。

    关于@Autowired
    如果没有看过Spring(一)你可能并不了解这个注解的含义.这个注解等效于xml配置响应的bean的autowire属性.是用来告诉容器,我们需要IoC容器给我们注入一个对象
    至于是什么对象,它遵循以下算法.

    • 它会默认按照类型先去容器里寻找。
      • 找到一个。则直接拿该对象注入.
      • 找到多个。则直接抛出找到多个的异常,除非此时又有@Qui注解,用来表示按配置中的Bean ID去找。
    • 如果没有抛出找到多个的异常.则会将该属性名首字母小写作为Bean ID去容器里寻找。

    接下去我们要来分享今天的重头戏了,AOP!!!

    1.2 AOP

    我们在Spring(一)中的简介Spring中有提到Spring基于IoCAOP思想从而实现了解耦。
    那么什么是AOP?AOP(Aspect Oriented Programming)的中文意思是面向切面编程.这是一种很专业的称呼,我们先来看看AOP的思想。

    AOP思想

    我们先来看看维基百科对AOP的定义

    In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior to existing code (an advice) without modifying the code itself, instead separately specifying which code is modified via a "pointcut" specification, such as "log all function calls when the function's name begins with 'set'". This allows behaviors that are not central to the business logic (such as logging) to be added to a program without cluttering the code, core to the functionality. AOP forms a basis for aspect-oriented software development.

    上述中有几个关键术语

    • advice

    advice是AOP范式编程中的术语,其是要来描述某些类中的方法, 这些方法是用来修改其他类的定义的方法功能。这些方法会在连接点处被加入, 从而实现方法增强

    • pointcut(切入点)

    所谓的切入点就是我们要切入的是哪些类.

    基于维基百科的介绍, AOP的思想分离程序中交叉在一起的关注点,而每个关注点对应可以说是一段功能,AOP的想法就是将每个关注点模块化,使得他们独立无不干扰。从而具备职责分明可插拔互不干扰等特点。因为具备这些特点,所以能让代码具备很强的拓展性维护性.(因为可插拔,要增加一项功能,我们就能将其模块化, 插入原来的程序.不要的时候可以直接删除,对程序没有任何影响).

    我们可以用图来更加形象的说明AOP,如下图。

    • 每一个独立功能都看成是一个圆圈, 由这些圆圈就组成了面。程序运行的时候是由外到内,通过一个个面,直到到达红心部分, 再从红心部分向外执行。
    • 红心部分代表着我们的业务逻辑.我们的业务逻辑是被包裹在最内部, 每像外一层就给我们的业务逻辑上多加了一层的功能。外面的这些层相当于一个个的代理,负责不同的功能。

    AOP的三种实现方式

    基于AOP的想法,在Java中我们有主要的三种方式可以来实现AOP.

    1.装饰者模式

    装饰者模式的思想:
    不更改原有类的代码,动态的扩展一个对象的功能, 也就是对对象进行功能争强。
    为什么装饰者模式能实现AOP:
    通过对装饰者模式,我们也可以对将我们的业务逻辑代码和其他非业务逻辑代码做分离, 让装饰者作为代理来负责对应的功能, 而分离出去的代码如何修改争强,我们都不用去懂我们的业务逻辑代码,这正好符合我们AOP想要的。

    说的太抽象,我们直接看具体的场景和代码.

    假设我们现在有需要设计一个EmployeeServiceWrapper即EmployeeService的一个包装类,对它做功能上的争强,增加处理事务的能力

    • 首先包装类要实现和被包装类一样的接口,这样第三方使用的时候才能兼容。
    • 包装类要依赖被包装类对象具有所需要的功能对象,所以其需要有一个被包装类的成员变量,和一个具有该功能的对象,当然如果没有可以在包装类中直接实现该功能。

    使用装饰者模式实现的EmpoyeeServiceWrapper

    import lombok.Setter;
    
    @Setter
    public class EmployeeServiceWrapper implements IEmployeeService {
    
        // 被争强的真是对象
        private IEmployeeService target;
        // 提供管理事务的对象.
        private TxManager tx;
        
        
        
        
        @Override
        public void save() {
            
            try {
                tx.open();
                target.save();
                tx.commit();
            } catch (Exception e) {
                // TODO: handle exception
                tx.rollBack();
            }
        }
    
        @Override
        public void update() {
            
            try {
                tx.open();
                target.update();
                tx.commit();
            } catch (Exception e) {
                // TODO: handle exception
                tx.rollBack();
            }
    
        }
        public EmployeeServiceWrapper(IEmployeeService target, TxManager tx) {
            super();
            this.target = target;
            this.tx = tx;
        }
    }
    

    被代理类EmployeeServiceImpl

    @Setter
    public class EmployeeServiceImpl implements IEmployeeService {
        
        
        
        public IEmployeeDAO getEmployeeDAO() {
            return employeeDAO;
        }
    
        public void setEmployeeDAO(IEmployeeDAO employeeDAO) {
            this.employeeDAO = employeeDAO;
        }
    
        private IEmployeeDAO employeeDAO;
        
        @Override
        public void save() {
            employeeDAO.save();
            // TODO Auto-generated method stub
            
        }
    
        @Override
        public void update() {
            employeeDAO.update();
            // TODO Auto-generated method stub
            
        }   
    }
    

    模拟事务管理的类TxManager

    public class TxManager {
    
        public void open() {
            // TODO Auto-generated method stub
            System.out.println("开启事务");
        }
    
        public void commit() {
            // TODO Auto-generated method stub
            System.out.println("提交事务");
        }
    
        public void rollBack() {
            // TODO Auto-generated method stub
            System.out.println("回滚事务");
        }
    
    }
    

    测试代码, EmployeeServiceWrapperTest

    public class EmployeeServiceWrapperTest {
        
        @Test
        public void testEmplyeeServiceWrapper() {
            
            IEmployeeDAO employeeDAO = new EmployeeDAOImpl();
            IEmployeeService employeeService = new EmployeeServiceImpl();
            ((EmployeeServiceImpl)employeeService).setEmployeeDAO(employeeDAO);
            
            TxManager tx = new TxManager();
            IEmployeeService wrapper = new EmployeeServiceWrapper(employeeService, tx);
            
            wrapper.save();
            System.out.println();
            wrapper.update();
        }
    }
    

    输出

    可见包装类并没有改变原有真实类的功能,还提供了事务管理功能.所以使用这种方式,我们也实现了AOP,但是这种AOP还是十分不灵活.

    基于装饰者模式实现AOP的缺陷
    • 如果我还有100个类需要增强功能,则需要再创建100个对应的包装类。
    • 包装类的这种方式,将被包装类完全的暴露给了调用者。从编码角度来说,容易因为调用成了被包装类(目标类)而导致程序功能异常。

    2.静态代理

    静态代理中真实角色必须是实现已经存在的,并将其作为代理对象的内部属性.

    • 实现同样的接口.
    • 在静态代理内部new真实角色对象(被代理对象)

    StaticProxyEmployeeService

    public class StaticProxyEmployeeService implements IEmployeeService {
    
        // 目标类\被代理类\实际类 会在静态代理中创建,不会在外暴露.
        private IEmployeeService target;
        private TxManager tx = new TxManager();
        
        @Override
        public void save() {
            try {
                tx.open();
                
                if (null == target) {
                    target = new EmployeeServiceImpl();
                    ((EmployeeServiceImpl)target).setEmployeeDAO(new EmployeeDAOImpl());
                }
                target.save();
                
                tx.commit();
            } catch (Exception e) {
                // TODO: handle exception
                tx.rollBack();
            }   
        }
    
        @Override
        public void update() {
            try {
                tx.open();
                
                if (null == target) {
                    target = new EmployeeServiceImpl();
                }
                target.update();
                
                tx.commit();
            } catch (Exception e) {
                // TODO: handle exception
                tx.rollBack();
            }   
        }
    }
    

    测试用例

        @Test
        public void testStaticProxy() {
            
            IEmployeeService employeeService = new StaticProxyEmployeeService();
            employeeService.save();
            System.out.println();
            employeeService.update();
            
        }
    

    输出

    静态代理和装饰者模式的区别

    相同点

    • 他们都是要实现一样的接口
    • 他们都要维护被代理对象具备争强功能的对象

    不同点

    • 静态代理对外不暴露出真实的被包装类。将其隐藏了起来,包含被真是对象的安全.而装饰者模式是暴露的
    静态代理存在的缺陷
    • 和装饰者模式一样,还不够灵活, 如果需要增强的有100个类,我们还要声明。

    3.动态代理

    动态代理在Java中有两种实现如下,这里我们主要介绍基于JDK的动态代理

    • 基于JDK的动态代理
    • 基于cglib的动态代理

    基于JDK的动态代理是在JVM的运行时的时候通过反射技术动态创建出来,不存在其对应的字节码文件。那么我们要如何来告诉JDK我们要创建的动态代理怎么增加我们想要的功能呢?

    必须明确的前提

    首先, 我们可以明确的是我们创建出来的代理对象必须是实现真实类所实现的接口,因为我们是对真实类增强

    如何让动态代理来做增强
    • JDK的动态代理是在JVN的运行时通过反射技术动态创建的,根据这个条件,我们肯定不可能直接定义一个动态代理类, 并自己创建。
    • 所以JDK必定提供了相应的实现, 让我们能够动态的创造出一个实现了实际类的接口的代理对象,将这个对象返回给我们。
    • 但是JDK并不知道我们要如何去增强对象,在什么什么时候进行争强,所以JDK又提供了一个InvocationHandler接口,让我们来实现,实现接口可订制增强功能, 这个类就是具备增加强功能的类。
    3.1 怎么创建动态代理对象出来

    基于上述的讨论我们提出了这个问题, JDK中确实提供了java.lang.reflect.Proxy用于给我们创建动态代理。

        public static Object newProxyInstance(ClassLoader loader,
                                              Class<?>[] interfaces,
                                              InvocationHandler h)
    
    • loader 传的是真实类的ClassLoader
    • interfaces 传的是真实类实现的接口, 可以通过反射获取
    • h 传的就是实现InvocationHandler的对象
    3.2 怎么对真实对象做增强操作

    JDK中还提供了java.lang.reflect.InvocationHandler接口,实现该接口类就是具备增强功能的类。JDK会去调用该对象的invoke方法.

    3.3 代码演示
    • 争强类还是必维护target和具备争强功能的对象,并且要实现InvocationHandler接口。
    • 增强类还需要提供代理对象的获取方法。
    public class EmployeeServiceHandler implements InvocationHandler {
    
        
        public void setTarget(Object target) {
            this.target = target;
        }
    
        public void setTx(TxManager tx) {
            this.tx = tx;
        }
    
        private Object target;
        private TxManager tx;
        
        
        public <T> T getProxyObject() {
            return (T) Proxy.newProxyInstance(
                    target.getClass().getClassLoader(), 
                    target.getClass().getInterfaces(), 
                    this);
        }
        
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
            Object ret = nullValue();
            try {
                tx.open();
                ret = method.invoke(target, args);
                tx.commit();
                
            } catch (Exception e) {
    
                tx.rollBack();
            }
            return ret;
        }
    
    }
    

    Bean的xml配置

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        
        <bean id="employeeDAO" class="com.sweetcs._10aop.commons.dao.impl.EmployeeDAOImpl"></bean>
    
        <bean id="employeeServiceProxy" class="com.sweetcs._10aop.jdk_proxy.EmployeeServiceHandler">
            <property name="target">
                <bean class="com.sweetcs._10aop.commons.service.impl.EmployeeServiceImpl">
                    <property name="employeeDAO" ref="employeeDAO"></property>
                </bean>
            </property>
            <property name="tx">
                <bean class="com.sweetcs._10aop.commons.transition.TxManager"></bean>
            </property>
        </bean>
    </beans>
    

    测试用例

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration
    public class ProxyTest {
    
        @Autowired
        EmployeeServiceHandler serviceHandler;
        
        @Test
        public void testCreateEmployeeService() {
            
            IEmployeeService service = serviceHandler.getProxyObject();
            service.save();
            
            service.update();
        }
    }
    

    输出

    1.4 Spring实现AOP的方式

    接着我们就来看看利用Spring中实现AOP。

    相关文章

      网友评论

      本文标题:Spring(二)-Spring AOP

      本文链接:https://www.haomeiwen.com/subject/tfzyiftx.html