【Spring】IoC与AOP初体验

作者: 大数据阶梯之路 | 来源:发表于2019-01-27 21:37 被阅读39次

    说说Spring这个经典的框架,至今仍在流行使用,发展出来生态十分好,Spring是企业应用开发(特点:结构复杂、外部资源众多、事务密集、数据规模大、用户数量多、有较强的安全性考虑和较高的性能要求。)的“一站式”选择,贯穿表现层、业务层、持久层。而Spring的出现,并不是为了取代其他已有的框架,而是为了更好地和与其他框架进行无缝整合。Spring坚持一个原则:不重新发明轮子。

    一、IoC

    即Inversion of Control,俗称控制反转,也常被称为依赖注入 DI(Dependency Injection),主要是为了降低程序代码之间的耦合。
    依赖是什么?依赖指的是通过方法参数、局部变量、返回值等建立的对于其他对象的调用关系。(比如B类的方法通过传参把A类对象传递给B类,B类再进行调用A类的功能,这样就称为B类依赖A类。)

    如果程序耦合度太高,对程序的维护和扩展是十分不利的,我们或许可以通过工厂方法模式的思路来解决耦合高的问题,但大量的工厂类被引入开发过程中,明显会增加开发的工作量,毕竟过于应用设计模式来增加程序的灵活性的同时也会增加程序的复杂性。spring能够分担这些额外的工作,其提供完整的IoC实现,让我们可以专注于业务类和DAO类的设计。

    下面贴出一个IoC的小案例练习代码——打印机。

    一、先定义两个接口
    /**
    *@Author 小江  [com.zhbit]
    *@Date 2019/1/24 18:20
    *Description 墨盒颜色接口
    */
    public interface Ink {
        public String getColor(int r,int g,int b);
    }
    
    /**
     *@Author 小江  [com.zhbit]
     *@Date 2019/1/24 18:20
     *Description 纸张大小接口
     */
    public interface Paper {
        public static final String newLine = "\r\n";
    
        public void putInChar(char c);  //输出一个字符到纸张
        public String getContent();      //获得输出到纸张的内容
    }
    二、打印机类
    /**
    *@Author 小江  [com.zhbit]
    *@Date 2019/1/24 18:19
    *Description
    */
    public class Printer {
        //面向接口编程,而不是具体的实现类
        private Ink ink = null;
        private Paper paper = null;
    
        public void setInk(Ink ink) {
            this.ink = ink;
        }
        public void setPaper(Paper paper) {
            this.paper = paper;
        }
        //打印机打印方法
        public void print(String str){
            //输出颜色标记
            System.out.println("使用"+ink.getColor(255,200,0)+"颜色打印:\n");
            //循环将字符串输出到纸张
            for(int i=0;i<str.length();i++){
                paper.putInChar(str.charAt(i));
            }
            //将纸张内容输出
            System.out.print(paper.getContent());
        }
    }
    三、接口的具体实现类
    import java.awt.*;
    /**
    *@Author 小江  [com.zhbit]
    *@Date 2019/1/24 18:28
    *Description 彩色墨盒,Ink接口的具体实现类
    */
    public class ColorInk implements Ink {
        //打印采用颜色
        @Override
        public String getColor(int r, int g, int b) {
            Color color = new Color(r,g,b);
            //截取从2位置开始的rgb颜色再拼接上#
            return "#"+Integer.toHexString(color.getRGB()).substring(2);
        }
    }
    
    import java.awt.*;
    *@Author 小江  [com.zhbit]
    *@Date 2019/1/24 18:32
    *Description 灰色墨盒,Ink接口的具体实现类
    */
    public class GreyInk implements Ink {
        //打印采用灰色
        @Override
        public String getColor(int r, int g, int b) {
            int c = (r+g+b)/3;
            Color color = new Color(c,c,c);
            return "#"+Integer.toHexString(color.getRGB()).substring(2);
        }
    }
    
    /**
    *@Author 小江  [com.zhbit]
    *@Date 2019/1/24 23:22
    *Description 纸张大小类,Paper的具体实现类
    */
    public class TextPaper implements Paper {  
        private int cols = 20;          //列数
        private int rows = 8;           //行数
        private String content = "";    //内容
        private int posX = 0;           //横向偏移位置
        private int posY = 0;           //纵向偏移位置
        private int posP = 1;           //当前页数
    
        @Override
        public void putInChar(char c) {
            content += c;
            ++posX;
            //判断是否换行
            if(posX==cols){
                content += newLine;
                ++posY;
                posX = 0;
            }
            //判断是否翻页
            if(posY==rows){
                content += "===第"+posP+"页===";
                content += newLine+newLine;
                posY = 0;
                ++posP;
            }
        }
        @Override
        public String getContent() {
            String print = this.content;
            if(!(posX==0 && posY==0)){
                int count = rows-posY;
                //给每页的每行无内容加上空行
                for(int i=0;i<count;i++){
                    print += newLine;
                }
                print += "===第"+posP+"页===";
            }
            return print;
        }
        //setter方法,用于注入列每行的字符数
        public void setCols(int cols) {
            this.cols = cols;
        }
        //setter方法,用于注入每页的行数
        public void setRows(int rows) {
            this.rows = rows;
        }
    }
    

    上面体现着面向接口编程的思想,比如在开发Printer程序的时候,只需要了解Ink接口和Paper接口就好,完全不需要依赖这些接口的某个具体实现类。这种开发模式的好处是:①明确了接口定义,在编写代码的时候,完全不需要考虑和某个具体实现类的依赖关系,从而可以构建出更加复杂的系统,②可根据需要方便地更换接口的实现。通过spring强大的组装能力,我们在开发每个程序组件的时候,只要明确关联组件的接口定义,而不需要关心具体实现,这就是面向接口编程。

    接下来就是spring的配置文件了,用它来完成各“零件”的定义和打印机的组装了。

    <?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="colorInk" class="ColorInk"></bean>
        <bean id="greyInk" class="GreyInk"></bean>
        <bean id="A4Paper" class="TextPaper">
            <property name="cols" value="10"/>
            <property name="rows" value="5"/>
        </bean>
        <bean id="A5Paper" class="TextPaper">
            <property name="cols" value="15"/>
            <property name="rows" value="7"/>
        </bean>
    
        <bean id="printer" class="Printer">
            <property name="ink" ref="greyInk"/>
            <property name="paper" ref="A5Paper"/>
        </bean>
    </beans>
    

    从配置文件可看出spring管理bean的灵活性,bean与bean之间的依赖关系放在配置文件中组织,而不是写在代码里,实例的属性值不再由程序中的代码主动创建和管理,而是被动地接受spring的注入,使得组件之间以配置文件而不是硬编码的方式组织在一起。这里的注入起关键作用的是属性的setter方法,这种称为设值注入setter()方法就是一个为了组装时“注入”数据留下来的“插槽”,所以setter方法要满足Javabean的命名规范。如<property name="XXX">其中name 的依据是setXXX()。在注入的时候,<property>的value属性用于注入基本数据类型以及字符串类型的值,ref属性用于注入定义好的bean。

    接着写个测试主类来使用打印机功能

    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    /**
    *@Author 小江  [com.zhbit]
    *@Date 2019/1/24 23:23
    *Description 主类,用来测试案例
    */
    public class Main {
        public static void main(String[] args){
            ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
            Printer printer = (Printer) context.getBean("printer");
            String content = "We get to decide what our story is. Nobody else gets to tell you what your story is." +
                    "我们的故事我们自己决定,轮不到别人对你指手画脚。";
            printer.print(content);
        }
    }
    

    BeanFactory接口是springIoC容器的核心,负责管理组件和它们之间的依赖关系,应用程序通过BeanFactory接口与springIoC容器交互,而ApplicationContext接口是BeanFactory的子接口,功能更加强大,后续基本都使用它来创建管理bean。关于BeanFactory和ApplicationContext的区别我就不重新叙述了,贴一篇不错的文章解释:https://blog.csdn.net/pseudonym_/article/details/72826059

    最后运行效果图.png

    二、AOP

    即Aspect Oriented Programming,俗称面向切面编程,是面向对象编程OOP的有益补充,AOP一般适用于具有横切逻辑的场合,如访问控制、日志输出、事务管理、性能监控等,而这都是一个健壮的业务系统所必需的。
    在业务系统中,总有一些散落、渗透到系统各处且不得不处理的事情,这些穿插在既定业务中的操作就是所谓的“横切逻辑”,也称为切面。比如业务代码和横切逻辑代码都冗杂在一起,显得耦合度很高,或许你会想着把一些重复的代码抽取出来封装成专门的类和方法来调用,做到分离便于管理和维护,但这样也是无法彻底地分离的,毕竟业务代码中仍然保留着要对这些方法的调用的代码,当需要增加或减少横切逻辑的时候,还是要修改业务方法中的调用代码才能实现。于是我们希望能无须编写显式的调用,在需要的时候,系统能“自动”调用所需的功能,在正是AOP要解决的问题。
    面向切面编程,简单来说就是在不改变原有程序的基础上为代码段增加新的功能,对其进行增强处理。设计思想来源于设计模式中的代理模式。在代理模式中为对象设置一个代理对象,当通过代理对象的fun()调用原对象的fun()方法时,就可以在代理方法中添加新的功能,这就是所谓的增强处理,前后都可做增强处理。下面贴一张原理图:

    【Spring】IoC与AOP初体验

    在这种模式下,给开发者的感觉就是在原有代码乃至原业务流程都不改变的情况下,直接在业务流程中切入新代码,增加新功能,这便是所谓的面向切面编程。
    切面可以理解为由增强处理和切入点组成,既包含横切逻辑的定义,也包含了连接点的定义。面向切面编程主要关心两个问题,即在什么位置执行什么功能。

    接下来我们来了解些相关的基本概念:

    • 切面(Aspect):一个模块化的横切逻辑(或称为横切关注点),可能会横切多个对象。
    • 连接点(Join Point):程序执行中的某个具体的执行点。如上图中原对象的fun()就是一个连接点。
    • 增强处理(Advice):切面在某个特定连接点上执行的代码逻辑。(有 前置增强、后置增强、环绕增强、异异常抛出增强、最终增强 等)
    • 切入点(Pointcut):对连接点的特征进行描述,可以使用正则表达式,增强处理和一个切入点表达式相关联。
    • 目标对象(Target object):被一个或多个切面增强的对象。
    • AOP代理(AOPproxy):由AOP框架所创建的对象,实现执行增强处理方法等功能。
    • 织入(Weaving):将增强处理连接到应用程序中的类型或对象上的过程。

    同样来一个小例子来体验: (PS:没采用xml配置文件来实现,体现前置增强和后置增强)

    首先来创建一个maven项目,如下是maven的pom.xml文件,引入所需要的springAOP架包。

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com</groupId>
        <artifactId>zhbit</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <!--aspectj依赖-->
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjrt</artifactId>
                <version>1.8.10</version>
            </dependency>
    
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjtools</artifactId>
                <version>1.8.10</version>
            </dependency>
    
            <!--spring相关包-->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
                <version>4.3.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
                <version>4.3.1.RELEASE</version>
            </dependency>
        </dependencies>
    </project>
    

    定义一个吃饭接口和对应实现类

    public interface Eat {
        public void eating();
    }
    
    public class PeasonEating implements Eat{
        public void eating() {
            System.out.println("人正在吃饭...");
        }
    }
    

    定义一个切面类

    import org.aspectj.lang.annotation.*;
    
    /**
     * FileName: TheAOP
     * Author:   小江
     * Date:     2019/1/26 23:04
     * History:
     */
    
    @Aspect   //表示该类是一个切面
    public class TheAOP {
    
        @Pointcut("execution(* Eat.eating(..))")   //定义命令的切点
        public void eat(){}
    
        @Before("eat()")
        public void prepare(){
            System.out.println("吃饭前织入——当前天气状况晴朗");
        }
        @Before("eat()")
        public void cooking(){
            System.out.println("吃饭前织入——当前为北京时间12:00");
        }
    
        @AfterReturning("eat()")
        public void washing(){
            System.out.println("吃饭后织入——吃饭总耗时为30分钟");
        }
        @AfterThrowing("eat()")
        public void blocking(){
            System.out.println("当前事情异常——人突然被约出去吃饭");
        }
    
    }
    

    创建一个Java配置类

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    /**
     * FileName: Config
     * Author:   小江
     * Date:     2019/1/26 23:14
     * History:
     */
    
    @Configuration
    @EnableAspectJAutoProxy  //启用Aspectj自动代理
    public class Config {
    
        @Bean   //声明一个切面bean
        public TheAOP theAOP(){
            return new TheAOP();
        }
        @Bean
        public Eat eat(){
            return new PeasonEating();
        }
    }
    

    写个测试主类来见证奇迹的时刻,奇迹?

    public class Main {
        public static void main(String[] args){
            //加载Java配置文件类获取spring上下文
            ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
            //获取业务对象
            Eat e = context.getBean(Eat.class);
            //调用业务方法
            e.eating();
        }
    }
    

    奇在哪里?看!真正吃饭的业务功能与横切逻辑的日志功能的代码根本没耦合到一起,通过配置使得切面的功能自动织入到业务功能,切面自动为我们服务,我们不需过多关注,体现关注点分离。有图有真相哈哈哈~

    最终运行效果图

    三、总结

    • 依赖注入让组件之间以配置文件的形式组织在一起,而不是以硬编码的方式偶合在一起。
    • spring配置文件是完成组装的主要场所,是完成注入和织入的地方。
    • AOP的目的是从系统中分离出切面,将其独立于业务逻辑实现,并在程序执行时织入程序中执行。

    相关文章

      网友评论

        本文标题:【Spring】IoC与AOP初体验

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