Spring框架

第一章 核心业务的干扰性

1.1 设计四则运算器接口

  1. public interface Calculator {
  2. int add(int i, int j);
  3. int sub(int i, int j);
  4. int mul(int i, int j);
  5. int div(int i, int j);
  6. }
public class CalculatorPureImpl implements Calculator {

    @Override
    public int add(int i, int j) {

        int result = i + j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int sub(int i, int j) {

        int result = i - j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        int result = i * j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        int result = i / j;

        System.out.println("方法内部 result = " + result);

        return result;
    }
}

1.2 为四则运算添加日志

public class CalculatorLogImpl implements Calculator {

    @Override
    public int add(int i, int j) {

        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);

        int result = i + j;

        System.out.println("方法内部 result = " + result);

        System.out.println("[日志] add 方法结束了,结果是:" + result);

        return result;
    }

    @Override
    public int sub(int i, int j) {

        System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);

        int result = i - j;

        System.out.println("方法内部 result = " + result);

        System.out.println("[日志] sub 方法结束了,结果是:" + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);

        int result = i * j;

        System.out.println("方法内部 result = " + result);

        System.out.println("[日志] mul 方法结束了,结果是:" + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);

        int result = i / j;

        System.out.println("方法内部 result = " + result);

        System.out.println("[日志] div 方法结束了,结果是:" + result);

        return result;
    }
}

1.3 问题分析

现有代码缺陷

针对带日志功能的实现类,我们发现有如下缺陷:

  • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
  • 附加功能分散在各个业务功能方法中,不利于统一维护

解决思路

解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

困难

解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

第二章 动态代理

1.1 代理设计模式

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护

  • 广告商找大明星拍广告需要经过经纪人
  • 合作伙伴找大老板谈合作要约见面时间需要经过秘书
  • 房产中介是买卖双方的代理

1.2 静态代理

要求代理对象和被代理对象,都有共同的父类,或者实现相同的接口。而被代理的对象作为代理对象的成员变量出现。

public class CalculatorStaticProxy implements Calculator {

    // 将被代理的目标对象声明为成员变量
    private Calculator target;

    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }

    @Override
    public int add(int i, int j) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(i, j);
        System.out.println("[日志] add 方法结束了,结果是:" + addResult);
        return addResult;
    }

1.3 动态代理

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

静态代理,只能代理同一类型的对象

public class ProxyFactory {
    public static Object getProxyObject(Object obj){
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("[日志] add 方法开始了,参数是:" + args[0] + "," + args[1]);
                Object result = method.invoke(obj,args);
                System.out.println("[日志] add 方法结束了,结果是:" + result);
                return  result;
            }
        });
    }

第三章 面向方面(AOP)

1.1 什么是aop

AOP:Aspect Oriented Programming(面向切面编程/面向方面编程),AOP是OOP的一个延伸

  • OOP(面向对象编程)
    • 三大特征:封装、继承和多态
    • 比如说,有DOG类、Cat类、Horse类,它们都有eat方法,run方法,eat打印了我能吃,run打印了我能跑,按照OOP的编程思想,那么我们可以抽象出父类Animal,在父类中放置相同的属性或者方法,这样来避免多子类中重复的代码。
  • OOP是纵向抽取和继承体系,OOP很多场合都能够解决我们的问题,但是有一些场合它也处理不了解决不了代码重复的问题。

1.2 横切逻辑

  • 横切逻辑存在是存在问题
    • 横切逻辑往往在很多方法中,它是重复的
    • 横切逻辑和业务代码混杂在一起,让业务代码变得很臃肿,业务代码应该只处理业务逻辑
    • OOP已经不能处理横切逻辑的问题了,AOP横空出世,AOP独辟蹊径的提出了横向抽取机制,将业务逻辑和横切逻辑分离,但分离不是难事,难的是分离之后怎么把横切逻辑再融合到原有业务逻辑上,达到和原来一样的效果

所以,我们要做的这个比较难的事就是在不修改(不侵犯)原有业务逻辑的基础上做逻辑增强(横切逻辑增强)。

3.jpg

1.3 AOP的优势

因为我们AOP要解决的问题就是在不改变业务逻辑基础上增强横切逻辑。在这个过程中,业务逻辑我们是不能改变的,所以我们不能面向业务逻辑,只能面向横切逻辑;另外还有一层意思,AOP要做增强的横切逻辑往往不是影响一个方法,往往会影响很多方法(对多处的业务逻辑进行增强),影响了一大片,有一个“面”的概念在里面,所以叫做面向切面编程。

AOP的实现方式

Spring的AOP的实现方式就是动态代理技术

AOP的优势

  • 作用:程序运行期间,在不改变原有业务逻辑的情况下进行方法增强
  • 优势:减少重复代码,提高开发效率,业务逻辑和增强的横切逻辑分离便于维护

1.4 AOP实现转账案例

案例:模拟转账(并且模拟转账异常)

  • 汇款人账户减少一定的金额
  • 收款人账户增加一定的金额
  • 计算之后,更新数据库
  • 问题:模拟转账异常(人为制造异常,在两次update之间造了异常)

问题:模拟转账异常(人为制造异常,在两次update之间造了异常

  • applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context.xsd
">
    <!-- 启动注解扫描-->
    <context:component-scan base-package="com.atguigu"></context:component-scan>
    <!-- 配置dbcp连接池-->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <!-- BasicDataSource成员注入-->
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/test"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
    <!-- 配置QueryRunner-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <!-- -构造方法注入-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>
</beans>
  • dao层
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    @Qualifier("queryRunner")
    private QueryRunner qr ;

    /**
     *  根据账户名查询账户
     */
    public Account queryAccountByName(String name) throws SQLException {
        String sql = "select id,name,money from account where name = ?";
        return qr.query(sql,new BeanHandler<Account>(Account.class),name);
    }

    /**
     *  保存转账后的金额
     */
    public int updateAccountByName(Account account) throws SQLException {
        String sql = "update account set name = ? , money = ? where id = ?";
        return qr.update(sql,account.getName(),account.getMoney(),account.getId());
    }
}
  • service层
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    @Qualifier("accountDao")
    private AccountDao accountDao;

    /**
     *  根据收款人和付款人查询账户余额
     *  计算金额
     *    收款人余额 = 余额+转账金额
     *    付款人余额 = 余额-转账金额
     *  数据保存到数据库
     */
    public void transfer(String fromName, String toName, double money)throws SQLException {
        //查询付款人账户
        Account accountFrom = accountDao.queryAccountByName(fromName);
        //查询收款人账户
        Account accountTo = accountDao.queryAccountByName(toName);
       // 计算金额
        accountFrom.setMoney( accountFrom.getMoney() - money );
        accountTo.setMoney( accountTo.getMoney() + money);
        //跟新数据库
        accountDao.updateAccountByName(accountFrom);
        int a= 1/0;
        accountDao.updateAccountByName(accountTo);
    }
}

我们不可能在每一个方法中添加tcf控制,因此引入动态代理技术来在不改变原有业务逻辑代码的基础上做逻辑增强。

  • 动态代理工厂类
/**
 * 动态代理工厂
 * 代理业务层,在不改变业务层逻辑的情况下
 * 增强原有业务层的功能(事务管理)
 */
@Component("proxyFactory")
public class ProxyFactory {
    @Autowired
    @Qualifier("transactionManager")
    private TransactionManager transactionManager;

    /**
     *  返回代理对象
     *  传递被代理对象(业务层)
     */
    public Object getProxy(final Object object){
       return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), new InvocationHandler() {
           @Override
           public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
               Object result = null;
               try {
                   transactionManager.begin();
                   result = method.invoke(object, args);
                   transactionManager.commit();
               }catch (Exception ex){
                   ex.printStackTrace();
                   transactionManager.rollback();
               }finally {
                   transactionManager.release();
               }
               return result;
           }
       });
    }
}
  • 业务层改造
@Service("accountServiceImplNoTCF")
public class AccountServiceImplNoTCF implements AccountService {
    @Autowired
    @Qualifier("accountDao")
    private AccountDao accountDao;
    /**
     *  根据收款人和付款人查询账户余额
     *  计算金额
     *    收款人余额 = 余额+转账金额
     *    付款人余额 = 余额-转账金额
     *  数据保存到数据库
     */
    public void transfer(String fromName, String toName, double money)throws SQLException {
            Account accountFrom = accountDao.queryAccountByName(fromName);
        //查询收款人账户
        Account accountTo = accountDao.queryAccountByName(toName);
        // 计算金额
        accountFrom.setMoney( accountFrom.getMoney() - money );
        accountTo.setMoney( accountTo.getMoney() + money);
        //跟新数据库
        accountDao.updateAccountByName(accountFrom);
        int a= 1/0;
        accountDao.updateAccountByName(accountTo);
    }
}

第四章 Spring中AOP使用

1.1 AOP中的相关术语

  • Joinpoint(连接点)
    • 横切程序执行的特定位置,比如类开始初始化前,类初始化之后,类中某个方法调用前、调用后,方法抛出异常后等,这些代码中的特定点就称为“连接点”。
    • Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。
    • 我们知道黑客攻击系统需要找到突破口,没有突破口就无法进行攻击,从这一角度上来说,AOP是一个黑客(因为它要向目前类中嵌入额外的代码逻辑),连接点就是AOP向目标类打入楔子的候选点。
  • Pointcut(切入点)
    • 一个类中可以有很多个方法,每个方法又有多个Joinpoint,在这么多个方法中,如何定位到自己感兴趣的方法呢?靠的是切点
    • 注意:切点只定位到某个方法上,所以如果希望定位到具体连接点上,还需要提供方位信息
    • 比如:如果把一个方法理解成数据表中的一条记录的话,那么切入点就好比你select语句的where条件 ,就可以定位到你感兴趣的方法
  • Advice(通知/增强)
    • 增强的第一层意思就是你的横切逻辑代码(增强逻辑代码)
    • 在Spring中,增强除了用于描述横切逻辑外,包含一层意思就是横切逻辑执行的方位信息。刚刚说了切点只能定位到方法,在进一步使用方位信息就可以定位到我们感兴趣的连接点了(方法调用前、方法调用后还是方法抛出异常时等)。
  • Target(目标对象)
    • 增强逻辑的织入目标类。比如未添加任何事务控制的AccountServiceImplNoTcf类
  • Weaving(织入)
    • 织入是将增强逻辑/横切逻辑添加到目标类具体连接点上的过程,AOP像一台织布机,将目标类、增强或者引介通过AOP(其实就是动态代理技术)这台织布机天衣无缝地编织到一起。
  • Spring采用动态代理织入。
    • Proxy(代理),一个类被AOP织入增强后,就产出了一个结果类,它是融合了原类和增强逻辑的代理类。
  • Aspect(切面)
    • 切面由切点和增强(引介)组成。
    • 切面=切点+增强
    • =切点+方位信息+横切逻辑
    • =连接点+横切逻辑
  • 最终切面完成:把横切逻辑织入到哪些方法的方法前/后等
  • 本质:把横切逻辑增强到连接点(切点和方位信息都是为了确定连接点)上

1.2 Spring关于JDK/CGLIB动态代理的选择

Spring发现涉及到接口那就使用JDK动态代理,如果不涉及接口就使用CGLIB动态代理.

CGLIB是一个强大的、高性能的代码生成库。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作

AOP:日志、性能监控、事务、权限控制

1.3 AOP的xml配置

  • 引入pom坐标
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.8.9</version>
</dependency>
  • 横切逻辑日志对象
public class LogUtil {
    //方法执行之前
    public void printBeforeMethod(){
        System.out.println("方法之前执行");
    }

    //方法执行之后打印
    public void printAfterMethod(){
        System.out.println("方法执行之后");
    }

    //方法异常执行打印
    public void printAfterThrowing(){
        System.out.println("方法异常时");
    }

    //方法正常执行打印
    public void printAfterReturn(){
        System.out.println("方法正常时");
    }
}
  • service层
@Service("accountServiceImpl")
public class AccountServiceImpl implements AccountService {

    public int saveAccount(Account account) {
        System.out.println("模拟保存账户");
        return 0;
    }

    @Override
    public int updateAccountById(Account account) {
        System.out.println("模拟更新账户");
        return 0;
    }

    @Override
    public int deleteAccountById(int id) {
        System.out.println("模拟删除账户");
        return 0;
    }

    @Override
    public Account queryAccountById(int id) {
        System.out.println("模拟查询账户");
        return null;
    }
}

applicationContext.xml配置

  • <aop:config> 所有aop配置的根标签
  • <aop:aspect id="logAspect" ref="log"> 配置切面对象,id自定义,ref横切逻辑类的bean标签id值
  • <aop:pointcut> 配置切入点
    • 属性值:id自定义
    • 属性值:expression 匹配方法的表达式
  • <aop:before> 被切入的方法前执行
    • 属性method:printBeforeMethod切入的方法名
    • 属性pointcut-ref:切入点标签的id属性值
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
       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
">
    <context:component-scan base-package="com.atguigu"></context:component-scan>

    <!-- 横切逻辑配置-->
    <bean id="log" class="com.atguigu.utils.LogUtil">

    </bean>
    <!-- 配置aop的根标签-->
    <aop:config>
        <!-- 切面配置-->
        <aop:aspect id="logAspect" ref="log">
            <!--
                配置切入点
                before:方法执行之前
                method:切入的方法名
                pointcut:感兴趣的方法,被切入的方法

            -->
            <!--<aop:before method="printBeforeMethod"
                        pointcut="execution( int com.atguigu.service.AccountService.saveAccount(com.atguigu.pojo.Account))"></aop:before>-->
            <!--
                表达式配置
                方法参数:* 匹配任意参数,必须有参数
                         .. 匹配任意参数,有无参数均可
                方法名:*
                返回值:*
            -->
            <aop:pointcut id="point1" expression="execution( * com.atguigu.service.AccountService.*(..))"></aop:pointcut>
            <aop:before method="printBeforeMethod" pointcut-ref="point1"></aop:before>
            <aop:after-returning method="pringAfterReturn" pointcut-ref="point1"></aop:after-returning>
            <aop:after-throwing method="printAfterThrowing" pointcut-ref="point1"></aop:after-throwing>
            <aop:after method="printAfterMethod" pointcut-ref="point1"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>
execution(表达式)
表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
写法说明:
全匹配方式:
public void atguigu
com.atguigu.service.impl.AccountServiceImpl.saveAccount(com.atguigu.pojo.Account)
访问修饰符可以省略
void
com.atguigu.service.impl.AccountServiceImpl.saveAccount(com.atguigu.pojo.Account)
返回值可以使用*号,表示任意返回值
*
com.atguigu.service.impl.AccountServiceImpl.saveAccount(com.atguigu.pojo.Account)


包名可以使用*号,表示任意包,但是有几级包,需要写几个*
* *.*.*.*.AccountServiceImpl.saveAccount(com.atguigu.pojo.Account)
使用..来表示当前包,及其子包
* com..AccountServiceImpl.saveAccount(com.atguigu.pojo.Account)
类名可以使用*号,表示任意类
* com..*.saveAccount(com.atguigu.pojo.Account)
方法名可以使用*号,表示任意方法
* com..*.*( com.atguigu.pojo.Account)
参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
* com..*.*(*)
参数列表可以使用..表示有无参数均可,有参数可以是任意类型
* com..*.*(..)
全通配方式:
* *..*.*(..)
注:
通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。
execution(* com.atguigu.service.impl.*.*(..))

1.4 环绕通知

它是spring为我们提供的一种可以在代码中手动控制增强方法何时执行的方式,灵活度比较高,设置可以控制原业务逻辑是否执行。

注意:通常情况下,环绕通知都是独立使用的,不要和上面的四种通知类型混合使用

  • <aop:around method="printRound" pointcut-ref="point1"></aop:around> 环绕通知配置
  • ProceedingJoinPoint进程切入点对象,执行我们的业务逻辑方法
    • 方法:proceed() 执行我们自己的业务逻辑方法
  • applicationContext.xml配置
<aop:around method="printRound" pointcut-ref="point1"></aop:around>
  • 日志对象:横切逻辑
//环绕通知
public Object printRound(ProceedingJoinPoint joinPoint){
    Object result = null;
    try {
        System.out.println("方法执行之前");
        result = joinPoint.proceed();
        System.out.println("方法正常返回值");
    }catch (Throwable throwable){
        System.out.println("方法异常时");
    }
    System.out.println("方法执行后");
    return result;
}

第五章 基于注解的AOP配置

<!-- aop注解开关-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
  • 横切逻辑对象
@Component("logUtil")
//横切逻辑对象
@Aspect
public class LogUtil {
    //方法执行之前
    @Before("execution(* com.atguigu.service.AccountServiceImpl.*(..))")
    public void printBeforeMethod(){
        System.out.println("方法之前执行");
    }

    //方法执行之后打印
    @After("execution(* com.atguigu.service.AccountServiceImpl.*(..))")
    public void printAfterMethod(){
        System.out.println("方法执行之后");
    }

    @AfterThrowing("execution(* com.atguigu.service.AccountServiceImpl.*(..))")
    //方法异常执行打印
    public void printAfterThrowing(){
        System.out.println("方法异常时");
    }

    @AfterReturning("execution(* com.atguigu.service.AccountServiceImpl.*(..))")
    //方法正常执行打印
    public void pringAfterReturn(){
        System.out.println("方法正常时");
    }
}
  • 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class MainTest {

    @Autowired
    @Qualifier("accountServiceImpl")
    private AccountService accountService;

    @Test
    public void testAop(){
        accountService.saveAccount( new Account());
    }

}
  • 横切逻辑的抽取
@Component("logUtil")
//横切逻辑对象
@Aspect
public class LogUtil {

    @Pointcut("execution(* com.atguigu.service.AccountServiceImpl.*(..))")
    public void point(){
    }

    //方法执行之前
    @Before("point()")
    public void printBeforeMethod(){
        System.out.println("方法之前执行");
    }

    //方法执行之后打印
    @After("point()")
    public void printAfterMethod(){
        System.out.println("方法执行之后");
    }

    @AfterThrowing("point()")
    //方法异常执行打印
    public void printAfterThrowing(){
        System.out.println("方法异常时");
    }

    @AfterReturning("point()")
    //方法正常执行打印
    public void pringAfterReturn(){
        System.out.println("方法正常时");
    }
}
  • 配置Spring框架启动类
@Component
@ComponentScan("com.atguigu")
//启用动态代理
@EnableAspectJAutoProxy
public class SpringConfig {
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class MainTest {

    @Autowired
    @Qualifier("accountServiceImpl")
    private AccountService accountService;

    @Test
    public void testAop(){
        accountService.saveAccount( new Account());
    }

}