1. Spring 框架了解

  • Spring 是分层的 Java SE/EE 应用 full-stack 轻量级开源框架,以 IoC(Inverse Of Control:反转控制)和 AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现层 Spring MVC 和持久层 Spring JDBC 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的 Java EE 企业应用开源框架。
  • Spring 的优势
    • 方便解耦,简化开发
      通过 Spring 提供的 IoC 容器,可以将对象间的依赖关系交由 Spring 进行控制,避免硬编码所造成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。
    • AOP 编程的支持
      通过 Spring 的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松应付。
    • 声明式事务的支持
      可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量。
    • 方便程序的测试
      可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情
    • 方便集成各种优秀框架
      Spring 可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、Quartz
      等)的直接支持。
    • 降低 JavaEE API 的使用难度
      Spring 对 JavaEE API(如 JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些 API 的使用难度降低。
    • 源码是经典学习范例
      Spring 的源代码设计精妙、结构清晰、匠心独用,处处体现着大师对 Java 设计模式灵活运用以及对 Java 技术的高深造诣。它的源代码无意是 Java 技术的最佳实践的范例。
  • Spring 的体系结构

2. 工厂模式解耦

  • Bean:有可重用组件的含义。

  • JavaBean:用 Java 语言编写的可重用组件,javabean > 实体类

  • 工厂模式:一个创建 Bean 对象的工程,是创建 service 和 dao 的对象的。

  • 工厂模式解耦:

    • 需要一个配置文件来配置 service 和 dao,配置内容:唯一标识 = 全限定类名(key=value)
    • 通过读取配置文件中的内容,反射创建对象,避免通过 new 关键字来达到解耦。
  • 解耦例子:
    bean.properties,定义接口所对应的实现类的全类名

    accountService=pers.ylq.service.impl.AccountServiceImpl
    accountDao=pers.ylq.dao.impl.AccountDaoImpl
    

    创建工厂类,根据接口名字返回实现类对象

    package pers.ylq.factory;
    
    import java.io.InputStream;
    import java.util.Properties;
    
    public class BeanFactory {
        private static Properties props;
    
        static {
            try {
                props = new Properties();
                InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
                props.load(is);
            } catch (Exception e) {
                throw new ExceptionInInitializerError("初始化properties失败!");
            }
        }
    
        public static Object getBean(String beanName) {
            Object bean = null;
            String beanPath = props.getProperty(beanName);
            try {
                bean = Class.forName(beanPath).newInstance();
            } catch (Exception e) {
            }
            return bean;
        }
    }
    

    在 service 中利用以下语句获取对象:
    private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
    代替以前的
    private IAccountDao accountDao = new AccountDaoImpl();
    servlet 中同理用工厂获取 service 实现类对象。
    这样,即使实现类对象不存在,在编译期程序也不会报错,运行才报错,达到了解耦的目的。

  • 上面例子存在的问题:

    • 每调用一次都会创建一个新的对象,是多例的。
    • 多例不存在线程安全问题,因为每个用户的对象都不同,不会去访问同一个资源。
    • 单例存在线程安全问题,多个用户使用同一个对象,去访问成员变量时就会出现问题。
    • 但在 service 和 dao 中并不需要成员变量,也就是可以通过改为单例来提高程序执行效率。
    • 所以,第一次创建完对象以后,需要将对象存储起来。
  • 改进完以后的工厂

    public class BeanFactory {
        private static Properties props;
        private static Map<String, Object> beans;
    
        static {
            try {
                props = new Properties();
                InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
                props.load(is);
                beans = new HashMap<String, Object>();
                //取出配置文件中的数据存入容器
                Enumeration<Object> keys = props.keys();
                while (keys.hasMoreElements()) {
                    String key = keys.nextElement().toString();
                    String beanPath = props.getProperty(key);
                    Object value = Class.forName(beanPath).newInstance();
                    beans.put(key, value);
                }
    
            } catch (Exception e) {
                throw new ExceptionInInitializerError("初始化properties失败!");
            }
        }
    
        public static Object getBean(String beanName) {
            return beans.get(beanName);
        }
    }
    

    这样,每次从工厂获取的对象都是同一个,单例模式,提高了效率,但要注意不要使用成员变量,以免出现线程安全问题。

3. IoC 反转控制

  • IoC 反转控制:把创建对象的权利交给框架,是框架的重要特征,削减计算机程序的耦合。

  • pom.xml 配置:

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    
  • 入门案例:

    配置 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="accountService" class="pers.ylq.service.impl.AccountServiceImpl"/>
        <bean id="accountDao" class="pers.ylq.dao.impl.AccountDaoImpl"/>
    </beans>
    
    • id 为唯一标识,用来获取
    • class 为所对应的类的全类名

    使用案例

    public static void main(String[] args) {
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        //也可通过重载不强制转换
        IAccountDao adao = ac.getBean("accountDao", IAccountDao.class);
    }
    
  • ApplicationContext 的三个常用实现类:

    • ClassPathXmlApplicationContext:它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。(更常用)
    • FileSystemXmlApplicationContext:它可以加载磁盘任意路径下的配置文件(必须有访问权限)
    • AnnotationConfigApplicationContext:它是用于读取注解创建容器的。
  • 核心容器的两个接口引发出的问题:

    • ApplicationContext:单例对象适用,我们一般采用此接口
      • 它在构建核心容器时,创建对象采取的策略是采用立即加载的方式。也就是说,只要一读取完配置文件马上就创建配置文件中配置的对象。
    • BeanFactory:多例对象适用
      • 它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据 id 获取对象了,什么时候才真正的创建对象。
  • 创建 Bean 的三种方式

    1. 使用默认构造函数创建

      <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
      
    2. 使用普通工厂中的方法创建对象(使用某个类中的方法创建对象),并存入 Spring 容器

      <bean id="instanceFactory" class="com.itheima.factory.InstanceFactory"></bean>
      <bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"></bean>
      

      factory-bean:指定所用的工厂的 id
      factory-method:指定所用工厂获取对象的方法名

    3. 使用工厂中的静态方法创建对象(使用某个类中的静态方法创建对象),并存入 Spring 容器

      <bean id="accountService" class="com.itheima.factory.StaticFactory" factory-method="getAccountService"></bean>
      

      class:工厂类的全类名
      factory-method:指定所用工厂获取对象的静态方法名

  • bean 的作用范围:bean 标签的 scope 属性,单例和多例最常用

    • singleton:单例的(默认值)
    • prototype:多例的
    • request:作用于 Web 应用的请求范围
    • session:作用于 Web 应用的会话范围
    • global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是 session。
  • bean 的生命周期

    • 单例对象
      • 出生:当容器创建时对象出生
      • 活着:只要容器还在,对象一直活着
      • 死亡:容器销毁,对象消亡
      • 总结:单例对象的生命周期和容器相同
    • 多例对象
      • 出生:当我们使用对象时 Spring 框架为我们创建
      • 活着:对象只要是在使用过程中就一直活着。
      • 死亡:当对象长时间不用,且没有别的对象引用时,由 Java 的垃圾回收器回收。
    • 在 bean 标签中,有 init-method 和 destory-method 两个属性,指定对象建立和对象销毁时执行的方法。

4. DI 依赖注入

  • DI:依赖注入(Dependency Injection)

  • 依赖关系的管理:在当前类需要用到其他类的对象,由 Spring 为我们提供,我们只需要在配置文件中说明。

  • 能注入的数据类型

    • 基本类型和 String
    • 其他 bean 类型(在配置文件中或者注解配置过的 bean)
    • 复杂类型/集合类型
  • 注入的方式:

    • 使用构造函数提供
    • 使用 set 方法提供
    • 使用注解提供
  • 构造函数注入:在 bean 标签内部使用标签 constructor-arg 来提供构造方法的参数,前提是有对应的构造方法

    <bean id="accountService" class="pers.ylq.service.impl.AccountServiceImpl">
        <constructor-arg name="username" value="张三"/>
        <constructor-arg name="age" value="18"/>
        <constructor-arg name="date" ref="now"/>
    </bean>
    <bean id="now" class="java.util.Date"/>
    

    constructor-arg 标签的属性

    • type:给指定类型的参数赋值(参数有多个相同类型则无法使用),了解
    • index:给指定索引的参数赋值。索引的位置是从 0 开始,了解
    • name:给构造方法中指定名称的参数赋值。常用
    • ===== 以上三个用于指定给构造函数中哪个参数赋值 =====
    • value:用于提供基本类型和 String 类型的数据
    • ref:用于指定其他的 bean 类型数据。它指的就是在 Spring 的 IoC 核心容器中出现过的 bean 对象
  • 构造函数注入

    • 优势:在获取 bean 对象时,注入数据是必须的操作,否则对象无法创建成功。
    • 改变了 bean 对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供。
  • set 方法注入(常用):在 bean 标签内部使用 property 标签,来调用 set 方法注入值。

    <bean id="now" class="java.util.Date"/>
    <bean id="accountService2" class="pers.ylq.service.impl.AccountServiceImpl2">
        <property name="username" value="张三"/>
        <property name="age" value="18"/>
        <property name="date" ref="now"/>
    </bean>
    
    • 类中需要有对应的 set 方法。
    • name:用于属性指定注入时所调用的 set 方法
    • value:用于提供基本类型和 String 类型的数据
    • ref:用于指定其他的 bean 类型数据。它指的就是在 Spring 的 IoC 核心容器中出现过的 bean 对象。
  • set 方法注入:

    • 优势:创建对象时没有明确的限制,可以直接使用默认构造函数
    • 弊端:如果有某个成员必须有值,则获取对象是有可能 set 方法没有执行。
  • 复杂类型的注入/集合类型的注入

    <bean id="accountService3" class="pers.ylq.service.impl.AccountServiceImpl3">
        <property name="mySet">
            <set>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </set>
        </property>
        <property name="myList">
            <list>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </list>
        </property>
        <property name="myMap">
            <map>
                <entry key="testA" value="aaa"></entry>
                <entry key="testB">
                    <value>BBB</value>
                </entry>
            </map>
        </property>
        <property name="myProps">
            <props>
                <prop key="testC">ccc</prop>
                <prop key="testD">ddd</prop>
            </props>
        </property>
    </bean>
    
    • 用于给 List 结构集合注入的标签:list、array、set
    • 用于个 Map 结构集合注入的标签: map、props
    • 结构相同,标签可以互换

5. Spring 的常用注解

  • 使用注解的前提:

    • 需要使用 xmlns:context 命名空间
    • 需要指定要扫描的包,扫描包及其子包的注解
    <?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.xsd">
    
        <context:component-scan base-package="pers.ylq"/>
    </beans>
    
  • 用于创建对象的注解

    • Component:用于把当前类对象存入 Spring 容器中
      • 属性 value:用于指定 bean 的 id。当我们不写时,它的默认值是当前类名,且首字母改小写。
    • Controller:一般用在表现层
    • Service:一般用在业务层
    • Repository:一般用在持久层
    • 后三个是 Spring 框架提供明确的三层使用的注解,使我们的三层对象更加清晰,作用和属性与 Component 一致。
  • 用于注入数据的注解

    • Autowired:自动按照类型注入。

      • 位置:可以是变量上,也可以是方法上。
      • 细节:可以不添加 set 方法就能注入。
      • 只要容器中有唯一的一个 bean 对象类型和要注入的变量类型匹配,就可以注入成功
      • 如果类型匹配的有多个,则会用变量名和 bean 对象类型的 id 去进行匹配,若一致,也会注入成功。
    • Qualifier:在按照类中注入的基础之上(@Autowired)再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以单独使用。

      @Autowried
      @Qualifier("id")
      

      属性 value:用于指定注入 bean 的 id。

    • Resource:直接按照 bean 的 id 注入。它可以独立使用,此注解由 J2EE 提供,不能作用方法与参数。

      • 属性 name:用于指定 bean 的 id。
    • Value:用于注入基本类型和 String 类型的数据

      • 属性 value:用于指定数据的值。它可以使用 Spring 的 EL 表达式,${表达式}
    • 集合类型的注入只能通过 XML 来实现。

  • 用于改变作用范围的注解

    • Scope:用于指定 bean 的作用范围
      • 属性 value:指定范围的取值。常用取值:singleton(单例)、prototype(多例)
  • 和生命周期相关注解(了解)

    • 作用就和在 bean 标签中使用 init-method 和 destroy-methode 的作用是一样的
    • PostConstruct:用于指定初始化方法
    • PreDestroy:用于指定销毁方法

6. Spring 新注解

  • Configuration:指定当前类是一个 Spring 配置类
    • 当使用注解配置 Spring 的时候,通过 ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); 来获取容器。
    • 当配置类作为 AnnotationConfigApplicationContext 对象创建的参数时,该注解可以不写,否则需要扫描该类所在的包并用此注解声明此类是一个配置类或者使用 Impor 注解导入到主配置文件。
  • ComponentScan:用于通过注解指定 Spring 在创建容器时要扫描的包
    • 属性 value:用于指定创建容器时要扫描的包。使用该注解就不需要在 XML 中使用 context:component-scan 标签。
    • 属性 basePackages:和 value 一样。
  • Bean:用于把当前方法的返回值作为 bean 对象存入 Spring 的 IoC 容器中
    • 属性 name:用于指定 bean 的 id。默认值是当前方法的名称
    • 当我们使用注解配置方法时,如果方法有参数,Spring 框架会去容器中查找有没有可用的 bean 对象,查找的方式和 Autowired 注解的作用是一样的,可以使用 Qualifier 去指定使用的 bean 对象。
    • @Bean(name = "queryRunner")
      @Scope("prototype")
      public QueryRunner createQueryRunner(@Qualifier("ds1") DataSource dataSource) {
          return new QueryRunner(dataSource);
      }
      
  • Import:用于导入其他的配置类
    • 属性 value:用于指定其他配置类的字节码,当我们使用 Import 的注解之后,此类就父配置类,而导入的都是子配置类。
  • PropertySource:用于指定 properties 文件的位置
    • 属性 value:指定文件的名称和路径。

      • 关键字:classpath,表示类路径下
    • @PropertySource("classpath:jdbcConfig.properties")
      public class JdbcConfiguration {
          @Value("${driver}")
          private String driver;
          @Value("${url}")
          private String url;
          @Value("${jdbc.username}")
          private String username;
          @Value("${password}")
          private String password;
          @Bean(name = "dataSource")
          public DataSource createDataSource() {
              ComboPooledDataSource ds = new ComboPooledDataSource();
              try {
                  ds.setDriverClass(driver);
              } catch (PropertyVetoException e) {
                  e.printStackTrace();
              }
              ds.setJdbcUrl(url);
              ds.setUser(username);
              ds.setPassword(password);
              return ds;
          }
      }
      
      • ${username} 取出的是主机名,不是数据库用户名
  • 注解用于自己定义的类时方便,XML 在引入第三方类时方便。

7. Spring 整合 Junit

  • 我们需要程序能自动帮我们创建容器,能够在测试类中进行依赖注入。
  1. 导入 Spring 整合 junit 的 jar(坐标)

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    
  2. 使用 Junit 提供的注解 @Runwith 把原有的 main 方法替换了,替换成 Spring 提供的

  3. 告知 Spring 的运行器,Spring 和 IoC 创建是基于 XML 还是注解的,并且说明位置

    • @ContextConfiguration
      • locations:指定 xml 文件的位置,加上 classpath 关键字,表示在类路径下
      • classes:指定注解类所在地位置
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfiguration.class)
    //@ContextConfiguration(locatios = "classpath:bean.xml")
    public class AccountServiceTest {
        @Autowired
        private IAccountService as = null;
        @Test
        public void testFindAll() {
            List<Account> accounts = as.findAllAccount();
        }
    }
    

8. 银行转账案例

  • 转账过程中的事务问题
  • 可见,每次操作数据库都是一个单独的事务,事务管理发生在持久层,我们需要让事务管理由业务层管理。
  • ThreadLocal 是线程的局部变量存储容器,可以在线程中通过 set()get() 存取变量。
  • 解决转账事务问题:
    • 编写连接工具类,将线程和连接进行绑定
      package com.itheima.utils;
      
      import javax.sql.DataSource;
      import java.sql.Connection;
      
      /**
       * 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
       */
      public class ConnectionUtils {
      
          private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
      
          private DataSource dataSource;
      
          public void setDataSource(DataSource dataSource) {
              this.dataSource = dataSource;
          }
      
          /**
           * 获取当前线程上的连接
           * @return
           */
          public Connection getThreadConnection() {
              try{
                  //1.先从ThreadLocal上获取
                  Connection conn = tl.get();
                  //2.判断当前线程上是否有连接
                  if (conn == null) {
                      //3.从数据源中获取一个连接,并且存入ThreadLocal中
                      conn = dataSource.getConnection();
                      tl.set(conn);
                  }
                  //4.返回当前线程上的连接
                  return conn;
              }catch (Exception e){
                  throw new RuntimeException(e);
              }
          }
      
          /**
           * 把连接和线程解绑
           */
          public void removeConnection(){
              tl.remove();
          }
      }
      
    • 编写事务管理类
      package com.itheima.utils;
      
      /**
       * 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
       */
      public class TransactionManager {
      
          private ConnectionUtils connectionUtils;
      
          public void setConnectionUtils(ConnectionUtils connectionUtils) {
              this.connectionUtils = connectionUtils;
          }
      
          /**
           * 开启事务
           */
          public  void beginTransaction(){
              try {
                  connectionUtils.getThreadConnection().setAutoCommit(false);
              }catch (Exception e){
                  e.printStackTrace();
              }
          }
      
          /**
           * 提交事务
           */
          public  void commit(){
              try {
                  connectionUtils.getThreadConnection().commit();
              }catch (Exception e){
                  e.printStackTrace();
              }
          }
      
          /**
           * 回滚事务
           */
          public  void rollback(){
              try {
                  connectionUtils.getThreadConnection().rollback();
              }catch (Exception e){
                  e.printStackTrace();
              }
          }
      
      
          /**
           * 释放连接
           */
          public  void release(){
              try {
                  connectionUtils.getThreadConnection().close();//还回连接池中
                  connectionUtils.removeConnection();
              }catch (Exception e){
                  e.printStackTrace();
              }
          }
      }
      
    • 在 dao 层使用 connectionUtils 获取连接
      public class AccountDaoImpl implements IAccountDao {
      
          private QueryRunner runner;
          private ConnectionUtils connectionUtils;
      
          public void setRunner(QueryRunner runner) {
              this.runner = runner;
          }
      
          public void setConnectionUtils(ConnectionUtils connectionUtils) {
              this.connectionUtils = connectionUtils;
          }
      
          @Override
          public void updateAccount(Account account) {
              try {
                  runner.update(connectionUtils.getThreadConnection(),"update account set name=?,money=? where id=?", account.getName(), account.getMoney(), account.getId());
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      }
      
    • 在 service 层进行事务的管理
      public class AccountServiceImpl_OLD implements IAccountService {
      
          private IAccountDao accountDao;
          private TransactionManager txManager;
      
          public void setTxManager(TransactionManager txManager) {
              this.txManager = txManager;
          }
      
          public void setAccountDao(IAccountDao accountDao) {
              this.accountDao = accountDao;
          }
      
          @Override
          public void transfer(String sourceName, String targetName, Float money) {
              try {
                  //1.开启事务
                  txManager.beginTransaction();
                  //2.执行操作
                  //2.1根据名称查询转出账户
                  Account source = accountDao.findAccountByName(sourceName);
                  //2.2根据名称查询转入账户
                  Account target = accountDao.findAccountByName(targetName);
                  //2.3转出账户减钱
                  source.setMoney(source.getMoney()-money);
                  //2.4转入账户加钱
                  target.setMoney(target.getMoney()+money);
                  //2.5更新转出账户
                  accountDao.updateAccount(source);
                  //int i=1/0;
                  //2.6更新转入账户
                  accountDao.updateAccount(target);
                  //3.提交事务
                  txManager.commit();
              }catch (Exception e){
                  //4.回滚操作
                  txManager.rollback();
                  e.printStackTrace();
              }finally {
                  //5.释放连接
                  txManager.release();
              }
          }
      }
      

9. 基于子类的动态代理

  • 动态代理:
    • 特点:字节码随用随创建,随用随加载
    • 作用:不修改源码的基础上对方法增强
    • 分类:
      • 基于接口的动态代理
      • 基于子类的动态代理
    • 基于子类的动态代理:
      • 涉及的类:Enhancer
      • 提供者:第三方 cglib 库
    • 如何创建代理对象:
      • 使用 Enhancer 类中的 create 方法
    • 创建代理对象的要求:
      • 被代理类不能是最终类
    • create 方法的参数:
      • Class:被代理对象的字节码
        • 它是用于指定被代理对象的字节码。
      • Callback:用于提供增强的代码
        • 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
        • 我们一般写的都是该接口的子接口实现类:MethodInterceptor
  •    package cglib;
    
    
       import net.sf.cglib.proxy.Enhancer;
       import net.sf.cglib.proxy.MethodInterceptor;
       import net.sf.cglib.proxy.MethodProxy;
    
       import java.lang.reflect.Method;
    
       public class Client {
    
           public static void main(String[] args) {
               final Producer producer = new Producer();
               Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
                   @Override
                   public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                       Object returnValue = null;
                       Float money = (Float) objects[0];
    
                       if ("saleProduct".equals(method.getName())) {
                           returnValue = method.invoke(producer, money * 0.8f);
                       }
                       return returnValue;
                   }
               });
               cglibProducer.saleProduct(12000);
           }
       }
    

10. AOP 面向切面编程

  • AOP:简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。

  • AOP 的作用:在程序运行期间,不修改源码对已有方法进行增强。

  • AOP 的实现方式:使用动态代理技术。

  • AOP 相关术语:

    • Joinpoint (连接点):所谓连接点是指那些被拦截到的点。在 Spring 中,这些点指的是方法,因为 Spring 只支持方法类型的连接点。
    • Pointcut (切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
    • Advice (通知/增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
      • 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
    • Introduction (引介):引介是一种特殊的通知在不修改类代码的前提下,Introduction 可以在运行期为类动态地添加一些方法或 Field。
    • Target (目标对象):代理的目标对象。
    • Weaving (织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
    • Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类。
    • Aspect (切面):是切入点和通知(引介)的结合。
  • 基于 XML 的 AOP 配置步骤

    1. 导入解析切入点表达式的 jar 包
      <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.8.7</version>
      </dependency>
      
    2. 使用 aop:config 标签表明开始 AOP 的配置
    3. 使用 aop:aspect 标签表明配置切面
      • id 属性:是给切面提供一个唯一标识
      • ref 属性:是指定通知类 bean 的 Id。
    4. 在 aop:aspect 标签的内部使用对应标签来配置通知的类型
      • aop:before:表示配置前置通知
      • aop:after-returning:后置通知
      • aop:after-throwing:异常通知
      • aop:after:最终通知
        • method 属性:用于指定类中哪个方法是前置通知
        • pointcut 属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
    <!-- 配置通知类 -->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>
    <!--配置AOP-->
    <aop:config>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
            <aop:before method="printLog" pointcut="execution(* service.impl.*.*(..))"></aop:before>
        </aop:aspect>
    </aop:config>
    
  • 切入点表达式的写法

    • 关键字:execution(表达式)
    • 表达式:访问修饰符 返回值 包名。包名...类名。方法名(参数列表)
    • 标准的表达式写法:public void pers.ylq.service.impl.AccountServiceImpl.saveAccount()
    • 访问修饰符可以省略
    • 返回值可以使用通配符(*),表示任意返回值
    • 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
      • * *.*.*.*.AccountServiceImpl.saveAccount())
    • 包名可以使用 .. 表示当前包及其子包
      • * *..AccountServiceImpl.saveAccount()
    • 类名和方法名都可以使用*来实现通配
      • * *..*.*()
    • 参数列表:
      • 基本类型直接写名称:int
      • 引用类型写包名。类名的方式:java.lang.String
      • 可以使用通配符表示任意类型,但是必须有参数
      • 可以使用 .. 表示有无参数均可,有参数可以是任意类型
    • 全通配写法:* *..*.*(..)
    • 实际开发中切入点表达式的通常写法:
      • 切到业务层实现类下的所有方法:* pers.ylq.service.impl.*.*(..)
  • aop:pointcut 可以配置切入表达式,在通知标签中,通过 pointcut-ref 属性引入

    <aop:pointcut id="pt1" expression="execution(* pers.ylq.service.impl.*.*(..))"/>
    
    • 此标签写在 aop:aspect 内部只能当前切面使用。
    • 写在 aop:aspect 外面,所以切面可用,但是要写在所以切面配置的前面。
  • 环绕通知:它是 Spring 框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。

  • Spring 框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法 proceed(),此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,在程序执行时,Spring 框架会为我们提供该接口的实现类供我们使用。

    <aop:around method="aroundPringLog" pointcut="execution(* service.impl.*.*(..))"></aop:around>
    
    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();//得到方法执行所需的参数
            System.out.println("前置通知");
            rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
            System.out.println("后置通知");
            return rtValue;
        }catch (Throwable t){
            System.out.println("异常通知");
            throw new RuntimeException(t);
        }finally {
            System.out.println("最终通知");
        }
    }
    
  • AOP 注解:

    • aop:aspectj-autoproxy 标签:配置 Spring 开启注解 AOP 的支持。
    • @Aspect:表示当前类是一个切面类
    • @Pointcut:配置切入点表达式
    • @Before:前置通知
    • @AfterReturning:后置通知
    • @AfterThrowing:异常通知
    • @After:最终通知
    • @Around:环绕通知
    • @Component("logger")
      @Aspect
      public class Logger {
      
          @Pointcut("execution(* pers.ylq.service.impl.*.*(..))")
          private void pt1(){}
      
          @Before("pt1()")
          public  void beforePrintLog(){
              System.out.println("前置通知");
          }
          @AfterReturning("pt1()")
          public  void afterReturningPrintLog(){
              System.out.println("后置通知");
          }
      
          @AfterThrowing("pt1()")
          public  void afterThrowingPrintLog(){
              System.out.println("异常通知");
          }
      
      
          @After("pt1()")
          public  void afterPrintLog(){
              System.out.println("最终通知");
          }
      }
      
  • 使用注解的时候,当我们不是呀环绕通知的时候,Spring 自带通知的执行顺序为 前置通知--> 最终通知--> 后置/异常通知,也就是说最终通知在后置通知和异常通知之前执行,是错误的,我们需要注意。

  • 不使用 XML 的配置方式:@EnableAspectJAutoProxy

11. JdbcTemplate

  • 所需 jar 包
    • spring-jdbc-5.0.2.RELEASE.jar
    • spring-tx-5.0.2.RELEASE.jar 和事务相关
    • <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.6</version>
      </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>5.0.2.RELEASE</version>
      </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>5.0.2.RELEASE</version>
      </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-tx</artifactId>
          <version>5.0.2.RELEASE</version>
      </dependency>
      
  • Spring 内置数据库连接池
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName("com.mysql.jdbc.Driver");
    ds.setUrl("jdbc:mysql://localhost:3306/eesy");
    ds.setUsername("root");
    ds.setPassword("123456");
    
  • 构造方法:
    • JdbcTemplate();
    • JdbcTemplate(DataSource ds);
  • 常用方法:
    • setDataSource(DataSource ds); 设置数据源
    • update(String sql,Object... args);
    • query(String sql,RowMapper<T> rowmapper,Object... args);
    • queryForObject(String sql,RowMapper<T> rowmapper,Object... args);
    • queryForObject(String sql,Class<T> requiredType,Object... args);
  • JdbcTemplate 提供了 RowMapper 的子类 new BeanPropertyRowMapper<>(Class<T> mappedClass) 来进行数据的封装。
  • 当多个 dao 层的类使用 JdbcTemplate 操作的时候,可以继承 JdbcDaoSupport,由官方提供,里面有 JdbcTemplate,我们无需重复定义,只是使用继承的时候没办法用注解注入值。

12. 事务控制

  • Spring 的事务控制在业务层。

  • Spring 的事务控制是基于 AOP 的。

  • Spring 中事务的 API

    • PlatformTransactionManager:此接口是 Spring 的事务管理器,它里面提供了我们常用的操作事务的方法。
      • TransactionStatus getTransaction(TransactionDefinition definition) 获取事务状态信息
      • void commit(TransactionStatus status) 提交事务
      • void rollback(TransactionStatus status) 回滚事务
      • 我们在开发中都是使用它的实现类
        • org.springframework.jdbc.datasource.DataSourceTransactionManager 使用 Spring
          JDBC 或 或 iBatis 进行持久化数据时使用
        • org.springframework.orm.hibernate5.HibernateTransactionManager 使用
          Hibernate
    • TransactionDefinition:它是事务的定义信息对象
      • String getName() 获取事务对象名称
      • int getIsolationLevel() 获取事务隔离级别
      • int getPropagationBehavior() 获取事务传播行为
      • int getTimeout() 获取事务超时时间
      • boolean isReadOnly() 获取事务是否只读
    • TransactionStatus:此接口提供的是事务具体的运行状态
      • void flush() 刷新事务
      • boolean hasSavepoint() 获取是否存在存储点
      • boolean isCompleted() 获取事务是否完成
      • boolean isNewTransaction() 获取事务是否为新事务
      • boolean isRollbackOnly() 获取事务是否回滚
      • void setRollbackOnly() 设置事务回滚
  • 事务的传播行为:

    • REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值),增删改用这个
    • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务),查询用这个
  • So? 以上 API 我们通过配置文件实现。。。

  • 基于 XML 的声明式事务控制配置步骤

    1. 配置事务管理器
    2. 配置事务的通知
      • 此时我们需要导入事务的约束 tx 名称空间和约束,同时也需要 aop 的
      • 使用 tx:advice 标签配置事务通知
        • 属性 id:给事务通知起一个唯一标识
        • transaction-manager:给事务通知提供一个事务管理器引用
    3. 配置 AOP 中的通用切入点表达式
    4. 建立事务通知和切入点表达式的对应关系
    5. 配置事务的属性
      • 在事务的通知 tx:advice 标签的内部
      • isolation:用于指定事务的隔离级别。默认值是 DEFAULT,表示使用数据库的默认隔离级别。
      • propagation:用于指定事务的传播行为。默认值是 REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择 SUPPORTS。
      • read-only:用于指定事务是否只读。只有查询方法才能设置为 true。默认值是 false,表示读写。
      • timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
      • rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。
      • no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    <!-- 配置事务的通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 配置事务的属性-->
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"></tx:method>
        </tx:attributes>
    </tx:advice>
    
    <!-- 配置aop-->
    <aop:config>
        <!-- 配置切入点表达式-->
        <aop:pointcut id="pt1" expression="execution(* pers.ylq.service.impl.*.*(..))"></aop:pointcut>
        <!--建立切入点表达式和事务通知的对应关系 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
    </aop:config>
    
  • 基于注解的配置方式:

    • 第一步:配置事务管理器并注入数据源
      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSource"/>
      </bean>
      
    • 第二步:开启 Spring 对注解事务的支持
      <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
      
    • 第三步:在需要事务支持的地方(业务层类上)使用 @Transactional 注解
      • 其属性可以配置事务
  • @EnableTransactionManagement 注解,可以以不使用 XML 的方式开启 Spring 对注解事务的支持。