从零开始重新认识 Spring Framework

2020-07-30 18:46:21 浏览数 (1)

如果你想快速入门 Spring 框架,或者想快速复习 Spring 的基本操作,那么这篇文章再适合你不过了。

本文没有书上的啰里啰嗦,很言简意赅,但是需要读者有一定的基础,这里不会介绍一些常用的概念是啥,都是在实践中学习用法,所以没什么精华可言,只 停留在会用的层面,有些重要的会说一下原理。

因为关于概念的讲解网上有太多太多了,这里我就不再 reinvent the wheel (造轮子)了,只说一下如何使用这些功能。

IOC

我们以一个实际的例子来看一下什么是 IOC,这里使用到了 Account 表,我们创建对应的类文件。

pom 文件:

代码语言:javascript复制
<dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>commons-dbutils</groupId>
            <artifactId>commons-dbutils</artifactId>
            <version>1.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>
</dependencies>

由于仅仅是实现简单的增删改查,所以这里就不展示代码了,主要是看他是如何将对象交给 Spring 来管理的。

使用配置 Bean 的方式可以将类放入容器中,供 Spring 创建对象使用。

无论是基于 xml 的配置还是基于注解的配置都可以分为两类,即用于 创建对象 的配置和用于 注入数据 的配置。


使用配置文件

bean.xml 配置文件:

  • bean 标签用于 创建对象
    • id:可随意取名;
    • class: 一定是存在的类,而且需要写全限定类名;
    • property 是用于 注入数据 的;
      • ref:指定值为其他的 Bean
      • name:必须的一个属性,表示通过 set 方法注入,即该 property 作为 bean 的一个属性值,该 bean 中 一定要提供一个 set 方法来设置该属性

一般只要我们使用了 Spring,一旦有需要创建的对象就要将类配置到 Bean 中,这就是 IOC 控制反转,将创建对象的权利交给 Spring。

  • 还可以通过 constructor-arg 注入数据,即通过构造方法设置属性;同样,如果使用构造方法注入,那么 类中一定也要提供对应的构造方法
    • name:必须的属性值,即构造方法中的属性名
    • ref:所使用的其他的 Bean
  • scope
    • 配置是多例的还是单例的,因为这里使用 QueryRunner 一般每次执行都要创建一个,所以配置成多例的。
  • 还可以通过 工厂 的方式创建对象,后面会用到,这里就不在演示了。
代码语言:javascript复制
<?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: 可随意取名
            class: 一定是存在的类
            property:
                - ref: 指定值为其他的 Bean
                - name: 必须的一个属性, 表示通过 set 方法注入,
                        即该 property 作为 bean 的一个属性值,则该 bean 中一定要提供一个 set 方法来设置它;
      -->
    <bean id="accountService" class="top.wsuo.service.impl.AccountServiceImpl">
        <property ref="accountDao" name="accountDao"/>
    </bean>

    <!--  
        一般只要我们使用了 Spring,  一旦有需要创建的对象就要将类配置到 Bean 中.
        这就是 IOC 控制反转,将创建对象的权利交给 Spring.
      -->
    <bean id="accountDao" class="top.wsuo.dao.impl.AccountDaoImpl">
        <property name="runner" ref="runner"/>
    </bean>
    <!--  
        同样的配置一个类,上面是通过 set 方法配置的,所以只需要设置 property 子标签配置;
        这里通过 constructor-arg 配置,即通过构造方法设置属性
            - name: 必须的属性值, 即构造方法中的属性名
            - ref: 所使用的其他的 Bean
        scope:
            - 配置是多例的还是单例的,因为这里使用 QueryRunner 一般每次执行都要创建一个,所以配置成多例的.
      -->
    <bean class="org.apache.commons.dbutils.QueryRunner" id="runner" scope="prototype">
        <!--    注入数据源   -->
        <constructor-arg name="ds" ref="dataSource"/>
    </bean>

    <!--  
        配置 DataSource: 
            - 这里使用 c3p0 提供的一个 ComboPooledDataSource 类,该类已经提供了相关属性的 set 方法;
            - 所以可以直接使用 property 子标签.
      -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///eesy?useUnicode=true&amp;characterEncoding=UTF-8"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

</beans>

相关属性的解释请查看注释。

另外也可以将配置信息抽取到一个文件当中,使用 EL 表达式获取,具体如下:

启动的时候需要调用以下方法:

代码语言:javascript复制
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
accountService = context.getBean("accountService", IAccountService.class);

使用注解的方式

下面我们使用注解的方式来实现相同的功能。

注解只讲两类:

  • 用于创建对象的注解;
    • @Component 及其衍生注解;
    • @Bean注解;
  • 用于注入数据的注解。
    • @Autowired 等注解;

为了能实现不依赖于配置文件,我们使用@Configure 注解来实现 xml 配置文件的功能,如果我们不使用这个注解,那么还是 必须要提供配置文件,只不过在配置文件中只需要注明要扫描的包即可,如下:

代码语言:javascript复制
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
        
	<context:component-scan base-package="top.wsuo"/>
	
</beans>

但是如果我们连这个配置文件也不想写也想实现扫描包的话就需要加上 @ComponentScan("top.wsuo") 注解。

我们的任务是将需要的类装进 Spring容器 中:

  • 对于自己写的类我们只需要加上 @Repository 或者 @Service 等注解即可加入到容器中(作用是 创建对象 ),如果我们要使用容器中的类只需加上 @Autowired(用于 注入数据 )等注解即可。

那么对于别人写的类呢? 比如你要引用其它的包,哪些类肯定是不能修改的,我们只能通过另外两种方法把它加进容器中了。

1、对于那种 使用构造方法 创建对象的类来说我们就需要单独创建一个方法,而且要使用 @Bean 注解。

代码语言:javascript复制
@Configuration
@ComponentScan("top.wsuo")
public class SpringConfiguration {

    @Bean
    @Scope("prototype") // 代表多例
    public QueryRunner createQueryRunner(DataSource dataSource) {
        return new QueryRunner(dataSource);
    }
    
}

这种就是使用 构造方法 创建对象,其实就相当于使用工厂的方式创建对象。

  • 如果 @Bean 中的值不给,那么默认的 id 就是方法的返回值首字母小写,也可以自己指定 id 值;
  • 这里的 @Scope 注解指定该类为多例的。

2、set 方法:

代码语言:javascript复制
@Bean(name = "dataSource")
DataSource createDataSource() throws PropertyVetoException {
	ComboPooledDataSource ds = new ComboPooledDataSource();
	ds.setDriverClass("com.mysql.jdbc.Driver");
	ds.setJdbcUrl("jdbc:mysql:///eesy?useUnicode=true&amp;characterEncoding=UTF-8");
	ds.setUser("root");
	ds.setPassword("root");
	return ds;
}

像这种就是使用 set 方法注入。

对于注入数据,我们可以使用下面的四种注解:

  • @Autowired:根据对象的类型自动注入,如果有一样的类型,则看对象的变量名(类名首字母小写),如果匹配则注入,如果没有匹配的则注入失败;
  • @Qualifier:配合上面的注解使用,在 @Autowired 找不到对象的时候使用,在该注解中可以指定类的 id 值;
  • @Resource:直接按照 id 值注入;
  • @Value:注入基本类型和 String 类型。

使用注解的方式注入数据无需提供 set 方法或者构造方法。

来看一个例子:

代码语言:javascript复制
@Autowired
private QueryRunner runner;

@Resource(name = "accountDao")
private IAccountDao accountDao;

像上面的那个 QueryRunner,该类型只有他自己,所以不管它的变量名是啥都能找到。


如果使用全注解的话,获取容器的时候应该使用这个类:

代码语言:javascript复制
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);

其中 SpringConfiguration 是你的配置类。

在配置类中可以 导入子配置类 ,写法如下:

代码语言:javascript复制
@Configuration
@ComponentScan("top.wsuo")
@Import(jdbcConfig.class)

下面我们解决一下上面的代码中出现的问题,比如关于 jdbc 的配置信息,我们应该单独的抽取出来作为一个配置文件。

那么如何引入配置文件呢?

首先需要在配置类上面添加 @PropertySource 注解,然后将具体的值抽取为成员变量:

代码语言:javascript复制
@Value("jdbc.driver")
private String driver;

@Value("jdbc.url")
private String url;

@Value("jdbc.username")
private String username;

@Value("jdbc.password")
private String password;

注意 @PropertySource 注解的写法,要加上 classpath ,表示在类路径下:

代码语言:javascript复制
@PropertySource("classpath:jdbcConfig.properties")

Spring 整合 Junit

首先我们要先导入一个包:

代码语言:javascript复制
<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
</dependency>

然后在测试类上添加如下注解:

代码语言:javascript复制
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
  • @ContextConfiguration:代表使用注解还是xml
    • locations 表示使用配置文件,并提供 配置文件的路径
    • class 表示使用纯注解方式,并提供 配置类的全限定类名

AOP

上面的这个案例是有问题的:

事务控制

我们先来演示一个经典的实务问题,即转账问题

本来都是 1000 块,aaabbb 转账 100 块钱,如上图所示。

但是如果中间出了问题,那么金额会不翼而飞,这就涉及到事务问题。

我们可以先创建一个获取连接的工具类 ConnectionUtils ,它的作用就是从线程中获取连接,如果没有就创建一个连接并加进去:

代码语言:javascript复制
public class ConnectionUtils {

    private ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Connection getThreadConnection() {
        // 先从 ThreadLocal 中获取
        Connection connection = threadLocal.get();
        if (connection == null) {
            try {
            	// 如果为 null 就创建一个
                connection = dataSource.getConnection();
                threadLocal.set(connection);
            } catch (SQLException e) {
                throw new RuntimeException("有异常!");
            }
        }
        // 返回线程上的连接
        return connection;
    }

    /**
     * 将连接和线程解绑
     *      因为线程也是线程池
     */
    public void removeConnection() {
        threadLocal.remove();
    }

}

这样我们的数据库连接就和线程绑定到一起了,接下来就是事务控制的具体方法了,我们也单独的抽取出来一个工具类TransactionManager

代码语言:javascript复制
public class TransactionManager {

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }
    /**
     * 开启事务
     */
    public void beginTransaction() {
        try {
            connectionUtils.getThreadConnection().setAutoCommit(true);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    public void commitTransaction() {
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    public void rollback() {
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放连接
     */
    public void release() {
        try {
            connectionUtils.getThreadConnection().close();
            connectionUtils.removeConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

该类的作用就是将事务的四大操作抽出来,调用线程池中的线程从而获取连接。

然后我们在业务层调用 TransactionManager

代码语言:javascript复制
public class AccountServiceImpl implements IAccountService{

    private IAccountDao accountDao;
    private TransactionManager transactionManager;

    public void setTransactionManager(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public List<Account> findAllAccount() {
        try {
            transactionManager.beginTransaction();
            List<Account> accounts = accountDao.findAllAccount();
            transactionManager.commitTransaction();
            return accounts;
        } catch (Exception e) {
            transactionManager.rollback();
            throw new RuntimeException();
        } finally {
            transactionManager.release();
        }
    }
}

注意这个时候的 dao 层就不能使用默认的连接了,所以我们应该将 QueryRunner 中的数据库连接池断开,转给 ConnectionUtils,而在 dao 的实现类中自己提供连接对象,如下:

代码语言:javascript复制
@Override
public List<Account> findAllAccount() {
    try {
        return runner.query(connectionUtils.getThreadConnection(),
                "select * from account", new BeanListHandler<Account>(Account.class));
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

这样一来,我们就不用担心事务带来的问题了。

但是你会发现一个问题就是在业务层 AccountServiceImpl 实在是太麻烦了,每次进行一次事务就要重新的开启再关闭,大量的重复性工作,而且还有方法之间的依赖,体现在:如果我更改调用对象的方法名称,那么调用的时候就必须更改方法名,所以我们需要改进。

动态代理

怎么改进? 这里用到动态代理。

代码语言:javascript复制
/*
        动态代理:
            - 字节码随用随创建,用时再加载
            - 不修改源码的基础上对方法增强
            - 分类:
                - 基于接口的动态代理
                - 基于子类的动态代理
            - 如何创建?
                - Proxy 的 newProxyInstance
                    - ClassLoader: 和被代理对象使用的是同一个类加载器;
                    - Class[]: 字节码数组, 让代理对象和被代理对象有相同的方法,因为实现了同一个接口嘛;
                    - InvocationHandler: 写如何代理,增强的方法.
*/

我们引入一个例子来简单的来看一下动态代理:

有一个制造商比如肯德基,他有一个准则就是你要加盟我必须提供销售服务,而在 java 中使用接口制定标准,所以我们定义一个接口 IProduce

代码语言:javascript复制
public interface IProduce {
    void saleProduct(float money);
}

比如北京有一家店 Produce 加盟了它,那么这家店就要遵守规范,实现接口:

代码语言:javascript复制
public class Produce implements IProduce {
    /**
     * 销售食物
     * 
     * @param money 获得的钱
     */
    public void saleProduct(float money) {
        System.out.println("销售产品,拿到钱: "   money);
    }
}

那么我们就可以创建一个类来测试一下,比如这里有个人去北京的肯德基店里买东西了,但是他是代理商,要中间价的,所以他获取钱之后只给上一级 80% 的钱,我们通过代理来模拟一下:

代码语言:javascript复制
public static void main(String[] args) {
    final Produce produce = new Produce();
    // produce.saleProduct(10000f); -- 正常的消费者买商品

    /*
        动态代理:
            - 字节码随用随创建,用时再加载
            - 不修改源码的基础上对方法增强
            - 分类:
                - 基于接口的动态代理
                - 基于子类的动态代理
            - 如何创建?
                - Proxy 的 newProxyInstance
                    - ClassLoader: 和被代理对象使用的是同一个类加载器;
                    - Class[]: 字节码数组, 让代理对象和被代理对象有相同的方法,因为实现了同一个接口嘛;
                    - InvocationHandler: 写如何代理,增强的方法.
     */
    IProduce proxyProduce = (IProduce) Proxy.newProxyInstance(produce.getClass().getClassLoader(),
            produce.getClass().getInterfaces(),
            new InvocationHandler() {
                /**
                 *
                 * @param proxy 代理对象的引用
                 * @param method 当前执行的方法
                 * @param args 当前执行方法所需的参数
                 * @return 和被代理对象相同的返回值
                 * @throws Throwable 异常
                 */
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Object res = null;
                    Float money = (Float) args[0];
                    if ("saleProduct".equals(method.getName())) {
                        res = method.invoke(produce, money * 0.8f);
                    }
                    return res;
                }
            }
    );

    proxyProduce.saleProduct(1000f);
}

子类引用父类的变量必须是 final 类型的,就算你不写 JDK8 以上的版本也会自动给你加上。

运行结果:

代码语言:javascript复制
销售产品,拿到钱: 800.0

如果你还是对代理模式不懂的话可以先看这里哦:终于有人把 java代理 讲清楚了,万字详解!

下面我们通过使用动态代理来实现增强 IAccountService 的对象。

我们可以不写实现类,通过 BeanFactory 来返回增强后的代理对象。

代码语言:javascript复制
public class BeanFactory {

    private IAccountService accountService;

    private TransactionManager transactionManager;

    public void setTransactionManager(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(), new InvocationHandler() {
                    Object res = null;

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        try {
                            transactionManager.beginTransaction();
                            res = method.invoke(accountService, args);
                            transactionManager.commitTransaction();
                        } catch (Exception e) {
                            transactionManager.rollback();
                            throw new RuntimeException();
                        } finally {
                            transactionManager.release();
                        }
                        return res;
                    }
                });
    }
}

将该类交给 Spring 来管理,我们需要在 xml 配置文件中配置。

代码语言:javascript复制
<bean class="top.wsuo.factory.BeanFactory" id="factory">
        <property name="accountService" ref="accountService"/>
        <property name="transactionManager" ref="transactionManager"/>
</bean>

<bean factory-bean="factory" factory-method="getAccountService" id="proxyAccountService"/>

并且通过 工厂 的方式创建对象,proxyAccountService,那么我们注入的时候就要指明要注入的是哪一个,所以可以使用注解:

代码语言:javascript复制
@Autowired
@Qualifier("proxyAccountService")
private IAccountService as;

这样我们就算是完成任务了,但是有没有发现,这也太麻烦了吧?

配置 AOP

理解了动态代理之后,是时候引出 AOP 的概念了,就是面向切面编程。

它的作用就是在程序运行期间,在不修改源码的情况下,对已有的方法进行增强。它的实现方式就是使用了动态代理的技术。

所以现在明白我上面扯那么多的原因了吧,即深入浅出,层层递进。深入浅出之后你就会觉得 AOP 很简单了。

AOP 中的术语:

  • 切入点:对象中被增强的方法;
  • 连接点:对象中所有的方法,不管有没有被增强;
  • 通知:就是增强的过程;
    • 前置通知,就是 invoke 方法前面做的操作;
    • 后置通知,就是 invoke 方法后面做的操作;
    • 异常通知,相当于 catch 到异常所作的操作;
    • 最终通知,相当于 finally 代码块中所作的操作;
    • 环绕通知,整个外部的 invoke 方法;
  • 切面:切入点 通知。

这些就是术语,知道就行,我们要做的就是告诉 Spring 我们想让它干嘛。

我们用一个例子来解释一下,我们的需求是在业务层记录日志,所以我们打算将 logger 切入到 AccountServiceImpl 的方法中去:

在这之前我们需要先导入依赖坐标

代码语言:javascript复制
<!--  切入点表达式  -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.7</version>
</dependency>
使用配置文件的方式配置通知类型

bean.xml 中配置如下:

代码语言:javascript复制
<!--  配置 Spring 的 IOC   -->
    <bean class="top.wsuo.service.impl.AccountServiceImpl" id="accountService"/>

    <bean class="top.wsuo.utils.Logger" id="logger"/>
    <!--  Spring 中基于 xml 的 AOP 配置  -->
    <aop:config>
<!--    配置切面    -->
        <aop:aspect id="logAdvice" ref="logger">
<!--      配置通知的类型和切入点      -->
            <aop:before method="printLog"
                        pointcut="execution(public void top.wsuo.service.impl.AccountServiceImpl.saveAccount())"/>
        </aop:aspect>
    </aop:config>

其中 aop:config 表示开始配置 aop 信息: - aop:aspect:配置切面 - aop:before:配置前置通知,其中 pointcut 为切入点,method 是切入之后要执行的方法;

execution 是表达式,由访问修饰符、返回值、包名.方法名(参数) 组成。

标准的写法是:

代码语言:javascript复制
public void top.wsuo.service.impl.AccountServiceImpl.saveAccount()

但是可以使用通配符,其中有一个全通配写法:* *..*.*(..),这个意思是将类中的所有的方法都看作是 切入点

  • 访问修饰符可以省略;
  • 返回值可以使用通配符表示任意返回值;
  • 包名可以是任意包,但是有几个包就写几个 *.
  • *.. 表示当前包及其子包。
  • 参数类型可以使用通配符表示任意类型,可以使用 .. 表示有无参数都行。

如果写成这样:

代码语言:javascript复制
* *..AccountServiceImpl.saveAccount()

代表任意包下只要有一个类叫做AccountServiceImpl都可以匹配到。

实际开发中通常的写法是这样的,即匹配到业务层所有的实现类下的所有方法:

代码语言:javascript复制
execution(* top.wsuo.service.impl.*.*(..))

那么如何配置其他的通知呢?

代码语言:javascript复制
<aop:before method="beforePrintLog"
            pointcut="execution(* top.wsuo.service.impl.*.*(..))"/>
<aop:after-returning method="afterPrintLog"
            pointcut="execution(* top.wsuo.service.impl.*.*(..))"/>
<aop:after-throwing method="throwPrintLog"
            pointcut="execution(* top.wsuo.service.impl.*.*(..))"/>
<aop:after method="finalPrintLog"
            pointcut="execution(* top.wsuo.service.impl.*.*(..))"/>

运行结果:

其中后置通知和异常通知永远只能同时执行一个。

另外关于切入点表达式我们可以使用引用,即将重复的配置抽离出来,使用 aop:pointcut 标签:

代码语言:javascript复制
<aop:aspect id="logAdvice" ref="logger">
    <aop:before method="beforePrintLog" pointcut-ref="exp"/>
    <aop:after-returning method="afterPrintLog" pointcut-ref="exp"/>
    <aop:after-throwing method="throwPrintLog" pointcut-ref="exp"/>
    <aop:after method="finalPrintLog" pointcut-ref="exp"/>
    <aop:pointcut id="exp" expression="execution(* top.wsuo.service.impl.*.*(..))"/>
</aop:aspect>

这种情况下该切入点表达式只能应用于当前切面,其他的切面要想用必须重新写。

那有通用的配置吗?答案是肯定的,只需要将该标签移动到 aspect 标签之前:

代码语言:javascript复制
<aop:pointcut id="exp" expression="execution(* top.wsuo.service.impl.*.*(..))"/>
<aop:aspect id="logAdvice" ref="logger">

下面我们来演示一下环绕通知。

首先在配置文件中配置:

代码语言:javascript复制
<aop:pointcut id="exp" expression="execution(* top.wsuo.service.impl.*.*(..))"/>
<aop:aspect id="logAdvice" ref="logger">
    <aop:around method="aroundPrintLog" pointcut-ref="exp"/>
</aop:aspect>

然后我们在 logger 类中添加一个方法:

代码语言:javascript复制
public Object aroundPrintLog(ProceedingJoinPoint point) {
    Object res = null;
    try {
        Object[] args = point.getArgs();
        System.out.println("Logger 中记录日志的信息: 前置通知");
        res = point.proceed(args);
        System.out.println("Logger 中记录日志的信息: 后置通知");
    } catch (Throwable e) {
        System.out.println("Logger 中记录日志的信息: 异常通知");
    } finally {
        System.out.println("Logger 中记录日志的信息: 最终通知");
    }
    return res;
}

需要引入一个 ProceedingJoinPoint类,他有一个方法 proceed(args); 就是执行我们的业务方法,而其他的代码放在他的前面就是前置通知,放在后面就是后置通知······

你会惊喜的发现,这不就是动态代理吗?

确实,这是 Spring 为我们的提供的一种可以在代码中手动控制代码何时执行的一种方式,我们除了可以使用引入外部类的方式实现,也可以使用这种自己写具体功能的方式实现。

使用注解的方式配置通知类型

我们在配置文件中使用 aop:aspect 指定了一个类作为切面类,所以可以直接在该类上面使用注解替代:

  • @Aspect

我们在配置文件中使用 aop:before 指定日志类型,并绑定了一个方法,所以可以直接在方法上面使用注解:

  • @Before()

使用注解的前提就是必须先在配置文件中配置一下:

代码语言:javascript复制
<!--  配置 Spring 开启注解 AOP 的支持  -->
<aop:aspectj-autoproxy/>

那么切入点表达式怎么办呢?

可以这样替代:

代码语言:javascript复制
/**
 * 配置切入点表达式
 */
@Pointcut("execution(* top.wsuo.service.impl.*.*(..))")
private void pointcut(){};
/**
 * 前置通知
 */
@Before("pointcut()")
public  void beforePrintLog(){
    System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}

来看一下完整的:

代码语言:javascript复制
@Component("logger")
@Aspect
public class Logger {
    /**
     * 配置切入点表达式
     */
    @Pointcut("execution(* top.wsuo.service.impl.*.*(..))")
    private void pointcut(){};
    /**
     * 前置通知
     */
    @Before("pointcut()")
    public  void beforePrintLog(){
        System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
    }
    /**
     * 后置通知
     */
    @After("pointcut()")
    public  void afterReturningPrintLog(){
        System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
    }
    /**
     * 异常通知
     */
    @AfterThrowing("pointcut()")
    public  void afterThrowingPrintLog(){
        System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
    }
    /**
     * 最终通知
     */
    @AfterReturning("pointcut()")
    public  void afterPrintLog(){
        System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
    }
}

或者使用环绕通知,它的代码是自己写的,你想让他什么时候执行他就什么时候执行。

代码语言:javascript复制
@Around("pointcut()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
    Object rtValue = null;
    try{
        Object[] args = pjp.getArgs();//得到方法执行所需的参数
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。前置");
        rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。后置");
        return rtValue;
    }catch (Throwable t){
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。异常");
        throw new RuntimeException(t);
    }finally {
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。最终");
    }
}

那么能不能使用纯注解呢?

当然可以,只需要在配置类上加一个注解:

代码语言:javascript复制
@EnableAspectJAutoProxy

TX

再谈事务控制

之前我们已经提到事务控制了,但是没有使用 AOP 去解决这个问题,这里我们解决一下。

基于配置文件的方式方式配置事务控制

如果我们使用 xml 配置 Tx,那么我们可以这么写:

代码语言:javascript复制
<!--配置aop-->
<aop:config>
    <!--配置通用切入点表达式-->
    <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
    <aop:aspect id="txAdvice" ref="txManager">
        <!--配置前置通知:开启事务-->
        <aop:before method="beginTransaction" pointcut-ref="pt1"></aop:before>
        <!--配置后置通知:提交事务-->
        <aop:after-returning method="commit" pointcut-ref="pt1"></aop:after-returning>
        <!--配置异常通知:回滚事务-->
        <aop:after-throwing method="rollback" pointcut-ref="pt1"></aop:after-throwing>
        <!--配置最终通知:释放连接-->
        <aop:after method="release" pointcut-ref="pt1"></aop:after>
    </aop:aspect>
</aop:config>

这是我们自己实现的事务控制,那么 Spring 有没有帮我们实现呢? 答案是肯定的。

首先我们更改 xml 文件的命名空间:

代码语言:javascript复制
<?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:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
        
    <!-- 配置账户的持久层 -->
    <bean id="accountDao" class="top.wsuo.dao.impl.AccountDaoImpl">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf-8"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
    
    <bean class="top.wsuo.service.impl.AccountServiceImpl" id="accountService">
        <property name="accountDao" ref="accountDao"/>
    </bean>
    
    <!--  配合事务管理器  -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <property name="dataSource" ref="dataSource"/>
    </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:attributes>
    </tx:advice>
    
    <!--  配置 AOP  -->
    <aop:config>
        <aop:pointcut id="pt" expression="execution(* top.wsuo.service.impl.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
    </aop:config>
</beans>

我们来看一下几个新的标签:

  • DataSourceTransactionManager:这个类是 Spring 提供的,我们只需要用它创建对象;
  • <tx:advice>:配置事务的通知;
    • id:是唯一标识,可随意取名;
    • transaction-manager:给事务通知提供一个事务管理器的引用;
  • <tx:attributes>:配置事务的属性;
    • <tx:method:事务对应的方法;
      • name对应具体的方法名,可以使用通配符;
      • propagation:指定事务的传播行为,默认是REQUIRED表示一定会有事务,SUPPORTS 用于查询方法,表示可以没有事务。
      • read-only:指定事务是否只读。
      • isolation:指定事务的隔离级别,默认使用数据库的隔离级别。
  • <aop:advisor>:建立 AOPTx 的关系;
基于注解的方式配置事务控制

老样子,我们首先在配置文件中配置,让 Spring 开启对注解配置事务的支持:

代码语言:javascript复制
<tx:annotation-driven transaction-manager="transactionManager"/>

如果想进行事务控制,只需要在类上加一个注解:

代码语言:javascript复制
@Transactional

那如果纯注解呢?

在配置类上加上一个注解即可:

代码语言:javascript复制
@EnableTransactionManagement

JdbcTemplate

最后介绍一个工具类库:Spring JdbcTemplate

由于使用 Template 一般都是配合 Spring 使用的,所以我们将与数据库连接需要创建的对象都交给 Spring 来管理。

代码语言:javascript复制
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
    <constructor-arg ref="dataSource" name="dataSource"/>
</bean>

然后我们只需要从容器中获取即可:

代码语言:javascript复制
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
JdbcTemplate jdbcTemplate = ac.getBean("jdbcTemplate", JdbcTemplate.class);
  • 用于增删改的方法:
代码语言:javascript复制
jdbcTemplate.update("delete from account where id = ?", 1);
  • 用于查询的方法:
代码语言:javascript复制
List<Account> accounts = jdbcTemplate.query("select * from account where MONEY > ?;",
        new BeanPropertyRowMapper<>(Account.class), 100f);

Integer count = jdbcTemplate.queryForObject("select count(*) from account where MONEY = ?", Integer.class, 1000f);
  • BeanPropertyRowMapper 相当于 DBUtils 中的 BeanHandler 类,用于处理查询的返回值结果的。

但是到这里会有一个问题就是如果我们有多个 DaoImpl,那么对于每个实现类来说我们都要写一遍这个:

代码语言:javascript复制
private JdbcTemplate jdbcTemplate;

那么我们能不能抽取出来呢? 可以,但是 Spring 已经帮我们实现了,我们只需要将我们的实现类继承一个 JdbcDaoSupport 即可。

我们可以直接 getJdbcTemplate(),而不用去 Spring 中配置;

代码语言:javascript复制
@Override
public Account findAccountById(Integer id) {
    return Objects.requireNonNull(getJdbcTemplate()).query("select * from account where ID = ?;",
            new BeanPropertyRowMapper<>(Account.class), id).get(0);
}

xml 直接这样配置即可:

代码语言:javascript复制
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</bean>

<bean class="top.suo.dao.impl.AccountDaoImpl" id="accountDao">
    <property name="dataSource" ref="dataSource"/>
</bean>

如果你想更深层次的理解 Spring,强烈建议去看书或者官方文档,本文只是一个入门的水平,而且你有地方没懂的话还有可能入门失败。

0 人点赞