spring_Learn

注意,以下为个人学习总结使用,有他人帖子引用(节省点时间),如有侵权,请联系删改

(注意:有些许代码属于个人工程,使用的是工程名称,但是没给出具体代码,什么?发到git上什么的,有点懒了拉(笑))

Spring

首先,我们先来回顾下之前学过的javaee三层结构

详细见jsp环节

随着Spring,这三层也可以进行一个优化和缩减

什么是Spring

Spring 是分层的 Java SE/EE 应用 full-stack轻量级开源框架,以 IOC(Inverse Of Control:控制反转)和AOP(Aspect Oriented Programming:面向切面编程)为内核提供了展现层 SpringMVC 和持久层Spring JDBC 以及业务层事务管理等众多的企业级应用技术,还能整合众多著名的第三方框架和类库,逐渐成为使用最多的 Java EE 企业应用开源框架。

然后这三层可以改成


程序中的耦合

什么是耦合

首先我们来回顾下数据库连接(maven工程)

public class testjdbc {
    static String username = "root";
    static String password = "root";
    static String url = "jdbc:mysql://localhost:3306/lesson?serverTimezone=Asia/Shanghai";
    //算是回顾数据库操作吧(笑)
    public static void main(String[] args) throws Exception{
        //想起[注册驱动],这里先用一种错误的方式来连接
        DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
        //想起[获取连接对象]
        Connection connection = DriverManager.getConnection(url,username,password);
        //想起[使用PreparedStatement对象,预处理sql语句]
        String sql = "SELECT * FROM student";
        PreparedStatement pst = connection.prepareStatement(sql);
        //想起[使用resultSet执行语句]
        ResultSet resultSet = pst.executeQuery();//执行语句
        while(resultSet.next()){
            Integer id = resultSet.getInt("id");
            Integer age = resultSet.getInt("age");
            String name = resultSet.getString("name");
            String sex = resultSet.getString("sex");
            student student = new student(id,name,sex,age);
            System.out.println(student.toString());
        }
        //工具用完记得放回工具箱
        resultSet.close();
        pst.close();
        connection.close();
    }
}

这是一段标准的数据库连接,让我们聚焦其中第8行

DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());

这里的启动驱动必须要靠mysql依赖来启动,如果我们去pom.xml中去除这个依赖之后,程序就直接报错了

设想一下:如果一个数据库查询的功能错误,就会导致其他功能也同时报错,这是不是有点拖人下水的感觉。好了,我们直接说明什么是耦合

耦合

耦合指的是程序间的依赖关系:
类与类的依赖关系(上述代码表述的就是这个)
方法的依赖关系

注意:我们不能消除程序间的依赖关系,只能尽可能的降低程序的依赖关系。
这种降低程序间的依赖关系就叫做解耦

那么我们要怎么修改上述代码进行解耦呢? (降低耦合度)

使用反射解耦合

Class.forName("com.mysql.cj.jdbc.Driver");

这样就算依赖掉了,也不会出现报错的情况,顶多抛出一个异常


使用工厂解耦

首先需要明确几个概念

1.Bean 组件 在java中指的是dao层 业务层的接口实现
2.javaBean 实体类,avaBean并不完全等同于实体类,因为实体类只是组件中的一部分
3.工厂:创建javabean(创建dao 北务层实现类的对象)


现在我们给出一个处境(详细见com.mrme.maven---dao,service)

假设上述使用jdbc实现,但是现在需要我们使用orical实现,那么service部分我们需要修改:

    public void addAccount() {
        //使用jdbc实现,但是如果后面需要使用orical实现呢?
//        AddCountDao addCountDao = new AddCountDaoImpl();
        //使用orical实现,这样未免会出现修改源代码的情况
        AddCountDao addCountDao = new AddCountDaoOracalImpl();
        addCountDao.addCount();
    }

这样未免会出现需要修改源代码的地方,嗯哼..这是非常忌讳的,需要我们使用工厂来解决问题

解决方案:
1:通过一个配置文件来管理我们的dao接口实现类和业务类实现类properties配置文件来管理k=v(唯一标识=全限定名(包名+类名))
2.通过读取配置文件中的内容,获取v。基于反射的技术创建对象

下面是工厂

package com.mrme.factory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * 工厂类,专门帮助我们创建对象
 */
public class BeanFactory {
    //读取propertiesz中的信息
    static Properties properties;
    static{
        //类加载器加载bean.properties这个路径下的配置文件
        try {
            InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            properties = new Properties();
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static Object getBean(String name){
        //k得到v
        try {
            String v = properties.getProperty(name);
            Object o = Class.forName(v).newInstance();
            return o;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }
}

请注意 bean.properties中的内容要写对

# k = v(javabean名称 = 类全限定名)
AddCountDaoImpl = com.mrme.dao.impl.AddCountDaoImpl
AddCountDaoOracalImpl = com.mrme.dao.impl.AddCountDaoOracalImpl
AccountService = com.mrme.sevice.Impl.AccountServiceImpl

这样service中就可以改成

package com.mrme.sevice.Impl;

import com.mrme.dao.AddCountDao;
import com.mrme.dao.impl.AddCountDaoImpl;
import com.mrme.dao.impl.AddCountDaoOracalImpl;
import com.mrme.factory.BeanFactory;

public class AccountServiceImpl implements com.mrme.sevice.AccountService {
    @Override
    public void addAccount() {
        //使用jdbc实现,但是如果后面需要使用orical实现呢?
//        AddCountDao addCountDao = new AddCountDaoImpl();
        //使用orical实现,这样未免会出现修改源代码的情况
//        AddCountDao addCountDao = new AddCountDaoOracalImpl();
        AddCountDao addCountDao = (AddCountDao)BeanFactory.getBean("AddCountDaoImpl");
        addCountDao.addCount();
    }
}

后续只需要修改一个名称就可以了(15行中字符串)


优化工厂代码,生成单例bean

对于上述代码来说,其实还有一个问题,工厂每次生成的javabean的地址都是不固定的

说明每次生成的javabean都重写创造了新的对象,这样耗时耗资源。因此我们需要尽可能优化成单例

我们预处理所有的bean.propertiesd的语句,然后再从存储的地方取出就行,下面代码是修改工厂部分

package com.mrme.factory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 工厂类,专门帮助我们创建对象
 */
public class BeanFactory {
    //读取properties中的信息
    static Properties properties;
    static Map<String,Object> beans;
    static{
        //类加载器加载bean.properties这个路径下的配置文件
        try {
            //获取输入的流
            InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            //存到Properties中(操纵Properties文件程度的能力)
            properties = new Properties();
            properties.load(is);
            beans = new HashMap<>();
            //获取所有的k对
            Enumeration<Object> keys = properties.keys();
            while(keys.hasMoreElements()){
                String key = keys.nextElement().toString();
                String value = properties.getProperty(key);
                Object o = Class.forName(value).newInstance();
                beans.put(key,o);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
//    public static Object getBean(String name){
//        //k得到v
//        try {
//            String v = properties.getProperty(name);
//            Object o = Class.forName(v).newInstance();
//            return o;
//        } catch (Exception e) {
//            e.printStackTrace();
//            throw new RuntimeException();
//        }
//    }
        public static Object getBean(String name){
        return beans.get(name);
        }
}

就是用Map存kv对进行预处理

这种方法也是Spring中使用的单例思想(实际实现还是复杂)


IOC(控制反转)

从这开始就是Spring的正课了

首先我们先来阐明IOC是什么

根据上面我们的例子:我们创建对象有两种方式(1)new(2)通过工厂被动创建

而(2)就是IOC的体现

那么Spring中的IOC是如何做的呢?

mrme-spring-01

使用xml的ioc环境搭建

在resources中创建一个spring的xml文件

使用bean标签对各个bean进行管理

就是帮助我们管理bean的标签,将bean放在spring的ioc容器里面去
id : bean的名称 值任意 但是必须要唯一。
class : bean所属类的全限定名

<bean id="accountDao" class="com.mrme.dao.impl.AccountDaoImpl"></bean>
<bean id="AccountServiceImpl" class="com.mrme.service.impl.AccountServiceImpl"></bean>

访问:

package com.mrme;

import com.mrme.dao.AccountDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAccount {
    public static void main(String[] args) {
        //读取spring的配置文件 初始化一个ioc容器
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //获取bean
        AccountDao accountDao = (AccountDao) context.getBean("accountDao");
        accountDao.addAccount();
    }
}

bean工厂中的细节

上述代码中我们获取ioc容器使用ApplicationContext,当我们查看结构的时候,具有多重继承

其中最上层的继承就有BeanFactory

那么我们就可以抛出两个问题:

这两bean工厂有什么区别

直接给出结论:ApplicationContext在初始化容器的时候就直接初始化完成了,而BeanFactory需要在使用(getbean)才会初始化

ApplicationContext接口的实现类是?

ClassPathXmlApplicationContext

它可以加载类路径下的配置文件,要求配置文件必须在类路径下,否则加载不了(这种比较常用)。之前演示的就是ClassPathXmlApplicationContext,这里不再重复演示了。

FileSystemXmlApplicationContext

它可以加载磁盘任意路径下的配置文件(必须有访问权限)。(少用,毕竟都有地方给你存了还乱存)

AnnotationConfigApplicationContext

它是用于读取注解创建容器的,后面再介绍。


Spring对bean的管理方式

Spring实例化bean的方式

无参数构造函数的实例化方式

使用工厂中的普通方法实例化对象

我们设想一个场景:我们要使用别人写好的代码,别人写好的代码通常是封装在一个jar包中的,我们要怎么创建jar包中字节码文件的对象呢?(我们无法通过修改源码的方式来提供默认构造方法)

模拟一个工厂类(它可能是存在于jar包中的),通常jar包都会暴露一个工厂类,给调用者创建对象。

使用工厂中的静态方法实例化对象

public class TestUSerDao {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applcationContext.xml");
        //法1
        UserDao userDao = (UserDao) context.getBean("UserDao");
        userDao.addUser();
        //法2
        PersonDao personDao = (PersonDao) context.getBean("PersonDao");
        personDao.addPerson();
        //法3
        OrderDao orderDao =(OrderDao) context.getBean("OrderFactory");
        orderDao.addOrder();
    }
}
<?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="UserDao" class="com.mrme.dao.impl.addUserImpl"></bean>
    <!--
        使用工厂中的普通方法实例化对象
        factory-bean:工厂bean的id
        factory-method:工厂bean中获取bean的方法
    -->
    <bean id="PersonFactory" class="com.mrme.dao.factory.PersonFactory"></bean>
    <bean id="PersonDao" class="com.mrme.dao.impl.PersonDaoImpl" factory-bean="PersonFactory" factory-method="getPerson"></bean>
    <!--
        使用工厂中的静态方法实例化对象
    -->
    <bean id="OrderFactory" class="com.mrme.dao.factory.OrderFactory" factory-method="getOrder"></bean>
</beans>

Bean的细节

Bean的作用域

在默认情况下,bean是单例的

如何修改为多例

<bean id="UserDao" class="com.mrme.dao.impl.addUserImpl" scope="prototype"></bean>
<bean id="UserDao" class="com.mrme.dao.impl.addUserImpl" scope="singleton"></bean>

使用,prototype多例,singleton单例

Bean的生命周期

其实很简单,容器初始化后就生成了,摧毁后就销毁了

在配置文件中配置其初始化或者销毁

<bean id="UserDao" class="com.mrme.dao.impl.addUserImpl" init-method="" destroy-method=""></bean>

详细见深究Spring中Bean的生命周期 - Java知音号 - 博客园


DI

什么是DI以及作用

依赖注入:Dependency Injection

DI的作用:依赖关系的管理,依赖都交给spring来维护(IOC的作用:降低程序间的耦合(依赖关系))

依赖注入的方法(三种)

第一种:使用set方法提供(默认)
第二种:使用构造函数提供(有构造的时候才需要用)
第三种:使用注解提供(后面再介绍)

详细见D:\java学习\com.mrme.maven\mrme-spring-03

Set方法注入

假设我们需要向一个新建的User中注入值,前往xml文件中

完成对bean标签的注入

name:类对象的名称

value:name对应的值(必须是基本数据类型和字符串类型)

<bean id="User" class="com.mrme.pojo.User">
        <property name="name" value="MRME"></property>
        <property name="age" value="20"></property>
        <property name="address" value="幻想乡"></property>
</bean>

测试:

public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("ApplicationContext.xml");
        User user = (User) applicationContext.getBean("User");
        System.out.println(user);
    }

如果User内还需要创建一个构造方法Car,就不能直接使用标签加入了(报错),我们需要再导入一个bean

:注入的是一个引用数据对象的值,对象是另一个标签

	<bean id="Car" class="com.mrme.pojo.Car">
        <property name="name" value="宝马"></property>
        <property name="type" value="114514"></property>
    </bean>

    <bean id="User" class="com.mrme.pojo.User">
        <property name="name" value="MRME"></property>
        <property name="age" value="20"></property>
        <property name="address" value="幻想乡"></property>
        <property name="car" ref="Car"></property>
    </bean>

这样之后就可以了,测试如下,正常显示

User{name='MRME', age=20, address='幻想乡', car=Car{type='114514', name='宝马'}}

使用构造函数提供

构造方法注入

	<!--构造方法-->
    <bean id="car2" class="com.mrme.pojo.Car2">
        <constructor-arg name="type" value="宝马"></constructor-arg>
        <constructor-arg name="year" value="114514"></constructor-arg>
    </bean>

如果是构造内加一个构造就用ref就行,方法同上

复杂数据类型(少用)

这里导入的例如list,map这样的复杂数据类型

<!--复杂构造-->
    <bean id="complex" class="com.mrme.pojo.complex">
        <property name="strs">
            <!--数组-->
            <array>
                <value>1</value>
                <value>2</value>
                <value>3</value>
            </array>
        </property>
        <!--list-->
        <property name="list">
            <list>
                <value>a1</value>
                <value>a2</value>
                <value>a3</value>
            </list>
        </property>
        <!--set-->
        <property name="set">
            <set>
                <value>b1</value>
                <value>b2</value>
                <value>b3</value>
            </set>
        </property>
        <!--map-->
        <property name="map">
            <map>
                <entry key="m1" value="1"></entry>
                <entry key="m2" value="2"></entry>
                <entry key="m3" value="3"></entry>
            </map>
        </property>
        <!--properties-->
        <property name="pros">
            <props>
                <prop key="c1">1</prop>
                <prop key="c2">2</prop>
                <prop key="c3">3</prop>
            </props>
        </property>
    </bean>

因为这种是硬编码,没有多少灵活性,使用比较少


基于IOC的案例

详细见D:\java学习\com.mrme.maven\mrme-spring-04-account


基于注解型的IOC

在spring中存在4种注解型IOC:

用于创建对象的:
作用:他们的作用就和在XML配置文件中编写一个标签实现的功能是一样的。

用于注入数据的:
作用:他们的作用就和在xml配置文件中的bean标签中写一个标签的作用是一样的。

用于改变作用范围的:
作用:他们的作用就和在bean标签中使用scope属性实现的功能是一样的。

和生命周期相关的:
作用:他们的作用就和在bean标签中使用init-method和destroy-methode的作用是一样的

下面开始正式介绍使用:


前置:需要在xml中开启包扫描,不扫描会报错

<!--context:component-scan:定义包扫描的规则,告诉spring,要去指定的包下面扫描所属的类-->
<!--base-package:规定包扫描范围-->
<context:component-scan base-package="com.mrme"></context:component-scan>

用于创建对象的注解

作用:类似xml中bean标签

使用:

@Component 修饰类

作用就是把当前类交给Spring容器
具有一个value属性,用于指定bean的id
如果不写bean就会默认用当前类名小写

//AccountDaoImpl类
@Component(value = "AccountDao")
@Component//默认就是accountDaoImpl

@Component注解的其他衍生注解:

@Controller @Service @Repository

这三种注解功能和@Component的作用是一样的,仅仅只是明确三层开发

三种分别用于控制层,业务层,持久层(dao)


用于注入数据的注释

@Autowired
AccountDao accountDao;

作用:类似xml中的或者

使用:

@Autowired标签

修饰成员变量

注入原理先按照类型进行注入,再按照名称进行注入(其中名称注入是与bean中的id进行匹配),按照类型注入只有唯一的类型才能注入成功,如果有多的就需要再进行名称注入

两个都是AccountDao类,就需要名称注入了

@Component(value = "AccountDao")

也就是注释中的value

详细原理

类型注入如上,容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。

但是如果有同种的类型出现,就会出现需要名称注入


其他类型的注入数据的注释

这类的标签跟xml中的作用一样

@Qualifier:

无法单独使用,需要配合@Component @Controller @Service @Repository使用,在按照类型注入的基础之上再按照名称注入。(也就是在类型匹配基础上再名称匹配)

但是在给方法参数注入时可以单独使用(这个我们后面讨论)

	@Autowired
    @Qualifier("accountDaoImpl2")//注意名称小写
    AccountDao accountDao;

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

@Resource

作用:默认的情况下,可以直接按照bean的id注入。它可以独立使用。

属性:name:用于指定bean的id。

	  type: 按照类型进行注入。

如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。(名称注入)

@Resource装配顺序:

1,如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常

2,如果指定了name,则从Spring上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常

3,如果指定了type,则从Spring上下文中找到类型匹配的唯一bean进行装配,找不到或找到多个,都抛出异常

4,如果既没指定name,也没指定type,则自动按照byName方式进行装配

当然也可以配合@Qualifier(唔,有点画蛇添足了耶)

@Value

作用:用于注入基本类型和String类型的数据。

属性:value:用于指定数据的值。它也可以使用spring中SpEL(也就是spring的el表达式)。

SpEL的写法:${表达式}。

@Test
    public void test01(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("ApplicationContext.xml");
//        AccountService accountService = (AccountService) applicationContext.getBean("AccountService");
//        accountService.addService();
        Animal animal = (Animal) applicationContext.getBean("animal");
        System.out.println(animal);
    }
 @Value("野兽先辈")
    private String name;
    @Value("24")
    private int age;
    @Value("是学生")
    private String type;

改变作用域和关于生命周期的注解

@Scope:

作用:用于指定bean的作用范围。

属性:value:指定范围的取值。常用取值:singleton(单例)、prototype(多例)。

@Scope(value = "prototype")
@Scope(value = "singleton")

@PreDestroy 作用:用于指定销毁方法。

@PostConstruct 作用:用于指定初始化方法。


使用IOC对先前案例的优化

详细见 mrme-spring-04-account-annotation

主要的优化是:xml中的大多bean配置被配置成为IOC注入

IOC注入的方式无法配置外部导入的依赖,例如

<!--配置c3p0数据库的基本配置-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <!--还是写localhost算了,别jb乱配host-->
        <property name="jdbcUrl" value="jdbc:mysql://localhost/lesson?serverTimezone=Asia/Shanghai"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
        <property name="checkoutTimeout" value="1000"></property>
    </bean>
    <!--配置dbUtils-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

这些都是外部的依赖

所以这里还需要进行进一步的优化:使用java配置类优化IOC案例

java配置类(重要)

详细见 mrme-spring-04-account-javaconfig

由于我们导入的类不能进行修改,但是bean标签的配置过于麻烦,有没有方法能够不配置xml又可以使用这些类呢

有的兄弟有的:java配置类

(1) 配置类的作用和bean.xml一样。首先介绍

Configuration注解:

作用:指定当前类是一个配置类

细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写(后面再介绍这个细节)。

@Configuration
public class SpringConfiguration {
}

我们先来回忆一下,当我们要使用xml时,我们需要先指定扫描范围,但是现在我们已经失去xml了

这就是下一个要介绍的注解:

ComponentScan注解

作用跟xml中的

<context:component-scan base-package="mrme"></context:component-scan>

一样

此注解的属性有两个:valuebasePackages,并且互为别名。(用那个都可以)

@Configuration
@ComponentScan(basePackages = "com.mrme")
public class SpringConfiguration {
}

Bean注解

作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中

属性: name:用于指定bean的id。当不写时,默认值是当前方法的名称(value和name两个属性都可以使用)

细节:当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象。

@Configuration
@ComponentScan(basePackages = "com.mrme")
public class SpringConfiguration {
    @Bean
    public DataSource dataSource(){
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        try {
            dataSource.setUser("root");
            dataSource.setPassword("root");
            dataSource.setJdbcUrl("com.mysql.cj.jdbc.Driver");
            dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
            return dataSource;
        } catch (PropertyVetoException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }
    @Bean
    public QueryRunner queryRunner(@Qualifier("dataSource") DataSource dataSource){
        QueryRunner runner = new QueryRunner(dataSource);
        return runner;
    }
}

在queryRunner中使用了@Qualifier,使得DataSource dataSource等同与@Qualifier("dataSource")指向内容

这里的用法就是:给方法参数注入,可以单独使用


AnnotationConfigApplicationContext

用于注释配置

配置完后我们需要测试

但是先前使用的ClassPathXmlApplicationContext现在已经不能用了(这个是基于存在resourse中的文件来获取),这次我们需要从方法中调用就需要使用AnnotationConfigApplicationContext

用于读取注解创建容器

@Test
    public void test01(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        AccountService acc = (AccountService) applicationContext.getBean("accountServiceImpl");
        List<Account> account = acc.findAllAccount();
        for(Account x : account){
            System.out.println(x);
        }

    }

跟ClassPathXmlApplicationContext差不多的用法


对java配置类的优化

首先我们来看配置类SpringConfiguration.java,这里创建的两个方法都是属于管理db的方法(详细见上)但是如果后面要引入更多模块的方法,管理就会变的混乱,违反了职责单一性的原则

所以我们可以对各个不同模块的类进行分离再用一个父类导入各个子模块

那么我们就需要使用

import注解

作用:用于导入其他的配置类

值:Value,传入的是多个Class(用{}分割)

细节:当我们使用Import的注解之后,有Import注解的类就父配置类,而导入的都是子配置类。我们此时不需要加Configuration注解,只需要在父配置类上添加Import注解

public @interface Import {
    Class<?>[] value();
}
@Import(value = {dataSourceConfiguration.class})
@Configuration
@ComponentScan(basePackages = "com.mrme")
public class SpringConfiguration {

}

这里已经把原来的类分离成另外的 dataSourceConfiguration类(这个类就不需要再进行修饰了,import后已经是了)


其次看下面代码

	@Bean
    public DataSource dataSource(){
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        try {
            dataSource.setUser("root");
            dataSource.setPassword("root");
            dataSource.setJdbcUrl("jdbc:mysql://localhost/lesson?serverTimezone=Asia/Shanghai");
            dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
            dataSource.setCheckoutTimeout(3000);
            return dataSource;
        } catch (PropertyVetoException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

其中的值设计都使用了硬编码

前面Maven中我们使用到了properties文件进行存储数据

User=root
Password=root
jdbcUrl=jdbc:mysql://localhost/lesson?serverTimezone=Asia/Shanghai
DriverClass=com.mysql.cj.jdbc.Driver
CheckoutTimeout=3000

这里也可以使用

@PropertySource

作用:用于指定properties文件的位置

public @interface PropertySource {
    String name() default "";

    String[] value();

    boolean ignoreResourceNotFound() default false;

    String encoding() default "";

    Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}

需要传入name,默认可以不写name,也就是property文件的路径

@PropertySource("classpath:db.properties")
public class dataSourceConfiguration {
    @Value("${jdbc.User}")
    private String User;

    @Value("${jdbc.Password}")
    private String Password;

    @Value("${jdbc.JdbcUrl}")
    private String JdbcUrl;

    @Value("${jdbc.DriverClass}")
    private String DriverClass;

    @Bean
    public DataSource dataSource(){
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        try {
            dataSource.setUser(User);
            dataSource.setPassword(Password);
            dataSource.setJdbcUrl(JdbcUrl);
            dataSource.setDriverClass(DriverClass);
            dataSource.setCheckoutTimeout(3000);
            return dataSource;
        } catch (PropertyVetoException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }
    @Bean
    public QueryRunner queryRunner(@Qualifier("dataSource") DataSource dataSource){
        QueryRunner runner = new QueryRunner(dataSource);
        return runner;
    }
}

关于:@Configuration注解一定需要吗?

可以不要,最好是必要的

@Configuration
@ComponentScan(basePackages = "com.mrme")
@Import(value = {dataSourceConfiguration.class})
public class SpringConfiguration {

}

代码中如果注释掉@Configuration代码也会编译通过

如果使用@Configuration由于需要使用到ADataSource,这个时候,不再重新的创建ADataSource的对象,而是直接从spring容器(spring的单例池中获取)。所以ADataSource只会创建1次

去除后

第一次创建是使用@Bean注解修饰aDataSource方法的时候,创建的。第二次创建是@Bean注解修饰bRunner方法的时候,由于BRunner对象的创建需要依赖于ADataSource。此时ADataSource这个bean不是从单例池中获取,而是重新又创建了1次。


Spring中整合junit

导入Spring整合Junit的maven坐标

		<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

使用junit提供的一个注解(@Runwith)把原有的main方法替换成spring提供的main方法。

@Runwith

public @interface RunWith {
    Class<? extends Runner> value();
}

该注解的功能是替换Runner(运行器)。

接着使用

@ContextConfiguration

public @interface ContextConfiguration {
    @AliasFor("locations")
    String[] value() default {};

    @AliasFor("value")
    String[] locations() default {};

    Class<?>[] classes() default {};

    Class<? extends ApplicationContextInitializer<?>>[] initializers() default {};

    boolean inheritLocations() default true;

    boolean inheritInitializers() default true;

    Class<? extends ContextLoader> loader() default ContextLoader.class;

    String name() default "";
}

导入需要使用的ioc环境

其中location和value是一样的,用于xml环境导入,例如

@ContextConfiguration("classpath:xxx.xml")

​ Class<?>[] classes() default {};用于导入类

@ContextConfiguration(classes = {SpringConfiguration.class, dataSourceConfiguration.class})//测试模板中引入ioc环境

具体的测试类完成后是:

@RunWith(SpringJUnit4ClassRunner.class)//测试模板类需要一个测试启动器
@ContextConfiguration(classes = {SpringConfiguration.class, dataSourceConfiguration.class})//测试模板中引入ioc环境
public class TestJunit {
    @Autowired
    AccountService accountService;

    @Test
    public void test01(){
        List<Account> account = accountService.findAllAccount();
        for(Account x : account){
            System.out.println(x);
        }
    }
}

主要就是为了省去

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfiguration.class);
AccountService acc = (AccountService) applicationContext.getBean("accountServiceImpl");

这样全部由Spring来进行管理,不用到处获取bean了,我们程序员可真是轻松啊(* _ *)


补一个东西:获取properties中的数据

<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>

搭建事物转账案例

详细见 mrme-spring-05-account

搭建完代码后我们来看这的代码

@Override
    public void transform(String sourceName, String targetName, int money) {
        Account sourceAccount = accountDao.findAccountName(sourceName);
        Account targetAccount = accountDao.findAccountName(targetName);
        if(sourceAccount != null|| targetAccount != null){
            if(sourceAccount.getMoney() >= money){
                //转钱
                sourceAccount.setMoney(sourceAccount.getMoney()-money);
                targetAccount.setMoney(targetAccount.getMoney()+money);
                //更新
                accountDao.updataAccountMoney(sourceAccount);
                accountDao.updataAccountMoney(targetAccount);
            }
        }
    }

看似没什么问题,但是假设代码运行到11后停止报错了,那么转账的钱就之扣不加了,这其中就违反了事务的一致性

让我们回到数据库复习下事务是什么

什么是事务:事务指的是一组逻辑操作,这些操作是最小单位不可分割的,这些操作要么全部执行成功,要么全部执行失败。事务的特性:ACID原则
A:原子性事务的逻辑操作必须是最小单位不可分割的。
C:一致性指的是事务提交前和事务提交后的数据必须保持一致。
I:隔离性指的是多个事务之间必须互相独立不能影响。
D:持久性指的是事务提交之后,数据必须要持久化的保存在数据表里面。

事务的隔离级别:
读未提交:一个事务读到了另外一个事务没有提交的数据容易造成脏读的数据
读已提交:oracle数据库默认的事务隔离级别。解决了脏读数据的出现,但是出现了不可重复读的现象。
可重复读:mysql默认的事务隔离级别。解决了不可重复读的问题,但是出现了幻读的问题。
可串行化:解决了所有事务的问题(脏读不可重复读幻读的问题)


再来看这几段代码

我们选取3,4,11,12行代码

        Account sourceAccount = accountDao.findAccountName(sourceName);
        Account targetAccount = accountDao.findAccountName(targetName);
        accountDao.updataAccountMoney(sourceAccount);
        accountDao.updataAccountMoney(targetAccount);

这每行代码都是独立执行的都需要调用一次数据库,也就是在每次执行都是在调用不同的线程池,但是转账这个事务是表示一个事务的,这也表明没有满足原子性,因此我们需要对这一个方法进行控制单一线程池(也就是使得线程中只有一个能控制事物的连接对象)

如何统一数据库连接?

(1)ThreadLocal:将当前线程与数据库连接对象绑定(一个类,不是bean)

(2)定义一个事物管理器类:

​ 开启事务的方法
​ 提交事务的方法
​ 回滚事务的方法
​ 释放资源事务的方法

伪代码如下:


补充try-catch快捷键ctrl+alt+T


单线程连接数据库的类

package com.mrme.Util;

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

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@Component
public class ConnectionUtil {

    @Autowired
    DataSource dataSource;
    //ThreadLocal用于保存线程的作用
    private ThreadLocal<Connection> tl = new ThreadLocal<>();

    //获取数据库连接
    public Connection getConnection(){
        try {
            Connection connection = tl.get();
            if(connection == null){//如果获取的是null,说明tl还没初始化,还需要从线程池中获取第一个线程(并且初始化tl)
                connection = dataSource.getConnection();
                tl.set(connection);
            }
            return connection;
        } catch (SQLException throwables) {
            throwables.printStackTrace();
            throw new RuntimeException();
        }
    }

    //还需要关闭,释放资源
    public void closeConnection(){java
        tl.remove();
    }
}

管理单线程连接数据库的类

package com.mrme.Util;

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

import java.sql.SQLException;

/**
 * 事务管理相关的工具类,它包含了开启事务、提交事务、回滚事务和释放连接等方法。
 */

@Component
public class TransactionManager {

    @Autowired
    ConnectionUtil connectionUtil;//获取到我们上一个唯一获取连接的类

    /**
     * 开启事物,也就是关闭事务的自动提交(手动提交)
     */
    public void beginTransaction(){
        try {
            connectionUtil.getConnection().setAutoCommit(false);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    public void conmit(){
        try {
            connectionUtil.getConnection().commit();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    public void rollBack(){
        try {
            connectionUtil.getConnection().rollback();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    public void close(){
        try {
            connectionUtil.getConnection().close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

更改后的业务类

package com.mrme.Service.Impl;

import com.mrme.Dao.AccountDao;
import com.mrme.Service.AccountService;
import com.mrme.Util.TransactionManager;
import com.mrme.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    AccountDao accountDao;
    @Autowired
    TransactionManager transactionManager;

    /***
     * 首先检查双方是否存在,然后检查金额是否足够,最后再更新账户数据
     * @param sourceName 转账方
     * @param targetName 接收方
     * @param money 金额
     */
    @Override
    public void transform(String sourceName, String targetName, int money) {
        try {
            //开启事务
            transactionManager.beginTransaction();
            //实现业务
            Account sourceAccount = accountDao.findAccountName(sourceName);
            Account targetAccount = accountDao.findAccountName(targetName);
            if(sourceAccount != null|| targetAccount != null){
                if(sourceAccount.getMoney() >= money){
                    //转钱
                    sourceAccount.setMoney(sourceAccount.getMoney()-money);
                    targetAccount.setMoney(targetAccount.getMoney()+money);
                    //更新
                    accountDao.updataAccountMoney(sourceAccount);
                    int a =10/0;
                    accountDao.updataAccountMoney(targetAccount);
                }
            }
            //提交事务
            transactionManager.conmit();
        } catch (Exception e) {
            e.printStackTrace();
            transactionManager.rollBack();
        } finally {
            transactionManager.close();
        }
    }

}

除了以上其实还有问题

(3)那就是dao层的的QueueRunner连接数据库还是使用database连接

那么 ApplictaionContext.xml 中的就不能使用ds了,需要手动导入

<bean id="runner" class="org.apache.commons.dbutils.QueryRunner">
<!--        <constructor-arg name="ds" ref="dataSource"></constructor-arg>-->
    </bean>

也就是我们刚刚手动创的类 ConnectionUtil.java 中获取连接

正好也提供了一个可以手动提供connection的方法

public int update(Connection conn, String sql, Object... params) throws SQLException {
        return this.update(conn, false, sql, params);
    }

代理

什么是代理:一开始买电脑只能自己从厂家哪里买 到 通过代理商来买(代理商帮你联系厂家)

从这个过程中我们看到代理作用就是增加了个人的个人的方便程度

换到代码中:其实就是在不修改源码的基础上对方法增强。(毕竟都是买电脑对吧,笑)

特点:代理对象在程序的运行过程中创建。

java中拥有两种代理方式:

jdk动态代理,基于子类的动态代理

jdk动态代理

基于接口的动态代理:涉及的类:Proxy

如何创建代理对象:使用Proxy类中的newProxyInstance方法

创建代理对象的要求:被代理类最少实现一个接口,如果没有则不能使用

例子见 mrme-spring-05-proxy

package com.mrme;

public interface ProductDao {
    public void buyProduct(String name);
}
package com.mrme.impl;

import com.mrme.ProductDao;

public class ProductDaoImpl implements ProductDao {
    @Override
    public void buyProduct(String name) {
        System.out.println(name + "售卖了电脑");
    }
}
package com.mrme.impl;

import com.mrme.ProductDao;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 消费者
 * 分别直接买
 * 让代理买
 */
public class consumer {
    public static void main(String[] args) {
        final ProductDaoImpl productDaoimpl = new ProductDaoImpl();
        productDaoimpl.buyProduct("厂家");
        //模拟让代理买电脑
        ProductDao proxy =(ProductDao)Proxy.newProxyInstance(productDaoimpl.getClass().getClassLoader(), productDaoimpl.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 {
                if(method.getName().equals("buyProduct")){
                    method.invoke(productDaoimpl,args);
                }
                return null;
            }
        });
        proxy.buyProduct("经销商");
    }
}


基于子类的动态代理

基于接口的动态代理:涉及的类:cglib(这个类需要导入maven)

<dependencies>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.1_3</version>
        </dependency>
    </dependencies>

如何创建代理对象:使用Enhancer.create

不需要接口的实现

package com.mrme.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 proxy {
    public static void main(String[] args) {
        Producer producer = new Producer();
        producer.buyProdecer("用户");
        //    public Object create(Class[] argumentTypes, Object[] arguments) {
        /**
         * Class[] argumentTypes:对象的class,
         * Object[] arguments:真正干活的
         */
        Producer proxy = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * @param o 当前代理对象的引用
             * @param method 代理对象需要执行的方法
             * @param objects 代理对象执行该方法需要用到的参数
             * @param methodProxy 当前执行方法的代理对象(不常用)
             * @return 返回值也是跟被代理对象执行方法的返回值一样
             * @throws Throwable
             */
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                if(method.getName().equals("buyProdecer")){
                    /**
                     * public Object invoke(Object obj, Object... args)
                     * Object obj:要增强的对象
                     * Object... args:代理对象执行该方法需要用到的参数
                     */
                    method.invoke(producer,objects);
                }
                return null;
            }
        });
        proxy.buyProdecer("厂商");
    }
}


使用动态代理实现事务控制

详细见 mrme-spring-05-account


AOP

AOP:面向切面编程,AOP是OOP的扩展和延伸,用来解决OOP开发中遇到的问题。

AOP利用的是一种横切技术,解剖开封装的对象内部,并将那些影响多个类的公共行为封装到一个可重用模块,这就是所谓的Aspect方面/切面。所谓的切面,简单点所说,就是将哪些与业务无关,却为业务模块所共同调用的行为(方法)提取封装,减少系统的重复代码,以达到逻辑处理过程中各部分之间低耦合的隔离效果。

AOP采取横向抽取机制,取代了传统的纵向继承体系重复性代码。

说人话:类似代理的实现

AOP的相关术语


AOP---AspectJ XML

算是入门把

xml配置

		<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>

切点模拟设置,我们假设要加强deleteUser

public class UserServiceImpl implements UserService {
    @Override
    public void findUser() {
        System.out.println("findUser实现了");
    }

    @Override
    public void updataUser() {
        System.out.println("updataUser实现了");
    }

    @Override
    public void insertUser() {
        System.out.println("insertUser实现了");
    }

    @Override
    public void deleteUser() {
        System.out.println("deleteUser实现了");
    }
}

代理对象模拟

public void CheckUser(){
        System.out.println("CheckUser实现了");
    }

xml配置

<bean id="userService" class="com.mrme.impl.UserServiceImpl"></bean>

    <bean id="userAspect" class="com.mrme.aspect.UserAspect"></bean>

    <!--aop配置-->
    <aop:config>
        <!--
        配置切点
        id:就是起个名字罢了,唯一就行
        expression:切点表达式
            execution:切点表示式  切点方法的形参形式 包名 + 类名 + 方法名(传入参数)
                *:任意形参(通常用于方法重写的多形式传入)    ..;表示任意方法参数
        -->
        <aop:pointcut id="p1" expression="execution(* com.mrme.impl.UserServiceImpl.deleteUser(..))"/>
        <!--
        配置代理对象 把通知应用到切点方法上
        aop:aspect:设置代理对象
        ref:设置代理对象的id
        -->
        <aop:aspect ref="userAspect">
            <!--
            配置通知的类型
            aop:before:表示 前置通知
            method:切点方法
            pointcut-ref:方法的来源切点
            -->
            <aop:before method="CheckUser" pointcut-ref="p1"></aop:before>
        </aop:aspect>
    </aop:config>

这里我们使用了前置通知当然还有很多的通知类型

其他通知的类型

共有

后缀通知:aop:after-returning 顾名思义就是切点方法执行完成后再执行配置方法

环绕通知:aop:around 切点方法执行完成前后都执行配置方法

抛出异常通知:aop:after-throwing 切点方法执行抛出异常后执行配置方法

最终通知:aop:after 切点方法执行无论是否抛出异常都执行配置方法

其中最特殊的用法是环绕通知,需要使用ProceedingJoinPoint joinPoint配合使用

public void around(ProceedingJoinPoint joinPoint){
        //以joinPoint.proceed();为分割线前面是前置异常,后面是后缀异常
        try {
            System.out.println("CheckUser环绕通知实现了1");
            joinPoint.proceed();
            System.out.println("CheckUser环绕通知实现了2");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

其他通知的方法同前置异常

详细见 mrme-spring-06-aspectj


使用注解(AspectJ 注解开发

前提:开始spring对aop的注释的支持

	<!--开启包扫描-->
    <context:component-scan base-package="com.mrme"></context:component-scan>
    <!--开始Spring对aop注解支持-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

在需要开启的代理类前面加上@Aspect,如下(当然@Component还是要加的)

@Component
@Aspect
public class UserAspect {
    //前置
    @Before(value = "execution(* com.mrme.impl.UserServiceImpl.deleteUser(..))")
    public void CheckUser(){
        System.out.println("CheckUser实现了");
    }
    //后置
    @AfterReturning(value = "execution(* com.mrme.impl.UserServiceImpl.findUser(..))")
    public void after(){
        System.out.println("CheckUser后置通知实现了");
    }
    //环绕
    @Around(value = "execution(* com.mrme.impl.UserServiceImpl.insertUser(..))")
    public void around(ProceedingJoinPoint joinPoint){
        //以joinPoint.proceed();为分割线前面是前置异常,后面是后缀异常
        try {
            System.out.println("CheckUser环绕通知实现了1");
            joinPoint.proceed();
            System.out.println("CheckUser环绕通知实现了2");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
    //抛出异常通知
    @AfterThrowing("execution(* com.mrme.impl.UserServiceImpl.deleteUser(..))")
    public void throwing(){
        System.out.println("CheckUser抛出异常时通知实现了");
    }
}

其他跟没用注释的是一样的


Spring中的事务与新特性

JdbcTemplate

它是spring框架中提供的一个对象。是对原生jdbc的简单封装,Spring框架为我们提供了很多操作的模板类

操作关系型数据库的:JdbcTemplate,HibernateTemplate

操作nosql数据库的:RedisTemplate

操作消息队列的:JmsTemplate

今天我们的主角在spring-jdbc-5.0.2.RELEASEsh除了要导入这个jar包之外,我们还需要导入spring-tx-5.0.2.RELEASE.jar(它是和事务相关的)。

创建这个类很简单:

JdbcTemplate jt = new JdbcTemplate();

实例代码:

public static void main(String[] args) {
        //使用spring自带的数据源
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost/lesson?serverTimezone=Asia/Shanghai");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        String sql = "insert into account(name,money) values('mrme4',114514)";
        jdbcTemplate.execute(dsql);
    }

但是,上述代码存在明显的隐患,set,new这些就是


JdbcTemplate的CRUD和DAO操作

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:ApplicationContext.xml"})
public class TestJdbcTempPlate {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    public void test01(){
        String sql = "select * from account";
        List<Account> accountList = jdbcTemplate.query(sql,new MyRoMapper());
        accountList.forEach(account -> {
            System.out.println(account);
        });
    }
}
public class MyRoMapper implements RowMapper<Account> {

    @Override
    public Account mapRow(ResultSet resultSet, int i) throws SQLException {
        String name = resultSet.getString("name");
        int money = resultSet.getInt("money");
        Account account = new Account();
        account.setMoney(money);
        account.setName(name);
        return account;
    }
}

其中的核心依赖

		<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.0.RELEASE</version>
        </dependency>

JdbcTemplate中主要提供了两个核心

query方法:专门执行查询操作的方法(select)

updata方法:专门执行的更新方法(insert,updata,delete)

详细请看源码


配置编程式事务

搭建案例环境

详细见 mrme-spring-07-account

配置平台事务管理器和事务管理模板

作用类似AOP

<!--配置平台事务管理器-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务管理模板-->
<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<!--注入平台事务管理器-->
<property name="transactionManager" ref="transactionManager"></property>
</bean>

具体实现:

@Component
public class AccountServiceImpl implements AccountService {
    @Autowired
    AccountDao accountDao;
    @Autowired
    TransactionTemplate transactionTemplate;
    /***
     * 首先检查双方是否存在,然后检查金额是否足够,最后再更新账户数据
     * @param sourceName 转账方
     * @param targetName 接收方
     * @param money 金额
     */
    @Override
    public void transform(final String sourceName,final String targetName,final int money) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                Account sourceAccount = accountDao.findAccountNAme(sourceName);
                Account targetAccount = accountDao.findAccountNAme(targetName);
                if (sourceAccount != null || targetAccount != null) {
                    if (sourceAccount.getMoney() >= money) {
                        //转钱
                        sourceAccount.setMoney(sourceAccount.getMoney() - money);
                        targetAccount.setMoney(targetAccount.getMoney() + money);
                        //更新
                        accountDao.updataAccountMoney(sourceAccount);
//                int a =10/0;
                        accountDao.updataAccountMoney(targetAccount);
                    }
                }
            }
        });
    }
}


配置声明式事务(XML方式,重点)

我们先去除xml中的bean配置和service中的TransactionTemplate transactionTemplate;

<bean id="accountDao" class="com.xq.dao.impl.AccountDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="accountService" class="com.xq.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://192.168.10.137:3306/ssm">
</property>
<property name="user" value="root"></property>
<property name="password" value="Admin123!"></property>
</bean>
<!--配置平台事务管理器-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
@Component
public class AccountServiceImpl implements AccountService {
    @Autowired
    AccountDao accountDao;
    /***
     * 首先检查双方是否存在,然后检查金额是否足够,最后再更新账户数据
     * @param sourceName 转账方
     * @param targetName 接收方
     * @param money 金额
     */
    @Override
    public void transform(final String sourceName,final String targetName,final int money) {
                Account sourceAccount = accountDao.findAccountNAme(sourceName);
                Account targetAccount = accountDao.findAccountNAme(targetName);
                if (sourceAccount != null || targetAccount != null) {
                    if (sourceAccount.getMoney() >= money) {
                        //转钱
                        sourceAccount.setMoney(sourceAccount.getMoney() - money);
                        targetAccount.setMoney(targetAccount.getMoney() + money);
                        //更新
                        accountDao.updataAccountMoney(sourceAccount);
//                int a =10/0;
                        accountDao.updataAccountMoney(targetAccount);
                    }
                }
            }
}

首先需要对配置事务增强(xml)

注意这里引入的tx是(xmlns:tx="http://www.springframework.org/schema/tx")而不是core内的

<!--配置事务增强-->
    <!--
    id:唯一即可
    transaction-manager:平台事务管理器的id
    -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!--配置事务的属性-->
        <tx:attributes>
            <!--指定给那个业务方法配置事务-->
            <!--
            name:业务名称的方法
            propagation:事务的传播行为
            -->
            <tx:method name="transform" propagation="REQUIRED"/>
        </tx:attributes>
        
        
        <!--aop-->
    </tx:advice>
    <aop:config>
        <aop:pointcut id="tran" expression="execution(* com.mrme.Service.impl.AccountServiceImpl.transform(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="tran"></aop:advisor>
    </aop:config>

这样救aop上了,结束

整体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"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--开启包扫描-->
    <context:component-scan base-package="com.mrme"></context:component-scan>

    <!--c3p0-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="password" value="root"></property>
        <property name="user" value="root"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost/lesson?serverTimezone=Asia/Shanghai"></property>
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
    </bean>

    <!--jdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置平台事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事务增强-->
    <!--
    id:唯一即可
    transaction-manager:平台事务管理器的id
    -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!--配置事务的属性-->
        <tx:attributes>
            <!--指定给那个业务方法配置事务-->
            <!--
            name:业务名称的方法
            propagation:事务的传播行为
            -->
            <tx:method name="transform" propagation="REQUIRED"/>
        </tx:attributes>
        <!--aop-->
    </tx:advice>
    <aop:config>
        <aop:pointcut id="tran" expression="execution(* com.mrme.Service.impl.AccountServiceImpl.transform(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="tran"></aop:advisor>
    </aop:config>
</beans>

配置声明式事务(注解的方式—重点)

基于上面的xml设置我们直接删除配置事务增强aop环节

毕竟我们需要使用注解的方式(@)

所以我们需要开启对注解的支持

    <!--配置开启注解对事务的支持-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

前往需要使用代理的业务加上@Transactional

我们进入源码

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

这两行表示的就是传播行为和隔离级别

@Transactional(isolation = Isolation.REPEATABLE_READ , propagation = Propagation.REQUIRED)


Spring5之整合日志框架

spring5框架自带了通用的日志封装。Spring5已经移除 Log4jConfigListener,官方建议使用 Log4j2。

导入依赖:

<!--日志依赖-->
		<dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.11.2</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>

创建log4j2.xml(请务必叫这个名字,否则爆破)

<?xml version="1.0" encoding="UTF-8" ?>
<!--日志级别以及优先级排序: OFF> FATAL> ERROR> WARN> INFO> DEBUG> TRACE> ALL-->
<!--Configuration后面的 status用于设置 log4j2自身内部的信息输出,可以不设置,
当设置成trace时,可以看到log4j2内部各种详细输出-->
<configuration status="DEBUG">
    <!--先定义所有的 appender-->
    <appenders>
        <!--输出日志信息到控制台-->
        <console name="Console" target="SYSTEM_OUT">
            <!--控制日志输出的格式-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t]%-5level %logger{36} - %msg%n"/>
        </console>
    </appenders>
    <!--然后定义 logger,只有定义 logger并引入的 appender,appender才会生效-->
    <!--root:用于指定项目的根日志,如果没有单独指定 Logger,则会使用 root作为默认的日
    志输出-->
    <loggers>
        <root level="info">
            <appender-ref ref="Console"/>
        </root>
    </loggers>
</configuration>

测试

package Test;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Testlog {
    Logger logger = LoggerFactory.getLogger(Testlog.class);
    @Test
    public void Test01(){
        logger.info("114514");
    }
}


spring5之@Nullable注解和函数式注册对象

@Nullable注解

@Nullable注解可以使用在方法上面,属性上面,参数上面,表示方法返回可以为空,属性值可以为空,参数值可以为空

Spring5 核心容器支持函数式风格GenericApplicationContext

@Test
public void testGenericApplicationContext() {
//1 创建 GenericApplicationContext 对象
GenericApplicationContext context = new GenericApplicationContext();
//2,需要刷新一下
context.refresh();
//3 调用 context 的方法对象注册
context.registerBean("account1", Account.class,() -> new Account(1002,"kobe",500.0));
//4 获取在 spring 注册的对象
// User user = (User)context.getBean("com.xq.pojo.Account");
Account user = (Account) context.getBean("account1");
System.out.println(user);
}

spring5整合Junit5单元测试

引入依赖

<!--Junit5-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.2</version>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-runner</artifactId>
            <version>1.6.2</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <version>1.6.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
        <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version> 5.0.9.RELEASE </version>
            <scope>provided</scope>
        </dependency>

使用

@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:applicationContext.xml")
//@SpringJUnitConfig(locations = "applicationContext.xml")// 组合注解@ExtendWith + @ContextConfiguration

public class MyTest2 {
@Autowired
AccountService accountService;
@Test//注意这个@Test是org.junit.jupiter.api.Test;而不是junit包中的
public void Tg() {
accountService.transfer("eric","james",500.0);
}
}

其实就是上面的注解不太一样,更新下而已


Spring Web Flux

Spring Web Flux是Spring5添加新的模块,用于web开发的,功能和SpringMVC类似的,Webflux使用当前一种比较流行响应式编程出现的框架。

使用传统web框架比如SpringMVC,这些基于Servlet容器,Webflux是一种异步非阻塞的框架,异步非阻塞的框架在Servlet3.1以后才支持,核心是基于Reactor的相关API实现的。

什么是异步非阻塞

异步和同步、非阻塞和阻塞 两者都是针对对象不一样

异步和同步针对调用者,调用者发送请求,如果等着对方回应之后才去做其他事情就是同步,如果发送请求之后不等着对方回应就去做其他事情就是异步

阻塞和非阻塞针对被调用者,被调用者受到请求之后,做完请求任务之后才给出反馈就是阻塞受到请求之后马上给出反馈然后再去做事情就是非阻塞

当然都是后者好(笑)

Webflux特点:

非阻塞式:在有限资源下,提高系统吞吐量和伸缩性,以Reactor为基础实现响应式编程。

函数式编程:Spring5框架基于java8,Webflux使用java7函数式编程方式实现路由请求。

响应式编程--java实现

什么是响应式编程:

响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流。而相关的计算模型会自动将变化的值通过数据流进行传播。(原生意思,就好像蜜蜂吃熊一样抽象( )

例如,在命令式编程环境中,a=b+c表示将表达式的结果赋给a,而之后改变b或c的值不会影响a。但在响应式编程中,a的值会随着b或c的更新而更新。

我们通过 Observer接口和Observable类来实现响应式编程

我们首先来认识Observer接口和Observable类。在 Java 程序中,类Observable 和接口 Observer 的最大功能就是实现观察者模式在Java 应用中,需要被观察的类必须继承于 Observable 类。每个观察者类都需要实现 Observer 接口,其定义如下:

public interface Observer{
// 第一个参数表示被观察者实例,第二参数表示修改的内容
void update(Observable o, Object arg)
}

例如现在房地产调控比较严格,很多购房者都在关注着房子的价格变化,每当房子价格变化时,所有的购房者都可以观察得到。实际上以上的购房者都属于观察者,他们都在关注着房子的价格。这个观察变化的过程就可以成为观察者模式。

观察者和被观察者类

//被观察者继承与Observable类
class house extends Observable {
    private float price;

    public house() {
    }

    public house(float price) {
        this.price = price;
    }

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        super.setChanged();//设置变化
        super.notifyObservers(price);
        this.price = price;
    }

    @Override
    public String toString() {
        return "house{" +
                "price=" + price +
                '}';
    }
}
//每个观察者类都需要实现Observer接口
class HousePriceObserver implements Observer {

    private String name;

    public HousePriceObserver(String name) {
        this.name = name;
    }

    @Override
    public void update(Observable o, Object arg) {
        //观察到更新执行
        if(arg instanceof Float){
            System.out.println(name + "观察到房价发生变化:" + arg);
        }
    }
}

模拟变化

public static void main(String[] args) {
        house house = new house(40000);
        HousePriceObserver h1 = new HousePriceObserver("A");
        HousePriceObserver h2 = new HousePriceObserver("B");
        HousePriceObserver h3 = new HousePriceObserver("C");
        //为被观察者添加观察者
        house.addObserver(h1);
        house.addObserver(h2);
        house.addObserver(h3);
        System.out.println(house.getPrice());
        //模拟变化后是否通知
        house.setPrice(60000);
        System.out.println(house.getPrice());
    }

image-20250429152513712

说明成功观察


基于Observable类来实现响应式编程:

//被观察者继承Observable
public class Observableimpl extends Observable {
    public static void main(String[] args) {
        //被观察者
        Observableimpl observableimpl = new Observableimpl();
        //添加观察者(模拟变化)
        observableimpl.addObserver((o,arg)->{
            System.out.println("数据流发生变化");//setchange
        });
        observableimpl.addObserver((o,arg)->{
            System.out.println("收到被观察者通知,准备改变");//notifyObservers
        });
        observableimpl.setChanged();
        observableimpl.notifyObservers();
    }
}

响应式编程--Reactor实现

响应式编程操作中,Reactor是满足Reactive规范框架。

Reactor有两个核心类,Mono和Flux,这两个类实现接口Publisher,提供丰富操作符。Flux对象实现发布者,返回N个元素;Mono实现发布者,返回0或者1个元素。

Flux和Mono都是数据流的发布者,使用Flux和Mono都可以发出三种数据信号:元素值、错误信号、完成信号,错误信号和完成信号都代表终止信号,终止信号用于告诉订阅者数据流结束了,错误信号终止数据流同时把错误信息传递给订阅者。

使用Reactor请先打xml依赖

<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.1.5.RELEASE</version>
</dependency>
public class Reactor {
    public static void main(String[] args) {
        Flux.just(1,2,3,4,5).subscribe(System.out::print);
        Mono.just(1).subscribe(System.out::print);
        //其他的方式 发送数据流
//数组
//Integer[] array = {1,2,3,4};
//Flux.fromArray(array);
        //列表
//List<Integer> list = Arrays.asList(array);
//Flux.fromIterable(list);
//Stream流
//Stream<Integer> stream = list.stream();
//Flux.fromStream(stream);
    }
}

三种信号的特点:

错误信号和完成信号都是终止信号,不能共存的。
如果没有发送任何元素值,而是直接发送错误或者完成信号,表示是空数据流。
如果没有错误信号,没有完成信号,表示是无限数据流。

调用just或者其他方法只是声明数据流,数据流并没有发出,只有进行订阅(subscribe)之后才会触发数据流,不订阅什么都不会发生的。


WebFlux执行流程和核心API

请学完springboot后回来学习2.44和2.45

posted @ 2025-04-30 17:09  MRME39  阅读(16)  评论(0)    收藏  举报