欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

一篇文章带你了解、学习、体验、回顾Spring基础

程序员文章站 2022-05-04 20:13:12
...

Spring介绍

优点

  • Spring是一个免费开源的框架(容器)
  • Spring是一个轻量级、非入侵式的框架:从框架的角度可以理解为:无需继承框架提供的任何类
    这样我们在更换框架时,之前写过的代码几乎可以继续使用。
  • 反转控制(IoC)、面向切面编程(AOP)
  • 支持事务的处理,对框架整合支持

组成

一篇文章带你了解、学习、体验、回顾Spring基础

框架结构

  • Data Access/Integration层包含有JDBC、ORM、OXM、JMS和Transaction模块。

  • Web层包含了Web、Web-Servlet、WebSocket、Web-Porlet模块。

  • AOP模块提供了一个符合AOP联盟标准的面向切面编程的实现。

  • **Core Container(核心容器):**包含有Beans、Core、Context和SpEL模块。

     Bean -- 处理Bean的jar
     context -- 处理spring上下文的jar
     core -- Spring 核心jar
     expression -- Spring 表达式
    
  • Test模块支持使用JUnit和TestNG对Spring组件进行测试。

Spring配置的可选方案

  • 在XML中进行显式配置;
  • 在java中进行显式配置;
  • 隐式的bean发现机制和自动装配;

很多情形下选择哪种配置方案很大程度上是个人喜好的原因,当然还可以进行各种配置方案之间的互配。在《Spring in action》书中,作者建议使用以下顺序:

  1. 自动装配的机制;
  2. 当必须要使用显示配置bean的时候,推荐使用javaConfig,它的优势是比XML显示配置更安全且更加强大;
  3. 最后选择XML配置;

Spring Ioc 与 DI

IoC:Inverse of Control(控制反转) : 它不是什么技术,而是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。

DI:Dependency Injection(依赖注入) : 指 Spring 创建对象的过程中,将对象依赖属性(简单值,集合,对象)通过配置设值给该对象

创建第一个Ioc程序

​ 首先,我们需要创建Spring的配置文件,Spring配置文件的名称和位置没有固定要求,一般建议把该文件放到src目录下面,名称可随便写,官方建议写成applicationContext.xml。然后我们还需要在配置文件中引入约束,Spring学习阶段的约束是schema约束。那么问题来了,这个约束又该怎么写呢?可参考docs\spring-framework-reference\html目录下的xsd-configuration.html文件,在其内容最后面找到如下内容。

一篇文章带你了解、学习、体验、回顾Spring基础

​ 将其复制黏贴到配置文件applicationContext.xml中,这样applicationContext.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">
</beans>
  • 创建一个接口然后将其实现。

    package com.yy.dao;
    
    public interface UserDao {
        void saveUser();
    }
    
    import com.yy.dao.UserDao;
    
    public class UserDaoImpl implements UserDao {
        public void saveUser() {
            System.out.println("存储了User对象....");
        }
    }
    
  • 然后将这个类交给Spring管理,即在文件中配置对象的创建。

    <bean name="userDao" class="com.yy.dao.Impl.UserDaoImpl"></bean>
    
  • 测试类进行测试

    我们先把获取ApplicationContext对象的过程封装成一个工具类,相对能简化一下开发步骤。

    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class applicationContextUtil {
        public static ApplicationContext getApplicationContext(){
            return new ClassPathXmlApplicationContext("applicationContext.xml");
        }
    }
    

    进行测试

    @Test
    public void saveUser(){
        ApplicationContext context = applicationContextUtil.getApplicationContext();
        UserDao userDao = (UserDao) context.getBean("userDao");
        userDao.saveUser();
    }
    
  • 结果

    存储了User对象....
    
  • 总结:发现虽然我们并没有创建UserDao对象但是我们依然通过applicationContext.xml里面的配置,从而拿取了对象。

创建第一个DI程序

  • 首先创建一个实体类

    package com.yy.pojo;
    
    import lombok.Data;
    
    @Data
    public class User {
        private Integer id;
        private String username;
        private String address;
    }
    
  • 然后在applicationContext.xml中给实体类注入数据

    <bean name="user" class="com.yy.pojo.User">
        <property name="id" value="201601"/>
        <property name="username" value="大脸猫"/>
        <property name="address" value="清华大学"/>
    </bean>
    
  • 测试类

    @Test
    public void getUser(){
        ApplicationContext context = applicationContextUtil.getApplicationContext();
        User user = (User)context.getBean("user");
        System.out.println(user.toString());
    }
    
  • 测试结果

    User(id=201601, username=大脸猫, address=清华大学)
    

总结

IoC 和 DI 其实是同一个概念的不同角度描述,DI 相对 IoC 而言,明确描述了被注入对象依赖 IoC 容器配置依赖对象

DI实现过程:

  • context.getBean("user")先根据要求找到,xml或注解中相应的配置。
  • 看看user依赖的是哪个实体类,拿到类名。
  • 使用反射的API,基于类名实例化对应的对象实例
  • 将对象实例,通过构造函数或者setter,传递给user对象

Java Bean的相关问题

bean的id和name属性的配置

​ 从之前编写的Spring配置文件中,可以发现<bean>标签中有一个id属性,其实它还有一个和id属性类似的name属性,根据它俩的值均能得到配置对象。

**id属性:**在Spring配置文件中会有多个bean标签,但它们的id属性值是不能相同的。Bean起名字时,在约束中采用的是ID约束(唯一约束),而且名字必须以字母开始,可以使用字母、数字、连字符、下划线、句号、冒号等,但id属性值绝对不能有特殊符号;

**name属性:**没有使用约束中的唯一约束,理论上name属性值是可以出现重复的,但是这在实际开发中是不可能出现的,而且它里面可以出现特殊字符。其实,在Spring和Struts1框架进行整合的时候,才有可能会用到。

bean声明周期的配置

​ 通过配置<bean>标签上的init-method作为Bean被初始化的时候执行的方法,配置destroy-method作为bean被销毁的时候执行的方法,要想bean被销毁的时候执行destroy-method属性中配置的方法,那么bean得是单例创建的(默认即单例创建),而且只有工厂被关闭时,destroy-method属性中配置的方法才能被执行。

  • 同样实现一个接口:
package com.yy.dao.Impl;

import com.yy.dao.UserDao;

public class UserDaoImpl implements UserDao {
    public void saveUser() {
        System.out.println("存储了User对象....");
    }

    public void start(){
        System.out.println("初始化...");
    }
    
    public void end(){
        System.out.println("销毁...");
    }
}
  • 在配置文件中配置初始化和销毁方法

    <bean name="userDao" class="com.yy.dao.Impl.UserDaoImpl" init-method="start" destroy-method="end"/>
    
  • 测试

    @Test
    public void saveUser(){
        ApplicationContext context = applicationContextUtil.getApplicationContext();
        UserDao userDao = (UserDao) context.getBean("userDao");
        userDao.saveUser();
    }
    
  • 结果

    初始化...
    存储了User对象....
    
  • 我们发现实际上并没有执行deploy方法,原因是ApplycationContext对象并没有关闭。

    @Test
    public void saveUser(){
        AbstractApplicationContext context = (AbstractApplicationContext) applicationContextUtil.getApplicationContext();
        UserDao userDao = (UserDao) context.getBean("userDao");
        userDao.saveUser();
        context.close();
    }
    
  • 结果

    初始化...
    存储了User对象....
    销毁...
    

    ​ 要想bean被销毁的时候执行destroy-method属性中配置的方法,那么前提bean得是单例创建的,默认即单例创建,就像下面这样。

    <.....scope="singleton"/>	
    

    若是设置成多例模式结果则不同。

bean的作用范围的配置

<bean>标签上有一个scope属性,它代表Bean的作用范围。该属性有五个属性值,分别是:

  • singleton:scope属性的默认值,spring会采用单例模式创建这个对象。

    单例模式时,创建的对象时相同的,多例模式则相反。

  • prototype:spring会采用多例模式创建这个对象。

  • request:应用在web项目中,spring创建这个类以后会将这个类存入到request域当中。

  • session:应用在web项目中,spring创建这个类以后会将这个类存入到session域当中。

  • globalsession:应用在web项目中,必须是prolet环境(你在一个地方存入数据以后,其它一些子系统当中就不需要进行登录了)下才能使用,他要单点登录(即SSO,single sign on)上,但如果没有这种环境,你配置一个global session就相当于配置了一个session。

Spring中Bean的实例化方式

Bean交给Spring管理之后,它在创建这些Bean的时候,有以下三种方式:

  1. 无参构造方法的方式(默认)
  2. 静态工厂实例化的方式
  3. 实例工厂实例化的方式

无参构造方法方式

​ 这种方式是Spring实例化Bean的默认方式,指的是在创建对象的时候,Spring会调用类里面的无参数的构造方法实现。

​ 上面应用的实例全是无参构造方法方式。

静态工厂实例化的方式

  • 首先创建一个Bean类

    package com.yy.spring;
    
    public class Bean {
        /**
         * 无参构造方法
         */
        public Bean(){
            System.out.println("Bean被执行");
        }
    }
    
  • 然后创建一个Bean工厂

    package com.yy.spring;
    
    public class BeanFactory {
        /**
         * 创建工厂
         * @return
         */
        public static Bean createBean(){
            System.out.println("BeanFactory被执行....");
            return  new Bean();
        }
    }
    
  • 配置文件

    <bean id="bean" class="com.yy.spring.BeanFactory" factory-method="createBean"></bean>
    
  • 测试结果

    BeanFactory被执行....
    Bean被执行
    

实例工厂实例化的方式

创建一个工厂类,在工厂类里面提供一个普通的方法,这个方法返回类对象,调用工厂类的方法时,创建工厂类对象,使用对象调用方法即可

  • 首先创建一个Bean类

    public class Bean {
        /**
         * 无参构造方法
         */
        public Bean(){
            System.out.println("Bean被执行");
        }
    }
    
  • 然后创建一个Bean工厂

    public class BeanFactory {
        /**
         * 创建工厂
         * @return
         */
        public Bean createBean(){
            System.out.println("BeanFactory被执行....");
            return  new Bean();
        }
    }
    
  • 进行相关配置

    <bean id="BeanFactory" class="com.yy.spring.BeanFactory"></bean>
    <bean id="bean" factory-bean="BeanFactory" factory-method="createBean"></bean>
    
  • 测试结果

    BeanFactory被执行....
    Bean被执行
    

Spring中Bean的属性注入

实际上,有关Bean的属性注入共有2种方式

  • 有参构造函数注入
  • set方法注入

有参构造函数注入

  • 创建一个实体类

    package com.yy.spring.domain;
    
    public class Car {
        private String name;
        private Double price;
    
        public Car(String name,Double price){
            super();
            this.name = name;
            this.price = price;
        }
    }
    
  • 然后,在Spring配置文件中对以上JavaBean进行如下配置。

    <bean id="car" class="com.yy.spring.pojo.Car">
        <constructor-arg name="name" value="汽车"></constructor-arg>
        <constructor-arg name="price" value="221.2"></constructor-arg>
    </bean>
    

set方法的方式注入属性

  • 同样创建一个实体类在实体类中生成set方法

    public class Car {
        private String name;
        private Double price;
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void setPrice(Double price) {
            this.price = price;
        }
    }
    
  • 在配置文件中进行相关的配置

    <bean id="car" class="com.yy.spring.domain.Car">
        <property name="name" value="汽车"></property>
        <property name="price" value="2.213"></property>
    </bean>
    

SpEL表达式

​ 在Spring3.0版本以后,提供了一种SpEL表达式语言的属性注入方式。SpEL,即Spring Expression Language,翻译过来就是Spring的表达式语言,其语法格式是#{SpEL}

使用了这种SpEL表达式语言的属性注入方式,还可以调用其他类的属性或者方法。

<bean name="car" class="com.yy.pojo.Car">
    <property name="name" value="兰博基尼"/>
    <property name="price" value="22"/>
</bean>
<bean name="carInfo" class="com.yy.pojo.CarInfo">
    <property name="name" value="#{car.name}"/>
    <property name="price" value="#{22 + 7}"/>
</bean>

如上,carInfo中使用了SpEL表达式,在表达式中既可以引用其它bean中的数据也可以在其中书写表达式

注意事项:CarInfo的属性值是通过SpEl从Car类中的属性值赋值而来,所以Car类中必须要有setget方法,set方法是为了用于配置文件进行依赖注入为Car的属性进行赋值,get方法是为了用于获取Car属性的值,进而为CarInfo进行赋值,而CarInfo只需要接受Car的属性值所以CarInfo类中只需要存在set方法即可

装配集合

<bean id="complexAssembly" class="pojo.ComplexAssembly">
    <!-- 装配Long类型的id -->
    <property name="id" value="1"/>
    
    <!-- 装配List类型的list -->
    <property name="list">
        <list>
            <value>value-list-1</value>
            <value>value-list-2</value>
            <value>value-list-3</value>
        </list>
    </property>
    
    <!-- 装配Map类型的map -->
    <property name="map">
        <map>
            <entry key="key1" value="value-key-1"/>
            <entry key="key2" value="value-key-2"/>
            <entry key="key3" value="value-key-2"/>
        </map>
    </property>
    
    <!-- 装配Properties类型的properties -->
    <property name="properties">
        <props>
            <prop key="prop1">value-prop-1</prop>
            <prop key="prop2">value-prop-2</prop>
            <prop key="prop3">value-prop-3</prop>
        </props>
    </property>
    
    <!-- 装配Set类型的set -->
    <property name="set">
        <set>
            <value>value-set-1</value>
            <value>value-set-2</value>
            <value>value-set-3</value>
        </set>
    </property>
    
    <!-- 装配String[]类型的array -->
    <property name="array">
        <array>
            <value>value-array-1</value>
            <value>value-array-2</value>
            <value>value-array-3</value>
        </array>
    </property>
</bean>

Spring的分模块开发的配置

Spring中的分模块开发有两种配置方式,

加载多个配置文件

Spring分模块开发的第一种配置方式就是指在加载配置文件的时候,一次加载多个配置文件。

new ClassPathXmlApplicationContext("applicationContext.xml","TApplicationContext.xml");

引入多个配置文件

​ Spring分模块开发的第二种配置方式就是指在一个配置文件中引入多个配置文件。这样说的话,我们可以在applicationContext.xml文件中引入TApplicationContext.xml文件。

<import resource="TApplicationContext.xml"/>

Spring注解开发

用IoC进行注解开发,需要进行配置一个扫描组件,就告诉他Spring项目下哪些包使用IoC注解了。

<!--配置一个扫描组件,告诉spring一个注解使用的包-->
<!--当com.yy.spring包下面还有其它子包那么那么同样会被扫描-->    
<context:component-scan base-package="com.yy.spring"></context:component-scan>
<!--当然还可以一次配置多个包,中间用逗号隔开-->
<context:component-scan base-package="com.yy.spring,com.yy.p"></context:component-scan>    
<!--还可以设置扫描范围内哪些注解可不可以用-->    
<context:include-filter>
<context:exclude-filter>
    
<!--下面例子的作用是禁用@Service注解-->  
<!--use-default-filter的作用是fliter标签可否用,默认可用,感觉这个标签有些多余-->    
<context:component-scan base-package="com.yy.spring" use-default-filters="true">
    <context:exclude-filter type="annotation" 			                          		                                 expression="org.springframework.stereotype.Service"/>
</context:component-scan>

简单的注解实例

  • 配置扫描器

    <context:component-scan base-package="com.yy.pojo,com.yy.dao"/>
    
  • pojo类种加入注解

    import lombok.Data;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Data
    @Component("car")//相当于<bean id="car" class="..."></bean>
    public class Car {
        @Value("mike")
        private String name;
        @Value("33.4")
        private Double price;
    }
    
  • 测试

    @Test
    public void getUser(){
        ApplicationContext context = applicationContextUtil.getApplicationContext();
        Car car = (Car) context.getBean("car");
        System.out.println(car.toString());
    }
    
  • 结果

    Car(name=mike, price=33.4)
    
  • 声明容器的注解

    • @Component 注解是用于把 SgtPeppers类实例化注入到 Spring容器当中,相当于配置文件当中的<bean id="" class=""/>

      另外与它类似的注解还有:

    • @Controller:标注控制层组件

    • @Service:标注业务层组件

    • @Repository:标注数据层组件

    • @Component:泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注

注入属性注解

  • Autowired:自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。 如果IoC容器中没有任何bean的类型和要注入的变量类型匹配,则报错。如果IoC容器中有多个类型匹配时:将变量名跟已经匹配的数据类型的key值比较,如果有唯一匹配则注入成功,否则失败。
  • @Qualifier:在自动按照类型注入的基础之上,再按照Bean的id注入。它在给字段注入时不能独立使用,必须和@Autowire一起使用;但是给方法参数注入时,可以独立使用。
  • @Resource:直接按照Bean的id注入。它也只能注入其他bean类型。
  • @Value:注入基本数据类型和String类型数据的 ,不能注入bean类型的

Scope作用范围注解

@scope: 用于指定bean的作用域,一般就是singleton和prototype 。

生命周期注解

  • @PostConstruct: 用于指定初始化方法,相当于bean标签中的init-method属性。
  • @PreDestroy 作用: 用于指定销毁方法。相当于bean标签中的destroy-method属性。

Spring从以下方式实现自动化装配

  • 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean;
  • 自动装配(autowiring):Spring自动满足bean之间的依赖;

用javaConfig的方式配置注解

在这里使用一个以 CD播放的例子

  1. 首先创建一个 CompactDisc接口

    package com.spring;
    
    public interface CompactDisc {
        void play();
    }
    
  2. 创建一个SgtPeppers类实现CompactDisc接口

    package com.spring;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class SgtPepper implements CompactDisc {
        private String title = "Sgt. Pepper's Lonely Hearts Club Band";
        private String artist = "The Beatles";
    
        public void play() {
            System.out.println("Playing " + title + " by " + artist);
        }
    }
    
  3. 使用配置类开启注解扫描

    package com.spring;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan
    public class CDPlayerConfig {
    }
    

    @Configuration用于定义配置类,被定义的类相当于.xml配置文件

    @ComponentScan会开启扫描与配置类相同的包中的注解 此时的包就是 package com.spring

  4. 测试组件是否能够扫描发现CompactDisc

    import com.spring.CompactDisc;
    import com.spring.CDPlayerConfig;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import static org.junit.Assert.assertNotNull;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = CDPlayerConfig.class)
    public class CDPlayerTest {
        @Autowired
        private CompactDisc cd;
    
        @Test
        public void cdShouldNotBeNull(){
            assertNotNull(cd);
        }
    }
    

    @RunWith就是一个运行器

    @RunWith(SpringJUnit4ClassRunner.class)就是用Spring的测试环境来运行测试

    @ContextConfiguration通常与@RunWith一起使用来进行测试

    使用方式有以下几种

    单个文件
    @ContextConfiguration(Locations=”../applicationContext.xml”)
    @ContextConfiguration(classes = SimpleConfiguration.class)

    多个文件时,可用{}

    @ContextConfiguration(locations = {“classpath*:/*.xml”,“classpath*:/*.xml”})

    assertNotNull(cd);是用户判断对象是否为NULL,如果为NULL则会报出异常说明@AuroWired注解注入失败。

  5. 经测试扫描组件能够发现CompactDisc

为组件扫描的bean命名

​ Spring应用上下文中所有的bean都会给定一个ID,虽然我们在前面的例子中没有给SgtPeppers bean设置ID,但是Spring会给它自动设置ID为sgtPeppers(首字母小写),当然也可以自己给bean设置ID,比如

import org.springframework.stereotype.Component;

@Component("beautifulBean")
public class SgtPepper implements CompactDisc {
	...
}

设置扫描组件基础包

前面例子,用@ComponentScan扫描配置类所在的包为基础包,还有可以通过配置类指定不同的基础包

例如:

直接在配置类注解中声明基础包的位置

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.spring")
public class CDPlayerConfig {}

如果想清晰的表明所设置的基础包,可以通过basePackages进行设置

@ComponentScan(basePackages = "com.spring")

当然可以设置多个包,通过扫描一个数组

@ComponentScan(basePackages = {"com.spring","com.mybatis"})

除了使用basePackage属性还可以使用basePackageClasses属性来指定包中所含类或者是接口来实现基础包。

@ComponentScan(basePackageClasses = {SgtPepper.class})

​ 当然也可通过逗号来设置多个包,这里《Spring in action》作者建议在使用basePackageClasses属性时,后面的类或者接口使用专门的类来作为指定类,即这些类的作用只是给配置类扫描配置基础包来使用的,防止在重构过程中某些指定的类被删除掉导致出现问题。

通过bean添加注解实现自动装配

自动装配在Spring中我们常见的有两种方式:构造器注入 和 Setter方法注入

package com.spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CDPlayer {
    private CompactDisc cd;

    @Autowired
    public CDPlayer(CompactDisc cd){
        this.cd = cd;
    }

}

上为构造器注入 下为Setter方法注入
    
@Component
public class CDPlayer {
    private CompactDisc cd;

    @Autowired
    public void setCd(CompactDisc cd){
        this.cd = cd;
    }

}

​ 其实Setter方法没有什么任何特别的地方,其它方法也可以,比如说把setCd()改成insertCd()效果事相同的,而所谓的Setter方法只是一种规范。

​ 如果没有匹配bean,那么在应用上下文创建的时候,Spring会抛出异常,为避免抛出异常的出现可以将@Autowired@required属性设置为false

​ java依赖注入规范中有一个@Inject注解其使用方法几乎 相同 大多数场景可以相互替换。

代理模式

静态代理模式

什么是代理模式?就是使用代理的形式将一些业务外包给一些专门的人做,自己专注做核心业务。

​ 举一个例子 - 本人要把自己的十几套房子租出去,那么我又不想去贴小广告,该怎么做呢?把钥匙给房屋出租代理公司就行了,剩下的事情让代理公司代理你去做。

首先我们为创建一个Rent接口

package com.proxy;

public interface Rent {
    void rent();
}

然后创建一个Host类继承Rent接口,代表房子主人要出租房子。

package com.proxy;

public class Host implements Rent{
    @Override
    public void rent() {
        System.out.println("房主同意租房子!");
    }
}

再创建一个代理让他出租房子,代理的业务还有带客户看房子、收费用等

package com.proxy;

public class Proxy implements Rent{
    private Host host;

    public Proxy(Host host) {
        this.host = host;
    }

    @Override
    public void rent() {
        seeHost();
        host.rent();
        fare();
    }

    public void seeHost(){
        System.out.println("看房子!");
    }

    public void fare(){
        System.out.println("付房租");
    }
}

最后是用户租房子

package com.proxy;

public class Client {
    public static void main(String[] args) {
        Host host = new Host();
        Proxy proxy = new Proxy(host);

        proxy.rent();
    }
}

执行结果

看房子!
房主同意租房子!
付房租

​ 由上面发现,房主所作的工作只是同意房子出租,剩下的工作都是由代理公司做的,但是房东所做的是最重要的内容。同样在平常写代码时,比如一些安全、日志等功能应使用代理的方式来实现,而且在给原有系统增加一些功能时我们的原则应该是尽量不要修改原来的代码通过代理来实现功能增加。

静态代理模式的好处:

  • 可以使真实角色操作更加纯粹!不用关注一些公共业务。
  • 公共业务由代理进行,实现了业务分工。
  • 公共业务扩展时,方便几种管理。

缺点:

  • 一个真实角色就会产生一个代理角色,代码量会翻倍。

动态代理

​ 动态代理就是通过反射的方式实现比静态代理更加便捷的代理,同时,Jdk提供了invocationHandler接口和Proxy类,借助这两个工具可以达到我们想要的效果。

JDK动态代理具体步骤:

  1. 通过实现InvocationHandler 接口创建自己的调用处理器;
  2. 通过为 Proxy类指定 ClassLoader对象和一组 interface来创建动态代理类;
  3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
  4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。

通过实例来实现一个动态代理

  • 首先创建一个日志代理类,来实现一个简单的日志打印。

    package dynamicproxy;
    
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/24 22:52
     * @description:日志代理类
     */
    public class LogProxy implements InvocationHandler {
        //被代理对象
        Object targetObject;
    
        public LogProxy(Object targetObject) {
            this.targetObject = targetObject;
        }
    
        /**
         * 将被代理的对象传入获得它的类加载器和实现接口作为Proxy.newProxyInstance方法的参数。
         * 第一个参数: targetObject.getClass().getClassLoader():targetObject对象的类加载器。
         * 第二个参数:targetObject.getClass().getInterfaces():targetObject对象的所有接口。
         * 第三个参数:this:也就是当前对象即实现了InvocationHandler接口的类的对象,在调用方法时会调用它的invoke方法。
         * @return
         */
        public Object createLogProxy(){
            Object objectProxy = Proxy.newProxyInstance(
                targetObject.getClass().getClassLoader(),   										targetObject.getClass().getInterfaces(),
                this);
            return objectProxy;
        }
    
        /**
         * 我们在测试类中通过上面createLogProxy函数获取代理对象,然后调用它的函数实际上是执行的invoke函数
         * @param proxy 该参数是代理的真实对象
         * @param method 该参数是代理的方法
         * @param args 代理方法中接受的参数
         * @return
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("----------------------");
            System.out.println("   对"+method.getName()+"方法进行了权限检验");
            System.out.println("   参数个数:"+method.getParameterCount());
            System.out.println("   参数类型:"+method.getParameterTypes());
            System.out.println("   返回参数类型:"+method.getReturnType());
            System.out.println("   ......");
            System.out.print("   方法执行打印:");
            //执行真正的方法
            Object ret = method.invoke(targetObject,args);
            System.out.println("   方法执行完毕");
            System.out.println("----------------------");
            return ret;
        }
    }
    
  • 创建一个动态代理的接口,同时我们要注意的是动态代理的实现方式也是通过接口实现的。

    package dynamicproxy;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/24 22:45
     * @description:动态代理测试接口
     */
    public interface ProgramingLanguage {
    
        Object JavaCoder(String name, Integer age);
        Object PHPCoder();
        void CppCoder();
    }
    
  • 对接口进行实现

    package dynamicproxy;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/24 22:49
     * @description:动态代理测试类
     */
    public class ProgramingLanguageImpl implements ProgramingLanguage {
    
        @Override
        public Object JavaCoder(String name, Integer age) {
            System.out.println(name +"是一个"+age+"岁的java工程师");
            return null;
        }
    
        @Override
        public Object PHPCoder() {
            System.out.println("PHP是最好的编程语言");
            return null;
        }
    
        @Override
        public void CppCoder() {
            System.out.println("C++是最好的编程语言");
        }
    }
    
  • 创建一个测试类进行测试

    import dynamicproxy.LogProxy;
    import dynamicproxy.ProgramingLanguage;
    import dynamicproxy.ProgramingLanguageImpl;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/24 23:02
     * @description:动态代理测试类
     */
    public class ProxyTest {
        public static void main(String[] args) {
            ProgramingLanguage proxy = (ProgramingLanguage) new LogProxy(new ProgramingLanguageImpl()).createLogProxy();
            proxy.CppCoder();
            proxy.JavaCoder("小明",23);
            proxy.PHPCoder();
        }
    }
    
  • 测试结果

    ----------------------
       对CppCoder方法进行了权限检验
       参数个数:0
       参数类型:[Ljava.lang.Class;@306a30c7
       返回参数类型:void
       ......
       方法执行打印:C++是最好的编程语言
       方法执行完毕
    ----------------------
    ----------------------
       对JavaCoder方法进行了权限检验
       参数个数:2
       参数类型:[Ljava.lang.Class;@b81eda8
       返回参数类型:class java.lang.Object
       ......
       方法执行打印:小明是一个23岁的java工程师
       方法执行完毕
    ----------------------
    ----------------------
       对PHPCoder方法进行了权限检验
       参数个数:0
       参数类型:[Ljava.lang.Class;@506c589e
       返回参数类型:class java.lang.Object
       ......
       方法执行打印:PHP是最好的编程语言
       方法执行完毕
    

``


切面编程-AOP

​ Spring的Aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 . 领域业务更纯粹 , 程序猿专注领域业务 , 其本质还是动态代理 .

​ 比如说一个公司的工资计算,每个部门都有自己的基本工资和绩效工资标准,因为每个部门干的工作不同,但是公司给每个部门的员工付出的社保、公积金比例是相同的,所以在公司月底进行工资计算时,我们可以把每个部门的基本工资和绩效工资计算写成独立的类,然后社保、公积金的计算就可以使用面向切面的方式来进行计算,这样就会减低代码量、降低业务之间的耦合性。

以下名词需要了解下:

  • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 …
  • 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。
  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。
  • 目标(Target):被通知对象。
  • 代理(Proxy):向目标对象应用通知之后创建的对象。
  • 切入点(PointCut):切面通知 执行的 “地点”的定义。
  • 连接点(JointPoint):与切入点匹配的执行点。

通过Spring API实现的AOP

  • 首先创建一个前置日志的类,简单实现一个前置日志通知

    package com.aop;
    
    import org.springframework.aop.MethodBeforeAdvice;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:45
     * @description:前置Log
     */
    @Component
    public class BeforeLog implements MethodBeforeAdvice {
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("前置通知:" + o.getClass().getName() + "对象的" + method.getName() + "方法被执行了");
        }
    }
    
  • 再创建一个后置通知

    package com.aop;
    
    import org.springframework.aop.AfterReturningAdvice;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:47
     * @description:后置Log
     */
    @Component
    public class AfterLog implements AfterReturningAdvice {
        @Override
        public void afterReturning(Object returnValue, Method method, Object[] objects, Object target) throws Throwable {
            System.out.println("后置通知:" + target.getClass().getName() + "对象的" + method.getName() + "方法被执行了");
        }
    }
    
  • 再创建接口和实现类用于测试准备

    package com.aop;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:41
     * @description:AOP测试接口
     */
    public interface UserService {
        void add();
        void delete();
        void update();
        void query();
    }
    
    package com.aop;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:43
     * @description:AOP测试类
     */
    @Component("userService")
    public class UserServiceImpl implements UserService {
        @Override
        public void add() {
            System.out.println("增加一个用户");
        }
    
        @Override
        public void delete() {
            System.out.println("删除一个用户");
        }
    
        @Override
        public void update() {
            System.out.println("修改一个用户");
        }
    
        @Override
        public void query() {
            System.out.println("查询一个用户");
        }
    }
    
  • 在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"
           xmlns:aop="http://www.springframework.org/schema/aop"
           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/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="com.aop"/>
        <aop:config>
            <!--切入点 expression:表达式匹配要执行的方法-->
            <aop:pointcut id="pointcut" expression="execution(* com.aop.UserServiceImpl.*(..))"/>
            <!--执行环绕; advice-ref执行方法 . pointcut-ref切入点-->
            <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
            <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
        </aop:config>
    
    </beans>
    

    上面涉及到了一个execution表达式:

    1、execution(): 表达式主体。

    2、第一个*号:方法返回类型, *号表示所有的类型。

    3、包名:表示需要拦截的包名。

    4、第二个*号:表示类名,*号表示所有的类。

    5、*(…):最后这个星号表示方法名,*号表示所有的方法,后面( )里面表示方法的参数,两个句点表示任何参数

  • 测试类进行测试

    import com.aop.UserService;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:53
     * @description:测试类
     */
    public class AopTest {
        @Test
        public void test(){
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationConfig.xml");
            UserService userService = (UserService) context.getBean("userService");
            userService.query();
        }
    }
    
  • 测试结果

    前置通知:com.aop.UserServiceImpl对象的query方法被执行了
    查询一个用户
    后置通知:com.aop.UserServiceImpl对象的query方法被执行了
    

自定义类来实现AOP

  • 创建一个通知类

    package com.aop.div;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:03
     * @description:通知测试类
     */
    @Component("advice")
    public class Advice {
        public void before(){
            System.out.println("前置通知");
        }
    
        public void after(){
            System.out.println("后置通知");
        }
    }
    
  • 创建一个实体类

    package com.aop.div;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:05
     * @description:
     */
    @Component("pointCut")
    public class PointCut {
        public void sayHello(){
            System.out.println("Hello");
        }
    }
    
  • 在XML中进行注册

    <aop:config>
        <aop:aspect ref="advice">
            <aop:pointcut id="pointcut-1" expression="execution(* com.aop.div.PointCut.*(..))"/>
            <aop:before pointcut-ref="pointcut-1" method="before"/>
            <aop:after pointcut-ref="pointcut-1" method="after"/>
        </aop:aspect>
    </aop:config>
    
  • 测试类进行测试

    @Test
    public void testAdvice(){
        ApplicationContext context = new 							      				                                ClassPathXmlApplicationContext("applicationConfig.xml");
        PointCut pointCut = (PointCut) context.getBean("pointCut");
        pointCut.sayHello();
    }
    
  • 测试结果

    前置通知
    查询一个用户
    后置通知
    

    相比使用spring Api的方式,此方式显得更见便捷,但是这种方式使用起来并不如上一种强大。

注解实现AOP

​ 在这里我们不使用就不使用任何xml文件了直接完全用javaConfig+注解的方式来实现以开始提到的计算社保公积金的一个小程序

  • 首先创建javaConfig类

    package com.aop.annotation;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:39
     * @description:使用Java进行环境配置
     */
    @Configuration
    @ComponentScan(basePackages = "com.aop.annotation")
    @EnableAspectJAutoProxy
    public class JavaConfig {
    }
    

    @EnableAspectJAutoProxy注解相当于xml中的<aop:aspectj-autoproxy/>它的作用是声明自动为spring容器中那些配置@aspectJ切面的bean创建代理

  • 创建接口和实现类

    package com.aop.annotation;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:46
     * @description:接口
     */
    @Component
    public interface Department {
        Double developerSalary(Double base,Double performance);
        Double salerSalary(Double base,Double performance);
    }
    
    
    package com.aop.annotation;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:48
     * @description:实体类
     */
    @Component
    public class DepartmentImpl implements Department{
    
        @Override
        public Double developerSalary(Double base, Double performance) {
            Double countSalary =  base + performance * 8;
            return countSalary;
        }
    
        @Override
        public Double salerSalary(Double base, Double performance) {
            Double countSalary = base + performance * 6;
            return countSalary;
        }
    }
    
  • 创建增强类

    package com.aop.annotation;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.aop.AfterReturningAdvice;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:56
     * @description:社保缴纳后置通知
     */
    @Component
    @Aspect
    public class SocialSecurityPay{
        private Double countSalary;
        private Double socialSePay;
        private Double acFund;
        private Double finalSalary;
    
    
        @After("execution(* com.aop.annotation.DepartmentImpl.*(..))")
        public void after(JoinPoint joinPoint) throws InvocationTargetException, IllegalAccessException {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
    
            countSalary = (Double) method.invoke(joinPoint.getTarget(),joinPoint.getArgs());
            socialSePay = 0.06 * countSalary;
            acFund = 0.08 * countSalary;
            finalSalary = countSalary - socialSePay - acFund;
            System.out.println("------------------------------------------");
            System.out.println(method.getName()+"本月工资为:" + countSalary);
            System.out.println(method.getName()+"公积金缴纳:" + acFund);
            System.out.println(method.getName()+"社保缴纳为:" + socialSePay);
            System.out.println(method.getName()+"事发薪资为:" + finalSalary);
        }
    }
    

    JoinPoint对象封装了Spring Aop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.

    常用API

    方法名 功能
    Signature getSignature(); 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
    Object[] getArgs(); 获取传入目标方法的参数对象
    Object getTarget(); 获取被代理的对象
    Object getThis(); 获取代理对象
  • 测试类进行测试

    package com.aop.annotation;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.ImportResource;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import static org.junit.Assert.assertNotNull;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 21:04
     * @description:测试类
     */
    public class AnnotationTest {
        @Test
        public void test(){
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
    
            Department department = (Department) context.getBean("departmentImpl");
            department.developerSalary(2000.0,20.2);
            department.salerSalary(2000.0,50.5);
        }
    }
    
  • 输出结果

    ------------------------------------------
    developerSalary本月工资为:2161.6
    developerSalary公积金缴纳:172.928
    developerSalary社保缴纳为:129.696
    developerSalary事发薪资为:1858.976
    ------------------------------------------
    salerSalary本月工资为:2303.0
    salerSalary公积金缴纳:184.24
    salerSalary社保缴纳为:138.18
    salerSalary事发薪资为:1980.5800000000002