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

Spring 整体学习(IOC容器 DI依赖注入 AOP 面向切面编程(动态代理) 配置注入 注解注入 )

程序员文章站 2022-05-24 23:43:51
...

Spring

一. Spring的IOC与DI

​ IOC(Invertion Of Control)是控制翻转,是一种编程的思想,就是以前在代码中我们需要通过 new 的方式来使用其他的对象,这种方式导致的结果就是类与类之间耦合度过高,耦合度高导致到迭代成本极高;通过控制的翻转的方式,就是我们项目中用到所有的对象,都通用交个一个容器来管理(在spring中我们叫做 IOC容器);
反转了依赖关系的满足方式,由之前的自己创建依赖对象,变为由工厂推送。(变主动为被动,即反转)
解决了具有依赖关系的组件之间的强耦合,使得项目形态更加稳健

​ DI(Dependency Injection)是依赖注入,它是对IOC编程思想的具体实现。在Spring创建对象的同时,为其属性赋值,称之为依赖注入。

1.1 依赖注入

依赖注入主要分为 属性注入(set方法注入)构造器注入

第一步,创建一个配置文件 application-context.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">
    
    <!-- SSDDisk和MachineDisk实现了共同的 Disk 接口 -->
    <!-- 当容器启动的时候会创建一个名为 ssdDisk 的对象,然后纳入到IOC容器中 -->
    <bean id="ssdDisk" class="org.example.ioc.impl.SSDDisk"></bean>
    <bean id="machineDisk" class="org.example.ioc.impl.MachineDisk"></bean>
    
    <!-- primary=true 表示以该对象为主,当从容器中根据类型来获取的时候,拿到是这个对象 -->
  	<!-- 可以避免 NoUniqueBeanDefinitionException 异常的发生 -->
    <bean id="computer" class="org.example.ioc.Computer" primary="true">
        <!-- property 会找到 Computer类的 setDisk方法来将属性设置到对象中 -->
        <property name="disk" ref="ssdDisk"></property>
        <property name="name" value="联想电脑"></property>
    </bean>
    
    <!-- 通过构造器的方式来实现注入 -->
    <bean id="computerTwo" class="org.example.ioc.Computer">
        <constructor-arg name="disk" ref="ssdDisk"></constructor-arg>
        <constructor-arg name="name" value="dell"></constructor-arg>
    </bean>
</beans>

第二步,创建容器,并获取IOC容器中的内容:

IOC容器中的所有bean都会提前创建好,而不是调用的时候再去创建

public class IocTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 IOC 容器
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("META-INF/application-context.xml");
        // 获取到IOC中的对象
		// Computer c = (Computer)ctx.getBeam("computer");
        
        // 可以根据类型来获取;因为目前对于上面的配置来说,我们有两个 Computer对象,拿到的是 
        // 加了 Primary = true 这个对象
        Computer c = ctx.getBeam(Computer.class);
    }
}

1.2 特殊类型注入

特殊类型指的是:

  1. 引用类型;
  2. List
  3. Map
  4. Set
  5. Propety
  6. 数组

例如给定如下的类:

public class Person {
    private String name;
    private Date birthday;
  	private List<Dog> dogs
    private List<String> friends;
    private Set<String> account;
    private Map<String, String> addresses;
    private Properties prop;
    private String[] array;
    // setter and getter
}
public class Dog {
  private String name;
  private Integer age;
}
<?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="birthday" class="java.util.Date"></bean>
    
 	<bean id="person" class="org.Person">
    	<property name="name" value="张三"></property>
        <property name="birthday" ref="birthday"></property>
      	<property name="dogs">
          	<!-- List对象注入 -->
          	<List>
              	<bean class="org.Dog">
                  	<property name="name" value="大黄"></property>
                  	<property name="age" value="5"></property>
              	</bean>
              	<bean class="org.Dog">
                  	<property name="name" value="小白"></property>
                  	<property name="age" value="2"></property>
              	</bean>
          	</List>
      	</property>
        <property name="friends">
            <!-- list注入 -->
        	<list>
            	<value>李四</value>
                <value>王五</value>
            </list>
        </property>
        <property name="account">
        	<!-- set注入 -->
        	<set>
            	<value>李四</value>
                <value>王五</value>
            </set>
        </property>
        <property name="addresses">
        	<!-- map注入 -->
        	<map>
            	<entry name="home-address" value="洪山"></entry>
                <entry name="home-address" value="江夏"></entry>
            </map>
        </property>
         <property name="prop">
        	<!-- map注入 -->
        	<props>
            	<prop name="usename">root</prop>
                <prop name="password">123456</prop>
            </props>
        </property>
        <!-- 数组注入可以通过 , 将值进行分隔 -->
        <!-- <property name="array" value="a,b,c"></property> -->
        <property name="array">
        	<array>
            	<value>a</value>
                <value>b</value>
                <value>c</value>
            </array>
        </property>
    </bean>
 </beans>

1.3 自动装配

在 “配置” 的方式上有一个属性 autowire="byName | byType" 两种方式:

  1. byName 是严格按照名字到容器中进行查找,就算有primary="true"也会根据名字找。
  2. byType 是根据属性的类型到容器中进行查找,如果多个会报错,除非在其中一个加上 primary=“true” 这个属性

1.4 单例和非单例模式

在 “配置” 的方式上有一个属性 scope="singleton | prototype" 两种方式创建bean:

  1. singleton 表示从容器中获取的永远是单例的。
  2. prototype表示每次从容器中获取的都是一个新的对象。

二. bean的生命周期

关于 spring中管理的bean的生命周期在spring的一个接口 BeanFactory(是spring容器的*接口) 的描述上有准确的描述,但是目前来说,先简单理解 spring 的生命周期:

  1. 实例化,调用构造方法。
  2. 设置属性。
  3. 初始化,调用 init-method(配置的方式) 对应的方法
  4. 创建完毕。
  5. 销毁阶段,调用 destory-method(配置的方式) 对应的方法
public class LifeCycleBean {
    
    private String name;
    
    public void setName(String name) {
        System.out.println("setName 方法被调用了...");
        this.name = name;
    } 
    
    public LifeCycleBean() {
        System.out.println("构造方法被调用了...");
    }
    
    public void init() {
         System.out.println("init 方法被调用了...");
    }
    
    public void destroy() {
        System.out.println("destroy 方法被调用了...");
    }
}
<?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">
    
    <!-- 
		对应的 org.LifeCycleBean 输入的结果:
            1. 构造方法被调用了...
			2. setName 方法被调用了...
			3. init 方法被调用了...
			4. destroy 方法被调用了... 
	-->
    <bean id="lifeCycleBean" class="org.LifeCycleBean" init-method="init" 
          destroy-method="destroy">
    	<property name="name" value="XXX"></property>
    </bean>
 </beans>

单例bean:singleton

随工厂启动创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》随工厂关闭销毁

多例bean:prototype

被使用时创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》JVM垃圾回收销毁

三. 注入的注解形式开发

spring提供两套创建 IOC 容器的方式:

  1. 配置的方式,这是历史遗留问题。
  2. 注解的方式,这也是未来的主流的方式。(注解创建成员对象的方式是通过构造器)

在三阶段,我们都是混合使用;在四阶段(包括以后工作) 都是适应注解的方式。

@ComponentScan(basePackages = "com.qf")
public class IocTestWithAnnotation {
    public static void main(String[] args) {       
      	/**
      	 * 通过注解的方式来创建 IOC容器,传递给 AnnotationConfigApplicationContext 的构造方法的
      	 * 类会被实例化并纳入到IOC容器中。然后通过它头顶 @ComponentScan(basePackages = "com.qf") 递归
      	 *  扫描 com.qf 下所有的类和接口,然后将他们头顶上带 @Service @Repository @Component 标注的类
      	 *  按照spring-bean的生命的方式来创建后纳入到IOC容器中。
      	 */
        ApplicationContext ctx = new AnnotationConfigApplicationContext(
            IocTestWithAnnotation.class);
        
        UserService userService = ctx.getBean(UserService.class);

        userService.add();
        userService.delete(45);
        
    }
}

此处注解相当于配置文件中的

@Repository: 这个注解放到 DAO 的实现类上。

@Sercvice: 这个注解放到 Serviece 的实现类上。`

@Component: 性质不明确的时候,就加上该注解。

此处注解相当于配置文件中的autowire="byType | byName "

@Autowired:首先根据类型查找,如果多个,就看谁的头顶的有 @Primary,就注入谁;如果没有没有@Primary 注解,就按照名字来进行注入,如果名字都没有区分就报异常。

@Qualifier: 它的出现就是为了和 @Autowire 搭配使用的,就是指定一个名字,在注入的时候根据类型的时候同时判断名字来实现注入,无视名字和@Primary注解,只根据@Qualifier来创建。

@Resource:首先根据名字来实现注入,如果命名没有相同,就根据类型查找,如果类型有多个,就看谁的大头顶有 @Primary 注解,名字优先于@Primary。

此处的注解相当于init-method | destroy-method配置

@PostConstruct: 对应着 init-method 这个配置的一个注解,是spring bean的生命周期的一个注解。在Spring中碰到方法名或者注解中带有 Post, 表示在XXXX之后。

@PreDestroy: 对应着destroy-method这个配置的一个注解,销毁的时候执行的方法。在Spring中碰到方法名或者注解中带有 Pre, 表示在XXXX之前。

四. AOP

AOP(Aspect Oriented Programming): 面向切面的编程。说白就是可以在方法执行的某个时机做一些其他的工作;他是对应 OOP 的一种增强。AOP底层的实现都是通过动态代理(JDK原生方式和CGLIB的方式)来实现的,在spring框架中,提供了两种实现的方式:

  1. spring原生的方式。(了解即可)
  2. 通过AspectJ框架的方式。

面试: OOP和AOP的区别?

答:OOP面向对象编程,只能通过纵向的添加的代码的方式来实现增强的功能;AOP可以实现在不改变原有方法的代码基础之上实现对方法的横向扩展。

连接点:类中方方法。

切入点:就是我们所关注的方法。

通知:就是当我们所关注的切入点,在发生某个行为的时候,出发某个调用。

4.1 AOP开发术语

  • 连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。
  • 切入点(Pointcut):被切入连接点。
  • 通知、增强(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知等。
  • 目标对象(Target):代理的目标对象
  • 织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。
  • 代理(Proxy):被AOP织入通知后,产生的结果类。
  • 切面(Aspect):由切点和通知组成,将横切逻辑织入切面所指定的连接点中。

4.2 通配切入点

根据表达式通配切入点

<!--匹配参数-->
<aop:pointcut id="myPointCut" expression="execution(* *(com.qf.aaron.aop.basic.User))" />
<!--匹配方法名(无参)-->
<aop:pointcut id="myPointCut" expression="execution(* save())" />
<!--匹配方法名(任意参数)-->
<aop:pointcut id="myPointCut" expression="execution(* save(..))" />
<!--匹配返回值类型-->
<aop:pointcut id="myPointCut" expression="execution(com.qf.aaron.aop.basic.User *(..))" />
<!--匹配类名-->
<aop:pointcut id="myPointCut" expression="execution(* com.qf.aaron.aop.basic.UserServiceImpl.*(..))" />
<!--匹配包名-->
<aop:pointcut id="myPointCut" expression="execution(* com.qf.aaron.aop.basic.*.*(..))" />
<!--匹配包名、以及子包名-->
<aop:pointcut id="myPointCut" expression="execution(* com.qf.aaron.aop..*.*(..))" />

4.3 AspectJ对于AOP的实现

第一步,创建一个接口,然后定义一个实现类。

public interface StudentService {

    void deleteStudent(Integer id);

    void add();
}
public class StudentServiceImpl implements StudentService {
    public void deleteStudent(Integer id) {
        System.out.println("删除学生:" + id);
    }

    @Override
    public void add() {
        System.out.println("add");
    }
}

第二步,创建一个通知类,类的头顶必须要有 @Aspect 这个注解

@Aspect
public class AopAdvisor {

    /**
     *  “* org.example.aop.service.impl.*.*(..)” 第一个 * org.example 表示人任意的返回值
     *     org.example.aop.service.impl.*.*  org.example.aop.service.impl下所有的类的所有的方法。
     *     (..) 表示接受任意的参数
     * @Before() 表示在执行这些方法之前。
     */
    @Before("execution(* org.example.aop.service.impl.StudentServiceImpl.deleteStudent(..))")
    public void before(JoinPoint jp) {
        System.out.println("UserService下的方法调用之前被执行了...");
        // 获取方法的签名
        MethodSignature signature = (MethodSignature)jp.getSignature();
		//获取方法的对象
        Method method = signature.getMethod();

        System.out.println("方法执行之前打印方法名: " + method.getName());
    }
    
    // 是方法无论正确获取不正确都会执行
    @After("execution(* org.example.aop.service.impl.StudentServiceImpl.add*(..))")
    public void after(JoinPoint jp) {
         System.out.println("方法无论是否抛出异常都会执行");
    }
    
    
    // 方法正确执行了,才会执行该方法
    @AfterReturning("execution(* org.example.aop.service.impl.StudentServiceImpl.add*(..))")
    public void after(JoinPoint jp) {
         System.out.println("方法正确执行了,才会执行该方法");
    }
    
    
     /**
      * 1. 只有当关注的方法抛出 NullPointerException 这个异常方法才会执行;
      * 2. 要保证 throwing的值和方法中异常的形参名要一致。
      * 3. 如果想让该方法处理所有的异常,要使用 Exception
      */
     @AfterThrowing(value = "execution(* org.example.service.impl.UserServiceImpl.add*(..))", throwing = "npe")
     public void catchSpecifyException(JoinPoint jp, NullPointerException npe) {
         System.out.println("方法抛出了 NPE 异常");
    }
    
    /**
     * 表示环绕通知。
     */ 
    @Around("execution(* org.example.service.impl.UserServiceImpl.add*(..))")
    public void around(ProceedingJoinPoint pjp) throws Exception{

      	Object[] args = pjp.getArgs();  // 表示获取方法执行的所有的参数
      	//获取方法的签名
        MethodSignature signature = (MethodSignature)pjp.getSignature();
        //获得当前方法对象
        Method method = signature.getMethod();
        //通过方法获得字节码对象,然后通过字节码对象来获取简单类名
        System.out.println(method.getDeclaringClass().getSimpleName());
      
        Object obj = pjp.proceed(); // 执行目标对象的方法
        
        // 做其他
        return obj;
    }
}

第三步,在配置文件中进行配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns="http://www.springframework.org/schema/beans"
        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.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

	<!-- 被代理的类必须要纳入到IOC容器中 -->
    <bean id="studentService" class="org.example.aop.service.impl.StudentServiceImpl"></bean>

    <bean id="advisor" class="org.example.aop.AopAdvisor"></bean>

    <!-- 表示开启 aspectj
        可以通过 proxy-target-class="true" 来控制使用 CGLIB 的方式来实现代理,false表示使用原生JDK的方式来实现动态代理;
        如果被代理的类没有接口,那么尽管指定使用jdk的代理方式,但是也还是会使用CGLIB的方式来实现动态代理。
    -->
    <aop:aspectj-autoproxy proxy-target-class="false"></aop:aspectj-autoproxy>
</beans>

五. spring与mybatis的整合

第一步,导入依赖:

<dependencies>
     <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.3.3</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.7</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.20</version>
    </dependency>
    <dependency>
      <groupId>com.github.pagehelper</groupId>
      <artifactId>pagehelper</artifactId>
      <version>5.1.9</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
    <!-- 是mybatis和spring整合的中间包 -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>2.0.6</version>
    </dependency>

    <!-- 连接池,功能和 druid 是一样的 -->
    <dependency>
      <groupId>com.zaxxer</groupId>
      <artifactId>HikariCP</artifactId>
      <version>2.6.1</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.3.3</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>5.3.3</version>
    </dependency>
</dependencies>

第二步,配置

<?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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">


    <!-- 当通过配置文件的方式来创建容器的时候,会根据 base-package="org.example" 进行递归扫描: @Service @Component, 纳入到IOC容器中 -->
    <context:component-scan base-package="org.example"></context:component-scan>

    <!-- 配置 HikariCP 的连接池 -->
    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/qf?serverTimezone=Asia/Shanghai"></property>
        <!-- 连接池中最大的连接的数量 -->
        <property name="maximumPoolSize" value="20"></property>
        <!-- 测试连接是否有效的 sql 语句 -->
        <property name="connectionTestQuery" value="select 1"></property>
    </bean>

    <!--
        之前在单独使用 MyBatis的时候,我们自己通过 mybatis-config.xml 核心配置文件来自己创建一个 SqlSessionFactory;
        但是在和Spring整合的时候,将该项工作交给 Spring容器了。
     -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 配置数据源 -->
        <property name="dataSource" ref="dataSource"></property>
        <!--
            "classpath:" 是spring提供的一个用来定位文件路径的缩写;
            classpath:org/example/**/*.xml 扫描 org/example 下所有的后代包的 xml文件
         -->
        <property name="mapperLocations" value="classpath:org/example/**/*.xml"></property>

        <!-- 设置分页插件 -->
        <property name="plugins">
            <array>
                <bean class="com.github.pagehelper.PageInterceptor">
                    <!-- 分页插件的配置 -->
                    <property name="properties">
                        <props>
                            <!-- 设置方言 -->
                            <prop key="helperDialect">mysql</prop>
                            <!-- 设置分页合理化 -->
                            <prop key="reasonable">true</prop>
                        </props>
                    </property>
                </bean>
            </array>
        </property>
        <!-- 配置日志打印的 -->
        <property name="configuration">
            <bean class="org.apache.ibatis.session.Configuration">
                <property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"></property>
            </bean>
        </property>
    </bean>

    <!-- 将所有的 Mapper纳入到容器中
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 因为获取 Mapper需要需要通过 SqlSession, 这里我们直接提供工厂就可以了  -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>

        <!-- 这是找那些 XXMapper 的基础包;只找接口; 需要和 annotationClass 配置使用;表示只找头顶有 @Mapper 注解的接口。-->
        <property name="basePackage" value="org.example"></property>
        <property name="annotationClass" value="org.apache.ibatis.annotations.Mapper"></property>
    </bean>
</beans>

六. spring的单元测试

第一步,引入依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.3</version>
    <scope>test</scope>
</dependency>

第二部,测试类的编写

@RunWith(SpringJUnit4ClassRunner.class)  // 用于spring测试
@ContextConfiguration("classpath:META-INF/application-context.xml")
public class UserServiceTest {

    @Autowired
    private IUserService userServiceImpl;

    @Test
    public void getPageData() {
        List<User> list = userServiceImpl.getPageData(2, 10);
        list.forEach(u -> System.out.println(u.getEmail() + "#" + u.getName()));
    }

    @Test
    public void validateTx() {
        userServiceImpl.validateTx();
    }
}