Mybatis
1、框架概述
1.1 什么是框架
框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法; 另一种定义认为,框架是可被应用开发者定制的应用骨架。前者是从应用方面而后者是从目的方面给出的定义。 简而言之,框架其实就是某种应用的半成品,就是一组组件,供你选用完成你自己的系统。简单说就是使用别人搭好的舞台,你来做表演。而且,框架一般是成熟的,不断升级的软件。
1.2 框架要解决的问题
框架要解决的最重要的一个问题是技术整合的问题,在 J2EE 的 框架中,有着各种各样的技术,不同的软件企业需要从 J2EE 中选择不同的技术,这就使得软件企业最终的应用依赖于这些技术,技术自身的复杂性和技术的风险性将会直接对应用造成冲击。而应用是软件企业的核心,是竞争力的关键所在,因此应该将应用自身的设计和具体的实现技术解耦。这样,软件企业的研发将集中在应用的设计上,而不是具体的技术实现,技术实现是应用的底层支撑,它不应该直接对应用产生影响。 框架一般处在低层应用平台(如 J2EE)和高层业务逻辑之间的中间层。
1.3 软件开发分层的重要性
框架的重要性在于它实现了部分功能,并且能够很好的将低层应用平台和高层业务逻辑进行了缓和。为了实现软件工程中的“高内聚、低耦合”。把问题划分开来各个解决,易于控制,易于延展,易于分配资源。我们常见的 MVC 软件设计思想就是很好的分层思想。
1.4 分层开发下的常见框架
MyBatis

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

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


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的配置文件

<?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配置文件

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

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入门案例设计模式的分析

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

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 源码分析

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);
}
结果

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;

发现只有 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>

正常查询
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>
测试结果

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中配置的内容和顺序

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连接池的分类


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 类中,可以找到如下代码

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

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);

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: 去掉后缀

比如说这个最好还有 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 中的语句

测试类的语句

自动合并了
17 Mybatis多表查询之多对多
实现 Role 到 User 多对多
通过前面的学习,我们使用 mybatis 实现一对多关系的维护,多对多关系其实我们看成是双向的一对多关系
17.1 用户与角色的关系模型

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

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());
}
}

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();
}
当查询不使用

当查询并使用

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);
}
}

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 基于注解方式实现配置二级缓存

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>

生成
一些用法
/**
* 收集姓名里面带有江,而且是男的
*/
@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中的转义字符

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的概述
-
Spring 是一个开源框架
-
Spring 为简化企业级开发而生,使用 Spring,JavaBean 就可以实现很多以前要靠 EJB 才能实现的功能。同样的功能,在 EJB 中要通过繁琐的配置和复杂的代码才能够实现,而在 Spring 中却非常的优雅和简洁。
-
Spring 是一个IOC(DI) 和AOP容器框架。
-
Spring 的优良特性
① 非侵入式:基于 Spring 开发的应用中的对象可以不依赖于 Spring 的 API
② 依赖注入:DI——Dependency Injection,反转控制 (IOC) 最经典的实现。
③ 面向切面编程:Aspect Oriented Programming——AOP
④ 容器:Spring 是一个容器,因为它包含并且管理应用对象的生命周期
⑤ 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML 和 Java 注解组合这些对象。
⑥ 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上 Spring 自身也提供了表述层的 SpringMVC 和持久层的 Spring JDBC)。
- Spring模块

1.2 Spring的运行环境
加入依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.1.RELEASE</version>
</dependency>
</dependencies>
创建 Spring xml 配置文件

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);
发现都是同一个对象,采用了工厂单例设计模式

也可以不需要 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的主要实现类
-
ClassPathXmlApplicationContext:对应类路径下的 XML 格式的配置文件
-
FileSystemXmlApplicationContext:对应文件系统中的 XML 格式的配置文件
-
在初始化时就创建单例的 bean,也可以通过配置的方式指定创建的 Bean 是多实例的。
2.1.5 ConfigurableApplicationContext
-
是 ApplicationContext 的子接口,包含一些扩展方法
-
refresh()和 close() 让 ApplicationContext 具有启动、关闭和刷新上下文的能力。
2.1.6 WebApplicationContext
- 专门为WEB应用而准备的,它允许从相对于WEB根目录的路径中完成初始化工作

2.2 通过类型获取bean
- 从IOC容器中获取bean时,除了通过id值获取,还可以通过bean的类型获取。但如果同一个类型的bean在XML文件中配置了多个,则获取时会抛出异常,所以同一个类型的bean在容器中必须是唯一的。
HelloWorld helloWorld = cxt.getBean(HelloWorld. class);
-
或者可以使用另外一个重载的方法,同时指定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 中使用的就是这种方式

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>

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接口。

<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 方法

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

默认情况下,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生命周期
-
Spring IOC 容器可以管理 bean 的生命周期,Spring 允许在 bean 生命周期内特定的时间点执行指定的任务。
-
Spring IOC 容器对 bean 的生命周期进行管理的过程:
① 通过构造器或工厂方法创建bean实例
② 为bean的属性设置值和对其他bean的引用
③ 调用bean的初始化方法
④ bean可以使用了
⑤ 当容器关闭时,调用bean的销毁方法
3) 在配置 bean 时,通过 init-method 和 destroy-method 属性为 bean 指定初始化和销毁方法
<bean id="student" class="com.atguigu.Student" p:name="江豪迪" init-method="init" destroy-method="destroy">
注意:当是多例模式的时候不能使用销毁方法
- 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>
- 添加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 自动装配的概念
-
手动装配:以 value 或 ref 的方式明确指定属性值都是手动装配。
-
自动装配:根据指定的装配规则,不需要明确指定,Spring自动将匹配的属性值注入bean 中。
2.9.2 装配模式
-
根据类型自动装配:将类型匹配的 bean 作为属性注入到另一个 bean 中。若 IOC 容器中有多个与目标 bean 类型一致的 bean,Spring 将无法判定哪个 bean 最合适该属性,所以不能执行自动装配 (会报错)
-
根据名称自动装配:必须将目标 bean 的名称和属性名设置的完全相同
-
通过构造器自动装配:当 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"/>
- 详细说明
①**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概述
- AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传
统 OOP(Object-Oriented Programming,面向对象编程) 的补充。
面向对象 纵向继承机制
面向切面 横向抽取机制
-
AOP 编程操作的主要对象是切面 (aspect),而切面用于模块化横切关注点(公共功能)。
-
在应用 AOP 编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
-
AOP 的好处:
① 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
② 业务模块更简洁,只包含核心业务代码
③ AOP 图解


3.2 AOP(术语)
1.连接点
类里面哪些方法可以被加强,这些方法称为连接点
2.切入点
实际被真正增强的方法,称为切入点
3.通知(增强)
实际再方法中加强的逻辑代码称为通知(比如说加日志,这就是通知)
通知有多种类型
前置通知
后置通知
环绕通知 前面和后面都有
异常通知 try catch
最终通知 finally
4.横切关注点
从每个方法中抽取出来的同一类非核心业务
5.切面
封装横切关注点信息的类,每个关注点体现为一个通知方法方法
6.目标
被通知的对象
7.代理
向目标对象应用通知之后创建的代理对象

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方法 |
切入点表达式应用到实际的切面类中

3.4.2 当前连接点细节
概述
切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在 JoinPoint 接口的实例对象中。
JoinPoint
getArgs() 获取实际参数数组
getSignature() 获取签名信息对象,可以进一步获取方法名
3.4.3 通知
概述
-
在具体的连接点上要执行的操作。
-
一个切面可以包括一个或者多个通知。
-
通知所使用的注解的值往往是切入点表达式。
前置通知
1) 前置通知:在方法执行之前执行的通知
2) 使用 @Before 注解
后置通知(无论什么样都会执行)
-
后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候
-
使用 @After 注解
返回通知
-
返回通知:无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
-
使用 @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 重用切入点的定义
-
在编写 AspectJ 切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。
-
在 AspectJ 切面中,可以通过 @Pointcut 注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
-
切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为 public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
-
其他通知可以通过方法名称引入该切入点

3.4.5 指定切面的优先级
-
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
-
切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定。
-
实现 Ordered 接口,getOrder() 方法的返回值越小,优先级越高。
-
若使用 @Order 注解,序号出现在注解中


第四章 以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 声明切入点
-
切入点使用aop:pointcut元素声明。
-
切入点必须定义在aop:aspect元素下,或者直接定义在aop:config元素下。
① 定义在<aop:aspect>元素下:只对当前切面有效
② 定义在<aop:config>元素下:对所有切面都有效
3) 基于 XML 的 AOP 配置不允许在切入点表达式中用名称引用其他切入点。
4.4 声明通知
-
在 aop 名称空间中,每种通知类型都对应一个特定的 XML 元素。
-
通知元素需要使用来引用切入点,或用直接嵌入切入点表达式。
-
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
-
JdbcTemplate 所需要的 JAR 包
spring-jdbc-4.0.0.RELEASE.jar
spring-orm-4.0.0.RELEASE.jar
spring-tx-4.0.0.RELEASE.jar
-
数据库驱动和数据源
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 种类传播行为。

事务的传播属性可以在 @Transactional 注解的 propagation 属性中定义.
propagtion 默认为用使用调用者的事务,不使用自己的事务
但是用 propagtion.REQUIRES_NEW 使用的是自己的事务处理方式,调用者的处理暂时挂起


也可以通过注解的方式进行配置
@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 标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
- 读未提交:READ UNCOMMITTED
允许 Transaction01 读取 Transaction02 未提交的修改。
-
读已提交:READ COMMITTED
要求 Transaction01 只能读取 Transaction02 已提交的修改。
-
可重复读:REPEATABLE READ
确保 Transaction01 可以多次从一个字段中读取到相同的值,即 Transaction01 执行期间禁止其它事务对这个字段进行更新。
-
串行化:SERIALIZABLE
确保 Transaction01 可以多次从一个表中读取到相同的行,在 Transaction01 执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
-
各个隔离级别解决并发问题的能力见下表
| 脏读 | 不可重复读 | 幻读 | |
|---|---|---|---|
| READ UNCOMMITTED | 有 | 有 | 有 |
| READ COMMITTED | 无 | 有 | 有 |
| REPEATABLE READ | 无 | 无 | 有 |
| SERIALIZABLE | 无 | 无 | 无 |
- 各种数据库产品对事务隔离级别的支持程度
| Oracle | MySQL | |
|---|---|---|
| READ UNCOMMITTED | × | √ |
| READ COMMITTED | √(默认) | √ |
| REPEATABLE READ | × | √(默认) |
3 在Spring中指定事务隔离级别
- 注解
用 @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);
}
}
- XML
在 Spring 2.x 事务通知中,可以在tx:method元素中指定隔离级别

5.4.4 触发事务回滚的异常
1 默认情况
捕获到 RuntimeException 或 Error 时回滚,而捕获到编译时异常不回滚。
2 配置
- 注解@Transactional 注解
① rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个
② noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个

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

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元素中进行指定

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 总结

第一章 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 框架结构图 (新版本):

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 包

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请求流程图解

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 流程分析

基本步骤:
① 客户端请求提交到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请求报头

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)

除了通过请求 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"

- hiddenHttpMethodFilter 的处理过程

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";
}

用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 国际化步骤;
- 得得到一个Locale对象;
- 使用ResourceBundle绑定国际化资源文件
- 使用ResourceBundle.getString("key");获取到国际化配置文件中的值
- web页面的国际化,fmt标签库来做
<fmt:setLocale><fmt:setBundle><fmt:message>
有了 JstlView 以后
-
让 Spring 管理国际化资源就行

<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> -
直接在页面使用
<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);
}
}
}
总体概览
-
用户发出请求,DispatcherServlet接收请求并拦截请求。
-
调用doDispatch()方法进行处理:
- getHandler():根据当前请求地址中找到能处理这个请求的目标处理器类(处理器);
- 根据当前请求在HandlerMapping中找到这个请求的映射信息,获取到目标处理器类
- mappedHandler = getHandler(processedRequest);
- getHandlerAdapter():根据当前处理器类找到能执行这个处理器方法的适配器;
- 根据当前处理器类,找到当前类的HandlerAdapter(适配器)
- HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
- 使用刚才获取到的适配器(AnnotationMethodHandlerAdapter)执行目标方法;
- mv = ha.handle(processedRequest,response,mappedHandler.getHandler());
- 目标方法执行后,会返回一个ModerAndView对象
- mv = ha.handle(processedRequest,response,mappedHandler.getHandler());
- 根据ModerAndView的信息转发到具体页面,并可以在请求域中取出ModerAndView中的模型数据
-
processDispatchResult(processedRequest, response, mappedHandler,
mv, dispatchException);
-
HandlerMapping 为处理器映射器,保存了每一个处理器能处理哪些请求的映射信息,handlerMap
HandlerAdapter 为处理器适配器,能解析注解方法的适配器,其按照特定的规则去执行 Handler
- getHandler():根据当前请求地址中找到能处理这个请求的目标处理器类(处理器);
具体细节
步骤一:找到类来处理
getHandler():
**怎么根据当前请求就能找到哪个类能来处理?**
-
getHandler() 会返回目标处理器类的执行链

-
HandlerMapping:处理器映射:他里面保存了每一个处理器能处理哪些请求的映射信息

-
handlerMap:ioc 容器启动创建 Controller 对象的时候扫描每个处理器都能处理什么请求,保存在 HandlerMapping 的 handlerMap 属性中;下一次请求过来,就来看哪个 HandlerMapping 中有这个请求映射信息就行了

循环遍历拿到能处理 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():
如何找到目标处理器类的适配器。要拿适配器才去执行目标方法

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
)

标了注解:
保存时哪个注解的详细信息;
如果参数有ModelAttribute注解;
拿到ModelAttribute注解的值让attrName保存
attrName="haha"
没标注解:
1)、先看是否普通参数(是否原生API)
再看是否Model或者Map,如果是就传入隐含模型;
2)、自定义类型的参数没有ModelAttribute 注解
1)、先看是否原生API
2)、再看是否Model或者Map
3)、再看是否是其他类型的比如SessionStatus、HttpEntity、Errors
4)、再看是否简单类型的属性;比如是否Integer,String,基本类型
如果是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;
}
总结:
- 运行流程简单版;
- 确定方法每个参数的值;
- 标注解:保存注解的信息;最终得到这个注解应该对应解析的值;
- 没标注解:
- 看是否是原生API;
- 看是否是Model或者是Map,SessionStatus、HttpEntity、Errors...
- 看是否是简单类型;paramName=""
- 给attrName赋值;attrName(参数标了@ModelAttribute("")就是指定的,没标就是"")
- attrName使用参数的类型首字母小写;或者使用之前@ModelAttribute("")的值
- 先看隐含模型中有每个这个attrName作为key对应的值;如果有就从隐含模型中获取并赋值
- 看是否是@SessionAttributes(value="haha");标注的属性,如果是从session中拿;
- 不是@SessionAttributes标注的,利用反射创建一个对象;
- 不是@SessionAttributes标注的,利用反射创建一个对象;
步骤四:包装成一个ModelAndView对象
- 任何方法的返回值,最终都会被包装成ModelAndView对象
步骤五:SpringMVC视图解析
SpringMVC 视图解析:
1、方法执行后的返回值会作为页面地址参考,转发或者重定向到页面
2、视图解析器可能会进行页面地址的拼串
processDispatchResult(processedRequest, response, mappedHandler,
mv, dispatchException);
-
调用 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
- 来到页面的方法视图渲染流程
- 将域中的数据在页面展示
- 页面就是用来渲染模型数据的
-
调用 render(mv, request, response)
- 渲染页面
-
View 与 ViewResolver
- ViewResolver的作用是根据视图名(方法的返回值)得到View对象
- 
-
怎么能根据方法的返回值(视图名)得到 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 对象

```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);
}
```


- 返回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

- View:

第八章 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:
数据绑定器有什么用?
- 数据绑定器负责数据绑定工作
- 数据绑定期间产生的类型转换、格式化、数据校验等问题

-
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 负责数据校验工作

- bindingResult负责保存以及解析数据绑定期间数据校验产生的错误


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 来指定想要的格式。如下:

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、先编写国际化配置文件

2、让 SpringMVC 管理国际化资源文件
<!-- 管理国际化资源文件 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="errors"></property>
</bean>
3、来到页面取值
4、高级国际化?
动态传入消息参数;

{0}:永远都是当前属性名;
@Length(min = 5, max = 10,message='xxxx')
按照字母排序
{1} 为 max {2} 为 min
第10章 动态资源,和静态资源被springMVC识别
<mvc:default-servlet-handler/> 与 <mvc:annotation-driven/>
-
都没配
-
动态能访问:
DefaultAnnotationHandlerMapping 中的 handlerMap 中保存了每一个资源的映射信息
-
静态不能访问:
handlerMap 中没有保存静态资源映射的请求

-
handleAdapter

-
-
<mvc:default-servlet-handler/>不加<mvc:annotation-driven/>-
动态不能访问:DefaultAnnotationHandlerMapping 被 SimpleUrlHandlerMapping 替换。
-
静态能访问的原因:SimpleUrlHandlerMapping 把所有请求都映射给 tomcat;

-
handleAdapter

-
-
都加上
-
都能访问
handlerMap

-
RequestMappingHandlerMapping: 动态资源可以访问

handleMethods 属性保存了每一个请求用哪个方法来处理;
SimpleUrlHandlerMapping:将请求直接交给 tomcat;有他,静态资源就没问题
-
handleAdapter

原来的 AnnotationMethodHandlerAdapter 被换成 RequestMappingHandlerAdapter
-
-
只加
<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 返回的结果

会自动转换成 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("我成功了");



11.4 HttpEntiy
- 代替RequestBody,
- 不仅能拿请求体数据,还能拿请求头数据

ResponseEntity
- 可以设置响应头

第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 的配置,为什么呢?




因此,一定要进行 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负责

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;不放行
- 返回boolean
-
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 拦截器步骤解析

12.5 多个拦截器步骤分析

MyFirstInterceptor...preHandle...
MySecondInterceptor...preHandle...
目标方法....
MySecondInterceptor...postHandle...
MyFirstInterceptor...postHandle...
响应页面....
MySecondInterceptor...afterCompletion...
MyFirstInterceptor...afterCompletion
异常流程:
-
哪一块 Interceptor 不放行
- 哪一块不放行从此以后都没有
-
MySecondInterceptor 不放行
-
但是他前面已经放行了的拦截器的 afterCompletion 总会执行
-
总结 interceptor 的流程:
拦截器的 preHandle:是按照顺序执行
拦截器的 postHandle:是按照逆序执行
拦截器的 afterCompletion:是按照逆序执行
已经放行了的拦截器的 afterCompletion 总会执行
13.6 拦截器的源码分析








第14章 国际化
14.1 写好配置文件

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>
注意:如果是谷歌浏览器要想实现英文的国际化一定要是

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 取出来

第15章 异常处理
15.1 异常源码
processDispatchResult(processedRequest, response, mappedHandler,
mv, dispatchException);
加了 MVC 异常处理,默认就是这个几个 HandlerExceptionResolver

- 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";
}
结果:

15.4 DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver:
判断是否是 SpringMVC 自带的异常或 Spring 自己的异常:
如:HttpRequestMethodNotSupportedException。如果没人处理则它自己处理

默认的异常有
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:
通过配置的方式进行异常处理

<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;

第17章 SpringMVC与Spring整合
17.1 分容目的
-
SpringMVC 和 Spring 整合的目的:分工明确
-
SpringMVC 的配置文件就来配置和网站转发逻辑以及网站功能有关的
(视图解析器,文件上传解析器,支持 ajax,xxx)
-
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 是一个子容器
- 子容器还可以引用父容器的组件
- 父容器不能引用子容器的组件

ssm整合

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配置

<?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>
<!–指定映射配置文件的位置,映射配置文件指的是每一个dao独立配置文件–>
<!–如果使用注解来配置的话,此处应该使用class属性指定被注解的dao全限定类名–>
<!–这是为了映射到UserDao.xml中去,以便进行后续的操作–>
<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配置文档
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>

然后通过 target 文件打开外部文件夹


4 了解自动配置原理
4.1 依赖管理

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 依赖

-
自动配好 SpringMVC
-
- 引入SpringMVC全套组件
- 自动配好SpringMVC常用组件(功能)
-
自动配好 Web 常见功能,如:字符编码问题
-
- SpringBoot帮我们配置好了所有web开发的常见场景
-
默认的包结构
-
- 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
- 无需以前的包扫描配置
- 想要改变扫描路径,@SpringBootApplication(scanBasePackages="com.atguigu")
-
-
- 或者@ComponentScan 指定扫描路径
-
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
// 默认是在启动类的包以及下面的子包所有的配置类

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

@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)

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

文件里面写死了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


自己去修改配置文件中的值进行替换
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

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/**
<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 功能失效

10 默认静态资源路径,以及环境页面的模板

11 处理rest请求源码

自定义 filter
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
//通过改变methodParam属性来改变request.getParamether(this.methodParam)的值
methodFilter.setMethodParam("_m");
return methodFilter;
}
12 处理器选择源码

`
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);
}
};
}

当只有一个的情况
///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

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) {


14.4 将map,Model放入请求中

14.5 方法参数中自定义对象的解析流程

14.6 不同返回值请求处理参数

14.7 responsebody请求策略


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>

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

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

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默认异常处理机制

这些参数都可以当成一个属性在 thymeleaf 中引用
20.1 默认异常处理机制

如果是浏览器发出的请求, 处理的是 /error 错误页面
如果是类型客户端像 postman 发出的请求返回的默认是 json 的错误信息

20.2 自定义异常处理
xx 代表什么都可以

20.3 自定义异常处理源码

20.4 springmvc异常处理机制

20.5 自定义handlerResolver
只要标注了注解就会在
添加
@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 每次都会调用方法都会创建出一个新的对象

如果精确的是 /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原理

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>

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>

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
注意: 默认的 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
逆向生成文件

配置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的使用
通过这个生成

@Mapper
public interface RoleMapper extends BaseMapper<Role> {
}

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
第一种方式
通过更改数据库中的

更改默认为当地的时间戳
(一般不用, 不允许更改数据库)
第二种方式
@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字段

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/
| 类型 | 参数名 | 描述 |
|---|---|---|
| Serializable | id | 主键ID |
| Wrapper | queryWrapper | 实体对象封装操作类(可以为 null) |
| Collection<? extends Serializable> | idList | 主键ID列表(不能为 null 以及 empty) |
| Map<String, Object> | columnMap | 表字段 map 对象 |
| IPage | page | 分页查询条件(可以为 RowBounds.DEFAULT) |
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);
| 类型 | 参数名 | 描述 |
|---|---|---|
| Wrapper | wrapper | 实体对象封装操作类(可以为 null) |
| Collection<? extends Serializable> | idList | 主键ID列表(不能为 null 以及 empty) |
| Serializable | id | 主键ID |
| Map<String, Object> | columnMap | 表字段 map 对象 |
29.7 配置逻辑删除
在表中添加一个字段

同时在类中也加入相同
private String deleted;
mybatis3.3以前

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 代码自动生成器
代码自动生成器
dao、pojo、conrtroller、service自动生成
AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、 Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
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>

30.2 购买阿里云





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 的测试引擎。

注意:
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>

2、1.x与2.x的不同

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.name或logging.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等
- 可以很容易的添加自定义的健康检查机制

//开启health细节展示,默认只有一个health状态显示
endpoint:
health:
show-details: always

3、Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被 pull(主动推送)或者 push(被动获取)方式得到;
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或者扩展已有Metrics

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:默认只暴露health和info 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@

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();
}

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....");
}
}

33 可视化监控
官方文档
https://github.com/codecentric/spring-boot-admin

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 功能

如果要用就在 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;
}

35 外部化配置
1 外部数据源
常用:java 属性文件,YAML 文件, 环境变量, 命令行参数
2 配置文件查找位置
(1) classpath 根路径
(2) classpath 根路径下 config 目录
(3) jar 包当前目录
(4) jar 包当前目录的 config 目录
(5) /config 子目录的直接子目录
3 配置文件加载顺序:
- 当前jar包内部的application.properties和application.yml
- 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
- 引用的外部jar包的application.properties和application.yml
- 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
4 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
36 自定义starter

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 不要有空格

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.atguigu.xiaodidi.xiaodidispringbootautoconfiguration.XiaodidiAutoConfiguration
5 进行打包

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

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 交互
服务围绕业务功能拆分
可以由全自动部署机制独立部署
去中心化,服务自治。服务可以使用不同的语言、不同的存储技术

具有哪些技术
- 服务调用
- 服务降级
- 服务注册与发先
- 服务熔断
- 负载均衡
- 服务消息队列
- 服务网关
- 配置中心管理
- 自动化构建部署
- 服务监控
- 全链路追踪
- 服务定时任务
- 调度操作
Spring Cloud 简介
是什么?符合微服务技术维度
SpringCloud= 分布式微服务架构的站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶
猜猜 SpringCloud 这个大集合里有多少种技术?

1.2 互联网大厂微服务架构案例
京东的

阿里的

京东物流的


1.3 SpringCloud技术栈



1.4 总结

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 升级

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 测试,若测试失败则打包失败。但是有时候我们打包的时候一些项目配置是针对生产环境的,在本地可能会测试失败,在正式环境是可以正常运行的,这时候我们就需要把打包前的测试禁止调

4 支付模块的创建
创建微服务模块的套路:
- 建Module
- 改POM
- 写YML
- 主启动
- 业务类

创建 cloud-provider-payment8001 微服务提供者支付 Module 模块:
1.建名为cloud-provider-payment8001的Maven工程

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 业务类
建立

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 消费者订单模块

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 服务的客户端模板工具集
使用:
使用 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>

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集群原理说明

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

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();
}
}



交替出现
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 #可以显示当前路径

同时显示地址 (访问信息有 IP 信息提示,(就是将鼠标指针移至 payment8001,payment8002 名下,会有 IP 地址提示))

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);
}
}


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 节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。

自我保护机制∶默认情况下 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 官网
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。

怎么用
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

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'

CP 架构
当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性。
结论:违背了可用性 A 的要求,只满足一致性和分区容错,即 CP。

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 架构说明

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 官方文档
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: 根据特定算法中从服务列表中选取一个要访问的服务

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规则类

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

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

14 Hystrix是什么
14.1 概述
分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,假设微服务 A 调用微服务 B 和微服务 C,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
对于高流量的应用来说,单一的后避依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
Hystrix是什么
Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
" 断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝 ),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
14.2 Hystrix停更进维
能干嘛
- 服务降级
- 服务熔断
- 接近实对的监控
- …
官网资料
Hystrix 官宣,停更进维
- 被动修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的使用



ctrl + s 保存



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"+"😂😂😂";
}
}
上面故意制造两种异常:
- int age = 10/0,计算异常
- 我们能接受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 的相关论文

14.14 Hystrix之服务熔断案例(上)
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 测试

根据自己的配置用 -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之服务总结
大神结论

熔断类型
熔断打开:请求不再进行调用当前服务,内部设置时钟一般为 MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态。
熔断关闭:熔断关闭不会对服务进行熔断。
熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断。
官网断路器流程图

官网步骤
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():同步执行,从依赖的服务返回一个单一的结果对象或是在发生错误的时候抛出异常。
- queue():异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。
而 HystrixObservableCommand实现了后两种执行方式:
obseve():返回Observable对象,它代表了操作的多个统
果,它是一个Hot Observable (不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于Hot Observable的每一个“订阅者”都有可能是从“事件源”的中途开始的,并可能只是看到了整个操作的局部过程)。 - 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
- 测试通过
- 先访问正确地址,再访问错误地址,再正确地址,会发现图示断路器都是慢慢放开的。

如何看?
- 7色

- 1圈
实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色 < 黄色 < 橙色 < 红色递减。
该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例。
- 1线
曲线:用来记录 2 分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。
- 整图说明

- 整图说明2

15 GateWay
15.1 概述
Cloud 全家桶中有个很重要的组件就是网关,在 1.x 版本中都是采用的 Zuul 网关;
但在 2.x 版本中,zuul 的升级一直跳票,SpringCloud 最后自己研发了一个网关替代 Zuul,那就是 SpringCloud Gateway—句话:gateway 是原 zuul1.x 版的替代

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 链的方式提供了网关基本的功能,例如: 安全,监控 / 指标,和限流。
作用
- 方向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
- …
微服务架构中网关的位置

15.2 GateWay非阻塞异步模型
有 Zuull 了怎么又出来 Gateway?我们为什么选择 Gateway?
- netflix不太靠谱,zuul2.0一直跳票,迟迟不发布。
- 一方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。
- Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的Zuul 2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?
- 多方面综合考虑Gateway是很理想的网关选择。
- SpringCloud Gateway具有如下特性
- 基于Spring Framework 5,Project Reactor和Spring Boot 2.0进行构建;
- 动态路由:能够匹配任何请求属性;
- 可以对路由指定Predicate (断言)和Filter(过滤器);
- 集成Hystrix的断路器功能;
- 集成Spring Cloud 服务发现功能;
- 易于编写的Predicate (断言)和Filter (过滤器);
- 请求限流功能;
- 支持路径重写。
- SpringCloud Gateway与Zuul的区别
- 在SpringCloud Finchley正式版之前,Spring Cloud推荐的网关是Netflix提供的Zuul。
- Zuul 1.x,是一个基于阻塞I/O的API Gateway。
- Zuul 1.x基于Servlet 2.5使用阻塞架构它不支持任何长连接(如WebSocket)Zuul的设计模式和Nginx较像,每次I/О操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第-次加载较慢的情况,使得Zuul的性能相对较差。
- Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul .x的性能较Zuul 1.x有较大提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求数)是Zuul的1.6倍。
- Spring Cloud Gateway建立在Spring Framework 5、Project Reactor和Spring Boot2之上,使用非阻塞API。
- 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。

上述模式的缺点:
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工作流程
三大核心概念
- Route(路由) - 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由;
- Predicate(断言) - 参考的是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由;
- Filter(过滤) - 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

web 请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate 就是我们的匹配条件;而 fliter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标 uri,就可以实现一个具体的路由了
Gateway 工作流程
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
HandlerMappinginfrastructure. 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 logicalandstatements. 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
- The After Route Predicate Factory
- The Before Route Predicate Factory
- The Between Route Predicate Factory
- The Cookie Route Predicate Factory
- The Header Route Predicate Factory
- The Host Route Predicate Factory
- The Method Route Predicate Factory
- The Path Route Predicate Factory
- The Query Route Predicate Factory
- The RemoteAddr Route Predicate Factory
- 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:
两个主要接口介绍:
- GlobalFilter
- Ordered
能干什么:
- 全局日志记录
- 统一网关鉴权
- …
代码案例:
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 - 正常反问

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


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 中配置的端口中去寻找配置文件, 然后放入服务器

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 使用可以实现配置的动态刷新。

Spring Cloud Bus 是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了 Java 的事件处理机制和消息中间件的功能。Spring Clud Bus 目前支持 RabbitMQ 和 Kafka。
能干嘛
Spring Cloud Bus 能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。

为何被称为总线
什么是总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
基本原理
ConfigClient 实例都监听 MQ 中同一个 topic(默认是 Spring Cloud Bus)。当一个服务刷新数据的时候,它会把这个信息放入到 Topic 中,这样其它监听同一 Topic 的服务就能得到通知,然后去更新自身的配置。
16.6 制作3366
1 安装rabbitmq

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, 而刷新所有客户端的配置
2. 利用消息总线触发一个服务端 ConfigServer 的 /bus/refresh 端点,而刷新所有客户端的配置
图二的架构显然更加适合,图—不适合的原因如下:
打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责。
破坏了微服务各节点的对等性。
有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修
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介绍
什么是 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

生产者 / 消费者之间靠消息媒介传递信息内容
消息必须走特定的通道 - 消息通道 Message Channel
消息通道里的消息如何被消费呢,谁负责收发处理 - 消息通道 MessageChannel 的子接口 SubscribableChannel,由 MessageHandler 消息处理器所订阅。
为什么用 Cloud Stream?
比方说我们用到了 RabbitMQ 和 Kafka,由于这两个消息中间件的架构上的不同,像 RabbitMQ 有 exchange,kafka 有 Topic 和 Partitions 分区。

这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候 Spring Cloud Stream 给我们提供了—种解耦合的方式。
Stream 凭什么可以统一底层差异?
在没有绑定器这个概念的情况下,我们的 SpringBoot 应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的 Channel 通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
通过定义绑定器 Binder 作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Binder:
INPUT 对应于消费者
OUTPUT 对应于生产者

Stream 中的消息通信方式遵循了发布 - 订阅模式
Topic 主题进行广播
- 在RabbitMQ就是Exchange
- 在Kakfa中就是Topic
17.4 Stream编码常用注解简介
Spring Cloud Stream 标准流程套路


Binder - 很方便的连接中间件,屏蔽差异。
Channel - 通道,是队列 Queue 的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过 Channel 对队列进行配置。
Source 和 Sink - 简单的可理解为参照对象是 Spring Cloud Stream 自身,从 Stream 发布消息就
编码 API 和常用注解

案例说明
准备 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 中的消息分组来解决。

注意在 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简介
为什么会出现这个技术?要解决哪些问题?
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时
或错误都会引起整个请求最后的失败。

是什么
https://github.com/spring-cloud/spring-cloud-sleuth
Spring Cloud Sleuth 提供了一套完整的服务跟踪的解决方案
在分布式系统中提供追踪解决方案并且兼容支持了 zipkin
解决

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
运行控制台
术语
完整的调用链路
表示一请求链路,一条链路通过 Trace ld 唯一标识,Span 标识发起的请求信息,各 span 通过 parent id 关联起来

—条链路通过 Trace ld 唯一标识,Span 标识发起的请求信息,各 span 通过 parent id 关联起来。

整个链路的依赖关系如下:

名词解释
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

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 中添加自己所需使用的依赖即可使用。
怎么玩
-
- Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
- Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
- RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
- Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
- Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
- Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
- Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
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
- [官网文档](https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring cloud alibaba nacos_discovery)
各种注册中心比较
| 服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
|---|---|---|---|
| Eureka | AP | 支持 | 低(2.x版本闭源) |
| Zookeeper | CP | 不支持 | 中 |
| consul | CP | 支持 | 高 |
| Nacos | AP | 支持 | 高 |
据说 Nacos 在阿里巴巴内部有超过 10 万的实例运行,已经过了类似双十一等各种大型流量的考验。
20.2 nacos安装
- 本地Java8+Maven环境已经OK先
- 从官网下载Nacos
- 解压安装包,直接运行bin目录下的startup.cmd
- 命令运行成功后直接访问http://localhost:8848/nacos,默认账号密码都是nacos
- 结果页面

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 功能,直接拷贝虚拟端口映射

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全景图

2 Nacos和CAP

3 Nacos服务发现实例模型

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}


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 的图形化管理界面


Namespace+Group+Data lD 三者关系?为什么这么设计?
1 是什么
类似 Java 里面的 package 名和类名最外层的 namespace 是可以用于区分部署环境的,Group 和 DatalD 逻辑上区分两个目标对象。
2 三者情况

默认情况: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


通过 spring.profile.active 属性就能进行多环境下配置文件的读取

测试
- http://localhost:3377/config/info
- 配置是什么就加载什么 test/dev
20.9 Nacos之Group分组方案
通过 Group 实现环境区分 - 新建 Group

在 nacos 图形界面控制台上面新建配置文件 DatalD

bootstrap+application
在 config 下增加一条 group 的配置即可。可配置为 DEV_GROUP 或 TEST GROUP

20.10 Nacos之Namespace空间方案
新建 dev/test 的 Namespace

回到服务管理 - 服务列表查看

按照域名配置填写

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集群_架构说明
官网架构图
集群部署架构图
因此开源的时候推荐用户把所有服务列表放到一个 vip 下面,然后挂到一个域名下面
http://ip1:port/openAPI 直连 ip 模式,机器挂则需要修改 ip 才可以使用。
http://VIP:port/openAPI 挂载 VIP 模式,直连 vip 即可,下面挂 server 真实 ip,可读性不好。
http://nacos.com:port/openAPI 域名+VIP 模式,可读性好,而且换 ip 方便,推荐模式
上图官网翻译, 真实情况

按照上述,我们需要 mysql 数据库。

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
请确保是在环境中安装使用:
- 64 bit OS Linux/Unix/Mac,推荐使用Linux系统。
- 64 bit JDK 1.8+;下载.配置。
- Maven 3.2.x+;下载.配置。
- 3个或3个以上Nacos节点才能构成集群。
Nacos 下载 Linux 版
- https://github.com/alibaba/nacos/releases/tag/1.1.4
- nacos-server-1.1.4.tar.gz 解压后安装
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
-
检查安装是否成功
l 安装完成后查看 MySQL 的版本
执行 mysqladmin –-version,如果打印出消息,即为成功

开启服务
启动: 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的内容到文件中去
成功:

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

内容
演示, 实际要放到不同的 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

5 编辑Nacos的启动脚本startup.sh,使它能够接受不同的启动端口
/opt/module/nacos/bin 目录下有 startup.sh

平时单机版的启动,都是./startup.sh 即可
但是,集群启动,我们希望可以类似其它软件的 shell 命令,传递不同的端口号启动不同的 nacos 实例。
命令: ./startup.sh -p 3333 表示启动端口号为 3333 的 nacos 服务器实例,和上一步的 cluster.conf 配置的一致。
修改内容


执行方式 - startup.sh - p 端口号

注意: 新版本已经有 p 节点了, 因此需要把上面操作的 p 换成 a
6 Nginx的配置,由它作为负载均衡器
修改 nginx 的配置文件 - nginx.conf

修改内容

按照指定启动

6. 截止到此处,1 个 Nginx+3 个 nacos 注册中心 +1 个 mysql
测试
- 启动3个nacos注册中心
startup.sh - p 3333startup.sh - p 4444startup.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
- 新建一个配置测试

- 新建后,可在linux服务器的mysql新插入一条记录
select * from config;
1

- 让微服务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,查看注册结果

高可用小总结

21 Sentinel
21.1 Sentinel是什么

—句话解释,之前我们讲解过的 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 控制台
刚启动, 空空如也, 啥都没有

Sentinel 采用的懒加载说明
- 执行一次访问即可
- 效果 - sentinel8080正在监控微服务8401

21.4 Sentinel流控规则简介
基本介绍

进一步解释说明:
- 资源名:唯一名称,默认请求路径。
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)。
- 阈值类型/单机阈值:
- QPS(每秒钟的请求数量)︰当调用该API的QPS达到阈值的时候,进行限流。
- 线程数:当调用该API的线程数达到阈值的时候,进行限流。
- 是否集群:不需要集群。
- 流控模式:
- 直接:API达到限流条件时,直接限流。
- 关联:当关联的资源达到阈值时,就限流自己。
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】。
- 流控效果:
- 快速失败:直接失败,抛异常。
- Warm up:根据Code Factor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效。
21.5 Sentinel流控-QPS直接失败
直接 -> 快速失败 (系统默认)
配置及说明
表示 1 秒钟内查询一次就是 OK, 若超过次数 1, 就直接 -> 快速失败, 报默认错误

测试
快速多次点击访问http://localhost:8401/testA
结果
返回页面 Blocked by Sentinel (flow limiting)
源码
com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
思考
直接调用默认报错信息,技术方面 OK,但是,是否应该有我们自己的后续处理?类似有个 fallback 的兜底方法?
21.6 Sentinel流控-线程数直接失败
线程数:当调用该 API 的线程数达到阈值的时候,进行限流。

21.7 Sentinel流控-关联
是什么?
- 当自己关联的资源达到阈值时,就限流自己
- 当与A关联的资源B达到阀值后,就限流A自己(B惹事,A挂了)
设置 testA
当关联资源 /testB 的 QPS 阀值超过 1 时,就限流 /testA 的 Rest 访问地址,当关联资源到阈值后限制配置好的资源名。

postman 模拟并发密集访问 testB, 将访问地址添加进新线程组



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 曲线如下图所示:
默认 coldFactor 为 3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值
WarmUp 配置
案例,阀值为 10+ 预热时长设置 5 秒。
系统初始化的阀值为 10/ 3 约等于 3, 即阀值刚开始为 3; 然后过了 5 秒后阀值才慢慢升高恢复到 10

测试
多次快速点击http://localhost:8401/testB - 刚开始不行,后续慢慢 OK
应用场景
如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来, 慢慢的把阀值增长到设置的阀值。
21.9 Sentinel流控-排队等待
匀速排队,让请求以均匀的速度通过,阀值类型必须设成 QPS,否则无效。
设置:/testA 每秒 1 次请求,超过的话就排队等待,等待的超时时间为 20000 毫秒。
匀速排队
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo。
该方式的作用如下图所示:

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 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 流控 - 关联
后台结果

21.10 Sentinel降级简介
熔断降级概述
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

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)。

表示在 1 秒钟如果由最小 5 个请求进入, 且如果由比例阈值 * 请求数个请求超过最大 RT, 那么就进行熔断 10 秒
测试

结论
按照上述配置,永远一秒钟打进来 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%。

测试
代码
@GetMapping("/testC")
public String testC() {
int a = 1/0;
return "testC";
}
配置

快速点击

注意: 每秒要按 5 下, 保证每秒请求数大于 5, 由于异常大于比例 20%, 因此会出现

不然就是

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 秒。

配置

注意: 表示 1 秒钟以内有 5 个请求进来, 而且大于 5 个异常数, 那么就进行熔断 10 秒钟
21.14 Sentinel热点key
1 基本介绍

官网
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
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 配置

一
@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 如果方法体抛异常

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 并且设置了默认规则, 或者配置了全局规则, 如果

那么返回的是
{
"code": 4444,
"message": "按客戶自定义,global handlerException----2"
}
但是如果配置的是 url 地址

那么返回的是

@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注解详解
@SentinelResource 注解
注意:注解方式埋点不支持 private 方法。
@SentinelResource用于定义资源,并提供可选的异常处理和 fallback 配置项。@SentinelResource注解包含以下属性:
value:资源名称,必需项(不能为空)entryType:entry 类型,可选项(默认为EntryType.OUT)blockHandler/blockHandlerClass:blockHandler对应处理BlockException的函数名称,可选项。blockHandler 函数访问范围需要是public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定blockHandlerClass为对应的类的Class对象,注意对应的函数必需为 static 函数,否则无法解析。fallback/fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个
Throwable类型的参数用于接收对应的异常。- fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass为对应的类的Class对象,注意对应的函数必需为 static 函数,否则无法解析。defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个
Throwable类型的参数用于接收对应的异常。- defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定
fallbackClass为对应的类的Class对象,注意对应的函数必需为 static 函数,否则无法解析。exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
Sentinel 主要有三个核心 Api:
- SphU定义资源
- Tracer定义统计
- 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 熔断框架比较
| - | Sentinel | Hystrix | resilience4j |
|---|---|---|---|
| 隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔商/信号量隔离 | 信号量隔离 |
| 熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
| 实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
| 动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
| 扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
| 基于注解的支持 | 支持 | 支持 | 支持 |
| 限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
| 流量整形 | 支持预热模式匀速器模式、预热排队模式 | 不支持 | 简单的Rate Limiter模式 |
| 系统自适应保护 | 支持 | 不支持 | 不支持 |
| 控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控,机器发观等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
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 业务规则配置

配置内容解析
[{
"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 发现业务规则有了

快速访问测试接口 - http://localhost:8401/rateLimit/byUrl - 页面返回 Blocked by Sentinel (flow limiting)
停止 8401 再看 sentinel - 停机后发现流控规则没有了

重新启动 8401 再看 sentinel
乍一看还是没有,稍等一会儿
多次调用 - http://localhost:8401/rateLimit/byUrl
重新配置出现了,持久化验证通过
22 Seato
分布式前
单机单库没这个问题
从 1:1 -> 1:N -> N:N
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用, 分别使用三个独立的数据源,业务操作需要调用三三 个服务来完成。此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。

一句话:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
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 下管辖的全部分支事务完成提交或回滚请求。

2 Seato的安装
2.1 安装地址和版本
去哪下
发布说明: https://github.com/seata/seata/releases
怎么玩
本地 @Transactional
全局 @GlobalTransactional
SEATA 的分布式交易解决方案

我们只需要使用一个 @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
查看

中的源码
** 注意: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验证
下订单 -> 减库存 -> 扣余额 -> 改(订单)状态
数据库初始情况:

正常下单 - http://localhost:2001/order/create?userld=1&productld=1&count=10&money=100
数据库正常下单后状况:

超时异常,没加 @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 以内,所以最后会抛异常。
数据库情况

故障情况
- 当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为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

分布式事务的执行流程
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”,最后生成行锁。
以上操作全部在一个数据库事务内完成, 这样保证了一阶段操作的原子性。

- 二阶段提交
二阶段如果顺利提交的话,因为 "业务 SQL" 在一阶段已经提交至数据库,所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

二阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务 SQL",还原业务数据。
回滚方式便是用 "before image" 还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 "after image"。
如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。


通过 XID 来管理事务, 每一个数据库都有一个 id, 不过 XID 都是同一个, 每个日志文件中都记录着, 进行全局事务的数据库当前数据的原来的数据, 保存初始快照, 然后执行结束的时候来一张结束快照, 如果拿到全局锁就把全局快照保存到数据库中, 如果没有全局锁或者中途发生异常, 那么就依照初始快照中的内容进行全局回滚
\\




默认评论
Halo系统提供的评论