Mybatis

1、框架概述

1.1 什么是框架

框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;另一种定义认为,框架是可被应用开发者定制的应用骨架。前者是从应用方面而后者是从目的方面给出的定义。 简而言之,框架其实就是某种应用的半成品,就是一组组件,供你选用完成你自己的系统。简单说就是使用别人搭好的舞台,你来做表演。而且,框架一般是成熟的,不断升级的软件。

1.2 框架要解决的问题

框架要解决的最重要的一个问题是技术整合的问题,在J2EE的 框架中,有着各种各样的技术,不同的软件企业需要从J2EE中选择不同的技术,这就使得软件企业最终的应用依赖于这些技术,技术自身的复杂性和技术的风险性将会直接对应用造成冲击。而应用是软件企业的核心,是竞争力的关键所在,因此应该将应用自身的设计和具体的实现技术解耦。这样,软件企业的研发将集中在应用的设计上,而不是具体的技术实现,技术实现是应用的底层支撑,它不应该直接对应用产生影响。 框架一般处在低层应用平台(如J2EE)和高层业务逻辑之间的中间层。

1.3 软件开发分层的重要性

框架的重要性在于它实现了部分功能,并且能够很好的将低层应用平台和高层业务逻辑进行了缓和。为了实现软件工程中的“高内聚、低耦合”。把问题划分开来各个解决,易于控制,易于延展,易于分配资源。我们常见的MVC软件设计思想就是很好的分层思想。

image-202011041445441461.4 分层开发下的常见框架

MyBatis

image-20201104202212842

spring MVC

Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。Spring框架提供了构建Web应用程序的全功能MVC模块,使用Spring可插入的MVC架构,从而在使用Spring进行WEB开发时,可以选择使用Spring的SpringMVC框架或集合其他MVC开发框架,如Struts1(现在一般不用),Strutys2等

Spring框架

image-20201104202512301

MyBatis框架的概述

​ mybatis是一个优秀的基于java的持久层框架,它内部封装了jdbc,使开发者只需要关注sql语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。 mybatis通过xml或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。 采用ORM思想解决了实体和数据库映射的问题,对jdbc进行了封装,屏蔽了jdbc api底层访问细节,使我们不用与jdbc api打交道,就可以完成对数据库的持久化操作。 为了我们能够更好掌握框架运行的内部过程,并且有更好的体验,下面我们将从自定义Mybatis框架开始来学习框架。此时我们将会体验框架从无到有的过程体验,也能够很好的综合前面阶段所学的基础。

2、 JDBC编程的分析

2.1 jdbc程序的回顾

public class TestSelect {
	public static void main(String[] args) throws Exception{
		// 1、注册驱动
		Class.forName("com.mysql.jdbc.Driver");

		// 2、连接数据库
		Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");

		// 3、执行sql
		String sql = "SELECT * FROM t_department";
		Statement st = conn.createStatement();
		
		ResultSet rs = st.executeQuery(sql);//ResultSet看成InputStream
		while(rs.next()){//next()表示是否还有下一行
			Object did = rs.getObject(1);//获取第n列的值
			Object dname = rs.getObject(2);
			Object desc = rs.getObject(3);
			/*
			int did = rs.getInt("did");//也可以根据列名称,并且可以按照数据类型获取
			String dname = rs.getString("dname");
			String desc = rs.getString("description");
			 */
			
			System.out.println(did +"\t" + dname + "\t"+ desc);
		}

		// 4、关闭
		rs.close();
		st.close();
		conn.close();
	}
}

2.2 jdbc问题分析

1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。

2、Sql语句在代码中硬编码,造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。

3、使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。

4、对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便。

3、Mybatis框架的快速入门

​ 通过前面的学习,我们已经能够使用所学的基础知识构建自定义的Mybatis框架了。这个过程是基本功的考验,我们已经强大了不少,但现实是残酷的,我们所定义的Mybatis框架和真正的Mybatis框架相比,还是显得渺小。行业内所流行的Mybatis框架现在我们将开启学习。

3.1 Mybatis框架开发的准备

官方下载Mybatis框架

百度 Mybatis download

image-20201104203436221

image-20201104203511886

3.2 搭建Mybatis开发环境

3.2.1 创建maven工程

3.2.2 添加Mybatis3.4.5 的坐标

 <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>


    </dependencies>

3.2.3 编写User实体类

public class User implements Serializable {

    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

3.2.3 编写user数据库

3.2.4 编写UserDao接口

public interface UserDao {
    /**
     * 查询所有操作
     * @return
     */
    List<User> findAll();

}

3.2.5 编辑mysql的配置文件

image-20201104211306336

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <!--mybatis的主配置文件-->
<configuration>
    <!--配置环境-->
    <environments default="mysql">
        <!--配置Mysql的环境-->
        <environment id="mysql">
            <!--配置事务的类型-->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置数据源(连接池)-->
            <dataSource type="POOLED">
                <!--配置连接数据库的4个基本信息-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/eesy"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
                
                
            </dataSource>

            
        </environment>
    </environments>
    <mappers>
        <!--指定映射配置文件的位置,映射配置文件指的是每一个dao独立配置文件-->
        <mapper resource="com/atguigu/dao/UserDao.xml"/>
    </mappers>
    
    
</configuration>

3.2.6 创建UserDao.xml配置文件

image-20201104211229160

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.dao.UserDao">
    <!--配置查询所有-->
    <select id="findAll" resultType="com.atguigu.domain.User">
        select * from user
    </select>
</mapper>

也可以在java包的类下面直接写UserDao.xml

不过需要在pom.xml中配置

 <build>
        <resources>
            <resource>
                <directory>src/main/java</directory><!--所在的目录-->
                <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <!--filtering 选项 false 不启用过滤器, *.property 已经起到过滤的作用了 -->
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

image-20201105094948501

3.2.7 环境搭建注意事项

	第一个:创建IUserDao.xml 和 IUserDao.java时名称是为了和我们之前的知识保持一致。
		在Mybatis中它把持久层的操作接口名称和映射文件也叫做:Mapper
		所以:IUserDao 和 IUserMapper是一样的
	第二个:在idea中创建目录的时候,它和包是不一样的
		包在创建时:com.itheima.dao它是三级结构
		目录在创建时:com.itheima.dao是一级目录
	第三个:mybatis的映射配置文件位置必须和dao接口的包结构相同
	第四个:映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名
	第五个:映射配置文件的操作配置(select),id属性的取值必须是dao接口的方法名

3.2.8 编写测试类

package com.atguigu.test;



import com.atguigu.dao.UserDao;
import com.atguigu.domain.User;
import org.apache.ibatis.io.Resources;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

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


/**
 * 入门案例
 *
 */
public class mybatisTest {
    public static void main(String[] args) throws IOException {
        //1、读取配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2、创建SqlSessioinFaction工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);



        //3、使用工厂生产SqlSession对象
        SqlSession session = factory.openSession();


        //4、使用SqlSession创建Dao接口的代理对象
        UserDao mapper = session.getMapper(UserDao.class);
        //5、使用代理对象执行方法
        List<User> all = mapper.findAll();
        for (User user : all) {
            System.out.println(user);
        }
        //6、释放资源
        session.close();
        in.close();
    }
}

总结

当我们遵从了第三,四,五点之后,我们在开发中就无须再写dao的实现类。 mybatis的入门案例 第一步:读取配置文件 第二步:创建SqlSessionFactory工厂 第三步:创建SqlSession 第四步:创建Dao接口的代理对象 第五步:执行dao中的方法 第六步:释放资源

注意事项: 不要忘记在映射配置中告知mybatis要封装到哪个实体类中 配置的方式:指定实体类的全限定类名

3.3 用注解的方式写UserDao.xml配置文件

3.3.1 修改UserDao方法前面加上注解

public interface UserDao {
    /**
     * 查询所有操作
     * @return
     */
    @Select("select * from user")
    List<User> findAll();

}

3.3.2 然后修改SqlMapConfig.xml中的映射文件位置为class=""

<mappers>
    <!--指定映射配置文件的位置映射配置文件指的是每一个dao独立配置文件-->
    <!--如果使用注解来配置的话此处应该使用class属性指定被注解的dao全限定类名-->
    <mapper class="com.atguigu.dao.UserDao"/>
</mappers>

3.4 mybatis入门案例设计模式的分析

image-20201105104818949

4、自定义mybatis框架的分析

4.1 涉及知识点介绍

本章我们将使用前面所学的基础知识来构建一个属于自己的持久层框架,将会涉及到的一些知识点:工厂模式(Factory工厂模式)、构造者模式(Builder模式)、代理模式,反射,自定义注解,注解的反射,xml解析,数据库元数据,元数据的反射等。

4.2 分析流程

image-20201106093255172

4.3 前期准备

4.3.1 引入相关坐标

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.atguigu</groupId>
    <artifactId>day01_eesy_01mybatis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>


    </dependencies>
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory><!--所在的目录-->
                <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <!--filtering 选项 false 不启用过滤器, *.property 已经起到过滤的作用了 -->
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>


</project>

4.3.4 引入工具类到项目中

XML.configutation

package com.atguigu.mybatis.utils;


import com.atguigu.mybatis.cfg.Configuration;
import com.atguigu.mybatis.cfg.Mapper;
import com.atguigu.mybatis.io.Resources;


import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.IOException;
import java.io.InputStream;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 黑马程序员
 * @Company http://www.ithiema.com
 *  用于解析配置文件
 */
public class XMLConfigBuilder {



    /**
     * 解析主配置文件,把里面的内容填充到DefaultSqlSession所需要的地方
     * 使用的技术:
     *      dom4j+xpath
     */
    public static Configuration loadConfiguration(InputStream config){
        try{
            //定义封装连接信息的配置对象(mybatis的配置对象)
            Configuration cfg = new Configuration();

            //1.获取SAXReader对象
            SAXReader reader = new SAXReader();
            //2.根据字节输入流获取Document对象
            Document document = reader.read(config);
            //3.获取根节点
            Element root = document.getRootElement();
            //4.使用xpath中选择指定节点的方式,获取所有property节点
            List<Element> propertyElements = root.selectNodes("//property");
            //5.遍历节点
            for(Element propertyElement : propertyElements){
                //判断节点是连接数据库的哪部分信息
                //取出name属性的值
                String name = propertyElement.attributeValue("name");
                if("driver".equals(name)){
                    //表示驱动
                    //获取property标签value属性的值
                    String driver = propertyElement.attributeValue("value");
                    cfg.setDriver(driver);
                }
                if("url".equals(name)){
                    //表示连接字符串
                    //获取property标签value属性的值
                    String url = propertyElement.attributeValue("value");
                    cfg.setUrl(url);
                }
                if("username".equals(name)){
                    //表示用户名
                    //获取property标签value属性的值
                    String username = propertyElement.attributeValue("value");
                    cfg.setUsername(username);
                }
                if("password".equals(name)){
                    //表示密码
                    //获取property标签value属性的值
                    String password = propertyElement.attributeValue("value");
                    cfg.setPassword(password);
                }
            }
            //取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
            List<Element> mapperElements = root.selectNodes("//mappers/mapper");
            //遍历集合
            for(Element mapperElement : mapperElements){
                //判断mapperElement使用的是哪个属性
                Attribute attribute = mapperElement.attribute("resource");
                if(attribute != null){
                    System.out.println("使用的是XML");
                    //表示有resource属性,用的是XML
                    //取出属性的值
                    String mapperPath = attribute.getValue();//获取属性的值"com/atguigu/dao/IUserDao.xml"
                    //把映射配置文件的内容获取出来,封装成一个map
                    Map<String, Mapper> mappers = loadMapperConfiguration(mapperPath);
                    //给configuration中的mappers赋值
                    cfg.setMappers(mappers);
                }
            }
            //返回Configuration
            return cfg;
        }catch(Exception e){
            e.printStackTrace();

        }finally{
            try {
                config.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 根据传入的参数,解析XML,并且封装到Map中
     * @param mapperPath    映射配置文件的位置
     * @return  map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
     *          以及执行所需的必要信息(value是一个Mapper对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
     */
    private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException {
        InputStream in = null;
        try{
            //定义返回值对象
            Map<String,Mapper> mappers = new HashMap<String,Mapper>();
            //1.根据路径获取字节输入流
            in = Resources.getResourceAsStream(mapperPath);
            //2.根据字节输入流获取Document对象
            SAXReader reader = new SAXReader();
            Document document = reader.read(in);
            //3.获取根节点
            Element root = document.getRootElement();
            //4.获取根节点的namespace属性取值
            String namespace = root.attributeValue("namespace");//是组成map中key的部分
            //5.获取所有的select节点
            List<Element> selectElements = root.selectNodes("//select");
            //6.遍历select节点集合
            for(Element selectElement : selectElements){
                //取出id属性的值      组成map中key的部分
                String id = selectElement.attributeValue("id");
                //取出resultType属性的值  组成map中value的部分
                String resultType = selectElement.attributeValue("resultType");
                //取出文本内容            组成map中value的部分
                String queryString = selectElement.getText();
                //创建Key
                String key = namespace+"."+id;
                //创建Value

                //把要查询的语句,和要进行代理的类放到mappers集合里
                Mapper mapper = new Mapper();
                mapper.setQueryString(queryString);
                mapper.setResultType(resultType);
                //把key和value存入mappers中
                mappers.put(key,mapper);
            }
            return mappers;
        }catch(Exception e){
            throw new RuntimeException(e);
        }finally{
            in.close();
        }
    }

    /**
     * 根据传入的参数,得到dao中所有被select注解标注的方法。
     * 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息
     * @param daoClassPath
     * @return
     */
/*    private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{
        //定义返回值对象
        Map<String,Mapper> mappers = new HashMap<String, Mapper>();

        //1.得到dao接口的字节码对象
        Class daoClass = Class.forName(daoClassPath);
        //2.得到dao接口中的方法数组
        Method[] methods = daoClass.getMethods();
        //3.遍历Method数组
        for(Method method : methods){
            //取出每一个方法,判断是否有select注解
            boolean isAnnotated = method.isAnnotationPresent(Select.class);
            if(isAnnotated){
                //创建Mapper对象
                Mapper mapper = new Mapper();
                //取出注解的value属性值
                Select selectAnno = method.getAnnotation(Select.class);
                String queryString = selectAnno.value();
                mapper.setQueryString(queryString);
                //获取当前方法的返回值,还要求必须带有泛型信息
                Type type = method.getGenericReturnType();//List<User>
                //判断type是不是参数化的类型
                if(type instanceof ParameterizedType){
                    //强转
                    ParameterizedType ptype = (ParameterizedType)type;
                    //得到参数化类型中的实际类型参数
                    Type[] types = ptype.getActualTypeArguments();
                    //取出第一个
                    Class domainClass = (Class)types[0];
                    //获取domainClass的类名
                    String resultType = domainClass.getName();
                    //给Mapper赋值
                    mapper.setResultType(resultType);
                }
                //组装key的信息
                //获取方法的名称
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String key = className+"."+methodName;
                //给map赋值
                mappers.put(key,mapper);
            }
        }
        return mappers;
    }*/







}

Executor

package com.atguigu.mybatis.utils;



import com.atguigu.mybatis.cfg.Mapper;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;

/**
 * @author 黑马程序员
 * @Company http://www.ithiema.com
 * 负责执行SQL语句,并且封装结果集
 */
public class Executor {

    public <E> List<E> selectList(Mapper mapper, Connection conn) {
        PreparedStatement pstm = null;
        ResultSet rs = null;
        try {
            //1.取出mapper中的数据
            String queryString = mapper.getQueryString();//select * from user
            String resultType = mapper.getResultType();//com.itheima.domain.User
            Class domainClass = Class.forName(resultType);
            //2.获取PreparedStatement对象
            pstm = conn.prepareStatement(queryString);
            //3.执行SQL语句,获取结果集
            rs = pstm.executeQuery();
            //4.封装结果集
            List<E> list = new ArrayList<E>();//定义返回值
            while(rs.next()) {
                //实例化要封装的实体类对象
                E obj = (E)domainClass.newInstance();

                //取出结果集的元信息:ResultSetMetaData
                ResultSetMetaData rsmd = rs.getMetaData();
                //取出总列数
                int columnCount = rsmd.getColumnCount();
                //遍历总列数
                for (int i = 1; i <= columnCount; i++) {
                    //获取每列的名称,列名的序号是从1开始的
                    String columnName = rsmd.getColumnName(i);
                    //根据得到列名,获取每列的值
                    Object columnValue = rs.getObject(columnName);
                    //给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
                    PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);//要求:实体类的属性和数据库表的列名保持一种
                    //获取它的写入方法
                    Method writeMethod = pd.getWriteMethod();
                    //把获取的列的值,给对象赋值
                    writeMethod.invoke(obj,columnValue);
                }
                //把赋好值的对象加入到集合中
                list.add(obj);
            }
            return list;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            release(pstm,rs);
        }
    }


    private void release(PreparedStatement pstm,ResultSet rs){
        if(rs != null){
            try {
                rs.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }

        if(pstm != null){
            try {
                pstm.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}

DataSourceUtil

package com.atguigu.mybatis.utils;

import com.atguigu.mybatis.cfg.Configuration;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DataSourceUtil {
    /**
     * 用于获取一个连接
     * @param cfg
     * @return
     */
    public static Connection getConnection(Configuration cfg) {

        Connection connection = null;
        try {
            Class.forName(cfg.getDriver());
            connection = DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }


        return connection;
    }
}

4.3.3 编写SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!--配置环境-->
    <environments default="mysql">
        <!--配置Mysql的环境-->
        <environment id="mysql">
            <!--配置事务的类型-->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置数据源(连接池)-->
            <dataSource type="POOLED">
                <!--配置连接数据库的4个基本信息-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/eesy"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
                
                
            </dataSource>

            
        </environment>
    </environments>
    <mappers>
        <!--指定映射配置文件的位置,映射配置文件指的是每一个dao独立配置文件-->
        <mapper resource="com/atguigu/dao/UserDao.xml"/>
    </mappers>
    
    
</configuration>

4.3.4 编写读取配置文件类

package com.atguigu.mybatis.io;

import java.io.InputStream;

public class Resources {
    /**
     * 根据传入的参数获取一个字节输出流
     * @param path 资源文件的路径
     * @return 一个字节输出流
     */
    public static InputStream getResourceAsStream(String path) {
        return Resources.class.getClassLoader().getResourceAsStream(path);

    }
}

4.3.5 编写Mapper类

package com.atguigu.mybatis.cfg;

import java.util.Map;

public class Mapper {
    private String queryString;//查询语句
    private String resultType;//实体类的全限定的类名


    public Mapper(String queryString, String resultType) {
        this.queryString = queryString;
        this.resultType = resultType;
    }

    public Mapper() {
    }

    public String getQueryString() {
        return queryString;
    }

    public void setQueryString(String queryString) {
        this.queryString = queryString;
    }

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }

    @Override
    public String toString() {
        return "Mapper{" +
                "queryString='" + queryString + '\'' +
                ", resultType='" + resultType + '\'' +
                '}';
    }

}

4.3.6 编写Configuration配置类

package com.atguigu.mybatis.cfg;

import java.util.HashMap;
import java.util.Map;

public class Configuration {
    private String driver;
    private String url;
    private String username;
    private String password;
    private Map<String,Mapper> mappers = new HashMap<String,Mapper>();
    public Configuration(String driver, String url, String username, String password, Map<String, Mapper> mappers) {
        this.driver = driver;
        this.url = url;
        this.username = username;
        this.password = password;
        this.mappers = mappers;
    }

    public Configuration() {
    }

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Map<String, Mapper> getMappers() {
        return mappers;
    }

    public void setMappers(Map<String, Mapper> mappers) {
        this.mappers.putAll(mappers);
    }
}

4.3.7 编写User实现类

public class User implements Serializable {

    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

4.4 基于XML的自定义mybatis框架

4.4.1 编写持久层接口和IUserDao.xml

<?xml version="1.0" encoding="UTF-8"?>

<mapper namespace="com.atguigu.dao.UserDao">
    <!--配置查询所有-->
    <select id="findAll" resultType="com.atguigu.domain.User">
        select * from user
    </select>
</mapper>

4.4.2 编写构建者类

package com.atguigu.mybatis.sqlsession;

import com.atguigu.mybatis.defaults.DefaultSqlSessionFactory;
import com.atguigu.mybatis.utils.XMLConfigBuilder;

import java.io.InputStream;

public class SqlSessionFactoryBuilder {

    /**
     * 返回一个工厂对象
     * @param inputStream
     * @return
     */
    public SqlSessionFactory build(InputStream inputStream) {

        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory();
        defaultSqlSessionFactory.setConfiguration(XMLConfigBuilder.loadConfiguration(inputStream));
        return defaultSqlSessionFactory;


    }
}

4.4.3 编写SqlSessionFactory 接口和实现类

package com.atguigu.mybatis.sqlsession;

public interface SqlSessionFactory {
    /**
     * 用于打开一个新的SqlSession对象
     * @return
     */
    SqlSession openSession();
}
package com.atguigu.mybatis.defaults;

import com.atguigu.mybatis.cfg.Configuration;
import com.atguigu.mybatis.sqlsession.SqlSession;
import com.atguigu.mybatis.sqlsession.SqlSessionFactory;

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private Configuration configuration;

    public Configuration getConfiguration() {
        return configuration;
    }

    public void setConfiguration(Configuration configuration) {
        this.configuration = configuration;
    }

    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);


    }
}

4.4.4 编写SqlSession接口和实现类

package com.atguigu.mybatis.sqlsession;

public interface SqlSession {
    /**
     * 根据参数创建一个代理对象
     * @param daoInterfaceClass dao的接口字节码
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<T> daoInterfaceClass);

    /**
     * 释放资源
     */
    void close();
}
package com.atguigu.mybatis.defaults;

import com.atguigu.mybatis.cfg.Configuration;
import com.atguigu.mybatis.proxy.MapperProxy;
import com.atguigu.mybatis.sqlsession.SqlSession;
import com.atguigu.mybatis.utils.DataSourceUtil;

import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;

public class DefaultSqlSession implements SqlSession {
    private Configuration cfg;
    private Connection connection;

    public DefaultSqlSession(Configuration cfg) {
        this.cfg = cfg;
        connection = DataSourceUtil.getConnection(cfg);
    }

    public <T> T getMapper(Class<T> daoInterfaceClass) {
        //用于创建一个新的代理对象进行代理,其中new Class[]{daoInterfaceClass}代表新建一个Class数组,里面有daoInterfaceClass这个元素,因为
        //daoIntefaceClass本身就是一个接口,因此不用.inteface,(UserDao本身就是一个接口)
        return (T)Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(), new Class[]{daoInterfaceClass},new MapperProxy(cfg.getMappers(),connection));

    }

    public void close() {
        if (connection != null) {

            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

4.4.5 编写用于创建Dao接口代理对象的类

package com.atguigu.mybatis.proxy;

import com.atguigu.mybatis.cfg.Configuration;
import com.atguigu.mybatis.cfg.Mapper;
import com.atguigu.mybatis.utils.Executor;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.IllegalFormatException;
import java.util.Map;

public class MapperProxy implements InvocationHandler {
    private Map<String,Mapper> map;
    private Connection conn;

    public MapperProxy(Map<String, Mapper> map,Connection conn) {
        this.map = map;
        this.conn = conn;
    }

    /**
     * 用来对方法进行增强的,我们的增强其实就是调用selectList方法
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String name = method.getName();
        String name1 = method.getDeclaringClass().getName();
        Mapper mapper = map.get(name1 + "." + name);
        if (mapper == null) {
            throw new IllegalArgumentException("传入的参数有错误");
        }
        return new Executor().selectList(mapper, conn);
    }
}

3.3.6 运行测试类

package com.atguigu.test;



import com.atguigu.dao.UserDao;
import com.atguigu.domain.User;
import com.atguigu.mybatis.io.Resources;
import com.atguigu.mybatis.sqlsession.SqlSession;
import com.atguigu.mybatis.sqlsession.SqlSessionFactory;
import com.atguigu.mybatis.sqlsession.SqlSessionFactoryBuilder;


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


/**
 * 入门案例
 *
 */
public class mybatisTest {
    public static void main(String[] args) throws IOException {
        //1、读取配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2、创建SqlSessioinFaction工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);


        //3、使用工厂生产SqlSession对象
        SqlSession session = factory.openSession();


        //4、使用SqlSession创建Dao接口的代理对象
        UserDao mapper = session.getMapper(UserDao.class);
        //5、使用代理对象执行方法
        List<User> all = mapper.findAll();
        for (User user : all) {
            System.out.println(user);
        }
        //6、释放资源
        session.close();
        in.close();
    }
}

4.5 基于注解的自定义mybatis框架

4.5.1 SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!--配置环境-->
    <environments default="mysql">
        <!--配置Mysql的环境-->
        <environment id="mysql">
            <!--配置事务的类型-->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置数据源(连接池)-->
            <dataSource type="POOLED">
                <!--配置连接数据库的4个基本信息-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/eesy"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
                
                
            </dataSource>

            
        </environment>
    </environments>
    <mappers>
        <!--指定映射配置文件的位置,映射配置文件指的是每一个dao独立配置文件-->
        <mapper class="com.atguigu.dao.UserDao"/>
    </mappers>
    
    
</configuration>

4.5.2 XMlConfigBuilder增加泛型方式

package com.atguigu.mybatis.utils;


import com.atguigu.mybatis.annotation.Select;
import com.atguigu.mybatis.cfg.Configuration;
import com.atguigu.mybatis.cfg.Mapper;
import com.atguigu.mybatis.io.Resources;


import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.IOException;
import java.io.InputStream;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 黑马程序员
 * @Company http://www.ithiema.com
 *  用于解析配置文件
 */
public class XMLConfigBuilder {



    /**
     * 解析主配置文件,把里面的内容填充到DefaultSqlSession所需要的地方
     * 使用的技术:
     *      dom4j+xpath
     */
    public static Configuration loadConfiguration(InputStream config){
        try{
            //定义封装连接信息的配置对象(mybatis的配置对象)
            Configuration cfg = new Configuration();

            //1.获取SAXReader对象
            SAXReader reader = new SAXReader();
            //2.根据字节输入流获取Document对象
            Document document = reader.read(config);
            //3.获取根节点
            Element root = document.getRootElement();
            //4.使用xpath中选择指定节点的方式,获取所有property节点
            List<Element> propertyElements = root.selectNodes("//property");
            //5.遍历节点
            for(Element propertyElement : propertyElements){
                //判断节点是连接数据库的哪部分信息
                //取出name属性的值
                String name = propertyElement.attributeValue("name");
                if("driver".equals(name)){
                    //表示驱动
                    //获取property标签value属性的值
                    String driver = propertyElement.attributeValue("value");
                    cfg.setDriver(driver);
                }
                if("url".equals(name)){
                    //表示连接字符串
                    //获取property标签value属性的值
                    String url = propertyElement.attributeValue("value");
                    cfg.setUrl(url);
                }
                if("username".equals(name)){
                    //表示用户名
                    //获取property标签value属性的值
                    String username = propertyElement.attributeValue("value");
                    cfg.setUsername(username);
                }
                if("password".equals(name)){
                    //表示密码
                    //获取property标签value属性的值
                    String password = propertyElement.attributeValue("value");
                    cfg.setPassword(password);
                }
            }
            //取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
            List<Element> mapperElements = root.selectNodes("//mappers/mapper");
            //遍历集合
            for(Element mapperElement : mapperElements){
                //判断mapperElement使用的是哪个属性
                Attribute attribute = mapperElement.attribute("resource");
                if(attribute != null){
                    System.out.println("使用的是XML");
                    //表示有resource属性,用的是XML
                    //取出属性的值
                    String mapperPath = attribute.getValue();//获取属性的值"com/atguigu/dao/IUserDao.xml"
                    //把映射配置文件的内容获取出来,封装成一个map
                    Map<String, Mapper> mappers = loadMapperConfiguration(mapperPath);
                    //给configuration中的mappers赋值
                    cfg.setMappers(mappers);
                }else{
                    System.out.println("使用的是注解");
                    //表示没有resource属性,用的是注解
                    //获取class属性的值
                    String daoClassPath = mapperElement.attributeValue("class");
                    //根据daoClassPath获取封装的必要信息
                    Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath);
                    //给configuration中的mappers赋值
                    cfg.setMappers(mappers);
                }
            }
            //返回Configuration
            return cfg;
        }catch(Exception e){
            e.printStackTrace();

        }finally{
            try {
                config.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 根据传入的参数,解析XML,并且封装到Map中
     * @param mapperPath    映射配置文件的位置
     * @return  map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
     *          以及执行所需的必要信息(value是一个Mapper对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
     */
    private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException {
        InputStream in = null;
        try{
            //定义返回值对象
            Map<String,Mapper> mappers = new HashMap<String,Mapper>();
            //1.根据路径获取字节输入流
            in = Resources.getResourceAsStream(mapperPath);
            //2.根据字节输入流获取Document对象
            SAXReader reader = new SAXReader();
            Document document = reader.read(in);
            //3.获取根节点
            Element root = document.getRootElement();
            //4.获取根节点的namespace属性取值
            String namespace = root.attributeValue("namespace");//是组成map中key的部分
            //5.获取所有的select节点
            List<Element> selectElements = root.selectNodes("//select");
            //6.遍历select节点集合
            for(Element selectElement : selectElements){
                //取出id属性的值      组成map中key的部分
                String id = selectElement.attributeValue("id");
                //取出resultType属性的值  组成map中value的部分
                String resultType = selectElement.attributeValue("resultType");
                //取出文本内容            组成map中value的部分
                String queryString = selectElement.getText();
                //创建Key
                String key = namespace+"."+id;
                //创建Value

                //把要查询的语句,和要进行代理的类放到mappers集合里
                Mapper mapper = new Mapper();
                mapper.setQueryString(queryString);
                mapper.setResultType(resultType);
                //把key和value存入mappers中
                mappers.put(key,mapper);
            }
            return mappers;
        }catch(Exception e){
            throw new RuntimeException(e);
        }finally{
            in.close();
        }
    }

    /**
     * 根据传入的参数,得到dao中所有被select注解标注的方法。
     * 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息
     * @param daoClassPath
     * @return
     */
    private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{
        //定义返回值对象
        Map<String,Mapper> mappers = new HashMap<String, Mapper>();

        //1.得到dao接口的字节码对象
        Class daoClass = Class.forName(daoClassPath);
        //2.得到dao接口中的方法数组
        Method[] methods = daoClass.getMethods();
        //3.遍历Method数组
        for(Method method : methods){
            //取出每一个方法,判断是否有select注解
            boolean isAnnotated = method.isAnnotationPresent(Select.class);
            if(isAnnotated){
                //创建Mapper对象
                Mapper mapper = new Mapper();
                //取出注解的value属性值
                Select selectAnno = method.getAnnotation(Select.class);
                String queryString = selectAnno.value();
                mapper.setQueryString(queryString);
                //获取当前方法的返回值,还要求必须带有泛型信息
                Type type = method.getGenericReturnType();//List<User>
                //判断type是不是参数化的类型
                if(type instanceof ParameterizedType){
                    //强转
                    ParameterizedType ptype = (ParameterizedType)type;
                    //得到参数化类型中的实际类型参数
                    Type[] types = ptype.getActualTypeArguments();
                    //取出第一个
                    Class domainClass = (Class)types[0];
                    //获取domainClass的类名
                    String resultType = domainClass.getName();
                    //给Mapper赋值
                    mapper.setResultType(resultType);
                }
                //组装key的信息
                //获取方法的名称
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String key = className+"."+methodName;
                //给map赋值
                mappers.put(key,mapper);
            }
        }
        return mappers;
    }







}

4.5.3 UserDao 增加注解

public interface UserDao {
    /**
     * 查询所有操作
     * @return
     */
    @Select("select * from user")
    List<User> findAll();

}

4.5.4 UserDao新建一个注解

package com.atguigu.mybatis.annotation;

import sun.awt.SunHints;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
    String value();
}

5、用配置文件实现mybatis的增删改数据操作

5.1 首先搭建环境

构建工程,增加依赖,写UserDao类,写User类,配置SqlMapConfig.xml配置类,写UserDao.xml

注意在SqlMapConfig.xml的的信息标签是<mapper source=“com/atguigu/dao/UserDao.xml”

5.2 在UserDao里面添加saveUser,deleteUser,updateUser findTotal 模糊查询 方法

package com.atguigu.dao;


import com.atguigu.domain.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.lang.annotation.Target;
import java.util.List;

/**
 * 用户的持久型接口
 */


public interface UserDao {
    /**
     * 查询所有操作
     * @return
     */

    List<User> findAll();
    void saveUser(User user);
    void updateUser(User user);
    void deleteUser(Integer id);
    List<User> getUserByName(String username);
    User getUserById(Integer id);
    Integer findTotal();

}

5.3 在UserDao.xml写配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--代表你要映射的Dao接口,后面通过namespace.方法名封装成map集合的key属性-->
<mapper namespace="com.atguigu.dao.UserDao">
    <!--通过返回值类型得到需要封装的类-->
    <select id="findAll" resultType="com.atguigu.domain.User">
        select * from user;
    </select>
    <insert id="saveUser" parameterType="com.atguigu.domain.User">
        insert into user(username,address,sex,birthday) value (#{username},#{address},#{sex},#{birthday})

    </insert>
    <update id="updateUser" >
        update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id = #{id}

    </update>
    <delete id="deleteUser" >
        delete from user where id = #{id};
    </delete>
    <select id="getUserById" resultType="com.atguigu.domain.User" parameterType="Integer">
        select * from user where id = #{id};


    </select>
    <select id="getUserByName" resultType="com.atguigu.domain.User" parameterType="String">
        select * from user where username like #{username};

    </select>
    <select id="findTotal" resultType="Integer">
    	select count(*) from user;
    </select>
</mapper>

注意标签有一个 parameterType属性,里面放着参数的类型,可写可不写,idea会自动检测

5.4 ognl表达式

细节: parameterType属性: 代表参数的类型,因为我们要传入的是一个类的对象,所以类型就写类的全名称。 sql语句中使用#{}字符: 它代表占位符,相当于原来jdbc部分所学的?,都是用于执行语句时替换实际的数据。 具体的数据是由#{}里面的内容决定的。 #{}中内容的写法: 由于我们保存方法的参数是 一个User对象,此处要写User对象中的属性名称。 它用的是ognl表达式。

ognl表达式: 它是apache提供的一种表达式语言,全称是: Object Graphic Navigation Language 对象图导航语言 它是按照一定的语法格式来获取数据的。 语法格式就是使用 #{对象.对象}的方式

{user.username}它会先去 找user对象,然后在user对象中找到username属性,并调用getUsername()方法把值取出来。但是我们在parameterType属性上指定了实体类名称,所以可以省略user.而直接写username。

访问对象属性: person.name

调用方法: person.getName()

调用静态属性/方法: @java.lang.Math@PI

​ @java.util.UUID@randomUUID()

调用构造方法: new com.atguigu.bean.Person(‘admin’).name

运算符: +,-*,/,%

逻辑运算符: in,not in,>,>=,<,<=,==,!=

注意:xml中特殊符号如”,>,<等这些都需要使用转义字符

5.4 写增加操作的测试类

package com.atguigu.test;

import com.atguigu.dao.UserDao;
import com.atguigu.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.InputStream;
import java.util.Date;
import java.util.List;

import static org.junit.Assert.*;

public class UserDaoTest {
    InputStream in;
    SqlSession session;
    UserDao mapper;
    @Before
    public void init() throws Exception{
        //1、读取配置文件
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2、创建SqlSessioinFaction工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);



        //3、使用工厂生产SqlSession对象
        session = factory.openSession();


        //4、使用SqlSession创建Dao接口的代理对象
        mapper = session.getMapper(UserDao.class);
    }


    @After
    public void close() throws Exception{
        //把commit移动到这里可以保证每次都提交事务
        session.commit();
        //6、释放资源
        session.close();
        in.close();


    }
    @Test
    public void findAll() {
        //5、使用代理对象执行方法
        List<User> all = mapper.findAll();
        for (User user : all) {
            System.out.println(user);
        }
    }

    @Test
    public void saveUser() {
        User user = new User();
        user.setAddress("罗门西村31幢402");
        user.setBirthday(new Date());
        user.setSex("男");
        user.setUsername("江豪迪");
        mapper.saveUser(user);
    }

    @Test
    public void updateUser() {
        User user = new User();
        user.setId(50);

        user.setAddress("罗门西村31幢402");
        user.setBirthday(new Date());
        user.setSex("男");
        user.setUsername("江gege");
        mapper.updateUser(user);
    }

    @Test
    public void deleteUser() {
        mapper.deleteUser(51);
    }

    @Test
    public void getUserByName() {
        User userById = mapper.getUserById(54);
        System.out.println(userById);
    }

    @Test
    public void getUserById() {
        List<User> userByName = mapper.getUserByName("江%");
        for (User user : userByName) {
            System.out.println(user);
        }
    }
    @Test
    public void findTotal(){
	  Integer count = mapper.findTotal();
        System.out.println(count);
        
    }
}

可以把事务的提交放到close中去,以便每次都提交事务

@After
public void close() throws Exception{
    //把commit移动到这里可以保证每次都提交事务
    session.commit();
    //6、释放资源
    session.close();
    in.close();


}

5.5 模糊查询的另一种配置方式

第一步:修改SQL语句的配置,配置如下: <!-- 根据名称模糊查询 --> <select id="findByName" parameterType="string" resultType="com.itheima.domain.User"> select * from user where username like '%${value}%' </select> 我们在上面将原来的#{}占位符,改成了${value}。注意如果用模糊查询的这种写法,那么${value}的写法就是固定的,不能写成其它名字。

5.6 #{}与${}的区别

${}:insert into emp values(null,admin,23,男)

statement:必须使用字符串拼接的方式操作SQL,一定要注意单引号问题

#{}:insert into emp values(null,?,?,?)

preparedStatement:可以使用通配符操作SQL,因为在为String赋值时可以自动加单引号, 因此不需要注意单引号的问题

使用建议:建议使用#{},在特殊情况下,需要使用${},例如模糊查询和批量删除

5.6.1 源码分析

image-20201107100051609

5.7 有多个参数的查询的处理

1.当传输参数为单个String或基本数据类型和其包装类

#{}:可以以任意的名字获取参数值
${}:只能以${value}或{_parameter}获取

2.当传输参数为Javabean时

都可以通过属性名直接获属性值,但是要注意${}的单引号

3.当传输多个参数时

mybatis会把参数封装成一个map集合,因此可以用参数调用

#{}:#{arg0},#{arg1};#{param1},${param2}
${}:${param1},${param2},但是要注意单引号的问题
   @Update("update user set username = #{username} where id = #{id}")
    void updateNameById(@Param("username") String username, @Param("id") Integer id);
}//注解开发
 <update id="updateNameById">
        update user set username = #{param1} where id = #{param2}
    </update>
<!--xml开发-->

4、当传输为Map参数时

都可以通过键的名字直接获值,但是要注意${}的单引号问题

5、可以通过 @Update(“update user set username = #{username} where id = #{id}”) void updateNameById(@Param(“username”) String username, @Param(“id”) Integer id);

6、当传输为List和Array,mybatis会将List或Array放在Map中,List以list为键,Array以array为键

6、测试类的before,after

其中Before代表普通测试文件开始之前执行,after表示普通测试文件执行完毕需要的操作,注意需要把测试类和before和after有联系的类写在外面,以便执行

public class mybatisTest {
    InputStream in;
    SqlSession session;
    UserDao mapper;
    @Before
    public void init() throws Exception{
        //1、读取配置文件
         in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2、创建SqlSessioinFaction工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);



        //3、使用工厂生产SqlSession对象
         session = factory.openSession();


        //4、使用SqlSession创建Dao接口的代理对象
         mapper = session.getMapper(UserDao.class);
    }



    @Test
    public void test01() throws Exception{
     ;
        //5、使用代理对象执行方法
        List<User> all = mapper.findAll();
        for (User user : all) {
            System.out.println(user);
        }

    }
    @After
    public void close() throws Exception{
        //6、释放资源
        session.close();
        in.close();

    }
    @Test
    public void test02() throws Exception {
        User user = new User();
        user.setAddress("罗门西村31幢402");
        user.setBirthday(new Date());
        user.setSex("男");
        user.setUsername("江豪迪");
        mapper.saveUser(user);
        session.commit();

    }
}

7、怎么得到保存数据以后的id值

 <insert id="saveUser" parameterType="com.atguigu.domain.User">

        <!-- 获取插入操作后,获取插入操作的id-->
        <!--keyProperty是实现类的id,keyColumn是数据库的id,order代表什么时候执行-->
        <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        </selectKey>
        insert into user(username,address,sex,birthday) value (#{username},#{address},#{sex},#{birthday})

    </insert>

测试类

  @Test
    public void saveUser() {
        User user = new User();
        user.setAddress("罗门西村31幢402");
        user.setBirthday(new Date());
        user.setSex("男");
        user.setUsername("江豪迪");
        System.out.println(user);
        mapper.saveUser(user);
        System.out.println(user);
    }

结果

image-20201107092825822

8、使用实体类包装对象作为查询条件

8.1 传递pojo对象

开发中通过pojo传递查询条件 ,查询条件是综合的查询条件,不仅包括用户查询条件还包括其它的查询条件(比如将用户购买商品信息也作为查询条件),这时可以使用包装对象传递输入参数。 Pojo类中包含pojo。 需求:根据用户名查询用户信息,查询条件放到QueryVo的user属性中。

8.2 编写QueryVo

package com.atguigu.domain;

public class QueryVo {
    User user;

    public QueryVo(User user) {
        this.user = user;
    }

    public QueryVo() {
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}

8.3 userDao.xml的编写

<!-- 通过QueryVo对象来进行模糊查询,其中#{user.username} 中的user代表QueryVo的属性,username代表QueryVo的属性的USer的属性username-->
    <select id="getUserByNameByVo" resultType="com.atguigu.domain.User" parameterType="com.atguigu.domain.QueryVo">

        select * from user where username like #{user.username};


    </select>

8.4 测试类

@Test
public void getUserByNameByVo() {
    QueryVo queryVo = new QueryVo();
    User user = new User();
    user.setUsername("%江%");
    queryVo.setUser(user);
    List<User> userByNameByVo = mapper.getUserByNameByVo(queryVo);
    for (User user1 : userByNameByVo) {
        System.out.println(user1);
    }
}

9.Mybatis的输出结果封装

9.1 resultType配置结果类型

resultType属性可以指定结果集的类型,它支持基本类型和实体类类型。 我们在前面的CRUD案例中已经对此属性进行过应用了。 需要注意的是,它和parameterType一样,如果注册过类型别名的,可以直接使用别名。没有注册过的必须使用全限定类名。例如:我们的实体类此时必须是全限定类名(今天最后一个章节会讲解如何配置实体类的别名) 同时,当是实体类名称是,还有一个要求,实体类中的属性名称必须和查询语句中的列名保持一致,否则无法实现封装。

9.2 使用改别名的方式实现封装

User类

public class User implements Serializable {
    private Integer userId;
    private String userName;
    private Date userBirthday;
    private String userSex;
    private String userAddress;
    

image-20201107105243545

发现只有userName属性还在,因为内部调用的是set的方式进行赋值,因此只有userName赋值出来了,因为sql不区分大小写

修改userDao.xml配置文件,取别名

<!-- 配置查询所有操作 --> <select id="findAll" resultType="com.itheima.domain.User"> select id as userId,username as userName,birthday as userBirthday, sex as userSex,address as userAddress from user </select>

image-20201107105519247

正常查询

9.3 使用resultMap进行输出结果封装

resultMap标签可以建立查询的列名和实体类的属性名称不一致时建立对应关系。从而实现封装。 在select标签中使用resultMap属性指定引用即可。同时resultMap可以实现将查询结果映射为复杂类型的pojo,比如在查询结果映射对象中包括pojo和list实现一对一查询和一对多查询。

在配置文件中加入resultMap标签

<!-- 建立User实体和数据库表的对应关系
传智播客——专注于 Java、.Net 和 Php、网页平面设计工程师的培训
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090
type属性:指定实体类的全限定类名 id属性:给定一个唯一标识,是给查询select标签引用用的。 --> 
<resultMap type="com.itheima.domain.User" id="userMap">
   <id column="id" property="userId"/> 
    <result column="username" property="userName"/> 
    <result column="sex" property="userSex"/> 
    <result column="address" property="userAddress"/> 
    <result column="birthday" property="userBirthday"/> </resultMap> 
id标签:用于指定主键字段 
result标签:用于指定非主键字段 
column属性:用于指定数据库列名 
property属性:用于指定实体类属性名称

将映射配置改为resultMap=“userMap”

<select id = "findAll" resultMap = "userMap">
	select * from user
</select>

测试结果

image-20201107110000672

10、Mybatis实现dao的传统开发方式

10.1 持久性Dao接口实现类

package com.itheima.dao.impl;

import com.itheima.dao.IUserDao;
import com.itheima.domain.User;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

import java.util.List;

/**
 * @author 黑马程序员
 * @Company http://www.ithiema.com
 */
public class UserDaoImpl implements IUserDao {

    private SqlSessionFactory factory;

    public UserDaoImpl(SqlSessionFactory factory){
        this.factory = factory;
    }

    @Override
    public List<User> findAll() {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用SqlSession中的方法,实现查询列表
        List<User> users = session.selectList("com.itheima.dao.IUserDao.findAll");//参数就是能获取配置信息的key
        //3.释放资源
        session.close();
        return users;
    }

    @Override
    public void saveUser(User user) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用方法实现保存
        session.insert("com.itheima.dao.IUserDao.saveUser",user);
        //3.提交事务
        session.commit();
        //4.释放资源
        session.close();
    }

    @Override
    public void updateUser(User user) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用方法实现更新
        session.update("com.itheima.dao.IUserDao.updateUser",user);
        //3.提交事务
        session.commit();
        //4.释放资源
        session.close();
    }

    @Override
    public void deleteUser(Integer userId) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用方法实现更新
        session.update("com.itheima.dao.IUserDao.deleteUser",userId);
        //3.提交事务
        session.commit();
        //4.释放资源
        session.close();
    }

    @Override
    public User findById(Integer userId) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用SqlSession中的方法,实现查询一个
        User user = session.selectOne("com.itheima.dao.IUserDao.findById",userId);
        //3.释放资源
        session.close();
        return user;
    }

    @Override
    public List<User> findByName(String username) {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用SqlSession中的方法,实现查询列表
        List<User> users = session.selectList("com.itheima.dao.IUserDao.findByName",username);
        //3.释放资源
        session.close();
        return users;
    }

    @Override
    public int findTotal() {
        //1.根据factory获取SqlSession对象
        SqlSession session = factory.openSession();
        //2.调用SqlSession中的方法,实现查询一个
        Integer count = session.selectOne("com.itheima.dao.IUserDao.findTotal");
        //3.释放资源
        session.close();
        return count;
    }
}

10.2 映射配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.dao.IUserDao">

    <!-- 查询所有 -->
    <select id="findAll" resultType="com.itheima.domain.User">
        select * from user;
    </select>

    <!-- 保存用户 -->
    <insert id="saveUser" parameterType="com.itheima.domain.User">
        <!-- 配置插入操作后,获取插入数据的id -->
        <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        </selectKey>
        insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});
    </insert>

    <!-- 更新用户 -->
    <update id="updateUser" parameterType="com.itheima.domain.User">
        update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id}
    </update>

    <!-- 删除用户-->
    <delete id="deleteUser" parameterType="java.lang.Integer">
        delete from user where id = #{uid}
    </delete>
    
    <!-- 根据id查询用户 -->
    <select id="findById" parameterType="INT" resultType="com.itheima.domain.User">
        select * from user where id = #{uid}
    </select>

    <!-- 根据名称模糊查询 -->
    <select id="findByName" parameterType="string" resultType="com.itheima.domain.User">
          select * from user where username like #{name}
   </select>

    <!-- 获取用户的总记录条数 -->
    <select id="findTotal" resultType="int">
        select count(id) from user;
    </select>
</mapper>

10.3 测试类

package com.itheima.test;

import com.itheima.dao.IUserDao;
import com.itheima.dao.impl.UserDaoImpl;
import com.itheima.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.InputStream;
import java.util.Date;
import java.util.List;

/**
 * @author 黑马程序员
 * @Company http://www.ithiema.com
 *
 * 测试mybatis的crud操作
 */
public class MybatisTest {

    private InputStream in;
    private IUserDao userDao;

    @Before//用于在测试方法执行之前执行
    public void init()throws Exception{
        //1.读取配置文件,生成字节输入流
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2.获取SqlSessionFactory
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        //3.使用工厂对象,创建dao对象
        userDao = new UserDaoImpl(factory);
    }

    @After//用于在测试方法执行之后执行
    public void destroy()throws Exception{
        //6.释放资源
        in.close();
    }

    /**
     * 测试查询所有
     */
    @Test
    public void testFindAll(){
        //5.执行查询所有方法
        List<User> users = userDao.findAll();
        for(User user : users){
            System.out.println(user);
        }

    }
    /**
     * 测试保存操作
     */
    @Test
    public void testSave(){
        User user = new User();
        user.setUsername("dao impl user");
        user.setAddress("北京市顺义区");
        user.setSex("男");
        user.setBirthday(new Date());
        System.out.println("保存操作之前:"+user);
        //5.执行保存方法
        userDao.saveUser(user);

        System.out.println("保存操作之后:"+user);
    }

    /**
     * 测试更新操作
     */
    @Test
    public void testUpdate(){
        User user = new User();
        user.setId(50);
        user.setUsername("userdaoimpl update user");
        user.setAddress("北京市顺义区");
        user.setSex("女");
        user.setBirthday(new Date());

        //5.执行保存方法
        userDao.updateUser(user);
    }

    /**
     * 测试删除操作
     */
    @Test
    public void testDelete(){
        //5.执行删除方法
        userDao.deleteUser(54);
    }

    /**
     * 测试删除操作
     */
    @Test
    public void testFindOne(){
        //5.执行查询一个方法
        User  user = userDao.findById(50);
        System.out.println(user);
    }

    /**
     * 测试模糊查询操作
     */
    @Test
    public void testFindByName(){
        //5.执行查询一个方法
        List<User> users = userDao.findByName("%王%");
        for(User user : users){
            System.out.println(user);
        }
    }

    /**
     * 测试查询总记录条数
     */
    @Test
    public void testFindTotal(){
        //5.执行查询一个方法
        int count = userDao.findTotal();
        System.out.println(count);
    }



}

11、SqlMapConfig.xml配置文件别名和属性

11.1 SqlMapConfig.xml中配置的内容和顺序

image-20201107150458421

11.2 properties(属性)

在使用properties标签配置时,我们可以采用两种方式指定属性配置

11.2.1 第一种

<configuration>
    <properties>
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/eesy"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </properties>
    <!--配置环境-->
    <environments default="mysql">
        <!--配置Mysql的环境-->
        <environment id="mysql">
            <!--配置事务的类型-->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置数据源(连接池)-->
            <dataSource type="POOLED">
                <!--配置连接数据库的4个基本信息-->
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
                
                
            </dataSource>

11.2.2 第二种

在classpath下定义db.properties文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/eesy_mybatis
jdbc.username=root
jdbc.password=1234
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <!--mybatis的主配置文件-->

<configuration>
    <!--resource属性:用于指定properties配置文件位置,要求配置文件必须在类路径下  resource="jdbcConfig.properties"





-->
    <properties resource="jdbcConfig.properties">

    </properties>
    <!--配置环境-->
    <environments default="mysql">
        <!--配置Mysql的环境-->
        <environment id="mysql">
            <!--配置事务的类型-->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置数据源(连接池)-->
            <dataSource type="POOLED">
                <!--配置连接数据库的4个基本信息-->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
                
                
            </dataSource>

            
        </environment>

也可以这样写

		url属性:
			URL:Uniform Resource Locator 统一资源定位符
			http://localhost:8080/mystror/categoryServlet  URL
		uri:Uniform Resource Identifier 统一资源标识符
			/mystroe/CategoryServlet
		  它时可以在web应用中唯一定位一个资源的路径

可以这样写,把文件地址直接copy到浏览器地址栏,就可以显示出来本机的文件路径的url地址
<properties url="file:///C:/Users/10185/IdeaProjects/day01_eesy_02mybatis/src/main/resources/jdbcConfig.properties">

    </properties>

11.3 typeAliases(类型别名)

在前面我们讲的Mybatis支持的默认别名,我们也可以采用自定义别名方式来开发

11.3.1 自定义别名:

<typeAliases>
    <typeAlias type="com.atguigu.domain.User" alias="user"/>
</typeAliases>
 <select id="findAll" resultType="user">
        select id,username name,address,sex,birthday from user;
    </select>

11.3.2 用package方法配置别名

<typeAliases>
    <package name="com.atguigu.domain"/>
</typeAliases>

用于指定要配置别名的包,当指定之后,该包下的实体类都会注册别名,并且类名就是别名,不在区分大小写

11.3.3 用package给mapper配置别名

<mappers>
    <!--指定映射配置文件的位置,映射配置文件指的是每一个dao独立配置文件-->
    <!--如果使用注解来配置的话,此处应该使用class属性指定被注解的dao全限定类名-->
    <!--这是为了映射到UserDao.xml中去,以便进行后续的操作-->
    <package name="com.atguigu.dao"/>
    

</mappers>

package标签是用于指定dao接口所在的包,当指定了之后就不需要再写mapper以及resource或者class了

注意此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。

12 用注解的方法配置UserDao下面有新的

package com.atguigu.dao;


import com.atguigu.domain.QueryVo;
import com.atguigu.domain.User;
import org.apache.ibatis.annotations.*;

import java.lang.annotation.Target;
import java.util.List;

/**
 * 用户的持久型接口
 */


public interface UserDao {
    /**
     * 查询所有操作
     * @return
     */
    @Select("select id,username name,address,sex,birthday from user")
    List<User> findAll();
    @Insert("insert into user(username,address,sex,birthday) value (#{name},#{address},#{sex},#{birthday})")
    /*  <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        </selectKey>*/
    @SelectKey(keyProperty = "id",keyColumn = "id",resultType = Integer.class,before = false, statement = "select last_insert_id()")
    void saveUser(User user);
    @Update("update user set username=#{name},address=#{address},sex=#{sex},birthday=#{birthday} where id = #{id}")
    void updateUser(User user);
    @Delete("delete from user where id = #{id}")
    void deleteUser(Integer id);
    @Select("select id,username name,address,sex,birthday from user where username like #{name} " )
    List<User> getUserByName(String username);
    @Select("select id,username name,address,sex,birthday from user where id = #{id};")
    User getUserById(Integer id);
    @Select("select id,username name,address,sex,birthday from user where username like #{user.name}")
    List<User> getUserByNameByVo(QueryVo queryVo);
    @Select("select count(*) from user")
    Integer findTotal();
   @Update("update user set username = #{username} where id = #{id}")
    void updateNameById(@Param("username") String username, @Param("id") Integer id);
}

其中用多个参数用xml配置,变量前面也需要写@param

<update id="updateNameById">
    update user set username = #{username} where id = #{id}
</update>

13 Mybatis连接池与事务深入

13.1 Mybatis的连接池技术

我们在前面的WEB课程中也学习过类似的连接池技术,而在Mybatis中也有连接池技术,但是它采用的是自己的连接池技术。在Mybatis的SqlMapConfig.xml配置文件中,通过来实现Mybatis中连接池的配置。

13.2 mybatis连接池的分类

image-20201107203906975

image-20201107203917824

13.3 Mybatis中数据源的配置

我们的数据源配置就是在 SqlMapConfig.xml 文件中,具体配置如下:
<!-- 配置数据源(连接池)信息 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
MyBatis 在初始化时,根据<dataSource>的 type 属性来创建相应类型的的数据源 DataSource,即: type=”POOLED”:MyBatis 会创建 PooledDataSource 实例
type=”UNPOOLED” : MyBatis 会创建 UnpooledDataSource 实例
type=”JNDI”:MyBatis 会从 JNDI 服务上查找 DataSource 实例,然后返回使用

13.4 Mybatis 中DataSource的存取

MyBatis 是通过工厂模式来创建数据源 DataSource 对象的, MyBatis 定义了抽象的工厂接口:org.apache.ibatis.datasource.DataSourceFactory,通过其 getDataSource()方法返回数据源DataSource。

下面是 DataSourceFactory 源码,具体如下:

public interface DataSourceFactory{
    void setProperties(Properties props);
    DataSource getDataSource();
    
    
    
}

MyBatis创建了DataSource示例后,会将其放到Configuation对象内的Environment对象中,供以后使用

​ 具体分析过程如下:

1、先进入XMLConfigBuilder类中,可以找到如下代码

image-20201107205147335

2、分析configuration对象的environoment属性,结果如下

image-20201107205429598

13.5 Mybatis中连接池获取连接的具体操作

import org.apache.ibatis.reflection.ExceptionUtil;

import java.lang.reflect.InvocationHandler;获得连接.在使用mapper进行数据库操作时,会使用JdbcTransaction获得连接.

        JdbcTransaction

protected DataSource dataSource;

        Connection connection = dataSource.getConnection();

        获取连接.PooledDataSource.popConnection().
        while (conn == null) {}  当获得的连接不为空时返回,否则一直执行.

        1.当空闲连接不为空时,从空闲连接中获取连接,并将连接从空闲连接中去除.

        if (state.idleConnections.size() > 0) {
        // Pool has available connection
        conn = state.idleConnections.remove(0);
        if (log.isDebugEnabled()) {
        log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
        }

        2.当空闲连接为空时,判断正在使用的连接的数量是否小于设置的连接池最大使用连接数,如果小于,则新建连接

        else {
        // Pool does not have available connection
        if (state.activeConnections.size() < poolMaximumActiveConnections) {
        // Can create new connection
        conn = new PooledConnection(dataSource.getConnection(), this);
@SuppressWarnings("unused")
//used in logging, if enabled
            Connection realConn = conn.getRealConnection();
                    if (log.isDebugEnabled()) {
                    log.debug("Created connection " + conn.getRealHashCode() + ".");
                    }

                    3.当空闲连接为空,正在使用连接等于连接池最大连接时,不能创建新的连接,只能等待旧的连接释放

                    获得最先使用的连接,判断被检出的时间,即使用的时间是否超过设置的最大被检出时间.

                    如果大于.

                    声明逾期连接数量 +1

                    逾期连接累计被检出时间+ 此连接被检出时间

                    累计被检出时间 + 此连接被检出时间

                    从使用的连接中移除此连接

                    如果连接不是自动提交,则回滚

                    以旧的连接的数据创建新的连接

                    将旧连接状态置为false.

                    else {
                    // Cannot create new connection
                    PooledConnection oldestActiveConnection = state.activeConnections.get(0);
                    long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
                    if (longestCheckoutTime > poolMaximumCheckoutTime) {
                    // Can claim overdue connection
                    state.claimedOverdueConnectionCount++;
                    state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                    state.accumulatedCheckoutTime += longestCheckoutTime;
                    state.activeConnections.remove(oldestActiveConnection);
                    if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                    oldestActiveConnection.getRealConnection().rollback();
                    }
                    conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                    oldestActiveConnection.invalidate();
                    if (log.isDebugEnabled()) {
                    log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
                    }

                    4.当空闲连接为空,使用连接等于最大连接,最先使用的连接没有过期,则只能等待连接过期

                    等待设置的等待时间

                    累计等待时间 += 等待了的时间

                    try {
                    if (!countedWait) {
                    state.hadToWaitCount++;
                    countedWait = true;
                    }
                    if (log.isDebugEnabled()) {
                    log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                    }
                    long wt = System.currentTimeMillis();
                    state.wait(poolTimeToWait);
                    state.accumulatedWaitTime += System.currentTimeMillis() - wt;
                    } catch (InterruptedException e) {
                    break;}

                    5.当获得连接,且连接部位null,则跳出了while循环

                    5.1.判断连接是否可用

                    如果可用,对连接进行参数设置

                    设置连接代码,是由url+username+password,进行hashcode获得的int类型的值.

                    设置被检出时间,和最后的使用时间,都是当前时间.

                    添加到使用的连接中

                    请求连接的计数器+1

                    累计获得请求耗费的时间 += 此方法执行开始到此行代码执行时耗费的时间.

                    if (conn != null) {
                    if (conn.isValid()) {
                    if (!conn.getRealConnection().getAutoCommit()) {
                    conn.getRealConnection().rollback();
                    }
                    conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
                    conn.setCheckoutTimestamp(System.currentTimeMillis());
                    conn.setLastUsedTimestamp(System.currentTimeMillis());
                    state.activeConnections.add(conn);
                    state.requestCount++;
                    state.accumulatedRequestTime += System.currentTimeMillis() - t;
                    }

                    5.2.如果连接不可用.

                    坏连接数量+1

                    本地坏连接数量+1

                    连接位置null,会继续获取连接,

                    else {
                    if (log.isDebugEnabled()) {
                    log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
                    }
                    state.badConnectionCount++;
                    localBadConnectionCount++;
                    conn = null;

                    5.2.2.如果连续获取连接都是坏连接.且坏连接的数量>设置的空闲连接的最大值+3

                    则报错,扔出异常
                    if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
                    if (log.isDebugEnabled()) {
                    log.debug("PooledDataSource: Could not get a good connection to the database.");
                    }
                    throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                    }

                    6.获得连接后,对连接进行非空判断,

                    如果为空,则抛出异常

                    if (conn == null) {
                    if (log.isDebugEnabled()) {
                    log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
                    }
                    throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
                    }

                    7.返回获得的连接,pooledConnection.

                    return conn;


                    从上述方法可以看出,

                    获取连接时,会从连接池进行获取,
                    如果连接池中没有连接,会判断使用的连接数量是否大于设置的总数
                    如果小于总数,则创建新的连接
                    如果不小于总数,则需要复用连接,
                    判断最先使用的连接是否过期,如果过期,则直接以过期的连接创建新的连接进行使用,过期的连接置为invalid
                    如果没有过期的连接,则需要等待设置的等待时间,然后在进行获取连接
                    如果连接为null,则会一直进行循环获取.
                    获取连接后,对连接进行校验,会调用pingConnection()方法
                    如果连接不可用则则为坏连接,并将连接置为null,继续进行获取
                    当坏连接的数量>最大空闲连接数量+3时,抛出异常,
                    最后对连接进行!null判断,如果获得的连接为空,则抛出异常.


                    关闭连接
                    单独的mybatis框架使用的是SqlSession的默认实现类,DefaultSqlSession,
                    这个类不是线程安全的,他有成员属性,而且有方法可以对这个属性进行修改,有的方法需要使用这个属性,就造成,如果是多个线程同时使用,会不安全.
                    所以,我们单独使用mybatis时,是直接从Factory中new一个SqlSession,然后使用完成后,要关闭.每次都是用新的来操作.
                    当使用spring和mybatis整合的时候,可以使用SqlSessionTemplate来实现对session的代理.
                    当SqlSessionTemplate交给spring管理的时候,会在全局创建一个session,单例的,所有的dao公用同一个session.
                    因为SqlSessionTemplate是线程安全的,
                    且,SqlSessionTemplate在代理方法执行完成后,会有一个session.close().的操作.
                    具体的源码及执行流程,以后会梳理.

                    当使用完连接后,要进行关闭,一直搞不明白,为什么需要关闭,对于这些不是很理解.

                    而且,关闭的话,是关闭session还是关闭connection,对于两者的关系,还在梳理中.

                    使用spring整合的时候,不需要手动关闭session,可能是底层有类会对session进行关闭.

                    梳理完mybatis后,要梳理spring的源码,把和mybatis的整合看一下.

                    当关闭连接时,会使用代理,判断调用的方法,如果是ConnectionClosed(),就会调用pushConnection()方法,将连接放回连接池中,并不是关闭连接.如果是别的方法,则还是会正常的执行.

class PooledConnection implements InvocationHandler

        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            String methodName = method.getName();
            if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
                dataSource.pushConnection(this);
                return null;
            } else {
                try {
                    if (method.getDeclaringClass() != Object.class) {
                        // issue #578. 
                        // toString() should never fail
                        // throw an SQLException instead of a Runtime
                        checkConnection();
                    }
                    return method.invoke(realConnection, args);
                } catch (Throwable t) {
                    throw ExceptionUtil.unwrapThrowable(t);
                }
            }
        }
 

PooledConnection实现了InvocationHandler接口,方法直接使用此类进行代理.

        当调用invoke方法后,会根据方法名进行判断,如果是close()方法,则将连接放回连接池,不会直接关闭连接.

        PooledDataSource.pushConnection

protected void pushConnection(PooledConnection conn) throws SQLException {

        1.将连接从使用连接移除

        state.activeConnections.remove(conn);

        2.判断连接是否可用

        if (conn.isValid()) {

        2.1.当空闲连接的数量小于设置的最大的空闲连接的数量,且,连接的连接类型代码和记录的连接类型代码相同时

//conn.getConnectionTypeCode(),是每个pooledConnection的属性,使用url+username+password,进行hashcode算法,获得的int类型数据,每个conn的typecode应该是唯一的,typecode属性实在popConnection时设置的.

//expectedConnectionTypeCode是pooledConnection的属性,在创建pooledConnection时赋值

//同一个DataSource的typecode应该是一致的,通过同一个DataSource获得的connection的typecode也是相同的

//typecode = ("" + url + username + password).hashCode()

//同一个连接设置获得的typecode是相同的
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        if (!conn.getRealConnection().getAutoCommit()) {
        conn.getRealConnection().rollback();
        }

//以当前连接创建新的连接,然后放入到空闲连接中
        PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
        state.idleConnections.add(newConn);

//设置创建时间戳和最后使用时间戳
        newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
        newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());

//将旧的连接置为无效
        conn.invalidate();
        if (log.isDebugEnabled()) {
        log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
        }

//唤醒所有的线程
        state.notifyAll();
        } else {

//如果不满足上述的条件,则直接关闭连接,并将连接置为无效
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        if (!conn.getRealConnection().getAutoCommit()) {
        conn.getRealConnection().rollback();
        }

//此处是真正的关闭连接,使用的是connection,不是pooledConnection.
        conn.getRealConnection().close();
        if (log.isDebugEnabled()) {
        log.debug("Closed connection " + conn.getRealHashCode() + ".");
        }
        conn.invalidate();
        }
        }

        3.当连接不可用时,直接丢弃连接,不把连接放入到连接池中

        else {
        if (log.isDebugEnabled()) {
        log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
        }
        state.badConnectionCount++;
        }




        总结,pushConnection

        用户获得连接时获得的是poolConnection中的proxyConnection,即为代理连接.
        return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
        ProxyConnection在pooledConnection中创建,是pooledConnection的属性,在pooledConnection创建时即创建代理.
        且pooledConnection中还有一个属性是Connection,
        当调用close()方法时,会使用代理,判断方法,如果是close(),则将连接放回连接池,如果是别的方法,则执行connection的方法
        return method.invoke(realConnection, args);


        测试连接
        在pop和push时都会对连接的可用性进行判断,会调用pingConnection方法.

        PooledDataSource.pingConnection();

protected boolean pingConnection(PooledConnection conn) {

//判断是否开启了验证,这个属性是在配置文件中设置,

        if (poolPingEnabled) {

//poolPingConnectionsNotUsedFor是配置文件中的属性.

//conn.getTimeElapsedSinceLastUse() = System.currentTimeMillis() - lastUsedTimestamp

//即当前时间 - 连接创建的时间.如果> 我们配置文件中设置的间隔时间,就进行测试,如果不大于,就不测试

        if (poolPingConnectionsNotUsedFor >= 0
        && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {

        log.debug("Testing connection " + conn.getRealHashCode() + " ...");

//执行配置文件中的sql语句,进行测试

        Connection realConn = conn.getRealConnection();
        Statement statement = realConn.createStatement();
        ResultSet rs = statement.executeQuery(poolPingQuery);
        rs.close();
        statement.close();

        log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");

//如果测试不成功,则报错,并把连接关闭,并返回false

//如果为false则表示连接不可用,在pop中,会将conn=null,会继续获取连接

//在push中,开始时就从active中移除了conn,如果conn不可用,就不把conn放入idle中.

        catch (Exception e) {
        log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());

        conn.getRealConnection().close();

        log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
             从上述方法可以看出,

                获取连接时,会从连接池进行获取,
                如果连接池中没有连接,会判断使用的连接数量是否大于设置的总数
                如果小于总数,则创建新的连接
                如果不小于总数,则需要复用连接,
                判断最先使用的连接是否过期,如果过期,则直接以过期的连接创建新的连接进行使用,过期的连接置为invalid
                如果没有过期的连接,则需要等待设置的等待时间,然后在进行获取连接
                如果连接为null,则会一直进行循环获取.
                获取连接后,对连接进行校验,会调用pingConnection()方法
                如果连接不可用则则为坏连接,并将连接置为null,继续进行获取
                当坏连接的数量>最大空闲连接数量+3时,抛出异常,
                最后对连接进行!null判断,如果获的连接为空,则抛出异常. 
                关闭连接

13.6 jndi方式连接数据库

13.6.1 新建web工程 加入META-INF

里面新建一个context.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!-- 
<Resource 
name="jdbc/eesy_mybatis"						数据源的名称
type="javax.sql.DataSource"						数据源类型
auth="Container"								数据源提供者
maxActive="20"									最大活动数
maxWait="10000"									最大等待时间
maxIdle="5"										最大空闲数
username="root"									用户名
password="1234"									密码
driverClassName="com.mysql.jdbc.Driver"			驱动类
url="jdbc:mysql://localhost:3306/eesy_mybatis"	连接url字符串
/>
 -->
<Resource 
name="jdbc/eesy_mybatis"
type="javax.sql.DataSource"
auth="Container"
maxActive="20"
maxWait="10000"
maxIdle="5"
username="root"
password="1234"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/eesy_mybatis"
/>
</Context>

13.6.2 建立依赖

<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.5</version>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.6</version>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.12</version>
  </dependency>
  <dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.0</version>
  </dependency>
</dependencies>

13.6.3 SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- 导入约束 -->
<!DOCTYPE configuration  
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
      <package name="com.itheima.domain"></package>
   </typeAliases>
   <!-- 配置mybatis的环境 -->
   <environments default="mysql">
      <!-- 配置mysql的环境 -->
      <environment id="mysql">
         <!-- 配置事务控制的方式 -->
         <transactionManager type="JDBC"></transactionManager>
         <!-- 配置连接数据库的必备信息  type属性表示是否使用数据源(连接池)-->
         <dataSource type="JNDI">
            <property name="data_source" value="java:comp/env/jdbc/eesy_mybatis"/>
         </dataSource>
      </environment>
   </environments>
   
   <!-- 指定mapper配置文件的位置 -->
   <mappers>
      <package name="com.atguigu.dao"/>
   </mappers>
</configuration>

13.6.4 在index.jsp页面上写测试(不能写测试类,因为要经过tomcat服务器)

<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.ibatis.io.Resources" %>
<%@ page import="org.apache.ibatis.session.SqlSessionFactoryBuilder" %>
<%@ page import="org.apache.ibatis.session.SqlSessionFactory" %>
<%@ page import="org.apache.ibatis.session.SqlSession" %>
<%@ page import="com.atguigu.dao.UserDao" %>
<%@ page import="com.atguigu.pojo.User" %>
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<%
    InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    SqlSessionFactory build = builder.build(resourceAsStream);
    SqlSession sqlSession = build.openSession(true);
    UserDao mapper = sqlSession.getMapper(UserDao.class);
    List<User> all = mapper.findAll();
    for (User user : all) {
        System.out.println(user);
    }
    sqlSession.close();
    resourceAsStream.close();%>




</body>
</html>

13.7 事务变成自动提交

session = factory.openSession(true);

image-20201107210916559

14 Mybatis的动态Sql语句

Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL 是动态变化的,此时在前面的学习中我们的 SQL 就不能满足要求了。

多条件查询时,若传入的参数不包含有该条件就不应该存在在sql语句中

findUserByidAndName (User user) 如果user属性user.name 没有赋值,那么查出来的结果就是空的,那么下面的where语句无论怎么样都是空的

比如 select * from user where id = #{id} and name = #{name} 里面的name属性为空值

如果name属性为空值,那么无论怎样结果就为空,因此需要动态sql作if判断

14.1 动态SQL之if标签

我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询,

如果 username 不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。

<select id="findUserByCondition" resultMap="userMap">
    select * from user where 1=1
    <if test="name != null">
        and username like #{name}
    </if>
    <if test="sex != null">
        and sex like #{sex}
    </if>

</select>

测试类

@Test
public void findUserByCondition() {
    User user = new User();
    user.setName("%江%");
    user.setSex("女");
    List<User> userByCondition = mapper.findUserByCondition(user);
    for (User user1 : userByCondition) {
        System.out.println(user1);
    }
}

14.2 动态SQL之where标签的使用

<select id="findUserByCondition" resultMap="userMap">
    select * from user
    <where>
    <if test="name != null">
        and username like #{name}
    </if>
    <if test="sex != null">
        and sex like #{sex}
    </if>
</where>
</select>

14.3 动态标签之foreach标签

14.3.1 需求

传入多个 id 查询用户信息,用下边两个 sql 实现:

SELECT * FROM USERS WHERE username LIKE ‘%张%’ AND (id =10 OR id =89 OR id=16) SELECT * FROM USERS WHERE username LIKE ‘%张%’ AND id IN (10,89,16)

这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来。这样我们将如何进行参数的传递?

14.3.2 在QueryVo中加入一个List集合用于封装参数

List<Integer> ids;

14.3.3 持久层Dao接口

List<User> findUserByManyId(QueryVo queryVo);

14.3.4 持久层Dao映射配置

<select id="findUserByManyId" resultMap="userMap" parameterType="queryVo">
    select * from user 
    <where>
        <if test="ids != null and ids.size()>0"></if>
         <foreach collection="ids" open="id in (" item="uid" close=")" separator=",">
             #{uid}
         </foreach>
        
    </where>
    
    
    
</select>
SQL 语句:

select 字段 from user where id in (?)

<foreach>标签用于遍历集合,它的属性:

collection:代表要遍历的集合元素,注意编写时不要写#{} open:代表语句的开始部分

close:代表结束部分

item:代表遍历集合的每个元素,生成的变量名

sperator:代表分隔符

补充

 void deleteUserMany(List<Integer> lists);

<delete id="deleteUserMany">
    delete from user where id in (
        <foreach collection="list" item="eid" separator=",">
            #{eid}

        </foreach>    
    </delete>
注意当传递参数是list或array的时候,mybatis或将list或array放在map集合中,list以list为键,array以array为键

14.3.5 编写测试方法

public void findUserByManyId() {
    QueryVo queryVo = new QueryVo();

    queryVo.setIds(Arrays.asList(55,56,57,58,59,60));
    List<User> userByManyId = mapper.findUserByManyId(queryVo);
    for (User user : userByManyId) {
        System.out.println(user);
    }
}

14.4 trim标签的使用

Trim 可以在条件判断完的SQL语句前后 添加或者去掉指定的字符

prefix: 添加前缀

prefixOverrides: 去掉前缀

suffix: 添加后缀

suffixOverrides: 去掉后缀

image-20201110212042589

比如说这个最好还有and如果最后一条语句不执行,那么拼接完成的sql语句最后面会有and,因此需要用suffixOverrides去掉后缀

15 Mybatis中简化编写的SQL片段

15.1 自定代码片段

<!-- 抽取重复的语句代码片段 -->
<sql id="defaultSql"> select * from user
</sql>

15.2 引用代码片段

 <select id="findUserByManyId" resultMap="userMap" parameterType="queryVo">
        <include refid="defaultSql"></include>
        <where>
            <if test="ids != null and ids.size()>0"></if>
             <foreach collection="ids" open="id in (" item="uid" close=")" separator=",">
                 #{uid}
             </foreach>
            
        </where>
        
        
        
    </select>

16 Mybatis多表查询之一对多

16.1 一对一查询(多对一)

mybatis中的多表查询 表之间的关系有几种: 一对多 多对一 一对一 多对多 举例: 用户和订单就是一对多 订单和用户就是多对一 一个用户可以下多个订单 多个订单属于同一个用户

	人和身份证号就是一对一
		一个人只能有一个身份证号
		一个身份证号只能属于一个人

	老师和学生之间就是多对多
		一个学生可以被多个老师教过
		一个老师可以交多个学生
特例:
	如果拿出每一个订单,他都只能属于一个用户。
	所以Mybatis就把多对一看成了一对一。

需求

查询所有账户信息,关联查询下单用户信息。注意:

因为一个账户信息只能供某个用户使用,所以从查询账户信息出发关联查询用户信息为一对一查询。如

果从用户信息出发查询用户下的账户信息则为一对多查询,因为一个用户可以有多个账户。

方法一

自定义账户信息实体类
public class Account implements Serializable {


	private Integer id; 
    private Integer uid; 
    private Double money;
编写sql语句
SELECT account.*,user.username,user.address

from account,user where account.uid = user.id
定义AccountUser类

为了能够封装上面 SQL 语句的查询结果,定义 AccountCustomer 类中要包含账户信息同时还要包含用户信息,所以我们要在定义 AccountUser 类时可以继承 User 类。

public class AccountUser extends Account implements Serializable { 
  
 private String username; 
 private String address; 
 public String getUsername() { 
  return username; 
 } 
 public void setUsername(String username) { 
  this.username = username; 
 } 
 public String getAddress() { 
  return address; 
 } 
 public void setAddress(String address) { 
  this.address = address; 
 } 
 @Override 
 public String toString() { 
  return super.toString() + "   AccountUser [username=" + username + ", address=" + address + "]"; 
 } 
} 
定义账户的持久型接口
public interface IAccountDao { 
  
 /** 
  * 查询所有账户,同时获取账户的所属用户名称以及它的地址信息 
  * @return 
  */ 
 List<AccountUser> findAll(); 
} 
定义AccountDao.xml文件中的查询配置信息
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE mapper   
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"   
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 
<mapper namespace="com.itheima.dao.IAccountDao"> 
 <!-- 配置查询所有操作--> 
 <select id="findAll" resultType="accountuser"> 
  select a.*,u.username,u.address from account a,user u where a.uid =u.id; 
 </select>  </mapper> 
 
注意:因为上面查询的结果中包含了账户信息同时还包含了用户信息,所以我们的返回值类型returnType的值设置为AccountUser类型,这样就可以接收账户信息和用户信息了。 
定义测试类
public class AccountTest { 
  
 private InputStream in ; 
 private SqlSessionFactory factory; 
 private SqlSession session; 
 private IAccountDao accountDao; 
  
 @Test 
 public void testFindAll()  {  
  //6.执行操作 
  List<AccountUser> accountusers = accountDao.findAll(); 
  for(AccountUser au : accountusers) { 
   System.out.println(au); 
  }     
 } 
  
 @Before//在测试方法执行之前执行 
 public void init()throws Exception { 
  //1.读取配置文件 
  in = Resources.getResourceAsStream("SqlMapConfig.xml"); 
  //2.创建构建者对象 
  SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); 
  //3.创建SqlSession工厂对象 
  factory = builder.build(in); 
  //4.创建SqlSession对象 
  session = factory.openSession(); 
  //5.创建Dao的代理对象 
  accountDao = session.getMapper(IAccountDao.class); 
 } 
  
 @After//在测试方法执行完成之后执行 
 public void destroy() throws Exception{ 
  session.commit(); 
  //7.释放资源 
  session.close(); 
  in.close(); 
 } 
} 

定义专门的po类作为输出类型,其中定义了sql查询结果集所有的字段。此方法较为简单,企业中使用普遍。

方法二

使用resultMap,定义专门的resultMap用于映射一对一查询结果。 通过面向对象的(has a)关系可以得知,我们可以在Account类中加入一个User类的对象来代表这个账户是哪个用户的

修改Account类
public class Account implements Serializable {
    private Integer id;
    private Integer uid;
    private Double money;
    private User user;
修改AccountDao接口中的方法
public interface AccountDao {

    List<Account> findAll();

}

第二种方式,将返回值改为了Account类型

因为Account类中包含了一个User类的对象,它可以封装账户所对应的用户信息

重新定义AccountDao.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--代表你要映射的Dao接口,后面通过namespace.方法名封装成map集合的key属性-->
<mapper namespace="com.atguigu.dao.AccountDao">
    <!--建立映射关系-->
    <resultMap id="account" type="account">
       <id column="accountid" property="id"></id>
        <result column="money" property="money"></result>
        <result column="uid" property="uid"></result>
        <!-- 它是用于指定从表方的引用实体属性的 -->
        <association property="user" javaType="user">
            <id column="id" property="id"></id>
            <result column="username" property="name"></result>
            <result column="birthday" property="birthday"></result>
            <result column="sex" property="sex"></result>
            <result column="address" property="address"></result>


        </association>


    </resultMap>



    <select id="findAll" resultMap="account">
        SELECT `user`.*,`account`.`ID` accountid,money,uid FROM `user`,`account` WHERE account.uid = `user`.id


    </select>




</mapper>
加入测试方法
@Test
public void findAll() {
    List<Account> all = mapper.findAll();
    for (Account account : all) {
        System.out.println(account);
        System.out.println(account.getUser());
    }

}

16.2 多对一查询

要求查询每一个user的accounts的信息,可以有多个

修改user类,增加accounts属性

   private Integer id;
    private String name;
    private Date birthday;
    private String sex;
    private String address;
    private List<Account> accounts;

修改userDao.xml中的findAll方法

增加resultMap, id为userAccountMap

   <resultMap id="userAccountMap" type="user">
        <id column="id" property="id"></id>
        <result column="username" property="name"></result>
        <result column="birthday" property="birthday"></result>
        <result column="sex" property="sex"></result>
        <result column="address" property="address"></result>
        <!-- collection是用于建立一对多中集合属性的对应关系 
   ofType用于指定集合元素的数据类型 
   --> 
        <collection property="accounts" ofType="Account">
            <id column="accountid" property="id"></id>
            <result column="uid" property="uid"></result>
            <result column="money" property="money"></result>
        </collection>

collection 部分定义了用户关联的账户信息。表示关联查询结果集 property=“accList”: 关联查询的结果集存储在User对象的上哪个属性。 ofType=“account”: 指定关联查询的结果集中的对象类型即List中的对象类型。此处可以使用别名,也可以使用全限定名。

<select id="findAll" resultMap="userAccountMap">
SELECT `user`.*,account.`ID` accountid,uid,money FROM `user` LEFT JOIN account ON `user`.`id` = account.`UID`


    </select>

测试方法

@Test
public void findAll() {
    //5、使用代理对象执行方法
    List<User> all = mapper.findAll();
    for (User user : all) {
        System.out.println(user);
        System.out.println(user.getAccounts());
    }
}

注意mybatis中会自动把1对多的合并

sql中的语句

image-20201108144156572

测试类的语句

image-20201108144226552

自动合并了

17 Mybatis多表查询之多对多

实现Role到User多对多

通过前面的学习,我们使用mybatis实现一对多关系的维护,多对多关系其实我们看成是双向的一对多关系

17.1 用户与角色的关系模型

image-20201108155715780

在MySql数据库中添加角色表,用户角色的中间表

角色表

image-20201108155757782

17.2 业务要求及实现SQL

需求: 实现查询所有对象并且加载它所分配的用户信息。 分析: 查询角色我们需要用到Role表,但角色分配的用户的信息我们并不能直接找到用户信息,而是要通过中间表(USER_ROLE表)才能关联到用户信息。 下面是实现的SQL语句:

SELECT r.*,u.id uid, u.username username, u.birthday birthday, u.sex sex, u.address address FROM ROLE r INNER JOIN USER_ROLE ur ON ( r.id = ur.rid) INNER JOIN USER u ON (ur.uid = u.id);

17.3 编写角色实体类

public class Role implements Serializable {
    private Integer id;
    private String roleName;
    private String roleDesc;
    private List<User> users;

17.4 编写Role持久层接口

public interface RoleDao {
    List<Role> findAll();
}

17.5 编写映射文件

<mapper namespace="com.atguigu.dao.RoleDao">
    <resultMap id="roleUserMap" type="role">
        <id column="roleId" property="id"></id>
        <result column="role_desc" property="roleDesc"></result>
        <result column="role_name" property="roleName"></result>
        <collection property="users" ofType="user">
            <id column="id" property="id"></id>
            <result column="username" property="name"></result>
            <result column="birthday" property="birthday"></result>
            <result column="sex" property="sex"></result>
            <result column="address" property="address"></result>
        </collection>

    </resultMap>
    <select id="findAll" resultMap="roleUserMap">
        SELECT `user`.*,`role`.`ID` roleId,`role`.`ROLE_DESC`,`role`.`ROLE_NAME` FROM `role`
         LEFT JOIN `user_role` a ON a.rid = `role`.`ID`
          LEFT JOIN `user` ON `user`.id = a.`UID`
</select>

17.6 编写测试类

@Test
public void findAll() {
    List<Role> all = mapper.findAll();
    for (Role role : all) {
        System.out.println(role);
        System.out.println(role.getUsers());

    }
}

image-20201108160221006

User到Role的多对多 从User出发,我们也可以发现一个用户可以具有多个角色,这样用户到角色的关系也还是一对多关系。这样我们就可以认为User与Role的多对多关系,可以被拆解成两个一对多关系来实现。

18 Mybatis延迟加载策略

​ 通过前面的学习,我们已经掌握了Mybatis中一对一,一对多,多对多关系的配置及实现,可以实现对象的关联查询。实际开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的账户信息。此时就是我们所说的延迟加载。

18.1 何为延迟加载?

延迟加载:

​ 就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据,延迟加载也称懒加载

​ 坏处:

​ 因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降

18.2 实现需求

需求:

​ 查询账户(Account)信息并且关联查询用户(User)信息。如果先查询账户(Account)信息即可满足要求,当我们需要查询用户(User)信息时再查询用户(User)信息,.把对用户(User)信息的按需去查询就是延迟加载

​ mybatis第三天实现多表操作时,我们使用了resultMap来实现一对一,一对多,多对多关系的操作。主要是通过association、collection实现一对一及一对多映射。association、collection具备延迟加载功能。

当一对一 和多对一 不采用延迟加载

当一对多 和多对多 采用延迟加载

18.3 一对一使用延迟加载

18.3.1 账户的持久层接口

package com.atguigu.dao;

import com.atguigu.domain.Account;
import com.atguigu.domain.AccountUser;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface AccountDao {
    /**
     *  查询所有账户,同时获得当前账户所属用户名称以及它的地址信息
     * @return
     */

    List<Account> findAll();

}

18.3.2 账户持久层映射文件

<mapper namespace="com.atguigu.dao.AccountDao">
    <!--建立映射关系-->
    <resultMap id="account" type="account">
       <id column="id" property="id"></id>
        <result column="money" property="money"></result>
        <result column="uid" property="uid"></result>
        <!-- 它是用于指定从表方的引用实体属性的 -->
        <association property="user" column="uid" javaType="user" select="com.atguigu.dao.UserDao.getUserById"></association>
        <!--column相当于 User getUserByid(Integer id) 中的 Integer id -->


    </resultMap>



    <select id="findAll" resultMap="account">
        SELECT * from account;

    </select>

18.3.3 用户的持久层接口和映射文件

public interface UserDao{
    User findById(Integer userId);
    
    
    
}
<select id="getUserById" resultMap="userMap" >
    select * from user where id = #{id};


</select>

18.3.4 开启Mybatis的延迟加载策略

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <!--mybatis的主配置文件-->

<configuration>
    <properties url="file:///C:/Users/10185/IdeaProjects/day01_eesy_02mybatis/src/main/resources/jdbcConfig.properties">

    </properties>
    <settings>
        <!--开启mybatis支持延迟加载-->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!--当开启时任何方法的调用都会加载改对象的所有的属性否则每个属性会按需下载版本3.4.1以前默认时true以后默认时false-->
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>
    <typeAliases>
        <package name="com.atguigu.domain"/>
    </typeAliases>
    <!--配置环境-->
    <environments default="mysql">
        <!--配置Mysql的环境-->
        <environment id="mysql">
            <!--配置事务的类型-->
            <transactionManager type="JDBC"></transactionManager>
            <!--配置数据源连接池-->
            <dataSource type="POOLED">
                <!--配置连接数据库的4个基本信息-->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>


            </dataSource>


        </environment>
    </environments>
    <mappers>
        <!--指定映射配置文件的位置映射配置文件指的是每一个dao独立配置文件-->
        <!--如果使用注解来配置的话此处应该使用class属性指定被注解的dao全限定类名-->
        <!--这是为了映射到UserDao.xml中去以便进行后续的操作-->
        <package name="com.atguigu.dao"/>


    </mappers>
    
    
</configuration>

18.3.5 测试文件的编写

 @Test
    public void findAll() {
        List<Account> all = mapper.findAll();
       

    }

当查询不使用

image-20201108214706644

当查询并使用

image-20201108214744679

18.4 一对多使用延迟加载

编写AccountDao新建方法

Account getAccountByUser(Integer id)

编写AccountDao.xml

 <select id="getAccountByUser" resultMap="account">
        select * from account where id = #{id}
    </select>

修改UserDao.xml

<resultMap id="userAccountMap" type="user">
    <id column="id" property="id"></id>
    <result column="username" property="name"></result>
    <result column="birthday" property="birthday"></result>
    <result column="sex" property="sex"></result>
    <result column="address" property="address"></result>
    <collection property="accounts" column="id" select="com.atguigu.dao.AccountDao.getAccountByUser">
    </collection>
<!--column等于传入select方法的参数
相当于Account getAccountByUser(Integer id) 的(Integer id)

-->

</resultMap>
<!--通过返回值类型得到需要封装的类-->
<sql id="defaultSql">
    select * from user
</sql>
<select id="findAll" resultMap="userAccountMap">
        SELECT * from user;


</select>

19 Mybatis的缓存

Mybatis中的缓存 什么是缓存 存在于内存中的临时数据。 为什么使用缓存 减少和数据库的交互次数,提高执行效率。 什么样的数据能使用缓存,什么样的数据不能使用 适用于缓存: 经常查询并且不经常改变的。 数据的正确与否对最终结果影响不大的。 不适用于缓存: 经常改变的数据 数据的正确与否对最终结果影响很大的。 例如:商品的库存,银行的汇率,股市的牌价。

19.1 一级缓存

19.1.1 证明一级缓存的存在

一级缓存使SqlSession级别的缓存,只要SqlSession没有flush或close,它就存在

    @Test
    public void getAccountByUser() {
        List<Account> accountsByUser = mapper.getAccountsByUser(46);
        sqlSession.close();
        SqlSession sqlSession = build.openSession(true);
        AccountDao mapper = sqlSession.getMapper(AccountDao.class);
        
        sqlSession.clearCache();//此方法也可以清空缓存 

        List<Account> accountsByUser1 = mapper.getAccountsByUser(46);
        System.out.println(accountsByUser==accountsByUser1);

    }
}

结果为false

@Test
public void getAccountByUser() {
    List<Account> accountsByUser = mapper.getAccountsByUser(46);
    List<Account> accountsByUser1 = mapper.getAccountsByUser(46);
    System.out.println(accountsByUser==accountsByUser1);

}

结果为true

我们可以发现,虽然在上面的代码中我们查询了两次,但最后只执行了一次数据库操作,这就是mybatis提供给我们的一级缓存了

19.1.2 一级缓存的分析

一级缓存是SqlSession范围的缓存,当调用SqlSession的修改,添加,删除,commit(),close()等方法时就会清空一级缓存

第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。 得到用户信息,将用户信息存储到一级缓存中。
如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。

19.2 二级缓存

19.2.1 二级缓存的开启

第一步 : 在SqlMapConfig.xml文件 中开启二级缓存
<settings>
	<!--开启二级缓存的支持-->
    <setting name = "cacheEnabled" value="true">
    </setting>

</settings>
因为cacheEnabled的取值默认就为true,所以这一步可以省略不配置,为true代表开启二级缓存,为false代表不开启二级缓存
第二步:配置相关的Mapper映射文件
<cache>标签代表当前这个mapper映射将使用二级缓存,区分的标准就看mapper的namespace值
    <mapper namespace="com.atguigu.dao.AccountDao">
   <!--代表这个使用二级缓存-->
    <cache/>
    <!--建立映射关系-->
    <resultMap id="account" type="account">
       <id column="id" property="id"></id>
        <result column="money" property="money"></result>
        <result column="uid" property="uid"></result>
        <!-- 它是用于指定从表方的引用实体属性的 -->
        <association property="user" column="uid" javaType="user" select="com.atguigu.dao.UserDao.getUserById"></association>


    </resultMap>
        
第三步:配置statement上面的userCache属性
<!--在需要的方法前面调用userCache属性-->
<select id="getAccountsByUser" resultMap="account" useCache="true">
    select * from account where uid = #{id}
</select>

将UserDao.xml映射文件中的select标签中设置userCache=“true"代表当前这个statment要使用二级缓存,如果不使用二级缓存可以设置为false

19.2.2 二级缓存的测试

    @Test
    public void getAccountByUser() {
        List<Account> accountsByUser = mapper.getAccountsByUser(46);
        for (Account account : accountsByUser) {
            User user = account.getUser();
            System.out.println(user.getAccounts());
        }
        System.out.println(accountsByUser.hashCode());
        sqlSession.close();
        SqlSession sqlSession = build.openSession(true);
        AccountDao mapper = sqlSession.getMapper(AccountDao.class);
        List<Account> accountsByUser1 = mapper.getAccountsByUser(46);
        System.out.println(accountsByUser1.hashCode());
        System.out.println(accountsByUser==accountsByUser1);

    }
}

image-20201109093715673

19.2.3 二级缓存的总结

经过上面的测试,我们发现执行了两次查询,并且在执行第一次查询后,我们关闭了一级缓存,再去执行第二次查询时,我们发现并没有对数据库发出sql语句,所以此时的数据就只能是来自于我们所说的二级缓存。

通过图片可以发现,第二次执行调用了缓存,但是对象不是同一个,因为二级缓存不是一个对象

而是通过键值对的方式进行保存的,类似于json表达式,和redis缓存,通过里面的数据重新创建对象

19.2.4 二级缓存的注意事项

当我们在使用二级缓存时,所缓存的类一定要实现java.io.Serializable接口,这种就可以使用序列化方式来保存对象

public class User implements Serializable { 
 
 private Integer id; 
 private String username; 
 private Date birthday; 
 private String sex; 
 private String address; 
} 

20 Mybatis注解开发

这几年来注解开发越来越流行,mybatis也可以使用注解开发方式,这样我们就可以减少编写Mapper映射文件了

@Insert:实现新增

@Update:实现更新

@Delete:实现删除

@Select:实现查询

@Result:实现结果集封装

@Results:可以与@Result一起使用,封装多个结果集

@ResultMap:实现引用@Results定义的封装 @

One:实现一对一结果集封装

@Many:实现一对多结果集封装

@SelectProvider: 实现动态SQL映射

@CacheNamespace:实现注解二级缓存的使用

20.1 当和数据库名字一样时使用注解

package com.atguigu.dao;

import com.atguigu.domain.User;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.List;

/**
 * mybatis中针对,CRUD一共有四个注解
 */
public interface UserDao {
    /**
     * 查询所有用户
     * @return user集合
     */
    @Select("select * from user")
    List<User> findAll();

    /**
     * 保存当前用户
     * @param user
     */
    @Insert("insert into user(username,address,birthday,sex) values(#{username},#{address},#{birthday},#{sex})")
    void saveUser(User user);

    /**
     * 更新当前用户
     * @param user
     */
    @Update("update user set username = #{username}, address = #{address}, birthday = #{birthday}, sex = #{sex} where id = #{id}")
    void updateUser(User user);

    /**
     * 通过id删除用户
     * @param id
     */
    @Delete("delete from user where id = #{id}")
    void deleteUser(Integer id);
    @Select("select * from user where id = #{id}")
    User findUserById(Integer id);

    /**
     * 通过name模糊查询User
     * @param name
     * @return
     */
    @Select("select * from user where username like #{name}")
    List<User> findUserByName(String name);
    @Select("select count(1) from user")
    Integer findUserCount();
}

原先写的

package com.atguigu.dao;


import com.atguigu.domain.QueryVo;
import com.atguigu.domain.User;
import org.apache.ibatis.annotations.*;

import java.lang.annotation.Target;
import java.util.List;

/**
 * 用户的持久型接口
 */


public interface UserDao {
	//使用idea保存数据,并获取自适应键值
    @Insert("insert into user(username,address,sex,birthday) value (#{name},#{address},#{sex},#{birthday})")
    /*  <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        </selectKey> 用xml方式*/
    @SelectKey(keyProperty = "id",keyColumn = "id",resultType = Integer.class,before = false, statement = "select last_insert_id()")
    void saveUser(User user);
    @Update("update user set username=#{name},address=#{address},sex=#{sex},birthday=#{birthday} where id = #{id}")
    void updateUser(User user);
    @Delete("delete from user where id = #{id}")
    void deleteUser(Integer id);
    @Select("select id,username name,address,sex,birthday from user where username like #{name} " )
    List<User> getUserByName(String username);
    @Select("select id,username name,address,sex,birthday from user where id = #{id};")
    User getUserById(Integer id);
    @Select("select id,username name,address,sex,birthday from user where username like #{user.name}")
    List<User> getUserByNameByVo(QueryVo queryVo);
    @Select("select count(*) from user")
    Integer findTotal();
   @Update("update user set username = #{username} where id = #{id}")
    void updateNameById(@Param("username") String username, @Param("id") Integer id);
}

当要传入两个参数时需要前面加上@param

20.2 当与数据库的字段不一样时使用注解

package com.atguigu.dao;

import com.atguigu.domain.User;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.mapping.FetchType;

import javax.print.attribute.standard.JobOriginatingUserName;
import java.util.List;

/**
 * mybatis中针对,CRUD一共有四个注解
 */
public interface UserDao {
    /**
     * 查询所有用户
     * @return user集合
     */
    @Select("select * from user")
    @Results(value = {
            @Result(id = true, column = "id", property = "userId"),
            @Result(column = "address", property = "userAddress"),
            @Result(column = "sex", property = "userSex"),
            /*注意这里username可以不写,因为User里面字段就叫username,如果和数据库里面的字段相同的时候可以不配置column和property的对应关系*/
            @Result(column = "birthday", property = "userBirthday"),
            @Result(column = "id",property = "accounts",many = @Many(select = "com.atguigu.dao.AccountDao.findAccountsByUserId",fetchType = FetchType.LAZY))


    },id = "userMap")
    List<User> findAll();

    /**
     * 保存当前用户
     * @param user
     */
    @Insert("insert into user(username,address,birthday,sex) values(#{username},#{address},#{birthday},#{sex})")
    void saveUser(User user);

    /**
     * 更新当前用户
     * @param user
     */
    @Update("update user set username = #{username}, address = #{address}, birthday = #{birthday}, sex = #{sex} where id = #{id}")
    void updateUser(User user);

    /**
     * 通过id删除用户
     * @param id
     */
    @Delete("delete from user where id = #{id}")
    void deleteUser(Integer id);
    @Select("select * from user where id = #{id}")
    @ResultMap("userMap")
    User findUserById(Integer id);

    /**
     * 通过name模糊查询User
     * @param name
     * @return
     */
    @Select("select * from user where username like #{name}")
    @ResultMap("userMap")
    List<User> findUserByName(String name);

    /**
     * 查询sql数据个数
     * @return
     */
    @Select("select count(1) from user")
    /*注意返回值要对应这里就不需要写@ResultMap("userMap")了*/
    Integer findUserCount();



}

20.3 当一对多多对一的时候使用注解

20.3.1 复杂关系映射的注解说明

@Results注解 
代替的是标签<resultMap>  
该注解中可以使用单个@Result注解,也可以使用@Result集合 
@Results({@Result(),@Result()})或@Results(@Result()) 
 
@Resutl注解 
代替了 <id>标签和<result>标签 
 @Result 中 属性介绍: 
  id 是否是主键字段 
 column 数据库的列名 
 property需要装配的属性名 
 one  需要使用的@One注解(@Result(one=@One)())) 
 many  需要使用的@Many注解(@Result(many=@many)())) 
 
@One注解(一对一) 
 代替了<assocation>标签,是多表查询的关键,在注解中用来指定子查询返回单一对象。 
@One注解属性介绍: 
 select  指定用来多表查询的sqlmapper 
 fetchType会覆盖全局的配置参数lazyLoadingEnabled。。 
使用格式: 
 @Result(column=" ",property="",one=@One(select="")) 
 
@Many注解(多对一) 
      代替了<Collection>标签,是是多表查询的关键,在注解中用来指定子查询返回对象集合。 
 注意:聚集元素用来处理“一对多”的关系。需要指定映射的Java实体类的属性,属性的javaType(一般为ArrayList)但是注解中可以不定义; 
 使用格式: 
  @Result(property="",column="",many=@Many(select="")) 

需求:

​ 加载账户信息时并且加载该账户的用户信息,根据情况可实现延迟加载。(注解方式实现)

20.3.2 使用注解实现一对一复杂关系映射及延迟加载

account对象

public class Account implements Serializable {
    private Integer id;
    private Integer uid;
    private Double money;
    private User user;

accountDao对象

public interface AccountDao {
    @Select("select * from account")
    @Results(value = {
            @Result(column = "uid",property = "uid"),
            @Result(column = "uid",property = "user",one = @One(select = "com.atguigu.dao.UserDao.findUserById",fetchType = FetchType.EAGER)
            )

    },id = "accountMap")


    List<Account> findAll();

    @Select("select * from account where uid = #{id}")
    @ResultMap("accountMap")
    List<Account> findAccountsByUserId(Integer id);
}

20.3.3 使用注解实现一对多复杂关系映射及延迟加载

package com.atguigu.dao;

import com.atguigu.domain.User;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.mapping.FetchType;

import javax.print.attribute.standard.JobOriginatingUserName;
import java.util.List;

/**
 * mybatis中针对,CRUD一共有四个注解
 */
public interface UserDao {
    /**
     * 查询所有用户
     * @return user集合
     */
    @Select("select * from user")
    @Results(value = {
            @Result(id = true, column = "id", property = "userId"),
            @Result(column = "address", property = "userAddress"),
            @Result(column = "sex", property = "userSex"),
            /*注意这里username可以不写,因为User里面字段就叫username,如果和数据库里面的字段相同的时候可以不配置column和property的对应关系*/
            @Result(column = "birthday", property = "userBirthday"),
            @Result(column = "id",property = "accounts",many = @Many(select = "com.atguigu.dao.AccountDao.findAccountsByUserId",fetchType = FetchType.LAZY))


    },id = "userMap")
    List<User> findAll();

    /**
     * 保存当前用户
     * @param user
     */
    @Insert("insert into user(username,address,birthday,sex) values(#{username},#{address},#{birthday},#{sex})")
    void saveUser(User user);

    /**
     * 更新当前用户
     * @param user
     */
    @Update("update user set username = #{username}, address = #{address}, birthday = #{birthday}, sex = #{sex} where id = #{id}")
    void updateUser(User user);

    /**
     * 通过id删除用户
     * @param id
     */
    @Delete("delete from user where id = #{id}")
    void deleteUser(Integer id);
    @Select("select * from user where id = #{id}")
    @ResultMap("userMap")
    User findUserById(Integer id);

    /**
     * 通过name模糊查询User
     * @param name
     * @return
     */
    @Select("select * from user where username like #{name}")
    @ResultMap("userMap")
    List<User> findUserByName(String name);

    /**
     * 查询sql数据个数
     * @return
     */
    @Select("select count(1) from user")
    /*注意返回值要对应这里就不需要写@ResultMap("userMap")了*/
    Integer findUserCount();



}

20.4 mybatis基于注解的二级缓存

在要开启二级缓存的dao接口的前面加上@CacheNamespace(blocking=true)//mybatis基于注解方式实现配置二级缓存

image-20201109185752959

21 mybatis补充之注解开发之一对多分布查询时参数有多个

新建myTest类

public class MyTest implements Serializable {
    private Integer uid;
    private String userAddress;
    private String name;

myTestdao类

public interface MyTestDao {
    /**
     * 通过user数据库中的id属性和address属性来得到MyTest
     * @param id
     * @param address
     * @return
     */
    @Results({
            @Result(id = true,column = "id",property = "uid"),
            @Result(column = "address",property = "userAddress")
    }



    )
    @Select("select * from mytest where id = #{arg0} and address = #{arg1}")
    MyTest findMyTestByUserIdAndAddress(Integer id,String address);
}

UserDao

public interface UserDao {
    /**
     * 查询所有用户
     * @return user集合
     */
    @Select("select * from user")
    @Results(value = {
            @Result(id = true, column = "id", property = "userId"),
            @Result(column = "address", property = "userAddress"),
            @Result(column = "sex", property = "userSex"),
            /*注意这里username可以不写,因为User里面字段就叫username,如果和数据库里面的字段相同的时候可以不配置column和property的对应关系*/
            @Result(column = "birthday", property = "userBirthday"),
        //其中这里的column是一个map集合,arg0代表 @Select("select * from mytest where id = #{arg0} and address = #{arg1}")
//    MyTest findMyTestByUserIdAndAddress(Integer id,String address);中的arg0#{arg0},arg1代表#{arg1}给这两个属性赋值
            @Result(column = "{arg0=id,arg1=address}", property = "myTests",many = @Many(select = "com.atguigu.dao.MyTestDao.findMyTestByUserIdAndAddress",fetchType = FetchType.LAZY))


    },id = "userMap")

    List<User> findAll();

22 Mybatis中反向代码生成器

在pom.xml中配置

<build>
    <finalName>zsxt</finalName>
    <plugins>
        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.2</version>
            <configuration>
                <verbose>true</verbose>
                <overwrite>true</overwrite>
            </configuration>
        </plugin>
    </plugins>
</build>

写入generator.properties

注意没有引号的

jdbc.driverLocation=C:\\Users\\10185\\.m2\\repository\\mysql\\mysql-connector-java\\5.1.20\\mysql-connector-java-5.1.20.jar
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://localhost:3306/eesy
jdbc.userId=root
jdbc.password=123456

写generatorConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!--导入属性配置-->
    <properties resource="generator.properties"></properties>

    <!--指定特定数据库的jdbc驱动jar包的位置-->
    <classPathEntry location="${jdbc.driverLocation}"/>

    <context id="default" targetRuntime="MyBatis3">

        <!-- optional,旨在创建class时,对注释进行控制 -->
        <commentGenerator>
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!--jdbc的数据库连接 -->
        <jdbcConnection
                driverClass="${jdbc.driverClass}"
                connectionURL="${jdbc.connectionURL}"
                userId="${jdbc.userId}"
                password="${jdbc.password}">
        </jdbcConnection>


        <!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制-->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>


        <!-- Model模型生成器,用来生成含有主键key的类,记录类 以及查询Example类
            targetPackage     指定生成的model生成所在的包名
            targetProject     指定在该项目下所在的路径
        -->
        <javaModelGenerator targetPackage="com.atguigu.bean"
                            targetProject="src/main/java">

            <!-- 是否允许子包,即targetPackage.schemaName.tableName -->
            <property name="enableSubPackages" value="false"/>
            <!-- 是否对model添加 构造函数 -->
            <property name="constructorBased" value="true"/>
            <!-- 是否对类CHAR类型的列的数据进行trim操作 -->
            <property name="trimStrings" value="true"/>
            <!-- 建立的Model对象是否 不可改变  即生成的Model对象不会有 setter方法,只有构造方法 -->
            <property name="immutable" value="false"/>
        </javaModelGenerator>

        <!--Mapper映射文件生成所在的目录 为每一个数据库的表生成对应的SqlMap文件 -->
        <sqlMapGenerator targetPackage="com.atguigu.mapper"
                         targetProject="src/main/java">
            <property name="enableSubPackages" value="false"/>
        </sqlMapGenerator>

        <!-- 客户端代码,生成易于使用的针对Model对象和XML配置文件 的代码
                type="ANNOTATEDMAPPER",生成Java Model 和基于注解的Mapper对象
                type="MIXEDMAPPER",生成基于注解的Java Model 和相应的Mapper对象
                type="XMLMAPPER",生成SQLMap XML文件和独立的Mapper接口
        -->
        <javaClientGenerator targetPackage="com.atguigu.mapper"
                             targetProject="src/main/java" type="XMLMAPPER">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>


        <table tableName="user" domainObjectName="User"></table>
        <table tableName="account" domainObjectName="Account"></table>



    </context>
</generatorConfiguration>

image-20201111124423747

生成

一些用法

/**
 * 收集姓名里面带有江,而且是男的
 */
@Test
public void countByExample() {
    UserExample userExample = new UserExample();
    UserExample.Criteria criteria = userExample.createCriteria();
    criteria.andUsernameLike("%江%");
    criteria.andSexLike("男");

    int i = mapper.countByExample(userExample);
    System.out.println(i);
}

注意生成的代码里面不支持(a or b) and c 因此需要用 (c and a) or (c and b)

也可以自己写一个方法用于连接

public Criteria andOrDemo(String value){
    addCriterion("(b = \""+value+"\" or c = \""+value+"\")");
    return (Criteria) this;
 }

mybatis的一些补充

1 xml中的转义字符

image-20201110211828302

23 Mybatis的分页操作

23.1 maven中的配置

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.11</version>
</dependency>

23.2 在<typeAliases后面写一个关于pageHelper的插件

<typeAliases>
    <package name="com.atguigu.domain"/>
</typeAliases>
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
<!--配置环境-->

23.3 在TestUserDao中进行测试

@Test
public void findAll() {
    //5、使用代理对象执行方法,注意只会在当前语句执行的下一行代码执行这个代理拦截操作
    PageHelper.startPage(2,2);
    List<User> all = mapper.findAll();
    PageInfo<User> info = new PageInfo<>(all);
    System.out.println("当前页码:"+info.getPageNum());
    System.out.println("总记录数:"+info.getTotal());
    System.out.println("每页的记录数:"+info.getPageSize());
    System.out.println("总页码:"+info.getPages());
    System.out.println("是否第一页:"+info.isIsFirstPage());
    System.out.println("连续显示的页码:");

    for (User user : all) {
        System.out.println(user);
    }
}

spring

第一章 Spring的入门

1.1 spring的概述

  1. Spring是一个开源框架

  2. Spring为简化企业级开发而生,使用Spring,JavaBean就可以实现很多以前要靠EJB才能实现的功能。同样的功能,在EJB中要通过繁琐的配置和复杂的代码才能够实现,而在Spring中却非常的优雅和简洁。

  3. Spring是一个IOC(DI)和AOP容器框架。

  4. Spring的优良特性

非侵入式:基于Spring开发的应用中的对象可以不依赖于Spring的API

依赖注入:DI——Dependency Injection,反转控制(IOC)最经典的实现。

面向切面编程:Aspect Oriented Programming——AOP

容器:Spring是一个容器,因为它包含并且管理应用对象的生命周期

组件化:Spring实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用XML和Java注解组合这些对象。

一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上Spring 自身也提供了表述层的SpringMVC和持久层的Spring JDBC)。

  1. Spring模块

image-20201213104055097

1.2 Spring的运行环境

加入依赖

    <dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.1.RELEASE</version>
    </dependency>
    </dependencies>

创建Spring xml配置文件

image-20201213104630318

1.3 HelloWorld

1)目标:使用Spring创建对象,为属性赋值

2)创建Student类,创建id和name属性

3)在xml中进行配置

<bean id="Student" class="com.atguigu.Student">
        <property name="id" value="0417180307"></property>
        <property name="name" value="江豪迪"></property>
</bean>

调用

ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("bean.xml");
Object student = classPathXmlApplicationContext.getBean("Student");
System.out.println(student);

发现都是同一个对象,采用了工厂单例设计模式

image-20201213105501486

也可以不需要id属性

<bean class="com.atguigu.Student">
        <property name="id" value="0417180307"></property>
        <property name="name" value="江豪迪"></property>
</bean>

但是调用这个对象的时候需要用class属性

Student bean = classPathXmlApplicationContext.getBean(Student.class);
System.out.println(bean);

但是如果有两个一摸一样的属性,就需要用id属性去区分

<bean id="student1" class="com.atguigu.Student">
        <property name="id" value="0417180307"></property>
        <property name="name" value="江豪迪"></property>
</bean>
<bean id="student2" class="com.atguigu.Student">
        
</bean>
Student bean = classPathXmlApplicationContext.getBean("student1",Student.class);
System.out.println(bean);

第二章 IOC容器和Bean的配置

2.1 IOC和DI

2.1.1 IOC(Inversion of Control):反转控制

在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源.在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。

2.1.2 DI(Dependency Injection):依赖注入

IOC的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入。相对于IOC而言,这种表述更直接。

总结: IOC 就是一种反转控制的思想, 而DI是对IOC的一种具体实现。

2.1.3 IOC容器在Spring中的实现

前提: Spring中有IOC思想, IOC思想必须基于 IOC容器来完成, 而IOC容器在最底层实质上就是一个对象工厂.

1)在通过IOC容器读取Bean的实例之前,需要先将IOC容器本身实例化。

2)Spring提供了IOC容器的两种实现方式

① BeanFactory:IOC容器的基本实现,是Spring内部的基础设施,是面向Spring本身的,不是提供给开发人员使用的。

② ApplicationContext:BeanFactory的子接口,提供了更多高级特性。面向Spring的使用者,几乎所有场合都使用ApplicationContext而不是底层的BeanFactory。

2.1.4 ApplicationContext的主要实现类

  1. ClassPathXmlApplicationContext:对应类路径下的XML格式的配置文件

  2. FileSystemXmlApplicationContext:对应文件系统中的XML格式的配置文件

  3. 在初始化时就创建单例的bean,也可以通过配置的方式指定创建的Bean是多实例的。

2.1.5 ConfigurableApplicationContext

  1. 是ApplicationContext的子接口,包含一些扩展方法

  2. refresh()和close()让ApplicationContext具有启动、关闭和刷新上下文的能力。

2.1.6 WebApplicationContext

  1. 专门为WEB应用而准备的,它允许从相对于WEB根目录的路径中完成初始化工作

image-20201213181929530

2.2 通过类型获取bean

  1. 从IOC容器中获取bean时,除了通过id值获取,还可以通过bean的类型获取。但如果同一个类型的bean在XML文件中配置了多个,则获取时会抛出异常,所以同一个类型的bean在容器中必须是唯一的。

HelloWorld helloWorld = cxt.getBean(HelloWorld. class);

  1. 或者可以使用另外一个重载的方法,同时指定bean的id值和类型
    

HelloWorld helloWorld = cxt.getBean(“helloWorld”,HelloWorld. class);

但是如果有两个一摸一样的属性,就需要用id属性去区分

<bean id="student1" class="com.atguigu.Student">
        <property name="id" value="0417180307"></property>
        <property name="name" value="江豪迪"></property>
</bean>
<bean id="student2" class="com.atguigu.Student">
        
</bean>
Student bean = classPathXmlApplicationContext.getBean("student1",Student.class);
System.out.println(bean);

2.3 给bean的属性赋值

1.通过bean的setXxx()方法赋值

HelloWorld中使用的就是这种方式

image-20201213182512650

2.通过bean的构造器赋值

1)Spring自动匹配合适的构造器

     <bean id="book" class="com.atguigu.spring.bean.Book" >
           <constructor-arg value= "10010"/>
           <constructor-arg value= "Book01"/>
           <constructor-arg value= "Author01"/>
           <constructor-arg value= "20.2"/>
     </bean >

2)通过索引值指定参数位置

  <bean id="book" class="com.atguigu.spring.bean.Book" >
           <constructor-arg value= "10010" index ="0"/>
           <constructor-arg value= "Book01" index ="1"/>
           <constructor-arg value= "Author01" index ="2"/>
           <constructor-arg value= "20.2" index ="3"/>
     </bean >

3)通过类型区分重载的构造器

<bean id="book" class="com.atguigu.spring.bean.Book" >
      <constructor-arg value= "10010" index ="0" type="java.lang.Integer" />
      <constructor-arg value= "Book01" index ="1" type="java.lang.String" />
      <constructor-arg value= "Author01" index ="2" type="java.lang.String" />
      <constructor-arg value= "20.2" index ="3" type="java.lang.Double" />
</bean >

3 通过p名称空间

为了简化XML文件的配置,越来越多的XML文件采用属性而非子元素配置信息。Spring 从2.5版本开始引入了一个新的p命名空间,可以通过元素属性的方式配置Bean 的属性。使用p命名空间后,基于XML的配置方式将进一步简化。

通过在上面加入

<bean 
	id="studentSuper" 
	class="com.atguigu.helloworld.bean.Student"
	p:studentId="2002" p:stuName="Jerry2016" p:age="18" />

4 可以使用的值

1.字面量

1)可以使用字符串表示的值,可以通过value属性或value子节点的方式指定

2)基本数据类型及其封装类,String等类型都可以采用字面值注入的方式

3)若字面值中包含特殊字符,可以使用<![CDATA[]]把字面值包裹起来

2.null值
<bean id="student" class="com.atguigu.Student"  p:name="江豪迪">
    <property name="id"><null/></property>
</bean>
3.给bean的级联属性赋值
<bean id="student" class="com.atguigu.Student"  p:name="江豪迪">
    <property name="id"><null/></property>
    <property name="teacher">
        <bean id="teacher" class="com.atguigu.Teacher" p:name="江盈瑶" p:id="314141">
        </bean>
    </property>
</bean>

image-20201214181156290

4.外部已声明的bean,引用其他的bean
    <bean id="student" class="com.atguigu.Student"  p:name="江豪迪">
        <property name="id"><null/></property>
        <property name="teacher" ref="teacher2">
          
        </property>
    </bean>

    <bean id="teacher2" class="com.atguigu.Teacher" p:id="341123" p:name="江盈瑶"/>

</beans>

内部bean

当bean实例仅仅给一个特定的属性使用时,可以将其声明为内部bean。内部bean声明直接包含在元素里,不需要设置任何id或name属性

<bean id="shop2" class="com.atguigu.spring.bean.Shop" >
    <property name= "book">
        <bean class= "com.atguigu.spring.bean.Book" >
           <property name= "bookId" value ="1000"/>
           <property name= "bookName" value="innerBook" />
           <property name= "author" value="innerAuthor" />
           <property name= "price" value ="50"/>
        </bean>
    </property>
</bean >

2.4 集合属性

在Spring中可以通过一组内置的XML标签来配置集合属性,例如:

2.4.1 数组和List

配置java.util.List类型的属性,需要指定标签,在标签里包含一些元素。这些标签 可以通过指定简单的常量值,通过指定对其他Bean的引用。通过指定内置bean定义。通过指定空元素。甚至可以内嵌其他集合。

 **数组的定义和List一样,都使用<list>元素。**

 配置java.util.Set需要使用<set>标签,定义的方法与List一样。
<property name="strings">
    <list>
        <value>我是一个人</value>
        <value>我还是一个人</value>
    </list>
</property>

2.4.2 Map

Java.util.Map通过标签定义,标签里可以使用多个作为子标签。每个条目包含一个键和一个值。

 必须在<key>标签里定义键。

 因为键和值的类型没有限制,所以可以自由地为它们指定<value>、<ref>、<bean>或<null/>元素。

 可以将Map的键和值作为<entry>的属性定义:简单常量使用key和value来定义;bean引用通过key-ref和value-ref属性定义。
<property name="maps">
    <map>
        <entry>
            <key>
                <value>江迪</value>
            </key>
            <value>小迪迪</value>
        </entry>
        <entry>
            <key>

            </key>
            <ref bean="teacher2"></ref>
        </entry>
    </map>
</property>

2.4.3 集合类型的bean

如果只能将集合对象配置在某个bean内部,则这个集合的配置将不能重用。我们需要 将集合bean的配置拿到外面,供其他bean引用。

配置集合类型的bean需要引入util名称空间

<util:list id="list1">
    <ref bean="teacher2"></ref>
    <ref bean="teacher3"></ref>
</util:list>
<property name="strings">
    <ref bean="list1"></ref>

</property>

注意需要引用util命名空间,还有下面的声明

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

2.5 FactoryBean

Spring中有两种类型的bean,一种是普通bean,另一种是工厂bean,即FactoryBean。

 工厂bean跟普通bean不同,其返回的对象不是指定类的一个实例,其返回的是该工厂bean的getObject方法所返回的对象。

 工厂bean必须实现org.springframework.beans.factory.FactoryBean接口。

img

  <bean id=*"product"* class=*"com.atguigu.spring.bean.ProductFactory"*>       <property name=*"productName"*  value=*"Mp3"* />  </bean>  
package com.atguigu;

import org.springframework.beans.factory.FactoryBean;

public class MyFactory implements FactoryBean<Student> {

    public Student getObject() throws Exception {
        return new Student();
    }

    public Class<?> getObjectType() {
        return Student.class;
    }

    public boolean isSingleton() {
        return false;
    }
}

注意这里不要

public Student getObject() throws Exception {
    ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("bean.xml");
    return classPathXmlApplicationContext.getBean(Student.class);
}

会报错

xml文件

<bean id="factory" class="com.atguigu.MyFactory"></bean>

应用

  public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("one.xml");
        Object factory = classPathXmlApplicationContext.getBean("factory");
        System.out.println(factory);
    }
}

这里默认会调用getObject方法

image-20201214200713260

2.6 bean的作用域

在Spring中,可以在元素的scope属性里设置bean的作用域,以决定这个bean是单实例的还是多实例的。

Image

 默认情况下,Spring只为每个在IOC容器里声明的bean创建唯一一个实例,整个IOC容器范围内都能共享该实例:所有后续的getBean()调用和bean引用都将返回这个唯一的bean实例。该作用域被称为singleton,它是所有bean的默认作用域。



​ 当bean的作用域为单例时,Spring会在IOC容器对象创建时就创建bean的对象实例。而当bean的作用域为prototype时,IOC容器在获取bean的实例时创建bean的实例对象。

设置为多实例

<bean id="student" class="com.atguigu.Student" p:name="江豪迪" scope="prototype">

2.7 bean生命周期

  1. Spring IOC容器可以管理bean的生命周期,Spring允许在bean生命周期内特定的时间点执行指定的任务。

  2. Spring IOC容器对bean的生命周期进行管理的过程:

 ① 通过构造器或工厂方法创建bean实例
 
 ② 为bean的属性设置值和对其他bean的引用
 
 ③ 调用bean的初始化方法
 
 ④ bean可以使用了
 
 ⑤ 当容器关闭时,调用bean的销毁方法
  1. 在配置bean时,通过init-method和destroy-method 属性为bean指定初始化和销毁方法
<bean id="student" class="com.atguigu.Student" p:name="江豪迪"  init-method="init" destroy-method="destroy">

注意:当是多例模式的时候不能使用销毁方法

  1. bean的后置处理器
 ① bean后置处理器允许在调用**初始化方法前后**对bean进行额外的处理
 
 ② bean后置处理器对IOC容器里的所有bean实例逐一处理,而非单一实例。
 
**其典型应用是:检查bean属性的正确性或根据特定的标准更改bean的属性。**

 ③ bean后置处理器需要实现接口:

org.springframework.beans.factory.config.BeanPostProcessor。在初始化方法被调用前后,Spring将把每个bean实例分别传递给上述接口的以下两个方法:

●postProcessBeforeInitialization(Object, String)

●postProcessAfterInitialization(Object, String)

同时需要在xml中配置实现BeanPostProcessor的类,供所有的bean使用

 <bean class="com.atguigu.AfterHander"></bean>
  1. 添加bean后置处理器后bean的生命周期
 ①通过构造器或工厂方法**创建bean实例**
 
 ②为bean的**属性设置值**和对其他bean的引用
 
 ③将bean实例传递给bean后置处理器的postProcessBeforeInitialization()方法
 
 ④调用bean的**初始化**方法
 
 ⑤将bean实例传递给bean后置处理器的postProcessAfterInitialization()方法
 
 ⑥bean可以使用了
 
 ⑦当容器关闭时调用bean的**销毁方法**

2.8 引用外部属性文件

当bean的配置信息逐渐增多时,查找和修改一些bean的配置信息就变得愈加困难。这时可以将一部分信息提取到bean配置文件的外部,以properties格式的属性文件保存起来,同时在bean的配置文件中引用properties属性文件中的内容,从而实现一部分属性值在发生变化时仅修改properties属性文件即可。这种技术多用于连接数据库的基本信息的配置。

2.8.1 直接配置

<!-- 直接配置 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
	<property name="user" value="root"/>
	<property name="password" value="root"/>
	<property name="jdbcUrl" value="jdbc:mysql:///test"/>
	<property name="driverClass" value="com.mysql.jdbc.Driver"/>
</bean>

2.8.2 使用外部的属性文件

1.创建properties属性文件

jdbc.username=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306
jdbc.driverClass=com.mysql.jdbc.Driver

2.使用命名空间或者直接导入包

<!--<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="location" value="db.properties"></property>

</bean>-->



<bean class="com.atguigu.AfterHander"></bean>
<context:property-placeholder location="db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driverClass}"/>
    <property name="password" value="${jdbc.password}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="url" value="${jdbc.url}"/>
</bean>

2.9 自动装配

2.9.1 自动装配的概念

  1. 手动装配:以value或ref的方式明确指定属性值都是手动装配。

  2. 自动装配:根据指定的装配规则,不需要明确指定,Spring自动将匹配的属性值注入bean中。

2.9.2 装配模式

  1. 根据类型自动装配:将类型匹配的bean作为属性注入到另一个bean中。若IOC容器中有多个与目标bean类型一致的bean,Spring将无法判定哪个bean最合适该属性,所以不能执行自动装配(会报错)

  2. 根据名称自动装配:必须将目标bean的名称和属性名设置的完全相同

  3. 通过构造器自动装配:当bean中存在多个构造器时,此种自动装配方式将会很复杂。不推荐使用。

2.9.3 选用建议

相对于使用注解的方式实现的自动装配,在XML文档中进行的自动装配略显笨拙,在项目中更多的使用注解的方式实现

2.10 通过注解配置bean

2.10.1 概述

相对于XML方式而言,通过注解的方式配置bean更加简洁和优雅,而且和MVC组件式开发的理念十分契合,是开发中常用的使用方式

2.10.2 使用注解标识组件

1)普通组件:@Component

标识一个受Spring IOC容器管理的组件1)

2)持久化层组件:@Repository

标识一个受Spring IOC容器管理的持久化层组件

3) 业务逻辑层组件:@Service

标识一个受Spring IOC容器管理的业务逻辑层组件

4) 表述层控制器组件:@Controller

标识一个受Spring IOC容器管理的表述层控制器组件

5) 组件命名规则

 ①默认情况:使用组件的简单类名首字母小写后得到的字符串作为bean的id

 ②使用组件注解的value属性指定bean的id

 注意:事实上Spring并没有能力识别一个组件到底是不是它所标记的类型,即使将@Respository注解用在一个表述层控制器组件上面也不会产生任何错误,所以@Respository、@Service、@Controller这几个注解仅仅是为了让开发人员自己明确当前的组件扮演的角色。

只要包含上面集合注解,就可以实现通过注解配置bean

2.10.3 扫描组件

组件被上述注解标识后还需要通过Spring进行扫描才能够侦测到

1)指定被扫描的package

<context:component-scan base-package="com.atguigu.component"/>
  1. 详细说明
 ①**base-package**属性指定一个需要扫描的基类包,Spring容器将会扫描这个基类包及其子包中的所有类。
 
 ②当需要扫描多个包时可以使用逗号分隔。
 
 ③如果仅希望扫描特定的类而非基包下的所有类,可使用resource-pattern属性过滤特定的类,示例:
<context:component-scan base-package="com.atguigu.spring"
resource-pattern="springService/*.class"/>

表示只扫描springService包下的文件,/*.class不能少

④包含与排除

    < context:include-filter >子节点表示要包含的目标类

注意:通常需要与use-default-filters属性配合使用才能够达到“仅包含某些 组件”这样的效果。即:通过将use-default-filters属性设置为false 禁用默认过滤器,然后扫描的就只是include-filter中的规则指定的 组件了。

   **< context:exclude-filter >子节点表示要排除在外的目标类**

   **component-scan下可以拥有若干个include-filter和exclude-filter子节点**
<!-- <context:component-scan base-package="com.atguigu.spring" >
     <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
     <context:exclude-filter type="assignable" expression="com.atguigu.spring.springDao.UserDaoImpl"/>
 </context:component-scan>
 -->

 <context:component-scan base-package="com.atguigu.spring" use-default-filters="false">
     <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
 </context:component-scan>

注意:如果用include的时候use-default-filter里面的值一定要改成false

   过滤表达式
类别 示例 说明
annotation com.atguigu.XxxAnnotation 过滤所有标注了XxxAnnotation的类。这个规则根据目标组件是否标注了指定类型的注解进行过滤。
assignable com.atguigu.BaseXxx 过滤所有BaseXxx类的子类。这个规则根据目标组件是否是指定类型的子类的方式进行过滤。
aspectj com.atguigu.*Service+ 所有类名是以Service结束的,或这样的类的子类。这个规则根据AspectJ表达式进行过滤。
regex com.atguigu.anno.* 所有com.atguigu.anno包下的类。这个规则根据正则表达式匹配到的类名进行过滤。
custom com.atguigu.XxxTypeFilter 使用XxxTypeFilter类通过编码的方式自定义过滤规则。该类必须实现org.springframework.core.type.filter.TypeFilter接口

2.10.4 组件装配

  • 基于注解的组件化管理:
  • @Component,@Controller(控制层),@Service(业务层),@Repository(持久层)
  • 以上四个注解功能完全相同,不过在实际开发中,要在实现不同功能的类上加上相应的注解
  • 完成组件化管理的过程:
  • 1、在需要被spring管理的类上加上相应注解
  • 2、在配置文件中通过context:component-scan对所设置的包结构进行扫描,就会将加上注解的类,作为spring的组件进行加载
  • 组件:指spring中管理的bean 作为spring的组件进行加载:会自动在spring的配置文件中生成相对应的bean,这些bean的id会以类的首字母小写为值; 也可以通过@Controller(“beanId”)为自动生成的bean指定id
  • 自动装配:在需要赋值的非字面量属性上,加上@Autowired,就可以在spring容器中,通过不同的方式匹配到相对应的bean
  • @Autowired装配时,会默认使用byType的方式,此时要求spring容器中只有一个能够为其赋值可以设置@Autowired注解的required属性为 false表示不自动装配,
  • 当byType实现不了装配时,会自动切换到byName,此时要求spring容器中,有一个bean的id和属性名一致
  • 若自动装配时,匹配到多个能够复制的bean,可使用@Qualifier(value=“beanId”)指定使用的bean
  • @Autowired和@Qualifier(value=“beanId”)可以一起作用域一个带形参的方法上,此时,@Qualifier(value=“beanId”)
  • 所指定的bean作用于形参

注意:一定要用adminService接口,不然会报错,因为默认使用jdk动态代理,一定要有接口,如果没有实现类才会使用cglib动态代理

1)@Autowired:根据属性类型进行自动装配

第一步 把 service 和 dao 对象创建,在 service 和 dao 类添加创建对象注解

第二步 在 service 注入 dao 对象,在 service 类添加 dao 类型属性,在属性上面使用注解

@Service
public class UserService {
 //定义 dao 类型属性
 //不需要添加 set 方法
 //添加注入属性注解
 @Autowired
 private UserDao userDao;
 public void add() {
 System.out.println("service add.......");
 userDao.add();
 }
}

//Dao实现类
@Repository
//@Repository(value = "userDaoImpl1")
public class UserDaoImpl implements UserDao {
    @Override
    public void add() {
        System.out.println("dao add.....");
    }
}
12345678910111213141516171819202122

(2)@Qualifier:根据名称进行注入,这个@Qualifier 注解的使用,和上面@Autowired 一起使用

//定义 dao 类型属性
//不需要添加 set 方法
//添加注入属性注解
@Autowired //根据类型进行注入
//根据名称进行注入(目的在于区别同一接口下有多个实现类,根据类型就无法选择,从而出错!)
@Qualifier(value = "userDaoImpl1") 
private UserDao userDao;
1234567

(3)@Resource:可以根据类型注入,也可以根据名称注入(它属于javax包下的注解,不推荐使用!)

//@Resource //根据类型进行注入
@Resource(name = "userDaoImpl1") //根据名称进行注入
private UserDao userDao;
123

(4)@Value:注入普通类型属性

@Value(value = "abc")
private String name
12

6、完全注解开发

(1)创建配置类,替代 xml 配置文件

@Configuration //作为配置类,替代 xml 配置文件
@ComponentScan(basePackages = {"com.atguigu"})
public class SpringConfig {
    
}
12345

(2)编写测试类

@Test
public void testService2() {
 //加载配置类
 ApplicationContext context
 = new AnnotationConfigApplicationContext(SpringConfig.class);
 UserService userService = context.getBean("userService",
UserService.class);
 System.out.println(userService);
 userService.add();
}

第三章 AOP

3.1 AOP概述

  1. AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传

统 OOP(Object-Oriented Programming,面向对象编程)的补充。

面向对象 纵向继承机制

面向切面 横向抽取机制

  1. AOP编程操作的主要对象是切面(aspect),而切面用于模块化横切关注点(公共功能)

  2. 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。

  3. AOP的好处:

① 每个事物逻辑位于一个位置,代码不分散,便于维护和升级

② 业务模块更简洁,只包含核心业务代码

③ AOP图解

image-20201220142433068

image-20201220191429651

3.2 AOP(术语)

1.连接点

类里面哪些方法可以被加强,这些方法称为连接点

2.切入点

实际被真正增强的方法,称为切入点

3.通知(增强)

实际再方法中加强的逻辑代码称为通知(比如说加日志,这就是通知)

通知有多种类型

前置通知

后置通知

环绕通知 前面和后面都有

异常通知 try catch

最终通知 finally

4.横切关注点

从每个方法中抽取出来的同一类非核心业务

5.切面

封装横切关注点信息的类,每个关注点体现为一个通知方法方法

6.目标

被通知的对象

7.代理

向目标对象应用通知之后创建的代理对象

image-20201220195628751

3.3 AspectJ

3.3.1 简介

AspectJ:Java社区里最完整最流行的AOP框架

再Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。

3.3.2 再Maven中创建依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.atguigu</groupId>
   <artifactId>springday12</artifactId>
   <version>1.0-SNAPSHOT</version>
   <dependencies>
       <dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-context</artifactId>
           <version>5.1.1.RELEASE</version>
       </dependency>
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>druid</artifactId>
           <version>1.1.9</version>
       </dependency>

       <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <version>5.1.8</version>
       </dependency>
       <!-- https://mvnrepository.com/artifact/net.sourceforge.cglib/com.springsource.net.sf.cglib -->
       <dependency>
           <groupId>cglib</groupId>
           <artifactId>cglib</artifactId>
           <version>2.2.2</version>
       </dependency>
       <dependency>
           <groupId>aopalliance</groupId>
           <artifactId>aopalliance</artifactId>
           <version>1.0</version>
       </dependency>
       <dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-aspects</artifactId>
           <version>5.1.1.RELEASE</version>
       </dependency>

3.3.3 再xml文件配置Aspect

<!--开启Aspect生成代理对象-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

3.3.4 再Proxy类中加入注解

@Component
@Aspect//标注当前类为切面
public class Proxy {
    @Before("execution(* com.atguigu.spring5.User.add(..))")
    public void before() {
        System.out.println("before......");
    }


}

3.3.5 在User类中加入@Component用来标注

@Component
public class User {
    public void add() {
        System.out.println("添加");
    }
}

3.3.6 测试类进行测试

public static void main(String[] args) {
    ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("my.xml");
    User user = classPathXmlApplicationContext.getBean("user", User.class);
    user.add();
}

3.4 AOP细节

3.4.1 切入点表达式

作用

用稿表达式的方式定位一个或多个具体的连接点

语法细节

1)切入点表达式的语法格式

execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))

2)举例说明

表达式 execution(***** com.atguigu.spring.ArithmeticCalculator.*(..))
含义 ArithmeticCalculator接口中声明的所有方法。 第一个“”代表任意修饰符及任意返回值。 第二个“”代表任意方法。 “..”匹配任意数量、任意类型的参数。 若目标类、接口与该切面类在同一个包中可以省略包名。
表达式 execution(public * ArithmeticCalculator.*(..))
含义 ArithmeticCalculator接口的所有公有方法
表达式 execution(public double ArithmeticCalculator.*(..))
含义 ArithmeticCalculator接口中返回double类型数值的方法
表达式 execution(public double ArithmeticCalculator.*(double, ..))
含义 第一个参数为double类型的方法。 “..” 匹配任意数量、任意类型的参数。
表达式 execution(public double ArithmeticCalculator.*(double, double))
含义 参数类型为double,double类型的方法

3)在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。

表达式 execution (* .add(int,..)) || execution( *.sub(int,..))
含义 任意类中第一个参数为int类型的add方法或sub方法
表达式 !execution (* *.add(int,..))
含义 匹配不是任意类中第一个参数为int类型的add方法
切入点表达式应用到实际的切面类中

image-20201220201537163

3.4.2 当前连接点细节

概述

切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中。

JoinPoint

getArgs() 获取实际参数数组

getSignature() 获取签名信息对象,可以进一步获取方法名

3.4.3 通知

概述
  1. 在具体的连接点上要执行的操作。

  2. 一个切面可以包括一个或者多个通知。

  3. 通知所使用的注解的值往往是切入点表达式。

前置通知

1)前置通知:在方法执行之前执行的通知

2)使用@Before注解

后置通知(无论什么样都会执行)
  1. 后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候

  2. 使用@After注解

返回通知
  1. 返回通知:无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。

  2. 使用@AfterReturning注解,在返回通知中访问连接点的返回值

 ①在返回通知中,只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
 
 ②必须在通知方法的签名中添加一个同名参数。在运行时Spring AOP会通过这个参数传递返回值
 
 ③原始的切点表达式需要出现在pointcut属性中
@AfterReturning(value = "execution(* com.atguigu.spring5.User.div(..))",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
    String s = result.toString();
    System.out.println(s+"在哪里执行");

}
异常通知
@AfterThrowing(value = "execution(* com.atguigu.spring5.User.div(..))",throwing = "throw1")
public void afterThrow(Exception throw1){
    String s = throw1.toString();
    System.out.println(s);


}*/
环绕通知
@Around(value = "execution(* com.atguigu.spring5.User.div(..))")
public Object around(ProceedingJoinPoint joinPoint) {
    try {
        System.out.println("前置对象");
        Object proceed = joinPoint.proceed();
        System.out.println("返回对象");
        return proceed;

    } catch (Throwable throwable) {
        System.out.println("异常对象");
    }finally {
        System.out.println("后置对象");
    }

    return -1;
}

3.4.4 重用切入点的定义

  1. 在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。

  2. 在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。

  3. 切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。

  4. 其他通知可以通过方法名称引入该切入点

image-20201220222556416

3.4.5 指定切面的优先级

  1. 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。

  2. 切面的优先级可以通过实现Ordered接口或利用@Order注解指定。

  3. 实现Ordered接口,getOrder()方法的返回值越小,优先级越高。

  4. 若使用@Order注解,序号出现在注解中

img

img

第四章 以XML方式配置切面

4.1 概述

 除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。

 正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。

4.2 配置细节

 在bean配置文件中,所有的Spring AOP配置都必须定义在<aop:config>元素内部。对于每个切面而言,都要创建一个<aop:aspect>元素来为具体的切面实现引用后端bean实例。
切面bean必须有一个标识符,供<aop:aspect>元素引用。

4.3 声明切入点

  1. 切入点使用aop:pointcut元素声明。

  2. 切入点必须定义在aop:aspect元素下,或者直接定义在aop:config元素下。

 ① 定义在<aop:aspect>元素下:只对当前切面有效
 
 ② 定义在<aop:config>元素下:对所有切面都有效
  1. 基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。

4.4 声明通知

  1. 在aop名称空间中,每种通知类型都对应一个特定的XML元素。

  2. 通知元素需要使用来引用切入点,或用直接嵌入切入点表达式。

  3. method属性指定切面类中通知方法的名称

<context:component-scan base-package="com.atguigu.spring5"/>
<aop:config>
    
    <aop:pointcut id="cut" expression="execution(* com.atguigu.spring5.User.*(..))"/>

    <aop:aspect ref="proxy">
        <!--<aop:before method="around" pointcut="execution(* com.atguigu.spring5.User.*(..))"/>-->
        <aop:before method="around" pointcut-ref="cut"></aop:before>
    </aop:aspect>


</aop:config>

第五章 JdbcTemplate

5.1 概述

为了使JDBC更加易于使用,Spring在JDBCAPI上定义了一个抽象层,以此建立一个JDBC存取框架

作为Spring JDBC框架的核心,JDBC模板的设计目的是为不同类型的JDBC操作提供模板方法,通过这种方式,可以在尽可能保留灵活性的情况下,将数据库存取的工作量降到最低。

 可以将Spring的JdbcTemplate看作是一个小型的轻量级持久化层框架,和我们之前使用过的DBUtils风格非常接近。

5.2 环境准备

5.2.1 导入JAR包

1)IOC容器所需要的JAR包

1)    IOC容器所需要的JAR包

commons-logging-1.1.1.jar

      spring-beans-4.0.0.RELEASE.jar

spring-context-4.0.0.RELEASE.jar

spring-core-4.0.0.RELEASE.jar

spring-expression-4.0.0.RELEASE.jar

  1. JdbcTemplate所需要的JAR包

    spring-jdbc-4.0.0.RELEASE.jar
    

spring-orm-4.0.0.RELEASE.jar

spring-tx-4.0.0.RELEASE.jar

  1. 数据库驱动和数据源

    druid-1.1.9.jar
    
    mysql-connector-java-5.1.7-bin.jar
    

maven环境搭建

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.atguigu</groupId>
    <artifactId>springday12</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.9</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.8</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/net.sourceforge.cglib/com.springsource.net.sf.cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alexkasko.springjdbc/springjdbc-iterable -->
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.0.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>4.0.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>4.0.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>

5.2.2 配置文件

    <context:component-scan base-package="com.atguigu.spring5" use-default-filters="true">

    </context:component-scan>
    <!--开启Aspect生成代理对象-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    <context:property-placeholder location="db.properties"></context:property-placeholder>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="password" value="${jdbc.password}"/>

    </bean>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

</beans>

5.3 持久化操作

5.3.1 增删改

jdbcTemplate.update(String,Object...)
 bean.update("update t_book set `name` = '数据' where id = ? ", 5);

5.3.2 批量增删改

JdbcTemplate.batchUpdate(String, List<Object[]>)

Object[]封装了SQL语句每一次执行时所需要的参数

      List集合封装了SQL语句多次执行时的所有参数
   ArrayList<Object[]> objectsArrayList = new ArrayList<>();
        objectsArrayList.add(new Object[] {"我是人123123","54","小迪迪","36","1000123","小迪迪最帅"});
        objectsArrayList.add(new Object[] {"我是人1123213",54,"小迪迪","36",1000,"小迪迪最帅"});
        objectsArrayList.add(new Object[] {"我是人2",54,"小迪迪","36",1000,"小迪迪最帅"});
        objectsArrayList.add(new Object[] {"我是人3",54,"小迪迪","36",1000,"小迪迪最帅"});
        objectsArrayList.add(new Object[] {"我是人4",54,"小迪迪","36",1000,"小迪迪最帅"});
        bean.batchUpdate("insert t_book values (null,?,?,?,?,?,?)",objectsArrayList);

5.3.3 查询单行

	JdbcTemplate.queryForObject(String, RowMapper<Department>, Object...)
String  sql = "select eid,ename,age,sex from emp where eid = ?";

RowMapper<Emp> rowMapper = new BeanPropertyRowMapper<>(Emp.class);

Emp emp = jdbcTemplate.queryForObject(sql,new Object[] {7} ,rowMapper)

5.3.4 查询多行

String sql = "select eid,ename,age,sex from emp";
RowMapper<Emp> rowMapper = new BeanPropertyRowMapper<>(Emp.class);
List<Emp> list = jdbcTemplate.query(sql,rowMapper);
for(Emp emp : list){
	System.out.println(emp);
}

5.3.5 查询单个值

 String sql = "select count(*) from t_book";

        Integer integer = bean.queryForObject(sql, Integer.class);
        System.out.println(integer);

5.4 申明式事务管理

5.4.1 事务管理的实现

<?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"
       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">

    <!--配置哪些包或被解析-->
    <context:component-scan base-package="com.atguigu.book"/>
    <!--配置文件-->
    <context:property-placeholder location="db.properties"></context:property-placeholder>
    <!--根据配置文件得到dataSource-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="password" value="${jdbc.password}"/>

    </bean>
    <!--根据dataSource获得一个持久层框架-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" >
        <property name="dataSource" ref="dataSource"/>
    </bean>


    <!--配置事务管理器-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>


    <!--开启注解驱动,即对事务有关的注解进行扫描,解析含义并执行功能-->
    <!--transaction-manager和上面的事务管理器id需要进行匹配,来指定事务需要的事务管理器-->
   <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>


</beans>
@Service
public class BookServiceImpl implements BookService {

   @Autowired
   private BookDao dao;
//表示在当前方法中启动事务,只要出现异常就回滚
   @Transactional
   public void buyBook(String bid, String uid) {
      Integer price = dao.selectPrice(bid);
      dao.updateSt(bid);
      dao.updateBalance(uid, price);
   }
}

5.4.2 事务的传播行为

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。

image-20201225141750386

事务的传播属性可以在@Transactional注解的propagation属性中定义.

propagtion默认为用使用调用者的事务,不使用自己的事务

但是用propagtion.REQUIRES_NEW使用的是自己的事务处理方式,调用者的处理暂时挂起

image-20201225141917552

image-20201225141923328

也可以通过注解的方式进行配置

	@Transactional(propagation = Propagation.REQUIRES_NEW)
	public void buyBook(String bid, String uid) {
		Integer price = dao.selectPrice(bid);
		dao.updateSt(bid);
		dao.updateBalance(uid, price);
	}
}

5.4.3 事务的隔离级别

1 数据库事务并发问题

假设现在有两个事务:Transaction01和Transaction02并发执行。

1) 脏读

 ①Transaction01将某条记录的AGE值从20修改为30。

 ②Transaction02读取了Transaction01更新后的值:30。

 ③Transaction01回滚,AGE值恢复到了20。

 ④Transaction02读取到的30就是一个无效的值。

2) 不可重复读

 ①Transaction01读取了AGE值为20。

 ②Transaction02将AGE值修改为30。

 ③Transaction01再次读取AGE值为30,和第一次读取不一致。

3) 幻读

 ①Transaction01读取了STUDENT表中的一部分数据。

 ②Transaction02向STUDENT表中插入了新的行。

 ③Transaction01读取了STUDENT表时,多出了一些行。
2 隔离级别

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

  1. 读未提交:READ UNCOMMITTED

允许Transaction01读取Transaction02未提交的修改。

  1. 读已提交:READ COMMITTED

    要求Transaction01只能读取Transaction02已提交的修改。

  2. 可重复读:REPEATABLE READ

    确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。

  3. 串行化:SERIALIZABLE

    确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

  4. 各个隔离级别解决并发问题的能力见下表

脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
  1. 各种数据库产品对事务隔离级别的支持程度
Oracle MySQL
READ UNCOMMITTED ×
READ COMMITTED √(默认)
REPEATABLE READ × √(默认)
3 在Spring中指定事务隔离级别
  1. 注解

用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别

    @Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED)
   public void buyBook(String bid, String uid) {
      Integer price = dao.selectPrice(bid);
      dao.updateSt(bid);
      dao.updateBalance(uid, price);
   }
}
  1. XML

在Spring 2.x事务通知中,可以在tx:method元素中指定隔离级别

img

5.4.4 触发事务回滚的异常

1 默认情况

捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。

2 配置
  1. 注解@Transactional 注解
 ① rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个
 
 ② noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个

img

  1. XML

在Spring 2.x事务通知中,可以在tx:method元素中指定回滚规则。如果有不止一种异常则用逗号分隔。

img

5.4.6 事务的超时和只读属性

1 简介

由于事务可以在行和表上获得锁,因此长事务会占用资源,并对整体性能产生影响。

如果一个事务只读取数据但不做修改,数据库引擎可以对这个事务进行优化。

 超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。

 只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。(如果有写入的操作就会有数据安全性问题)
2 设置

1)注解

@Transaction注解
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED,rollbackFor = Exception.class,timeout = 3,readOnly = true)

2)XML

在Spring 2.x事务通知中,超时和只读属性可以在tx:method元素中进行指定

img

5.4.7 基于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:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--配置哪些包或被解析-->
    <context:component-scan base-package="com.atguigu.book"/>
    <!--配置文件-->
    <context:property-placeholder location="db.properties"></context:property-placeholder>
    <!--根据配置文件得到dataSource-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="password" value="${jdbc.password}"/>

    </bean>
    <!--根据dataSource获得一个持久层框架-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" >
        <property name="dataSource" ref="dataSource"/>
    </bean>


    <!--配置事务管理器-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>


    <!--配置一个切面,用于进行事务-->
    <aop:config>
        <aop:pointcut id="myPointcut" expression="execution(* com.atguigu.book.controller.BookController.buyBook(..))"/>
        <aop:advisor advice-ref="myAdvice" pointcut-ref="myPointcut"/>
        <aop:advisor advice-ref="myAdvice" pointcut="execution(* com.atguigu.book.service.BookService.buyBook(..))"/>


    </aop:config>

    <tx:advice transaction-manager="dataSourceTransactionManager" id="myAdvice">
     <!--<tx:attributes>
            <tx:method name="buyBook*"></tx:method>
        </tx:attributes>-->
        

    </tx:advice>

注意如果

  <tx:advice transaction-manager="dataSourceTransactionManager" id="myAdvice">
     <!--<tx:attributes>
            <tx:method name="buyBook*"></tx:method>
            <tx:method name="find*" read-only="true"/>
			<tx:method name="get*" read-only="true"/>

        </tx:attributes>-->
        

    </tx:advice>

表示配置每一个切入点

不配置tx:method会不用事务

<tx:method name=”*"/>来使用原有事务

springMVC

0 总结

image-20210713171140038

第一章 SpringMVC概述

1.1 SpringMVC概述

1) Spring 为展现层提供的基于 MVC 设计理念的优秀的 Web 框架,是目前最主流的MVC 框架之一

2)Spring3.0 后全面超越 Struts2,成为最优秀的 MVC 框架。

3)Spring MVC 通过一套 MVC 注解,让 POJO 成为处理请求的控制器,而无须实现任何接口。

4)支持 REST 风格的 URL 请求。 Restful

5)采用了松散耦合可插拔组件结构,比其他 MVC 框架更具扩展性和灵活性。

1.2 SpringMVC是什么

一种轻量级的、基于MVC的Web层应用框架。偏前端而不是基于业务逻辑层。Spring框架的一个后续产品。

2)Spring框架结构图(新版本):

image-20201226123630269

1.3 SpringMVC能干什么

1) 天生与Spring框架集成,如:(IOC,AOP)

2) 支持Restful风格

3) 进行更简洁的Web层开发

4) 支持灵活的URL到页面控制器的映射

5) 非常容易与其他视图技术集成,如:Velocity、FreeMarker等等

6) 因为模型数据不存放在特定的API里,而是放在一个Model里(Map数据结构实现,因此很容易被其他框架使用)

7) 非常灵活的数据验证、格式化和数据绑定机制、能使用任何对象进行数据绑定,不必实现特定框架的API

8) 更加简单、强大的异常处理

9) 对静态资源的支持

10) 支持灵活的本地化、主题等解析

1.4 SpringMVC怎么玩

1) 将Web层进行了职责解耦,基于请求-响应模型

2) 常用主要组件

DispatcherServlet:前端控制器

Controller:处理器/页面控制器,做的是MVC中的C的事情,但控制逻辑转移到前端控制器了,用于对请求进行处理

HandlerMapping :请求映射到处理器,找谁来处理,如果映射成功返回一个HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象、多个HandlerInterceptor拦截器对象)

View Resolver : 视图解析器,找谁来处理返回的页面。把逻辑视图解析为具体的View,进行这种策略模式,很容易更换其他视图技术;

n 如InternalResourceViewResolver将逻辑视图名映射为JSP视图

LocalResolver:本地化、国际化

MultipartResolver:文件上传解析器

HandlerExceptionResolver:异常处理器

1.5 永远的HelloWorld

新建web工程,加入jar包

image-20201226123737325

1.5.1 配置maven依赖

<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.1.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.9</version>
  </dependency>

  <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.8</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/net.sourceforge.cglib/com.springsource.net.sf.cglib -->
  <dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
  </dependency>
  <dependency>
    <groupId>aopalliance</groupId>
    <artifactId>aopalliance</artifactId>
    <version>1.0</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.1.1.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.3</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.1.1.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.1.1.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.10</version>
  </dependency>




</dependencies>

1.5.2 在web.xml配置DispatcherServlet

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <servlet>
    <servlet-name>springDispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--可以不配置,默认使用/WEB-INF/<servlet-name>-servlet.xml-->
<!--    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>-->
  </servlet>
  
  <servlet-mapping>
    <servlet-name>springDispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>
	①解释配置文件的名称定义规范:
 
							实际上也可以不通过contextConfigLocation来配置SpringMVC的配置文件,而使用默认的默认的配置文件为:/WEB-INF/<servlet-name>-servlet.xml

1.5.3 加入SpringMVC的配置文件

如果配置了init-param参数的话 写的配置文件就是springmvc.xml

如果没有配置,在WEB-INF下面写springDiapatcherServlet-servlet.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"
       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.controller"/>
    <!--配置映射解析器:如何将控制器返回的结果字符串,转换成一个物理的视图文件-->
    <bean id="internalResourceViewResolver"
          class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>

    </bean>



</beans>

1.5.4 创建一个入口页面,index.jsp

<a href="${pageContext.request.contextPath }/hello">Hello World</a>

1.5.5 编写处理请求的处理器,并标识为处理器

package com.atguigu.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
/**
 * 映射请求的名称:用于客户端请求;类似Struts2中action映射配置的action名称
 * 1. 使用 @RequestMapping 注解来映射请求的 URL
 * 2. 返回值会通过视图解析器解析为实际的物理视图, 对于 InternalResourceViewResolver 视图解析器, 
 * 会做如下的解析:
 *                 通过 prefix + returnVal + suffix 这样的方式得到实际的物理视图, 然后做转发操作.
 *                 /WEB-INF/views/success.jsp
 */

public class myController {
    @RequestMapping("/hello")
    public String hello() {
        System.out.println("helloworld");
        return "success";
    }
}

1.5.6 编写视图

	/WEB-INF/views/success.jsp

1.6 HelloWorld深度解析

1.6.1 HelloWorld请求流程图解

image-20201226143035276

1.6.2 一般请求恶的映射路径名称和处理请求的方法名称最好一致(实质上方法名称任意)

@RequestMapping(value="/helloworld",method=RequestMethod.GET)
public String helloworld(){
//public String abc123(){
System.out.println("hello,world");
return "success";
}

注意/WEB-INF/views/不要少了/

1.6.3 处理请求方式有哪几种

public enum RequestMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}

1.6.4 @REquestMapping可以应用在什么地方

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {…}

1.6.5 流程分析

image-20201226143532686

基本步骤:

① 客户端请求提交到DispatcherServlet

② 由DispatcherServlet控制器查询一个或多个HandlerMapping,找到处理请求的Controller

③ DispatcherServlet将请求提交到Controller(也称为Handler)

④ Controller调用业务逻辑处理后,返回ModelAndView

⑤ DispatcherServlet查询一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图

⑥ 视图负责将结果显示到客户端

第二章 @RequestMapping注解

2.1 @RequestMapping映射请求注解

2.1.1 @RequestMapping概念

1) SpringMVC使用@RequestMapping注解为控制器指定可以处理哪些 URL 请求

2) 在控制器的类定义及方法定义处都可标注 @RequestMapping

标记在类上:提供初步的请求映射信息。相对于 WEB 应用的根目录

标记在方法上:提供进一步的细分映射信息。相对于标记在类上的 URL。

3) 若类上未标注 @RequestMapping,则方法处标记的 URL 相对于 WEB 应用的根目录

4) 作用:DispatcherServlet 截获请求后,就通过控制器上 @RequestMapping 提供的映射信息确定请求所对应的处理方法。

2.1.2 @RequestMapping源码参考

package org.springframework.web.bind.annotation;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String[] value() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}

2.2 RequestMapping可标注的位置

<html>
<body>
<a href="xiaodidi/hello">helloWorld</a>
</body>
</html>
package com.atguigu.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/xiaodidi")
public class myController {
    //param属性表示参数一定要有username属性,同时password值是123456,注意不能有空格,不能存在xiao这个参数
    //username  !username username=4 username!=admin

    @RequestMapping(value = "/hello",params = {"username","password=123456","!xiao"})
    public String hello(String username,String password) {
        System.out.println(username);
        System.out.println(password);
        System.out.println("helloworld");
        return "success";
    }
}

浏览器的位置http://localhost:8080/springMVC11/xiaodidi/hello

相当于多了路经

2.3 RequestMapping映射请求方式

2.3.1 标准的HTTP请求报头

image-20201226145511410

2.3.2 映射请求参数,请求方法或请求头

1)@RequestMapping 除了可以使用请求 URL 映射请求外,还可以使用请求方法、请求参数及请求头映射请求

2)@RequestMapping 的 value【重点】、method【重点】、params【了解】 及 heads【了解】 分别表示请求 URL、请求方法、请求参数及请求头的映射条件,他们之间是与的关系,联合使用多个条件可让请求映射更加精确化。

3)params 和 headers支持简单的表达式:

//param属性表示参数一定要有username属性,同时password值是123456,注意不能有空格,不能存在xiao这个参数
//username  !username username=4 username!=admin

@RequestMapping(value = "/hello",params = {"username","password=123456","!xiao"})

2.3.3 实验代码

1)定义控制器方法

    @RequestMapping(value = "/hello",method = RequestMethod.POST)

image-20201226145901161

除了通过请求URL映射请求外,还可以使用请求方法,请求参数及请求头映射请求,比如说下面就是中文,如果不是就报404错误

@RequestMapping(value = "/hello",headers = {"Accept-Language=zh-CN,zh;q=0.8"})
    public String hello(String username,String password) {
        System.out.println(username);
        System.out.println(password);
        System.out.println("helloworld");
        return "success";
    }

2.4 RequestMapping支持Ant路经分隔

2.4.1 Ant

1)Ant风格资源地址支持3种匹配符:【了解】

?:匹配文件名中的一个字符

*:匹配文件名中的任意字符

** : ** 匹配多层路经

2)@RequestMapping还支持Ant风格的URL

/user/*/createUser
匹配 /user/aaa/createUser、/user/bbb/createUser 等 URL
/user/**/createUser
匹配 /user/createUser、/user/aaa/bbb/createUser 等 URL
/user/createUser??
匹配 /user/createUseraa、/user/createUserbb 等 URL

2.4.2 实验代码

1)定义控制器方法

//Ant 风格资源地址支持 3 种匹配符
//@RequestMapping(value="/testAntPath/*/abc")
//@RequestMapping(value="/testAntPath/**/abc")
@RequestMapping(value="/testAntPath/abc??")
public String testAntPath(){
System.out.println("testAntPath...");
return "success";
}

2)页面链接

<!-- Ant 风格资源地址支持 3 种匹配符 -->
<a href="springmvc/testAntPath/*/abc">testAntPath</a>
<a href="springmvc/testAntPath/xxx/yyy/abc">testAntPath</a>
<a href="springmvc/testAntPath/abcxx">testAntPath</a>

2.5 RequestMapping映射请求占位符PathVariable注解

2.5.1 @PathVariable

带占位符的URL 是 Spring3.0 新增的功能,该功能在 SpringMVC 向 REST 目标挺进发展过程中具有里程碑的意义

通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中

URL 中的 {xxx} 占位符可以通过 @PathVariable("xxx") 绑定到操作方法的入参中。

2.5.2 实验代码

1)定义控制器的方法

//@PathVariable 注解可以将请求URL路径中的请求参数,传递到处理请求方法的入参中
浏览器的请求:  testPathVariable/1001
@RequestMapping(value="/testPathVariable/{id}")
public String testPathVariable(@PathVariable("id") Integer id){
System.out.println("testPathVariable...id="+id);
return "success";
}

第三章 REST

3.1 参考资料

1)理解本真的REST架构风格: http://kb.cnblogs.com/page/186516/

2)REST: http://www.infoq.com/cn/articles/rest-introduction

3.2 REST是什么?

1)REST:即Representational State Transfer(资源)表现层状态转换。是目前最流行的一种互联网软件架构。他结构清洗,符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用

3.3 HiddenHttpMethodFilter过滤器源码分析

1)为什么请求隐含参数名称必须叫做"_method"

image-20210101121240645

  1. hiddenHttpMethodFilter 的处理过程

image-20210101121319662

3.4 实验代码

1)配置HiddenHttpMethodFilter过滤器

<!-- 支持REST风格的过滤器:可以将POST请求转换为PUT或DELETE请求 -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

2)代码

/**
 * 1.测试REST风格的  GET,POST,PUT,DELETE 操作
 * 以CRUD为例:
 * 新增: /order POST
 * 修改: /order/1 PUT           update?id=1
 * 获取: /order/1 GET                get?id=1
 * 删除: /order/1 DELETE        delete?id=1
 
 * 2.如何发送PUT请求或DELETE请求?
 * ①.配置HiddenHttpMethodFilter
 * ②.需要发送POST请求
 * ③.需要在发送POST请求时携带一个 name="_method"的隐含域,值为PUT或DELETE
 
 * 3.在SpringMVC的目标方法中如何得到id值呢?
 *   使用@PathVariable注解
 */
@RequestMapping(value="/testRESTGet/{id}",method=RequestMethod.GET)
public String testRESTGet(@PathVariable(value="id") Integer id){
System.out.println("testRESTGet id="+id);
return "success";
}
 
@RequestMapping(value="/testRESTPost",method=RequestMethod.POST)
public String testRESTPost(){
System.out.println("testRESTPost");
return "success";
}
 
@RequestMapping(value="/testRESTPut/{id}",method=RequestMethod.PUT)
public String testRESTPut(@PathVariable("id") Integer id){
System.out.println("testRESTPut id="+id);
return "success";
}
 
@RequestMapping(value="/testRESTDelete/{id}",method=RequestMethod.DELETE)
public String testRESTDelete(@PathVariable("id") Integer id){
System.out.println("testRESTDelete id="+id);
return "success";
}

3)请求链接

<!-- 实验1 测试 REST风格 GET 请求 -->
<a href="springmvc/testRESTGet/1">testREST GET</a><br/><br/>
 
<!-- 实验2 测试 REST风格 POST 请求 -->
<form action="springmvc/testRESTPost" method="POST">
<input type="submit" value="testRESTPost">
</form>
 
<!-- 实验3 测试 REST风格 PUT 请求 -->
<form action="springmvc/testRESTPut/1" method="POST">
<input type="hidden" name="_method" value="PUT">
<input type="submit" value="testRESTPut">
</form>
 
<!-- 实验4 测试 REST风格 DELETE 请求 -->
<form action="springmvc/testRESTDelete/1" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="submit" value="testRESTDelete">
</form>

注意:上面的方法适用于tomcat7,如果当前服务器是tomcat8那么

需要在返回页面上加一个isErrorpage=“true”

<%--
  Created by IntelliJ IDEA.
  User: 10185
  Date: 2020/12/26
  Time: 12:53
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    我回来了
</body>
</html>

第四章 处理请求参数

请求数据 : 请求参数 cookie信息 请求头信息…..

JavaWEB : HttpServletRequest

    Request.getParameter(参数名); Request.getParameterMap();

          Request.getCookies();

          Request.getHeader();   

4.1 请求处理方法签名

1) Spring MVC 通过分析处理方法的签名(方法名+ 参数列表),HTTP请求信息绑定到处理方法的相应形参中。

2) Spring MVC 对控制器处理方法签名的限制是很宽松的,几乎可以按喜欢的任何方式对方法进行签名。

3) 必要时可以对方法及方法入参标注相应的注解( @PathVariable 、@RequestParam、@RequestHeader 等)、

4) Spring MVC 框架会将 HTTP 请求的信息绑定到相应的方法入参中,并根据方法的返回值类型做出相应的后续处理。

4.2 @RequestParam注解

1)在处理方法入参处使用 @RequestParam 可以把请求参数传递给请求方法

2)value:参数名

3)required:是否必须。默认为 true, 表示请求参数中必须包含对应的参数,若不存在,将抛出异常

4)defaultValue: 默认值,当没有传递参数时使用该值

4.2.1 实验代码

通过@RequsetParam(“username”)函数来进行赋值

jsp页面

<form action="xiaodidi/hello1" method="post">
     username:<input type="text" name="username">
     password:<input type="text" name="password">
    <input type="submit">
 @RequestMapping(value = "/hello1", method = RequestMethod.POST)
    public String hello1(@RequestParam("username") String username,@RequestParam("password") String password) {
        System.out.println("username="+username+",password="+password);
        return "success";
    }
}

如果没有username这个参数,如果required=true,那么如果没有username这个值,就会报错,单数如果required=false,那么没有也没有关系,还有defaultvalue属性,如果没有这个属性那么就会赋默认值,此时,required=true和false都可以

jsp代码

<form action="xiaodidi/hello1" method="post">
     password:<input type="text" name="password">
    <input type="submit">

服务器代码

@RequestMapping(value = "/hello1", method = RequestMethod.POST)
public String hello1(@RequestParam(value = "username",required = false,defaultValue = "小迪迪") String username,@RequestParam("password") String password) {
    System.out.println("username="+username+",password="+password);
    return "success";
}

结果

username=小迪迪,password=123123

4.3 @RequestHeader注解和@CookieValue注解

1)使用@RequestHeader绑定请求报头的属性值

2)请求头包含了若干个属性,服务器可据此获知客户端的消息,通过@RequestHeader即可将请求头中的属性值绑定到处理方法的入参中

@RequestMapping(value = "/hello1", method = RequestMethod.POST)
public String hello1(@RequestParam(value = "username",required = false) String username,@RequestParam("password") String password,
                     @RequestHeader("Accept-Language") String account,
                     @CookieValue(value = "sessionId",required = false) String sessionId) {
    System.out.println("username="+username+",password="+password);
    System.out.println("Accept"+account);
    System.out.println("sessionId"+sessionId);
    return "success";
}

4.4 使用POJO作为参数

1) 使用 POJO 对象绑定请求参数值

2) Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配,自动为该对象填充属性值支持级联属性。如:dept.deptId、dept.address.tel 等

4.4.1 实验代码

index.jsp代码

<form action="xiaodidi/hello1" method="post">
     password:<input type="text" name="password">
     住址ID:<input type="text" name="address.addressId">
     住址name:<input type="text" name="address.addressName">
    <input type="submit">

User类

public class User {
    private String username;
    private String password;
    private Address address;

Address类

public class Address {
    private String addressId;
    private String addressName;

控制类

@RequestMapping(value = "/hello1", method = RequestMethod.POST)
public String hello1(User user) {
    System.out.println(user);
    return "success";
}

注意:可以使用POJOi获取客户端的数据,要求实体类对象中的属性名一定要和页面中表单元素的name属性值一致,求支持级联关系

4.5 使用Servlet原生API作为参数

1)MVC的Handle方法可以接受哪些ServletAPI类型的参数

1)HttpServletRequest

2)HttpServetResponse

3)HttpSession

4)java.security.Principal

5)Locale 国际化有关的区域信息对象

6)InputStream:ServletInputStream inputStream = request.getInputStream();

7)OutputStream:ServletOutputStream outputStream = response.getOutputStream();

8)Reader request.getReader();

9)Writer response.getWriter();

    @RequestMapping(value = "/hello1", method = RequestMethod.POST)
    public String hello1(User user, HttpServletRequest request) {
        String password = request.getParameter("password");
        System.out.println(password);
        System.out.println(user);
        return "success";
    }

第五章 处理响应数据

5.1 处理post响应请求乱码

 <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

注意:一定要在第一行进行配置,因为xml中的filter是从上到下进行过滤的操作的

5.2 SpringMVC输出模型数据概述

5.2.1 提供了以下几种途径输出模型数据

(1)ModelAndView:处理方法返回值类型为ModelAndView时,方法体即可通过该对象添加模型数据

(2)Map或Model:入参为org.springframework.ui.Model、org.springframework.ui.ModelMap或java.util.Map时,处理方法返回时,Map中的数据会自动添加到模型中

用map来进行页面的显示

'’

@RequestMapping(value = "/hello1", method = RequestMethod.POST)
public String hello1(User user, HttpServletResponse response, HttpServletRequest request, Map map) throws IOException {
    String password = request.getParameter("password");
    map.put("username1", "我可以吗");
    return "success";
}

image-20210113205536805

用model来进行页面的展示
@RequestMapping(value = "/hello1", method = RequestMethod.POST)
public String hello1(User user, HttpServletResponse response, HttpServletRequest request, Model model) throws IOException {
    String password = request.getParameter("password");
    model.addAttribute("username1", "我也可以吗");
    return "success";
}
用modelMap进行页面的展示
@RequestMapping(value = "/hello1", method = RequestMethod.POST)
public String hello1(User user, HttpServletResponse response, HttpServletRequest request, ModelMap modelMap) throws IOException {
    String password = request.getParameter("password");
    modelMap.addAttribute("username1", "我也是可以吗");
    return "success";
}

注意Map,Model,ModelMap,最终都是BindingAwareModelMap在工作:

相当于给BindingAwareModelMap中保存的东西都会放在请求域之中

5.3 处理模型数据之ModelAndView

5.3.1 ModelAndView介绍

控制器处理方法的返回值如果为ModelAndView,则其既包含视图信息,也包含模型数据信息

1)两个重要的成员变量

private Object view;视图信息

private ModelMap model 模型数据

添加模型数据:

MoelAndView addObject(String attributeName, Object attributeValue) 设置模型数据

ModelAndView addAllObject(Map<String, ?> modelMap)

4)设置视图:

void setView(View view) 设置视图对象

void setViewName(String viewName) 设置视图名字

5)获取模型数据

protected Map<String, Object> getModelInternal() 获取模型数据

public ModelMap getModelMap()

public Map<String, Object> getModel()

@RequestMapping(value = "/hello1", method = RequestMethod.POST)
public ModelAndView hello1(User user, HttpServletResponse response, HttpServletRequest request, ModelMap modelMap) throws IOException {
    ModelAndView success = new ModelAndView("success");
    success.addObject("username1","我是ModelAndView");//实际上存放到request域中
    return success;

}

或者

   @RequestMapping(value = "/hello1", method = RequestMethod.POST)
    public ModelAndView hello1(User user, HttpServletResponse response, HttpServletRequest request, ModelMap modelMap) throws IOException {
        ModelAndView success = new ModelAndView();
        success.setViewName("success");
        success.addObject("username1","我是ModelAndView另一种方式");
        return success;

    }
}

5.4 使用SessionAttributes(最好不用,用原生API)

@SessionAttributes("username1")//表示将request里面保存的name为username1的数据复制一份给session
或者
@SessionAttributes(class="")
@SessionAttributes(types = String.class)//表示所有value类型为value类型的都被保存到session域中

5.5 @ModelAttribute(现在用myBatis框架)

该注解如果加到变量上面就可以不用自己创建一个对象了,就可以用以前创建的一个对象来进行赋值,比如:

会提前执行这个带有@ModelAttribute注解的方法

@ModelAttribute
public void myModelAttribute(@RequestParam(value = "id",required = false) Integer id,Model model) {
    if (id != null) {
        Employee employee = employeeDao.get(id);
        model.addAttribute("employee", employee);
    }

}
@RequestMapping("/updateBook")

public String updateBook(@ModelAttribute("haha") Book book) {
	System.out.println("页面要提交过来的图书信息"+book);
	return "success";
}
/**
1)SpringMVC要封装请求参数的Book对象不应该是自己new出来的
	而应该是【从数据库中】拿到的准备好的对象
2)再来使用这个对象封装请求参数
如果没有@ModelAttribute注解,那么SpringMVC会自动创建一个book对象,然后用set语句把页面上request传进来的所有参数进行赋值,如果传进来的值为null,那么就为空了,但是用@ModelAttribute注解不会为空,会用数据库原有的对象进行set,进行更改原有的值,这样就可以防止出现故障
*/

第六章 视图解析

6.1 forward和redirect前缀

通过SpringMVC来实现转发和重定向。

  • 直接 return “success”,会走视图解析器进行拼串
  • 转发:return “forward:/succes.jsp”;直接写绝对路径,/表示当前项目下,不走视图解析器
  • 重定向:return “redirect:/success.jsp”;不走视图解析器
@Controller
public class ResultSpringMVC {
   @RequestMapping("/hello01")
   public String test1(){
       //转发
       //会走视图解析器
       return "success";
  }

   @RequestMapping("/hello02")
   public String test2(){
       //转发二
       //不走视图解析器
       return "forward:/success.jsp";
  }

   @RequestMapping("/hello03")
   public String test3(){
       //重定向
       //不走视图解析器
       return "redirect:/success.jsp";
  }
}

使用原生的ServletAPI时要注意,/路径需要加上项目名才能成功

   @RequestMapping("/result/t2")
   public void test2(HttpServletRequest req, HttpServletResponse resp) throwsIOException {	
       //重定向
       resp.sendRedirect("/index.jsp");
  }

   @RequestMapping("/result/t3")
   public void test3(HttpServletRequest req, HttpServletResponse resp) throwsException {
       //转发
       req.setAttribute("msg","/result/t3");
       req.getRequestDispatcher("/WEB-INF/jsp/test.jsp").forward(req,resp);
  }

6.2 jstlView

导包导入了jstl的时候会自动创建为一个jstlView;可以快速方便的支持国际化功能;

可以支持快速国际化;

javaWeb国际化步骤;

  1. 得得到一个Locale对象;
  2. 使用ResourceBundle绑定国际化资源文件
  3. 使用ResourceBundle.getString(“key”);获取到国际化配置文件中的值
  4. web页面的国际化,fmt标签库来做
    • <fmt:setLocale>
    • <fmt:setBundle>
    • <fmt:message>

有了JstlView以后

  1. 让Spring管理国际化资源就行

    1605114156138

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/pages/"></property>
            <property name="suffix" value=".jsp"></property>
            <property name="viewClass" value="org.springframework.web.servlet.view.JstlView">			
            </property>
    </bean>
    
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
            <property name="basename" value="i18n"></property>
    </bean>
    
  2. 直接在页面使用<fmt:message>

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>@%>
    ...
<h1>
	<fmt:message key="welcomeinfo"/>
</h1>
<form action="">
    <fmt:message key="username"/>:<input /><br/>
    <fmt:message key="password"/>:<input /><br/>
    <input type="submit" value='<fmt:message key="loginBtn"/>'/>
</form>
    ...

注意:

一定要过SpringMVC的视图解析流程,人家会创建一个jstlView帮你快速国际化;

  • 不能写redirect:
  • 不能写forward: 如果写了就不会经过InternalResourceViewresolver而直接经过ResourceView中,因此不会进行国际化
		if (viewName.startsWith(FORWARD_URL_PREFIX)) {
            String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
            return new InternalResourceView(forwardUrl);
        }

6.3 mvc:view-controller

在springDispatcherServletj-servlet.xml中进行配置

设定/mars页面全部跳转到success页面
<mvc:view-controller path="/mars" view-name="success"/>
<!--开启MVC注解驱动模式-->
<mvc:annotation-driven/>

6.4 自定义view对象

注意同时需要实现ordered接口,不然默认第一个还是最初始的那个解释器,如果设置了order,默认显示你自己自定义的那个解释器,默认为Integer的最大值

package com.atguigu.view;

import org.springframework.core.Ordered;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;

import javax.swing.*;
import java.util.Locale;

public class MyViewSolver implements ViewResolver, Ordered {
    private Integer order = 0;
    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        if (viewName.startsWith("myView:")) {
            return new MyView();
        }
        return null;
    }

    public int getOrder() {
        return order;
    }

    public void setOrder(Integer order) {
        this.order = order;
    }
}
package com.atguigu.view;

import org.springframework.web.servlet.View;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

public class MyView implements View {
    @Override
    public String getContentType() {
        return "text/html";
    }

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

        response.setContentType("text/html");
        response.getWriter().write("我最帅");
    }
}

要把自己的解释器对象配置到ioc里面,让ioc知道

在springDispatcherServlet-servlet.xml

<bean class="com.atguigu.view.MyViewSolver">
    <property name="order" value="1"/>
</bean>

第七章 视图源码执行流程

7.0 SpringMVC的九大组件

  • multipartResolver:文件上传解析器
  • localeResolver:区域信息解析器,和国际化有关
  • themeResolver:主题解析器
  • handlerMappings:handler的映射器
  • handlerAdapters:handler的适配器
  • handlerExceptionResolvers:异常解析功能
  • viewNameTranslator:请求到视图名的转换器
  • flashMapManager:SpringMVC中允许重定向携带数据的功能
  • viewResolvers:视图解析器
     /** 文件上传解析器*/
    private MultipartResolver multipartResolver;
    /** 区域信息解析器;和国际化有关 */
    private LocaleResolver localeResolver;
    /** 主题解析器;强大的主题效果更换 */
    private ThemeResolver themeResolver;
    /** Handler映射信息;HandlerMapping */
    private List<HandlerMapping> handlerMappings;
    /** Handler的适配器 */
    private List<HandlerAdapter> handlerAdapters;
    /** SpringMVC强大的异常解析功能;异常解析器 */
    private List<HandlerExceptionResolver> handlerExceptionResolvers;
    /**  */
    private RequestToViewNameTranslator viewNameTranslator;
    /** FlashMap+Manager:SpringMVC中运行重定向携带数据的功能 */
    private FlashMapManager flashMapManager;
    /** 视图解析器; */
    private List<ViewResolver> viewResolvers;

onRefresh()->initStrategies() DispatcherServlet中:

protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }

例:初始化HandlerMapping

private void initHandlerMappings(ApplicationContext context) {
        this.handlerMappings = null;

        if (this.detectAllHandlerMappings) {
            // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
            Map<String, HandlerMapping> matchingBeans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
            if (!matchingBeans.isEmpty()) {
                this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
                // We keep HandlerMappings in sorted order.
                OrderComparator.sort(this.handlerMappings);
            }
        }
        else {
            try {
                HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
                this.handlerMappings = Collections.singletonList(hm);
            }
            catch (NoSuchBeanDefinitionException ex) {
                // Ignore, we'll add a default HandlerMapping later.
            }
        }

        // Ensure we have at least one HandlerMapping, by registering
        // a default HandlerMapping if no other mappings are found.
        if (this.handlerMappings == null) {
            this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
            if (logger.isDebugEnabled()) {
                logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
            }
        }
    }
组件的初始化:   有些组件在容器中是使用类型找的,有些组件是使用id找的;

去容器中找这个组件,如果没有找到就用默认的配置;

7.1 前端控制器DisatcherServlet

7.2 SpringMVC执行流程

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        try {
            ModelAndView mv = null;
            Exception dispatchException = null;
            try {
                
                //1、检查是否文件上传请求
                
                processedRequest = checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                
                // Determine handler for the current request.
                //2、根据当前的请求地址找到那个类能来处理;
                
                mappedHandler = getHandler(processedRequest);

                //3、如果没有找到哪个处理器(控制器)能处理这个请求就404,或者抛异常
                
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // Determine handler adapter for the current request.
                //4、拿到能执行这个类的所有方法的适配器;(反射工AnnotationMethodHandlerAdapter)
                
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                
                // Process last-modified header, if supported by the handler.
                
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        String requestUri = urlPathHelper.getRequestUri(request);
                        logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
                try {
                    
                    // Actually invoke the handler.处理(控制)器的方法被调用
                    //控制器(Controller),处理器(Handler)
                    //5、适配器来执行目标方法;
                    //将目标方法执行完成后的返回值作为视图名,设置保存到ModelAndView中
                    //目标方法无论怎么写,最终适配器执行完成以后都会将执行后的信息封装成ModelAndView
                    
                    mv = ha.handle(processedRequest,response,mappedHandler.getHandler());
                } finally {
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }
                }
                applyDefaultViewName(request, mv);//如果没有视图名设置一个默认的视图名;
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception ex) {
                dispatchException = ex;
            }
            
            //转发到目标页面;
            //6、根据方法最终执行完成后封装的ModelAndView;
            //转发到对应页面,而且ModelAndView中的数据可以从请求域中获取
            processDispatchResult(processedRequest, response, mappedHandler, 
                                  mv, dispatchException);
        } catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        } catch (Error err) {
            triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                
                // Instead of postHandle and afterCompletion
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                return;
            }
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }

总体概览

  1. 用户发出请求,DispatcherServlet接收请求并拦截请求。
  2. 调用doDispatch()方法进行处理:
    1. getHandler():根据当前请求地址中找到能处理这个请求的目标处理器类(处理器);
      • 根据当前请求在HandlerMapping中找到这个请求的映射信息,获取到目标处理器类
      • mappedHandler = getHandler(processedRequest);
    2. getHandlerAdapter():根据当前处理器类找到能执行这个处理器方法的适配器;
      • 根据当前处理器类,找到当前类的HandlerAdapter(适配器)
      • HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    3. 使用刚才获取到的适配器(AnnotationMethodHandlerAdapter)执行目标方法;
      • mv = ha.handle(processedRequest,response,mappedHandler.getHandler());
    4. 目标方法执行后,会返回一个ModerAndView对象
      • mv = ha.handle(processedRequest,response,mappedHandler.getHandler());
    5. 根据ModerAndView的信息转发到具体页面,并可以在请求域中取出ModerAndView中的模型数据
      • processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    HandlerMapping为处理器映射器,保存了每一个处理器能处理哪些请求的映射信息,handlerMap

    HandlerAdapter为处理器适配器,能解析注解方法的适配器,其按照特定的规则去执行Handler

具体细节

步骤一:找到类来处理

getHandler():

  		**怎么根据当前请求就能找到哪个类能来处理?**
  • getHandler()会返回目标处理器类的执行链

    ![img](/imgs/Image [1]-1610675964737.png)

  • HandlerMapping:处理器映射:他里面保存了每一个处理器能处理哪些请求的映射信息

    ![img](/imgs/Image [2]-1610675964737.png)

  • handlerMap:ioc容器启动创建Controller对象的时候扫描每个处理器都能处理什么请求,保存在HandlerMapping的handlerMap属性中;下一次请求过来,就来看哪个HandlerMapping中有这个请求映射信息就行了

    ![img](/imgs/Image [3]-1610675964737.png)

循环遍历拿到能处理url的类

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        for (HandlerMapping hm : this.handlerMappings) {
            if (logger.isTraceEnabled()) {
                logger.trace(
                        "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
            }
            HandlerExecutionChain handler = hm.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
        return null;
    }
步骤二:找到目标处理器的设配器

getHandlerAdapter():

如何找到目标处理器类的适配器。要拿适配器才去执行目标方法

![img](/imgs/Image [4]-1610675964737.png)

AnnotationMethodHandlerAdapter

  • 能解析注解方法的适配器;
  • 处理器类中只要有标了注解的这些方法就能用;
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
        for (HandlerAdapter ha : this.handlerAdapters) {
            if (logger.isTraceEnabled()) {
                logger.trace("Testing handler adapter [" + ha + "]");
            }
            if (ha.supports(handler)) {
                return ha;
            }
        }
        throw new ServletException("No adapter for handler [" + handler +
                "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
    }
步骤三: 执行目标方法的细节;

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

return invokeHandlerMethod(request, response, handler);

protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
          //拿到方法的解析器
        ServletHandlerMethodResolver methodResolver = getMethodResolver(handler);
          //方法解析器根据当前请求地址找到真正的目标方法
        Method handlerMethod = methodResolver.resolveHandlerMethod(request);
          //创建一个方法执行器;
        ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver);
          //包装原生的request, response,
        ServletWebRequest webRequest = new ServletWebRequest(request, response);
          //创建了一个,隐含模型
    
        ExtendedModelMap implicitModel = new BindingAwareModelMap();//**重点

         //真正执行目标方法;目标方法利用反射执行期间确定参数值,提前执行modelattribute等所有的操作都在这个方法中;
        Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
    	//=======================看后边补充的代码块===========================
        ModelAndView mav =
                methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
    
        methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest);
    
        return mav;
    }

Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
publicfinal Object invokeHandlerMethod(Method handlerMethod, Object handler,
           NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {
       Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod);
       try {
           boolean debug = logger.isDebugEnabled();
           for (String attrName : this.methodResolver.getActualSessionAttributeNames()) {
               Object attrValue = this.sessionAttributeStore.retrieveAttribute(webRequest, attrName);
               if (attrValue != null) {
                   implicitModel.addAttribute(attrName, attrValue);
               }
           }
              
         //找到所有@ModelAttribute注解标注的方法;
           for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) {
               Method attributeMethodToInvoke = BridgeMethodResolver.findBridgedMethod(attributeMethod);
               //先确定modelattribute方法执行时要使用的每一个参数的值;
              Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel);
      //==========================看后边补充的代码块=====================================
               if (debug) {
                   logger.debug("Invoking model attribute method: " + attributeMethodToInvoke);
               }
               String attrName = AnnotationUtils.findAnnotation(attributeMethod, ModelAttribute.class).value();
               
               if (!"".equals(attrName) && implicitModel.containsAttribute(attrName)) {
                   continue;
               }
               
               ReflectionUtils.makeAccessible(attributeMethodToInvoke);
              
              //提前运行ModelAttribute,
               Object attrValue = attributeMethodToInvoke.invoke(handler, args);
               if ("".equals(attrName)) {
                   Class<?> resolvedType = GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass());
                   attrName = Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue);
               }
               
               /*
               
               方法上标注的ModelAttribute注解如果有value值   
   			@ModelAttribute("abc")
   			hahaMyModelAttribute()
   			
               标了:	attrName="abc"
               没标:	attrName="";attrName就会变为返回值类型首字母小写,
                    比如void ,或者book;
                    
                       @ModelAttribute标在方法上的另外一个作用;
                       可以把方法运行后的返回值按照方法上@ModelAttribute("abc")
                       指定的key放到隐含模型中;
                       如果没有指定这个key;就用返回值类型的首字母小写
                       
                       {
                           haha=Book [id=100, bookName=西游记, author=吴承恩, stock=98, 									sales=10, price=98.98], 
                           void=null
                     	}
               */
               //把提前运行的ModelAttribute方法的返回值也放在隐含模型中
               if (!implicitModel.containsAttribute(attrName)) {
                   implicitModel.addAttribute(attrName, attrValue);
               }
           }

              //再次解析目标方法参数是哪些值
           Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel);
           if (debug) {
               logger.debug("Invoking request handler method: " + handlerMethodToInvoke);
           }
           ReflectionUtils.makeAccessible(handlerMethodToInvoke);

           
           //执行目标方法
           return handlerMethodToInvoke.invoke(handler, args);
       }
       catch (IllegalStateException ex) {
           // Internal assertion failed (e.g. invalid signature):
           // throw exception with full handler method context...
           throw new HandlerMethodInvocationException(handlerMethodToInvoke, ex);
       }
       catch (InvocationTargetException ex) {
           // User-defined @ModelAttribute/@InitBinder/@RequestMapping method threw an exception...
           ReflectionUtils.rethrowException(ex.getTargetException());
           return null;
       }
   }

确定方法运行时使用的每一个参数的值

Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel);

@RequestMapping("/updateBook")
    public String updateBook
           (
                @RequestParam(value="author")String author,
                Map<String, Object> model,
                HttpServletRequest request,
                @ModelAttribute("haha")Book book
            )

![img](/imgs/Image [5]-1610675964737.png)


标了注解:
          保存时哪个注解的详细信息;
          如果参数有ModelAttribute注解;
               拿到ModelAttribute注解的值让attrName保存
                    attrName="haha"

没标注解:
          1)、先看是否普通参数(是否原生API
               再看是否Model或者Map,如果是就传入隐含模型;
          2)、自定义类型的参数没有ModelAttribute 注解
                    1)、先看是否原生API
                    2)、再看是否Model或者Map
                    3)、再看是否是其他类型的比如SessionStatusHttpEntityErrors
           			4)、再看是否简单类型的属性;比如是否IntegerString,基本类型
                    		如果是paramName=“”
           			5)attrName=""
           			
如果是自定义类型对象,最终会产生两个效果;
     1)、如果这个参数标注了ModelAttribute注解就给attrName赋值为这个注解的value
     2)、如果这个参数没有标注ModelAttribute注解就给attrName赋值""

private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,
            NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {
        Class<?>[] paramTypes = handlerMethod.getParameterTypes();
          //创建了一个和参数个数一样多的数组,会用来保存每一个参数的值
        Object[] args = new Object[paramTypes.length];

                      
        for (int i = 0; i < args.length; i++) {
            MethodParameter methodParam = new MethodParameter(handlerMethod, i);
            methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer);
            GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());
            String paramName = null;
            String headerName = null;
            boolean requestBodyFound = false;
            String cookieName = null;
            String pathVarName = null;
            String attrName = null;
            boolean required = false;
            String defaultValue = null;
            boolean validate = false;
            Object[] validationHints = null;
            int annotationsFound = 0;
            Annotation[] paramAnns = methodParam.getParameterAnnotations();
            
            //找到目标方法这个参数的所有注解,如果有注解就解析并保存注解的信息;
            for (Annotation paramAnn : paramAnns) {
                if (RequestParam.class.isInstance(paramAnn)) {
                    RequestParam requestParam = (RequestParam) paramAnn;
                    paramName = requestParam.value();
                    required = requestParam.required();
                    defaultValue = parseDefaultValueAttribute(requestParam.defaultValue());
                    annotationsFound++;
                }
                else if (RequestHeader.class.isInstance(paramAnn)) {
                    RequestHeader requestHeader = (RequestHeader) paramAnn;
                    headerName = requestHeader.value();
                    required = requestHeader.required();
                    defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue());
                    annotationsFound++;
                }
                else if (RequestBody.class.isInstance(paramAnn)) {
                    requestBodyFound = true;
                    annotationsFound++;
                }
                else if (CookieValue.class.isInstance(paramAnn)) {
                    CookieValue cookieValue = (CookieValue) paramAnn;
                    cookieName = cookieValue.value();
                    required = cookieValue.required();
                    defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue());
                    annotationsFound++;
                }
                else if (PathVariable.class.isInstance(paramAnn)) {
                    PathVariable pathVar = (PathVariable) paramAnn;
                    pathVarName = pathVar.value();
                    annotationsFound++;
                }
                else if (ModelAttribute.class.isInstance(paramAnn)) {
                    ModelAttribute attr = (ModelAttribute) paramAnn;
                    attrName = attr.value();
                    annotationsFound++;
                }
                else if (Value.class.isInstance(paramAnn)) {
                    defaultValue = ((Value) paramAnn).value();
                }
                else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
                    validate = true;
                    Object value = AnnotationUtils.getValue(paramAnn);
                    validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value});
                }
            }
            if (annotationsFound > 1) {
                throw new IllegalStateException("Handler parameter annotations are exclusive choices - " +
                        "do not specify more than one such annotation on the same parameter: " + handlerMethod);
            }

             //没有找到注解的情况;
            if (annotationsFound == 0) {
                
                //解析普通参数
                Object argValue = resolveCommonArgument(methodParam, webRequest);
                //=====================看后边补充的代码块=========================
                 //会进入resolveStandardArgument(解析标准参数)
                
                
                if (argValue != WebArgumentResolver.UNRESOLVED) {
                    args[i] = argValue;
                }
                else if (defaultValue != null) {
                    args[i] = resolveDefaultValue(defaultValue);
                }
                else {
                    
               //判断是否是Model或者是Map旗下的,如果是将之前创建的隐含模型直接赋值给这个参数
                    Class<?> paramType = methodParam.getParameterType();
                    if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) {
                        if (!paramType.isAssignableFrom(implicitModel.getClass())) {
                            throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " +
                                    "Model or Map but is not assignable from the actual model. You may need to switch " +
                                    "newer MVC infrastructure classes to use this argument.");
                        }
                        args[i] = implicitModel;
                    }
                    else if (SessionStatus.class.isAssignableFrom(paramType)) {
                        args[i] = this.sessionStatus;
                    }
                    else if (HttpEntity.class.isAssignableFrom(paramType)) {
                        args[i] = resolveHttpEntityRequest(methodParam, webRequest);
                    }
                    else if (Errors.class.isAssignableFrom(paramType)) {
                        throw new IllegalStateException("Errors/BindingResult argument declared " +
                                "without preceding model attribute. Check your handler method signature!");
                    }
                    else if (BeanUtils.isSimpleProperty(paramType)) {
                        paramName = "";
                    }
                    else {
                        attrName = "";
                    }
                }
            }


               //确定值的环节
            if (paramName != null) {
                args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler);
            }
            else if (headerName != null) {
                args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler);
            }
            else if (requestBodyFound) {
                args[i] = resolveRequestBody(methodParam, webRequest, handler);
            }
            else if (cookieName != null) {
                args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler);
            }
            else if (pathVarName != null) {
                args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler);
            }

            
            //确定自定义类型参数的值;还要将请求中的每一个参数赋值给这个对象
            else if (attrName != null) {
                WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler);
                //=====================看后边代码补充============================
                boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]));
                if (binder.getTarget() != null) {
                    doBind(binder, webRequest, validate, validationHints, !assignBindingResult);
                }
                args[i] = binder.getTarget();
                if (assignBindingResult) {
                    args[i + 1] = binder.getBindingResult();
                    i++;
                }
                implicitModel.putAll(binder.getBindingResult().getModel());
            }
        }
        return args;
    }

如果没有注解:

resolveCommonArgument)就是确定当前的参数是否是原生API

		@Override
        protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest) throws Exception {
            HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
            HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);

            if (ServletRequest.class.isAssignableFrom(parameterType) ||
                    MultipartRequest.class.isAssignableFrom(parameterType)) {
                Object nativeRequest = webRequest.getNativeRequest(parameterType);
                if (nativeRequest == null) {
                    throw new IllegalStateException(
                            "Current request is not of type [" + parameterType.getName() + "]: " + request);
                }
                return nativeRequest;
            }
            else if (ServletResponse.class.isAssignableFrom(parameterType)) {
                this.responseArgumentUsed = true;
                Object nativeResponse = webRequest.getNativeResponse(parameterType);
                if (nativeResponse == null) {
                    throw new IllegalStateException(
                            "Current response is not of type [" + parameterType.getName() + "]: " + response);
                }
                return nativeResponse;
            }
            else if (HttpSession.class.isAssignableFrom(parameterType)) {
                return request.getSession();
            }
            else if (Principal.class.isAssignableFrom(parameterType)) {
                return request.getUserPrincipal();
            }
            else if (Locale.class.equals(parameterType)) {
                return RequestContextUtils.getLocale(request);
            }
            else if (InputStream.class.isAssignableFrom(parameterType)) {
                return request.getInputStream();
            }
            else if (Reader.class.isAssignableFrom(parameterType)) {
                return request.getReader();
            }
            else if (OutputStream.class.isAssignableFrom(parameterType)) {
                this.responseArgumentUsed = true;
                return response.getOutputStream();
            }
            else if (Writer.class.isAssignableFrom(parameterType)) {
                this.responseArgumentUsed = true;
                return response.getWriter();
            }
            return super.resolveStandardArgument(parameterType, webRequest);
        }

resolveModelAttribute

SpringMVC确定POJO值的三步;
1、如果隐含模型中有这个key(标了ModelAttribute注解就是注解指定的value,没标就是参数类型的首字母小写)指定的值;
     如果有将这个值赋值给bindObject;
2、如果是SessionAttributes标注的属性,就从session中拿;
3、如果都不是就利用反射创建对象;
private WebDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam,
            ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception {

        // Bind request parameter onto object...  
        String name = attrName;
     
        if ("".equals(name)) {
               //如果attrName是空串;就将参数类型的首字母小写作为值 
            	//Book book2121 -> name=book
            name = Conventions.getVariableNameForParameter(methodParam);
        }
        Class<?> paramType = methodParam.getParameterType();
        Object bindObject;
    
   		 //确定目标对象的值
        if (implicitModel.containsKey(name)) {
            bindObject = implicitModel.get(name);
        }
        else if (this.methodResolver.isSessionAttribute(name, paramType)) {
            bindObject = this.sessionAttributeStore.retrieveAttribute(webRequest, name);
            if (bindObject == null) {
                raiseSessionRequiredException("Session attribute '" + name + "' required - not found in session");
            }
        }
        else {
            bindObject = BeanUtils.instantiateClass(paramType);
        }
    
    
        WebDataBinder binder = createBinder(webRequest, bindObject, name);
        initBinder(handler, name, binder, webRequest);
        return binder;
    }

总结:

  1. 运行流程简单版;
  2. 确定方法每个参数的值;
    1. 标注解:保存注解的信息;最终得到这个注解应该对应解析的值;
    2. 没标注解:
      1. 看是否是原生API;
      2. 看是否是Model或者是Map,SessionStatus、HttpEntity、Errors…
      3. 看是否是简单类型;paramName=""
      4. 给attrName赋值;attrName(参数标了@ModelAttribute("")就是指定的,没标就是"")
        1. attrName使用参数的类型首字母小写;或者使用之前@ModelAttribute("")的值
        2. 先看隐含模型中有每个这个attrName作为key对应的值;如果有就从隐含模型中获取并赋值
        3. 看是否是@SessionAttributes(value=“haha”);标注的属性,如果是从session中拿;
        4. 不是@SessionAttributes标注的,利用反射创建一个对象;
      5. 不是@SessionAttributes标注的,利用反射创建一个对象;
步骤四:包装成一个ModelAndView对象
  1. 任何方法的返回值,最终都会被包装成ModelAndView对象
步骤五:SpringMVC视图解析

SpringMVC视图解析:

1、方法执行后的返回值会作为页面地址参考,转发或者重定向到页面

2、视图解析器可能会进行页面地址的拼串

processDispatchResult(processedRequest, response, mappedHandler, 
    mv, dispatchException);
  1. 调用processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)

    • 来到页面的方法视图渲染流程
    • 将域中的数据在页面展示
    • 页面就是用来渲染模型数据的
  2. 调用render(mv, request, response)

    • 渲染页面
  3. View与ViewResolver

    • ViewResolver的作用是根据视图名(方法的返回值)得到View对象
    • ![img](/imgs/Image [1]-1605108563777-1610675964737.png)
  4. 怎么能根据方法的返回值(视图名)得到View对象?

```java
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
            HttpServletRequest request) throws Exception {

          //遍历所有的ViewResolver;
        for (ViewResolver viewResolver : this.viewResolvers) {


​ //viewResolver视图解析器根据方法的返回值,得到一个View对象; ​ View view = viewResolver.resolveViewName(viewName, locale);


​ if (view != null) { ​ return view; ​ } ​ } ​ return null; ​ } ​ ​ ​ - resolveViewName实现 ​ ​ java ​ @Override ​ public View resolveViewName(String viewName, Locale locale) throws Exception { ​ if (!isCache()) { ​ return createView(viewName, locale); ​ } ​ else { ​ Object cacheKey = getCacheKey(viewName, locale); ​ View view = this.viewAccessCache.get(cacheKey); ​ if (view == null) { ​ synchronized (this.viewCreationCache) { ​ view = this.viewCreationCache.get(cacheKey); ​ if (view == null) {


​ // Ask the subclass to create the View object. ​ //根据方法的返回值创建出视图View对象; ​ view = createView(viewName, locale);


​ if (view == null && this.cacheUnresolved) { ​ view = UNRESOLVED_VIEW; ​ } ​ if (view != null) { ​ this.viewAccessCache.put(cacheKey, view); ​ this.viewCreationCache.put(cacheKey, view); ​ if (logger.isTraceEnabled()) { ​ logger.trace(“Cached view [” + cacheKey + “]”); ​ } ​ } ​ } ​ } ​ } ​ return (view != UNRESOLVED_VIEW ? view : null); ​ } ​ } ​ ``` ​
​ - 创建View对象 ​
​ ![img](/imgs/Image [2]-1605108694469-1610675964737.png)

  ```java
  @Override
      protected View createView(String viewName, Locale locale) throws Exception {
          // If this resolver is not supposed to handle the given view,
          // return null to pass on to the next resolver in the chain.
          if (!canHandle(viewName, locale)) {
              return null;
          }
          // Check for special "redirect:" prefix.
          if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
              String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
              RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
              return applyLifecycleMethods(viewName, view);
          }
          // Check for special "forward:" prefix.
          if (viewName.startsWith(FORWARD_URL_PREFIX)) {
              String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
              return new InternalResourceView(forwardUrl);
          }
          // Else fall back to superclass implementation: calling loadView.
          //如果没有前缀就使用父类默认创建一个View;
          return super.createView(viewName, locale);
      }
  ```
    
   ![img](/imgs/Image [3]-1605108720943-1610675964737.png) 
    
   ![img](/imgs/Image [4]-1605108727771-1610675964737.png) 

- 返回View对象

    - 视图解析器得到View对象的流程就是,所有配置的视图解析器都来尝试根据视图名(返回值)得到View(视图)对象;如果能得到就返回,得不到就换下一个视图解析器;
    - 调用View对象的render方法

    ```java
    @Override
        public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
            if (logger.isTraceEnabled()) {
                logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
                    " and static attributes " + this.staticAttributes);
            }
    
            Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
    
            prepareResponse(request, response);


​ //渲染要给页面输出的所有数据 ​ renderMergedOutputModel(mergedModel, request, response); ​ } ​ ​ ​ - InternalResourceView有这个方法renderMergedOutputModel; ​ ​ java ​ @Override ​ protected void renderMergedOutputModel( ​ Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { ​
​ // Determine which request handle to expose to the RequestDispatcher. ​ HttpServletRequest requestToExpose = getRequestToExpose(request); ​
​ // Expose the model object as request attributes.


​ //将模型中的数据放在请求域中 ​ exposeModelAsRequestAttributes(model, requestToExpose);



​ // Expose helpers as request attributes, if any. ​ exposeHelpers(requestToExpose); ​
​ // Determine the path for the request dispatcher. ​ String dispatcherPath = prepareForRendering(requestToExpose, response); ​
​ // Obtain a RequestDispatcher for the target resource (typically a JSP). ​ RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath); ​ if (rd == null) { ​ throw new ServletException(“Could not get RequestDispatcher for [” + getUrl() + ​ “]: Check that the corresponding file exists within your web application archive!”); ​ } ​
​ // If already included or response already committed, perform include, else forward. ​ if (useInclude(requestToExpose, response)) { ​ response.setContentType(getContentType()); ​ if (logger.isDebugEnabled()) { ​ logger.debug(“Including resource [” + getUrl() + “] in InternalResourceView ‘” + getBeanName() + “’”); ​ } ​ rd.include(requestToExpose, response); ​ } ​
​ else { ​ // Note: The forwarded resource is supposed to determine the content type itself. ​ if (logger.isDebugEnabled()) { ​ logger.debug(“Forwarding to resource [” + getUrl() + “] in InternalResourceView ‘” + getBeanName() + “’”); ​ } ​
//转发页面 rd.forward(requestToExpose, response); } } ```

- 将模型中的所有数据取出来全放在request域中

    ```java
    protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
            for (Map.Entry<String, Object> entry : model.entrySet()) {
                String modelName = entry.getKey();
                Object modelValue = entry.getValue();
                if (modelValue != null) {
                    
                    //将ModelMap中的数据放到请求域中
                    request.setAttribute(modelName, modelValue);


​ if (logger.isDebugEnabled()) { ​ logger.debug(“Added model object ‘” + modelName + “’ of type [” + modelValue.getClass().getName() + ​ “] to request in view with name ‘” + getBeanName() + “’”); ​ } ​ } ​ else { ​ request.removeAttribute(modelName); ​ if (logger.isDebugEnabled()) { ​ logger.debug(“Removed model object ‘” + modelName + ​ “’ from request in view with name ‘” + getBeanName() + “’”); ​ } ​ } ​ } ​ } ​ ``` ​
​ 总结: ​
​ - 视图解析器只是为了得到视图对象 ​ - 视图对象才能真正的转发(将模型数据全部放在请求域中)或者重定向到页面视图对象才能真正的渲染视图 ​
​ - ViewResolver ​
​ ![img](/imgs/Image [5]-1605108929361-1610675964737.png) ​
- View:

    ![img](/imgs/Image [6]-1610675964737.png)

第八章 rest的crud

8.1 环境准备

pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.atguigu</groupId>
  <artifactId>springMVC11</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>springMVC11 Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.9</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.8</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/net.sourceforge.cglib/com.springsource.net.sf.cglib -->
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>2.2.2</version>
    </dependency>
    <dependency>
      <groupId>aopalliance</groupId>
      <artifactId>aopalliance</artifactId>
      <version>1.0</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.1.3</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
    </dependency>
    <dependency>

      <groupId>jstl</groupId>


      <artifactId>jstl</artifactId>


      <version>1.2</version>


    </dependency>


    <dependency>


      <groupId>taglibs</groupId>


      <artifactId>standard</artifactId>


      <version>1.1.2</version>


    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.0</version>
    </dependency>




  </dependencies>

  <build>
    <finalName>springMVC11</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>


</project>

web.xml

<web-app>

  <display-name>Archetype Created Web Application</display-name>
  <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <servlet>
    <servlet-name>springDispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--可以不配置,默认使用/WEB-INF/<servlet-name>-servlet.xml-->
<!--    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>-->
  </servlet>


  <servlet-mapping>
    <servlet-name>springDispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>












</web-app>

springDispatcherServlet-Servlet.xml

<web-app>

  <display-name>Archetype Created Web Application</display-name>
  <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <servlet>
    <servlet-name>springDispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--可以不配置,默认使用/WEB-INF/<servlet-name>-servlet.xml-->
<!--    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>-->
  </servlet>


  <servlet-mapping>
    <servlet-name>springDispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>












</web-app>

springDispatchServlet-servlet.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:mvc="http://www.springframework.org/schema/mvc"
       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/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!--配置扫描的组件-->
    <context:component-scan base-package="com.atguigu"/>
    <!--配置映射解析器:如何将控制器返回的结果字符串,转换成一个物理的视图文件-->
    <bean id="internalResourceViewResolver"
          class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>

    </bean>
    <mvc:view-controller path="/mars" view-name="success"/>
    <!--开启MVC注解驱动模式-->
    <mvc:annotation-driven/>
    <bean class="com.atguigu.view.MyViewSolver">
        <property name="order" value="1"/>
    </bean>

</beans>

8.2 显示所有操作,添加操作,修改操作

<button  onclick="location.href='emps'">点击显示所有的Employee</button>
<button onclick="location.href='getDep'">点击进入添加employeeDao页面</button>
@Controller
public class controller11 {
    @Autowired
    EmployeeDao employeeDao;
    @Autowired
    DepartmentDao departmentDao;
    @RequestMapping("/emps")
    public ModelAndView getAll() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("success1");
        Collection<Employee> all = employeeDao.getAll();
        modelAndView.addObject("temps", all);
        return modelAndView;
    }

    /**
     * 得到depentment对象,进行回显
     * @return
     */
    @RequestMapping(value = "/getDep")
    public ModelAndView getDep() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("getDep");
        Collection<Department> departments = departmentDao.getDepartments();
        modelAndView.addObject("departments", departments);
        return modelAndView;
    }

    /**
     * 进行添加操作
     * @param employee
     * @return
     */
    @RequestMapping(value = "/addEmp",method = RequestMethod.POST)
    public ModelAndView addEmp(Employee employee) {
        employeeDao.save(employee);
        return new ModelAndView("redirect:/emps");

    }

    /**
     * 进行修改操作的回显,这里页面用了springmvc的<form:form></form:form>标签
     * 一定要传入一个employee对象,用来进行回显,springmvc指定一定要进行回显,如果没有直接报错
     * @param id
     * @param model
     * @return
     */

    @RequestMapping(value = "/update/{id}")
    public String update(@PathVariable("id") Integer id,Model model) {
        Employee employee = employeeDao.get(id);
        model.addAttribute("employee", employee);
        return "getUpdate";
    }

    /**
     * 用来进行修改数据,首先先执行带有@ModelAttribute注解的方法
     * @param id
     * @param employee
     * @param request
     * @return
     */
    @RequestMapping(value = "/updateEmp/{id}",method = RequestMethod.PUT)
    public String updateEmp(@PathVariable("id") Integer id, @ModelAttribute("employee")Employee employee, HttpServletRequest request) {
        System.out.println("要修改的员工"+employee);
        return "success";
    }

    /**
     * 首先执行这个方法,把employee放入请求中,让上面的方法执行
     * @param id
     * @param model
     */
    @ModelAttribute
    public void myModelAttribute(@RequestParam(value = "id",required = false) Integer id,Model model) {
        if (id != null) {
            Employee employee = employeeDao.get(id);
            model.addAttribute("employee", employee);
        }

    }
}

8.3 显示所有数据的页面

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%--
  Created by IntelliJ IDEA.
  User: 10185
  Date: 2021/1/15
  Time: 14:43
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>全部用户</h1>
    <table border="1">
        <tr>
            <th>id</th>
            <th>lastName</th>
            <th>email</th>
            <th>gender</th>
            <th>department</th>
        </tr>

            <c:forEach items="${temps}" var="emp">
                <tr>
                <td>${emp.id}</td>
                <td>${emp.lastName}</td>
                <td>${emp.email}</td>
                <td>${emp.gender}</td>
                <td>${emp.department.departmentName}</td>
                    <td><a href="update/${emp.id}">修改</a></td>
                 </tr>
            </c:forEach>


    </table>
</body>
</html>

8.4 添加页面

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%--
  Created by IntelliJ IDEA.
  User: 10185
  Date: 2021/1/15
  Time: 19:56
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="addEmp" method="post">
        id:<input type="text" name="id">
        lastName:<input type="text" name="lastName">
        email:<input type="text" name="email">
        gender:<input type="radio" name="gender" value="0"><input type="radio" name="gender" value="1">
        dept:<select name="department.id">

            <c:forEach var="dep" items="${requestScope.departments}">
        <option value="${dep.id}">${dep.departmentName}</option>
            </c:forEach>
        <input type="submit" value="提交页面">

    </select>

    </form>
</body>
</html>

8.5 修改操作

注意这里用来form:form标签,需要现在controller里面先用一个employee进行回显数据,然后进行修改操作,如果不设设置@ModelAttribute先把数据库中的数据进行保存,然后再这个employee对象中进行修改,这样原来的数据就不会丢失了,注意会从隐含对象中拿,不会从请求对象中拿,所以不能用HTTPrequest

@RequestMapping(value = "/getDep")
public ModelAndView getDep() {
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.setViewName("getDep");
    Collection<Department> departments = departmentDao.getDepartments();
    modelAndView.addObject("departments", departments);
    modelAndView.addObject("employee", new Employee());
    return modelAndView;
}

注意如果下面页面上面没有modelAttribute标签,那么变量名就是command

modelAndView.addobject(“command”,new Employee());

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%--
  Created by IntelliJ IDEA.
  User: 10185
  Date: 2021/1/16
  Time: 9:14
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%pageContext.setAttribute("ctp", request.getContextPath());%>


<html>
<head>
    <title>Title</title>
</head>
<body>


<form:form action="${ctp}/updateEmp/${employee.id}" method="post" modelAttribute="employee">
    <input type="hidden" name="_method" value="put">
    <input type="hidden" name="id" value="${employee.id}">

    邮箱:<form:input path="email"></form:input><br>
    性别:<br>
    男:<form:radiobutton path="gender" value="1"></form:radiobutton>
    女:<form:radiobutton path="gender" value="0"></form:radiobutton><br>
    部门:
    <form:select path="department.id" items="${departments}"
                 itemLabel="departmentName" itemValue="id">
    </form:select>
    <input type="submit" value="修改">
</form:form>
</body>
</html>

第九章 数据绑定&&数据格式化&&数据校验

9.1 数据绑定

9.1.1 数据绑定用法

SpringMVC封装自定义类型对象的时候
javaBean要和页面提交的数据进行一一绑定
1)、页面提交的所有数据都是字符串
2)、Integer age,Date birth;
    employName=zhangsan&age=18&gender=1
    String age = request.getParameter("age");
牵扯到以下操作
1)、数据绑定期间的数据类型转换String--Integer String--Boolean,xxx
2)、数据绑定期间的数据格式化问题比如提交的日期进行转换
         birth=2017-12-15----->Date    2017/12/15  2017.12.15  2017-12-15
3)、数据校验
         我们提交的数据必须是合法的
         前端校验js+正则表达式
         后端校验重要数据也是必须的
         1)、校验成功数据合法
         2)、校验失败

那么springMVC是怎么样将字符串进行格式化的呢

9.1.2 数据绑定源码

bindRequestParameters方法将请求参数于JavaBean进行绑定,为自定义对象赋值。

ModelAttributeMethodProcessor
public final Object resolveArgument(
            MethodParameter parameter, ModelAndViewContainer mavContainer,/`
            NativeWebRequest request, WebDataBinderFactory binderFactory)
            throws Exception {
        String name = ModelFactory.getNameForParameter(parameter);
        Object attribute = (mavContainer.containsAttribute(name)) ?
                mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
    
    	//WebDataBinder
        WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
    
    
        if (binder.getTarget() != null) {
            
               //将页面提交过来的数据封装到javaBean的属性中
            bindRequestParameters(binder, request);
               //+++++++++
            
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors()) {
                if (isBindExceptionRequired(binder, parameter)) {
                    throw new BindException(binder.getBindingResult());
                }
            }
        }

WebDataBinder:

数据绑定器有什么用?

  1. 数据绑定器负责数据绑定工作
  2. 数据绑定期间产生的类型转换、格式化、数据校验等问题

img

  • conversionService组件:

    • 负责数据类型的转换以及格式化功能;
    • ConversionService中有非常多的converter;
    • 不同类型的转换和格式化用它自己的converter
        ...
    @org.springframework.format.annotation.DateTimeFormat java.util.Date -> java.lang.String: org.springframework.format.datetime.DateTimeFormatAnnotationFormatterFactory@32abc654
        @org.springframework.format.annotation.NumberFormat java.lang.Double -> java.lang.String: org.springframework.format.number.NumberFormatAnnotationFormatterFactory@140bb45d
        @org.springframework.format.annotation.NumberFormat java.lang.Float -> java.lang.String: org.springframework.format.number.NumberFormatAnnotationFormatterFactory@140bb45d
        ....
    org.springframework.format.number.NumberFormatAnnotationFormatterFactory@140bb45d
        java.lang.String -> @org.springframework.format.annotation.NumberFormat java.math.BigInteger: org.springframework.format.number.NumberFormatAnnotationFormatterFactory@140bb45d
        java.lang.String -> java.lang.Boolean : org.springframework.core.convert.support.StringToBooleanConverter@22f562e2
        java.lang.String -> java.lang.Character : org.springframework.core.convert.support.StringToCharacterConverter@5f2594f5
        java.lang.String -> java.lang.Enum : org.springframework.core.convert.support.StringToEnumConverterFactory@1347a7be
        java.lang.String -> java.lang.Number : 
    ...
        java...
    
  • validators负责数据校验工作

![img](/imgs/Image [1]-1605255368587.png)

  • bindingResult负责保存以及解析数据绑定期间数据校验产生的错误

![img](/imgs/Image [2]-1605255330651.png)

1605255897274

9.1.3 自定义类型转换器:

1 新建一个类实现converter接口
package com.atguigu.binder;

import com.atguigu.bean.Employee;
import com.atguigu.dao.DepartmentDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;

public class myBinder implements Converter<String,Employee> {
    @Autowired
    DepartmentDao departmentDao;

    @Override
    public Employee convert(String s) {
        String[] split = s.split("/");
        Employee employee = new Employee();
        employee.setId(Integer.parseInt(split[0]));
        employee.setLastName(split[1]);
        employee.setDepartment(departmentDao.getDepartment(Integer.parseInt(split[2])));
        return employee;
    }
}
2 配置conversionServiceFactoryBean
 通过配置文件定义自己的conversionService,把自己的converter加入到conversionServiceFactoryBean对象里面
<bean id="myConversionServiceFactoryBean" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="com.atguigu.binder.myBinder">
                </bean>
            </set>
        </property>
    </bean>
3 让mvc知道你创建了一个自己的conversionService
<mvc:annotation-driven conversion-service="myConversionServiceFactoryBean">
    

9.2 数据格式化

把ConversionServiceFactoryBean改成FormattingConversionServiceFactoryBean
<bean id="myConversionServiceFactoryBean" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="com.atguigu.binder.myBinder">
            </bean>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="myConversionServiceFactoryBean">

</mvc:annotation-driven>
然后再类中在需要格式化的类上加上注解

在SpringMVC中Controller中方法参数为Date类型想要限定请求传入时间格式时,可以通过@DateTimeFormat来指定,但请求传入参数与指定格式不符时,会返回400错误。

如果在Bean属性中有Date类型字段,想再序列化转为指定格式时,也可用@DateTimeFormat来指定想要的格式。如下:

img

9.3 数据校验

9.3.1 导入hibernate的jar包

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.atguigu</groupId>
  <artifactId>springMVC11</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>springMVC11 Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.9</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.8</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/net.sourceforge.cglib/com.springsource.net.sf.cglib -->
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>2.2.2</version>
    </dependency>
    <dependency>
      <groupId>aopalliance</groupId>
      <artifactId>aopalliance</artifactId>
      <version>1.0</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.1.3</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.1.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
    </dependency>
    <dependency>

      <groupId>jstl</groupId>


      <artifactId>jstl</artifactId>


      <version>1.2</version>


    </dependency>


    <dependency>


      <groupId>taglibs</groupId>


      <artifactId>standard</artifactId>


      <version>1.1.2</version>


    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.0</version>
    </dependency>
    <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>1.1.0.Final</version>
    </dependency>

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>5.4.1.Final</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.logging</groupId>
      <artifactId>jboss-logging</artifactId>
      <version>3.3.0.Final</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml</groupId>
      <artifactId>classmate</artifactId>
      <version>1.3.3</version>
    </dependency>
  </dependencies>




</project>

9.3.2 在employee上面加上注解信息

public class Employee {

   private Integer id;
   @NotNull
   /*代表在长度一定要在5-16之间*/
   @Length(min = 5,max = 16)
   private String lastName;
   /*用hibernate规定的格式*/
   @Email
   private String email;
   //1 male, 0 female
   private Integer gender;
   
   private Department department;

   @DateTimeFormat(pattern = "yyyy-MM-dd")
   //表示需要在当前时间之前
   @Past
   //表示需要在当前时间之后
   /*@Future*/
   private Date date;

9.3.3 控制器的写法

  • 对SpringMVC封装对象加上@Valid注解
  • 校验结果在BindingResult的result中
@RequestMapping(value = "/addEmp",method = RequestMethod.POST)
public String addEmp(@Valid Employee employee, BindingResult bindingResult, @RequestParam("email") String email) {




    if (bindingResult.hasErrors()) {
        System.out.println("有错误");
        return "getDep";
    }
    employeeDao.save(employee);
    return "redirect:/getAll";


}

9.3.4 mvc表单的页面的写法

  • 来到页面使用form:errors取出错误信息
  • 可以把错误信息存到Model中,然后在页面中取Model的对应的key

9.3.5 原生表单的写法

把错误信息放到model中即可

9.3.6 国际化定制

国际化定制自己的错误消息显示

编写国际化的文件
  • errors_zh_CN.properties
  • errors_en_US.properties

key有规定(精确优先):

 codes
     [
          Email.employee.email,      校验规则.隐含模型中这个对象的key.对象的属性
          Email.email,                       校验规则.属性名
          Email.java.lang.String,      校验规则.属性类型
          Email
    ];

1、先编写国际化配置文件

![img](/imgs/Image [18].png)

2、让SpringMVC管理国际化资源文件

<!-- 管理国际化资源文件 -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="errors"></property>
    </bean>

3、来到页面取值

4、高级国际化?

 动态传入消息参数;

![img](/imgs/Image [19].png)

{0}:永远都是当前属性名;

	@Length(min = 5, max = 10,message='xxxx')

按照字母排序

{1}为max {2}为min

第10章 动态资源,和静态资源被springMVC识别

<mvc:default-servlet-handler/> <mvc:annotation-driven/>

  1. 都没配

    • 动态能访问:

      DefaultAnnotationHandlerMapping中的handlerMap中保存了每一个资源的映射信息

    • 静态不能访问:

      handlerMap中没有保存静态资源映射的请求

      ![img](/imgs/Image [7].png)

    • handleAdapter

      ![img](/imgs/Image [9].png)

  2. <mvc:default-servlet-handler/>不加<mvc:annotation-driven/>

    • 动态不能访问:DefaultAnnotationHandlerMapping被SimpleUrlHandlerMapping替换。

    • 静态能访问的原因:SimpleUrlHandlerMapping把所有请求都映射给tomcat;

      ![img](/imgs/Image [10].png)

    • handleAdapter

      ![img](/imgs/Image [11].png)

  3. 都加上

    • 都能访问

      handlerMap

      ![img](/imgs/Image [12].png)

    • RequestMappingHandlerMapping:动态资源可以访问

      ![img](/imgs/Image [13].png)

      handleMethods属性保存了每一个请求用哪个方法来处理;

      SimpleUrlHandlerMapping:将请求直接交给tomcat;有他,静态资源就没问题

    • handleAdapter

      ![img](/imgs/Image [14].png)

      原来的AnnotationMethodHandlerAdapter被换成RequestMappingHandlerAdapter

  4. 只加<mvc:annotation-driven/>

    • 动态能访问,静态无法访问

第11章 用springmvc ajax

11.1 导入jackson

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-core</artifactId>
  <version>2.9.5</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.5</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-annotations</artifactId>
  <version>2.9.5</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.module</groupId>
  <artifactId>jackson-module-jaxb-annotations</artifactId>
  <version>2.9.5</version>
</dependency>

注意:这个包的版本和spring版本有关系,注意

11.2 responseBody的使用

11.2.1 控制器中的代码

@Controller
public class AjaxController {
    @Autowired
    private EmployeeDao employeeDao;
    @Autowired
    private DepartmentDao departmentDao;
    @RequestMapping(value = "/ajaxGetAll", method = RequestMethod.GET)
    @ResponseBody
    public Collection<Employee> getAll() {
        return employeeDao.getAll();
    }
}

11.2.2 返回的结果

image-20210120152739629

会自动转换成json的形式

  • @JsonIgnore可以忽略字段

  • @JsonFormat(pattern="")自定制序列化字段格式

  •       @DateTimeFormat(pattern = "yyyy-MM-dd")
          @Past
          @JsonFormat(pattern = "yyyy-MM-dd")
          private Date birth;
    
          private String email;
          //1 male, 0 female
    
          private Integer gender;
    
          @JsonIgnore
          private Department department;
    
<script>
    $("#button1").click(function () {
    $.ajax(
        {
            url:"http://localhost:8080/springMVC11/ajaxGetAll",
            data:{},
            success:function (data) {
                console.log("当前数据是"+data);
                alert("wosdfhji")
            },
            dataType:"json"

        }

    ) })
</script>

11.3 requestBody的使用

<script>
    $("#button1").click(function () {var a = {
        lastname:"张三",
        email:"aaa@aaa.com",
        gender:0

    };
        $.ajax(

            {
                url:"${ctp}/ajaxReturnAll",
                type:"post",
                data:JSON.stringify(a),
                contentType:"application/json",
                success:function(data) {
                    console.log("当前数据是"+data);
                },
                dataType:"json"
            }

        );
    });
</script>
@RequestMapping(value = "/ajaxReturnAll", method = RequestMethod.POST)
public String returnAll(@RequestBody Employee employee) {
    System.out.println(employee);
    return "success";
}

controller的代码

@RequestMapping(value = "test2/ssm.html",method = RequestMethod.POST)

 public void test2(@RequestParam("array[]") int[] array, HttpServletResponse response) throws IOException {
     System.out.println(Arrays.toString(array));
     response.getWriter().write("我成功了");

image-20210201141656406

image-20210201143443299

image-20210201142413995

11.4 HttpEntiy

  • 代替RequestBody,
  • 不仅能拿请求体数据,还能拿请求头数据

1605347056358

ResponseEntity

  • 可以设置响应头

1605348403547

第12章 下载文件和上传文件

12.1 下载文件

@RequestMapping("/downloadFile")
public ResponseEntity<byte[]> downloadFile(HttpServletRequest request) throws IOException {
    ServletContext servletContext = request.getServletContext();
    String realPath = servletContext.getRealPath("/script/jquery-1.7.2.js");
    FileInputStream fileInputStream = new FileInputStream(realPath);
    byte[] bytes = new byte[fileInputStream.available()];
    fileInputStream.read(bytes);
    fileInputStream.close();
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Disposition", "attchement;filename=" + "jquery");
    HttpStatus statusCode = HttpStatus.OK;
    return new ResponseEntity<>(bytes, headers, statusCode);


}

12.2 上传文件

12.2.1 maven导入包

<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.0</version>
</dependency>
<dependency>
  <groupId>commons-fileupload</groupId>
  <artifactId>commons-fileupload</artifactId>
  <version>1.2.1</version>

12.2.2 进行ioc容器配置

<!--通过ioc容器寻找,id一定要配置multipartResolver-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize" value="#{1024*1024*20}"></property>
    <property name="defaultEncoding" value="utf-8"></property>

</bean>

注意:一定要进行id的配置,为什么呢?

image-20210120212035252

image-20210120212159150

image-20210120212240294

image-20210120212316552

因此,一定要进行id值的配置

12.2.3 controller中的代码

 @RequestMapping(value = "uploadFile",method = RequestMethod.POST)
    public String uploadFile(@RequestParam(value = "headerOfFile",required = false)MultipartFile multipartFile) {
        System.out.println(multipartFile);

        try {
            multipartFile.transferTo(new File("D:\\upload\\"+multipartFile.getOriginalFilename()));
            System.out.println("上传成功");
        } catch (IOException e) {
            System.out.println("上传失败"+e.getMessage());
        }
        return "success";

    }
}

12.2.4 jsp中的代码

<form action="uploadFile" method="post" enctype="multipart/form-data">
    头像:<input type="file" name="headerOfFile">
    昵称:<input type="text" name="filename">
    <input type="submit">


</form>

12.3 上传多文件

 @RequestMapping(value = "uploadFile",method = RequestMethod.POST)
   public String uploadFile(@RequestParam(value = "headerOfFile",required = false)MultipartFile[] multipartFiles) {
       for (MultipartFile multipartFile : multipartFiles) {
           if (!multipartFile.isEmpty()) {
               try {
                   multipartFile.transferTo(new File("D:\\upload" + multipartFile.getOriginalFilename()));
                   System.out.println("上传成功");
               } catch (IOException e) {
                   System.out.println("上传失败"+e.getMessage());
               }
           }
       }
       return "success";

   }
}

12.4 HttpMessageConverter接口:

Spring3.0 新添加的一个接口,负责

将请求信息转换为一个对象(类型为 T)

将对象(类型为 T)输出为响应信息

注意:一般Controller返回String类型是走视图解析(ViewResolver)

		如果返回其他类型是由HttpMessageConverter负责

1605356444254

HttpMessageConverter接口定义的方法:

  • Boolean canRead(Class clazz,MediaType mediaType):
    • 指定转换器可以读取的对象类型,即转换器是否可将请求信息转换为 clazz 类型的对象,同时指定支持 MIME 类型(text/html,applaiction/json等)
  • Boolean canWrite(Class clazz,MediaType mediaType):
    • 指定转换器是否可将 clazz 类型的对象写到响应流中,响应流支持的媒体类型在MediaType 中定义
  • LIst getSupportMediaTypes():
    • 该转换器支持的媒体类型
  • T read(Class<? extends T> clazz,HttpInputMessage inputMessage):
    • 将请求信息流转换为 T 类型的对象
  • void write(T t,MediaType contnetType,HttpOutputMessgae outputMessage):
    • 将T类型的对象写到响应流中,同时指定相应的媒体类型为 contentType

第13章 拦截器的使用

13.1 拦截器的使用

SpringMVC提供了拦截器机制: 允许运行目标方法之前进行一些拦截工作,或者目标方法运行之后进行一些其他处理。

Filter:javaWeb HandlerInterceptor:SpringMVC

HandlerInterceptor

  • preHandle:在目标方法运行之前调用:

    • 返回boolean
      • return true;(chain.doFilter())放行;
      • return false;不放行
  • postHandle:在目标方法运行之后调用

  • afterCompletion:资源响应之后调用

13.2 实现HandlerInterceptor接口

package com.atguigu.Interceptor;

import org.aopalliance.intercept.Interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyInterceptor1 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("myInterceptor1--->preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("myInterceptor1--->postHandle");

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("myInterceptor1--->afterCompletion");

    }
}

13.3 配置拦截器

<mvc:interceptors>
    <!--默认拦截所有请求↓-->
    <!-- <bean class="com.atguigu.Interceptor.MyInterceptor1"></bean>-->

    <!--拦截具体请求↓-->
    <mvc:interceptor>
        <!--只拦截path所对应的请求-->
        <mvc:mapping path="/testInter"/>
        <bean class="com.atguigu.Interceptor.MyInterceptor2"></bean>
    </mvc:interceptor>
</mvc:interceptors>

13.4 拦截器步骤解析

image-20210121145632406

12.5 多个拦截器步骤分析

1605414721082

MyFirstInterceptor...preHandle...
MySecondInterceptor...preHandle...
目标方法....
MySecondInterceptor...postHandle...
MyFirstInterceptor...postHandle...
响应页面....
MySecondInterceptor...afterCompletion...
MyFirstInterceptor...afterCompletion

异常流程:

  1. 哪一块Interceptor不放行

    1. 哪一块不放行从此以后都没有
  2. MySecondInterceptor不放行

    1. 但是他前面已经放行了的拦截器的afterCompletion总会执行

总结interceptor的流程:

拦截器的preHandle:是按照顺序执行

拦截器的postHandle:是按照逆序执行

拦截器的afterCompletion:是按照逆序执行

已经放行了的拦截器的afterCompletion总会执行

13.6 拦截器的源码分析

image-20210121134336719

image-20210121134434107

image-20210121135320052

image-20210121135459503

image-20210121135634719

image-20210121135822788

image-20210121144652786

image-20210121144706940

第14章 国际化

14.1 写好配置文件

image-20210121195930013

username=用户名
password=密码
login=登入
username=UserName
password=PassWord
login=Login

14.2 让Spring的ResourceBundleMessageSource管理配置文件

 <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="login"></property>
    
</bean>

14.3 编写controller

@RequestMapping("/inter")
public String inter(HttpServletResponse response) {
    return "login";
}

14.4 编写页面

<form>
    <fmt:message key="username"/>:<input type="text">
    <fmt:message key="password"/>:<input type="password"><br>
    <input type="submit" value="<fmt:message key="login"/>">

</form>

注意:如果是谷歌浏览器要想实现英文的国际化一定要是

image-20210121200333125

14.5 自定义LocaleResolver

14.5.1 实现LocaleResolver接口

package com.atguigu.locale;

import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class MyLocale implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String locale = request.getParameter("locale");

        if (locale != null && locale.length() > 0) {
            String[] s = locale.split("_");
            return new Locale(s[0], s[1]);
        }
        return request.getLocale();
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

14.5.2 在ioc容器中定义自己创建的LocalResolver接口

</bean>
    <bean id="localeResolver" class="com.atguigu.locale.MyLocale"></bean>
</beans>

14.5.3 页面

<form>
    <fmt:message key="username"/>:<input type="text">
    <fmt:message key="password"/>:<input type="password"><br>
    <input type="submit" value="<fmt:message key="login"/>">
    <a href="${ctp}/login2?locale=zh_CN">中文</a>
    <a href="${ctp}/login2?locale=en_US">英文</a>
</form>

14.5.4 控制器

@RequestMapping("/inter")
public String inter(HttpServletResponse response) {
    return "login";
}

@RequestMapping("/login2")
public String login2(HttpServletRequest request) {
    return "login";
}

14.6 FixedLocaleResolver:

使用系统默认的区域信息

@Override
    public Locale resolveLocale(HttpServletRequest request) {
        Locale locale = getDefaultLocale();
        if (locale == null) {
            locale = Locale.getDefault();
        }
        return locale;
    }

    @Override
    public LocaleContext resolveLocaleContext(HttpServletRequest request) {
        return new TimeZoneAwareLocaleContext() {
            @Override
            public Locale getLocale() {
                return getDefaultLocale();
            }
            @Override
            public TimeZone getTimeZone() {
                return getDefaultTimeZone();
            }
        };
    }

    @Override
    public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
        throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");
    }

14.7 SessionLocaleResolver:

区域信息是从session中获取,可以根据请求参数创建一个locale对象,把他放在session中。

@Override
    public Locale resolveLocale(HttpServletRequest request) {
        Locale locale = (Locale) WebUtils.getSessionAttribute(request, LOCALE_SESSION_ATTRIBUTE_NAME);
        if (locale == null) {
            locale = determineDefaultLocale(request);
        }
        return locale;
    }

14.8 CookieLocaleResolver

区域信息是从cookie中获取

@Override
    public Locale resolveLocale(HttpServletRequest request) {
        parseLocaleCookieIfNecessary(request);
        return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
    }

14.9 通过sessionLocaleResolver和LocaleChangeInterceptor.java对象国际化

通过获取request中的name=locale的属性,通过LocaleChangeInterceptor.java拦截器封装成Locale对象存放到session中,然后通过sessionLocalResolver取出来

image-20210121211535694

第15章 异常处理

15.1 异常源码

processDispatchResult(processedRequest, response, mappedHandler, 
    mv, dispatchException);

加了MVC异常处理,默认就是这个几个HandlerExceptionResolver

![img](/imgs/Image [52].png)

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver

如果异常解析器都不能处理就直接抛出去;

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
            HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

        boolean errorView = false;

    	//如果有异常
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            }
            else {
                
                //处理异常
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                
                //===================================
                mv = processHandlerException(request, response, handler, exception);
                
                
                errorView = (mv != null);
            }
        }

        // Did the handler return a view to render?
        if (mv != null && !mv.wasCleared()) {
               //来到页面
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
                        "': assuming HandlerAdapter completed request handling");
            }
        }

        if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            
            // Concurrent handling started during a forward
            return;
        }

        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request, response, null);
        }
    }

所有异常解析器尝试解析,解析完成进行后续,解析失败下一个解析器继续解析

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
            Object handler, Exception ex) throws Exception {

        // Check registered HandlerExceptionResolvers...
        ModelAndView exMv = null;
        for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
            exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break;
            }
        }
        if (exMv != null) {
            if (exMv.isEmpty()) {
                return null;
            }
            
            // We might still need view name translation for a plain error model...
            if (!exMv.hasView()) {
                exMv.setViewName(getDefaultViewName(request));
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
            }
            WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
            return exMv;
        }

        throw ex;
    }

15.2 ExceptionHandler

15.2.1 局部异常处理

/**
 * 只要发生了异常就会跳转到myError页面,比带有@ControllerAdvice的类中的方法优先处理
 * @exceptionHandler中的value属性代表发生什么异常会跳转到myError页面
 * @param model
 * @return
 */
@ExceptionHandler(value = Exception.class)
public String exceptionTest1(Model model) {
    model.addAttribute("exc", "我发生了异常,你信吗");
    return "myError";

}

注意异常越精确就选择哪个方法处理异常

15.2.2 全局异常处理

/**
 * 用这个注解标识的类代表专门处理异常的类,但是比普通的异常处理类优先级低
 */
@ControllerAdvice
public class ExceptionController {
    @ExceptionHandler(ArithmeticException.class)
    public String arrayException(Model model) {
        model.addAttribute("exc", "算术异常");
        return "myError";
    }
}

15.3 @ResponseStatus

编写一个异常类

package com.chenhui.component;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(reason = "拒绝登录", value = HttpStatus.NOT_ACCEPTABLE)
public class UsernameNotFoundException extends RuntimeException {
    static final long serialVersionUID = 1L;
}

测试:

    @RequestMapping("/testException2")
    public String exceptionTest2(String username){
        System.out.println("testException");
        if (!"admin".equals(username)){
            System.out.println("登录失败");
            //+++++抛出自己的错误信息
            throw new UsernameNotFoundException();
            
        }
        System.out.println("登陆成功");
        return "success";
    }

结果:

1605446703449

15.4 DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver:

判断是否是SpringMVC自带的异常或Spring自己的异常:

如:HttpRequestMethodNotSupportedException。如果没人处理则它自己处理

![img](/imgs/Image [51].png)

默认的异常有

try {
            if (ex instanceof NoSuchRequestHandlingMethodException) {
                return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, request, response,
                        handler);
            }
            else if (ex instanceof HttpRequestMethodNotSupportedException) {
                return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, request,
                        response, handler);
            }
            else if (ex instanceof HttpMediaTypeNotSupportedException) {
                return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, request, response,
                        handler);
            }
            else if (ex instanceof HttpMediaTypeNotAcceptableException) {
                return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response,
                        handler);
            }
            else if (ex instanceof MissingServletRequestParameterException) {
                return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request,
                        response, handler);
            }
            else if (ex instanceof ServletRequestBindingException) {
                return handleServletRequestBindingException((ServletRequestBindingException) ex, request, response,
                        handler);
            }
            else if (ex instanceof ConversionNotSupportedException) {
                return handleConversionNotSupported((ConversionNotSupportedException) ex, request, response, handler);
            }
            else if (ex instanceof TypeMismatchException) {
                return handleTypeMismatch((TypeMismatchException) ex, request, response, handler);
            }
            else if (ex instanceof HttpMessageNotReadableException) {
                return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, request, response, handler);
            }
            else if (ex instanceof HttpMessageNotWritableException) {
                return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, request, response, handler);
            }
            else if (ex instanceof MethodArgumentNotValidException) {
                return handleMethodArgumentNotValidException((MethodArgumentNotValidException) ex, request, response, handler);
            }
            else if (ex instanceof MissingServletRequestPartException) {
                return handleMissingServletRequestPartException((MissingServletRequestPartException) ex, request, response, handler);
            }
            else if (ex instanceof BindException) {
                return handleBindException((BindException) ex, request, response, handler);
            }
            else if (ex instanceof NoHandlerFoundException) {
                return handleNoHandlerFoundException((NoHandlerFoundException) ex, request, response, handler);
            }
        }
        catch (Exception handlerException) {
            logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
        }
        return null;
    }

14.5 SimpleMappingExceptionResolver:

通过配置的方式进行异常处理

![img](/imgs/Image [54].png)

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <!-- exceptionMappings:配置哪些异常去哪些页面 -->
        <property name="exceptionMappings">
            <props>
                <!-- key:异常全类名;value:要去的页面视图名;会走视图解析 -->
                <prop key="java.lang.NullPointerException">myerror</prop>
            </props>
        </property>
        <!--指定错误信息取出时使用的key  -->
        <property name="exceptionAttribute" value="ex"></property>
    </bean>

第16章 SpringMVC总结

SpringMVC运行流程:

1、所有请求,前端控制器(DispatcherServlet)收到请求,调用doDispatch进行处理
2、根据HandlerMapping中保存的请求映射信息找到,处理当前请求的,处理器执行链(包含拦截器)
3、根据当前处理器找到他的HandlerAdapter(适配器)
4、拦截器的preHandle先执行
5、适配器执行目标方法,并返回ModelAndView
          1)、ModelAttribute注解标注的方法提前运行
          2)、执行目标方法的时候(确定目标方法用的参数)
                    1)、有注解
                    2)、没注解:
                             1)、 看是否Model、Map以及其他的
                              2)、如果是自定义类型
                                             1)、从隐含模型中看有没有,如果有就从隐含模型中拿
                                              2)、如果没有,再看是否SessionAttributes标注的属性,如果是从Session中拿,如果拿不到会抛异常
                                             3)、都不是,就利用反射创建对象
6、拦截器的postHandle执行
7、处理结果;(页面渲染流程)
             1)、如果有异常使用异常解析器处理异常;处理完后还会返回ModelAndView
              2)、调用render进行页面渲染
                         1)、视图解析器根据视图名得到视图对象
                         2)、视图对象调用render方法;
               3)、执行拦截器的afterCompletion;

![img](/imgs/Image [55].png)

第17章 SpringMVC与Spring整合

17.1 分容目的

  1. SpringMVC和Spring整合的目的:分工明确

    1. SpringMVC的配置文件就来配置和网站转发逻辑以及网站功能有关的

      (视图解析器,文件上传解析器,支持ajax,xxx)

    2. Spring的配置文件来配置和业务有关的(事务控制,数据源,xxx)

17.2 SpringMVC和Spring分容器

Spring管理业务逻辑组件

    <context:component-scan base-package="com.atguigu">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
    </context:component-scan>

SpringMVC管理控制器组件

    <context:component-scan base-package="com.atguigu" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:include-filter type="annotation" 
 //外置异常处理类                               expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
    </context:component-scan>

Spring是一个父容器

SpringMVC是一个子容器

  • 子容器还可以引用父容器的组件
  • 父容器不能引用子容器的组件

![img](/imgs/Image [56].png)

ssm整合

image-20210123205039043

1 pom.xml依赖导入

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.atguigu</groupId>
  <artifactId>firstssm</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>firstssm Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <!-- spring版本号 -->
    <spring.version>4.1.6.RELEASE</spring.version>
    <!-- mybatis版本号 -->
    <mybatis.version>3.2.6</mybatis.version>
    <!-- log4j日志文件管理包版本 -->
    <slf4j.version>1.7.7</slf4j.version>
    <log4j.version>1.2.17</log4j.version>
  </properties>
  <dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <!-- 表示开发的时候引入,发布的时候不会加载此包 -->
    <scope>test</scope>
  </dependency>
  <!-- spring核心包 -->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-oxm</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring.version}</version>
  </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.8.10</version>
    </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <!-- mybatis核心包 -->
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${mybatis.version}</version>
  </dependency>
  <!-- mybatis/spring包 -->
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.2.2</version>
  </dependency>
  <!-- 导入java ee jar 包 -->
  <dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
  </dependency>
  <!-- 导入Mysql数据库链接jar包 -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.39</version>
  </dependency>
  <!-- c3p0连接池jar -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.9</version>
  </dependency>
  <!-- 导入dbcp的jar包,用来在applicationContext.xml中配置数据库 -->
  <dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.2.2</version>
  </dependency>
  <!-- JSTL标签类 -->
  <dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
  </dependency>
  <!-- 日志文件管理包 -->
  <!-- log start -->
  <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>${log4j.version}</version>
  </dependency>
  <!-- 格式化对象,方便输出日志 -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.1.41</version>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${slf4j.version}</version>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${slf4j.version}</version>
  </dependency>
  <!-- log end -->
  <!-- 映入JSON -->
  <dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-mapper-asl</artifactId>
    <version>1.9.13</version>
  </dependency>
  <!-- 上传组件包 -->
  <dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
  </dependency>
  <dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
  </dependency>
  <dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.9</version>
  </dependency>

  <!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
  <dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.11</version>
  </dependency>
  <!--用于数据校验-->
  <dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
  </dependency>

  <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.1.Final</version>
  </dependency>
  <dependency>
    <groupId>org.jboss.logging</groupId>
    <artifactId>jboss-logging</artifactId>
    <version>3.3.0.Final</version>
  </dependency>
  <dependency>
    <groupId>com.fasterxml</groupId>
    <artifactId>classmate</artifactId>
    <version>1.3.3</version>
  </dependency>
  </dependencies>

  <build>
    <resources>
      <resource>
        <directory>src/main/java</directory><!--所在的目录-->
        <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <!--filtering 选项 false 不启用过滤器, *.property 已经起到过滤的作用了 -->
        <filtering>false</filtering>
      </resource>
    </resources>

  </build>
</project>

2 web.xml配置

<?xml version="1.0" encoding="UTF-8"?>

<web-app>
<display-name>Archetype Created Web Application</display-name>
<!--配置Spring容器启动-->

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <servlet>
    <servlet-name>springDispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--可以不配置,默认使用/WEB-INF/<servlet-name>-servlet.xml--><!--注意自己自定义名字和位置的springmvc的配置文件不能配置到WEB-INF.xml文件-->
    <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath:springmvc/springDispatcher.xml</param-value>
        </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>springDispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
<filter>
  <filter-name>CharacterEncodingFilter</filter-name>
  <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
  <init-param>
    <param-name>encoding</param-name>
    <param-value>UTF-8</param-value>
  </init-param>
  <init-param>
    <param-name>forceEncoding</param-name>
    <param-value>true</param-value>
  </init-param>
</filter>
<filter-mapping>
  <filter-name>CharacterEncodingFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
  <filter-name>HiddenHttpMethodFilter</filter-name>
  <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>HiddenHttpMethodFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>







</web-app>

3 db.properties

jdbc.username=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306/eesy
jdbc.driverClass=com.mysql.jdbc.Driver

4 spring配置

image-20210130202749136

<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.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" use-default-filters="true">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <!--配置文件-->
    <!--classpath代表类路径下面-->
    <context:property-placeholder location="classpath:db.properties"/>
    <!--根据配置文件得到dataSource-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:myBatis/myBatisApplicationContext.xml"/>
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="classpath:myBatisXml/*.xml"/>

    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.atguigu.dao"/>
    </bean>




    <!--配置事务管理器-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--配置事务加强,事务属性,事务建议-->
    <aop:config>
        <aop:pointcut id="txPoint" expression="execution(* com.atguigu.service.*.*(..))"/>
        <aop:advisor advice-ref="myTx" pointcut-ref="txPoint"/>
    </aop:config>
    <!--注意不要导错,transaction-manager中的值默认是TransactionManager如果要指定就在transaction-manager属性中进行设置-->
    <tx:advice id="myTx" transaction-manager="dataSourceTransactionManager">
        <tx:attributes>
            <!--上面的切入点只是代表你可以用aop进行切面,真正进行事务管理是下面进行配置的内容-->
            <!--先确保service中的所有方法都要被事务管理-->
            <tx:method name="*" rollback-for="java.lang.Exception"/>
            <tx:method name="get*" read-only="true"/>
        </tx:attributes>


    </tx:advice>
</beans>

4 springmvc配置

<?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:mvc="http://www.springframework.org/schema/mvc"
       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/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!--配置扫描的组件-->    <!--只扫描controller注解的,注意use-default-filters变成false-->
    <context:component-scan base-package="com.atguigu" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    <mvc:annotation-driven/>
    <mvc:default-servlet-handler/>
    <!--配置映射解析器:如何将控制器返回的结果字符串,转换成一个物理的视图文件-->
    <bean id="internalResourceViewResolver"
          class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>

    </bean>
    <!--配置文件上传解析器-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="#{1024*1024*20}"/>
        <property name="defaultEncoding" value="utf-8"/>

    </bean>







</beans>

5 myBatis配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--mybatis的主配置文件-->

<configuration>

    <typeAliases>
        <package name="com.atguigu.domain"/>
    </typeAliases>
   <!-- <mappers>
        &lt;!&ndash;指定映射配置文件的位置,映射配置文件指的是每一个dao独立配置文件&ndash;&gt;
        &lt;!&ndash;如果使用注解来配置的话,此处应该使用class属性指定被注解的dao全限定类名&ndash;&gt;
        &lt;!&ndash;这是为了映射到UserDao.xml中去,以便进行后续的操作&ndash;&gt;
        <package name="com.atguigu.dao"/>


    </mappers>-->


</configuration>

6 dao层配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--代表你要映射的Dao接口,后面通过namespace.方法名封装成map集合的key属性-->
<mapper namespace="com.atguigu.dao.AccountDao">
    <!--建立映射关系-->
    <resultMap id="account" type="account">
       <id column="accountid" property="id"></id>
        <result column="money" property="money"></result>
        <result column="uid" property="uid"></result>
        <!-- 它是用于指定从表方的引用实体属性的 -->
        <association property="user" javaType="user">
            <id column="id" property="id"></id>
            <result column="username" property="name"></result>
            <result column="birthday" property="birthday"></result>
            <result column="sex" property="sex"></result>
            <result column="address" property="address"></result>


        </association>


    </resultMap>



    <select id="findAll" resultMap="account">
        SELECT `user`.*,`account`.`ID` accountid,money,uid FROM `user`,`account` WHERE account.uid = `user`.id


    </select>




</mapper>

SpringBoot

整理

只有一个构造器的时候才能实现根据bean自动装配进行配置

@Configuration(proxyBeanMethods =true)
/*@ConditionalOnBean(name = "animal")*///代表如果别的容器有这个类就可以注册这个容器
@ImportResource("classpath:myTestBean.xml")
@Slf4j
@ToString
public class MyConfig {
    public Animal animal;
    public Pet pet;

    public MyConfig(Animal animal, Pet pet) {
        this.animal = animal;
        this.pet = pet;
    }

    public MyConfig() {
    }

全参构造方法不能实现自动配置

public class MyConfig {
    public Animal animal;
    public Pet pet;

    public MyConfig(Animal animal, Pet pet) {
        this.animal = animal;
        this.pet = pet;
    }

这种情况下就可以自动配置MyConfig参数,Animal和pet会从容器中去寻找

springboot配置文档

https://docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/appendix-application-properties.html#common-application-properties

1 环境要求

java8及以上

Maven3.3及以上

2 maven设置

maven安装位置下conf文件夹里面的setting文件加入aliyun

以及更改jdk为1.8

<mirrors>
      <mirror>
        <id>nexus-aliyun</id>
        <mirrorOf>central</mirrorOf>
        <name>Nexus aliyun</name>
        <url>http://maven.aliyun.com/nexus/content/groups/public</url>
      </mirror>
  </mirrors>

  <profiles>
         <profile>
              <id>jdk-1.8</id>
              <activation>
                <activeByDefault>true</activeByDefault>
                <jdk>1.8</jdk>
              </activation>
              <properties>
                <maven.compiler.source>1.8</maven.compiler.source>
                <maven.compiler.target>1.8</maven.compiler.target>
                <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
              </properties>
         </profile>
  </profiles>

3 HelloWorld

需求:浏览发送/hello请求,响应Hello,SpringBoot2

3.1 创建maven工程

3.2 引入依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
    </parent>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

3.3 创建主程序

/**
 * 主程序类
 * @SpringBootApplication:这是一个SpringBoot应用
 */
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
}

3.4 编写业务

@RestController
public class HelloController {


    @RequestMapping("/hello")
    public String handle01(){
        return "Hello, Spring Boot 2!";
    }


}

3.5 测试

直接运行main方法

3.6 简化配置

application.properties中进行属性配置

server.port=8888

3.7 简化部署

 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

image-20210220171841868

然后通过target文件打开外部文件夹

image-20210220171923320

image-20210220171945883

4 了解自动配置原理

4.1 依赖管理

image-20210220181145131

1、见到很多 spring-boot-starter-* : *就某种场景
2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入
3、SpringBoot所有支持的场景
https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter
4、见到的  *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
5、所有场景启动器最底层的依赖

4.2 无需关注版本号,自动版本仲裁

1、引入依赖默认都可以不写版本
2、引入非版本仲裁的jar,要写版本号。

4.3 自动配置

自动配好Tomcat依赖

image-20210220181353307

  • 自动配好SpringMVC

    • 引入SpringMVC全套组件
    • 自动配好SpringMVC常用组件(功能)
  • 自动配好Web常见功能,如:字符编码问题

    • SpringBoot帮我们配置好了所有web开发的常见场景
  • 默认的包结构

    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
    • 无需以前的包扫描配置
    • 想要改变扫描路径,@SpringBootApplication(scanBasePackages=“com.atguigu”)
      • 或者@ComponentScan 指定扫描路径
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot") 

//默认是在启动类的包以及下面的子包所有的配置类

image-20210220191213440

5 容器功能

5.1 组件添加

@configuration

  • 基本使用

  • Full模式与Lite模式

    • 示例
    • 最佳实战
      • 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
      • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
#############################Configuration使用示例######################################################
/**
 * 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
 * 2、配置类本身也是组件
 * 3、proxyBeanMethods:代理bean的方法
 *      Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
 *      Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】
 *      组件依赖必须使用Full模式默认。其他默认是否Lite模式
 *
 *
 *
 */
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

    /**
     * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
     * @return
     */
    @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
    public User user01(){
        User zhangsan = new User("zhangsan", 18);
        //user组件依赖了Pet组件
        zhangsan.setPet(tomcatPet());
        return zhangsan;
    }

    @Bean("tom")
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }
}


################################@Configuration测试代码如下########################################
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {

    public static void main(String[] args) {
        //1、返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //2、查看容器里面的组件
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        //3、从容器中获取组件(从容器获取的组件总是相同的,无论ProxyBeanMethods是什么)

        Pet tom01 = run.getBean("tom", Pet.class);

        Pet tom02 = run.getBean("tom", Pet.class);

        System.out.println("组件:"+(tom01 == tom02));


        //4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892
        MyConfig bean = run.getBean(MyConfig.class);
        System.out.println(bean);

        //如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
        //保持组件单实例
        User user = bean.user01();
        User user1 = bean.user01();
        System.out.println(user == user1);


        User user01 = run.getBean("user01", User.class);
        Pet tom = run.getBean("tom", Pet.class);

        System.out.println("用户的宠物:"+(user01.getPet() == tom));



    }
}

@import注解

可以快速配置到spring容器,默认的ioc名字就是全类名

@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
}

@Conditional

image-20210221205646293

@Configuration(proxyBeanMethods =true)
/*@ConditionalOnBean(name = "animal")*///代表如果别的容器有这个类就可以注册这个容器


public class MyConfig {
    @ConditionalOnBean(name = "tom")//这样无法进行加载
    @Bean("animal")
    public Animal getAnimal() {
        return new Animal();
    }
    @Bean("pet")
    public Pet getPet() {
        Animal animal = getAnimal();
        return new Pet(animal);
    }
    //默认是按照字母顺序进行注册的,如果animal比tom后注册则这个类永远都不能进行注入
   /* @ConditionalOnBean(name = "animal")*/

    @Bean("tom")
    public Date getDate() {
        return new Date();
    }

}

5.2 原生配置文件引入

1、@ImportResource

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

    <bean id="haha" class="com.atguigu.boot.bean.User">
        <property name="name" value="zhangsan"></property>
        <property name="age" value="18"></property>
    </bean>

    <bean id="hehe" class="com.atguigu.boot.bean.Pet">
        <property name="name" value="tomcat"></property>
    </bean>
</beans>
@ImportResource("classpath:beans.xml")
public class MyConfig {}

======================测试=================
        boolean haha = run.containsBean("haha");
        boolean hehe = run.containsBean("hehe");
        System.out.println("haha"+haha);//true
        System.out.println("hehe"+hehe);//true

5.3 配置绑定

 */
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
    private String name;
    private String id;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}
mycar.name=兰博基尼
mycar.id=314141

也可以使用、@EnableConfigurationProperties + @ConfigurationProperties方式

@EnableConfigurationProperties(Car.class)
//1、开启Car配置绑定功能
//2、把这个Car这个组件自动注册到容器中
public class MyConfig {
}

但是Car类上上面就不用写@Component注解了

6 自动配置

6.1 引导加载自动配置类

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication{}


======================
    

6.2 @SpringBootConfiguration

@Configuration。代表当前是一个配置类

6.3 @ComponentScan

指定扫描哪些,Spring注解;

6.4 @EnableAutoConfiguration

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}

1、@AutoConfigurationPackage

自动配置包?指定了默认的包规则

@Import(AutoConfigurationPackages.Registrar.class)  //给容器中导入一个组件
public @interface AutoConfigurationPackage {}

//利用Registrar给容器中导入一系列组件
//将指定的一个包下的所有组件导入进来?MainApplication 所在包下。

2、@Import(AutoConfigurationImportSelector.class)

image-20210222115350345

1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。
    默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
    spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories
    

image.png

文件里面写死了spring-boot一启动就要给容器中加载的所有配置类
spring-boot-autoconfigure-2.3.4.RELEASE.jar/META-INF/spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

6.2、按需开启自动配置项

虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则(@Conditional),最终会按需配置。

6.3、修改默认配置

        @Bean
        @ConditionalOnBean(MultipartResolver.class)  //容器中有这个类型组件
        @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
        public MultipartResolver multipartResolver(MultipartResolver resolver) {
            //给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
            //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
            // Detect if the user has created a MultipartResolver but named it incorrectly
            return resolver;
        }
给容器中加入了文件上传解析器

SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先

@Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
    }

总结:

  • SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定

  • 生效的配置类就会给容器中装配很多组件

  • 只要容器中有这些组件,相当于这些功能就有了

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改。

xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 —-> application.properties

image-20210222113926659

image-20210222114047948

自己去修改配置文件中的值进行替换

7 开发小技巧

lombok

简化JavaBean开发

  <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
================================简化日志开发===================================
@Slf4j
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String handle01(@RequestParam("name") String name){
        
        log.info("请求进来了....");
        
        return "Hello, Spring Boot 2!"+"你好:"+name;
    }
}

Spring Initailizr

image-20210222143427794

8 yml配置文件的写法

默认application.properties文件先加载,然后加载yml配置文件

8.1 基本语法

  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • ‘#‘表示注释
  • 字符串无需加引号,如果要加,‘‘与"“表示字符串内容 会被 转义/不转义(即”\n"会换行,但是’\n’不会换行)

8.2 数据类型

  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
  • 对象:键值对的集合。map、hash、set、object
行内写法:  k: {k1:v1,k2:v2,k3:v3}
#或
k: 
    k1: v1
  k2: v2
  k3: v3
  • 数组:一组按次序排列的值。array、list、queue
行内写法:  k: [v1,v2,v3]
#或者
k:
 - v1
 - v2
 - v3

8.3 示例

@Data
public class Person {
    
    private String userName;
    private Boolean boss;
    private Date birth;
    private Integer age;
    private Pet pet;
    private String[] interests;
    private List<String> animal;
    private Map<String, Object> score;
    private Set<Double> salarys;
    private Map<String, List<Pet>> allPets;
}

@Data
public class Pet {
    private String name;
    private Double weight;
}
# yaml表示以上对象
person:
  userName: zhangsan
  boss: false
  birth: 2019/12/12 20:12:33
  age: 18
  pet: 
    name: tomcat
    weight: 23.4
  interests: [篮球,游泳]
  animal: 
    - jerry
    - mario
  score:
    english: 
      first: 30
      second: 40
      third: 50
    math: [131,140,148]
    chinese: {first: 128,second: 136}
  salarys: [3999,4999.98,5999.99]
  allPets:
    sick:
      - {name: tom}
      - {name: jerry,weight: 47}
    health: [{name: mario,weight: 47}]

8.4 yml配置提示

自定义的类和配置文件绑定一般没有提示。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>


 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

9 配置静态资源访问

只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources

访问 : 当前项目根路径/ + 静态资源名

原理: 静态映射/**。

请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面

改变默认的静态资源路径

spring:
  mvc:
    static-path-pattern: /res/**

  resources:
    static-locations: [classpath:/haha/]

2、静态资源访问前缀

默认无前缀

spring:
  mvc:
    static-path-pattern: /res/**

当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找

3、webjar

自动映射 /webjars/**

https://www.webjars.org/

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.5.1</version>
        </dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

4、欢迎页支持

  • 静态资源路径下 index.html

    • 可以配置静态资源路径
    • 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致welcome page功能失效

  resources:
    static-locations: [classpath:/haha/]
  • controller能处理/index

5、自定义 Favicon

favicon.ico 放在静态资源目录下即可。

spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致 Favicon 功能失效

image-20210222184944209

10 默认静态资源路径,以及环境页面的模板

image-20210222210706750

11 处理rest请求源码

image-20210223094541396

自定义filter

//自定义filter
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        //通过改变methodParam属性来改变request.getParamether(this.methodParam)的值
        methodFilter.setMethodParam("_m");
        return methodFilter;
    }

12 处理器选择源码

image-20210223110737353

`

13 矩阵变量的使用

默认是关闭的需要开启

@Bean
public WebMvcConfigurer webMvcConfigurer() {

    return new WebMvcConfigurer() {
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            UrlPathHelper urlPathHelper = new UrlPathHelper();
            //设置不移除矩形url
            urlPathHelper.setRemoveSemicolonContent(false);
            configurer.setUrlPathHelper(urlPathHelper);
        }
    };
}

image-20210223122244570

当只有一个的情况

  ///cars/a;name=江豪迪;id="jiang"
    @RequestMapping("/cars/{path}")
    public HashMap<String, String> test1(@MatrixVariable("name")String name
            , @MatrixVariable("id") String id,@PathVariable("path")String path){
        HashMap<String, String> map = new HashMap<>();
        map.put(name, name);
        map.put(id, id);


        return map;
    }


}

14 springMvc参数解析流程

14.1 普通注解源码

@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody

image-20210224102822206

14.2 、Servlet API:

WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

ServletRequestMethodArgumentResolver 以上的部分参数

@Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> paramType = parameter.getParameterType();
        return (WebRequest.class.isAssignableFrom(paramType) ||
                ServletRequest.class.isAssignableFrom(paramType) ||
                MultipartRequest.class.isAssignableFrom(paramType) ||
                HttpSession.class.isAssignableFrom(paramType) ||
                (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
                Principal.class.isAssignableFrom(paramType) ||
                InputStream.class.isAssignableFrom(paramType) ||
                Reader.class.isAssignableFrom(paramType) ||
                HttpMethod.class == paramType ||
                Locale.class == paramType ||
                TimeZone.class == paramType ||
                ZoneId.class == paramType);
    }

14.3 Map和Model底层源码

public String hello(Map<String, String> map, Model model) {

image-20210224102730967

image-20210224102948872

14.4 将map,Model放入请求中

image-20210407215955819

14.5 方法参数中自定义对象的解析流程

image-20210407220022629

14.6 不同返回值请求处理参数

不同handler请求处理返回

14.7 responsebody请求策略

response请求策略

image-20210227213413848

15 自定义通过请求转换参数convert

@Bean
public WebMvcConfigurer webMvcConfigurer() {

    return new WebMvcConfigurer() {
    //通过请求转换参数
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addConverter(new Converter<String, Car>() {

                @Override
                public Car convert(String source) {
                    if (!StringUtils.isEmpty(source)) {

                        String[] split = source.split(",");
                        Car car = new Car();
                        car.setName(split[0]);
                        car.setId(split[1]);
                        return car;
        
                    }
                    return null;
                }
            });
            
        }
 

16 自定义策略以及消息转换

@Bean
public WebMvcConfigurer webMvcConfigurer() {

    return new WebMvcConfigurer() {
      
    //新增参数解析器
        @Override
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

            converters.add(new HttpMessageConverter<Object>() {
                @Override
                public boolean canRead(Class<?> clazz, MediaType mediaType) {
                    return true;
                }

                @Override
                public boolean canWrite(Class<?> clazz, MediaType mediaType) {
                    return Pet.class.isAssignableFrom(clazz);
                }

                @Override
                public List<MediaType> getSupportedMediaTypes() {
                    String str = "application/pet";
                    //生成一个单例集合
                    List<String> strings = new ArrayList<>(Collections.singletonList(str));
                    strings.add("text/html;charset=UTF-8");

                    //把一个集合变成MediaType类型
                    return MediaType.parseMediaTypes(strings);
                }


                @Override
                public Object read(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
                    return null;
                }

                @Override
                public void write(Object object, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                    if (object instanceof Pet) {
                        Pet pet = (Pet) object;
                        Animal animal = pet.getAnimal();
                        String petString = animal.toString()+"是pet的动物";
                        byte[] bytes = petString.getBytes(StandardCharsets.UTF_8);
                        //设置以防中文乱码
                        outputMessage.getHeaders().setContentType(MediaType.parseMediaType("text/html;charset=UTF-8"));
                        //在页面中显示自定义解析器
                        outputMessage.getBody().write(bytes);
                    }
                }
            });
             @Override
            //相当于format=pet然后通过map集合来得到这个mediaType
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
               HashMap<String, MediaType> stringMediaTypeHashMap = new HashMap<>();
               stringMediaTypeHashMap.put("pet", MediaType.parseMediaType("application/pet"));
               stringMediaTypeHashMap.put("json", MediaType.APPLICATION_JSON);
               stringMediaTypeHashMap.put("xml", MediaType.APPLICATION_XML);
               ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(stringMediaTypeHashMap);
                configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy,new HeaderContentNegotiationStrategy()));
            }
            
        }

17 模板引擎-Thymeleaf

1、thymeleaf简介

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.

现代化、服务端Java模板引擎

2、基本语法

1、表达式

表达式名字 语法 用途
变量取值 ${…} 获取请求域、session域、对象等值
选择变量 *{…} 获取上下文对象值
消息 #{…} 获取国际化等值
链接 @{…} 生成链接
片段表达式 ~{…} jsp:include 作用,引入公共页面片段

2、字面量

文本值: ‘one text’ , ‘Another one!’ **,…**数字: 0 , 34 , 3.0 , 12.3 **,…**布尔值: true , false

空值: null

变量: one,two,…. 变量不能有空格

3、文本操作

字符串拼接: +

变量替换: |The name is ${name}|

4、数学运算

运算符: + , - , * , / , %

5、布尔运算

运算符: and , or

一元运算: ! , not

** **

6、比较运算

比较: > , < , >= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )

7、条件运算

If-then: (if) ? (then)

If-then-else: (if) ? (then) : (else)

Default: (value) ?: (defaultvalue)

8、特殊操作

无操作: _

3、设置属性值-th:attr

设置单个值

<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  </fieldset>
</form>

设置多个值

<img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

以上两个的代替写法 th:xxxx

<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">

4、迭代

<tr th:each="prod : ${prods}">
        <td th:text="${prod.name}">Onions</td>
        <td th:text="${prod.price}">2.41</td>
        <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
  <td th:text="${prod.name}">Onions</td>
  <td th:text="${prod.price}">2.41</td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

5、条件运算

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>

image.png

6 thymeleaf的使用

6.1 引入Starter

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

6.2 自动配置好thymeleaf

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }

自动配好的策略

  • 1、所有thymeleaf的配置值都在 ThymeleafProperties
  • 2、配置好了 SpringTemplateEngine
  • 3、配好了 ThymeleafViewResolver
  • 4、我们只需要直接开发页面
  public static final String DEFAULT_PREFIX = "classpath:/templates/";


    public static final String DEFAULT_SUFFIX = ".html";  //xxx.html

6.3 页面开发

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
    <a href="www.atguigu.com" th:href="${link}">去百度</a>  <br/>
    <a href="www.atguigu.com" th:href="@{link}">去百度2</a>
</h2>
</body>
</html>

6.4 更改springmvc默认的templates

image-20210302214741838

18 配置拦截器

1、HandlerInterceptor 接口

/**
 * 登录检查
 * 1、配置好拦截器要拦截哪些请求
 * 2、把这些配置放在容器中
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        log.info("preHandle拦截的请求路径是{}",requestURI);

        //登录检查逻辑
        HttpSession session = request.getSession();

        Object loginUser = session.getAttribute("loginUser");

        if(loginUser != null){
            //放行
            return true;
        }

        //拦截住。未登录。跳转到登录页
        request.setAttribute("msg","请先登录");
//        re.sendRedirect("/");
        request.getRequestDispatcher("/").forward(request,response);
        return false;
    }

    /**
     * 目标方法执行完成以后
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle执行{}",modelAndView);
    }

    /**
     * 页面渲染以后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion执行异常{}",ex);
    }
}

2、配置拦截器

/**
 * 1、编写一个拦截器实现HandlerInterceptor接口
 * 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
 * 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
 */
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")  //所有请求都被拦截包括静态资源
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
    }
}

拦截器原理

1、根据当前请求,找到**HandlerExecutionChain【**可以处理请求的handler以及handler的所有 拦截器】

2、先来顺序执行 所有拦截器的 preHandle方法

  • 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
  • 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;

3、如果任何一个拦截器返回false。直接跳出不执行目标方法

4、所有拦截器都返回True。执行目标方法

5、倒序执行所有拦截器的postHandle方法。

6、前面的步骤有任何异常都会直接倒序触发 afterCompletion

7、页面成功渲染完成以后,也会倒序触发 afterCompletion

image.png

19 配置文件上传处理机制

<form method="post" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file"><br>
    <input type="text" name="xiao"/>
    <input type="submit" value="提交">
</form>
@RequestMapping("/upload")
public String upload(@RequestPart("file")MultipartFile multipartFile, HttpServletRequest request) throws Exception {
    String originalFilename = multipartFile.getOriginalFilename();
    multipartFile.transferTo(new File("D:\\upload\\"+originalFilename));
    request.setAttribute("xiao", "我不是人");
    return "login";
}
servlet:
  multipart:
    max-file-size: 20MB
    max-request-size: 100MB

文件上传底层源码

20 springboot默认异常处理机制

image-20210302220128683

这些参数都可以当成一个属性在thymeleaf中引用

20.1 默认异常处理机制

image-20210302220252547

如果是浏览器发出的请求,处理的是/error错误页面

如果是类型客户端像postman发出的请求返回的默认是json的错误信息

image-20210301151930891

20.2 自定义异常处理

xx代表什么都可以

image-20210301151633067

20.3 自定义异常处理源码

默认的springboot异常处理机制

20.4 springmvc异常处理机制

image-20210302215339614

20.5 自定义handlerResolver

只要标注了注解就会在image-20210302220048142

添加

@Component
public class MyException implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            response.sendError(300,ex.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
        }

        return new ModelAndView();

    }

21 Web原生组件的注入

使用api的方式

在启动类中加入@ServletComponentScan(“com.atguigu.boot”)

加入@WebServlet注解

@WebServlet("/woshi")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("经过了servlet");
        resp.getWriter().write("666");

    }
}

使用配置方式进行注册

@Configuration
public class Myconfig2 {
    @Bean
    public ServletRegistrationBean<MyServlet> getServlet() {
        return new ServletRegistrationBean<>(new MyServlet(),"/xiao","/gege");
    }
    @Bean
    public ServletListenerRegistrationBean<MyListen> getListener(){
        return new ServletListenerRegistrationBean<MyListen>(new MyListen());
    }
    @Bean
    public FilterRegistrationBean<MyFilter> getFilter() {
        //其中getServlet()代表用getServlet里面注册的路径
        return new FilterRegistrationBean<>(new MyFilter(),getServlet());
    }
    }

//注意配置类中的proxy

@Configuration(proxyBeanMethods = true)

如果是false每次都会调用方法都会创建出一个新的对象

image-20210303102500937

如果精确的是/my路径,那么就交给tomcat进行处理

22 springboot底层自动配置dispatcherServlet

C:\Users\10185.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.3.4.RELEASE\spring-boot-autoconfigure-2.3.4.RELEASE.jar!\org\springframework\boot\autoconfigure\web\servlet\DispatcherServletAutoConfiguration.class

里面用

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {

   @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
   public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
      DispatcherServlet dispatcherServlet = new DispatcherServlet();
      dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
      dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
      dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
      dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
      dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
      return dispatcherServlet;
   }

进行配置dispatcherServlet组件

然后

protected static class DispatcherServletRegistrationConfiguration {

   @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
   @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
   public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
         WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
      DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
            webMvcProperties.getServlet().getPath());
      registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
      registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
      multipartConfig.ifAvailable(registration::setMultipartConfig);
      return registration;
   }

}

从配置类中拿到配置信息,默认是/路径下的所有,可以通过spring.mvc来进行修改配置信息

23 底层配置tomcat原理

底层自动配置tomcat

24 定制化原理

24.1 定制化的常见的方式

  • 修改配置文件

  • xxxxCustomizer:定制化器,可以改变xxxx的默认规则

    import org.springframework.boot.web.server.WebServerFactoryCustomizer;
    import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
    import org.springframework.stereotype.Component;
    
    @Component
    public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
    
        @Override
        public void customize(ConfigurableServletWebServerFactory server) {
            server.setPort(9000);
        }
    
    }
    
  • 编写自定义配置类 xxxConfiguration;+@Bean替换

因为springboot默认配置的情况是如果容器中已经有就不进行配置了

因此如果使用xxxConfiguration+@Bean可以进行自己配置

Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件

@Configuration
public class AdminWebConfig implements WebMvcConfigurer
  • @EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能

    • 原理
    • 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…..
    • 2、一旦使用 @EnableWebMvc 、。会 @Import(DelegatingWebMvcConfiguration.class)
    • 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
      • 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
      • 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
      • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
    • 4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    • 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
  • … …

25 数据源的自动配置-HikariDataSource

25.1 导入jdbc场景

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>

image-20210304094658597

25.2 导入mysql数据库驱动

因为官方不知道需要用什么类型的数据库,因此需要自己导入mysql数据库驱动

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

25.3 分析自动配置

  • DataSourceAutoConfiguration : 数据源的自动配置

    • 修改数据源相关的配置:spring.datasource
    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
    • 底层配置好的连接池是:HikariDataSource
    @Configuration(proxyBeanMethods = false)
    @Conditional(PooledDataSourceCondition.class)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
            DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
            DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
    protected static class PooledDataSourceConfiguration

** **

  • DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置

  • JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud

    • 可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate
    • @Bean@Primary JdbcTemplate;容器中有这个组件
  • JndiDataSourceAutoConfiguration: jndi的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

25.4 修改配置项

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_account
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

25.5 测试

package com.atguigu.boot;


import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

/**
 * @author 10185
 * @create 2021/3/4 9:21
 */
@SpringBootTest
public class QueryTest {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    public void testTemplate() {
        Integer integer = jdbcTemplate.queryForObject("select count(*) from t_menu", Integer.class);
        System.out.println(integer);

    }

}

26 改成Druid数据源,并进行配置

26.0 Druid的官方文档

https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98

26.1 配置文件中加入Druid的数据源

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.5</version>
</dependency>

26.2 进行spring组件的装配

如果spring组件中没有数据源才会自动装配HikariDataSource,因此只要在组件中加入druid就不会进行自动装配,并开启监控功能,同时加入@configurationProperties注解用于把数据源的信息自动装配到数据源中

@Bean
@ConfigurationProperties("spring.datasource")
public DataSource druidDataSource() throws SQLException {
    DruidDataSource druidDataSource = new DruidDataSource();
    //开启监控功能
    druidDataSource.setFilters("stat,slf4j");
    return druidDataSource;
}

26.3 进行配置Servlet进行请求的处理

配置如果发送请求druid/*请求就会进行druid的监控页面,并设置登录的账号和密码

@Bean
public ServletRegistrationBean<StatViewServlet> DruidStatView() {

    //添加servlet组件,用于接受/druid/*请求的数据
    ServletRegistrationBean<StatViewServlet> statViewServletServletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
    //添加账号和密码
    statViewServletServletRegistrationBean.addInitParameter("loginUsername", "mars");
    statViewServletServletRegistrationBean.addInitParameter("loginPassword", "123");
    return statViewServletServletRegistrationBean;
}

27 用springboot start 自动配置druid

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.17</version>
</dependency>

image-20210304123256422

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_account
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

    druid:
      aop-patterns: com.atguigu.admin.*  #监控SpringBean
      filters: stat,wall     # 底层开启功能,stat(sql监控),wall(防火墙)

      stat-view-servlet:   # 配置监控页功能
        enabled: true
        login-username: admin
        login-password: admin
        resetEnable: false

      web-stat-filter:  # 监控web
        enabled: true
        urlPattern: /*
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'


      filter:
        stat:    # 对上面filters里面的stat的详细配置
          slow-sql-millis: 1000
          logSlowSql: true
          enabled: true
        wall:
          enabled: true
          config:
            drop-table-allow: false

SpringBoot配置示例

https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

配置项列表https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8

注意:默认的druid监控页默认是关闭的

28 springboot整合mybatis

pom.xml加入依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

mapper.xml文件地址的设置

设置mapper.xml文件的地址

mybatis:
  mapper-locations: classpath:mapperxml/*.xml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_account
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

逆向生成文件

image-20210305212245719

配置mybatis配置文件的位置

# 配置mybatis规则
mybatis:
#  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
  #配置驼峰命名的方式来解析变量
  configuration:
    map-underscore-to-camel-case: true
    
 可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可,两个只可以使用一个如果写了configuration配置文件,那么就不能使用写xml配置文件的方式来配置

如果没有@Mapper注解,也可以使用

@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    mapperScannerConfigurer.setBasePackage("com.atguigu.boot.mapper");
    return mapperScannerConfigurer;

或者用这个@MapperScan(“com.baomidou.mybatisplus.mapper”)

方式来指定maper的存放的位置

29 myBatisPlus的使用

https://baomidou.com/guide/

通过这个生成

image-20210306093153291

@Mapper
public interface RoleMapper extends BaseMapper<Role> {
}

image-20210306093238709

29.1 myBatisPlus实现主键自增

AUTO(0), //数据可id自增 NONE(1), //未设置主键 INPUT(2), //手动输入 ID_WORKER(3), //默认的全局唯一id UUID(4), //全局唯一id uuid ID_WORKER_STR(5); // ID_WORKEK 字符串表示法

29.2 myBatisPlus设置createTime

第一种方式

通过更改数据库中的

image-20210306103005780

更改默认为当地的时间戳

(一般不用,不允许更改数据库)

第二种方式

@TableField(fill = FieldFill.INSERT)
private Date createTime;

//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
package com.atguigu.boot.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Date;

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    //插入时候的填充策略
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");  //日志
        //设置字段的值(String fieldName字段名,Object fieldVal要传递的值,MetaObject metaObject)
        this.fillStrategy(metaObject, "createTime", new Date());
        this.fillStrategy(metaObject, "updateTime", new Date());

        
        //this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
       // this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
        /* 上面选其一使用,下面的已过时(注意 strictInsertFill 有多个方法,详细查看源码) */
        //this.setFieldValByName("operator", "Jerry", metaObject);
        //this.setInsertFieldValByName("operator", "Jerry", metaObject);
    }

    //更新时间的填充策略
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        this.fillStrategy(metaObject, "updateTime", new Date());
        
        
        //this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
       // this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
        /* 上面选其一使用,下面的已过时(注意 strictUpdateFill 有多个方法,详细查看源码) */
        //this.setFieldValByName("operator", "Tom", metaObject);
        //this.setUpdateFieldValByName("operator", "Tom", metaObject);
    }
}

测试

@ResponseBody
@RequestMapping("/saveRole")
public String saveRole() {
    Role role = new Role();
    role.setName("小");
    roleService.save(role);
    return "成功";



}

//自动会加入时间戳

29.3 乐观锁

面试中经常会问到乐观锁,悲观锁

乐观锁:顾名思义十分乐观,它总是被认为不会出现问题,无论干什么都不去上锁!如果出现了问题,再次更新测试

悲观锁:顾名思义十分悲观,它总是出现问题,无论干什么都会上锁!再去操作!

乐观锁实现方式

取出记录是,获取当前version 更新时,带上这个version 执行更新事,set version=newVersion where version =oldVersion 如果version不对,就更新失败 乐观锁: 1、先查询,获得版本号 version=1

--A
update user set name ="shuishui" ,version =version+1
where id =2 and version=1

--B 如果线程抢先完成,这个时候version=2,会导致A修改失败
update user set name ="shuishui" ,version =version+1
where id =2 and version=1

使用乐观锁

1 在数据库中加入version字段

image-20210306140312911

2 在pojo类中加入version字段
@Version
private String version;
3 加入配置
//开启事务
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {

    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }
}

29.4 查询操作

// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
————————————————
版权声明本文为CSDN博主?Handsome?的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明
原文链接https://blog.csdn.net/zdsg45/article/details/105138493/

29.5 myBatis-plus自带分页查询

放入分页的组件

@Bean
public PaginationInterceptor paginationInterceptor() {
    PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
    //指定数据库类型,以防每次都要去适配数据库
    paginationInterceptor.setDbType(DbType.MYSQL);
    return paginationInterceptor;
}
@ResponseBody
@RequestMapping("/pageTest")
public List<Role> pageTest() {
    Page<Role> rolePage = new Page<>(1, 5);
    roleMapper.selectPage(rolePage, null);
    return rolePage.getRecords();
}

29.6 删除操作

// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

29.7 配置逻辑删除

在表中添加一个字段

image-20210306155422737

同时在类中也加入相同

private String deleted;

mybatis3.3以前

image-20210306155233852

myBatis 3.3 以后

global-config:
  db-config:
    logic-delete-field: deleted
    logic-delete-value: 1
    logic-not-delete-value: 0

使用logic-delete-field:deleted 来指定全局的逻辑删除指标 相当于deleted

如果某个类全局逻辑指标要自己定义的时候添加

@TableLogic
private String deleted;

默认先从注解开始寻找,如果注解没有指定逻辑删除指标的时候,在从全局配置文件中寻找,如果都没有,那么就没有逻辑删除

注意:只针对myBatis-plus创建的sql有效,自己创建sql没有效果

29.8 代码自动生成器


代码自动生成器
daopojoconrtrollerservice自动生成

AutoGenerator 是 MyBatis-Plus 的代码生成器通过 AutoGenerator 可以快速生成 Entity MapperMapper XMLServiceController 等各个模块的代码极大的提升了开发效率

package com.kuang;
import com.baomidou.mybatisplus.annotation.DbType; 
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType; 
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType; 
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

import java.util.ArrayList;
// 代码自动生成器 
public class KuangCode {
     public static void main(String[] args) {       
         // 需要构建一个 代码自动生成器 对象        
         AutoGenerator mpg = new AutoGenerator();   
         // 配置策略
         // 1、全局配置        
         GlobalConfig gc = new GlobalConfig();    
         String projectPath = System.getProperty("user.dir"); 
         gc.setOutputDir(projectPath+"/src/main/java");    
         gc.setAuthor("狂神说");     
         gc.setOpen(false);      
         gc.setFileOverride(false);  // 是否覆盖     
         gc.setServiceName("%sService"); // 去Service的I前缀      
         gc.setIdType(IdType.ID_WORKER);  
         gc.setDateType(DateType.ONLY_DATE);  
         gc.setSwagger2(true);    
         mpg.setGlobalConfig(gc);
         
          //2、设置数据源  
         DataSourceConfig dsc = new DataSourceConfig();  
         dsc.setUrl("jdbc:mysql://localhost:3306/kuang_community? useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8");       
         dsc.setDriverName("com.mysql.cj.jdbc.Driver");   
         dsc.setUsername("root");      
         dsc.setPassword("123456");  
         dsc.setDbType(DbType.MYSQL);  
         mpg.setDataSource(dsc);
 
          //3、包的配置     
         PackageConfig pc = new PackageConfig();  
         pc.setModuleName("blog");     
         pc.setParent("com.kuang");    
         pc.setEntity("entity");  
         pc.setMapper("mapper");     
         pc.setService("service");   
         pc.setController("controller");    
         mpg.setPackageInfo(pc);
 
          //4、策略配置      
         StrategyConfig strategy = new StrategyConfig();       
  strategy.setInclude("blog_tags","course","links","sys_settings","user_record"," user_say"); // 设置要映射的表名
         strategy.setNaming(NamingStrategy.underline_to_camel);   
         strategy.setColumnNaming(NamingStrategy.underline_to_camel);  
         // 自动lombok
         strategy.setEntityLombokModel(true);
         strategy.setLogicDeleteFieldName("deleted");  
         // 自动填充配置      
         TableFill gmtCreate = new TableFill("gmt_create", FieldFill.INSERT); 
         TableFill gmtModified = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE);    
         ArrayList<TableFill> tableFills = new ArrayList<>(); 
         tableFills.add(gmtCreate);    
         tableFills.add(gmtModified);     
         strategy.setTableFillList(tableFills);   
         // 乐观锁      
         strategy.setVersionFieldName("version");
         strategy.setRestControllerStyle(true); 
         strategy.setControllerMappingHyphenStyle(true);
         // localhost:8080/hello_id_2     
         mpg.setStrategy(strategy);
         mpg.execute(); 
         //执行   
     } 
}

————————————————
版权声明本文为CSDN博主?Handsome?的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明
原文链接https://blog.csdn.net/zdsg45/article/details/105138493/

30 nosql redis

30.1 添加redis环境依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

image-20210307101305775

30.2 购买阿里云

image-20210306203121155

image-20210307093729518

image-20210307095831056

image-20210307095914857

image-20210307101347517

redis环境搭建

1、阿里云按量付费redis。经典网络

2、申请redis的公网连接地址

3、修改白名单 允许0.0.0.0/0 访问

30.3 redisSpringboot的操作

  redis:
# 这种方式用如果有重复会报错
#    host: r-bp1r4qcmqvrm4wbfoppd.redis.rds.aliyuncs.com
#    port: 6379
#    password: xiaodidi:Jhd3141415996

	
    url: redis://xiaodidi:Jhd3141415996@r-bp1r4qcmqvrm4wbfoppd.redis.rds.aliyuncs.com:6379

30.4 使用jedis客户端

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>lettuce-core</artifactId>
            <groupId>io.lettuce</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!--加入jedis客户端-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
@Test
public void testConnection() {
    ValueOperations<String, String> stringStringValueOperations =
            stringRedisTemplate.opsForValue();
    stringStringValueOperations.set("didi", "gege");
    String s = stringStringValueOperations.get("xiao");
    System.out.println(s);
    Class<? extends RedisConnectionFactory> aClass = redisConnectionFactory.getClass();
    System.out.println(aClass);
}

30.5 完成拦截每一个请求并计数的小实验

@Component
public class MyInterceptor implements HandlerInterceptor {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //每次过来加1
            stringRedisTemplate.opsForValue().increment(request.getRequestURI(), 1);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("执行postHandler");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("执行afterCompletion");
    }
}
@Configuration
public class MyWebConfigurer implements WebMvcConfigurer {
    @Autowired
    MyInterceptor interceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns("/login");

    }
}

31 springbootTest

1 基本依赖以及改变

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。

JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。

JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。

image

注意:

SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)

JUnit 5’s Vintage Engine Removed from spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

现在版本

@SpringBootTest
class Boot05WebAdminApplicationTests {


    @Test
    void contextLoads() {

    }
}

以前版本

@SpringBootTest + @RunWith(SpringTest.class

SpringBoot整合Junit以后。

  • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  • Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚

2 jUnit5 常用注解

JUnit5的注解与JUnit4的注解有所变化

https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

  • **@Test :**表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • **@ParameterizedTest :**表示方法是参数化测试,下方会有详细介绍
  • **@RepeatedTest :**表示方法可重复执行,下方会有详细介绍
  • **@DisplayName :**为测试类或者测试方法设置展示名称
  • **@BeforeEach :**表示在每个单元测试之前执行
  • **@AfterEach :**表示在每个单元测试之后执行
  • **@BeforeAll :**表示在所有单元测试之前执行
  • **@AfterAll :**表示在所有单元测试之后执行
  • **@Tag :**表示单元测试类别,类似于JUnit4中的@Categories
  • **@Disabled :**表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
  • **@Timeout :**表示测试方法运行如果超过了指定时间将会返回错误
  • **@ExtendWith :**为测试类或测试方法提供扩展类引用
import org.junit.jupiter.api.Test; //注意这里使用的是jupiter的Test注解!!


public class TestDemo {

  @Test
  @DisplayName("第一次测试")
  public void firstTest() {
      System.out.println("hello world");
  }

注意@springbootTest注解的使用方法和控制器类似,需要放到spring启动类相同的包或者子包

3 断言机制

这些类都在Assertions下的静态方法

因此可以通过导入Assertions

import static org.junit.jupiter.api.AssertEquals.assertEquals;

也可以直接类名加方法名进行调用

用来对单个值进行简单的验证。如:

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null

2、数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

@Test
@DisplayName("array assertion")
public void array() {
 assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

3、组合断言

assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言

@Test
@DisplayName("assert all")
public void all() {
 assertAll("Math",
    () -> assertEquals(2, 1 + 1),
    () -> assertTrue(1 > 0)
 );
}

4、异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用**@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows()** ,配合函数式编程就可以进行使用。

@Test
@DisplayName("异常测试")
public void exceptionTest() {
    ArithmeticException exception = Assertions.assertThrows(
           //扔出断言异常
            ArithmeticException.class, () -> System.out.println(1 % 0));

}

5、超时断言

Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间

@Test
@DisplayName("超时测试")
public void timeoutTest() {
    //如果测试方法时间超过1s将会异常
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}

6、快速失败

通过 fail 方法直接使得测试失败

@Test
@DisplayName("fail")
public void shouldFail() {
 fail("This should fail");
}

7、前置条件(assumptions)

JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

@DisplayName("前置条件")
public class AssumptionsTest {
 private final String environment = "DEV";
 
 @Test
 @DisplayName("simple")
 public void simpleAssume() {
    assumeTrue(Objects.equals(this.environment, "DEV"));
    assumeFalse(() -> Objects.equals(this.environment, "PROD"));
 }
 
 @Test
 @DisplayName("assume then do")
 public void assumeThenDo() {
    assumingThat(
       Objects.equals(this.environment, "DEV"),
       () -> System.out.println("In DEV")
    );
 }
}

assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

5、嵌套测试

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop);
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

6、参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

** **

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

@NullSource: 表示为参数化测试提供一个null的入参

@EnumSource: 表示为参数化测试提供一个枚举入参

@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参

@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
    System.out.println(string);
    Assertions.assertTrue(StringUtils.isNotBlank(string));
}


@ParameterizedTest
@MethodSource("method")    //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
    System.out.println(name);
    Assertions.assertNotNull(name);
}

static Stream<String> method() {
    return Stream.of("apple", "banana");
}

7、迁移指南

在进行迁移的时候需要注意如下的变化:

  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
  • 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
  • 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
  • 把@Ignore 替换成@Disabled。
  • 把@Category 替换成@Tag。
  • 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。

32 指标监控

1、SpringBoot Actuator

1、简介

未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

image.png

2、1.x与2.x的不同

image.png

3、如何使用

  • 引入场景
  • 访问 http://localhost:8080/actuator/**
  • 暴露所有监控信息为HTTP
management:
  endpoints:
    enabled-by-default: true #暴露所有端点信息
    web:
      exposure:
        include: '*'  #以web方式暴露
  • 测试

http://localhost:8080/actuator/beans

http://localhost:8080/actuator/configprops

http://localhost:8080/actuator/metrics

http://localhost:8080/actuator/metrics/jvm.gc.pause

http://localhost:8080/actuator/endpointName/detailPath 。。。。。。

4、可视化

https://github.com/codecentric/spring-boot-admin

2、Actuator Endpoint

1、最常使用的端点

ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
mappings 显示所有@RequestMapping路径列表。
scheduledtasks 显示应用程序中的计划任务。
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown 使应用程序正常关闭。默认禁用。
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump 执行线程转储。

如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:

ID 描述
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile 返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

management:
  endpoints:
    web:
      exposure:
        include: '*'
        #默认就是开启的,不用进行变化
    enabled-by-default: true

2、Health Endpoint

健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

重要的几点:

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
  • 可以很容易的添加自定义的健康检查机制

image.png

//开启health细节展示,默认只有一个health状态显示
  endpoint:
    health:
      show-details: always

image-20210307213215493

3、Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;

  • 通过Metrics对接多种监控系统
  • 简化核心Metrics开发
  • 添加自定义Metrics或者扩展已有Metrics

image.png

4、管理Endp2oints

1、开启与禁用Endpoints
  • 默认所有的Endpoint除过shutdown都是开启的。
  • 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint..enabled = true
management:
  endpoint:
    beans:
      enabled: true
  • 或者禁用所有的Endpoint然后手动开启指定的Endpoint
management:
  endpoints:
    enabled-by-default: false
  endpoint:
    beans:
      enabled: true
    health:
      enabled: true
2、暴露Endpoints

支持的暴露方式

  • HTTP:默认只暴露healthinfo Endpoint
  • JMX:默认暴露所有Endpoint
  • 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
ID JMX Web
auditevents Yes No
beans Yes No
caches Yes No
conditions Yes No
configprops Yes No
env Yes No
flyway Yes No
health Yes Yes
heapdump N/A No
httptrace Yes No
info Yes Yes
integrationgraph Yes No
jolokia N/A No
logfile N/A No
loggers Yes No
liquibase Yes No
metrics Yes No
mappings Yes No
prometheus N/A No
scheduledtasks Yes No
sessions Yes No
shutdown Yes No
startup Yes No
threaddump Yes No

3 自定义health指标

@Component
public class MyHealth implements HealthIndicator {




        @Override
        public Health health() {
            // perform some specific health check
            int errorCode = 0;
            if (errorCode != 0) {
                return Health.down().withDetail("error", 31414).build();
            }
            return Health.up().build();
        }


/*
    构建Health
    Health build = Health.down()
            .withDetail("msg", "error service")
            .withDetail("code", "500")
            .withException(new RuntimeException())
            .build();*/
}

也可以用

@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {

    /**
     * 真实的检查方法
     * @param builder
     * @throws Exception
     */
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        //mongodb。  获取连接进行测试
        Map<String,Object> map = new HashMap<>();
        // 检查完成
        if(1 == 2){
//            builder.up(); //健康
            builder.status(Status.UP);
            map.put("count",1);
            map.put("ms",100);
        }else {
//            builder.down();
            builder.status(Status.OUT_OF_SERVICE);
            map.put("err","连接超时");
            map.put("ms",3000);
        }


        builder.withDetail("code",100)
                .withDetails(map);

    }
}

4 从maven中获取版本号

info:
#  获取工程的版本号
  appName: @project.artifactId@
# 获取工程的版本
  version: @project.version@

image-20210308090329933

5 自定义info信息

@Component
public class MyInfo implements InfoContributor {
    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("mars", "xioa");
    }
}

6 自定义监控指标

Counter counter;

public MyController(MeterRegistry meterRegistry) {
    counter = meterRegistry.counter("myCount");
}
 @ResponseBody
    @RequestMapping("/pageTest")
    public List<Role> pageTest() {
        //每次调用方法指标就加1
        counter.increment();
        Page<Role> rolePage = new Page<>(1, 5);
        roleMapper.selectPage(rolePage, null);
        return rolePage.getRecords();
    }

image-20210308093222110

7 自定义端口

@Endpoint(id = "container")
@Component
public class CustomPort {

    @ReadOperation
    public Map<String, String> getDockerInfo(){
        return Collections.singletonMap("info","docker started...");
    }

    @WriteOperation
    private void restartDocker(){
        System.out.println("docker restarted....");
    }




}

image-20210308093536290

33 可视化监控

官方文档

https://github.com/codecentric/spring-boot-admin

image-20210308095714790

33.1 监控工程

1 导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <version>2.3.1</version>
</dependency>

2 更改端口号

server.port=8088

3 启动项加入注解

@EnableAdminServer
@SpringBootApplication
public class MonitorApplication {

    public static void main(String[] args) {
        SpringApplication.run(MonitorApplication.class, args);
    }

}

33.2 项目工程

1 导入依赖

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.3.1</version>
</dependency>

2 配置信息

#可以监控所有的指标
management.endpoints.web.exposure.include=*
#使用ip注册当前实例
spring.boot.admin.client.instance.prefer-ip=true
#配置当前应用的名字,会在监控端显示名字
spring.application.name=江豪迪的springboot项目

34 Profile环境切换

为了方便多环境适配,springboot简化profile功能

image-20210308122715744

如果要用就在application-{}.yml/properties

#切换生产环境,如果test就优先使用application-test.yml配置文件
spring.profiles.active=test
//表明当前环境是prod环境的时候才能进行注入
@Profile("prod")
@ConfigurationProperties(prefix = "boss")
@Component
@Data

public class Boss {
    /*@Value("boss.name:张三")*/

    private String name;





}

也可以标注到@Bean方法下面

//只有生产环境的时候才进行自动注入
@Profile("prod")
@Bean
public Boss getBoss() {
    Boss boss = new Boss();
    boss.setName("我是大聪明蛋");
    return boss;
}
//只有是测试环境的时候才进行注入
@Profile("test")
@Bean
public Boss getBoss1() {
    Boss boss = new Boss();
    boss.setName("我是天才");
    return boss;
}

image-20210308130146224

35 外部化配置

1 外部数据源

常用:java属性文件,YAML文件,环境变量,命令行参数

2 配置文件查找位置

(1) classpath 根路径

(2) classpath 根路径下config目录

(3) jar包当前目录

(4) jar包当前目录的config目录

(5) /config子目录的直接子目录

3 配置文件加载顺序:

  1.  当前jar包内部的application.properties和application.yml
  2.  当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
  3.  引用的外部jar包的application.properties和application.yml
  4.  引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

4 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项

36 自定义starter

image-20210308181712658

1 小迪迪自动配置类

@Configuration
//如果容器中又xiaodidiService.class就不注册这个组件
@ConditionalOnMissingBean(XiaodidiService.class)
//将配置文件注册到容器中
@EnableConfigurationProperties(XiaodidiProperties.class)


public class XiaodidiAutoConfiguration {
    @Bean
    public XiaodidiService xiaodidiService() {
        return new XiaodidiService();

    }

}

2 小迪迪配置类

package com.atguigu.xiaodidi.xiaodidispringbootautoconfiguration.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author 10185
 * @create 2021/3/8 14:19
 */
@ConfigurationProperties("xiaodidi.properties")
public class XiaodidiProperties {
    private String name;
    private Integer id;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

3 小迪迪service

public class XiaodidiService {
    @Autowired
    XiaodidiProperties xiaodidiProperties;
    public void getXiaodidi() {
        System.out.println("name"+xiaodidiProperties.getName()+"id"+xiaodidiProperties.getId());

    }

}

4 小迪迪自动创建工厂

注意META-INF不要有空格

image-20210308182206371

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.atguigu.xiaodidi.xiaodidispringbootautoconfiguration.XiaodidiAutoConfiguration

5 进行打包

image-20210308182105086

37 springboot原理解析

1、SpringBoot启动过程

  • 创建 SpringApplication

    • 保存一些信息。
    • 判定当前应用的类型。ClassUtils。Servlet
    • bootstrappers**:初始启动引导器(List):去spring.factories文件中找** org.springframework.boot.Bootstrapper
    • ApplicationContextInitializer;去spring.factories****找 ApplicationContextInitializer
      • List<ApplicationContextInitializer> initializers
    • ApplicationListener ;应用监听器。spring.factories****找 ApplicationListener
      • List<ApplicationListener> listeners
  • 运行 SpringApplication

    • StopWatch
    • 记录应用的启动时间
    • **创建引导上下文(Context环境)**createBootstrapContext()
      • 获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置
    • 让当前应用进入headless模式。java.awt.headless
    • 获取所有 RunListener**(运行监听器)【为了方便所有Listener进行事件感知】**
      • getSpringFactoriesInstances 去spring.factories****找 SpringApplicationRunListener.
    • 遍历 SpringApplicationRunListener 调用 starting 方法;
      • 相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。
    • 保存命令行参数;ApplicationArguments
    • 准备环境 prepareEnvironment();
      • 返回或者创建基础环境信息对象。StandardServletEnvironment
      • 配置环境信息对象。
        • 读取所有的配置源的配置属性值。
      • 绑定环境信息
      • 监听器调用 listener.environmentPrepared();通知所有的监听器当前环境准备完成
    • 创建IOC容器(createApplicationContext())
      • 根据项目类型(Servlet)创建容器,
      • 当前会创建 AnnotationConfigServletWebServerApplicationContext
    • 准备ApplicationContext IOC容器的基本信息 prepareContext()
      • 保存环境信息
      • IOC容器的后置处理流程。
      • 应用初始化器;applyInitializers;
        • 遍历所有的 ApplicationContextInitializer 。调用 initialize.。来对ioc容器进行初始化扩展功能
        • 遍历所有的 listener 调用 contextPrepared。EventPublishRunListenr;通知所有的监听器contextPrepared
      • 所有的监听器 调用 contextLoaded。通知所有的监听器 contextLoaded;
    • **刷新IOC容器。**refreshContext
      • 创建容器中的所有组件(Spring注解)
    • 容器刷新完成后工作?afterRefresh
    • 所有监听 器 调用 listeners.started(context); 通知所有的监听器 started
    • **调用所有runners;**callRunners()
      • 获取容器中的 ApplicationRunner
      • 获取容器中的 CommandLineRunner
      • 合并所有runner并且按照@Order进行排序
      • 遍历所有的runner。调用 run 方法
    • 如果以上有异常,
      • 调用Listener 的 failed
    • 调用所有监听器的 running 方法 listeners.running(context); 通知所有的监听器 running
    • **running如果有问题。继续通知 failed 。**调用所有 Listener 的 **failed;**通知所有的监听器 failed

38 自定义ApplicationContextInitializer

image-20210309124912560

public class XiaodidiApplicatonListener implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println("我开始进行监听l ");
    }
}

SpringCloud

1 springCloud理论知识的讲解

1.1 什么是微服务

In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.——James Lewis and Martin Fowler (2014)

微服务是一种架构风格 一个应用拆分为一组小型服务 每个服务运行在自己的进程内,也就是可独立部署和升级 服务之间使用轻量级HTTP交互 服务围绕业务功能拆分 可以由全自动部署机制独立部署 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术 image-20210310123149612

具有哪些技术

  • 服务调用
  • 服务降级
  • 服务注册与发先
  • 服务熔断
  • 负载均衡
  • 服务消息队列
  • 服务网关
  • 配置中心管理
  • 自动化构建部署
  • 服务监控
  • 全链路追踪
  • 服务定时任务
  • 调度操作

Spring Cloud简介

是什么?符合微服务技术维度

SpringCloud=分布式微服务架构的站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶

猜猜SpringCloud这个大集合里有多少种技术?

image-20210310123229206

1.2 互联网大厂微服务架构案例

京东的

image-20210310123316138

阿里的

image-20210310123401545

京东物流的

image-20210310123450369

image-20210310123509015

1.3 SpringCloud技术栈

image-20210310123640156

image-20210310123654149

image-20210310123704301

1.4 总结

image-20210310123833897

2 第二季Boot和Cloud版本

Spring Boot 2.X 版

注意需要从官方文档得到Boot 和 Cloud的兼容版本

接下来开发用到的组件版本

Cloud - Hoxton.SR1 Boot - 2.2.2.RELEASE Cloud Alibaba - 2.1.0.RELEASE Java - Java 8 Maven - 3.5及以上 MySQL - 5.7及以上 04_Cloud组件停更说明 停更引发的“升级惨案”

停更不停用 被动修复bugs 不再接受合并请求 不再发布新版本 Cloud升级

image-20210321151704244 Spring Cloud官方文档

https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/

Spring Cloud中文文档

https://www.bookstack.cn/read/spring-cloud-docs/docs-index.md

Spring Boot官方文档 https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/

3 父工程新建

3.1 父工程project环境搭建

约定 > 配置 > 编码

创建微服务cloud整体聚合父工程Project,有8个关键步骤:

1.New Project - maven工程 - create from archetype: maven-archetype-site 2.聚合总父工程名字 3.Maven选版本 4.工程名字 5.字符编码 - Settings - File encoding 6.注解生效激活 - Settings - Annotation Processors 7.Java编译版本选8 8.File Type过滤 - Settings - File Type

3.2 父工程的pom文件

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.atguigu</groupId>
  <artifactId>springCloud5</artifactId>
  <version>1.0-SNAPSHOT</version>

  <!-- 统一管理jar包版本 -->
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <junit.version>4.12</junit.version>
    <log4j.version>1.2.17</log4j.version>
    <lombok.version>1.16.18</lombok.version>
    <mysql.version>5.1.47</mysql.version>
    <druid.version>1.1.16</druid.version>
    <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
  </properties>

  <!-- 子模块继承之后,提供作用:
      锁定版本+子modlue不用写groupId和version -->
  <dependencyManagement>
    <dependencies>
      <!--spring boot 2.2.2-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.2.2.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--spring cloud Hoxton.SR1-->
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Hoxton.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--spring cloud alibaba 2.1.0.RELEASE-->
      <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2.1.0.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
      </dependency>
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid.version}</version>
      </dependency>
      <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>${mybatis.spring.boot.version}</version>
      </dependency>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
      </dependency>
      <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${log4j.version}</version>
      </dependency>
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
        <optional>true</optional>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <fork>true</fork>
          <addResources>true</addResources>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

3.3 跳过maven测试

IntelliJ IDEA的Maven项目有时候通过右边Maven Projects面板的package或者install命令打包的时候,会报错导致打包失败,这是由于这两个命令打包前默认会运行tests测试,若测试失败则打包失败。但是有时候我们打包的时候一些项目配置是针对生产环境的,在本地可能会测试失败,在正式环境是可以正常运行的,这时候我们就需要把打包前的测试禁止调 image-20210310205643374

4 支付模块的创建

创建微服务模块的套路:

  1. 建Module
  2. 改POM
  3. 写YML
  4. 主启动
  5. 业务类

image-20210310210003057

创建cloud-provider-payment8001微服务提供者支付Module模块:

1.建名为cloud-provider-payment8001的Maven工程

image-20210310210111549

2 改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8001</artifactId>
    <dependencies>
    
  
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <!--
        <dependency>
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>

3 写YML

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver              # mysql驱动包
    url: jdbc:mysql://localhost:3306/my?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities    # 所有Entity别名类所在包

4 主启动

@SpringBootApplication
public class PaymentMenu8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMenu8001.class, args);
    }
}

5 业务类

建立

image-20210311113300876

CommonResult的创建

/**
 * @author 10185
 * @create 2021/3/10 22:04
 * @code 给web端的信息
 * @message 给web端传递的信息
 * @data 给web端传递的数据
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    private Integer code;
    private String message;
    private T data;




}

搭建一些基础的业务逻辑

PaymentController的创建

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.api.PaymentService;
import com.atguigu.springcloud.util.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * @author 10185
 * @create 2021/3/10 22:03
 */
@RestController
@RequestMapping("/payment")
public class PaymentController {
    @Autowired
    PaymentService paymentService;
    @PostMapping("/create")
    public CommonResult<Integer> create(@requestBody Payment payment) {
        int payment1 = paymentService.createPayment(payment);
        return payment1>0?new CommonResult<>(200, "成功", payment1)
                : new CommonResult<>(444, "失败", null);
    }



    @GetMapping("/getPayment/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Integer id) {
        Payment paymentById = paymentService.getPaymentById(id);
        return paymentById != null?new CommonResult<>(200, "查询成功",paymentById )
                :new CommonResult<>(444, "查询失败", null);
    }


}

5 消费者订单模块

image-20210313135636728

1 改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-order80</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

2 修改项目启动端口号

server:
  port: 80

3 主启动

@SpringBootApplication
public class PaymentMenu80 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMenu80.class, args);
    }
}

4 配置RestTemplate

package springcloud.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @author 10185
 * @create 2021/3/11 11:47
 */
@Configuration
public class OrderConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

5 控制层

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
    private static String PAYMENT_URL = "http://localhost:8001";
    @Autowired
    RestTemplate restTemplate;

    @PostMapping("/create")
    public CommonResult create(Payment payment) {
        log.info(payment.toString());
        return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
    }
    @GetMapping("/get/{id}")
    public CommonResult get(@PathVariable Integer id) {
        return restTemplate.getForObject(PAYMENT_URL+"/payment/getPayment/"+id, CommonResult.class);
    }



}

RestTemplate

RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集

官网地址https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html

使用:

使用restTemplate访问restful接口非常的简单粗暴无脑。 (url, requestMap, ResponseBean.class)这三个参数分别代表。 REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。

6 工程重构

观察cloud-consumer-order80与cloud-provider-payment8001两工程有重复代码(entities包下的实体)(坏味道),重构。

1.新建 - cloud-api-commons

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-api-commons</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.1.0</version>
        </dependency>
    </dependencies>



</project>

image-20210313143955799

2 删除另外两个工程中的Payment和CommonResult

3 清理并打包

4 导入依赖

<dependency>
    <groupId>com.atguigu</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

7 Eureka

7.1 Eureka基础知识

什么是服务治理

Spring Cloud封装了Netflix 公司开发的Eureka模块来实现服务治理

在传统的RPC远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

什么是服务注册与发现

Eureka采用了CS的设计架构,Eureka Sever作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。

在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息比如服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何RPC远程框架中,都会有一个注册中心存放服务地址相关信息(接口地址)

Eureka包含两个组件:Eureka Server和Eureka Client

Eureka Server提供服务注册服务

各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。

EurekaClient通过注册中心进行访问

它是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)

7.2 EurekaServer服务端安装

1 创建名为cloud-eureka-server7001的Maven工程

<!-- eureka新旧版本 -->
<!-- 以前的老版本(2018)-->
<dependency>
    <groupid>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<!-- 现在新版本(2020.2)--><!-- 我们使用最新的 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

2 修改pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-eureka-server7001</artifactId>
    <dependencies>
        <!--eureka-server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--boot web actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--一般通用配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
    </dependencies>


</project>

3 添加application.yml

server:
  port: 7001

eureka:
  instance:
    hostname: localhost #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己
    register-with-eureka: false
    #false表示自己端就是注册中心我的职责就是维护服务实例并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

4 主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7001.class, args);
    }
}

7.3 支付微服务8001入驻EurekaServer

EurekaClient端cloud-provider-payment8001将注册进EurekaServer成为服务提供者provider,类似学校对外提供授课服务。

1.修改cloud-provider-payment8001

2.改POM

添加spring-cloud-starter-netflix-eureka-client依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

3 写YML

eureka:
  client:
    #表示是否将自己注册进Eurekaserver默认为true
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息默认为true单节点无所谓集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://localhost:7001/eureka

4 主启动

@SpringBootApplication
@EnableEurekaClient//<-----添加该注解
public class PaymentMain001 {

    public static void main(String[] args) {
        SpringApplication.run(PaymentMain001.class, args);
    }
}

7.4 Eureka集群说明

1 Eureka集群原理说明

image-20210313191711594

7.5 Eureka集群环境构建

1 创建cloud-eureka-server7002工程

2 hosts配置文件

修改C:\Windows\System32\drivers\etc路径下的hosts文件,修改映射配置添加进hosts文件

127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com

3 修改cloud-eureka-server7001配置文件

server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com #eureka服务端的实例名称
  client:
    register-with-eureka: false     #false表示不向注册中心注册自己
    fetch-registry: false     #false表示自己端就是注册中心我的职责就是维护服务实例并不需要去检索服务
    service-url:
      #集群指向其它eureka
      defaultZone: http://eureka7002.com:7002/eureka/

4 修改cloud-eureka-server7002配置文件

server:
  port: 7002

eureka:
  instance:
    hostname: eureka7002.com #eureka服务端的实例名称
  client:
    register-with-eureka: false     #false表示不向注册中心注册自己
    fetch-registry: false     #false表示自己端就是注册中心我的职责就是维护服务实例并不需要去检索服务
    service-url:
      #集群指向其它eureka
      defaultZone: http://eureka7001.com:7001/eureka/

5 订单支付两微服务注册进Eureka集群

  • 将支付服务8001微服务,订单服务80微服务发布到上面2台Eureka集群配置中

将它们的配置文件的eureka.client.service-url.defaultZone进行修改

eureka:
  client:
    #表示是否将自己注册进Eurekaserver默认为true
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息默认为true单节点无所谓集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka

image-20210313192457049

7.6 支付微服务集群配置

支付服务提供者8001集群环境构建

参考cloud-provicer-payment8001

1.新建cloud-provider-payment8002

2.改POM

3.写YML - 端口8002

4.主启动

5.业务类

6.修改8001/8002的Controller,添加serverPort

@RestController
@Slf4j
public class PaymentController{

    @Value("${server.port}")
    private String serverPort;//添加serverPort

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment)
    {
        int result = paymentService.create(payment);
        log.info("*****插入结果:" + result);

        if(result > 0) {
            return new CommonResult(200,"插入数据库成功,serverPort: "+serverPort/*添加到此处*/, result);
        }else{
            return new CommonResult(444,"插入数据库失败",null);
        }
    }
}

开启cloud-consumer-order80负载均衡

package com.atguigu.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.util.CommonResult;

/**
 * @author 10185
 * @create 2021/3/11 11:45
 */
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
    private static String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
    @Autowired
    RestTemplate restTemplate;

    @PostMapping("/create")
    public CommonResult create(Payment payment) {
        log.info(payment.toString());
        return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
    }
    @GetMapping("/get/{id}")
    public CommonResult get(@PathVariable Integer id) {
        return restTemplate.getForObject(PAYMENT_URL+"/payment/getPayment/"+id, CommonResult.class);
    }



}

同时

 */
@Configuration
public class OrderConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

image-20210313204349811

image-20210313204406253

image-20210313204410193

交替出现

7.7 actuator微服务信息完善

主机名称:服务名称修改(也就是将IP地址,换成可读性高的名字)

修改cloud-provider-payment8001,cloud-provider-payment8002

修改部分 - YML - eureka.instance.instance-id

eureka:
  client:
    #表示是否将自己注册进Eurekaserver默认为true
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息默认为true单节点无所谓集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka
  instance:
    instance-id: payment8002 #添加此处
    prefer-ip-address: true #可以显示当前路径

image-20210313205216169

同时显示地址(访问信息有IP信息提示,(就是将鼠标指针移至payment8001,payment8002名下,会有IP地址提示))

image-20210313205228744

7.8 服务发现Discovery

对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息

  • 修改cloud-provider-payment8001的Controller
@RestController
@Slf4j
public class PaymentController{
	...
    
    @Resource
    private DiscoveryClient discoveryClient;

    ...

    @GetMapping(value = "/payment/discovery")
    public Object discovery()
    {
        List<String> services = discoveryClient.getServices();
        for (String element : services) {
            log.info("*****element: "+element);
        }

        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        for (ServiceInstance instance : instances) {
            log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
        }

        return this.discoveryClient;
    }
}

8001 启动类

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient//添加该注解(E以后可以不用写)
public class PaymentMain001 {

    public static void main(String[] args) {
        SpringApplication.run(PaymentMain001.class, args);
    }
}

image-20210313212324289

image-20210313212334388

7.9 Eureka自我保护机制

概述

保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。

如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THANTHRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUSTTO BE SAFE

导致原因

一句话:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存。

属于CAP里面的AP分支。

为什么会产生Eureka自我保护机制?

为了EurekaClient可以正常运行,防止与EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除

什么是自我保护模式?

默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。

image-20210313214345488

自我保护机制∶默认情况下EurekaClient定时向EurekaServer端发送心跳包

如果Eureka在server端在一定时间内(默认90秒)没有收到EurekaClient发送心跳包,便会直接从服务注册列表中剔除该服务,但是在短时间( 90秒中)内丢失了大量的服务实例心跳,这时候Eurekaserver会开启自我保护机制,不会剔除该服务(该现象可能出现在如果网络不通但是EurekaClient为出现宕机,此时如果换做别的注册中心如果一定时间内没有收到心跳会将剔除该服务,这样就出现了严重失误,因为客户端还能正常发送心跳,只是网络延迟问题,而保护机制是为了解决此问题而产生的)。

在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。

它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着。

综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。

禁用自我保护机制

  • 在eurekaServer端7001处设置关闭自我保护机制

出厂默认,自我保护机制是开启的

使用eureka.server.enable-self-preservation = false可以禁用自我保护模式

eureka:
  ...
  server:
    #关闭自我保护机制,保证不可用服务被及时踢除
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 2000

关闭效果:

spring-eureka主页会显示出一句:

THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.

  • 生产者心跳配置

默认:

eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
eureka:
  ...
  instance:
    instance-id: payment8001
    prefer-ip-address: true
    #心跳检测与续约时间
    #开发时没置小些保证服务关闭后注册中心能即使剔除服务
    #Eureka客户端向服务端发送心跳的时间间隔单位为秒(默认是30秒)
    lease-renewal-interval-in-seconds: 1
    #Eureka服务端在收到最后一次心跳后等待时间上限单位为秒(默认是90秒)超时将剔除服务
    lease-expiration-duration-in-seconds: 2

8 支付服务注册进zookeeper

注册中心Zookeeper zookeeper是一个分布式协调工具,可以实现注册中心功能

关闭Linux服务器防火墙后,启动zookeeper服务器

用到的Linux命令行:

systemctl stop firewalld关闭防火墙 systemctl status firewalld查看防火墙状态 ipconfig查看IP地址 ping查验结果 zookeeper服务器取代Eureka服务器,zk作为服务注册中心

具体搭建zookeeper环境请见大数据笔记中的zookeeper

8.1 新建cloud-provider-payment8004的maven工程

8.2 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8004</artifactId>
    <dependencies>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包可以使用Payment支付Entity -->
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- SpringBoot整合zookeeper客户端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            <!--先排除自带的zookeeper3.5.3 防止与3.4.9起冲突-->
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--添加zookeeper3.4.9版本-->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>



</project>

8.3 YML

#8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
  port: 8004

#服务别名----注册zookeeper到注册中心名称
spring:
  application:
    name: cloud-provider-payment
  cloud:
    zookeeper:
      connect-string: 192.168.241.102:2181

8.4 主启动类

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @author 10185
 * @create 2021/3/14 8:55
 */
@SpringBootApplication
//该注解用于向使用consul或者zookeeper作为注册中心时注册服务
@EnableDiscoveryClient
public class PaymentMenu8004 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMenu8004.class, args);
    }
}

8.5 Controller

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.util.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

/**
 * @author 10185
 * @create 2021/3/10 22:03
 */
@Slf4j
@RestController
@RequestMapping("/payment")
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/zk")
    public String paymentzk()
    {
        return "springcloud with zookeeper: "+serverPort+"\t"+ UUID.randomUUID().toString();
    }


}

8.6 启动zookeeper

zk start

8.7 启动8004注册进zookeeper

验证测试:浏览器 - http://localhost:8004/payment/zk

验证测试2 :接着用zookeeper客户端操作

[zk: localhost:2181(CONNECTED) 0] ls /
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 1] ls /services/cloud-provider-payment
[a4567f50-6ad9-47a3-9fbb-7391f41a9f3d]
[zk: localhost:2181(CONNECTED) 2] get /services/cloud-provider-payment/a4567f50-6ad9-47a3-9fbb-7391f41a9f3d
{"name":"cloud-provider-payment","id":"a4567f50-6ad9-47a3-9fbb-7391f41a9f3d","address":"192.168.199.218","port":8004,"ss
lPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","
name":"cloud-provider-payment","metadata":{}},"registrationTimeUTC":1612811116918,"serviceType":"DYNAMIC","uriSpec":{"pa
rts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":"
:","variable":false},{"value":"port","variable":true}]}}
[zk: localhost:2181(CONNECTED) 3]

json格式化get /services/cloud-provider-payment/a4567f50-6ad9-47a3-9fbb-7391f41a9f3d的结果:

{
    "name": "cloud-provider-payment", 
    "id": "a4567f50-6ad9-47a3-9fbb-7391f41a9f3d", 
    "address": "192.168.199.218", 
    "port": 8004, 
    "sslPort": null, 
    "payload": {
        "@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance", 
        "id": "application-1", 
        "name": "cloud-provider-payment", 
        "metadata": { }
    }, 
    "registrationTimeUTC": 1612811116918, 
    "serviceType": "DYNAMIC", 
    "uriSpec": {
        "parts": [
            {
                "value": "scheme", 
                "variable": true
            }, 
            {
                "value": "://", 
                "variable": false
            }, 
            {
                "value": "address", 
                "variable": true
            }, 
            {
                "value": ":", 
                "variable": false
            }, 
            {
                "value": "port", 
                "variable": true
            }
        ]
    }
}

9 订单服务注册进zookeeper

9.1 新建cloud-consumerzk-order80

9.2 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
    <artifactId>springCloud5</artifactId>
    <groupId>com.atguigu</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>cloud-consumer-order80</artifactId>
    <dependencies>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- SpringBoot整合zookeeper客户端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            <!--先排除自带的zookeeper-->
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--添加zookeeper3.4.9版本-->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

9.3 YML

server:
  port: 80
#服务别名----注册zookeeper到注册中心名称
spring:
  application:
    name: cloud-consumer-order
  cloud:
    zookeeper:
      connect-string: 192.168.241.102:2181

9.4 主启动

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author 10185
 * @create 2021/3/10 21:04
 */
@SpringBootApplication
public class PaymentMenu80 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMenu80.class, args);
    }
}

9.5 业务类

package com.atguigu.springcloud.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @author 10185
 * @create 2021/3/11 11:47
 */
@Configuration
public class OrderConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
package com.atguigu.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

/**
 * @author 10185
 * @create 2021/3/11 11:45
 */
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderController {
    private static String INVOKE_URL = "http://cloud-provider-payment";
    @Autowired
    RestTemplate restTemplate;

    @GetMapping(value = "/payment/zk")
    public String paymentInfo()
    {
        String result = restTemplate.getForObject(INVOKE_URL+"/payment/zk",String.class);
        return result;
    }



}

9.6 验证测试

运行ZooKeeper服务端,cloud-consumerzk-order80,cloud-provider-payment8004。

打开ZooKeeper客户端:

[zk: localhost:2181(CONNECTED) 0] ls /
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 1] ls /services
[cloud-consumer-order, cloud-provider-payment]
[zk: localhost:2181(CONNECTED) 2]

访问测试地址 - http://localhost/consumer/payment/zk

10 Consul

10.1 Consul简介

Consul官网

https://www.consul.io/

Consul下载地址

https://www.consul.io/downloads

What is Consul?

Consul is a service mesh solution providing a full featured control plane with service discovery, configuration, and segmentation functionality. Each of these features can be used individually as needed, or they can be used together to build a full service mesh. Consul requires a data plane and supports both a proxy and native integration model. Consul ships with a simple built-in proxy so that everything works out of the box, but also supports 3rd party proxy integrations such as Envoy. link

Consul是一个服务网格解决方案,它提供了一个功能齐全的控制平面,具有服务发现、配置和分段功能。这些特性中的每一个都可以根据需要单独使用,也可以一起用于构建全服务网格。Consul需要一个数据平面,并支持代理和本机集成模型。Consul船与一个简单的内置代理,使一切工作的开箱即用,但也支持第三方代理集成,如Envoy。

Consul是一套开源的分布式服务发现和配置管理系统,由HashiCorp 公司用Go语言开发。

提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。

它具有很多优点。包括:基于raft协议,比较简洁;支持健康检查,同时支持HTTP和DNS协议支持跨数据中心的WAN集群提供图形界面跨平台,支持Linux、Mac、Windows。

image-20210314100126572

怎么用

https://www.springcloud.cc/spring-cloud-consul.html

10.2 安装并运行Consul

windows版解压缩后,得consul.exe,打开cmd

windows版解压缩后,得consul.exe大考cmd

查看版本consul -v

D:\Consul>consul -v
Consul v1.9.3
Revision f55da9306
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

开发模式启动consul agent -dev

image-20210314101731066

10.3 服务提供者注册进Consul

1 启动consul

2 新建Module支付服务cloud-provide-payment8006

3 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8006</artifactId>
    <dependencies>
        <!-- 引入自己定义的api通用包可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--SpringCloud consul-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>
    </dependencies>




</project>

4 YML

###consul服务端口号
server:
  port: 8006

spring:
  application:
    name: consul-provider-payment
  ####consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #hostname: 127.0.0.1
        service-name: ${spring.application.name}

5 主启动类

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @author 10185
 * @create 2021/3/14 8:55
 */
@SpringBootApplication
//该注解用于向使用consul或者zookeeper作为注册中心时注册服务
@EnableDiscoveryClient
public class PaymentMenu8006 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMenu8006.class, args);
    }
}

6 业务类

package com.atguigu.springcloud.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

/**
 * @author 10185
 * @create 2021/3/10 22:03
 */
@Slf4j
@RestController
@RequestMapping("/payment")
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/consul")
    public String paymentConsul()
    {
        return "springcloud with consul: "+serverPort+"\t"+ UUID.randomUUID().toString();
    }


}

验证测试

  • http://localhost:8006/payment/consul
  • http://localhost:8500 - 会显示provider8006

10.4 服务消费者注册进Consul

1 新建Module消费服务order80 - cloud-consumerconsul-order80

2 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
    <artifactId>springCloud5</artifactId>
    <groupId>com.atguigu</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>cloud-consumerconsul-order80</artifactId>
    <dependencies>
        <!--SpringCloud consul-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>

3 yml

###consul服务端口号
server:
  port: 80

spring:
  application:
    name: cloud-consumer-order
  ####consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #hostname: 127.0.0.1
        service-name: ${spring.application.name}

4 主启动类

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author 10185
 * @create 2021/3/10 21:04
 */
@SpringBootApplication
public class OrderConsulMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderConsulMain80.class, args);
    }
}

5 Controller

package com.atguigu.springcloud.config;

        import org.springframework.cloud.client.loadbalancer.LoadBalanced;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.web.client.RestTemplate;

/**
 * @author 10185
 * @create 2021/3/11 11:47
 */
@Configuration
public class OrderConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
package com.atguigu.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

/**
 * @author 10185
 * @create 2021/3/11 11:45
 */
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderController {
    public static final String INVOKE_URL = "http://consul-provider-payment";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/consumer/payment/consul")
    public String paymentInfo()
    {
        String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul",String.class);
        return result;
    }




}

.验证测试

运行consul,cloud-providerconsul-payment8006,cloud-consumerconsul-order80

http://localhost:8500/ 主页会显示出consul,cloud-providerconsul-payment8006,cloud-consumerconsul-order80三服务。

8.访问测试地址 - http://localhost/consumer/payment/consul

11 三个注册中心的异同点

组件名 语言CAP 服务健康检查 对外暴露接口 Spring Cloud集成
Eureka Java AP 可配支持 HTTP
Consul Go CP 支持 HTTP/DNS
Zookeeper Java CP 支持客户端 已集成

CAP:

  • C:Consistency (强一致性)
  • A:Availability (可用性)
  • P:Partition tolerance (分区容错性)

最多只能同时较好的满足两个。

CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求。

因此,根据CAP原理将NoSQL数据库分成了满足CA原则、满足CP原则和满足AP原则三大类:

CA - 单点集群,满足—致性,可用性的系统,通常在可扩展性上不太强大。 CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。 AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

AP架构(Eureka)

当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。

结论:违背了一致性C的要求,只满足可用性和分区容错,即AP’

image-20210314192047122

CP架构

当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性。

结论:违背了可用性A的要求,只满足一致性和分区容错,即CP。

image-20210314192105662

CP 与 AP 对立同一的矛盾关系。

12 Ribbon的负载均衡和RestTemplate调用

12.1 Ribbon入门介绍

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。

简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。

简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。

Ribbon目前也进入维护模式。

Ribbon未来可能被Spring Cloud LoadBalacer替代。

LB负载均衡(Load Balance)是什么

简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA (高可用)。

常见的负载均衡有软件Nginx,LVS,硬件F5等。

Ribbon本地负载均衡客户端VS Nginx服务端负载均衡区别

Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。 Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

集中式LB

即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;

进程内LB

将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。

Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

一句话

负载均衡 + RestTemplate调用

12.2 Ribbon的负载均衡和Rest调用的实操

1 架构说明

image-20210314192904138

Ribbon在工作时分成两步:

  • 第一步先选择EurekaServer ,它优先选择在同一个区域内负载较少的server。
  • 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。

其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。

2 POM

先前工程项目中没有引入spring-cloud-starter-ribbon也可以使用ribbon

<dependency>
    <groupld>org.springframework.cloud</groupld>
    <artifactld>spring-cloud-starter-netflix-ribbon</artifactid>
</dependency>

这是因为spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用。

3 RestTemplate的使用

RestTemplate官方文档

https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html

getForObject() /get ForEntity() -GET请求方法

getForObject():返回对象为响应体中数据转化成的对象,基本上可以理解为json

getForEntity():返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头,响应状态码,响应体等.

@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id)
{
    ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);

    if(entity.getStatusCode().is2xxSuccessful()){
        return entity.getBody();//getForObject()
    }else{
        return new CommonResult<>(444,"操作失败");
    }
}

12.3 Ribbon默认自带的负载规则

IRule:根据特定算法中从服务列表中选取一个要访问的服务

image-20210314212626429

RoundRobinRule 轮询 RandomRule 随机 RetryRule 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重 WeightedResponseTimeRule 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择 BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例 ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能和server的可用性选择服务器

12.4 Ribbon负载均衡替换实操

1 注意配置细节

官方文档明确给出了警告:

这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,

否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。

(也就是说不要将Ribbon配置类与主启动类同包)

3.新建package - com.atguigu.myrule

2 在com.atguigu.myrule下新建MySelfRule规则类

image-20210314213923095

package com.atguigu.myrule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 10185
 * @create 2021/3/14 21:17
 */
@Configuration
public class MySelfRule  {
    @Bean
    public IRule myRule() {
        return new RandomRule();
    }
}

3 主启动添加@RibbonClient

package com.atguigu.springcloud;

import com.atguigu.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

/**
 * @author 10185
 * @create 2021/3/10 21:04
 */
@SpringBootApplication
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class PaymentMenu80 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMenu80.class, args);
    }
}

6.测试

开启cloud-eureka-server7001,cloud-consumer-order80,cloud-provider-payment8001,cloud-provider-payment8002

浏览器-输入http://localhost/consumer/payment/get/1

返回结果中的serverPort在8001与8002两种间反复横跳。

12.5 Ribbon默认负载轮询算法原理,源码分析

默认负载轮训算法: rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。

List instances = discoveryClient.getInstances(“CLOUD-PAYMENT-SERVICE”);

如:

List [0] instances = 127.0.0.1:8002 List [1] instances = 127.0.0.1:8001 8001+ 8002组合成为集群,它们共计2台机器,集群总数为2,按照轮询算法原理:

当总请求数为1时:1%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001 当总请求数位2时:2%2=О对应下标位置为0,则获得服务地址为127.0.0.1:8002 当总请求数位3时:3%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001 当总请求数位4时:4%2=О对应下标位置为0,则获得服务地址为127.0.0.1:8002 如此类推…

public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    //重点关注这方法
    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}
package com.netflix.loadbalancer;

import com.netflix.client.config.IClientConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The most well known and basic load balancing strategy, i.e. Round Robin Rule.
 *
 * @author stonse
 * @author Nikos Michalakis <nikos@netflix.com>
 *
 */
public class RoundRobinRule extends AbstractLoadBalancerRule {

    private AtomicInteger nextServerCyclicCounter;
    private static final boolean AVAILABLE_ONLY_SERVERS = true;
    private static final boolean ALL_SERVERS = false;

    private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);

    public RoundRobinRule() {
        nextServerCyclicCounter = new AtomicInteger(0);
    }

    public RoundRobinRule(ILoadBalancer lb) {
        this();
        setLoadBalancer(lb);
    }

    //重点关注这方法。
    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
            List<Server> reachableServers = lb.getReachableServers();
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();

            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }

            int nextServerIndex = incrementAndGetModulo(serverCount);
            server = allServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;//求余法
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
}
————————————————
版权声明本文为CSDN博主巨輪的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明
原文链接https://blog.csdn.net/u011863024/article/details/114298270

12.6 手写轮询算法原理

自己试着写一个类似RoundRobinRule的本地负载均衡器。

  • 7001/7002集群启动
  • 8001/8002微服务改造- controller

image-20210315100153848

1.ApplicationContextConfig去掉注解@LoadBalanced,OrderMain80去掉注解@RibbonClient

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {

    @Bean
    //@LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

}

2.创建LoadBalancer接口

package com.atguigu.springcloud.utils;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

/**
 * @author 10185
 * @create 2021/3/15 8:41
 */
public interface LoadBalanced {
    /**
     * 通过该方法获取需要得到的下标
     * @param remoteAddress 远程地址
     * @return
     */
    ServiceInstance getServiceInstance(String remoteAddress) throws Exception;





}

3 实现LoadBalancer接口

package com.atguigu.springcloud.utils;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import com.sun.xml.internal.ws.api.pipe.NextAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author 10185
 * @create 2021/3/15 8:44
 */
@Component
public class LoadBalancedImpl implements LoadBalanced{
    @Autowired
    DiscoveryClient discoveryClient;
    private AtomicInteger atomicInteger = new AtomicInteger(0);
    private List<ServiceInstance> instances;



    @Override
    public ServiceInstance getServiceInstance(String remoteAddress) {
        instances = discoveryClient.getInstances(remoteAddress);
        if (CollectionUtil.isNotEmpty(instances)) {

            return instances.get(getCurrentIndex());
        }
            return null;


    }

    /**
     * 得到对应机器的下标
     * @return
     */
    private int getCurrentIndex() {

        int size = instances.size();
        int next;
        int current;
        do {
            current = atomicInteger.get();
            next = (current + 1) % size;
        }while (!this.atomicInteger.compareAndSet(current,next));
        return next;
    }





}

4 orderController

 @GetMapping("/get/{id}")
    public CommonResult get(@PathVariable Integer id) throws Exception {
        ServiceInstance serviceInstance = loadBalanced.getServiceInstance("CLOUD-PAYMENT-SERVICE");
        URI uri = serviceInstance.getUri();
        log.info(uri.toString());
        return restTemplate.getForObject(uri+"/payment/getPayment/"+id, CommonResult.class);
    }

13 OpenFeign

13.1 OpenFeign是什么

Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign. link

Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。

13.2 Feign能干什么

Feign旨在使编写Java Http客户端变得更容易。

前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。

Feign集成了Ribbon

利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

Feign和OpenFeign两者区别

Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@Feignclient可以解析SpringMVc的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@Feignclient可以解析SpringMVc的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

13.3 OpenFeign服务调用

接口+注解: 微服务调用接口+@FeignClient

1 新建cloud-consumer-feign-order80

2 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>LearnCloud</artifactId>
        <groupId>com.lun.springcloud</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-feign-order80</artifactId>

    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.lun.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--一般基础通用配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>
————————————————
版权声明:本文为CSDN博主「巨輪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011863024/article/details/114298270

3 YML

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

4 主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderFeignMain80.class, args);
    }
}

5 业务类

业务逻辑接口+@FeignClient配置调用provider服务

新建PaymentFeignService接口并新增注解@FeignClient

package com.atguigu.springcloud;

import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.util.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author 10185
 * @create 2021/3/15 10:31
 */
@Component
@FeignClient("CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
    @GetMapping("/payment/getPayment/{id}")
    CommonResult<Payment> getPaymentById(@PathVariable("id") Integer id);
    
}

6 控制层Controller

@GetMapping("/get/{id}")
public CommonResult get(@PathVariable Integer id) throws Exception {
   /* ServiceInstance serviceInstance = loadBalanced.getServiceInstance("CLOUD-PAYMENT-SERVICE");
    URI uri = serviceInstance.getUri();
    log.info(uri.toString());
    return restTemplate.getForObject(uri+"/payment/getPayment/"+id, CommonResult.class);*/
    System.out.println("我更改业务逻辑");
    return paymentFeignService.getPaymentById(id);

6.测试

先启动2个eureka集群7001/7002

再启动2个微服务8001/8002

启动OpenFeign启动

http://localhost/consumer/payment/get/1

Feign自带负载均衡配置项

13.4 OpenFeign超时控制

超时设置,故意设置超时演示出错情况

1 服务提供方8001/8002故意写暂停程序

@GetMapping(value = "/feign/timeout")
public String paymentFeignTimeout()
{
    // 业务逻辑处理正确,但是需要耗费3秒钟
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return port;
}

2 服务消费方80添加超时方法PaymentFeignService

@GetMapping(value = "/payment/feign/timeout")
String paymentFeignTimeout();

3.服务消费方80添加超时方法OrderFeignController

@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout()
{
    // OpenFeign客户端一般默认等待1秒钟
    return paymentFeignService.paymentFeignTimeout();
}

4 测试

多次刷新http://localhost/consumer/payment/feign/timeout

将会跳出错误Spring Boot默认错误页面,主要异常:feign.RetryableException:Read timed out executing GET http://CLOUD-PAYMENT-SERVCE/payment/feign/timeout。

默认OpenFeign等待1秒钟,超过后报错

可以在YML文件里需要开启OpenFeign客户端超时控制

#设置feign客户端超时时间(OpenFeign默认支持ribbon)(单位毫秒)
ribbon:
  #指的是建立连接所用的时间适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

不用管警告,可以使用

13.5 OpenFeign日志 增强

1 日志打印功能

Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节

说白了就是对Feign接口的调用情况进行监控和输出

2 日志级别

NONE:默认的,不显示任何日志

BASIC:仅记录请求方法,URL,响应状态码及执行时间

HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息

FULL:出来HEADERS中定义的信息之外,还有请求和响应的正文及元数据

3 配置日志bean

@Bean
Logger.Level feignLoggerLevel()
{
    return Logger.Level.FULL;
}

4 配置yml

logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.atguigu.springcloud.service.PaymentFeignService: debug

image-20210315130703406

14 Hystrix是什么

14.1 概述

分布式系统面临的问题

复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

服务雪崩

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”. 对于高流量的应用来说,单一的后避依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

Hystrix是什么

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

14.2 Hystrix停更进维

能干嘛

  • 服务降级
  • 服务熔断
  • 接近实对的监控

官网资料

link

Hystrix官宣,停更进维

link

  • 被动修bugs
  • 不再接受合并请求
  • 不再发布新版本

14.3 Hystrix的服务降级熔断限流概念初讲

服务降级

服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback

哪些情况会出发降级

程序运行导常 超时 服务熔断触发服务降级 线程池/信号量打满也会导致服务降级

服务熔断

类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。

服务的降级 -> 进而熔断 -> 恢复调用链路

服务限流

秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。

14.4 Hystrix支付微服务构建

1 将cloud-eureka-server7001改配置为单机版

server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com #eureka服务端的实例名称
  client:
    register-with-eureka: false     #false表示不向注册中心注册自己。
    fetch-registry: false     #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    service-url:
      #集群指向其它eureka
     # defaultZone: http://eureka7002.com:7002/eureka/
    #配置单机版  
      defaultZone: http://eureka7001.com:7001/eureka/

2 新建cloud-provider-hygtrix-payment8001

3 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-hygtrix-payment8001</artifactId>
    <dependencies>
        <!--hystrix-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包可以使用Payment支付Entity -->
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

4 YML

server:
  port: 8001

spring:
  application:
    name: cloud-provider-hystrix-payment

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
      defaultZone: http://eureka7001.com:7001/eureka

5 主启动

@SpringBootApplication
@EnableEurekaClient

public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
}

6 业务类

package com.atguigu.springcloud.service;

import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    /**
     */
    public String paymentInfo_OK(Integer id)
    {
        return "线程池:  "+Thread.currentThread().getName()+"  paymentInfo_OK,id:  "+id+"\t"+"O(∩_∩)O哈哈~";
    }

    public String paymentInfo_TimeOut(Integer id)
    {
        try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
        return "线程池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗时(秒): 3";
    }
}

7 controller

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController
{
    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id)
    {
        String result = paymentService.paymentInfo_OK(id);
        log.info("*****result: "+result);
        return result;
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
    {
        String result = paymentService.paymentInfo_TimeOut(id);
        log.info("*****result: "+result);
        return result;
    }
}

8 正常测试

启动eureka7001

启动cloud-provider-hystrix-payment8001

访问

success的方法 - http://localhost:8001/payment/hystrix/ok/1 每次调用耗费5秒钟 - http://localhost:8001/payment/hystrix/timeout/1

上述module均OK

以上述为根基平台,从正确 -> 错误 -> 降级熔断 -> 恢复。

14.5 JMeter高并发压测后卡顿

JMeter官网

https://jmeter.apache.org/index.html

JMeter的使用

image-20210315215319490

image-20210315215355425

image-20210315215437635

ctrl + s保存

image-20210315215559646

image-20210315215857092

image-20210315215945385

14.6 订单微服务调用支付服务出现卡顿

1.新建 - cloud-consumer-feign-hystrix-order80

2.POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-feign-hystrix-order80</artifactId>
    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--hystrix-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--一般基础通用配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>

3 yml

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

4 主启动

package com.atguigu.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author 10185
 * @create 2021/3/15 22:03
 */
@SpringBootApplication
@EnableFeignClients
//@EnableHystrix
public class OrderHystrixMain80
{
    public static void main(String[] args)
    {
        SpringApplication.run(OrderHystrixMain80.class,args);
    }
}

5 业务类

package com.atguigu.springboot.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 */
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" /*,fallback = PaymentFallbackService.class*/)
public interface PaymentHystrixService
{
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}

6 controller

package com.atguigu.springboot.controller;

import com.atguigu.springboot.service.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderHystirxController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id)
    {
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_TimeOut(id);
        return result;
    }
}

6.正常测试

http://localhost/consumer/payment/hystrix/ok/1

7.高并发测试

2W个线程压8001

消费端80微服务再去访问正常的Ok微服务8001地址

http://localhost/consumer/payment/hystrix/ok/32

消费者80被拖慢

原因:8001同一层次的其它接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕。

正因为有上述故障或不佳表现才有我们的降级/容错/限流等技术诞生。

14.7 降级容错解决的维度要求

超时导致服务器变慢(转圈)-超时不再等待

出错(宕机或程序运行出错)-出错要有兜底

解决:

  • 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级。
  • 对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级。
  • 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级。

14.8 _Hystrix之服务降级字符侧fallback

降级配置-@HystrixCommand

8001先从自身找问题

设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback

8001fallback

业务类启用-@HystrixCommand报异常后如何处理

一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法

1 主程序类激活

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author 10185
 * @create 2021/3/10 21:04
 */
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker//添加到此处

public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
}

2 8001service中进行设置

package com.atguigu.springcloud.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    /**
     */
    public String paymentInfo_OK(Integer id)
    {
        return "线程池:  "+Thread.currentThread().getName()+"  paymentInfo_OK,id:  "+id+"\t"+"O(∩_∩)O哈哈~";
    }
    @HystrixCommand(fallbackMethod = "paymentInfoTimeOutHandler"/*指定善后方法名*/
            ,commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds"
            , value = "3000")})
    public String paymentInfo_TimeOut(Integer id)
    {

        try { TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        return "线程池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗时(秒): 5";
    }
    public String paymentInfoTimeOutHandler(Integer id) {
        return "线程池:  "+Thread.currentThread().getName()+"  paymentInfo_OK,id:  "+id+"\t"+"😂😂😂";
    }
}

上面故意制造两种异常:

  1. int age = 10/0,计算异常
  2. 我们能接受3秒钟,它运行5秒钟,超时异常。

当前服务不可用了,做服务降级,兜底的方案都是paymentInfo_TimeOutHandler

14.9 Hystrix之服务降级订单单侧fallback

80订单微服务,也可以更好的保护自己,自己也依样画葫芦进行客户端降级保护

题外话,切记 - 我们自己配置过的热部署方式对java代码的改动明显

但对@HystrixCommand内属性的修改建议重启微服务

1 YML

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
#开启

ribbon:
  #指的是建立连接所用的时间适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000
feign:
  hystrix:
    enabled: true

#不设置超时时间
hystrix:
  command:
    default:
      execution:
        timeout:
          enabled: false

#或者
#hystrix:
#  command:
#    default:
#      execution:
#        isolation:
#          thread:
#            timeoutInMilliseconds: 3000

2 主启动

package com.atguigu.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author 10185
 * @create 2021/3/15 22:03
 */
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80
{
    public static void main(String[] args)
    {
        SpringApplication.run(OrderHystrixMain80.class,args);
    }
}

3 controller

package com.atguigu.springboot.controller;

import com.atguigu.springboot.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderHystirxController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id)
    {
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }


        @GetMapping("/consumer/payment/hystrix/timeout/{id}")
        @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
        })
        public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
            //int age = 10/0;
            String result = paymentHystrixService.paymentInfo_TimeOut(id);
            return result;
        }

        //善后方法
        public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
            return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
        }
    }

14.10 Hystrix之全局服务降级DefaultProperties

目前问题1 每个业务方法对应一个兜底的方法,代码膨胀 解决方法

1:1每个方法配置一个服务降级方法,技术上可以,但是不聪明

1:N除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = “”)统一跳转到统一处理结果页面

通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量

package com.atguigu.springboot.controller;

import com.atguigu.springboot.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentTimeOutDefaultFallbackMethod")
public class OrderHystirxController {
    @Resource
    private PaymentHystrixService paymentHystrixService;


    @HystrixCommand
    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id)
    {
        String result = paymentHystrixService.paymentInfo_OK(id);
        int a  = 1/0;
        return result;
    }


        @GetMapping("/consumer/payment/hystrix/timeout/{id}")
        @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
        })
        public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
            //int age = 10/0;
            String result = paymentHystrixService.paymentInfo_TimeOut(id);
            return result;
        }

        //善后方法
        public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
            return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
        }
        //善后方法   (注意这里不要有参数,因为是全局的不知道是否有参数,所以统一没有任何参数)
        public String paymentTimeOutDefaultFallbackMethod() {
            return "我是全局消息异常";
        }
    }

14.11 Hystrix之通配服务降级FeignFallback

目前问题2 统一和自定义的分开,代码混乱

服务降级,客户端去调用服务端,碰上服务端宕机或关闭

本次案例服务降级处理是在客户端80实现完成的,与服务端8001没有关系,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦

未来我们要面对的异常

运行 超时 宕机 修改cloud-consumer-feign-hystrix-order80

根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口, 重新新建一个类(AaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理

PaymentFallbackService类实现PaymentHystrixService接口

1 修改cloud-consumer-feign-hystrix-order80

根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口, 重新新建一个类(AaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理

PaymentFallbackService类实现PaymentHystrixService接口

package com.atguigu.springboot.service;

import org.springframework.stereotype.Component;

/**
 * @author 10185
 * @create 2021/3/16 14:03
 */
@Component
public class PaymentFallbackService implements PaymentHystrixService{
    @Override
    public String paymentInfo_OK(Integer id) {
        return "我发生了不知道什么的异常";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
        return "我发生了超时";
    }
}

2 yml

加入

feign:hystrix:enabled

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
#开启

ribbon:
  #指的是建立连接所用的时间适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000
feign:
  hystrix:
    enabled: true
#默认是1秒
#不设置超时时间
hystrix:
  command:
    default:
      execution:
        timeout:
          enabled: false

#或者
#hystrix:
#  command:
#    default:
#      execution:
#        isolation:
#          thread:
#            timeoutInMilliseconds: 3000

3 PaymentHystrixService接口

package com.atguigu.springboot.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 */
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" ,fallback = PaymentFallbackService.class)
public interface PaymentHystrixService
{
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id);

}

测试

单个eureka先启动7001

PaymentHystrixMain8001启动

正常访问测试 - http://localhost/consumer/payment/hystrix/ok/1

故意关闭微服务8001

客户端自己调用提示 - 此时服务端provider已经down了,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器。

14.12 feign:hystrix:enabled注解的解释

,controller中超时时间配置不生效原因:
       关键在于feign:hystrix:enabled: true的作用,官网解释“Feign将使用断路器包装所有方法”,也就是将@FeignClient标记的那个service接口下所有的方法进行了hystrix包装(类似于在这些方法上加了一个@HystrixCommand),这些方法会应用一个默认的超时时间为1s,所以你的service方法也有一个1s的超时时间,service1s就会报异常,controller立马进入备用方法,controller上那个3秒那超时时间就没有效果了。
改变这个默认超时时间方法:
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000
然后ribbon的超时时间也需加上
ribbon:
  ReadTimeout: 5000
  ConnectTimeout: 5000

目前拥有的超时处理

自己设置的 ribbon的超时 feign:hystrix:enabled 中配置的 1秒

14.13 Hystrix之服务熔断理论

断路器,相当于保险丝。

熔断机制概述

熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。

在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。

Martin Fowler的相关论文

image-20210316154836762

14.14 Hystrix之服务熔断案例(上)

Hutool国产工具类

1 修改cloud-provider-hystrix-payment8001

2 在service中添加方法

//=====服务熔断
//表示在10秒中需要有10次请求同时有60%以上是失败的就开启服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
        @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
    if(id < 0) {
        throw new RuntimeException("******id 不能负数");
    }
    String serialNumber = IdUtil.simpleUUID();

    return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
    return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " +id;
}

3 在controller中访问

@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
    String result = paymentService.paymentCircuitBreaker(id);
    log.info("****result: "+result);
    return result;
}

4 测试

image-20210316160228882

根据自己的配置用-1调用了10次,然后在1调用发现调用失败

5 官方文档

The precise way that the circuit opening and closing occurs is as follows:

Assuming the volume across a circuit meets a certain threshold : HystrixCommandProperties.circuitBreakerRequestVolumeThreshold() And assuming that the error percentage, as defined above exceeds the error percentage defined in : HystrixCommandProperties.circuitBreakerErrorThresholdPercentage() Then the circuit-breaker transitions from CLOSED to OPEN. While it is open, it short-circuits all requests made against that circuit-breaker. After some amount of time (HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next request is let through. If it fails, the command stays OPEN for the sleep window. If it succeeds, it transitions to CLOSED and the logic in 1) takes over again. link

6 HystrixCommandProperties配置类

package com.netflix.hystrix;

...

public abstract class HystrixCommandProperties {
    private static final Logger logger = LoggerFactory.getLogger(HystrixCommandProperties.class);

    /* defaults */
    /* package */ static final Integer default_metricsRollingStatisticalWindow = 10000;// default => statisticalWindow: 10000 = 10 seconds (and default of 10 buckets so each bucket is 1 second)
    private static final Integer default_metricsRollingStatisticalWindowBuckets = 10;// default => statisticalWindowBuckets: 10 = 10 buckets in a 10 second window so each bucket is 1 second
    private static final Integer default_circuitBreakerRequestVolumeThreshold = 20;// default => statisticalWindowVolumeThreshold: 20 requests in 10 seconds must occur before statistics matter
    private static final Integer default_circuitBreakerSleepWindowInMilliseconds = 5000;// default => sleepWindow: 5000 = 5 seconds that we will sleep before trying again after tripping the circuit
    private static final Integer default_circuitBreakerErrorThresholdPercentage = 50;// default => errorThresholdPercentage = 50 = if 50%+ of requests in 10 seconds are failures or latent then we will trip the circuit
    private static final Boolean default_circuitBreakerForceOpen = false;// default => forceCircuitOpen = false (we want to allow traffic)
    /* package */ static final Boolean default_circuitBreakerForceClosed = false;// default => ignoreErrors = false 
    private static final Integer default_executionTimeoutInMilliseconds = 1000; // default => executionTimeoutInMilliseconds: 1000 = 1 second
    private static final Boolean default_executionTimeoutEnabled = true;

    ...
}
————————————————
版权声明本文为CSDN博主巨輪的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明
原文链接https://blog.csdn.net/u011863024/article/details/114298282

14.15 Hystrix之服务总结

大神结论

Martin Fowler的相关论文

image-20210316160802085

熔断类型

熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态。 熔断关闭:熔断关闭不会对服务进行熔断。 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断。

官网断路器流程图

image-20210316160839148

官网步骤

The precise way that the circuit opening and closing occurs is as follows:

Assuming the volume across a circuit meets a certain threshold : HystrixCommandProperties.circuitBreakerRequestVolumeThreshold() And assuming that the error percentage, as defined above exceeds the error percentage defined in : HystrixCommandProperties.circuitBreakerErrorThresholdPercentage() Then the circuit-breaker transitions from CLOSED to OPEN. While it is open, it short-circuits all requests made against that circuit-breaker. After some amount of time (HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next request is let through. If it fails, the command stays OPEN for the sleep window. If it succeeds, it transitions to CLOSED and the logic in 1) takes over again. link

断路器在什么情况下开始起作用

//=====服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
    @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
    ...
}

涉及到断路器的三个重要参数:

快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次7,即使所有的请求都超时或其他原因失败,断路器都不会打开。 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。

断路器开启或者关闭的条件

到达以下阀值,断路器将会开启:

当满足一定的阀值的时候(默认10秒内超过20个请求次数) 当失败率达到一定的时候(默认10秒内超过50%的请求失败) 当开启的时候,所有请求都不会进行转发

一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。

断路器打开之后

1:再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。

2:原来的主逻辑要如何恢复呢?

对于这一问题,hystrix也为我们实现了自动恢复功能。

当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。

ALL配置

@HystrixCommand(fallbackMethod = "fallbackMethod", 
                groupKey = "strGroupCommand", 
                commandKey = "strCommand", 
                threadPoolKey = "strThreadPool",
                
                commandProperties = {
                    // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离
                    @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
                    // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
                    @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
                    // 配置命令执行的超时时间
                    @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
                    // 是否启用超时时间
                    @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
                    // 执行超时的时候是否中断
                    @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
                    
                    // 执行被取消的时候是否中断
                    @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
                    // 允许回调方法执行的最大并发数
                    @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
                    // 服务降级是否启用,是否执行回调函数
                    @HystrixProperty(name = "fallback.enabled", value = "true"),
                    // 是否启用断路器
                    @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                    // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
                    
                    // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50, 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                    // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,如果成功就设置为 "关闭" 状态。
                    @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
                    // 断路器强制打开
                    @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
                    // 断路器强制关闭
                    @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
                    // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
                    @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
                    
                    // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
                    // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常
                    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
                    // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。
                    @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
                    // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
                    @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
                    // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。
                    @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
                    // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
                    // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
                    // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
                    @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
                    
                    // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。
                    @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
                    // 是否开启请求缓存
                    @HystrixProperty(name = "requestCache.enabled", value = "true"),
                    // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中
                    @HystrixProperty(name = "requestLog.enabled", value = "true"),

                },
                threadPoolProperties = {
                    // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
                    @HystrixProperty(name = "coreSize", value = "10"),
                    // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,否则将使用 LinkedBlockingQueue 实现的队列。
                    @HystrixProperty(name = "maxQueueSize", value = "-1"),
                    // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
                    // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
                }
               )
public String doSomething() {
	...
}
————————————————
版权声明本文为CSDN博主巨輪的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明
原文链接https://blog.csdn.net/u011863024/article/details/114298282

14.16 服务限流 - 后面高级篇讲解alibaba的Sentinel说明

官方解释

步骤说明

创建HystrixCommand (用在依赖的服务返回单个操作结果的时候)或HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候)对象。 命令执行。 其中 HystrixCommand实现了下面前两种执行方式 execute():同步执行,从依赖的服务返回一个单一的结果对象或是在发生错误的时候抛出异常。

  1. queue():异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。 而 HystrixObservableCommand实现了后两种执行方式: obseve():返回Observable对象,它代表了操作的多个统 果,它是一个Hot Observable (不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于Hot Observable的每一个“订阅者”都有可能是从“事件源”的中途开始的,并可能只是看到了整个操作的局部过程)。
  2. toObservable():同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有“订间者”的时候并不会发布事件,而是进行等待,直到有“订阅者"之后才发布事件,所以对于Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)。 若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。 检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(第8步);如果断路器是关闭的,检查是否有可用资源来执行命令(第5步)。 线程池/请求队列信号量是否占满。如果命令依赖服务的专有线程地和请求队列,或者信号量(不使用线程的时候)已经被占满,那么Hystrix也不会执行命令,而是转接到fallback处理理辑(第8步) 。 Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。 HystrixCommand.run():返回一个单一的结果,或者抛出异常。 HystrixObservableCommand.construct():返回一个Observable对象来发射多个结果,或通过onError发送错误通知。 Hystix会将“成功”、“失败”、“拒绝”、“超时” 等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行"熔断/短路”。 当命令执行失败的时候,Hystix会进入fallback尝试回退处理,我们通常也称波操作为“服务降级”。而能够引起服务降级处理的情况有下面几种: 第4步∶当前命令处于“熔断/短路”状态,断洛器是打开的时候。 第5步∶当前命令的钱程池、请求队列或者信号量被占满的时候。 第6步∶HystrixObsevableCommand.construct()或HytrixCommand.run()抛出异常的时候。 当Hystrix命令执行成功之后,它会将处理结果直接返回或是以Observable的形式返回。 tips:如果我们没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常,Hystrix依然会运回一个Obsevable对象,但是它不会发射任结果数惯,而是通过onError方法通知命令立即中断请求,并通过onError方法将引起命令失败的异常发送给调用者

14.17 Hystrix图形化Dashboard监控实战

1 概述

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。

Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。

2 仪表盘9001

1 新建cloud-consumer-hystrix-dashboard9001
2 POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-hystrix-dashboard9001</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>
3 YML
server:
  port: 9001
4 HystrixDashboardMain9001+新注解@EnableHystrixDashboard
package com.atguigu.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001
{
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboardMain9001.class, args);
    }
}
5 所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
6.启动cloud-consumer-hystrix-dashboard9001该微服务后续将监控微服务8001

浏览器输入http://localhost:9001/hystrix

3 payment8001加上监控路径

1 修改cloud-provider-hystrix-payment8001

注意:新版本Hystrix需要在主启动类PaymentHystrixMain8001中指定监控路径

package com.atguigu.springcloud;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

/**
 * @author 10185
 * @create 2021/3/10 21:04
 */
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker//添加到此处

public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }

}

4 监控测试

监控测试

启动1个eureka

启动8001,9001

观察监控窗口

9001监控8001 - 填写监控地址 - http://localhost:8001/hystrix.stream 到 http://localhost:9001/hystrix页面的输入框。

测试地址

  • http://localhost:8001/payment/circuit/1
  • http://localhost:8001/payment/circuit/-1
  • 测试通过
  • 先访问正确地址,再访问错误地址,再正确地址,会发现图示断路器都是慢慢放开的。

img

如何看?

  • 7色

img

  • 1圈

实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。

该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例。

  • 1线

曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。

  • 整图说明

img

  • 整图说明2

img

15 GateWay

上一代zuul 1.x官网

Gateway官网

15.1 概述

Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关;

但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul,那就是SpringCloud Gateway—句话:gateway是原zuul1.x版的替代

img

Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和Project Reactor等技术。

Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。

SpringCloud Gateway是Spring Cloud的一个全新项目,基于Spring 5.0+Spring Boot 2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供—种简单有效的统一的API路由管理方式。

SpringCloud Gateway作为Spring Cloud 生态系统中的网关,目标是替代Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty

Spring Cloud Gateway的目标提供统一的路由方式且基于 Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

作用

  • 方向代理
  • 鉴权
  • 流量控制
  • 熔断
  • 日志监控

微服务架构中网关的位置

img

15.2 GateWay非阻塞异步模型

有Zuull了怎么又出来Gateway?我们为什么选择Gateway?

  1. netflix不太靠谱,zuul2.0一直跳票,迟迟不发布。
    1. 一方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。
    2. Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的Zuul 2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?
    3. 多方面综合考虑Gateway是很理想的网关选择。
  2. SpringCloud Gateway具有如下特性
    1. 基于Spring Framework 5,Project Reactor和Spring Boot 2.0进行构建;
    2. 动态路由:能够匹配任何请求属性;
    3. 可以对路由指定Predicate (断言)和Filter(过滤器);
    4. 集成Hystrix的断路器功能;
    5. 集成Spring Cloud 服务发现功能;
    6. 易于编写的Predicate (断言)和Filter (过滤器);
    7. 请求限流功能;
    8. 支持路径重写。
  3. SpringCloud Gateway与Zuul的区别
    1. 在SpringCloud Finchley正式版之前,Spring Cloud推荐的网关是Netflix提供的Zuul。
    2. Zuul 1.x,是一个基于阻塞I/O的API Gateway。
    3. Zuul 1.x基于Servlet 2.5使用阻塞架构它不支持任何长连接(如WebSocket)Zuul的设计模式和Nginx较像,每次I/О操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第-次加载较慢的情况,使得Zuul的性能相对较差。
    4. Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul .x的性能较Zuul 1.x有较大提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求数)是Zuul的1.6倍。
    5. Spring Cloud Gateway建立在Spring Framework 5、Project Reactor和Spring Boot2之上,使用非阻塞API。
    6. Spring Cloud Gateway还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验

Zuul1.x模型

Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Serviet IO处理模型。

Servlet的生命周期?servlet由servlet container进行生命周期管理。

  • container启动时构造servlet对象并调用servlet init()进行初始化;
  • container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service);
  • container关闭时调用servlet destory()销毁servlet。

img

上述模式的缺点

Servlet是一个简单的网络IO模型,当请求进入Servlet container时,Servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(如抽风用Jmeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势。

所以Zuul 1.X是基于servlet之上的一个阻塞式处理模型,即Spring实现了处理所有request请求的一个servlet (DispatcherServlet)并由该servlet阻塞式处理处理。所以SpringCloud Zuul无法摆脱servlet模型的弊端。

Gateway模型

WebFlux是什么?官方文档

传统的Web框架,比如说: Struts2,SpringMVC等都是基于Servlet APl与Servlet容器基础之上运行的。

但是在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring 5必须让你使用Java 8)。

Spring WebFlux是Spring 5.0 引入的新的响应式框架,区别于Spring MVC,它不需要依赖Servlet APl,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。

Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux. It does not work in a traditional Servlet Container or when built as a WAR.link

15.3 Gateway工作流程

三大核心概念

  1. Route(路由) - 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由;
  2. Predicate(断言) - 参考的是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由;
  3. Filter(过滤) - 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

img

web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。

predicate就是我们的匹配条件;而fliter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了

Gateway工作流程

官网总结

img

Clients make requests to Spring Cloud Gateway. If the Gateway Handler Mapping determines that a request matches a route, it is sent to the Gateway Web Handler. This handler runs the request through a filter chain that is specific to the request. The reason the filters are divided by the dotted line is that filters can run logic both before and after the proxy request is sent. All “pre” filter logic is executed. Then the proxy request is made. After the proxy request is made, the “post” filter logic is run. link

客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler。

Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post")执行业务逻辑。

Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

核心逻辑:路由转发 + 执行过滤器链。

15.4 Gateway9527搭建

1 新建Module - cloud-gateway-gateway9527

2 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-gateway-gateway9527</artifactId>
    <dependencies>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--一般基础配置类-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>

3 yml

server:
  port: 9527
#显示在页面上前面的服务提供的名字
spring:
  application:
    name: cloud-gateway
  #############################新增网关配置###########################
  cloud:
    gateway:
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001          #匹配后提供服务的路由地址
          #uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/getPayment/**         # 断言,路径相匹配的进行路由

        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001          #匹配后提供服务的路由地址
          #uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################

eureka:
  instance:
#服务显示后面的地址的隐藏
    hostname: cloud-gateway-service
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

cloud-provider-payment8001看看controller的访问地址

  • get
  • lb

我们目前不想暴露8001端口,希望在8001外面套一层9527

4 业务类

5 主启动类

package com.atguigu.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;


@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527
{
    public static void main(String[] args) {
        SpringApplication.run(GateWayMain9527.class, args);
    }
}

6 测试

  • 启动7001
  • 启动8001-cloud-provider-payment8001
  • 启动9527网关
  • 访问说明
    • 添加网关前 - http://localhost:8001/payment/get/1
    • 添加网关后 - http://localhost:9527/payment/get/1
    • 两者访问成功,返回相同结果

15.5 Gateway配置路由的另一种方式

1 官方案例

RemoteAddressResolver resolver = XForwardedRemoteAddressResolver
    .maxTrustedIndex(1);

...

.route("direct-route",
    r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24")
        .uri("https://downstream1")
.route("proxied-route",
    r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24")
        .uri("https://downstream2")
)

2 自己写一个

百度国内新闻网址,需要外网 - http://news.baidu.com/guonei

业务需求 - 通过9527网关访问到外网的百度新闻网址

cloud-gateway-gateway9527业务实现

package com.atguigu.springcloud.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 10185
 * @create 2021/3/17 19:10
 */
@Configuration

public class GateWayConfig {


    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder)
    {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route("atguigu", r->(r.path("/guonei").uri("http://news.baidu.com/guonei"))).build();

        return routes.build();

    }
}

测试:

浏览器中输入http://localhost:9527/guonei、返回http://news.baidu.com/guonei相同的页面

15.6 GateWay配置动态路由

默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建一个动态路由进行转发,从而实现动态路由的功能(不写死一个地址)

启动

eureka7001 payment8001/8002

yml

server:
  port: 9527
#显示在页面上前面的服务提供的名字
spring:
  application:
    name: cloud-gateway
  #############################新增网关配置###########################
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/getPayment/**         # 断言,路径相匹配的进行路由

        - id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/payment/discovery/**         # 断言,路径相匹配的进行路由
####################################################################

eureka:
  instance:
    #服务显示后面的地址的隐藏
    hostname: cloud-gateway-service
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

2 测试

浏览器输入 - http://localhost:9527/payment/lb

结果

不停刷新页面,8001/8002两个端口切换。

15.7 GateWay常用的Predicate

官方文档

Route Predicate Factories这个是什么

Spring Cloud Gateway matches routes as part of the Spring WebFlux HandlerMapping infrastructure. Spring Cloud Gateway includes many built-in route predicate factories. All of these predicates match on different attributes of the HTTP request. You can combine multiple route predicate factories with logical and statements. link

Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。

Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个RoutePredicate工厂可以进行组合。

Spring Cloud Gateway创建Route 对象时,使用RoutePredicateFactory 创建 Predicate对象,Predicate 对象可以赋值给Route。Spring Cloud Gateway包含许多内置的Route Predicate Factories。 所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and。

predicate

美: [‘predɪkeɪt] 英: [‘predɪkət]

v. 断言;使基于;使以…为依据;表明

adj. 述语的;谓项的

n. 谓语(句子成分,对主语加以陈述,如 John went home 中的 went home)

常用的Route Predicate Factory

  1. The After Route Predicate Factory
  2. The Before Route Predicate Factory
  3. The Between Route Predicate Factory
  4. The Cookie Route Predicate Factory
  5. The Header Route Predicate Factory
  6. The Host Route Predicate Factory
  7. The Method Route Predicate Factory
  8. The Path Route Predicate Factory
  9. The Query Route Predicate Factory
  10. The RemoteAddr Route Predicate Factory
  11. The weight Route Predicate Factory

讨论几个Route Predicate Factory

The After Route Predicate Factory

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: https://example.org
        predicates:
        # 这个时间后才能起效
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]

可以通过下述方法获得上述格式的时间戳字符串

import java.time.ZonedDateTime;


public class T2
{
    public static void main(String[] args)
    {
        ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
        System.out.println(zbj);

       //2021-02-22T15:51:37.485+08:00[Asia/Shanghai]
    }
}

The Between Route Predicate Factory

spring:
  cloud:
    gateway:
      routes:
      - id: between_route
        uri: https://example.org
        # 两个时间点之间
        predicates:
        - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

The Cookie Route Predicate Factory

spring:
  cloud:
    gateway:
      routes:
      - id: cookie_route
        uri: https://example.org
        predicates:
        - Cookie=chocolate, ch.p

The cookie route predicate factory takes two parameters, the cookie name and a regular expression.

This predicate matches cookies that have the given name and whose values match the regular expression.

cmd测试

# 该命令相当于发get请求,且没带cookie
curl http://localhost:9527/payment/lb

# 带cookie的
curl http://localhost:9527/payment/lb --cookie "chocolate=chip"

The Header Route Predicate Factory

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: https://example.org
        predicates:
        #请求头中带X-Request-Id,同时为正整数
        - Header=X-Request-Id, \d+

The header route predicate factory takes two parameters, the header name and a regular expression.

This predicate matches with a header that has the given name whose value matches the regular expression.

测试

# 带指定请求头的参数的CURL命令
curl http://localhost:9527/payment/lb -H "X-Request-Id:123"

其它的,举一反三。

小结

说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。

15.8 GateWay的Filter

官方文档

Route filters allow the modification of the incoming HTTP request or outgoing HTTP response in some manner. Route filters are scoped to a particular route. Spring Cloud Gateway includes many built-in GatewayFilter Factories.

路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。

Spring Cloud Gateway的Filter:

  • 生命周期:
    • pre
    • post
  • 种类(具体看官方文档):
    • GatewayFilter - 有31种
    • GlobalFilter - 有10种

常用的GatewayFilter:AddRequestParameter GatewayFilter

自定义全局GlobalFilter:

两个主要接口介绍:

  1. GlobalFilter
  2. Ordered

能干什么:

  1. 全局日志记录
  2. 统一网关鉴权

代码案例:

GateWay9527项目添加MyLogGateWayFilter类:

package com.atguigu.springcloud.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author 10185
 * @create 2021/3/17 20:38
 */
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //得到请求中的username这个参数
        String username = exchange.getRequest().getQueryParams().getFirst("username");
        if (username == null) {
            log.error("不能添加到组件里");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

测试:

启动:

  • EurekaMain7001
  • PaymentMain8001
  • GateWayMain9527
  • PaymentMain8002

浏览器输入:

  • http://localhost:9527/payment/payment/discovery- 反问异常
  • http://localhost:9527/payment/payment/discovery?username=xiao - 正常反问

image-20210317205153004

16 Config分布式

16.1 config配置中心的介绍

分布式系统面临的配置问题

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。

SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理.……

是什么

SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

怎么玩

SpringCloud Config分为服务端和客户端两部分。

服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。

客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

能干嘛

集中管理配置文件 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置 将配置信息以REST接口的形式暴露 - post/crul访问刷新即可… 与GitHub整合配置

由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式。

官网

https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/

16.2 Config配置总控中心搭建

用你自己的账号在GitHub上新建一个名为springcloud-config的新Repository

1 由上一步获得刚新建的git地址-

git@github.com:marsxiaodidi/springcloud-config.git

2 在d盘新建SpringCloud2021

$ git clone https://github.com/marsxiaodidi/springcloud-config.git

image-20210320092915420

image-20210320093148044

3 看文件状态

$ cd springcloud-config
$ git status

4 加入文件

$ git add *.yml

5 提交文件

$ git commit *.yml

6 上传到云端

$ git push origin

7 新建Module模块cloud-config-center-3344

8 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-config-center-3344</artifactId>

    <dependencies>
        <!--添加消息总线RabbitMQ支持-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>







</project>

9 yml

server:
  port: 3344

spring:
  application:
    name:  cloud-config-center #注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          uri: git@github.com:marsxiaodidi/springcloud-config.git #GitHub上面的git仓库名字
          ####搜索目录
          search-paths:
            - springcloud-config
      ####读取分支
      label: master

#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

10 主启动类

@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344
{
    public static void main(String[] args) {
        SpringApplication.run(ConfigCenterMain3344.class, args);
    }
}

11 修改hosts文件,增加映射

127.0.0.1 config-3344.com

测试通过Config微服务是否可以从GitHub上获取配置内容

启动ConfigCenterMain3344

浏览器防问 - http://config-3344.com:3344/master/config-dev.yml

页面返回结果:

config:
  info: "master branch,springcloud-config/config-dev.yml version=7"

12 配置读取规则

配置读取规则

官方文档

/{label}/{application}-{profile}.yml(推荐)

master分支 http://config-3344.com:3344/master/config-dev.yml http://config-3344.com:3344/master/config-test.yml http://config-3344.com:3344/master/config-prod.yml dev分支 http://config-3344.com:3344/dev/config-dev.yml http://config-3344.com:3344/dev/config-test.yml http://config-3344.com:3344/dev/config-prod.yml /{application}-{profile}.yml

http://config-3344.com:3344/config-dev.yml http://config-3344.com:3344/config-test.yml http://config-3344.com:3344/config-prod.yml http://config-3344.com:3344/config-xxxx.yml(不存在的配置) /{application}/{profile}[/{label}]

http://config-3344.com:3344/config/dev/master http://config-3344.com:3344/config/test/master http://config-3344.com:3344/config/test/dev 重要配置细节总结

/{name}-{profiles}.yml /{label}-{name}-{profiles}.yml label:分支(branch) name:服务名 profiles:环境(dev/test/prod) 成功实现了用SpringCloud Config通过GitHub获取配置信息

16.3 Config客户端配置与测试

1 新建cloud-config-client-3355

2 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-config-client-3355</artifactId>
    <dependencies>
        <!--添加消息总线RabbitMQ支持-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>



</project>

3 bootstrap.xml

applicaiton.yml是用户级的资源配置项

bootstrap.yml是系统级的,优先级更加高

Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context的父上下文。

初始化的时候,BootstrapContext负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment。

Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。Bootstrap context和Application Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap Context和Application Context配置的分离。

要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的,因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml。

注意:bootstrap在启动时先会去bootstrap.xml中配置的端口中去寻找配置文件,然后放入服务器

image-20210320110619179

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    #Config客户端配置
    config:
      label: master #分支名称
      name: config #配置文件名称
      profile: dev #读取后缀名称   上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
      uri: http://localhost:3344 #配置中心地址k


#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
4 启动类
package com.atguigu.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author 10185
 * @create 2021/3/20 10:51
 */
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355
{
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class, args);
    }
}

5 controller

package com.atguigu.springboot.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 10185
 * @create 2021/3/20 10:53
 */
@RestController
public class ConfigClientController {
    @Value("${config.info}")
    private String configInfo;
    @GetMapping("/configInfo")
    public String getConfigInfo() {
         return configInfo;
    }
}

测试

启动Config配置中心3344微服务并自测

http://config-3344.com:3344/master/config-prod.yml http://config-3344.com:3344/master/config-dev.yml 启动3355作为Client准备访问

http://localhost:3355/configlnfo 成功实现了客户端3355访问SpringCloud Config3344通过GitHub获取配置信息可题随时而来

分布式配置的动态刷新问题

Linux运维修改GitHub上的配置文件内容做调整 刷新3344,发现ConfigServer配置中心立刻响应 刷新3355,发现ConfigClient客户端没有任何响应 3355没有变化除非自己重启或者重新加载 难到每次运维修改配置文件,客户端都需要重启??噩梦

16.4 Config动态刷新之手动版

避免每次更新配置都要重启客户端微服务3355

动态刷新步骤:

修改3355模块

1 pom引入actuator监控

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2 修改YML,添加暴露监控端口的配置

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

3 @RefreshScope业务类Controller修改

@RestController
@RefreshScope//<-----
public class ConfigClientController {

测试

此时修改github配置文件内容 -> 访问3344 -> 访问3355

http://localhost:3355/configInfo

3355改变没有??? 没有,还需一步

How

需要运维人员发送Post请求刷新3355

curl -X POST “http://localhost:3355/actuator/refresh” 1 再次测试

http://localhost:3355/configInfo

3355改变没有??? 改了。

成功实现了客户端3355刷新到最新配置内容,避免了服务重启

想想还有什么问题?

假如有多个微服务客户端3355/3366/3377 每个微服务都要执行—次post请求,手动刷新? 可否广播,一次通知,处处生效? 我们想大范围的自动刷新,求方法

16.5 Bus消息总线是什么

上一讲解的加深和扩充

一言以蔽之,分布式自动刷新配置功能。

Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新。

是什么

Spring Cloud Bus 配合Spring Cloud Config 使用可以实现配置的动态刷新。

img

Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。Spring Clud Bus目前支持RabbitMQ和Kafka。

能干嘛

Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。

img

为何被称为总线

什么是总线

在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。

基本原理

ConfigClient实例都监听MQ中同一个topic(默认是Spring Cloud Bus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。

16.6 制作3366

1 安装rabbitmq

image-20210320202013133

2 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-config-client-3366</artifactId>
    <dependencies>
        <!--添加消息总线RabbitMQ支持-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>



</project>

3 bootstrap.yml

server:
  port: 3366

spring:
  application:
    name: config-client
  cloud:
    #Config客户端配置
    config:
      label: master #分支名称
      name: config #配置文件名称
      profile: dev #读取后缀名称   上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
      uri: http://localhost:3344 #配置中心地址k


#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

4 主启动类

package com.atguigu.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author 10185
 * @create 2021/3/20 10:51
 */
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3366
{
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3366.class, args);
    }
}

5 configClientController

 */
@RestController
@RefreshScope//<-----
public class ConfigClientController {

   
        @Value("${server.port}")
        private String serverPort;

        @Value("${config.info}")
        private String configInfo;

        @GetMapping("/configInfo")
        public String configInfo()
        {
            return "serverPort: "+serverPort+"\t\n\n configInfo: "+configInfo;
        }

    
}

16.7 消息总线的设计思想

设计思想

1.利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置img

2.利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置img

图二的架构显然更加适合,图—不适合的原因如下:

打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责。

破坏了微服务各节点的对等性。

有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修

16.8 Bus动态刷新全局广播配置实现

1 给cloud-config-center-3344配置中心服务端添加消息总线支持

pom
<!--添加消息总线RabbitNQ支持-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bus-amap</artifactId>
</dependency>
<dependency>
	<groupId>org-springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yml
server:
  port: 3344

spring:
  application:
    name:  cloud-config-center #注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          uri: git@github.com:marsxiaodidi/springcloud-config.git #GitHub上面的git仓库名字
          ####搜索目录
          search-paths:
            - springcloud-config
      ####读取分支
      label: master
  #rabbitmq相关配置<--------------------------
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
#rabbitmq相关配置<--------------------------
#服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

##rabbitmq相关配置,暴露bus刷新配置的端点<--------------------------
management:
  endpoints: #暴露bus刷新配置的端点
    web:
      exposure:
        include: 'bus-refresh'

2 给cloud-config-client-3355客户端添加消息总线支持

pom
<!--添加消息总线RabbitNQ支持-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bus-amap</artifactId>
</dependency>
<dependency>
	<groupId>org-springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yml
package com.atguigu.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author 10185
 * @create 2021/3/20 10:51
 */
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355
{
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class, args);
    }
}

3 给cloud-config-client-3366客户端添加消息总线支持

测试

启动

EurekaMain7001 ConfigcenterMain3344 ConfigclientMain3355 ConfigclicntMain3366 运维工程师

修改Github上配置文件内容,增加版本号 发送POST请求 curl -X POST “http://localhost:3344/actuator/bus-refresh” —次发送,处处生效 配置中心

http://config-3344.com:3344/config-dev.yml 客户端

http://localhost:3355/configlnfo http://localhost:3366/configInfo 获取配置信息,发现都已经刷新了 —次修改,广播通知,处处生效

16.9 Bus动态刷新定点通知

不想全部通知,只想定点通知

只通知3355 不通知3366 简单一句话 - 指定具体某一个实例生效而不是全部

公式:http://localhost:3344/actuator/bus-refresh/{destination}

/bus/refresh请求不再发送到具体的服务实例上,而是发给config server通过destination参数类指定需要更新配置的服务或实例

案例

我们这里以刷新运行在3355端口上的config-client(配置文件中设定的应用名称)为例,只通知3355,不通知3366 curl -X POST “http://localhost:3344/actuator/bus-refresh/config-client:3355” 通知总结

17 Stream

17.1 Stream的引入

常见MQ(消息中间件):

ActiveMQ RabbitMQ RocketMQ Kafka 有没有一种新的技术诞生,让我们不再关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。(类似于Hibernate)

Cloud Stream是什么?屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型

17.2 Stream是什么及Binder介绍

官方文档1

官方文档2

Cloud Stream中文指导手册

什么是Spring Cloud Stream?

官方定义Spring Cloud Stream是一个构建消息驱动微服务的框架。

应用程序通过inputs或者 outputs 来与Spring Cloud Stream中binder对象交互。

通过我们配置来binding(绑定),而Spring Cloud Stream 的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。

通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。 Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。

目前仅支持RabbitMQ、 Kafka。

17.3 Stream设计思想

标准MQ

img

生产者/消费者之间靠消息媒介传递信息内容 消息必须走特定的通道 - 消息通道 Message Channel 消息通道里的消息如何被消费呢,谁负责收发处理 - 消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅。 为什么用Cloud Stream?

比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分区。

img

这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候Spring Cloud Stream给我们提供了—种解耦合的方式。

Stream凭什么可以统一底层差异?

在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。

通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。

Binder:

INPUT对应于消费者

OUTPUT对应于生产者

img

Stream中的消息通信方式遵循了发布-订阅模式

Topic主题进行广播

  • 在RabbitMQ就是Exchange
  • 在Kakfa中就是Topic

17.4 Stream编码常用注解简介

Spring Cloud Stream标准流程套路

img

img

Binder - 很方便的连接中间件,屏蔽差异。

Channel - 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置。

Source和Sink - 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就 编码API和常用注解

image-20210321101414323

案例说明

准备RabbitMQ环境(79_Bus之RabbitMQ环境配置有提及)

工程中新建三个子模块

cloud-stream-rabbitmq-provider8801,作为生产者进行发消息模块 cloud-stream-rabbitmq-consumer8802,作为消息接收模块 cloud-stream-rabbitmq-consumer8803,作为消息接收模块

17.5 Stream消息驱动之生产者

1 新建Module: cloud-stream-rabbitmq-provider8801

2 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-stream-rabbitmq-provider8801</artifactId>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
        <!--基础配置-->

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

3 yml

server:
  port: 8801

spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息;
        defaultRabbit: # 表示定义的名称,用于于binding整合
          type: rabbit # 消息组件类型
          environment: # 设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings: # 服务的整合处理
        output: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
          binder: {defaultRabbit} # 设置要绑定的消息服务的具体设置



eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: send-8801.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

4 主启动类StreamMQMain8801

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StreamMQMain8801 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8801.class,args);
    }
}

5 业务类

发送消息接口
package com.atguigu.springcloud.service;

/**
 * @author 10185
 * @create 2021/3/21 9:30
 */
public interface IMessageProvider {
    /**
     * 发送消息接口
     * @return
     */
   String send();
}
发送消息接口实现类
package com.atguigu.springcloud.service.impl;

import com.atguigu.springcloud.service.IMessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;


import javax.annotation.Resource;
import java.util.UUID;

/**
 * @author 10185
 * @create 2021/3/21 9:31
 */

@EnableBinding(Source.class)
public class IMessageProviderImpl implements IMessageProvider {
    @Qualifier("output")
    @Autowired
    private MessageChannel messageChannel;
    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
        messageChannel.send(MessageBuilder.withPayload(serial).build());
        System.out.println("*****serial: "+serial);
        return null;
    }
}

6 Controller

package com.atguigu.springcloud.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.atguigu.springcloud.service.IMessageProvider;

import javax.annotation.Resource;

@RestController
public class SendMessageController
{
    @Resource
    private IMessageProvider messageProvider;

    @GetMapping(value = "/sendMessage")
    public String sendMessage() {
        return messageProvider.send();
    }

}

17.6 Stream消息驱动之消费者

1 新建Module: cloud-stream-rabbitmq-consumer8802

2 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-stream-rabbitmq-consumer8802</artifactId>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--基础配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

3 YML

server:
  port: 8802

spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息
        defaultRabbit: # 表示定义的名称用于于binding整合
          type: rabbit # 消息组件类型
          environment: # 设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型本次为对象json如果是文本则设置text/plain
          binder: {defaultRabbit} # 设置要绑定的消息服务的具体设置

eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔默认是30秒
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔默认是90秒
    instance-id: receive-8802.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

4 主启动类StreamMQMain8802

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StreamMQMain8802 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8802.class,args);
    }
}

5 业务类

package com.atguigu.springcloud.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;


@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController
{
    @Value("${server.port}")
    private String serverPort;


    @StreamListener(Sink.INPUT)
    public void input(Message<String> message)
    {
        System.out.println("消费者1号,----->接受到的消息: "+message.getPayload()+"\t  port: "+serverPort);
    }
}

测试

  • 启动EurekaMain7001
  • 启动StreamMQMain8801
  • 启动StreamMQMain8802
  • 8801发送8802接收消息

17.7 Stream之消息重复消费

依照8802,克隆出来一份运行8803 - cloud-stream-rabbitmq-consumer8803。

启动

RabbitMQ 服务注册 - 7001 消息生产 - 8801 消息消费 - 8802 消息消费 - 8803 运行后有两个问题

有重复消费问题 消息持久化问题 消费

http://localhost:8801/sendMessage 目前是8802/8803同时都收到了,存在重复消费问题 如何解决:分组和持久化属性group(重要) 生产实际案例

比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决。

img

注意在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。不同组是可以全面消费的(重复消费)。

17.8 Stream之group解决消息重复消费

原理

微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。

不同的组是可以重复消费的,同一个组内会发生竞争关系,只有其中一个可以消费。

8802/8803都变成不同组,group两个不同

group: A_Group、B_Group

8802修改YML

spring:
  application:
    name: cloud-stream-provider
  cloud:
      stream:
        binders: # 在此处配置要绑定的rabbitmq的服务信息;
          defaultRabbit: # 表示定义的名称,用于于binding整合
            type: rabbit # 消息组件类型
            environment: # 设置rabbitmq的相关的环境配置
              spring:
                rabbitmq:
                  host: localhost
                  port: 5672
                  username: guest
                  password: guest
        bindings: # 服务的整合处理
          output: # 这个名字是一个通道的名称
            destination: studyExchange # 表示要使用的Exchange名称定义
            content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
            binder: defaultRabbit # 设置要绑定的消息服务的具体设置
            group: A_Group #<----------------------------------------关键

8803修改YML(与8802的类似位置 group: B_Group)

8803修改YML(与8802的类似位置 group: B_Group)

结论:还是重复消费

8802/8803实现了轮询分组,每次只有一个消费者,8801模块的发的消息只能被8802或8803其中一个接收到,这样避免了重复消费。

8802/8803都变成相同组,group两个相同

group: A_Group

8802修改YMLgroup: A_Group

8803修改YMLgroup: A_Group

结论:同一个组的多个微服务实例,每次只会有一个拿到

17.9 Stream之消息持久化

通过上述,解决了重复消费问题,再看看持久化。

停止8802/8803并去除掉8802的分组group: A_Group,8803的分组group: A_Group没有去掉。

8801先发送4条消息到RabbitMq。

先启动8802,无分组属性配置,后台没有打出来消息。

再启动8803,有分组属性配置,后台打出来了MQ上的消息。(消息持久化体现)

18 Sleuth服务跟踪

18.1 Sleuth简介

为什么会出现这个技术?要解决哪些问题?

在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时img或错误都会引起整个请求最后的失败。

é\u0093¾è·¯å¤\u009aèµ·æ\u009d¥ç\u009a\u0084æ\u0083\u0085å\u0086µ

是什么

https://github.com/spring-cloud/spring-cloud-sleuth Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案 在分布式系统中提供追踪解决方案并且兼容支持了zipkin 解决

img

18.2 Sleuth之zipkin搭建安装

1.zipkin

下载

SpringCloud从F版起已不需要自己构建Zipkin Server了,只需调用jar包即可 https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/ zipkin-server-2.12.9-exec.jar 运行jar

java -jar zipkin-server-2.12.9-exec.jar

运行控制台

http://localhost:9411/zipkin/

术语

完整的调用链路

表示一请求链路,一条链路通过Trace ld唯一标识,Span标识发起的请求信息,各span通过parent id关联起来

img

—条链路通过Trace ld唯一标识,Span标识发起的请求信息,各span通过parent id关联起来。

img

整个链路的依赖关系如下:

img

名词解释

Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识 span:表示调用链路来源,通俗的理解span就是一次请求信息

18.3 Sleuth链路监控展现

1 在cloud-provider-payment8001中添加pom

<!--包含了sleuth+zipkin-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

2 yml

spring:
  application:
    name: cloud-payment-service

  zipkin: #<-------------------------------------关键 
      base-url: http://localhost:9411
  sleuth: #<-------------------------------------关键
    sampler:
    #采样率值介于 0 到 1 之间,1 则表示全部采集
    probability: 1
    
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver              # mysql驱动包
    url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

3 业务类PaymentController

@RestController
@Slf4j
public class PaymentController {
    
    ...
    
 	@GetMapping("/payment/zipkin")
    public String paymentZipkin() {
        return "hi ,i'am paymentzipkin server fall back,welcome to here, O(∩_∩)O哈哈~";
    }    
}

4 在服务消费方cloud-consumer-order80添加pom

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

5 yml

spring:
    application:
        name: cloud-order-service
    zipkin:
      base-url: http://localhost:9411
    sleuth:
      sampler:
        probability: 1

6 业务类orderController

   // ====================> zipkin+sleuth
    @GetMapping("/consumer/payment/zipkin")
    public String paymentZipkin()
    {
        String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
        return result;
    }
}

4.依次启动eureka7001/8001/80 - 80调用8001几次测试下

5.打开浏览器访问: http://localhost:9411

image-20210321150035408

19 Cloud Alibaba

19.1 简介

为什么会出现SpringCloud alibaba

Spring Cloud Netflix项目进入维护模式

https://spring.io/blog/2018/12/12/spring-cloud-greenwich-rc1-available-now

什么是维护模式?

将模块置于维护模式,意味着Spring Cloud团队将不会再向模块添加新功能。

他们将修复block级别的 bug 以及安全问题,他们也会考虑并审查社区的小型pull request。

SpringCloud alibaba带来了什么

是什么

官网

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

诞生:2018.10.31,Spring Cloud Alibaba 正式入驻了Spring Cloud官方孵化器,并在Maven 中央库发布了第一个版本。

能干嘛

服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达

去哪下

如果需要使用已发布的版本,在dependencyManagement中添加如下配置

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.5.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

然后在 dependencies 中添加自己所需使用的依赖即可使用。

怎么玩

Spring Cloud Alibaba学习资料获取

官网

https://spring.io/projects/spring-cloud-alibaba#overview 英文

https://github.com/alibaba/spring-cloud-alibaba https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html 中文

https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

20 alibaba nacos

20.1 nacos的简介

为什么叫nacos

前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service。 是什么

一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos: Dynamic Naming and Configuration Service Nacos就是注册中心+配置中心的组合 -> Nacos = Eureka+Config+Bus 能干嘛

替代Eureka做服务注册中心 替代Config做服务配置中心 去哪下

https://github.com/alibaba/nacos/releases

各种注册中心比较

据说Nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验。

20.2 nacos安装

  • 本地Java8+Maven环境已经OK先
  • 官网下载Nacos
  • 解压安装包,直接运行bin目录下的startup.cmd
  • 命令运行成功后直接访问http://localhost:8848/nacos,默认账号密码都是nacos
  • 结果页面

img

20.3 Nacos之服务提供者注册

官方文档

1 新建Module - cloudalibaba-provider-payment9001

2 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-provider-payment9001</artifactId>
    <dependencies>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--日常通用jar包配置-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    </dependencies>


</project>

3 父pom

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.atguigu</groupId>
  <artifactId>springCloud5</artifactId>
  <packaging>pom</packaging>
  <version>1.0-SNAPSHOT</version>
  <modules>
    <module>cloud-provider-payment8001</module>
      <module>cloud-consumer-order80</module>
      <module>cloud-api-commons</module>
    <module>cloud-eureka-server7001</module>
      <module>cloud-provider-payment8004</module>
      <module>cloud-consumer-feign-hystrix-order80</module>
      <module>cloud-consumer-hystrix-dashboard9001</module>
      <module>cloud-gateway-gateway9527</module>
      <module>cloud-config-center-3344</module>
      <module>cloud-config-client-3355</module>
      <module>cloud-stream-rabbitmq-provider8801</module>
      <module>cloud-stream-rabbitmq-consumer8802</module>
      <module>cloudalibaba-provider-payment9001</module>
    <module>cloudalibaba-consumer-nacos-order83</module>
  </modules>

  <!-- 统一管理jar包版本 -->
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <junit.version>4.12</junit.version>
    <log4j.version>1.2.17</log4j.version>
    <lombok.version>1.16.18</lombok.version>
    <mysql.version>5.1.47</mysql.version>
    <druid.version>1.1.16</druid.version>
    <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
  </properties>

  <!-- 子模块继承之后,提供作用:
      锁定版本+子modlue不用写groupId和version -->
  <dependencyManagement>
    <dependencies>
      <!--spring boot 2.2.2-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.2.2.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--spring cloud Hoxton.SR1-->
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Hoxton.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--spring cloud alibaba 2.1.0.RELEASE-->
      <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2.1.0.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
      </dependency>
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid.version}</version>
      </dependency>
      <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>${mybatis.spring.boot.version}</version>
      </dependency>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
      </dependency>
      <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${log4j.version}</version>
      </dependency>
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
        <optional>true</optional>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <fork>true</fork>
          <addResources>true</addResources>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

4 yml

server:
  port: 9001

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址

management:
  endpoints:
    web:
      exposure:
        include: '*'

5 主启动类

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9001 {
    public static void main(String[] args) {
            SpringApplication.run(PaymentMain9001.class, args);
    }
}

6 业务类

package com.atguigu.springcloud.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    @GetMapping(value = "/payment/nacos/{id}")
    public String getPayment(@PathVariable("id") Integer id) {
        return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
    }
}

7 测试

http://localhost:9001/payment/nacos/1 nacos控制台 nacos服务注册中心+服务提供者9001都OK了 为了下一章节演示nacos的负载均衡,参照9001新建9002

新建cloudalibaba-provider-payment9002 9002其它步骤你懂的 或者取巧不想新建重复体力劳动,可以利用IDEA功能,直接拷贝虚拟端口映射

img

20.4 nacos之服务消费者注册和负载

1 新建Module-cloudalibaba-consumer-nacos-order83

2 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-consumer-nacos-order83</artifactId>
    <dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.lun.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>



</project>

为什么nacos支持负载均衡?因为spring-cloud-starter-alibaba-nacos-discovery内含netflix-ribbon包。

3 yml

server:
  port: 83

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider

4 主启动

package com.atguigu.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;


@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain83
{
    public static void main(String[] args)
    {
        SpringApplication.run(OrderNacosMain83.class,args);
    }
}

5 业务类

ApplicationContextConfig
package com.atguigu.springboot.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig
{
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate()
    {
        return new RestTemplate();
    }
}
OrderNacosController
package com.atguigu.springboot.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author 10185
 * @create 2021/3/21 16:11
 */
@RestController
public class OrderNacosController {
    @Autowired
    private RestTemplate restTemplate;
    @Value("${service-url.nacos-user-service}")
    private String serverUrl;

    @GetMapping("/consumer/payment/nacos/{id}")
    public String paymentInfo(@PathVariable("id") Long id) {
        return restTemplate.getForObject(serverUrl +"/payment/nacos/"+id,String.class);

    }


}

测试

  • 启动nacos控制台
  • http://localhost:83/Eonsumer/payment/nacos/13
    • 83访问9001/9002,轮询负载OK

20.5 Nacos服务注册中心对比提升

1 Nacos全景图

nacoså\u0085¨æ\u0099¯å\u009b¾

2 Nacos和CAP

Nacosä¸\u008eå\u0085¶ä»\u0096注å\u0086\u008cä¸\u00adå¿\u0083ç\u0089¹æ\u0080§å¯¹æ¯\u0094

3 Nacos服务发现实例模型

Nacosæ\u009c\u008då\u008a¡å\u008f\u0091ç\u008e°å®\u009eä¾\u008b模å\u009e\u008b

4 Nacos支持AP和CP模式的切换

C是所有节点在同一时间看到的数据是一致的;而A的定义是所有的请求都会收到响应。

何时选择使用何种模式?

—般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如Spring cloud和Dubbo服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例。

如果需要在服务级别编辑或者存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式。CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。

切换命令:

curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP

20.6 Nacos之服务配置中心

1 新建cloudalibaba-config-nacos-client3377

2 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-config-nacos-client3377</artifactId>
    <dependencies>
        <!--nacos-config-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--nacos-discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--一般基础配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>



</project>

3 bootstrap.yml

# nacos配置


spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
      config:
        server-addr: localhost:8848 #Nacos作为配置中心地址
        file-extension: yml #指定yml格式的配置



# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yml

# nacos-config-client-test.yml   ----> config.info

4 application.yml

server:
  port: 3377
spring:
  profiles:
    active: dev # 表示开发环境
    #active: test # 表示测试环境
    #active: info

5 主启动类

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;


@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClientMain3377
{
    public static void main(String[] args) {
        SpringApplication.run(NacosConfigClientMain3377.class, args);
    }
}

6 业务类

package com.atguigu.springcloud.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope //支持Nacos的动态刷新功能。
public class ConfigClientController
{
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo() {
        return configInfo;
    }
}

7 在Nacos中添加配置信息

Nacos中的dataid的组成格式及与SpringBoot配置文件中的匹配规则

官方文档

说明:之所以需要配置spring.application.name,是因为它是构成Nacos配置管理dataId 字段的一部分。

在 Nacos Spring Cloud中,dataId的完整格式如下:

${prefix}-${spring-profile.active}.${file-extension}

prefix默认为spring.application.name的值,也可以通过配置spring.cloud.nacos.config.prefix来配置。 spring.profile.active即为当前环境对应的 profile,详情可以参考 Spring Boot文档。注意:当spring.profile.active为空时,对应的连接符 - 也将不存在,datald 的拼接格式变成${prefix}.${file-extension} file-exetension为配置内容的数据格式,可以通过配置项spring .cloud.nacos.config.file-extension来配置。目前只支持properties和yaml类型。 通过Spring Cloud 原生注解@RefreshScope实现配置自动更新。

最后公式:

${spring.application.name)}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

img

image-20210321172027364

8 测试

启动前需要在nacos客户端-配置管理-配置管理栏目下有对应的yaml配置文件 运行cloud-config-nacos-client3377的主启动类 调用接口查看配置信息 - http://localhost:3377/config/info 自带动态刷新

修改下Nacos中的yaml配置文件,再次调用查看配置的接口,就会发现配置已经

注意: 如果配置文件中配置了yaml,那么nacos写配置文件后缀名也需要yaml

如果配置文件中配置了yml,那么nacos写配置文件后缀名也需要yml

20.7 Nacos之命名空间分组和DatalD三者关系

问题 - 多环境多项目管理

问题1:

实际开发中,通常一个系统会准备

dev开发环境 test测试环境 prod生产环境。 如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?

问题2:

一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…那怎么对这些微服务配置进行管理呢?

Nacos的图形化管理界面

img

img

Namespace+Group+Data lD三者关系?为什么这么设计?

1是什么

类似Java里面的package名和类名最外层的namespace是可以用于区分部署环境的,Group和DatalD逻辑上区分两个目标对象。

2三者情况

img

默认情况:Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT

Nacos默认的Namespace是public,Namespace主要用来实现隔离。 比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。 Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去 Service就是微服务:一个Service可以包含多个Cluster (集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。 比方说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这时就可以给杭州机房的Service微服务起一个集群名称(HZ) ,给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。 最后是Instance,就是微服务的实例。

20.8 Nacos之DatalD配置

指定spring.profile.active和配置文件的DatalD来使不同环境下读取不同的配置

默认空间+默认分组+新建dev和test两个DatalD

  • 新建dev配置DatalD

img

img

通过spring.profile.active属性就能进行多环境下配置文件的读取

img

测试

  • http://localhost:3377/config/info
  • 配置是什么就加载什么 test/dev

20.9 Nacos之Group分组方案

通过Group实现环境区分 - 新建Group

img

在nacos图形界面控制台上面新建配置文件DatalD

img

bootstrap+application

在config下增加一条group的配置即可。可配置为DEV_GROUP或TEST GROUP

img

20.10 Nacos之Namespace空间方案

新建dev/test的Namespace

img

回到服务管理-服务列表查看

img

按照域名配置填写

img

YML

# nacos配置
server:
  port: 3377

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
      config:
        server-addr: localhost:8848 #Nacos作为配置中心地址
        file-extension: yaml #指定yaml格式的配置
        group: DEV_GROUP
        namespace: 7d8f0f5a-6a53-4785-9686-dd460158e5d4 #<------------指定namespace


# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml

# nacos-config-client-test.yaml   ----> config.info

20.11 Nacos集群_架构说明

上图官网翻译,真实情况

img

按照上述,我们需要mysql数据库

image-20210321201302522

20.12 Nacos持久化切换配置

Nacos默认自带的是嵌入式数据库derby,nacos的pom.xml中可以看出。

derby到mysql切换配置步骤:

nacos-server-1.1.4\nacos\conf录下找到nacos-mysql.sql文件,执行脚本。 nacos-server-1.1.4\nacos\conf目录下找到application.properties,添加以下配置(按需修改对应值)。

spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=1234

启动Nacos,可以看到是个全新的空记录界面,以前是记录进derby。

20.13 Nacos之集群配置上

预计需要,1个Nginx+3个nacos注册中心+1个mysql

请确保是在环境中安装使用:

  1. 64 bit OS Linux/Unix/Mac,推荐使用Linux系统。
  2. 64 bit JDK 1.8+;下载.配置
  3. Maven 3.2.x+;下载.配置
  4. 3个或3个以上Nacos节点才能构成集群。

link

Nacos下载Linux版

1 Linux服务器上mysql数据库的配置

卸载linux自带的mariadb
rpm -qa|grep -i mariadb
rpm -e --nodeps mariadb-libs  (nodeps 把关联的文件全部去掉,后面带的是前面打出来的内容,可能不一致)
卸载linux sql
rpm -qa|grep -i mysql
rpm -e --nodeps 前面打出来的内容
安装MySql

l 拷贝安装包到opt目录下

MySQL-client-5.5.54-1.linux2.6.x86_64.rpm

MySQL-server-5.5.54-1.linux2.6.x86_64.rpm

l 执行如下命令进行安装

rpm -ivh MySQL-client-5.5.54-1.linux2.6.x86_64.rpm

rpm -ivh MySQL-server-5.5.54-1.linux2.6.x86_64.rpm
  1. 检查安装是否成功
    

l 安装完成后查看MySQL的版本

执行 mysqladmin –-version,如果打印出消息,即为成功

image-20201117105338377

开启服务

启动: service mysql start

停止: service mysql stop

设置root用户的密码
 mysqladmin -u root password ‘123123’
登录MySql
 mysql -uroot -p123123
建库
create database nacos_config
进入数据库
use nacos_config
复制nacos-sql.sql的内容到文件中去

成功:

image-20210322103245772

2 application.properties配置

cp application.properties.example application.properties

/opt/module/nacos/conf/application.properties 

3 添加以下内容,设置数据源

spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456

4 Linux服务器三nacos的集群配置cluster.conf

梳理出3台nacos集器的不同服务端口号,设置3个端口:

  • 3333
  • 4444
  • 5555

复制出cluster.conf

img

内容

演示,实际要放到不同的linux机器上面去

192.168.241.102:3333
192.168.241.102:4444
192.168.241.102:5555
123

注意,这个IP不能写127.0.0.1,必须是Linux命令hostname -i能够识别的IP

img

5 编辑Nacos的启动脚本startup.sh,使它能够接受不同的启动端口

/opt/module/nacos/bin目录下有startup.sh

img

平时单机版的启动,都是./startup.sh即可

但是,集群启动,我们希望可以类似其它软件的shell命令,传递不同的端口号启动不同的nacos实例。 命令: ./startup.sh -p 3333表示启动端口号为3333的nacos服务器实例,和上一步的cluster.conf配置的一致。

修改内容

img

img

执行方式 - startup.sh - p 端口号

img

注意:新版本已经有p节点了,因此需要把上面操作的p换成a

6 Nginx的配置,由它作为负载均衡器

修改nginx的配置文件 - nginx.conf

img

修改内容

img

按照指定启动

img

6.截止到此处,1个Nginx+3个nacos注册中心+1个mysql

测试

  • 启动3个nacos注册中心
    • startup.sh - p 3333
    • startup.sh - p 4444
    • startup.sh - p 5555
    • 查看nacos进程启动数ps -ef | grep nacos | grep -v grep | wc -l
  • 启动nginx
    • ./nginx -c /usr/local/nginx/conf/nginx.conf
    • 查看nginx进程ps - ef| grep nginx
  • 测试通过nginx,访问nacos - http://192.168.111.144:1111/nacos/#/login
  • 新建一个配置测试

img

  • 新建后,可在linux服务器的mysql新插入一条记录
select * from config;
1

img

  • 让微服务cloudalibaba-provider-payment9002启动注册进nacos集群 - 修改配置文件
server:
  port: 9002

spring:
  application:
    name: nacos-payment-provider
  c1oud:
    nacos:
      discovery:
        #配置Nacos地址
        #server-addr: Localhost:8848
        #换成nginx的1111端口,做集群
        server-addr: 192.168.111.144:1111

management:
  endpoints:
    web:
      exposure:
        inc1ude: '*'
  • 启动微服务cloudalibaba-provider-payment9002
  • 访问nacos,查看注册结果

img

高可用小总结

img

21 Sentinel

21.1 Sentinel是什么

官方Github

官方文档

image-20210324110055844

—句话解释,之前我们讲解过的Hystrix。

Hystrix与Sentinel比较:

Hystrix 需要我们程序员自己手工搭建监控平台 没有一套web界面可以给我们进行更加细粒度化得配置流控、速率控制、服务熔断、服务降级 Sentinel 单独一个组件,可以独立出来。 直接界面化的细粒度统一配置。 约定 > 配置 > 编码

都可以写在代码里面,但是我们本次还是大规模的学习使用配置和注解的方式,尽量少写代码

21.2 Sentinel下载安装运行

官方文档

服务使用中的各种问题:

服务雪崩 服务降级 服务熔断 服务限流 Sentinel 分为两个部分:

核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。 安装步骤:

下载

https://github.com/alibaba/Sentinel/releases 下载到本地sentinel-dashboard-1.7.0.jar 运行命令

前提 Java 8 环境 8080端口不能被占用 命令

java -jar sentinel-dashboard-1.7.0.jar

访问Sentinel管理界面

访问Sentinel管理界面

localhost:8080 登录账号密码均为sentinel

21.3 Sentinel初始化监控工程

1 启动nacos8848

2 新建工程-cloudalibaba-sentinel-service8401

3 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-sentinel-service8401</artifactId>
    <dependencies>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- SpringBoot整合Web组件+actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.6.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>


</project>

4 YML

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard地址
        port: 8719

management:
  endpoints:
    web:
      exposure:
        include: '*'

feign:
  sentinel:
    enabled: true # 激活Sentinel对Feign的支持

5 主启动

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @author 10185
 * @create 2021/3/24 10:02
 */
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
    public static void main(String[] args) {
        SpringApplication.run(MainApp8401.class, args);
    }
}

6 业务类FlowLimitController

package com.atguigu.springcloud.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@Slf4j
public class FlowLimitController {
    int a = 0;
    @GetMapping("/testA")
    public String testA() throws InterruptedException {
        a++;
        Thread.sleep(800);
        System.out.println("到这里了-"+a);
        return "------testA";
    }

    @GetMapping("/testB")
    public String testB()
    {
        log.info(Thread.currentThread().getName()+"\t"+"...testB");
        return "------testB";
    }
}

启动Sentinel8080 - java -jar sentinel-dashboard-1.7.0.jar

启动微服务8401

启动8401微服务后查看sentienl控制台

刚启动,空空如也,啥都没有

img

Sentinel采用的懒加载说明

  • 执行一次访问即可
    • http://localhost:8401/testA
    • http://localhost:8401/testB
  • 效果 - sentinel8080正在监控微服务8401

img

21.4 Sentinel流控规则简介

基本介绍

img

进一步解释说明:

  • 资源名:唯一名称,默认请求路径。
  • 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)。
  • 阈值类型/单机阈值:
    • QPS(每秒钟的请求数量)︰当调用该API的QPS达到阈值的时候,进行限流。
    • 线程数:当调用该API的线程数达到阈值的时候,进行限流。
  • 是否集群:不需要集群。
  • 流控模式:
    • 直接:API达到限流条件时,直接限流。
    • 关联:当关联的资源达到阈值时,就限流自己。
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】。
  • 流控效果:
    • 快速失败:直接失败,抛异常。
    • Warm up:根据Code Factor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
    • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效。

21.5 Sentinel流控-QPS直接失败

直接->快速失败(系统默认)

配置及说明

表示1秒钟内查询一次就是OK,若超过次数1,就直接->快速失败,报默认错误

img

测试

快速多次点击访问http://localhost:8401/testA

结果

返回页面 Blocked by Sentinel (flow limiting)

源码

com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController

思考

直接调用默认报错信息,技术方面OK,但是,是否应该有我们自己的后续处理?类似有个fallback的兜底方法?

21.6 Sentinel流控-线程数直接失败

线程数:当调用该API的线程数达到阈值的时候,进行限流。

img

21.7 Sentinel流控-关联

是什么?

  • 当自己关联的资源达到阈值时,就限流自己
  • 当与A关联的资源B达到阀值后,就限流A自己(B惹事,A挂了)

设置testA

当关联资源/testB的QPS阀值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名

img

postman模拟并发密集访问testB,将访问地址添加进新线程组

image-20210324123129380

image-20210324123216667

image-20210324123332832

Run - 大批量线程高并发访问B

Postman运行后,点击访问http://localhost:8401/testA,发现testA挂了

  • 结果Blocked by Sentinel(flow limiting)

HOMEWORK:

自己上机测试

链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】

21.8 Sentinel流控-预热

Warm Up

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档,具体的例子可以参见 WarmUpFlowDemo

通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

img

link

默认coldFactor为3,即请求QPS 从 threshold / 3开始,经预热时长逐渐升至设定的QPS阈值

WarmUp配置

案例,阀值为10+预热时长设置5秒。

系统初始化的阀值为10/ 3约等于3,即阀值刚开始为3;然后过了5秒后阀值才慢慢升高恢复到10

img

测试

多次快速点击http://localhost:8401/testB - 刚开始不行,后续慢慢OK

应用场景

如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值。

21.9 Sentinel流控-排队等待

匀速排队,让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效。

设置:/testA每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒。

匀速排队

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo。

该方式的作用如下图所示:

img

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

测试

添加日志记录代码到FlowLimitController的testA方法

@RestController
@Slf4j
public class FlowLimitController {
    @GetMapping("/testA")
    public String testA()
    {
        log.info(Thread.currentThread().getName()+"\t"+"...testA");//<----
        return "------testA";
    }

Postman模拟并发密集访问testA。具体操作参考117_Sentinel流控-关联

Postman模拟并发密集访问testA。具体操作参考117_Sentinel流控-关联

后台结果

img

21.10 Sentinel降级简介

官方文档

熔断降级概述

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

img

RT(平均响应时间,秒级)

平均响应时间 超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级。 窗口期过后关闭断路器。 RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)。 异常比列(秒级)

QPS >= 5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级 。 异常数(分钟级)

异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级 Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。

当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

Sentinei的断路器是没有类似Hystrix半开状态的。(Sentinei 1.8.0 已有半开状态)

半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用。

具体可以参考49_Hystrix的服务降级熔断限流概念初讲。

21.11 Sentinel降级-RT

平均响应时间(DEGRADE_GRADE_RT):当1s内持续进入5个请求,对应时刻的平均响应时间(秒级)均超过阈值( count,以ms为单位),那么在接下的时间窗口(DegradeRule中的timeWindow,以s为单位)之内,对这个方法的调用都会自动地熔断(抛出DegradeException )。注意Sentinel 默认统计的RT上限是4900 ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置。

注意:Sentinel 1.7.0才有平均响应时间DEGRADE_GRADE_RT),Sentinel 1.8.0的没有这项,取而代之的是慢调用比例(SLOW_REQUEST_RATIO)。

image-20210324194432253

表示在1秒钟如果由最小5个请求进入,且如果由比例阈值*请求数个请求超过最大RT,那么就进行熔断10秒

测试

img

结论

按照上述配置,永远一秒钟打进来10个线程(大于5个了)调用testD,我们希望200毫秒处理完本次任务,如果超过200毫秒还没处理完,在未来1秒钟的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了后续我停止jmeter,没有这么大的访问量了,断路器关闭(保险丝恢复),微服务恢复OK。

21.12 Sentinel降级-异常比例

是什么?

异常比例(DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值( DegradeRule中的 count)之后,资源进入降级状态,即在接下的时间窗口( DegradeRule中的timeWindow,以s为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是[0.0, 1.0],代表0% -100%。

注意,与Sentinel 1.8.0相比,有些不同(Sentinel 1.8.0才有的半开状态),Sentinel 1.8.0的如下:

异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

img

测试

代码

@GetMapping("/testC")
public String testC() {
    int a = 1/0;
    return "testC";
}

配置

image-20210324195734723

快速点击

image-20210324195752003

注意:每秒要按5下,保证每秒请求数大于5,由于异常大于比例20%,因此会出现

image-20210324195958513

不然就是

image-20210324200006978

21.13 Sentinel降级-异常数

是什么?

异常数( DEGRADE_GRADF_EXCEPTION_COUNT ):当资源近1分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若timeWindow小于60s,则结束熔断状态后码可能再进入熔断状态。

注意,与Sentinel 1.8.0相比,有些不同(Sentinel 1.8.0才有的半开状态),Sentinel 1.8.0的如下:

异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

接下来讲解Sentinel 1.7.0的。

异常数是按照分钟统计的,时间窗口一定要大于等于60秒

img

配置

image-20210324200146744

注意:表示1秒钟以内有5个请求进来,而且大于5个异常数,那么就进行熔断10秒钟

21.14 Sentinel热点key

1 基本介绍

img

官网

官方文档

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

img

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别

兜底方法,分为系统默认和客户自定义,两种

之前的case,限流出问题后,都是用sentinel系统默认的提示: Blocked by Sentinel (flow limiting)

我们能不能自定?类似hystrix,某个方法出问题了,就找对应的兜底降级方法?

结论 - 从HystrixCommand到@SentinelResource

2 代码

@RestController
@Slf4j
public class FlowLimitController
{

    ...

    @GetMapping("/testHotKey")
    @SentinelResource(value = "testHotKey",blockHandler/*兜底方法*/ = "deal_testHotKey")
    public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                             @RequestParam(value = "p2",required = false) String p2) {
        //int age = 10/0;
        return "------testHotKey";
    }
    
    /*兜底方法*/
    public String deal_testHotKey (String p1, String p2, BlockException exception) {
        return "------deal_testHotKey,o(╥﹏╥)o";  //sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
    }

}

3 配置

image-20210324205318361

@SentinelResource(value = “testHotKey”) 异常打到了前台用户界面看到,不友好 二

@SentinelResource(value = “testHotKey”, blockHandler = “dealHandler_testHotKey”) 方法testHotKey里面第一个参数只要QPS超过每秒1次,马上降级处理 异常用了我们自己定义的兜底方法 测试

error http://localhost:8401/testHotKey?p1=abc http://localhost:8401/testHotKey?p1=abc&p2=33 right http://localhost:8401/testHotKey?p2=abc

上述案例演示了第一个参数p1,当QPS超过1秒1次点击后马上被限流。

参数例外项

  • 普通 - 超过1秒钟一个后,达到阈值1后马上被限流
  • 我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样
  • 特例 - 假如当p1的值等于5时,它的阈值可以达到200

测试

right - http://localhost:8401/testHotKey?p1=5 error - http://localhost:8401/testHotKey?p1=3 当p1等于5的时候,阈值变为200 当p1不等于5的时候,阈值就是平常的1 前提条件 - 热点参数的注意点,参数必须是基本类型或者String

4 如果方法体抛异常

image-20210324205455183

21.15 Sentinel系统规则

官方文档

Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。link

系统规则

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。

系统规则支持以下的模式:

Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。 CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

21.16 SentinelResource配置

按资源名称限流 + 后续处理

启动Nacos成功

启动Sentinel成功

Module - cloudalibaba-sentinel-service8401添加Controller

**总结: **

1 如果配置了SentinelResource并且设置了默认规则,或者配置了全局规则,如果

image-20210324214818218

那么返回的是

{
  "code": 4444,
  "message": "按客戶自定义,global handlerException----2"
}

但是如果配置的是url地址

image-20210324214924586

那么返回的是

image-20210324214946317

@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "handleException")
public CommonResult byResource() {
    return new CommonResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001"));
}

public CommonResult handleException(BlockException exception) {
    return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用",null);
}

//使用全局自定义兜底方法,不用每个方法写一个处理方法
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
        blockHandlerClass = CustomerBlockHandler.class,//<-------- 自定义限流处理类
        blockHandler = "handlerException2")//<-----------
public CommonResult customerBlockHandler()
{
    return new CommonResult(200,"按客戶自定义",new Payment(2020L,"serial003"));
}
package com.atguigu.springcloud.controller;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.util.CommonResult;

public class CustomerBlockHandler {
    public static CommonResult handlerException(BlockException exception) {
        return new CommonResult(4444,"按客戶自定义,global handlerException----1",null);
    }
    
    public static CommonResult handlerException2(BlockException exception) {
        return new CommonResult(4444,"按客戶自定义,global handlerException----2",null);
    }
}

SentinelResource注解详解

Sentinel主要有三个核心Api:

  1. SphU定义资源
  2. Tracer定义统计
  3. ContextUtil定义了上下文

注意:blockHandlerClass需要额外多一个参数BlockException,不然会报错

21.17 Sentine 服务熔断Ribbon

Ribbon系列

  • 启动nacos和sentinel
  • 提供者9003/9004
  • 消费者84

1 启动nacos,sentinel

2 新建cloudalibaba-provider-payment9003/9004

3 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-provider-payment9003</artifactId>
    <dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>

4 yml

server:
  port: 9003

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址

management:
  endpoints:
    web:
      exposure:
        include: '*'

记得修改不同的端口号

5 主启动

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain9003.class, args);
    }
}

6 业务类

package com.atguigu.springboot.controller;

import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.util.CommonResult;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    //模拟数据库
    public static HashMap<Long,Payment> hashMap = new HashMap<>();
    static
    {
        hashMap.put(1L,new Payment(1L,"28a8c1e3bc2742d8848569891fb42181"));
        hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182"));
        hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183"));
    }

    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
    {
        Payment payment = hashMap.get(id);
        CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort:  "+serverPort,payment);
        return result;
    }

}

7 新建消费者84

1 新建cloudalibaba-consumer-nacos-order84
2 pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springCloud5</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-consumer-nacos-order84</artifactId>
    <dependencies>
        <!--SpringCloud openfeign -->
        <!--
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
      -->
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--日常通用jar包配置-->

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>



</project>
3 yml
server:
  port: 84

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: localhost:8080
        #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719

#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider

# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: false
4 主启动
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain84.class, args);
    }
}
5 业务类

ApplicationContextConfig

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;


@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

CircleBreakerController

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.alibaba.service.PaymentService;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class CircleBreakerController {
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;
 
    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback")//没有配置
    public CommonResult<Payment> fallback(@PathVariable Long id)
    {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);

        if (id == 4) {
            throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
        }else if (result.getData() == null) {
            throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
        }

        return result;
    }
    
}

修改后请重启微服务

热部署对java代码级生效及时 对@SentinelResource注解内属性,有时效果不好 目的

fallback管运行异常 blockHandler管配置违规 测试地址 - http://localhost:84/consumer/fallback/1

没有任何配置

只配置fallback

只配置blockHandler

fallback和blockHandler都配置

忽略属性

测试发现ribbon轮询调用

21.18 Sentinel服务熔断配置fallback和 blockHandler(注意一定要有BlockException blockException)

package com.atguigu.springboot.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;

import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.util.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class CircleBreakerController {
    public static final String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "handlerBlock1")//没有配置
    public CommonResult fallback(@PathVariable Long id)

    {
        CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);

        if (id == 4) {
            throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
        }else if (result.getData() == null) {
            throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
        }

        return result;
    }

    public CommonResult handlerFallback(@PathVariable  Long id,Throwable e) {
        Payment payment = new Payment(id,"null");
        return new CommonResult<>(444,"兜底异常handlerFallback,exception内容  "+e.getMessage(),payment);
    }
    //本例是blockHandler
    public CommonResult handlerBlock1(@PathVariable  Long id,BlockException e) {
        Payment payment = new Payment(id,"null");
        return new CommonResult(445,"blockHandler-sentinel限流,无此流水: blockException  ",null);
    }





}

注意:fallback 用来处理 java代码中的异常

blockHandler用来处理被熔断的代码

如果两个同时配,优先用blockHandler的异常处理方法

21.19 Sentinel服务熔断exceptionsTolgnore

exceptionsToIgnore,忽略指定异常,即这些异常不用兜底方法处理。

@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler",
            exceptionsToIgnore = {IllegalArgumentException.class})

21.20 Sentinel服务熔断OpenFeign

修改84模块

  • 84消费者调用提供者9003
  • Feign组件一般是消费侧

pom

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

yml

# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true

业务类

带@Feignclient注解的业务接口,fallback = PaymentFallbackService.class

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService
{
    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.stereotype.Component;

@Component
public class PaymentFallbackService implements PaymentService {
    @Override
    public CommonResult<Payment> paymentSQL(Long id)
    {
        return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService",new Payment(id,"errorSerial"));
    }
}

Controller

@RestController
@Slf4j
public class CircleBreakerController {

    ...
    
	//==================OpenFeign
    @Resource
    private PaymentService paymentService;

    @GetMapping(value = "/consumer/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id)
    {
        return paymentService.paymentSQL(id);
    }
}

主启动

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients//<------------------------
public class OrderNacosMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderNacosMain84.class, args);
    }
}

测试 - http://localhost:84/consumer/paymentSQL/1

测试84调用9003,此时故意关闭9003微服务提供者,84消费侧自动降级,不会被耗死。

21.21 熔断框架比较

21.22 Sentinel持久化规则

是什么

一旦我们重启应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。

怎么玩

将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效。

步骤

修改cloudalibaba-sentinel-service8401

POM

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

yml

server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #Nacos服务注册中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard地址
        port: 8719
      datasource: #<---------------------------关注点,添加Nacos数据源配置
        ds1:
          nacos:
            server-addr: localhost:8848
            dataId: cloudalibaba-sentinel-service
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow

management:
  endpoints:
    web:
      exposure:
        include: '*'

feign:
  sentinel:
    enabled: true # 激活Sentinel对Feign的支持

添加Nacos业务规则配置

img

配置内容解析

[{
    "resource": "/rateLimit/byUrl",
    "IimitApp": "default",
    "grade": 1,
    "count": 1, 
    "strategy": 0,
    "controlBehavior": 0,
    "clusterMode": false
}]

resource:资源名称; limitApp:来源应用; grade:阈值类型,0表示线程数, 1表示QPS; count:单机阈值; strategy:流控模式,0表示直接,1表示关联,2表示链路; controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待; clusterMode:是否集群。

启动8401后刷新sentinel发现业务规则有了

img

快速访问测试接口 - http://localhost:8401/rateLimit/byUrl - 页面返回Blocked by Sentinel (flow limiting)

停止8401再看sentinel - 停机后发现流控规则没有了

img

重新启动8401再看sentinel

乍一看还是没有,稍等一会儿 多次调用 - http://localhost:8401/rateLimit/byUrl 重新配置出现了,持久化验证通过

22 Seato

分布式前

单机单库没这个问题 从1:1 -> 1:N -> N:N 单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三三 个服务来完成。此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。

img

一句话:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。

1 Seato术语

Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

官方网址

能干嘛

一个典型的分布式事务过程

分布式事务处理过程的一ID+三组件模型:

Transaction ID XID 全局唯一的事务ID 三组件概念 TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。 TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。 RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。 处理过程:

TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID; XID在微服务调用链路的上下文中传播; RM向TC注册分支事务,将其纳入XID对应全局事务的管辖; TM向TC发起针对XID的全局提交或回滚决议; TC调度XID下管辖的全部分支事务完成提交或回滚请求。

img

2 Seato的安装

2.1 安装地址和版本

去哪下

发布说明: https://github.com/seata/seata/releases

怎么玩

本地@Transactional

全局@GlobalTransactional

SEATA 的分布式交易解决方案

img

我们只需要使用一个 @GlobalTransactional 注解在业务方法上:

当前我们安装的是0.9 到企业中去普遍使用的是1.0以上的版本,可能会有很多不同,1.0以上包含集群配置

下载binary版本

2.2 配置文件的修改

先备份一个seata文件

1 service模块的修改

service {
    ##fsp_tx_group是自定义的
    vgroup_mapping.my.test.tx_group="fsp_tx_group" 
    default.grouplist = "127.0.0.1:8091"
    enableDegrade = false
    disable = false
    max.commitretry.timeout= "-1"
    max.ollbackretry.timeout= "-1"
}

2 store模块的修改

## transaction log store
store {
	## store mode: file, db
	## 改成db
	mode = "db"
	
	## file store
	file {
		dir = "sessionStore"
		
		# branch session size, if exceeded first try compress lockkey, still exceeded throws exceptions
		max-branch-session-size = 16384
		# globe session size, if exceeded throws exceptions
		max-global-session-size = 512
		# file buffer size, if exceeded allocate new buffer
		file-write-buffer-cache-size = 16384
		# when recover batch read size
		session.reload.read_size= 100
		# async, sync
		flush-disk-mode = async
	}

	# database store
	db {
		## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
		datasource = "dbcp"
		## mysql/oracle/h2/oceanbase etc.
		## 配置数据源
		db-type = "mysql"
		driver-class-name = "com.mysql.jdbc.Driver"
		url = "jdbc:mysql://127.0.0.1:3306/seata"
		user = "root"
		password = "你自己密码"
		min-conn= 1
		max-conn = 3
		global.table = "global_table"
		branch.table = "branch_table"
		lock-table = "lock_table"
		query-limit = 100
	}
}

3 创建seata数据库

-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `global_table` (
  `xid` varchar(128)  not null,
  `transaction_id` bigint,
  `status` tinyint not null,
  `application_id` varchar(32),
  `transaction_service_group` varchar(32),
  `transaction_name` varchar(128),
  `timeout` int,
  `begin_time` bigint,
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`xid`),
  key `idx_gmt_modified_status` (`gmt_modified`, `status`),
  key `idx_transaction_id` (`transaction_id`)
);

-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
  `branch_id` bigint not null,
  `xid` varchar(128) not null,
  `transaction_id` bigint ,
  `resource_group_id` varchar(32),
  `resource_id` varchar(256) ,
  `lock_key` varchar(128) ,
  `branch_type` varchar(8) ,
  `status` tinyint,
  `client_id` varchar(64),
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`branch_id`),
  key `idx_xid` (`xid`)
);

-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
  `row_key` varchar(128) not null,
  `xid` varchar(96),
  `transaction_id` long ,
  `branch_id` long,
  `resource_id` varchar(256) ,
  `table_name` varchar(32) ,
  `pk` varchar(36) ,
  `gmt_create` datetime ,
  `gmt_modified` datetime,
  primary key(`row_key`)
);

4 修改registery.conf配置文件

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  # 改用为nacos
  type = "nacos"

  nacos {
  	## 加端口号
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
  }
  ...
}

目的是:指明注册中心为nacos,及修改nacos连接信息

先启动Nacos端口号8848 nacos\bin\startup.cmd

再启动seata-server - seata-server-0.9.0\seata\bin\seata-server.bat

3 Seato业务数据库的准备

以下演示都需要先启动Nacos后启动Seata,保证两个都OK。

分布式事务业务说明

这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。

当用户下单时,会在订单服务中创建一个订单, 然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。

该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

一言蔽之,下订单—>扣库存—>减账户(余额)。

创建业务数据库

seata_ order:存储订单的数据库; seata_ storage:存储库存的数据库; seata_ account:存储账户信息的数据库。 建库SQL

CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;

按照上述3库分别建对应业务表

  • seata_order库下建t_order表
CREATE TABLE `t_order` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
  `count` int(11) DEFAULT NULL COMMENT '数量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金额',
  `status` int(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
  • seata_storage库下建t_storage表
CREATE TABLE `t_storage` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
  `total` int(11) DEFAULT NULL COMMENT '总库存',
  `used` int(11) DEFAULT NULL COMMENT '已用库存',
  `residue` int(11) DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0','100');
SELECT * FROM t_storage;
  • seata_account库下建t_account表
CREATE TABLE `t_account` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
  `total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
  `used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
  `residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '1000', '0', '1000');

SELECT * FROM t_account;

按照上述3库分别建对应的回滚日志表

  • 订单-库存-账户3个库下都需要建各自的回滚日志表
  • \seata-server-0.9.0\seata\conf目录下的db_ undo_ log.sql
  • 建表SQL
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

4 环境搭建

https://blog.csdn.net/u011863024/article/details/114298288#142_SeataOrderModule_2648

项目地址

https://gitee.com/xiaodidi66666/springcloud5.git

查看

image-20210412125846700

中的源码

**注意:file.conf需要复制,然后修改 **

vgroup_mapping.fsp_tx_group = “default”

由于自己配数据源,因此需要使用@MapperScan({“com.atguigu.mapper”})来进行注入,没有回报错

4.1 seata-order-service2001

pom
<dependencies>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>seata-all</artifactId>
            <groupId>io.seata</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.37</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
</dependencies>
yml
server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server中的对应
        tx-service-group: fsp_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: 123456

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml
file.conf
transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #vgroup->rgroup
  #名字随便取
  vgroup_mapping.fsp_tx_group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
}

## transaction log store
store {
  ## store mode: file、db
  #改成数据库模式
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
   #配置数据源
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "123456"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"

  local {
    ## store locks in user's database
  }

  remote {
    ## store locks in the seata's server
  }
}
recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

support {
  ## spring
  spring {
    # auto proxy the DataSource bean
    datasource.autoproxy = false
  }
}
registry.conf
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}
domain
package com.atguigu.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author 10185
 * @create 2021/4/12 8:55
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {

    private  Integer code;
    private String message;
    private T data;
    public CommonResult(Integer code, String message)
    {
        this(code,message,null);
    }
}
package com.atguigu.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * @author 10185
 * @create 2021/4/12 8:56
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;

    private BigDecimal money;

    private Integer status;
}
Dao接口及实现
package com.atguigu.mapper;


import com.atguigu.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface OrderDao
{
    //1 新建订单
    void create(Order order);

    //2 修改订单状态,从零改为1
    void update(@Param("userId") Long userId,@Param("status") Integer status);

}
Service接口及实现
package com.atguigu.service;

import com.atguigu.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

/**
 * @author 10185
 * @create 2021/4/12 9:01
 */
@FeignClient(value = "seata-storage-service")
public interface StorageService {
    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);

}
package com.atguigu.service;


import com.atguigu.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(value = "seata-account-service")
public interface AccountService
{
    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
package com.atguigu.service;

import com.atguigu.domain.Order;
import org.springframework.stereotype.Service;

/**
 * @author 10185
 * @create 2021/4/12 9:00
 */

public interface OrderService {
    void create(Order order);

}
serviceimpl
package com.atguigu.serviceImpl;

import com.atguigu.domain.Order;
import com.atguigu.mapper.OrderDao;
import com.atguigu.service.AccountService;
import com.atguigu.service.OrderService;
import com.atguigu.service.StorageService;
import io.seata.rm.tcc.interceptor.ActionContextUtil;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author 10185
 * @create 2021/4/12 9:05
 */
@Service
@Slf4j

public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private StorageService storageService;
    @Autowired
    private AccountService accountService;

   //名字随便取只要不冲突

    @GlobalTransactional(name = "fspaaa",rollbackFor = Exception.class)
    @Override
    public void create(Order order) {
        //创建订单
        log.info("订单模块开始创建订单");
        orderDao.create(order);
        //库存服务少库存
        log.info("----->订单微服务开始调用库存,做扣减Count");
        storageService.decrease(order.getUserId(), order.getCount());
        log.info("----->订单微服务开始调用账户,做扣减,原来的金钱"+order.getMoney());
        //账户服务少钱

        accountService.decrease(order.getUserId(), order.getMoney());

        log.info("------>账户扣钱服务结束,修改后的金额为" + order.getMoney());
        //订单服务付钱状态改为1
        log.info("----->修改订单状态开始");
        orderDao.update(order.getUserId(), 0);
        log.info("----->修改订单结束");





    }




}
Controller
package com.atguigu.controller;

import com.atguigu.domain.CommonResult;
import com.atguigu.domain.Order;
import com.atguigu.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;

@RestController
public class OrderController
{
    @Resource
    private OrderService orderService;


    @GetMapping("/order/create")
    public CommonResult create(Order order)
    {

        orderService.create(order);
        return new CommonResult(200,"订单创建成功");
    }
}
  • config
  • MyBatisConfig

  • package com.atguigu.config;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @MapperScan({"com.atguigu.mapper"})
    public class MyBatisConfig {
    }
    
  • DataSourceProxyConfig

  • package com.atguigu.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import io.seata.rm.datasource.DataSourceProxy;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    
    import javax.sql.DataSource;
    
    @Configuration
    public class DataSourceProxyConfig {
    
        @Value("${mybatis.mapperLocations}")
        private String mapperLocations;
    
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DataSource druidDataSource(){
            return new DruidDataSource();
        }
    
        @Bean
        public DataSourceProxy dataSourceProxy(DataSource dataSource) {
            return new DataSourceProxy(dataSource);
        }
    
        @Bean
        public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSourceProxy);
            sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
            sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
            return sqlSessionFactoryBean.getObject();
        }
    
    }
    
主启动
package com.atguigu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
//取消数据源的自动创建,而是使用自己定义的
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderMainApp2001
{

    public static void main(String[] args)
    {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}

其他的步骤差不多,详细见博客或者gitee中的源码

Seata之@GlobalTransactional验证

5 Seata之@GlobalTransactional验证

下订单 -> 减库存 -> 扣余额 -> 改(订单)状态

数据库初始情况:

img

正常下单 - http://localhost:2001/order/create?userld=1&productld=1&count=10&money=100

数据库正常下单后状况:

img

超时异常,没加@GlobalTransactional

模拟AccountServiceImpl添加超时

@Service
public class AccountServiceImpl implements AccountService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);


    @Resource
    AccountDao accountDao;

    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, BigDecimal money) {
        LOGGER.info("------->account-service中扣减账户余额开始");
        //模拟超时异常,全局事务回滚
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        accountDao.decrease(userId,money);
        LOGGER.info("------->account-service中扣减账户余额结束");
    }
}

另外,OpenFeign的调用默认时间是1s以内,所以最后会抛异常。

数据库情况

img

故障情况

  • 当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1
  • 而且由于feign的重试机制,账户余额还有可能被多次扣减

超时异常,加了@GlobalTransactional

用@GlobalTransactional标注OrderServiceImpl的create()方法。

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    
    ...

    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:下订单->扣库存->减余额->改状态
     */
    @Override
    //rollbackFor = Exception.class表示对任意异常都进行回滚
    @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
    public void create(Order order)
    {
		...
    }
}

还是模拟AccountServiceImpl添加超时,下单后数据库数据并没有任何改变,记录都添加不进来,达到出异常,数据库回滚的效果

6 Seata之原理简介

2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。

Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架。

2020起始,用1.0以后的版本。Alina Gingertail

img

分布式事务的执行流程

TM开启分布式事务(TM向TC注册全局事务记录) ; 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态) ; TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务) ; TC汇总事务信息,决定分布式事务是提交还是回滚; TC通知所有RM提交/回滚资源,事务二阶段结束。

AT模式如何做到对业务的无侵入

是什么

前提

基于支持本地 ACID 事务的关系型数据库。 Java 应用,通过 JDBC 访问数据库。 整体机制

两阶段提交协议的演变:

一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。 二阶段: 提交异步化,非常快速地完成。 回滚通过一阶段的回滚日志进行反向补偿。 link

一阶段加载 在一阶段,Seata会拦截“业务SQL”

1 解析SQL语义,找到“业务SQL" 要更新的业务数据,在业务数据被更新前,将其保存成"before image”

2 执行“业务SQL" 更新业务数据,在业务数据更新之后,

3 其保存成"after image”,最后生成行锁。

以上操作全部在一个数据库事务内完成, 这样保证了一阶段操作的原子性。 img

  • 二阶段提交

二阶段如果顺利提交的话,因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

img

二阶段回滚 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务SQL",还原业务数据。

回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和"after image"。

如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。

img

img

通过XID来管理事务,每一个数据库都有一个id,不过XID都是同一个,每个日志文件中都记录着,进行全局事务的数据库当前数据的原来的数据,保存初始快照,然后执行结束的时候来一张结束快照,如果拿到全局锁就把全局快照保存到数据库中,如果没有全局锁或者中途发生异常,那么就依照初始快照中的内容进行全局回滚

image-20210412133046299\\