-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 235 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 235 KB
1
{"pages":[{"title":"about","text":"自信源于努力","link":"/about/index.html"},{"title":"categories","text":"","link":"/categories/index.html"}],"posts":[{"title":"动态代理","text":" 除了装饰者模式,动态代理也可用于进行方法增强,并且不用编写相关类,在不修改源码的情况下起到增强方法的作用。 1.基于接口的动态代理 此方法使用 jdk 自带的 api Proxy类的newProxyInstance方法实现,此方法的代理对象要求至少是实现一个接口,具体例子如下 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253public class Producer implements IProducer{ @Override public void sale(float money) { System.out.println(\"销售产品,并拿到钱:\"+money); } @Override public void afterSale(float money) { System.out.println(\"提供售后服务,并拿到钱:\"+money); }}public class Client { public static void main(String[] args) { Producer producer = new Producer(); /** * 获取代理对象: * 要求: * 被代理类最少实现一个接口 * 创建的方式: * Proxy.newProxyInstance(三个参数) * 参数含义: * ClassLoader:和被代理对象使用相同的类加载器。 * Interfaces:和被代理对象具有相同的行为。实现相同的接口 * InvocationHandler:如何代理,一般由匿名内部类实现 * */ IProducer proxyInstance = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() { /** * 执行被代理对象的任何方法,都会经过该方法 * 此方法有拦截的功能 * @param proxy 代理对象的引用,不一定每次都能用到 * @param method 当前执行的方法对象 * @param args 执行方法所需的参数 * @return 当前执行方法的返回值 * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { float money = (float) args[0]; if (method.getName().equals(\"sale\")) { money *= 0.8f; } return method.invoke(producer, money); } }); proxyInstance.sale(10000f); }} 2.基于子类的动态代理 如果被代理对象没有实现接口,就要使用基于子类的动态代理,要求被代理对象不能是最终类,使用Enhancer类的create方法,利用了cglib,使用maven的项目需导入坐标 12345<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.1_3</version></dependency> 具体例子如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950public class Producer{ public void sale(float money) { System.out.println(\"销售产品,并拿到钱:\"+money); } public void afterSale(float money) { System.out.println(\"提供售后服务,并拿到钱:\"+money); }}public class Client { public static void main(String[] args) { final Producer producer = new Producer(); /** * 基于子类的动态代理 * 要求: * 被代理对象不能是最终类 * 用到的类: * Enhancer * 用到的方法: * create(Class,Callback) * 方法的参数: * Class:被代理对象的字节码 * Callback:如何代理,一般用其子接口MethodInterceptor匿名类实现 */ Producer proxyProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() { /** * * @param proxy 代理对象的引用。不一定每次都用得到 * @param method 当前执行的方法对象 * @param args 执行方法所需的参数 * @param methodProxy 当前执行方法的代理对象 * @return * @throws Throwable */ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { float money = (float) args[0]; if (method.getName().equals(\"sale\")) { money *= 0.8f; } return method.invoke(producer, money); } }); proxyProducer.sale(10000f); }} 使用过动态代理之后可以对Producer的sale方法进行增强,将收到钱数额变成原来的0.8倍。","link":"/2020/05/18/%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86/"},{"title":"AJAX","text":"1. AJAX概述 AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和XML) 现在一般与json共同使用更多。 AJAX 是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下。 比如搜索框的下拉列表、注册时表单未提交时就会有格式判断。 2 . AJAX的原理XMLHttpRequest 是js的对象,是AJAX的基础,用于在后台和服务器交换数据。 XMLHttpRequest的创建123456789var xmlhttp;if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari xmlhttp=new XMLHttpRequest(); }else {// code for IE6, IE5 xmlhttp=new ActiveXObject(\"Microsoft.XMLHTTP\"); } 打开与服务器的连接open(method, url, async): method:请求方式,通常为GET或POST; url:请求的服务器地址,例如:/ajaxdemo1/AServlet,若为GET请求,还可以在URL后追加参数,在springMVC中即为@RequestMapping后的路径 async:这个参数可以不给,默认值为true,表示异步请求 1xmlHttp.open(\"GET\", \"/ajaxdemo1/AServlet?username=zhangsan&password=123\", true); 发送请求1234567//get请求无参数xmlHttp.send();//post请求附带参数//如果需要像 HTML 表单那样 POST 数据,请使用 setRequestHeader() 来添加 HTTP 头。然后在 send() 方法中规定您希望发送的数据xmlhttp.setRequestHeader(\"Content-type\",\"application/x-www-form-urlencoded\");xmlhttp.send(\"fname=Bill&lname=Gates\"); 接收服务器响应当请求发送出去后,服务器端Servlet就开始执行了,但服务器端的响应还没有接收到。接下来我们来接收服务器的响应。 onreadystatechange事件XMLHttpRequest对象有一个onreadystatechange事件,它会XMLHttpRequest对象的状态发生变化时被调用。下面介绍一下XMLHttpRequest对象的5种状态: 0:创建了XMLHttpRequest对象,还未调用open()方法; 1:open()方法已调用,但还没调用send()方法; 2:send()方法已调用; 3:开始读取服务器响应; 4:读取服务器响应结束。 onreadystatechange事件会在状态为1、2、3、4时引发。但我们通常只关心最后一种状态(4),即读取服务器响应结束后,客户端才会做出改变。可以通过XMLHttpRequest对象的readyState属性来得到该对象的状态。 12345xmlHttp.onreadystatechange = function() { if(xmlHttp.readyState == 4 && xmlHttp.status == 200) { alert(xmlHttp.responseText); } }; 服务器响应属性 responseText:获得字符串形式的响应数据。 1document.getElementById(\"Div1\").innerHTML=xmlhttp.responseText; responseXML:获得XML形式的响应数据。 12345678910111213141516xmlDoc=xmlhttp.responseXML;txt=\"\";x=xmlDoc.getElementsByTagName(\"title\");for (i=0;i<x.length;i++){ /** 对应输出为 Harry Potter Everyday Italian Learning XML XQuery Kick Start 分别为各个<title>标签中的值 */ txt=txt + x[i].childNodes[0].nodeValue + \"<br />\";}document.getElementById(\"myDiv\").innerHTML=txt; JQuery和AJAX由于AJAX本身编程相对复杂,JQuery框架对其进行了简化,主要有以下几种方法: 关键字 简介 $.ajax() 提交AJAX请求 $.get() 使用get方式提交ajax $.post 使用post方式提交ajax load() 最简单的调用ajax的方式 $.ajax() 方法语法: 1jQuery.ajax([settings]) settings:可选。用于配置 Ajax 请求的键值对集合。可以通过 $.ajaxSetup() 设置任何选项的默认值,常用参数如下: 参数名 类型 说明 url String 请求地址,默认值为当前页 data String 发送到服务器的数据。将自动转换为请求字符串格式。GET 请求中将附加在 URL 后。必须为 Key/Value 格式。 type String 请求方式 (“POST” 或 “GET”), 默认为 “GET”。 success Function 请求成功后的回调函数。 dataType String 预期服务器返回的数据类型。如果不指定,jQuery 将自动根据 HTTP 包 MIME 信息来智能判断,,传递给回调函数。可用值:”xml”、”html”、”script”、”json”、”text” 123456789101112$.ajax({ url:\"/testAjax\", data:{ \"username\":\"mmm\", \"password\":12345, \"address\":\"Hefei\" }, success:function (data) { alert(\"AJax()方法返回的数据\"+data) }, type:\"POST\" }) 之后的$.get()与$.post()方法就是对本方法的简化,不用在标出参数名,但调用函数时要按照一定顺序。 $.get() 方法语法: 1$.get(URL,callback); URL:希望请求的URL,可附带请求参数 callback:回调函数(可选) 12345678$.get( \"/testAjax\", {\"username\":value}, //将id=checkResult标签的内容修改为返回数据 function(result){ $(\"#checkResult\").html(result); } ); $.post() 方法语法: 1$.post(URL,data,callback); URL:希望请求的URL data:连同请求发送的数据(可选) callback:回调函数(可选) 1234567891011$.post( \"/testAjax\", { username:\"mhs\", password:123, age:26 }, function(data) { alert(\"jquery AJAX的Post()返回结果\"+data); } ) load() 方法从服务器加载数据,并把返回的数据放入备选元素中 语法: 1$(selector).load(URL,data,callback); URL:希望请求的URL data:连同请求发送的数据(可选) callback:回调函数(可选)","link":"/2020/05/18/ajax-0/"},{"title":"springboot核心","text":"本文源文件来自尚硅谷雷丰阳老师,将其使用的springboot 1.x替换成了springboot 2.x并在一些部分作出了一些补充,包括配置、日志、web、docker、数据访问。 一、Spring Boot 入门1、Spring Boot 简介 简化Spring应用开发的一个框架; 整个Spring技术栈的一个大整合; J2EE开发的一站式解决方案; 2、微服务2014,martin fowler 微服务:架构风格(服务微化) 一个应用应该是一组小型服务;可以通过HTTP的方式进行互通; 单体应用:ALL IN ONE 微服务:每一个功能元素最终都是一个可独立替换和独立升级的软件单元; 详细参照微服务文档 3、环境准备环境约束 –jdk1.8:Spring Boot 推荐jdk1.7及以上;java version “1.8.0_112” –maven3.x:maven 3.3以上版本;Apache Maven 3.3.9 –SpringBoot 2.2.6.RELEASE:2.2.6 1、MAVEN设置;给maven 的settings.xml配置文件的profiles标签添加 123456789101112<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> 2、IDEA设置整合maven进来; 4、Spring Boot HelloWorld一个功能: 浏览器发送hello请求,服务器接受请求并处理,响应Hello World字符串; 1、创建一个maven工程;(jar)2、导入spring boot相关的依赖1234567891011<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies> 3、编写一个主程序;启动Spring Boot应用123456789101112/** * @SpringBootApplication 来标注一个主程序类,说明这是一个Spring Boot应用 */@SpringBootApplicationpublic class HelloWorldMainApplication { public static void main(String[] args) { // Spring应用启动起来 SpringApplication.run(HelloWorldMainApplication.class,args); }} 4、编写相关的Controller、Service123456789@Controllerpublic class HelloController { @ResponseBody @RequestMapping(\"/hello\") public String hello(){ return \"Hello World!\"; }} 5、运行主程序测试6、简化部署123456789<!-- 这个插件,可以将应用打包成一个可执行的jar包;--> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> 将这个应用打成jar包,直接使用java -jar的命令进行执行; 5、Hello World探究1、POM文件1、父项目1234567891011121314<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version></parent>他的父项目是<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>1.5.9.RELEASE</version> <relativePath>../../spring-boot-dependencies</relativePath></parent>他来真正管理Spring Boot应用里面的所有依赖版本; Spring Boot的版本仲裁中心; 以后我们导入依赖默认是不需要写版本;(没有在dependencies里面管理的依赖自然需要声明版本号) 2、启动器1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> spring-boot-starter-==web==: spring-boot-starter:spring-boot场景启动器;帮我们导入了web模块正常运行所依赖的组件; Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器 2、主程序类,主入口类123456789101112/** * @SpringBootApplication 来标注一个主程序类,说明这是一个Spring Boot应用 */@SpringBootApplicationpublic class HelloWorldMainApplication { public static void main(String[] args) { // Spring应用启动起来 SpringApplication.run(HelloWorldMainApplication.class,args); }} @SpringBootApplication: Spring Boot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用; 12345678910@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication { @SpringBootConfiguration:Spring Boot的配置类; 标注在某个类上,表示这是一个Spring Boot的配置类; @Configuration:配置类上来标注这个注解; 配置类 —– 配置文件;配置类也是容器中的一个组件;@Component @EnableAutoConfiguration:开启自动配置功能; 以前我们需要配置的东西,Spring Boot帮我们自动配置;@EnableAutoConfiguration告诉SpringBoot开启自动配置功能;这样自动配置才能生效; 123@AutoConfigurationPackage@Import(EnableAutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration { @AutoConfigurationPackage:自动配置包 @Import(AutoConfigurationPackages.Registrar.class): Spring的底层注解@Import,给容器中导入一个组件;导入的组件由AutoConfigurationPackages.Registrar.class; ==将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器;== @Import(EnableAutoConfigurationImportSelector.class); 给容器中导入组件? EnableAutoConfigurationImportSelector:导入哪些组件的选择器; 将所有需要导入的组件以全类名的方式返回;这些组件就会被添加到容器中; 会给容器中导入非常多的自动配置类(xxxAutoConfiguration);就是给容器中导入这个场景需要的所有组件,并配置好这些组件; 有了自动配置类,免去了我们手动编写配置注入功能组件等的工作; SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,classLoader); ==Spring Boot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作;==以前我们需要自己配置的东西,自动配置类都帮我们; J2EE的整体整合解决方案和自动配置都在spring-boot-autoconfigure-1.5.9.RELEASE.jar; 6、使用Spring Initializer快速创建Spring Boot项目1、IDEA:使用 Spring Initializer快速创建项目IDE都支持使用Spring的项目创建向导快速创建一个Spring Boot项目; 选择我们需要的模块;向导会联网创建Spring Boot项目; 默认生成的Spring Boot项目; 主程序已经生成好了,我们只需要我们自己的逻辑 resources文件夹中目录结构 static:保存所有的静态资源; js css images; templates:保存所有的模板页面;(Spring Boot默认jar包使用嵌入式的Tomcat,默认不支持JSP页面);可以使用模板引擎(freemarker、thymeleaf); application.properties:Spring Boot应用的配置文件;可以修改一些默认设置; 2、STS使用 Spring Starter Project快速创建项目 二、配置文件1、配置文件SpringBoot使用一个全局的配置文件,配置文件名是固定的; •application.properties •application.yml 配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好; YAML(YAML Ain’t Markup Language) YAML A Markup Language:是一个标记语言 YAML isn’t Markup Language:不是一个标记语言; 标记语言: 以前的配置文件;大多都使用的是 xxxx.xml文件; YAML:以数据为中心,比json、xml等更适合做配置文件; YAML:配置例子 12server: port: 8081 XML: 123<server> <port>8081</port></server> 2、YAML语法:1、基本语法k:(空格)v:表示一对键值对(空格必须有); 以空格的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的 123server: port: 8081 path: /hello 属性和值也是大小写敏感; 2、值的写法字面量:普通的值(数字,字符串,布尔) k: v:字面直接来写; 字符串默认不用加上单引号或者双引号; “”:双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思 name: “zhangsan \\n lisi”:输出;zhangsan 换行 lisi ‘’:单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据 name: ‘zhangsan \\n lisi’:输出;zhangsan \\n lisi 对象、Map(属性和值)(键值对): k: v:在下一行来写对象的属性和值的关系;注意缩进 对象还是k: v的方式 123friends: lastName: zhangsan age: 20 行内写法: 1friends: {lastName: zhangsan,age: 18} 数组(List、Set):用- 值表示数组中的一个元素 1234pets: - cat - dog - pig 行内写法 1pets: [cat,dog,pig] 3、配置文件值注入在进行配置文件注入时,会同时加载application.properties和yml,不过properties的优先级更高,properties中没有配置的内容会使用yml的配置。 yml配置文件 123456789101112person: lastName: hello age: 18 boss: false birth: 2017/12/12 maps: {k1: v1,k2: 12} lists: - lisi - zhaoliu dog: name: 小狗 age: 12 properties配置文件 123456789101112person.last_name=张三person.age=18person.boss=trueperson.birth=2010/01/01person.maps.k1=mmmperson.maps.v1=aaaperson.maps.k2=dogperson.maps.v2.dog.name=xiaobeiperson.maps.v2.dog.age=3person.lists={abc,def,ghi}person.dog.name=wangcaiperson.dog.age=4 javaBean: 1234567891011121314151617181920/** * 将配置文件中配置的每一个属性的值,映射到这个组件中 * @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定; * prefix = \"person\":配置文件中哪个下面的所有属性进行一一映射 * * 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能; * */@Component@ConfigurationProperties(prefix = \"person\")public class Person { private String lastName; private Integer age; private Boolean boss; private Date birth; private Map<String,Object> maps; private List<Object> lists; private Dog dog; 我们可以导入配置文件处理器,以后编写配置就有提示了 123456<!--导入配置文件处理器,配置文件进行绑定就会有提示--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> 1、properties配置文件在idea中默认utf-8可能会乱码调整 2、@Value获取值和@ConfigurationProperties获取值比较 @ConfigurationProperties @Value 功能 批量注入配置文件中的属性 一个个指定 松散绑定(松散语法) 支持 不支持 SpEL 不支持 支持 JSR303数据校验 支持 不支持 复杂类型封装 支持 不支持 松散绑定:属性匹配规则 标准方式 person.firstName 方式一 大写用- person.first-name 方式二 大写用_ person.first_name SpEL: #{表达式} 表达式内可以使用各类运算符 JSR303数据校验: 对属性的格式校验,校验时在类上要标注@Validated,对字段的校验常见的有@Email(是否是合法的邮件地址)、@NotNull(是否非空)等 复杂类型:比如map、类等 配置文件yml还是properties他们都能获取到值; 如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用@Value; 如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用@ConfigurationProperties; 3、配置文件注入值数据校验123456789101112131415161718192021222324@Component@ConfigurationProperties(prefix = \"person\")@Validatedpublic class Person { /** * <bean class=\"Person\"> * <property name=\"lastName\" value=\"字面量/${key}从环境变量、配置文件中获取值/#{SpEL}\"></property> * <bean/> */ //lastName必须是邮箱格式 @Email //@Value(\"${person.last-name}\") private String lastName; //@Value(\"#{11*2}\") private Integer age; //@Value(\"true\") private Boolean boss; private Date birth; private Map<String,Object> maps; private List<Object> lists; private Dog dog; 4、@PropertySource&@ImportResource&@Bean@PropertySource:加载指定的配置文件; 1234567891011121314151617181920212223242526272829/** * 将配置文件中配置的每一个属性的值,映射到这个组件中 * @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定; * prefix = \"person\":配置文件中哪个下面的所有属性进行一一映射 * * 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能; * @ConfigurationProperties(prefix = \"person\")默认从全局配置文件中获取值; * */@PropertySource(value = {\"classpath:person.properties\"})@Component@ConfigurationProperties(prefix = \"person\")//@Validatedpublic class Person { /** * <bean class=\"Person\"> * <property name=\"lastName\" value=\"字面量/${key}从环境变量、配置文件中获取值/#{SpEL}\"></property> * <bean/> */ //lastName必须是邮箱格式 // @Email //@Value(\"${person.last-name}\") private String lastName; //@Value(\"#{11*2}\") private Integer age; //@Value(\"true\") private Boolean boss; @ImportResource:导入Spring的配置文件,让配置文件里面的内容生效; Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别; 想让Spring的配置文件生效,加载进来;@ImportResource标注在一个配置类上 并且在beans.xml中配置的类不能再用properties和yml进行注入 12@ImportResource(locations = {\"classpath:beans.xml\"})导入Spring的配置文件让其生效 不来编写Spring的配置文件 12345678<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd\"> <bean id=\"helloService\" class=\"com.atguigu.springboot.service.HelloService\"></bean></beans> SpringBoot推荐给容器中添加组件的方式;推荐使用全注解的方式 1、配置类@Configuration——>Spring配置文件 2、使用@Bean给容器中添加组件 12345678910111213141516/** * @Configuration:指明当前类是一个配置类;就是来替代之前的Spring配置文件 * * 在配置文件中用<bean><bean/>标签添加组件 * */@Configurationpublic class MyAppConfig { //将方法的返回值添加到容器中;容器中这个组件默认的id就是方法名 @Bean public HelloService helloService02(){ System.out.println(\"配置类@Bean给容器中添加组件了...\"); return new HelloService(); }} ##4、配置文件占位符 1、随机数12${random.value}、${random.int}、${random.long}${random.int(10)}、${random.int[1024,65536]} 2、占位符获取之前配置的值,如果没有可以是用:指定默认值123456789person.last-name=张三${random.uuid}person.age=${random.int}person.birth=2017/12/15person.boss=falseperson.maps.k1=v1person.maps.k2=14person.lists=a,b,cperson.dog.name=${person.hello:hello}_dogperson.dog.age=15 5、Profile1、多Profile文件我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml 默认使用application.properties的配置; 2、yml支持多文档块方式12345678910111213141516171819server: port: 8081spring: profiles: active: prod---server: port: 8083spring: profiles: dev---server: port: 8084spring: profiles: prod #指定属于哪个环境 3、激活指定profile 1、在配置文件中指定 spring.profiles.active=dev 2、命令行: java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar –spring.profiles.active=dev; 可以直接在测试的时候,配置传入命令行参数 3、虚拟机参数; -Dspring.profiles.active=dev 4、激活优先使用多profile文件,如果指定的环境在多profile没有配置,才会使用多文档块中的内容 6、配置文件加载位置springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件 –file:./config/ –file:./ –classpath:/config/ –classpath:/ 优先级由高到底,高优先级的配置会覆盖低优先级的配置; SpringBoot会从这四个位置全部加载主配置文件;互补配置; ==我们还可以通过spring.config.location来改变默认的配置文件位置== 项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的这些配置文件共同起作用形成互补配置; java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar –spring.config.location=G:/application.properties 7、外部配置加载顺序==SpringBoot也可以从以下位置加载配置; 优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置== 1.命令行参数 所有的配置都可以在命令行上进行指定 java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar –server.port=8087 –server.context-path=/abc 多个配置用空格分开; –配置项=值 2.来自java:comp/env的JNDI属性 3.Java系统属性(System.getProperties()) 4.操作系统环境变量 5.RandomValuePropertySource配置的random.*属性值 ==由jar包外向jar包内进行寻找;== ==优先加载带profile== 6.jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件 7.jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件 ==再来加载不带profile== 8.jar包外部的application.properties或application.yml(不带spring.profile)配置文件 9.jar包内部的application.properties或application.yml(不带spring.profile)配置文件 10.@Configuration注解类上的@PropertySource 11.通过SpringApplication.setDefaultProperties指定的默认属性 所有支持的配置加载来源; 参考官方文档 8、自动配置原理配置文件到底能写什么?怎么写?自动配置原理; 配置文件能配置的属性参照 1、自动配置原理:1)、SpringBoot启动的时候加载主配置类,开启了自动配置功能@EnableAutoConfiguration 2)、@EnableAutoConfiguration 作用: 利用EnableAutoConfigurationImportSelector给容器中导入一些组件? 可以查看selectImports()方法的内容; List configurations = getCandidateConfigurations(annotationMetadata, attributes);获取候选的配置 SpringFactoriesLoader.loadFactoryNames() 扫描所有jar包类路径下 META-INF/spring.factories 把扫描到的这些文件的内容包装成properties对象 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 **==将 类路径下 META-INF/spring.factories 里面配置的所有EnableAutoConfiguration的值加入到了容器中;==**```properties# Auto Configureorg.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.cloud.CloudAutoConfiguration,\\org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\\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.CassandraRepositoriesAutoConfiguration,\\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\\org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\\org.springframework.boot.autoconfigure.data.ldap.LdapDataAutoConfiguration,\\org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\\org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\\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.redis.RedisAutoConfiguration,\\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.jest.JestAutoConfiguration,\\org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\\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.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.flyway.FlywayAutoConfiguration,\\org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\\org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\\org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\\org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\\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.mobile.DeviceResolverAutoConfiguration,\\org.springframework.boot.autoconfigure.mobile.DeviceDelegatingViewResolverAutoConfiguration,\\org.springframework.boot.autoconfigure.mobile.SitePreferenceAutoConfiguration,\\org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\\org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\\org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\\org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\\org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration,\\org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\\org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,\\org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,\\org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration,\\org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\\org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\\org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration,\\org.springframework.boot.autoconfigure.social.FacebookAutoConfiguration,\\org.springframework.boot.autoconfigure.social.LinkedInAutoConfiguration,\\org.springframework.boot.autoconfigure.social.TwitterAutoConfiguration,\\org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\\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.DispatcherServletAutoConfiguration,\\org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration,\\org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration,\\org.springframework.boot.autoconfigure.web.HttpEncodingAutoConfiguration,\\org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration,\\org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\\org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\\org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration,\\org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\\org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\\org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration,\\org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration 每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置; 3)、每一个自动配置类进行自动配置功能; 4)、以HttpEncodingAutoConfiguration(Http编码自动配置)为例解释自动配置原理; 12345678910111213141516171819202122232425262728@Configuration //表示这是一个配置类,以前编写的配置文件一样,也可以给容器中添加组件@EnableConfigurationProperties(HttpEncodingProperties.class) //启动指定类的ConfigurationProperties功能;将配置文件中对应的值和HttpEncodingProperties绑定起来;并把HttpEncodingProperties加入到ioc容器中@ConditionalOnWebApplication //Spring底层@Conditional注解(Spring注解版),根据不同的条件,如果满足指定的条件,整个配置类里面的配置就会生效; 判断当前应用是否是web应用,如果是,当前配置类生效@ConditionalOnClass(CharacterEncodingFilter.class) //判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;@ConditionalOnProperty(prefix = \"spring.http.encoding\", value = \"enabled\", matchIfMissing = true) //判断配置文件中是否存在某个配置 spring.http.encoding.enabled;如果不存在,判断也是成立的//即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;public class HttpEncodingAutoConfiguration { //他已经和SpringBoot的配置文件映射了 private final HttpEncodingProperties properties; //只有一个有参构造器的情况下,参数的值就会从容器中拿 public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) { this.properties = properties; } @Bean //给容器中添加一个组件,这个组件的某些值需要从properties中获取 @ConditionalOnMissingBean(CharacterEncodingFilter.class) //判断容器没有这个组件? public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE)); return filter; } 根据当前不同的条件判断,决定这个配置类是否生效? 一但这个配置类生效;这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的; 5)、所有在配置文件中能配置的属性都是在xxxxProperties类中封装者‘;配置文件能配置什么就可以参照某个功能对应的这个属性类 1234@ConfigurationProperties(prefix = \"spring.http.encoding\") //从配置文件中获取指定的值和bean的属性进行绑定public class HttpEncodingProperties { public static final Charset DEFAULT_CHARSET = Charset.forName(\"UTF-8\"); 精髓: 1)、SpringBoot启动会加载大量的自动配置类 2)、我们看我们需要的功能有没有SpringBoot默认写好的自动配置类; 3)、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件有,我们就不需要再来配置了) 4)、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们就可以在配置文件中指定这些属性的值; xxxxAutoConfigurartion:自动配置类; 给容器中添加组件 xxxxProperties:封装配置文件中相关属性; 2、细节1、@Conditional派生注解(Spring注解版原生的@Conditional作用)作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效; @Conditional扩展注解 作用(判断是否满足当前指定条件) @ConditionalOnJava 系统的java版本是否符合要求 @ConditionalOnBean 容器中存在指定Bean; @ConditionalOnMissingBean 容器中不存在指定Bean; @ConditionalOnExpression 满足SpEL表达式指定 @ConditionalOnClass 系统中有指定的类 @ConditionalOnMissingClass 系统中没有指定的类 @ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者这个Bean是首选Bean @ConditionalOnProperty 系统中指定的属性是否有指定的值 @ConditionalOnResource 类路径下是否存在指定资源文件 @ConditionalOnWebApplication 当前是web环境 @ConditionalOnNotWebApplication 当前不是web环境 @ConditionalOnJndi JNDI存在指定项 自动配置类必须在一定的条件下才能生效; 我们怎么知道哪些自动配置类生效; ==我们可以通过启用 debug=true属性;来让控制台打印自动配置报告==,这样我们就可以很方便的知道哪些自动配置类生效; 1234567891011121314151617181920212223=========================AUTO-CONFIGURATION REPORT=========================Positive matches:(自动配置类启用的)----------------- DispatcherServletAutoConfiguration matched: - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition) - @ConditionalOnWebApplication (required) found StandardServletEnvironment (OnWebApplicationCondition) Negative matches:(没有启动,没有匹配成功的自动配置类)----------------- ActiveMQAutoConfiguration: Did not match: - @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition) AopAutoConfiguration: Did not match: - @ConditionalOnClass did not find required classes 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice' (OnClassCondition) 三、日志1、日志框架 小张;开发一个大型系统; 1、System.out.println(“”);将关键数据打印在控制台;去掉?写在一个文件? 2、框架来记录系统的一些运行时信息;日志框架 ; zhanglogging.jar; 3、高大上的几个功能?异步模式?自动归档?xxxx? zhanglogging-good.jar? 4、将以前框架卸下来?换上新的框架,重新修改之前相关的API;zhanglogging-prefect.jar; 5、JDBC—数据库驱动; 写了一个统一的接口层;日志门面(日志的一个抽象层);logging-abstract.jar; 给项目中导入具体的日志实现就行了;我们之前的日志框架都是实现的抽象层; 市面上的日志框架; JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j…. 日志门面 (日志的抽象层) 日志实现 JCL(Jakarta Commons Logging) SLF4j(Simple Logging Facade for Java) jboss-logging Log4j JUL(java.util.logging) Log4j2 Logback 左边选一个门面(抽象层)、右边来选一个实现; 日志门面: SLF4J; 日志实现:Logback; SpringBoot:底层是Spring框架,Spring框架默认是用JCL;‘ ==SpringBoot选用 SLF4j和logback;== 2、SLF4j使用1、如何在系统中使用SLF4j https://www.slf4j.org以后开发的时候,日志记录方法的调用,不应该来直接调用日志的实现类,而是调用日志抽象层里面的方法; 给系统里面导入slf4j的jar和 logback的实现jar 123456789import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class HelloWorld { public static void main(String[] args) { Logger logger = LoggerFactory.getLogger(HelloWorld.class); logger.info(\"Hello World\"); }} 图示; 每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件; 2、遗留问题a(slf4j+logback): Spring(commons-logging)、Hibernate(jboss-logging)、MyBatis、xxxx 统一日志记录,即使是别的框架和我一起统一使用slf4j进行输出? 如何让系统中所有的日志都统一到slf4j; ==1、将系统中其他日志框架先排除出去;== ==2、用中间包来替换原有的日志框架;== ==3、我们导入slf4j其他的实现== 3、SpringBoot日志关系1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId></dependency> SpringBoot使用它来做日志功能; 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> 底层依赖关系 总结: 1)、SpringBoot底层也是使用slf4j+logback的方式进行日志记录 2)、SpringBoot也把其他的日志都替换成了slf4j; 3)、中间替换包? 123456@SuppressWarnings(\"rawtypes\")public abstract class LogFactory { static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = \"http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j\"; static LogFactory logFactory = new SLF4JLogFactory(); 4)、如果我们要引入其他框架?一定要把这个框架的默认日志依赖移除掉? Spring框架用的是commons-logging; 12345678910<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions></dependency> ==SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可;== 4、日志使用;1、默认配置SpringBoot默认帮我们配置好了日志; 123456789101112131415161718//记录器Logger logger = LoggerFactory.getLogger(getClass());@Testpublic void contextLoads() { //System.out.println(); //日志的级别; //由低到高 trace<debug<info<warn<error //可以调整输出的日志级别;日志就只会在这个级别以以后的高级别生效 logger.trace(\"这是trace日志...\"); logger.debug(\"这是debug日志...\"); //SpringBoot默认给我们使用的是info级别的,没有指定级别的就用SpringBoot默认规定的级别;root级别 logger.info(\"这是info日志...\"); logger.warn(\"这是warn日志...\"); logger.error(\"这是error日志...\");} 日志输出格式: %d表示日期时间, %thread表示线程名, %-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息, %n是换行符 --> %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%nSpringBoot修改日志的默认配置 123456789101112131415logging.level.com.atguigu=trace#logging.path=# 不指定路径在当前项目下生成springboot.log日志# 可以指定完整的路径;#logging.file=G:/springboot.log# 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件logging.path=/spring/log# 在控制台输出的日志的格式logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n# 指定文件中日志输出的格式logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%n logging.file logging.path Example Description (none) (none) 只在控制台输出 指定文件名 (none) my.log 输出日志到my.log文件 (none) 指定目录 /var/log 输出到指定目录的 spring.log 文件中 2、指定配置给类路径下放上每个日志框架自己的配置文件即可;SpringBoot就不使用他默认配置的了 Logging System Customization Logback logback-spring.xml, logback-spring.groovy, logback.xml or logback.groovy Log4j2 log4j2-spring.xml or log4j2.xml JDK (Java Util Logging) logging.properties logback.xml:直接就被日志框架识别了; logback-spring.xml:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能 1234<springProfile name=\"staging\"> <!-- configuration to be enabled when the \"staging\" profile is active --> 可以指定某段配置只在某个环境下生效</springProfile> 如: 12345678910111213141516171819<appender name=\"stdout\" class=\"ch.qos.logback.core.ConsoleAppender\"> <!-- 日志输出格式: %d表示日期时间, %thread表示线程名, %-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息, %n是换行符 --> <layout class=\"ch.qos.logback.classic.PatternLayout\"> <springProfile name=\"dev\"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern> </springProfile> <springProfile name=\"!dev\"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern> </springProfile> </layout> </appender> 如果使用logback.xml作为日志配置文件,还要使用profile功能,会有以下错误 no applicable action for [springProfile] 5、切换日志框架可以按照slf4j的日志适配图,进行相关的切换; slf4j+log4j的方式; 12345678910111213141516171819<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>logback-classic</artifactId> <groupId>ch.qos.logback</groupId> </exclusion> <exclusion> <artifactId>log4j-over-slf4j</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions></dependency><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId></dependency> 切换为log4j2 123456789101112131415 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-logging</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId></dependency> 四、Web开发1、简介使用SpringBoot; 1)、创建SpringBoot应用,选中我们需要的模块; 2)、SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来 3)、自己编写业务代码; 自动配置原理? 这个场景SpringBoot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?xxx 12xxxxAutoConfiguration:帮我们给容器中自动配置组件;xxxxProperties:配置类来封装配置文件的内容; 2、SpringBoot对静态资源的映射规则;ResourceProperties定义了静态资源的访问路径,还可以设置和静态资源有关的参数,缓存时间等 1234567891011121314@ConfigurationProperties(prefix = \"spring.resources\", ignoreUnknownFields = false)public class ResourceProperties implements ResourceLoaderAware { //静态资源的访问路径 private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { \"classpath:/META-INF/resources/\", \"classpath:/resources/\", \"classpath:/static/\", \"classpath:/public/\" }; /** * Locations of static resources. Defaults to classpath:[/META-INF/resources/, * /resources/, /static/, /public/]. */ private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; //还可以设置和静态资源有关的参数,缓存时间等 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364WebMvcAuotConfiguration: @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug(\"Default resource handling disabled\"); return; } Integer cachePeriod = this.resourceProperties.getCachePeriod(); if (!registry.hasMappingForPattern(\"/webjars/**\")) { customizeResourceHandlerRegistration( registry.addResourceHandler(\"/webjars/**\") .addResourceLocations( \"classpath:/META-INF/resources/webjars/\") .setCachePeriod(cachePeriod)); } String staticPathPattern = this.mvcProperties.getStaticPathPattern(); //静态资源文件夹映射 if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration( registry.addResourceHandler(staticPathPattern) .addResourceLocations( this.resourceProperties.getStaticLocations()) .setCachePeriod(cachePeriod)); } } //配置欢迎页映射 @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping( ResourceProperties resourceProperties) { return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(), this.mvcProperties.getStaticPathPattern()); } //配置喜欢的图标 @Configuration @ConditionalOnProperty(value = \"spring.mvc.favicon.enabled\", matchIfMissing = true) public static class FaviconConfiguration { private final ResourceProperties resourceProperties; public FaviconConfiguration(ResourceProperties resourceProperties) { this.resourceProperties = resourceProperties; } @Bean public SimpleUrlHandlerMapping faviconHandlerMapping() { SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); //所有 **/favicon.ico mapping.setUrlMap(Collections.singletonMap(\"**/favicon.ico\", faviconRequestHandler())); return mapping; } @Bean public ResourceHttpRequestHandler faviconRequestHandler() { ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); requestHandler .setLocations(this.resourceProperties.getFaviconLocations()); return requestHandler; } } 1)、所有 /webjars/** ,都去 classpath:/META-INF/resources/webjars/ 找资源; webjars:以jar包的方式引入静态资源; http://www.webjars.org/ localhost:8080/webjars/jquery/3.3.1/jquery.js 123456<!--引入jquery-webjar-->在访问的时候只需要写webjars下面资源的名称即可 <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version> </dependency> 2)、”/**” 访问当前项目的任何资源,都去(静态资源的文件夹)找映射 12345"classpath:/META-INF/resources/", "classpath:/resources/","classpath:/static/", "classpath:/public/" "/":当前项目的根路径 localhost:8080/abc:去静态资源文件夹里面找abc 如果想要修改静态资源文件夹路径,可以在properties文件中配置spring.resources.static-locations属性指定,指定之后默认文件夹会失效。 3)、欢迎页; 静态资源文件夹下的所有index.html页面;被”/**”映射; localhost:8080/ 找index页面 4)、所有的 **/favicon.ico 都是在静态资源文件下找(如果替换之后不生效可在浏览器ctrl+f5刷新); 3、模板引擎JSP、Velocity、Freemarker、Thymeleaf SpringBoot推荐的Thymeleaf; 语法更简单,功能更强大; 1、引入thymeleaf;123456789101112 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> 2.1.6 </dependency>切换thymeleaf版本<properties> <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version> <!-- 布局功能的支持程序 thymeleaf3主程序 layout2以上版本 --> <!-- thymeleaf2 layout1--> <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version> </properties> 2、Thymeleaf使用1234567891011@ConfigurationProperties(prefix = \"spring.thymeleaf\")public class ThymeleafProperties { private static final Charset DEFAULT_ENCODING = Charset.forName(\"UTF-8\"); private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf(\"text/html\"); public static final String DEFAULT_PREFIX = \"classpath:/templates/\"; public static final String DEFAULT_SUFFIX = \".html\"; // 只要我们把HTML页面放在classpath:/templates/,thymeleaf就能自动渲染; 使用: 1、导入thymeleaf的名称空间 1<html lang=\"en\" xmlns:th=\"http://www.thymeleaf.org\"> 2、使用thymeleaf语法; 123456789101112<!DOCTYPE html><html lang=\"en\" xmlns:th=\"http://www.thymeleaf.org\"><head> <meta charset=\"UTF-8\"> <title>Title</title></head><body> <h1>成功!</h1> <!--th:text 将div里面的文本内容设置为 --> <div th:text=\"${hello}\">这是显示欢迎信息</div></body></html> 3、语法规则1)、th:text;改变当前元素里面的文本内容; th:任意html属性;来替换原生属性的值 2)、表达式? 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869Simple expressions:(表达式语法) Variable Expressions: ${...}:获取变量值;OGNL; 1)、获取对象的属性、调用方法 2)、使用内置的基本对象: #ctx : the context object. #vars: the context variables. #locale : the context locale. #request : (only in Web Contexts) the HttpServletRequest object. #response : (only in Web Contexts) the HttpServletResponse object. #session : (only in Web Contexts) the HttpSession object. #servletContext : (only in Web Contexts) the ServletContext object. ${session.foo} 3)、内置的一些工具对象:#execInfo : information about the template being processed.#messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.#uris : methods for escaping parts of URLs/URIs#conversions : methods for executing the configured conversion service (if any).#dates : methods for java.util.Date objects: formatting, component extraction, etc.#calendars : analogous to #dates , but for java.util.Calendar objects.#numbers : methods for formatting numeric objects.#strings : methods for String objects: contains, startsWith, prepending/appending, etc.#objects : methods for objects in general.#bools : methods for boolean evaluation.#arrays : methods for arrays.#lists : methods for lists.#sets : methods for sets.#maps : methods for maps.#aggregates : methods for creating aggregates on arrays or collections.#ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration). Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样; 补充:配合 th:object=\"${session.user}: <div th:object=\"${session.user}\"> <p>Name: <span th:text=\"*{firstName}\">Sebastian</span>.</p> <p>Surname: <span th:text=\"*{lastName}\">Pepper</span>.</p> <p>Nationality: <span th:text=\"*{nationality}\">Saturn</span>.</p> </div> Message Expressions: #{...}:获取国际化内容 Link URL Expressions: @{...}:定义URL; @{/order/process(execId=${execId},execType='FAST')} Fragment Expressions: ~{...}:片段引用表达式 <div th:insert=\"~{commons :: main}\">...</div> Literals(字面量) Text literals: 'one text' , 'Another one!' ,… Number literals: 0 , 34 , 3.0 , 12.3 ,… Boolean literals: true , false Null literal: null Literal tokens: one , sometext , main ,…Text operations:(文本操作) String concatenation: + Literal substitutions: |The name is ${name}|Arithmetic operations:(数学运算) Binary operators: + , - , * , / , % Minus sign (unary operator): -Boolean operations:(布尔运算) Binary operators: and , or Boolean negation (unary operator): ! , notComparisons and equality:(比较运算) Comparators: > , < , >= , <= ( gt , lt , ge , le ) Equality operators: == , != ( eq , ne )Conditional operators:条件运算(三元运算符) If-then: (if) ? (then) If-then-else: (if) ? (then) : (else) Default: (value) ?: (defaultvalue)Special tokens: No-Operation: _ 4、SpringMVC自动配置https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications 1. Spring MVC auto-configurationSpring Boot 自动配置好了SpringMVC 以下是SpringBoot对SpringMVC的默认配置:==(WebMvcAutoConfiguration)== Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans. 自动配置了ViewResolver(视图解析器:根据方法的返回值得到视图对象(View),视图对象决定如何渲染(转发?重定向?)) ContentNegotiatingViewResolver:组合所有的视图解析器的; ==如何定制:我们可以自己给容器中添加一个视图解析器;自动的将其组合进来;== Support for serving static resources, including support for WebJars (see below).静态资源文件夹路径,webjars Static index.html support. 静态首页访问 Custom Favicon support (see below). favicon.ico 自动注册了 of Converter, GenericConverter, Formatter beans. Converter:转换器; public String hello(User user):类型转换使用Converter Formatter 格式化器; 2017.12.17===Date; 12345@Bean@ConditionalOnProperty(prefix = \"spring.mvc\", name = \"date-format\")//在文件中配置日期格式化的规则public Formatter<Date> dateFormatter() { return new DateFormatter(this.mvcProperties.getDateFormat());//日期格式化组件} ==自己添加的格式化器转换器,我们只需要放在容器中即可== Support for HttpMessageConverters (see below). HttpMessageConverter:SpringMVC用来转换Http请求和响应的;User—Json; HttpMessageConverters 是从容器中确定;获取所有的HttpMessageConverter; ==自己给容器中添加HttpMessageConverter,只需要将自己的组件注册容器中(@Bean,@Component)== Automatic registration of MessageCodesResolver (see below).定义错误代码生成规则 Automatic use of a ConfigurableWebBindingInitializer bean (see below). ==我们可以配置一个ConfigurableWebBindingInitializer来替换默认的;(添加到容器)== 12初始化WebDataBinder;请求数据=====JavaBean; org.springframework.boot.autoconfigure.web:web的所有自动场景; If you want to keep Spring Boot MVC features, and you just want to add additional MVC configuration (interceptors, formatters, view controllers etc.) you can add your own @Configuration class of type WebMvcConfigurerAdapter, but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter or ExceptionHandlerExceptionResolver you can declare a WebMvcRegistrationsAdapter instance providing such components. If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc. 2、扩展SpringMVC1234567<mvc:view-controller path=\"/hello\" view-name=\"success\"/><mvc:interceptors> <mvc:interceptor> <mvc:mapping path=\"/hello\"/> <bean></bean> </mvc:interceptor></mvc:interceptors> 编写一个配置类(@Configuration),实现接口WebMvcConfigurer;不能标注@EnableWebMvc; 既保留了所有的自动配置,也能用我们扩展的配置; 12345678//使用WebMvcConfigurer可以来扩展SpringMVC的功能@Configurationpublic class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(\"/config\").setViewName(\"success\"); }} 原理: 1)、WebMvcAutoConfiguration是SpringMVC的自动配置类 2)、在做其他自动配置时会导入;WebMvcAutoConfiguration的内部类中有@Import(EnableWebMvcConfiguration.class) 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 @Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { ----------------------------------------------------------------------------------------- @Configuration(proxyBeanMethods = false) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { ----------------------------------------------------------------------------------------- @Configuration(proxyBeanMethods = false)public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } @Override protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); } ----------------------------------------------------------------------------------------- class WebMvcConfigurerComposite implements WebMvcConfigurer { private final List<WebMvcConfigurer> delegates = new ArrayList<>(); public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.delegates.addAll(configurers); } } @Override public void addViewControllers(ViewControllerRegistry registry) { for (WebMvcConfigurer delegate : this.delegates) { delegate.addViewControllers(registry); } } 3)、容器中所有的WebMvcConfigurer都会一起起作用; 4)、我们的配置类也会被调用; 效果:SpringMVC的自动配置和我们的扩展配置都会起作用; 3、全面接管SpringMVC;SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己配置;所有的SpringMVC的自动配置都失效了 我们需要在配置类中添加@EnableWebMvc即可; 123456789101112//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能@EnableWebMvc@Configurationpublic class MyMvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { // super.addViewControllers(registry); //浏览器发送 /atguigu 请求来到 success registry.addViewController(\"/atguigu\").setViewName(\"success\"); }} 原理: 为什么@EnableWebMvc自动配置就失效了; 1)@EnableWebMvc的核心 12@Import(DelegatingWebMvcConfiguration.class)public @interface EnableWebMvc { 2)、 12@Configurationpublic class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { 3)、 12345678910@Configuration@ConditionalOnWebApplication@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class })//容器中没有这个组件的时候,这个自动配置类才生效@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class })public class WebMvcAutoConfiguration { 4)、@EnableWebMvc将WebMvcConfigurationSupport组件导入进来; 5)、导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能; 5、如何修改SpringBoot的默认配置模式: 1)、SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如果有就用用户配置的,如果没有,才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默认的组合起来; 2)、在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置 == 3)、在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置 6、RestfulCRUD1)、默认访问首页123456789101112131415161718192021222324252627//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能//@EnableWebMvc 不要接管SpringMVC@Configurationpublic class MyMvcConfig extends WebMvcConfigurerAdapter { //第一种导入组件的方式 @Override public void addViewControllers(ViewControllerRegistry registry) { // super.addViewControllers(registry); //浏览器发送 /atguigu 请求来到 success registry.addViewController(\"/atguigu\").setViewName(\"success\"); } //第二种导入组件的方式 //所有的WebMvcConfigurerAdapter组件都会一起起作用 @Bean //将组件注册在容器 public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){ WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(\"/\").setViewName(\"login\"); registry.addViewController(\"/index.html\").setViewName(\"login\"); } }; return adapter; }} 2)、国际化1)、编写国际化配置文件; 2)、使用ResourceBundleMessageSource管理国际化资源文件 3)、在页面使用fmt:message取出国际化内容 步骤: 1)、编写国际化配置文件,抽取页面需要显示的国际化消息 2)、SpringBoot自动配置好了管理国际化资源文件的组件,需要在spring配置文件中指定国际化配置文件的位置; 1spring.messages.basename=i18n/login 12345678910111213141516171819202122232425262728@ConfigurationProperties(prefix = \"spring.messages\")public class MessageSourceAutoConfiguration { /** * Comma-separated list of basenames (essentially a fully-qualified classpath * location), each following the ResourceBundle convention with relaxed support for * slash based locations. If it doesn't contain a package qualifier (such as * \"org.mypackage\"), it will be resolved from the classpath root. */ private String basename = \"messages\"; //我们的配置文件可以直接放在类路径下叫messages.properties; @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); if (StringUtils.hasText(this.basename)) { //设置国际化资源文件的基础名(去掉语言国家代码的) messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray( StringUtils.trimAllWhitespace(this.basename))); } if (this.encoding != null) { messageSource.setDefaultEncoding(this.encoding.name()); } messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale); messageSource.setCacheSeconds(this.cacheSeconds); messageSource.setAlwaysUseMessageFormat(this.alwaysUseMessageFormat); return messageSource; } 3)、去页面获取国际化的值; 123456789101112131415161718192021222324252627282930313233343536<!DOCTYPE html><html lang=\"en\" xmlns:th=\"http://www.thymeleaf.org\"> <head> <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"> <meta name=\"description\" content=\"\"> <meta name=\"author\" content=\"\"> <title>Signin Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link href=\"asserts/css/bootstrap.min.css\" th:href=\"@{/webjars/bootstrap/4.0.0/css/bootstrap.css}\" rel=\"stylesheet\"> <!-- Custom styles for this template --> <link href=\"asserts/css/signin.css\" th:href=\"@{/asserts/css/signin.css}\" rel=\"stylesheet\"> </head> <body class=\"text-center\"> <form class=\"form-signin\" action=\"dashboard.html\"> <img class=\"mb-4\" th:src=\"@{/asserts/img/bootstrap-solid.svg}\" src=\"asserts/img/bootstrap-solid.svg\" alt=\"\" width=\"72\" height=\"72\"> <h1 class=\"h3 mb-3 font-weight-normal\" th:text=\"#{login.tip}\">Please sign in</h1> <label class=\"sr-only\" th:text=\"#{login.username}\">Username</label> <input type=\"text\" class=\"form-control\" placeholder=\"Username\" th:placeholder=\"#{login.username}\" required=\"\" autofocus=\"\"> <label class=\"sr-only\" th:text=\"#{login.password}\">Password</label> <input type=\"password\" class=\"form-control\" placeholder=\"Password\" th:placeholder=\"#{login.password}\" required=\"\"> <div class=\"checkbox mb-3\"> <label> <input type=\"checkbox\" value=\"remember-me\"/> [[#{login.remember}]] </label> </div> <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\" th:text=\"#{login.btn}\">Sign in</button> <p class=\"mt-5 mb-3 text-muted\">© 2017-2018</p> <a class=\"btn btn-sm\">中文</a> <a class=\"btn btn-sm\">English</a> </form> </body></html> 效果:根据浏览器语言设置的信息切换了国际化; 原理: 国际化Locale(区域信息对象);LocaleResolver(获取区域信息对象); 如果设置了spring.mvc.locale属性且设置为固定解析,并且则使用固定属性,否则默认从请求头中获取。 123#设置locale区域信息spring.mvc.locale=en_USspring.mvc.locale-resolver=fixed 1234567891011121314151617181920212223242526272829303132333435public class WebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = \"spring.mvc\", name = \"locale\") public LocaleResolver localeResolver() { if (this.mvcProperties .getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.mvcProperties.getLocale()); } AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); return localeResolver; }-----------------------------------------------------------------------------------------public class AcceptHeaderLocaleResolver implements LocaleResolver { private final List<Locale> supportedLocales = new ArrayList<>(4); @Override public Locale resolveLocale(HttpServletRequest request) { Locale defaultLocale = getDefaultLocale(); if (defaultLocale != null && request.getHeader(\"Accept-Language\") == null) { return defaultLocale; } Locale requestLocale = request.getLocale(); List<Locale> supportedLocales = getSupportedLocales(); if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) { return requestLocale; } Locale supportedLocale = findSupportedLocale(request, supportedLocales); if (supportedLocale != null) { return supportedLocale; } return (defaultLocale != null ? defaultLocale : requestLocale); } 4)、点击链接切换国际化 在页面按钮处添加超链接并带有区域信息 12<a class=\"btn btn-sm\" th:href=\"@{/index.html(locale='zh_CN')}\">中文</a><a class=\"btn btn-sm\" th:href=\"@{/index.html(locale='en_US')}\">English</a> 12345678910111213141516171819202122232425262728/** * 可以在连接上携带区域信息 */public class MyLocaleResolver implements LocaleResolver { @Override public Locale resolveLocale(HttpServletRequest request) { String l = request.getParameter(\"l\"); Locale locale = Locale.getDefault(); if(!StringUtils.isEmpty(l)){ String[] split = l.split(\"_\"); locale = new Locale(split[0],split[1]); } return locale; } @Override public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { }}//在配置类中添加 @Bean public LocaleResolver localeResolver(){ return new MyLocaleResolver(); }} 3)、登陆开发期间模板引擎页面修改以后,要实时生效 1)、禁用模板引擎的缓存 12# 禁用缓存spring.thymeleaf.cache=false 2)、页面修改完成以后ctrl+f9:重新编译; 用户名密码校验 12345678910111213@Controllerpublic class LoginController { @PostMapping(\"/user/login\") public String login(@RequestParam String username, @RequestParam String password, Map<String,Object> map) { if (username.equals(\"mhs\") && password.equals(\"123\") ) { //为防止页面刷新表单重复提交,使用重定向功能 return \"redirect:/main.html\"; }else { map.put(\"msg\", \"用户名密码错误\"); return \"login\"; } }} 登陆错误消息的显示 1<p style=\"color: red\" th:text=\"${msg}\" th:if=\"${not #strings.isEmpty(msg)}\"></p> 4)、拦截器进行登陆检查拦截器 123456789101112131415161718192021222324252627282930/** * 登陆检查, */public class LoginHandlerInterceptor implements HandlerInterceptor { //目标方法执行之前 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute(\"loginUser\"); if(user == null){ //未登陆,返回登陆页面 request.setAttribute(\"msg\",\"没有权限请先登陆\"); request.getRequestDispatcher(\"/index.html\").forward(request,response); return false; }else{ //已登陆,放行请求 return true; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { }} 注册拦截器 1234567891011121314151617181920212223//所有的WebMvcConfigurerAdapter组件都会一起起作用 @Bean //将组件注册在容器 public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){ WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(\"/\").setViewName(\"login\"); registry.addViewController(\"/index.html\").setViewName(\"login\"); registry.addViewController(\"/main.html\").setViewName(\"dashboard\"); } //注册拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //super.addInterceptors(registry); //静态资源; *.css , *.js //SpringBoot已经做好了静态资源映射 registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns(\"/**\") .excludePathPatterns(\"/index.html\",\"/\",\"/user/login\"); } }; return adapter; } 5)、CRUD-员工列表实验要求: 1)、RestfulCRUD:CRUD满足Rest风格; URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作 普通CRUD(uri来区分操作) RestfulCRUD 查询 getEmp emp—GET 添加 addEmp?xxx emp—POST 修改 updateEmp?id=xxx&xxx=xx emp/{id}—PUT 删除 deleteEmp?id=1 emp/{id}—DELETE 2)、实验的请求架构; 实验功能 请求URI 请求方式 查询所有员工 emps GET 查询某个员工(来到修改页面) emp/1 GET 来到添加页面 emp GET 添加员工 emp POST 来到修改页面(查出员工进行信息回显) emp/1 GET 修改员工 emp PUT 删除员工 emp/1 DELETE 3)、员工列表: thymeleaf公共页面元素抽取12345678910111213141、抽取公共片段<div th:fragment=\"copy\">&copy; 2011 The Good Thymes Virtual Grocery</div>2、引入公共片段<div th:insert=\"~{footer :: copy}\"></div>~{templatename::selector}:模板名::选择器~{templatename::fragmentname}:模板名::片段名3、默认效果:insert的公共片段在div标签中如果使用th:insert等属性进行引入,可以不用写~{}:行内写法可以加上:[[~{}]];[(~{})]; 三种引入公共片段的th属性: th:insert:将公共片段整个插入到声明引入的元素中 th:replace:将声明引入的元素替换为公共片段 th:include:将被引入的片段的内容包含进这个标签中 1234567891011121314151617181920212223<footer th:fragment=\"copy\">&copy; 2011 The Good Thymes Virtual Grocery</footer>引入方式<div th:insert=\"footer :: copy\"></div><div th:replace=\"footer :: copy\"></div><div th:include=\"footer :: copy\"></div>效果<div> <footer> &copy; 2011 The Good Thymes Virtual Grocery </footer></div><footer>&copy; 2011 The Good Thymes Virtual Grocery</footer><div>&copy; 2011 The Good Thymes Virtual Grocery</div> 侧面栏点击时自动高亮的问题 引入片段的时候传入参数: 在sidebar页面进行uri判断 12<a class=\"nav-link active\" th:class=\"${activeUri=='main.html'?'nav-link active':'nav-link'}\"></a> 在引入页面传入参数 1<div th:replace=\"~{common/side::sidebar(activeUri='main.html')}\"/> 1234567891011121314151617<nav class=\"col-md-2 d-none d-md-block bg-light sidebar\" id=\"sidebar\"> <div class=\"sidebar-sticky\"> <ul class=\"nav flex-column\"> <li class=\"nav-item\"> <a class=\"nav-link active\" th:class=\"${activeUri=='main.html'?'nav-link active':'nav-link'}\" href=\"#\" th:href=\"@{/main.html}\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-home\"> <path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"></path> <polyline points=\"9 22 9 12 15 12 15 22\"></polyline> </svg> Dashboard <span class=\"sr-only\">(current)</span> </a> </li><!--引入侧边栏;传入参数--><div th:replace=\"~{common/side::sidebar(activeUri='main.html')}\"/> 员工列表显示 使用th:each遍历并进行性别判断和日期格式转换,并且添加编辑员工的按钮 1<h2><button type=\"button\" class=\"btn btn-sm btn-success\">员工添加</button> </h2> 123456789101112<tr th:each=\"emp:${emps}\"> <td>[[${emp.id}]]</td> <td>[[${emp.lastName}]]</td> <td>[[${emp.email}]]</td> <td>[[${emp.gender==0?\"男\":\"女\"}]]</td> <td>[[${emp.department.departmentName}]]</td> <td th:text=\"${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}\"></td> <td> <button type=\"button\" class=\"btn btn-sm btn-primary\">编辑</button> <button type=\"button\" class=\"btn btn-sm btn-danger\">删除</button> </td></tr> 6)、CRUD-员工添加添加页面 1234567891011121314151617181920212223242526272829303132 <form th:action=\"@{/emp}\" th:method=\"post\"> <div class=\"form-group\"> <label>LastName</label> <input type=\"text\" class=\"form-control\" placeholder=\"zhangsan\" name=\"lastName\"> </div> <div class=\"form-group\"> <label>Email</label> <input type=\"email\" class=\"form-control\" placeholder=\"zhangsan@atguigu.com\" name=\"email\"> </div> <div class=\"form-group\"> <label name=\"gender\">Gender</label><br/> <div class=\"form-check form-check-inline\"> <input class=\"form-check-input\" type=\"radio\" name=\"gender\" value=\"1\"> <label class=\"form-check-label\">男</label> </div> <div class=\"form-check form-check-inline\"> <input class=\"form-check-input\" type=\"radio\" name=\"gender\" value=\"0\"> <label class=\"form-check-label\">女</label> </div> </div> <div class=\"form-group\"> <label>department</label> <select class=\"form-control\" name=\"department.id\"> <option th:each=\"department:${departments}\" th:text=\"${department.id}\" ></option> </select> </div> <div class=\"form-group\"> <label>Birth</label> <input type=\"text\" class=\"form-control\" placeholder=\"2000/01/30\" name=\"birth\"> </div> <button type=\"submit\" class=\"btn btn-primary\">添加</button></form> 后台代码 123456789101112@GetMapping(\"/emp\")public String toAddPage(Model model) { Collection<Department> departments = departmentDao.getDepartments(); model.addAttribute(\"departments\", departments); return \"emp/add\";}@PostMapping(\"/emp\")public String addEmp(Employee employee) { employeeDao.save(employee); return \"redirect:/emps\";} 由于直接返回字符串默认转发给模板引擎处理,因此在要使用转发/重定向。 提交表单格式的问题 要想让表单各项数据能直接封装进employee对象,必须保证各表单项的name属性值和Javabean属性名一致。 提交日期的默认格式为yyyy/MM/dd,否则会报错。 可通过修改spring配置文件修改 12#指定表单日期提交格式spring.mvc.date-format=yyyy-MM-dd 日期格式设置原理 12345678//在WebMvcAutoConfiguration类中有以下方法@Bean@Overridepublic FormattingConversionService mvcConversionService() { WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat()); addFormatters(conversionService); return conversionService;} 关于contoller返回值前缀可用于转发/重定向原理 12345678910111213141516171819202122232425public class ThymeleafViewResolver extends AbstractCachingViewResolver implements Ordered { protected View createView(String viewName, Locale locale) throws Exception { if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) { vrlogger.trace(\"[THYMELEAF] View \\\"{}\\\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.\", viewName); return null; } else { String forwardUrl; if (viewName.startsWith(\"redirect:\")) { vrlogger.trace(\"[THYMELEAF] View \\\"{}\\\" is a redirect, and will not be handled directly by ThymeleafViewResolver.\", viewName); forwardUrl = viewName.substring(\"redirect:\".length(), viewName.length()); RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible()); return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName); } else if (viewName.startsWith(\"forward:\")) { vrlogger.trace(\"[THYMELEAF] View \\\"{}\\\" is a forward, and will not be handled directly by ThymeleafViewResolver.\", viewName); forwardUrl = viewName.substring(\"forward:\".length(), viewName.length()); return new InternalResourceView(forwardUrl); } else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) { vrlogger.trace(\"[THYMELEAF] View \\\"{}\\\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.\", viewName); return null; } else { vrlogger.trace(\"[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it\", viewName, this.getViewClass().getSimpleName()); return this.loadView(viewName, locale); } } } 以上着重有以下几行 12345678910111213141516if (viewName.startsWith(\"redirect:\")) { ... //如果以\"redirect:\",则url截取之后的部分 forwardUrl = viewName.substring(\"redirect:\".length(), viewName.length()); //创建RedirectView并返回 RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible()); ...} else if (viewName.startsWith(\"forward:\")) { //如果以\"redirect:\",则url依然截取之后的部分 forwardUrl = viewName.substring(\"forward:\".length(), viewName.length()); //返回InternalResourceView return new InternalResourceView(forwardUrl);} else { //否则直接通过viewName加载视图 return this.loadView(viewName, locale);} 之后可以通过RedirectView和InternalResourceView看到Servlet原生API 123456789101112131415161718192021222324252627282930313233343536373839404142434445public class RedirectView extends AbstractUrlBasedView implements SmartView { /** * Convert model to request parameters and redirect to the given URL. * @see #appendQueryProperties * @see #sendRedirect */ @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws IOException { String targetUrl = createTargetUrl(model, request); targetUrl = updateTargetUrl(targetUrl, model, request, response); // Save flash attributes RequestContextUtils.saveOutputFlashMap(targetUrl, request, response); // Redirect sendRedirect(request, response, targetUrl, this.http10Compatible); } protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String targetUrl, boolean http10Compatible) throws IOException { String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl)); if (http10Compatible) { HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE); if (this.statusCode != null) { response.setStatus(this.statusCode.value()); response.setHeader(\"Location\", encodedURL); } else if (attributeStatusCode != null) { response.setStatus(attributeStatusCode.value()); response.setHeader(\"Location\", encodedURL); } else { // Send status code 302 by default. response.sendRedirect(encodedURL); } } else { HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl); response.setStatus(statusCode.value()); response.setHeader(\"Location\", encodedURL); } } 12345678910111213141516171819202122232425262728293031323334353637383940414243public class InternalResourceView extends AbstractUrlBasedView { /** * Render the internal resource given the specified model. * This includes setting the model as request attributes. */ @Override protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Expose the model object as request attributes. exposeModelAsRequestAttributes(model, request); // Expose helpers as request attributes, if any. exposeHelpers(request); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(request, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(request, 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(request, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug(\"Including [\" + getUrl() + \"]\"); } rd.include(request, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug(\"Forwarding to [\" + getUrl() + \"]\"); } rd.forward(request, response); } }} 7)、CRUD-员工修改put/delete请求的处理 由于html默认不支持put请求,因此需要 1、SpringMVC中配置HiddenHttpMethodFilter;(SpringBoot自动配置好的)2、页面创建一个post表单3、创建一个input项,name=”_method”;值就是我们指定的请求方式 1<input type=\"hidden\" name=\"_method\" value=\"put\"/> 修改添加二合一表单 修改时在域对象中有emp对象,因此可通过emp是否为空在判断是在修改添加页面 修改页面与添加页面有三处不同: 添加put请求的表单并且添加id的表单项 将修改之前的数据进行回显(注意下拉框和日期) 修改/添加按钮 123456789101112131415161718192021222324252627282930313233343536373839404142<!--需要区分是员工修改还是添加;--><form th:action=\"@{/emp}\" method=\"post\"> <!--发送put请求修改员工数据--> <!--1、SpringMVC中配置HiddenHttpMethodFilter;(SpringBoot自动配置好的)2、页面创建一个post表单3、创建一个input项,name=\"_method\";值就是我们指定的请求方式--> <input type=\"hidden\" name=\"_method\" value=\"put\" th:if=\"${emp!=null}\"/> <input type=\"hidden\" name=\"id\" th:if=\"${emp!=null}\" th:value=\"${emp.id}\"> <div class=\"form-group\"> <label>LastName</label> <input name=\"lastName\" type=\"text\" class=\"form-control\" placeholder=\"zhangsan\" th:value=\"${emp!=null}?${emp.lastName}\"> </div> <div class=\"form-group\"> <label>Email</label> <input name=\"email\" type=\"email\" class=\"form-control\" placeholder=\"zhangsan@atguigu.com\" th:value=\"${emp!=null}?${emp.email}\"> </div> <div class=\"form-group\"> <label>Gender</label><br/> <div class=\"form-check form-check-inline\"> <input class=\"form-check-input\" type=\"radio\" name=\"gender\" value=\"1\" th:checked=\"${emp!=null}?${emp.gender==1}\"> <label class=\"form-check-label\">男</label> </div> <div class=\"form-check form-check-inline\"> <input class=\"form-check-input\" type=\"radio\" name=\"gender\" value=\"0\" th:checked=\"${emp!=null}?${emp.gender==0}\"> <label class=\"form-check-label\">女</label> </div> </div> <div class=\"form-group\"> <label>department</label> <!--提交的是部门的id--> <select class=\"form-control\" name=\"department.id\"> <option th:selected=\"${emp!=null}?${dept.id == emp.department.id}\" th:value=\"${dept.id}\" th:each=\"dept:${depts}\" th:text=\"${dept.departmentName}\">1</option> </select> </div> <div class=\"form-group\"> <label>Birth</label> <input name=\"birth\" type=\"text\" class=\"form-control\" placeholder=\"zhangsan\" th:value=\"${emp!=null}?${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}\"> </div> <button type=\"submit\" class=\"btn btn-primary\" th:text=\"${emp!=null}?'修改':'添加'\">添加</button></form> 后台 123456789101112131415@GetMapping(\"/emp/{id}\") public String toEditPage(@PathVariable(\"id\")Integer id,Model model) { Employee employee = employeeDao.get(id); Collection<Department> departments = departmentDao.getDepartments(); model.addAttribute(\"departments\", departments); model.addAttribute(\"emp\", employee); return \"/emp/add\"; } @PutMapping(\"/emp\") public String editEmp(Employee employee) { System.out.println(employee); employeeDao.save(employee); return \"redirect:/emps\"; } 8)、CRUD-员工删除123456789101112131415161718192021<tr th:each=\"emp:${emps}\"> <td th:text=\"${emp.id}\"></td> <td>[[${emp.lastName}]]</td> <td th:text=\"${emp.email}\"></td> <td th:text=\"${emp.gender}==0?'女':'男'\"></td> <td th:text=\"${emp.department.departmentName}\"></td> <td th:text=\"${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}\"></td> <td> <a class=\"btn btn-sm btn-primary\" th:href=\"@{/emp/}+${emp.id}\">编辑</a> <button th:attr=\"del_uri=@{/emp/}+${emp.id}\" class=\"btn btn-sm btn-danger deleteBtn\">删除</button> </td></tr><script> $(\".deleteBtn\").click(function(){ //删除当前员工的 $(\"#deleteEmpForm\").attr(\"action\",$(this).attr(\"del_uri\")).submit(); return false; });</script> 7、错误处理机制1)、SpringBoot默认的错误处理机制默认效果: 1)、浏览器,返回一个默认的错误页面 浏览器发送请求的请求头: 2)、如果是其他客户端,默认响应一个json数据 原理: 可以参照ErrorMvcAutoConfiguration;错误处理的自动配置; 给容器中添加了以下组件 1、DefaultErrorAttributes: 1234567891011帮我们在页面共享信息;@Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); errorAttributes.put(\"timestamp\", new Date()); addStatus(errorAttributes, requestAttributes); addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); addPath(errorAttributes, requestAttributes); return errorAttributes; } 2、BasicErrorController:处理默认/error请求 12345678910111213141516171819202122232425@Controller@RequestMapping(\"${server.error.path:${error.path:/error}}\")public class BasicErrorController extends AbstractErrorController { @RequestMapping(produces = \"text/html\")//产生html类型的数据;浏览器发送的请求来到这个方法处理 public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); //去哪个页面作为错误页面;包含页面地址和页面内容 ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView(\"error\", model) : modelAndView); } @RequestMapping @ResponseBody //产生json数据,其他客户端来到这个方法处理; public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); } 3、ErrorPageCustomizer: 123456@Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { ErrorPage errorPage = new ErrorPage( this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())); errorPageRegistry.addErrorPages(errorPage); } 1234567public class ErrorProperties { public String getPath() { return this.path; } @Value(\"${error.path:/error}\") private String path = \"/error\"; 系统出现错误以后来到error请求进行处理;(类似于web.xml注册的错误页面规则) 4、DefaultErrorViewResolver: 123456789101112131415161718192021222324252627282930313233private static final Map<Series, String> SERIES_VIEWS;static { Map<Series, String> views = new EnumMap<>(Series.class); views.put(Series.CLIENT_ERROR, \"4xx\"); views.put(Series.SERVER_ERROR, \"5xx\"); SERIES_VIEWS = Collections.unmodifiableMap(views);}@Overridepublic ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView;}private ModelAndView resolve(String viewName, Map<String, Object> model) { //默认SpringBoot可以去找到一个页面? error/404 String errorViewName = \"error/\" + viewName; //模板引擎可以解析这个页面地址就用模板引擎解析 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { //模板引擎可用的情况下返回到errorViewName指定的视图地址 return new ModelAndView(errorViewName, model); } //模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html return resolveResource(errorViewName, model);} 步骤: 在服务器启动时就会进入ErrorPageCustomizer注册错误页面处理路径为/error 一但系统出现4xx或者5xx之类的错误;ErrorPageCustomizer就会生效(定制错误的响应规则);就会来到/error请求;就会被BasicErrorController处理; 1234567891011@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { //此方法执行之前会进入DefaultErrorAttributes获取属性 HttpStatus status = getStatus(request); Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); //进入组件DefaultErrorViewResolver处理请求得到modelandview ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView(\"error\", model); } 响应页面;去哪个页面是由DefaultErrorViewResolver解析得到的; 1234567891011protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { //所有的ErrorViewResolver得到ModelAndView for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null;} 2)、如果定制错误响应:1)、如何定制错误的页面; 1)、有模板引擎的情况下;error/状态码; 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error文件夹下】,发生此状态码的错误就会来到 对应的页面; 我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html); 页面能获取的信息; timestamp:时间戳 status:状态码 error:错误提示 exception:异常对象 message:异常消息 errors:JSR303数据校验的错误都在这里 要想在页面获取异常对象,需要在spring配置文件中加入,默认为false 1server.error.include-exception=true 2)、没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找; 3)、以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面; 2)、如何定制错误的json数据; 1)、自定义异常处理&返回定制json数据; 1234567891011@ControllerAdvicepublic class MyExceptionHandler { @ResponseBody @ExceptionHandler(UserNotExistException.class) public Map<String,Object> handleException(Exception e){ Map<String,Object> map = new HashMap<>(); map.put(\"code\",\"user.notexist\"); map.put(\"message\",e.getMessage()); return map; }} 使用这种方式可以返回定制的json数据,但不能产生自适应效果,也就是浏览器和其他客户端访问时都会产生json 2)、转发到/error进行自适应响应效果处理(但无法携带定制数据) 12345678910111213141516@ExceptionHandler(UserNotExistException.class) public String handleException(Exception e, HttpServletRequest request){ Map<String,Object> map = new HashMap<>(); //传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程 /** * Integer statusCode = (Integer) request .getAttribute(\"javax.servlet.error.status_code\"); */ request.setAttribute(\"javax.servlet.error.status_code\",500); map.put(\"code\",\"user.notexist\"); map.put(\"message\",e.getMessage()); //将信息放入request域中,以便之后取出 request.setAttribute(\"ext\",map); //转发到/error return \"forward:/error\"; } 必须要在request域内设置javax.servlet.error.status_code属性,否则状态码默认设置为200,因此无法找到正确的错误页面 123456789101112protected HttpStatus getStatus(HttpServletRequest request) { Integer statusCode = (Integer) request.getAttribute(\"javax.servlet.error.status_code\"); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } try { return HttpStatus.valueOf(statusCode); } catch (Exception ex) { return HttpStatus.INTERNAL_SERVER_ERROR; } } 3)、将我们的定制数据携带出去;出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法); 1、完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中; 2、页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到; 容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的; 自定义ErrorAttributes 1234567891011121314//给容器中加入我们自己定义的ErrorAttributes@Componentpublic class MyErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace); //将之前request域中的信息取出 Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute(\"ext\", 0); map.put(\"ext\", ext); map.put(\"company\",\"atguigu\"); return map; }} 最终的效果:响应是自适应的,可以通过定制ErrorAttributes改变需要返回的内容, 8、配置嵌入式Servlet容器SpringBoot默认使用Tomcat作为嵌入式的Servlet容器; 问题? 1)、如何定制和修改Servlet容器的相关配置;1、修改和server有关的配置(ServerProperties); 123456789server.port=8081server.context-path=/crudserver.tomcat.uri-encoding=UTF-8//通用的Servlet容器设置server.xxx//Tomcat的设置server.tomcat.xxx ServerProperties的原理 ServerProperties类中有许多属性都可以使用配置文件配置,@ConfigurationProperties注解使得将配置文件转化为Javabean对象,配置文件中前缀为server 123456789@ConfigurationProperties(prefix = \"server\", ignoreUnknownFields = true)public class ServerProperties { private Integer port; private final Servlet servlet = new Servlet(); private final Tomcat tomcat = new Tomcat(); ... 在服务器启动时容器中导入了许多自动配置类,其中ServletWebServerFactoryAutoConfiguration类上@EnableConfigurationProperties(ServerProperties.class)保证了ServerProperties可以的@ConfigurationProperties可以起作用 12345678910@Configuration(proxyBeanMethods = false)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@ConditionalOnClass(ServletRequest.class)@ConditionalOnWebApplication(type = Type.SERVLET)@EnableConfigurationProperties(ServerProperties.class)@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })public class ServletWebServerFactoryAutoConfiguration { 2、编写一个WebServerFactoryCustomizer:嵌入式的Servlet容器的定制器;来修改Servlet容器的配置 1234567891011@Bean //一定要将这个定制器加入到容器中 public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> embeddedServletContainerCustomizer(){ return new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>() { @Override public void customize(ConfigurableServletWebServerFactory factory) { factory.setPort(9000); factory.setContextPath(\"/cccc\"); } }; } 2)、注册Servlet三大组件【Servlet、Filter、Listener】由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。 注册三大组件用以下方式 ServletRegistrationBean 123456//注册三大组件@Beanpublic ServletRegistrationBean myServlet(){ ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),\"/myServlet\"); return registrationBean;} FilterRegistrationBean 1234567@Beanpublic FilterRegistrationBean myFilter(){ FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(new MyFilter()); registrationBean.setUrlPatterns(Arrays.asList(\"/hello\",\"/myServlet\")); return registrationBean;} ServletListenerRegistrationBean 12345@Beanpublic ServletListenerRegistrationBean myListener(){ ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener()); return registrationBean;} SpringBoot帮我们自动SpringMVC的时候,自动的注册SpringMVC的前端控制器;DIspatcherServlet; DispatcherServletAutoConfiguration中: 1234567891011121314151617@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)public ServletRegistrationBean dispatcherServletRegistration( DispatcherServlet dispatcherServlet) { ServletRegistrationBean registration = new ServletRegistrationBean( dispatcherServlet, this.serverProperties.getServletMapping()); //默认拦截: / 所有请求;包静态资源,但是不拦截jsp请求; /*会拦截jsp //可以通过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径 registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); registration.setLoadOnStartup( this.webMvcProperties.getServlet().getLoadOnStartup()); if (this.multipartConfig != null) { registration.setMultipartConfig(this.multipartConfig); } return registration;} 2)、SpringBoot能不能支持其他的Servlet容器; 要想使用其他的容器,要先排除tomcat依赖 12345678910<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-tomcat</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions></dependency> 3)、替换为其他嵌入式Servlet容器 默认支持: Tomcat(默认使用) 12345<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> 引入web模块默认就是使用嵌入式的Tomcat作为Servlet容器;</dependency> Jetty 1234567891011121314151617<!-- 引入web模块 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-tomcat</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions></dependency><!--引入其他的Servlet容器--><dependency> <artifactId>spring-boot-starter-jetty</artifactId> <groupId>org.springframework.boot</groupId></dependency> Undertow 1234567891011121314151617<!-- 引入web模块 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-tomcat</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions></dependency><!--引入其他的Servlet容器--><dependency> <artifactId>spring-boot-starter-undertow</artifactId> <groupId>org.springframework.boot</groupId></dependency> 4)、嵌入式Servlet容器自动配置原理;1. ServletWebServerFactoryAutoConfiguration:嵌入式的Servlet容器自动配置类 12345678910@Configuration(proxyBeanMethods = false)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@ConditionalOnClass(ServletRequest.class)@ConditionalOnWebApplication(type = Type.SERVLET)@EnableConfigurationProperties(ServerProperties.class)@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })public class ServletWebServerFactoryAutoConfiguration { 该类导入了BeanPostProcessorsRegistrar和ServletWebServerFactoryConfiguration类中的内部类EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465@Configuration(proxyBeanMethods = false)class ServletWebServerFactoryConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) static class EmbeddedTomcat { @Bean TomcatServletWebServerFactory tomcatServletWebServerFactory( ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.getTomcatConnectorCustomizers() .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatContextCustomizers() .addAll(contextCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatProtocolHandlerCustomizers() .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList())); return factory; } } /** * Nested configuration if Jetty is being used. */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) static class EmbeddedJetty { @Bean JettyServletWebServerFactory JettyServletWebServerFactory( ObjectProvider<JettyServerCustomizer> serverCustomizers) { JettyServletWebServerFactory factory = new JettyServletWebServerFactory(); factory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList())); return factory; } } /** * Nested configuration if Undertow is being used. */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) static class EmbeddedUndertow { @Bean UndertowServletWebServerFactory undertowServletWebServerFactory( ObjectProvider<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers, ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) { UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory(); factory.getDeploymentInfoCustomizers() .addAll(deploymentInfoCustomizers.orderedStream().collect(Collectors.toList())); factory.getBuilderCustomizers().addAll(builderCustomizers.orderedStream().collect(Collectors.toList())); return factory; } }} ServletWebServerFactoryConfiguration会根据容器中@ConditionalOnClass判断使用哪个嵌入式容器,然后将其工厂容器化 2. 嵌入式Tomcat的创建与启动 123456789101112131415161718@Configuration(proxyBeanMethods = false)@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)static class EmbeddedTomcat { @Bean TomcatServletWebServerFactory tomcatServletWebServerFactory( ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.getTomcatConnectorCustomizers() .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatContextCustomizers() .addAll(contextCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatProtocolHandlerCustomizers() .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList())); return factory; } EmbeddedTomcat将TomcatServletWebServerFactory做了一些设置,并导入容器 123456789101112131415161718192021222324252627282930313233public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware { @Override public WebServer getWebServer(ServletContextInitializer... initializers) { if (this.disableMBeanRegistry) { Registry.disableRegistry(); } //创建Tomcat Tomcat tomcat = new Tomcat(); //配置Tomcat的基本环节 File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir(\"tomcat\"); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); connector.setThrowOnFailure(true); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } prepareContext(tomcat.getHost(), initializers); //将配置好的Tomcat传入进去,返回一个TomcatWebServer;并且启动Tomcat服务器 return getTomcatWebServer(tomcat); } protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { //端口号>0就自动开始 return new TomcatWebServer(tomcat, getPort() >= 0); } 可以看到创建Tomcat的最后是调用了getTomcatWebServe( ) 方法,而方法调用了TomcatWebServer的构造器,因此进入TomcatWebServer中 123456789101112131415161718192021222324252627282930313233343536373839404142public class TomcatWebServer implements WebServer { public TomcatWebServer(Tomcat tomcat, boolean autoStart) { Assert.notNull(tomcat, \"Tomcat Server must not be null\"); this.tomcat = tomcat; this.autoStart = autoStart; initialize(); } private void initialize() throws WebServerException { logger.info(\"Tomcat initialized with port(s): \" + getPortsDescription(false)); synchronized (this.monitor) { try { addInstanceIdToEngineName(); Context context = findContext(); context.addLifecycleListener((event) -> { if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { removeServiceConnectors(); } }); //启动Tomcat this.tomcat.start(); rethrowDeferredStartupExceptions(); try { ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); } catch (NamingException ex) { } startDaemonAwaitThread(); } catch (Exception ex) { stopSilently(); destroySilently(); //不能启动则抛出异常 throw new WebServerException(\"Unable to start embedded Tomcat\", ex); } } } 在TomcatWebServer类中启动嵌入式Tomcat,如果不能启动则抛出异常 3. 我们对嵌入式容器的配置修改是怎么生效 ServletWebServerFactoryAutoConfiguration还导入了其内部类BeanPostProcessorsRegistrar类 12345678910111213public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { if (this.beanFactory == null) { return; } //如果容器中没有则注册WebServerFactoryCustomizerBeanPostProcessor类 registerSyntheticBeanIfMissing(registry, \"webServerFactoryCustomizerBeanPostProcessor\", WebServerFactoryCustomizerBeanPostProcessor.class); registerSyntheticBeanIfMissing(registry, \"errorPageRegistrarBeanPostProcessor\", ErrorPageRegistrarBeanPostProcessor.class); } BeanPostProcessorsRegistrar注册了WebServerFactoryCustomizerBeanPostProcessor类 1234567891011121314151617public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { //如果当前初始化的为WebServerFactory类型的组件,则开启后处理 if (bean instanceof WebServerFactory) { postProcessBeforeInitialization((WebServerFactory) bean); } return bean; } //在WebServerFactory初始化之前调用WebServerFactoryCustomizer的所有customize()方法 private void postProcessBeforeInitialization(WebServerFactory webServerFactory) { LambdaSafe.callbacks(WebServerFactoryCustomizer.class, getCustomizers(), webServerFactory) .withLogger(WebServerFactoryCustomizerBeanPostProcessor.class) .invoke((customizer) -> customizer.customize(webServerFactory)); } WebServerFactoryCustomizerBeanPostProcessor在WebServerFactory初始化前回调了WebServerFactoryCustomizer的方法,而该方法就是我们的定制方法,=,下面是我们自己的配置类中定制代码 12345678910111213@Configurationpublic class MyMvcConfig implements WebMvcConfigurer { @Bean public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> webServerFactoryCustomizer(){ return new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>() { @Override public void customize(ConfigurableServletWebServerFactory factory) { factory.setPort(9000); factory.setContextPath(\"/cccc\"); } }; } 步骤: 1)、SpringBoot根据导入的依赖情况,给容器中添加相应的ServletWebServerFactory 2)、容器中某个组件要创建对象就会惊动后置处理器WebServerFactoryCustomizerBeanPostProcessor; 只要是嵌入式的Servlet容器工厂,后置处理器就工作; 3)、后置处理器,从容器中获取所有的WebServerFactoryCustomizer,调用定制器的定制方法 ==嵌入式Servlet容器启动原理== 什么时候创建嵌入式的Servlet容器工厂?什么时候获取嵌入式的Servlet容器并启动Tomcat; 获取嵌入式的Servlet容器工厂: 1)、SpringBoot应用启动运行run方法 2)、refreshContext(context);SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器中的每一个组件】;并根据web应用的类型创建对应的IOC容器对象(如AnnotationConfigServletWebServerApplicationContext) 123456789101112131415161718192021222324252627282930313233343536373839public class SpringApplication { public ConfigurableApplicationContext run(String... args) { ... //创建IOC容器 context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); //刷新IOC容器 refreshContext(context); afterRefresh(context, applicationArguments); ... } protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; //如果不是普通应用则创建web应用容器 if (contextClass == null) { try { //根据web应用类型创建对应IOC容器 switch (this.webApplicationType) { case SERVLET: //AnnotationConfigServletWebServerApplicationContext contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default: contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) { throw new IllegalStateException( \"Unable create a default ApplicationContext, please specify an ApplicationContextClass\", ex); } } return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); } 3)、refresh(context);刷新刚才创建好的ioc容器; 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn(\"Exception encountered during context initialization - \" + \"cancelling refresh attempt: \" + ex); } // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } }} 4)、 onRefresh(); web的ioc容器重写了onRefresh方法,在此方法中调用getWebServerFactory(),根据环境创建工厂,因为此时为tomcat环境,因此创建了TomcatServletWebServerFactory。 5)、ServletWebServerFactory的创建触发了后置处理器,获取所有的定制器来先定制Servlet容器的相关配置; 6)、此时ServletWebServerFactory实际引用了TomcatServletWebServerFactory,因此调用了TomcatServletWebServerFactory的getWebServer( )方法,在该方法中tomcat被创建并启动。 1234567891011121314151617181920212223242526272829303132public class ServletWebServerApplicationContext extends GenericWebApplicationContext implements ConfigurableWebServerApplicationContext { @Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException(\"Unable to start web server\", ex); } } private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { //由于此时为tomcat环境,因此ServletWebServerFactory实际上就是TomcatServletWebServerFactory ServletWebServerFactory factory = getWebServerFactory(); //调用TomcatServletWebServerFactory的getWebServer()方法 this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException(\"Cannot initialize servlet context\", ex); } } initPropertySources(); } 123456789101112131415161718192021222324public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware { @Override public WebServer getWebServer(ServletContextInitializer... initializers) { if (this.disableMBeanRegistry) { Registry.disableRegistry(); } //创建Tomcat并启动 Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir(\"tomcat\"); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); connector.setThrowOnFailure(true); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); } 9、使用外置的Servlet容器嵌入式Servlet容器:应用打成可执行的jar 优点:简单、便携; 缺点:默认不支持JSP、优化定制比较复杂(使用定制器【ServerProperties、自定义EmbeddedServletContainerCustomizer】,自己编写嵌入式Servlet容器的创建工厂【EmbeddedServletContainerFactory】); 外置的Servlet容器:外面安装Tomcat—应用war包的方式打包; 步骤1)、必须创建一个war项目;(利用idea创建好目录结构) 2)、将嵌入式的Tomcat指定为provided; 12345<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope></dependency> 3)、必须编写一个SpringBootServletInitializer的子类,并调用configure方法 123456789public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { //传入SpringBoot应用的主程序 return application.sources(SpringBoot04WebJspApplication.class); }} 4)、启动服务器就可以使用; 原理jar包:执行SpringBoot主类的main方法,启动ioc容器,创建嵌入式的Servlet容器; war包:启动服务器,服务器启动SpringBoot应用【SpringBootServletInitializer】,启动ioc容器; servlet3.0(Spring注解版): 8.2.4 Shared libraries / runtimes pluggability: 规则: 1)、服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例: 2)、ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名 3)、还可以使用@HandlesTypes,在应用启动的时候加载我们感兴趣的类; 流程: 1)、启动Tomcat 2)、org\\springframework\\spring-web\\4.3.14.RELEASE\\spring-web-4.3.14.RELEASE.jar!\\META-INF\\services\\javax.servlet.ServletContainerInitializer: Spring的web模块里面有这个文件:org.springframework.web.SpringServletContainerInitializer 3)、SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型的类都传入到onStartup方法的Set<Class<?>>;为这些WebApplicationInitializer类型的类创建实例; 4)、每一个WebApplicationInitializer都调用自己的onStartup; 5)、相当于我们的SpringBootServletInitializer的类会被创建对象,并执行onStartup方法 6)、SpringBootServletInitializer实例执行onStartup的时候会createRootApplicationContext;创建容器 1234567891011121314151617181920212223242526272829303132333435363738protected WebApplicationContext createRootApplicationContext( ServletContext servletContext) { //1、创建SpringApplicationBuilder SpringApplicationBuilder builder = createSpringApplicationBuilder(); StandardServletEnvironment environment = new StandardServletEnvironment(); environment.initPropertySources(servletContext, null); builder.environment(environment); builder.main(getClass()); ApplicationContext parent = getExistingRootWebApplicationContext(servletContext); if (parent != null) { this.logger.info(\"Root context already created (using as parent).\"); servletContext.setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null); builder.initializers(new ParentContextApplicationContextInitializer(parent)); } builder.initializers( new ServletContextApplicationContextInitializer(servletContext)); builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class); //调用configure方法,子类重写了这个方法,将SpringBoot的主程序类传入了进来 builder = configure(builder); //使用builder创建一个Spring应用 SpringApplication application = builder.build(); if (application.getSources().isEmpty() && AnnotationUtils .findAnnotation(getClass(), Configuration.class) != null) { application.getSources().add(getClass()); } Assert.state(!application.getSources().isEmpty(), \"No SpringApplication sources have been defined. Either override the \" + \"configure method or add an @Configuration annotation\"); // Ensure error pages are registered if (this.registerErrorPageFilter) { application.getSources().add(ErrorPageFilterConfiguration.class); } //启动Spring应用 return run(application);} 7)、Spring的应用就启动并且创建IOC容器 1234567891011121314151617181920212223242526272829303132333435public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; FailureAnalyzers analyzers = null; configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); Banner printedBanner = printBanner(environment); context = createApplicationContext(); analyzers = new FailureAnalyzers(context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); //刷新IOC容器 refreshContext(context); afterRefresh(context, applicationArguments); listeners.finished(context, null); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } return context; } catch (Throwable ex) { handleRunFailure(context, listeners, analyzers, ex); throw new IllegalStateException(ex); }} ==启动Servlet容器,再启动SpringBoot应用== 五、Docker1、简介Docker是一个开源的应用容器引擎;是一个轻量级容器技术; Docker支持将软件编译成一个镜像;然后在镜像中各种软件做好配置,将镜像发布出去,其他使用者可以直接使用这个镜像; 运行中的这个镜像称为容器,容器启动是非常快速的。 2、核心概念docker主机(Host):安装了Docker程序的机器(Docker直接安装在操作系统之上); docker客户端(Client):连接docker主机进行操作; docker仓库(Registry):用来保存各种打包好的软件镜像; docker镜像(Images):软件打包好的镜像;放在docker仓库中; docker容器(Container):镜像启动后的实例称为一个容器;容器是独立运行的一个或一组应用 使用Docker的步骤: 1)、安装Docker 2)、去Docker仓库找到这个软件对应的镜像; 3)、使用Docker运行这个镜像,这个镜像就会生成一个Docker容器; 4)、对容器的启动停止就是对软件的启动停止; 3、安装Docker1)、安装linux虚拟机 1)、VMWare、VirtualBox(安装); 2)、导入虚拟机文件centos7-atguigu.ova; 3)、双击启动linux虚拟机;使用 root/ 123456登陆 4)、使用客户端连接linux服务器进行命令操作; 5)、设置虚拟机网络; 桥接网络===选好网卡====接入网线; 6)、设置好网络以后使用命令重启虚拟机的网络 1service network restart 7)、查看linux的ip地址 1ip addr 8)、使用客户端连接linux; 2)、在linux虚拟机上安装docker步骤: 12345678910111213141、检查内核版本,必须是3.10及以上uname -r2、安装dockeryum install docker3、输入y确认安装4、启动docker[root@localhost ~]# systemctl start docker[root@localhost ~]# docker -vDocker version 1.12.6, build 3e8e77d/1.12.65、开机启动docker[root@localhost ~]# systemctl enable dockerCreated symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.6、停止dockersystemctl stop docker 4、Docker常用命令&操作1)、镜像操作 操作 命令 说明 检索 docker search 关键字 eg:docker search redis 我们经常去docker hub上检索镜像的详细信息,如镜像的TAG。 拉取 docker pull 镜像名:tag :tag是可选的,tag表示标签,多为软件的版本,默认是latest 列表 docker images 查看所有本地镜像 删除 docker rmi image-id 删除指定的本地镜像 https://hub.docker.com/ 2)、容器操作软件镜像(QQ安装程序)—-运行镜像—-产生一个容器(正在运行的软件,运行的QQ); 步骤: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647481、搜索镜像docker search tomcat2、拉取镜像docker pull tomcat3、根据镜像启动容器docker run --name mytomcat -d tomcat:latest4、查看运行中的容器docker ps 5、 停止运行中的容器docker stop 容器的id6、查看所有的容器docker ps -a7、启动容器docker start 容器id-d:后台运行-p:将主机的端口映射到容器的一个端口 主机端口:容器内部的端口-e:设置环境变量--name:设置容器名---restart:重启规则 --restart=unless-stopped 表示docker启动时该容器自启 启动一个做了端口映射的tomcat,设置其root密码为123456,且docker启动自启docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d --restart=unless-stopped 413be204e9c38、删除一个容器 docker rm 容器id 10、为了演示简单关闭了linux的防火墙service firewalld status ;查看防火墙状态service firewalld stop:关闭防火墙11、查看容器的日志docker logs container-name/container-id12、进入容器内docker exec -ti 容器ID /bin/bash更多命令参看https://docs.docker.com/engine/reference/commandline/docker/可以参考每一个镜像的文档 tomcat启动后访问主页后出现404 解决方案 3)、安装MySQL示例1docker pull mysql 错误的启动 123456789101112131415161718[root@localhost ~]# docker run --name mysql01 -d mysql42f09819908bb72dd99ae19e792e0a5d03c48638421fa64cce5f8ba0f40f5846mysql退出了[root@localhost ~]# docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES42f09819908b mysql \"docker-entrypoint.sh\" 34 seconds ago Exited (1) 33 seconds ago mysql01538bde63e500 tomcat \"catalina.sh run\" About an hour ago Exited (143) About an hour ago compassionate_goldstinec4f1ac60b3fc tomcat \"catalina.sh run\" About an hour ago Exited (143) About an hour ago lonely_fermi81ec743a5271 tomcat \"catalina.sh run\" About an hour ago Exited (143) About an hour ago sick_ramanujan//错误日志//要指定mysql密码[root@localhost ~]# docker logs 42f09819908berror: database is uninitialized and password option is not specified You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD;这个三个参数必须指定一个 正确的启动 12345[root@localhost ~]# docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -d mysqlb874c56bec49fb43024b3805ab51e9097da779f2f572c22c695305dedd684c5f[root@localhost ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESb874c56bec49 mysql \"docker-entrypoint.sh\" 4 seconds ago Up 3 seconds 3306/tcp mysql01 做了端口映射 12345[root@localhost ~]# docker run -p 3306:3306 --name mysql02 -e MYSQL_ROOT_PASSWORD=123456 -d mysqlad10e4bc5c6a0f61cbad43898de71d366117d120e39db651844c0e73863b9434[root@localhost ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESad10e4bc5c6a mysql \"docker-entrypoint.sh\" 4 seconds ago Up 2 seconds 0.0.0.0:3306->3306/tcp mysql02 几个其他的高级操作 123456docker run --name mysql03 -v /conf/mysql:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag把主机的/conf/mysql文件夹挂载到 mysqldocker容器的/etc/mysql/conf.d文件夹里面改mysql的配置文件就只需要把mysql配置文件放在自定义的文件夹下(/conf/mysql)docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci指定mysql的一些配置参数 如何使用官方镜像Docker 中国官方镜像加速可通过 registry.docker-cn.com 访问。目前该镜像库只包含流行的公有镜像,而私有镜像仍需要从美国镜像库中拉取。 可以使用以下命令直接从该镜像加速地址进行拉取。 1docker pull registry.docker-cn.com/myname/myrepo:mytag 示例如下: 1docker pull registry.docker-cn.com/library/ubuntu:16.04 注:除非您修改了Docker守护进程的–registry-mirror参数,否则您将需要完整地指定官方镜像的名称。例如,library/ubuntu、library/redis、library/nginx。 给Docker守护进程配置加速器通过配置文件启动Docker,修改/etc/docker/daemon.json 文件并添加上 registry-mirrors 键值。 1sudo vim /etc/docker/daemon.json 123{ \"registry-mirrors\": [\"https://registry.docker-cn.com\"]} 也可选用网易的镜像地址:http://hub-mirror.c.163.com{“registry-mirrors”: [“http://hub-mirror.c.163.com"]} 修改保存后,重启 Docker 以使配置生效。 1sudo service docker restart 六、SpringBoot与数据访问1、JDBC123456789<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope></dependency> 1234567spring: datasource: username: root password: 123 # 在url之后要加上时区信息,否则会报错(mysql版本高对安全校验的提高) url: jdbc:mysql://127.0.0.1:3306/springboot?serverTimezone=Asia/Shanghai driver-class-name: com.mysql.jdbc.Driver 效果: 2.2.5版本默认是用com.zaxxer.hikari.HikariDataSource作为数据源; 数据源的相关配置都在DataSourceProperties里面; 自动配置原理: org.springframework.boot.autoconfigure.jdbc: 1、参考DataSourceConfiguration,根据配置创建数据源,默认使用Tomcat连接池;可以使用spring.datasource.type指定自定义的数据源类型; 2、SpringBoot默认可以支持; 1org.apache.tomcat.jdbc.pool.DataSource、HikariDataSource、BasicDataSource、 3、自定义数据源类型 123456789101112131415/** * Generic DataSource configuration. */@Configuration(proxyBeanMethods = false)@ConditionalOnMissingBean(DataSource.class)@ConditionalOnProperty(name = \"spring.datasource.type\")static class Generic { @Bean DataSource dataSource(DataSourceProperties properties) { //使用DataSourceBuilder创建数据源,利用反射创建响应type的数据源,并且绑定相关属性 return properties.initializeDataSourceBuilder().build(); }} 4、DataSourceInitializer:ApplicationListener; 作用: 1)、createSchema();运行建表语句; 2)、runDataScripts();运行插入数据的sql语句; 12345678910111213141516171819202122232425262728293031323334353637383940414243boolean createSchema() { //向getScripts()传入参数fallback=\"schema\" List<Resource> scripts = getScripts(\"spring.datasource.schema\", this.properties.getSchema(), \"schema\"); if (!scripts.isEmpty()) { if (!isEnabled()) { logger.debug(\"Initialization disabled (not running DDL scripts)\"); return false; } String username = this.properties.getSchemaUsername(); String password = this.properties.getSchemaPassword(); runScripts(scripts, username, password); } return !scripts.isEmpty(); }void initSchema() { //向getScripts()传入参数fallback=\"data\" List<Resource> scripts = getScripts(\"spring.datasource.data\", this.properties.getData(), \"data\"); if (!scripts.isEmpty()) { if (!isEnabled()) { logger.debug(\"Initialization disabled (not running data scripts)\"); return; } String username = this.properties.getDataUsername(); String password = this.properties.getDataPassword(); runScripts(scripts, username, password); } } private List<Resource> getScripts(String propertyName, List<String> resources, String fallback) { if (resources != null) { return getResources(propertyName, resources, true); } String platform = this.properties.getPlatform(); List<String> fallbackResources = new ArrayList<>(); //createSchema()传入fallback=\"schema\",因此文件名为schema-all.sql/schema.sql //initSchema()传入fallback=\"data\",因此文件名为data-all.sql/data.sql fallbackResources.add(\"classpath*:\" + fallback + \"-\" + platform + \".sql\"); fallbackResources.add(\"classpath*:\" + fallback + \".sql\"); return getResources(propertyName, fallbackResources, false); }x 默认只需要将文件命名为: 12345678schema-*.sql、data-*.sql默认规则:schema.sql,schema-all.sql;可以使用 schema: - classpath:department.sql 指定位置#在2.2.x开启应用启动建表spring.datasource.initialization-mode=always 5、操作数据库:自动配置了JdbcTemplate操作数据库 JdbcTemplateAutoConfiguration导入了JdbcTemplateConfiguration类,而JdbcTemplateConfiguration类中向容器中添加了JdbcTemplate,因此我们可以使用JdbcTemplate操作数据库 1234567891011121314151617181920212223242526272829@Configuration(proxyBeanMethods = false)@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })@ConditionalOnSingleCandidate(DataSource.class)@AutoConfigureAfter(DataSourceAutoConfiguration.class)@EnableConfigurationProperties(JdbcProperties.class)@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })public class JdbcTemplateAutoConfiguration {}@Configuration(proxyBeanMethods = false)@ConditionalOnMissingBean(JdbcOperations.class)class JdbcTemplateConfiguration { //向容器中导入JdbcTemplate @Bean @Primary JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); JdbcProperties.Template template = properties.getTemplate(); jdbcTemplate.setFetchSize(template.getFetchSize()); jdbcTemplate.setMaxRows(template.getMaxRows()); if (template.getQueryTimeout() != null) { jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds()); } return jdbcTemplate; }} 2、整合Druid数据源配置 1234567891011121314151617181920212223242526spring: datasource: username: root password: 123 url: jdbc:mysql://127.0.0.1:3306/springboot?serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource# initialization-mode: always 开启启动建表 # 数据源其他配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 由于spring.datasource默认使用DataSourceProperties类进行配置,而DataSourceProperties中没有druid配置的各项属性,因此我们需要自定义配置类 12345678910111213141516171819202122232425262728293031323334353637383940414243导入druid数据源@Configurationpublic class DruidConfig { @ConfigurationProperties(prefix = \"spring.datasource\") @Bean public DataSource druid(){ return new DruidDataSource(); } //配置Druid的监控 //1、配置一个管理后台的Servlet @Bean public ServletRegistrationBean statViewServlet(){ ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), \"/druid/*\"); Map<String,String> initParams = new HashMap<>(); initParams.put(\"loginUsername\",\"admin\"); initParams.put(\"loginPassword\",\"123456\"); initParams.put(\"allow\",\"\");//默认就是允许所有访问 initParams.put(\"deny\",\"192.168.15.21\"); bean.setInitParameters(initParams); return bean; } //2、配置一个web监控的filter @Bean public FilterRegistrationBean webStatFilter(){ FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebStatFilter()); Map<String,String> initParams = new HashMap<>(); initParams.put(\"exclusions\",\"*.js,*.css,/druid/*\"); bean.setInitParameters(initParams); bean.setUrlPatterns(Arrays.asList(\"/*\")); return bean; }} 3、整合MyBatis12345<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version></dependency> 步骤: 1)、配置数据源相关属性(见上一节Druid) 2)、给数据库建表 3)、创建JavaBean 4)、注解版1234567891011121314151617//指定这是一个操作数据库的mapper@Mapperpublic interface DepartmentMapper { @Select(\"select * from department where id=#{id}\") public Department getDeptById(Integer id); @Delete(\"delete from department where id=#{id}\") public int deleteDeptById(Integer id); @Options(useGeneratedKeys = true,keyProperty = \"id\") @Insert(\"insert into department(departmentName) values(#{departmentName})\") public int insertDept(Department department); @Update(\"update department set departmentName=#{departmentName} where id=#{id}\") public int updateDept(Department department);} 问题: 自定义MyBatis的配置规则;给容器中添加一个ConfigurationCustomizer; 比如支持驼峰命名法: 12@Select(\"SELECT * FROM department where id=#{id}\") Department getDeptById(Integer id); 在不定制的情况下,departmentName是封装不进去的 123456789101112131415@org.springframework.context.annotation.Configurationpublic class MyBatisConfig { @Bean public ConfigurationCustomizer configurationCustomizer(){ return new ConfigurationCustomizer(){ //开启驼峰命名法 @Override public void customize(Configuration configuration) { configuration.setMapUnderscoreToCamelCase(true); } }; }} 123456789使用MapperScan批量扫描所有的Mapper接口;@MapperScan(value = \"com.atguigu.springboot.mapper\")@SpringBootApplicationpublic class SpringBoot06DataMybatisApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot06DataMybatisApplication.class, args); }} 5)、配置文件版配置文件可与注解同时生效,但相关配置(如驼峰命名法)需要单独各自配置 mybatis-config.xml 12345678910<?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> <settings> <!--指定开启驼峰命名法--> <setting name=\"mapUnderscoreToCamelCase\" value=\"true\"/> </settings></configuration> EmployeeMapper.xml 123456789101112<?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=\"cn.edu.ustc.springboot.mapper.EmployeeMapper\"> <select id=\"getEmpById\" resultType=\"cn.edu.ustc.springboot.bean.Employee\"> select * from employee where id = #{id} </select> <insert id=\"insertEmp\"> INSERT INTO employee(lastName,gender,email,d_id) VALUES (#{lastName},#{gender},#{email},#{dId}) </insert></mapper> 123mybatis: config-location: classpath:mybatis/mybatis-config.xml 指定全局配置文件的位置 mapper-locations: classpath:mybatis/mapper/*.xml 指定sql映射文件的位置 更多使用参照 http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/ 4、整合SpringData JPA1)、SpringData简介 2)、整合SpringData JPAJPA:ORM(Object Relational Mapping); 1)、编写一个实体类(bean)和数据表进行映射,并且配置好映射关系; 12345678910111213//使用JPA注解配置映射关系@Entity //告诉JPA这是一个实体类(和数据表映射的类)@Table(name = \"tbl_user\") //@Table来指定和哪个数据表对应;如果省略默认表名就是user;public class User { @Id //这是一个主键 @GeneratedValue(strategy = GenerationType.IDENTITY)//自增主键 private Integer id; @Column(name = \"last_name\",length = 50) //这是和数据表对应的一个列 private String lastName; @Column //省略默认列名就是属性名 private String email; 2)、编写一个Dao接口来操作实体类对应的数据表(Repository) 123//继承JpaRepository来完成对数据库的操作public interface UserRepository extends JpaRepository<User,Integer> {} 3)、基本的配置JpaProperties 123456789101112spring: datasource: username: root password: 123 url: jdbc:mysql://localhost:3306/springboot?serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver jpa: #开启自动创建表或更新 hibernate: ddl-auto: update #开启打印sql语句 show-sql: true 七、启动配置原理几个重要的事件回调机制 配置在META-INF/spring.factories ApplicationContextInitializer SpringApplicationRunListener 只需要放在ioc容器中 ApplicationRunner CommandLineRunner 启动流程: 1、创建SpringApplication对象1234567891011121314public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, \"PrimarySources must not be null\"); //保存主配置类 this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); //判断当前web应用类型(REACTIVE/SERVLET/NONE即非web) this.webApplicationType = WebApplicationType.deduceFromClasspath(); //从类路径下找到META-INF/spring.factories配置的所有ApplicationContextInitializer;然后保存起来 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); //从类路径下找到ETA-INF/spring.factories配置的所有ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); //从多个配置类中找到有main方法的主配置类 this.mainApplicationClass = deduceMainApplicationClass();} 2、运行run方法123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); configureHeadlessProperty(); //获取SpringApplicationRunListeners;从类路径下META-INF/spring.factories SpringApplicationRunListeners listeners = getRunListeners(args); //回调所有的获取SpringApplicationRunListener.starting()方法 listeners.starting(); try { //封装命令行参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); //准备环境 //创建环境完成后回调SpringApplicationRunListener.environmentPrepared();表示环境准备完成 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); //创建ApplicationContext;根据webApplicationType创建SERVLET/REACTIVE/普通容器 context = createApplicationContext(); //获取SpringBootExceptionReporter;从类路径下META-INF/spring.factories exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); //准备上下文环境;将environment保存到ioc中;而且applyInitializers(); //applyInitializers():回调之前保存的所有的ApplicationContextInitializer的initialize方法 //回调所有的SpringApplicationRunListener的contextPrepared(); //prepareContext运行完成以后回调所有的SpringApplicationRunListener的contextLoaded(); prepareContext(context, environment, listeners, applicationArguments, printedBanner); //刷新容器;ioc容器初始化(如果是Servlet应用还会创建嵌入式的Tomcat并启动); //扫描,创建,加载所有组件的地方;(配置类,组件,自动配置) refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } //回调所有的获取SpringApplicationRunListener.started()方法 listeners.started(context); //从容器中分别获取ApplicationRunner和CommandLineRunner并调用其回调方法胜 //ApplicationRunner先回调,CommandLineRunner再回调 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { //回调所有的获取SpringApplicationRunListener.running()方法 listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context;} 3、事件监听机制配置在META-INF/spring.factories ApplicationContextInitializer 123456public class HelloApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { System.out.println(\"ApplicationContextInitializer...initialize...\"+applicationContext); }} SpringApplicationRunListener 123456789101112131415161718192021222324252627282930313233public class HelloSpringApplicationRunListener implements SpringApplicationRunListener { //必须有的构造器 public HelloSpringApplicationRunListener(SpringApplication application, String[] args){ } @Override public void starting() { System.out.println(\"SpringApplicationRunListener...starting...\"); } @Override public void environmentPrepared(ConfigurableEnvironment environment) { Object o = environment.getSystemProperties().get(\"os.name\"); System.out.println(\"SpringApplicationRunListener...environmentPrepared..\"+o); } @Override public void contextPrepared(ConfigurableApplicationContext context) { System.out.println(\"SpringApplicationRunListener...contextPrepared...\"); } @Override public void contextLoaded(ConfigurableApplicationContext context) { System.out.println(\"SpringApplicationRunListener...contextLoaded...\"); } @Override public void finished(ConfigurableApplicationContext context, Throwable exception) { System.out.println(\"SpringApplicationRunListener...finished...\"); }} 配置(META-INF/spring.factories) 12345org.springframework.context.ApplicationContextInitializer=\\com.atguigu.springboot.listener.HelloApplicationContextInitializerorg.springframework.boot.SpringApplicationRunListener=\\com.atguigu.springboot.listener.HelloSpringApplicationRunListener 只需要放在ioc容器中 ApplicationRunner 1234567@Componentpublic class HelloApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println(\"ApplicationRunner...run....\"); }} CommandLineRunner 1234567@Componentpublic class HelloCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println(\"CommandLineRunner...run...\"+ Arrays.asList(args)); }} 八、自定义starterstarter: 1、这个场景需要使用到的依赖是什么? 2、如何编写自动配置 12345678910111213@Configuration //指定这个类是一个配置类@ConditionalOnXXX //在指定条件成立的情况下自动配置类生效@AutoConfigureAfter //指定自动配置类的顺序@Bean //给容器中添加组件@ConfigurationPropertie结合相关xxxProperties类来绑定相关的配置@EnableConfigurationProperties //让xxxProperties生效加入到容器中自动配置类要能加载将需要启动就加载的自动配置类,配置在META-INF/spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\\ 3、模式: 启动器只用来做依赖导入; 专门来写一个自动配置模块; 启动器依赖自动配置;别人只需要引入启动器(starter) mybatis-spring-boot-starter;自定义启动器名-spring-boot-starter 步骤: 1)、启动器模块 12345678910111213141516171819202122<?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.starter</groupId> <artifactId>atguigu-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> <!--启动器--> <dependencies> <!--引入自动配置模块--> <dependency> <groupId>com.atguigu.starter</groupId> <artifactId>atguigu-spring-boot-starter-autoconfigurer</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies></project> 2)、自动配置模块 123456789101112131415161718192021222324252627282930313233343536373839<?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.starter</groupId> <artifactId>atguigu-spring-boot-starter-autoconfigurer</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>atguigu-spring-boot-starter-autoconfigurer</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.10.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--引入spring-boot-starter;所有starter的基本配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies></project> 1234567891011121314151617181920212223242526package com.atguigu.starter;import org.springframework.boot.context.properties.ConfigurationProperties;@ConfigurationProperties(prefix = \"atguigu.hello\")public class HelloProperties { private String prefix; private String suffix; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; }} 123456789101112131415161718package com.atguigu.starter;public class HelloService { HelloProperties helloProperties; public HelloProperties getHelloProperties() { return helloProperties; } public void setHelloProperties(HelloProperties helloProperties) { this.helloProperties = helloProperties; } public String sayHellAtguigu(String name){ return helloProperties.getPrefix()+\"-\" +name + helloProperties.getSuffix(); }} 12345678910111213141516171819202122package com.atguigu.starter;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration@ConditionalOnWebApplication //web应用才生效@EnableConfigurationProperties(HelloProperties.class)public class HelloServiceAutoConfiguration { @Autowired HelloProperties helloProperties; @Bean public HelloService helloService(){ HelloService service = new HelloService(); service.setHelloProperties(helloProperties); return service; }} 更多SpringBoot整合示例https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples","link":"/2020/05/18/springboot%E6%A0%B8%E5%BF%83/"},{"title":"springboot高级","text":"本文分别从缓存、消息、检索、任务、安全、分布式、热部署和监控管理方面,对spring boot高级部分做了简单总结,内容不深但覆盖全。 (一) Spring Boot与缓存一、 JSR107Java Caching定义了5个核心接口 CachingProvider 定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。 CacheManager 定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。 Cache 一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。 Entry 一个存储在Cache中的key-value对。 Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。 二、 Spring缓存抽象Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发; Cache接口有以下功能: 为缓存的组件规范定义,包含缓存的各种操作集合; Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等; 三、 重要缓存注解及概念 Cache 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 CacheManager 缓存管理器,管理各种缓存(Cache)组件 @Cacheable 根据方法的请求参数对其结果进行缓存 @CacheEvict 清空缓存 @CachePut 更新缓存 @EnableCaching 开启基于注解的缓存 keyGenerator 缓存数据时key生成策略 serialize 缓存数据时value序列化策略 1 . @Cacheable/@CachePut/@CacheEvict 主要的参数 value 缓存名称,字符串/字符数组形式; 如@Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} key 缓存的key,需要按照SpEL表达式编写,如果不指定则按照方法所有参数进行组合; 如@Cacheable(value=”testcache”,key=”#userName”) keyGenerator key的生成器;可以自己指定key的生成器的组件id 注意:key/keyGenerator:二选一使用; condition 缓存条件,使用SpEL编写,在调用方法之前之后都能判断; 如@Cacheable(value=”testcache”,condition=”#userName.length()>2”) unless(@CachePut、@Cacheable) 用于否决缓存的条件,只在方法执行之后判断; 如@Cacheable(value=”testcache”,unless=”#result ==null”) beforeInvocation(@CacheEvict) 是否在执行前清空缓存,默认为false,false情况下方法执行异常则不会清空; 如@CachEvict(value=”testcache”,beforeInvocation=true) allEntries(@CacheEvict) 是否清空所有缓存内容,默认为false; 如@CachEvict(value=”testcache”,allEntries=true) 2 . 缓存可用的SpEL表达式root 表示根对象,不可省略 被调用方法名 methodName 如 #root.methodName 被调用方法 method 如 #root.method.name 目标对象 target 如 #root.target 被调用的目标对象类 targetClass 如 #root.targetClass 被调用的方法的参数列表 args 如 #root.args[0] 方法调用使用的缓存列表 caches 如 #root.caches[0].name 参数名 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引; 如 #iban 、 #a0 、 #p0 返回值 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’ , @CachePut、@CacheEvict’的表达式beforeInvocation=false ) 如 #result 四、 缓存使用1. 基本使用步骤 引入spring-boot-starter-cache模块 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId></dependency> @EnableCaching开启缓存 在主配置类上标注 使用缓存注解 如@Cacheable、@CachePut 切换为其他缓存 2. 搭建实验环境 导入数据库文件 创建出department和employee表 12345678910111213141516171819202122-- ------------------------------ Table structure for department-- ----------------------------DROP TABLE IF EXISTS `department`;CREATE TABLE `department` ( `id` int(11) NOT NULL AUTO_INCREMENT, `departmentName` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Table structure for employee-- ----------------------------DROP TABLE IF EXISTS `employee`;CREATE TABLE `employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `lastName` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `gender` int(2) DEFAULT NULL, `d_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; 创建javaBean封装数据 整合MyBatis操作数据库 配置数据源信息 1234567891011spring.datasource.username=rootspring.datasource.password=123spring.datasource.url=jdbc:mysql://localhost:3306/springboot?serverTimezone=GMTspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver# 开启驼峰命名法(否则部分字段封装不了)mybatis.configuration.map-underscore-to-camel-case=true#打印sqllogging.level.cn.edu.ustc.springboot.mapper=debugdebug=true 使用注解版的MyBatis; @MapperScan指定需要扫描的mapper接口所在的包 主配置类开启@EnableCaching 3. 快速体验缓存@Cacheable、@CachePut、@CacheEvict的使用 123456789101112131415161718192021222324252627282930@Servicepublic class EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Cacheable(value={\"emp\"}, key = \"#id+#root.methodName+#root.caches[0].name\", condition = \"#a0>1\", unless = \"#p0==2\" ) public Employee getEmpById(Integer id) { System.out.println(\"查询员工:\"+id); return employeeMapper.getEmpById(id); } @CachePut(value = {\"emp\"},key = \"#employee.id\" ) public Employee updateEmp(Employee employee) { System.out.println(\"更新员工\"+employee); employeeMapper.updateEmp(employee); return employee; } @CacheEvict(value = {\"emp\"},allEntries = true,beforeInvocation = true) public Integer delEmp(Integer id){ int i=1/0; System.out.println(\"删除员工:\"+id); employeeMapper.delEmp(id); return id; }} 自定义KeyGenerator 使用时在注解属性内指定KeyGenerator=“myKeyGenerator” 123456789101112@Configurationpublic class MyCacheConfig { @Bean(\"myKeyGenerator\") public KeyGenerator myKeyGenerator() { return new KeyGenerator(){ @Override public Object generate(Object target, Method method, Object... params) { return method.getName()+\"[\"+ Arrays.asList(params).toString()+target+\"]\"; } }; }} @CacheConfig 标注在类上,用于抽取@Cacheable的公共属性 由于一个类中可能会使用多次@Cacheable等注解,所以各项属性可以抽取到@CacheConfig @Caching 组合使用@Cacheable、@CachePut、@CacheEvict 123456789101112@Caching( cacheable = { @Cacheable(/*value=\"emp\",*/key = \"#lastName\") }, put = { @CachePut(/*value=\"emp\",*/key = \"#result.id\"), @CachePut(/*value=\"emp\",*/key = \"#result.email\") } ) public Employee getEmpByLastName(String lastName){ return employeeMapper.getEmpByLastName(lastName); } 4. 工作原理缓存的自动配置类CacheAutoConfiguration向容器中导入了CacheConfigurationImportSelector,此类的selectImports()方法添加了许多配置类,其中SimpleCacheConfiguration默认生效 GenericCacheConfiguration JCacheCacheConfiguration EhCacheCacheConfiguration HazelcastCacheConfiguration InfinispanCacheConfiguration CouchbaseCacheConfiguration RedisCacheConfiguration CaffeineCacheConfiguration GuavaCacheConfiguration SimpleCacheConfiguration【默认】 NoOpCacheConfiguration 1234567891011121314151617@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })public class CacheAutoConfiguration { static class CacheConfigurationImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { CacheType[] types = CacheType.values(); String[] imports = new String[types.length]; for (int i = 0; i < types.length; i++) { //将即将导入的各配置类存入字符数组内 imports[i] = CacheConfigurations.getConfigurationClass(types[i]); } return imports; } } } SimpleCacheConfiguration向容器中导入了ConcurrentMapCacheManager 12345678910111213141516@Configuration(proxyBeanMethods = false)@ConditionalOnMissingBean(CacheManager.class)@Conditional(CacheCondition.class)class SimpleCacheConfiguration { //向容器中导入ConcurrentMapCacheManager @Bean ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers) { ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); List<String> cacheNames = cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { cacheManager.setCacheNames(cacheNames); } return cacheManagerCustomizers.customize(cacheManager); }} ConcurrentMapCacheManager使用ConcurrentMap以k-v的方式存储缓存缓存,下面以@Cacheable的运行流程为例说明ConcurrentMapCacheManager的作用。 ==@Cacheable的运行流程== 方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取;(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建,并以cacheNames-cache对放入ConcurrentMap。 去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key; SimpleKeyGenerator生成key的默认策略; 如果没有参数;key=new SimpleKey(); 如果有一个参数:key=参数的值 如果有多个参数:key=new SimpleKey(params); 没有查到缓存就调用目标方法; 将目标方法返回的结果,放进缓存中 @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,如果没有就运行方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据; 核心:1)、使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件2)、key使用keyGenerator生成的,默认是SimpleKeyGenerator 源码分析 默认使用ConcurrentMapCacheManager管理缓存,该类使用ConcurrentMap保存缓存,获取缓存如果没有Cache组件会自动创建,并以cacheNames-cache对放入ConcurrentMap。 12345678910111213141516171819202122public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware { private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(); private boolean dynamic = true; //获取缓存 public Cache getCache(String name) { Cache cache = this.cacheMap.get(name); //如果没有缓存会自动创建 if (cache == null && this.dynamic) { synchronized (this.cacheMap) { cache = this.cacheMap.get(name); if (cache == null) { cache = createConcurrentMapCache(name); this.cacheMap.put(name, cache); } } } return cache; }} 在@Cacheable标注方法执行前执行CacheAspectSupport的execute()方法,在该方法中会以一定的规则生成key,并尝试在缓存中通过该key获取值,若通过key获取到值则直接返回,不用执行@Cacheable标注方法,否则执行该方法获得返回值。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { //在执行@Cacheable标注的方法前执行此方法 @Nullable private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { if (contexts.isSynchronized()) { CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); Cache cache = context.getCaches().iterator().next(); try { return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker)))); } catch (Cache.ValueRetrievalException ex) { throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause(); } } else { return invokeOperation(invoker); } } processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); // 见findCachedItem方法 //此方法通过一定规则生成的key找cache,若没找到则返回null Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); List<CachePutRequest> cachePutRequests = new LinkedList<>(); if (cacheHit == null) { collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); } Object cacheValue; Object returnValue; if (cacheHit != null && !hasCachePut(contexts)) { // 如果通过该key找到缓存,且无@cacheput,则直接返回cacheValue cacheValue = cacheHit.get(); returnValue = wrapCacheValue(method, cacheValue); } else { // 若通过该key未找到缓存,则执行@cacheable标注方法 returnValue = invokeOperation(invoker); cacheValue = unwrapReturnValue(returnValue); } // Collect any explicit @CachePuts collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); // Process any collected put requests, either from @CachePut or a @Cacheable miss for (CachePutRequest cachePutRequest : cachePutRequests) { cachePutRequest.apply(cacheValue); } // Process any late evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); return returnValue; } @Nullable private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) { Object result = CacheOperationExpressionEvaluator.NO_RESULT; for (CacheOperationContext context : contexts) { if (isConditionPassing(context, result)) { //通过一定规则生成key值(生成规则见generateKey方法) Object key = generateKey(context, result); //通过生成的key寻找缓存 Cache.ValueWrapper cached = findInCaches(context, key); if (cached != null) { return cached; } else { if (logger.isTraceEnabled()) { logger.trace(\"No cache entry for key '\" + key + \"' in cache(s) \" + context.getCacheNames()); } } } } return null; } //key的生成策略 @Nullable protected Object generateKey(@Nullable Object result) { //如果@Cacheable设置了属性key,则根据设置值生成key if (StringUtils.hasText(this.metadata.operation.getKey())) { EvaluationContext evaluationContext = createEvaluationContext(result); return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); } //否则使用keyGenerator生成key,默认keyGenerator为SimpleKeyGenerator return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); } 默认情况下使用SimpleKeyGenerator生成key 123456789101112131415161718public class SimpleKeyGenerator implements KeyGenerator { //SimpleKeyGenerator的生成规则 public static Object generateKey(Object... params) { //若无参,则返回空key if (params.length == 0) { return SimpleKey.EMPTY; } if (params.length == 1) { Object param = params[0]; if (param != null && !param.getClass().isArray()) { //1个参数,则直接返回该参数 return param; } } //多个参数返回数组 return new SimpleKey(params); }} 默认的缓存类ConcurrentMapCache,使用ConcurrentMap存储k-v 1234567891011121314151617public class ConcurrentMapCache extends AbstractValueAdaptingCache { private final String name; //存储key-cacheValue private final ConcurrentMap<Object, Object> store; //通过key查找cacheValue protected Object lookup(Object key) { return this.store.get(key); } //方法调用完后将结果存入缓存中 public void put(Object key, @Nullable Object value) { this.store.put(key, toStoreValue(value)); }} 五、Redis与缓存1. 环境搭建导入依赖 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 在spring.properties指定Redis服务器地址 12#redis服务器主机地址spring.redis.host=192.168.31.162 2. RedisTemplateRedisAutoConfiguration向容器中导入了两个类RedisTemplate<Object, Object> redisTemplate和StringRedisTemplate,作为Redis客户端分别操作k-v都为对象和k-v都为字符串的值 Redis常见的五大数据类型 String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合) stringRedisTemplate.opsForValue()[String(字符串)] stringRedisTemplate.opsForList()[List(列表)] stringRedisTemplate.opsForSet()[Set(集合)] stringRedisTemplate.opsForHash()[Hash(散列)] stringRedisTemplate.opsForZSet()[ZSet(有序集合)] 3. Redis缓存使用在导入redis依赖后RedisCacheConfiguration类就会自动生效,创建RedisCacheManager,并使用RedisCache进行缓存数据,要缓存的对象的类要实现Serializable接口,默认情况下是以jdk序列化数据存在redis中,如下: 123k:\"emp::1\"v:\\xAC\\xED\\x00\\x05sr\\x00$cn.edu.ustc.springboot.bean.Employeeuqf\\x03p\\x9A\\xCF\\xE0\\x02\\x00\\x05L\\x00\\x03dIdt\\x00\\x13Ljava/lang/Integer;L\\x00\\x05emailt\\x00\\x12Ljava/lang/String;L\\x00\\x06genderq\\x00~\\x00\\x01L\\x00\\x02idq\\x00~\\x00\\x01L\\x00\\x08lastNameq\\x00~\\x00\\x02xpsr\\x00\\x11java.lang.Integer\\x12\\xE2\\xA0\\xA4\\xF7\\x81\\x878\\x02\\x00\\x01I\\x00\\x05valuexr\\x00\\x10java.lang.Number\\x86\\xAC\\x95\\x1D\\x0B\\x94\\xE0\\x8B\\x02\\x00\\x00xp\\x00\\x00\\x00\\x03t\\x00\\x07cch@aaasq\\x00~\\x00\\x04\\x00\\x00\\x00\\x01q\\x00~\\x00\\x08t\\x00\\x03cch 要想让对象以json形式存储在redis中,需要自定义RedisCacheManager,使用GenericJackson2JsonRedisSerializer类对value进行序列化 1234567891011121314151617181920@Configurationpublic class MyRedisConfig { @Bean RedisCacheManager cacheManager(RedisConnectionFactory factory){ //创建默认RedisCacheWriter RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(factory); //创建默认RedisCacheConfiguration并使用GenericJackson2JsonRedisSerializer构造的 SerializationPair对value进行转换 //创建GenericJackson2JsonRedisSerializer的json序列化器 GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); //使用json序列化器构造出对转换Object类型的SerializationPair序列化对 RedisSerializationContext.SerializationPair<Object> serializationPair = RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer); //将可以把Object转换为json的SerializationPair传入RedisCacheConfiguration //使得RedisCacheConfiguration在转换value时使用定制序列化器 RedisCacheConfiguration cacheConfiguration=RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(serializationPair); RedisCacheManager cacheManager = new RedisCacheManager(cacheWriter,cacheConfiguration); return cacheManager; }} 序列化数据如下: 1234567891011k:\"emp::3\"v:{ \"@class\": \"cn.edu.ustc.springboot.bean.Employee\", \"id\": 3, \"lastName\": \"aaa\", \"email\": \"aaaa\", \"gender\": 1, \"dId\": 5} 注意,这里必须用GenericJackson2JsonRedisSerializer进行value的序列化解析,如果使用Jackson2JsonRedisSerializer,序列化的json没有"@class": "cn.edu.ustc.springboot.bean.Employee",在读取缓存时会报类型转换异常。 4. Redis缓存原理配置类RedisCacheConfiguration向容器中导入了其定制的RedisCacheManager,在默认的RedisCacheManager的配置中,是使用jdk序列化value值 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263@Configuration(proxyBeanMethods = false)@ConditionalOnClass(RedisConnectionFactory.class)@AutoConfigureAfter(RedisAutoConfiguration.class)@ConditionalOnBean(RedisConnectionFactory.class)@ConditionalOnMissingBean(CacheManager.class)@Conditional(CacheCondition.class)class RedisCacheConfiguration { //向容器中导入RedisCacheManager @Bean RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers, RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) { //使用determineConfiguration()的返回值生成RedisCacheManagerBuilder //调用了RedisCacheManagerBuilder的cacheDefaults()方法(见下一代码块) RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults( determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader())); List<String> cacheNames = cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { builder.initialCacheNames(new LinkedHashSet<>(cacheNames)); } redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); //使用RedisCacheManagerBuilder的build()方法创建RedisCacheManager并进行定制操作 return cacheManagerCustomizers.customize(builder.build()); } private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration( CacheProperties cacheProperties, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ClassLoader classLoader) { //determineConfiguration()调用了createConfiguration() return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader)); } //createConfiguration()定义了其序列化value的规则 private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration( CacheProperties cacheProperties, ClassLoader classLoader) { Redis redisProperties = cacheProperties.getRedis(); org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration .defaultCacheConfig(); //使用jdk序列化器对value进行序列化 config = config.serializeValuesWith( SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader))); //设置properties文件中设置的各项属性 if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; }} RedisCacheManager的直接构造类,该类保存了配置类RedisCacheConfiguration,该配置在会传递给RedisCacheManager 123456789101112131415161718192021222324252627282930313233343536public static class RedisCacheManagerBuilder { private final RedisCacheWriter cacheWriter; //默认缓存配置使用RedisCacheConfiguration的默认配置 //该默认配置缓存时默认将k按字符串存储,v按jdk序列化数据存储(见下一代码块) private RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); private final Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>(); private boolean enableTransactions; boolean allowInFlightCacheCreation = true; private RedisCacheManagerBuilder(RedisCacheWriter cacheWriter) { this.cacheWriter = cacheWriter; } //传入RedisCacheManagerBuilder使用的缓存配置规则RedisCacheConfiguration类 public RedisCacheManagerBuilder cacheDefaults(RedisCacheConfiguration defaultCacheConfiguration) { Assert.notNull(defaultCacheConfiguration, \"DefaultCacheConfiguration must not be null!\"); this.defaultCacheConfiguration = defaultCacheConfiguration; return this; } //使用默认defaultCacheConfiguration创建RedisCacheManager public RedisCacheManager build() { RedisCacheManager cm = new RedisCacheManager(cacheWriter, defaultCacheConfiguration, initialCaches, allowInFlightCacheCreation); cm.setTransactionAware(enableTransactions); return cm; } RedisCacheConfiguration保存了许多缓存规则,这些规则都保存在RedisCacheManagerBuilder的RedisCacheConfiguration defaultCacheConfiguration属性中,并且当RedisCacheManagerBuilder创建RedisCacheManager传递过去 123456789101112131415161718192021222324public class RedisCacheConfiguration { private final Duration ttl; private final boolean cacheNullValues; private final CacheKeyPrefix keyPrefix; private final boolean usePrefix; private final SerializationPair<String> keySerializationPair; private final SerializationPair<Object> valueSerializationPair; private final ConversionService conversionService; //默认缓存配置 public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); registerDefaultConverters(conversionService); return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), SerializationPair.fromSerializer(RedisSerializer.string()),//key使用字符串 SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService); //value按jdk序列化存储 } RedisCacheManager在创建RedisCache时将RedisCacheConfiguration传递过去,并在创建RedisCache时通过createRedisCache()起作用 1234567891011public class RedisCacheManager extends AbstractTransactionSupportingCacheManager { private final RedisCacheWriter cacheWriter; private final RedisCacheConfiguration defaultCacheConfig; private final Map<String, RedisCacheConfiguration> initialCacheConfiguration; private final boolean allowInFlightCacheCreation; protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) { //如果调用该方法时RedisCacheConfiguration有值则使用定制的,否则则使用默认的RedisCacheConfiguration defaultCacheConfig,即RedisCacheManagerBuilder传递过来的配置 return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig); } RedisCache,Redis缓存,具体负责将缓存数据序列化的地方,将RedisCacheConfiguration的序列化对SerializationPair提取出来并使用其定义的序列化方式分别对k和v进行序列化操作 123456789101112131415161718192021222324252627282930313233343536373839public class RedisCache extends AbstractValueAdaptingCache { private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); private final String name; private final RedisCacheWriter cacheWriter; private final RedisCacheConfiguration cacheConfig; private final ConversionService conversionService; public void put(Object key, @Nullable Object value) { Object cacheValue = preProcessCacheValue(value); if (!isAllowNullValues() && cacheValue == null) { throw new IllegalArgumentException(String.format( \"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\\\"#result == null\\\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.\", name)); } //在put k-v时使用cacheConfig中的k-v序列化器分别对k-v进行序列化 cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl()); } //从cacheConfig(即RedisCacheConfiguration)中获取KeySerializationPair并写入key值 protected byte[] serializeCacheKey(String cacheKey) { return ByteUtils.getBytes(cacheConfig.getKeySerializationPair().write(cacheKey)); } //从cacheConfig(即RedisCacheConfiguration)中获取ValueSerializationPair并写入key值 protected byte[] serializeCacheValue(Object value) { if (isAllowNullValues() && value instanceof NullValue) { return BINARY_NULL_VALUE; } return ByteUtils.getBytes(cacheConfig.getValueSerializationPair().write(value)); } 分析到这也就不难理解,要使用json保存序列化数据时,需要自定义RedisCacheManager,在RedisCacheConfiguration中定义序列化转化规则,并向RedisCacheManager传入我们自己定制的RedisCacheConfiguration了,我定制的序列化规则会跟随RedisCacheConfiguration一直传递到RedisCache,并在序列化时发挥作用。 (二) Spring Boot与消息一、消息简介消息代理规范 JMS(Java Message Service)JAVA消息服务 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现 AMQP(Advanced Message Queuing Protocol) 高级消息队列协议,也是一个消息代理的规范,兼容JMS RabbitMQ是AMQP的实现 作用 通过消息服务中间件来提升系统异步通信、扩展解耦能力 当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地 应用场景 异步处理 用户注册操作和消息处理并行,提高响应速度 应用解耦 在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦 流量削峰 用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面 秒杀业务根据消息队列中的请求信息,再做后续处理 二、RabbitMQRabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。 1. 核心概念 Message 消息,消息是不具名的,它由消息头和消息体组成 消息头,包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等 Publisher 消息的生产者,也是一个向交换器发布消息的客户端应用程序 Exchange 交换器,将生产者消息路由给服务器中的队列 类型有direct(默认),fanout, topic, 和headers,具有不同转发策略 Queue 消息队列,保存消息直到发送给消费者 Binding 绑定,用于消息队列和交换器之间的关联 Connection 网络连接,比如一个TCP连接 Consumer 消息的消费者,表示一个从消息队列中取得消息的客户端应用程序 Virtual Host 虚拟主机,表示一批交换器、消息队列和相关对象。 vhost 是 AMQP 概念的基础,必须在连接时指定 RabbitMQ 默认的 vhost 是 / Broker 消息队列服务器实体 2. 运行机制消息路由 AMQP 中增加了Exchange 和 Binding 的角色, Binding 决定交换器的消息应该发送到那个队列 Exchange 类型 direct 点对点模式,消息中的路由键(routing key)如果和 Binding 中的 bindingkey 一致, 交换器就将消息发到对应的队列中。 fanout 广播模式,每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去 topic 将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。识别通配符: # 匹配 0 个或多个单词, *匹配一个单词 三、 Springboot中的RabbitMQ1. 环境准备在docker中安装rabbitmq并运行 12# 5672为服务端口,15672为web控制台端口docker run -d -p 5672:5672 -p 15672:15672 38e57f281891 导入依赖 123456789 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency><!--自定义消息转化器Jackson2JsonMessageConverter所需依赖--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> 配置文件 1234# 指定rebbitmq服务器主机spring.rabbitmq.host=192.168.31.162#spring.rabbitmq.username=guest 默认值为guest#spring.rabbitmq.password=guest 默认值为guest 2. RabbitMQ的使用RabbitAutoConfiguration中有内部类RabbitTemplateConfiguration,在该类中向容器中分别导入了RabbitTemplate和AmqpAdmin 在测试类中分别注入 12345@Autowired private RabbitTemplate rabbitTemplate; @Autowired private AmqpAdmin amqpAdmin; RabbitTemplate消息发送处理组件 可用来发送和接收消息 123456789101112//发送消息rabbitTemplate.convertAndSend(\"amq.direct\",\"ustc\",\"aaaa\"); Book book = new Book(); book.setName(\"西游记\"); book.setPrice(23.2f);//Book要实现Serializable接口 rabbitTemplate.convertAndSend(\"amq.direct\",\"ustc\",book);//接收消息Object o = rabbitTemplate.receiveAndConvert(\"ustc\"); System.out.println(o.getClass()); //class cn.edu.ustc.springboot.bean.Book System.out.println(o); //Book{name='西游记', price=23.2} 默认的消息转化器是SimpleMessageConverter,对于对象以jdk序列化方式存储,若要以Json方式存储对象,就要自定义消息转换器 12345678@Configurationpublic class AmqpConfig { @Bean public MessageConverter messageConverter() { //在容器中导入Json的消息转换器 return new Jackson2JsonMessageConverter(); }} AmqpAdmin管理组件 可用于创建和删除exchange、binding和queue 123456//创建Direct类型的ExchangeamqpAdmin.declareExchange(new DirectExchange(\"admin.direct\"));//创建QueueamqpAdmin.declareQueue(new Queue(\"admin.test\"));//将创建的队列与Exchange绑定amqpAdmin.declareBinding(new Binding(\"admin.test\", Binding.DestinationType.QUEUE,\"admin.direct\",\"admin.test\",null)); 消息的监听 在回调方法上标注@RabbitListener注解,并设置其属性queues,注册监听队列,当该队列收到消息时,标注方法遍会调用 可分别使用Message和保存消息所属对象进行消息接收,若使用Object对象进行消息接收,实际上接收到的也是Message 123456789101112131415161718@Servicepublic class BookService { @RabbitListener(queues = {\"admin.test\"}) public void receive1(Book book){ System.out.println(\"收到消息:\"+book); } @RabbitListener(queues = {\"admin.test\"}) public void receive1(Object object){ System.out.println(\"收到消息:\"+object.getClass()); //收到消息:class org.springframework.amqp.core.Message } @RabbitListener(queues = {\"admin.test\"}) public void receive2(Message message){ System.out.println(\"收到消息\"+message.getHeaders()+\"---\"+message.getPayload()); }} (三) Spring boot与检索一、 ElasticSearch入门1. ES的简介简介 我们的应用经常需要添加检索功能,开源的 ElasticSearch 是目前全文搜索引擎的首选。他可以快速的存储、搜索和分析海量数据。Spring Boot通过整合Spring Data ElasticSearch为我们提供了非常便捷的检索功能支持;Elasticsearch是一个分布式搜索服务,提供Restful API,底层基于Lucene,采用多shard(分片)的方式保证数据安全,并且提供自动resharding的功能,github等大型的站点也是采用了ElasticSearch作为其搜索服务。 概念 员工文档 的形式存储为例:一个文档代表一个员工数据。存储数据到ElasticSearch 的行为叫做 索引 ,但在索引一个文档之前,需要确定将文档存储在哪里。 一个 ElasticSearch 集群可以包含多个 索引 ,相应的每个索引可以包含多个 类型 。 这些不同的类型存储着多个 文档 ,每个文档又有 多个 属性 。 1234567索引(名词):如前所述,一个 *索引* 类似于传统关系数据库中的一个 *数据库* ,是一个存储关系型文档的地方。 *索引* (*index*) 的复数词为 *indices* 或 *indexes* 。索引(动词):*索引一个文档* 就是存储一个文档到一个 *索引* (名词)中以便被检索和查询。这非常类似于 SQL 语句中的 `INSERT` 关键词,除了文档已存在时,新文档会替换旧文档情况之外。 类似关系: 1234- 索引---数据库- 类型---表- 文档---表中的记录- 属性---列 2. ES的安装与运行与ES交互 9200端口 RESTful API通过HTTP通信 9300端口 Java客户端与ES的原生传输协议和集群交互 1234# 拉取ES镜像docker pull elasticsearch:7.6.1#运行ESdocker run -e \"discovery.type=single-node\" -e ES_JAVA_OPTS=\"-Xms256m -Xmx256m\" -d -p 9200:9200 -p 9300:9300 --name ES03 41072cdeebc5 ES_JAVA_OPTS指定java虚拟机相关参数 -Xms256m 初始堆内存大小为256m -Xmx256m 最大堆内存大小为256m discovery.type=single-node 设置为单点启动 3. ES的基础入门案例:创建一个员工目录,并支持各类型检索 索引员工文档 对于员工目录,我们将做如下操作: 每个员工索引一个文档,文档包含该员工的所有信息。 每个文档都将是 employee 类型 。 该类型位于 索引 megacorp 内。 12345678PUT /megacorp/employee/1{ "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ]} 注意,路径 /megacorp/employee/1 包含了三部分的信息: megacorp 索引名称 employee 类型名称 1 特定雇员的ID 请求体 —— JSON 文档 —— 包含了这位员工的所有详细信息 1234567891011121314{ \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"1\", \"_version\": 1, \"result\": \"created\", \"_shards\": { \"total\": 2, \"successful\": 1, \"failed\": 0 }, \"_seq_no\": 0, \"_primary_term\": 1} 同理,添加更多员工 1234567891011121314151617PUT /megacorp/employee/2{ "first_name" : "Jane", "last_name" : "Smith", "age" : 32, "about" : "I like to collect rock albums", "interests": [ "music" ]}PUT /megacorp/employee/3{ "first_name" : "Douglas", "last_name" : "Fir", "age" : 35, "about": "I like to build cabinets", "interests": [ "forestry" ]} 检索文档 HTTP GET 请求并指定文档的地址——索引库、类型和ID。 1GET /megacorp/employee/1 返回数据 12345678910111213141516171819{ \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"1\", \"_version\": 1, \"_seq_no\": 0, \"_primary_term\": 1, \"found\": true, \"_source\": { \"first_name\": \"John\", \"last_name\": \"Smith\", \"age\": 25, \"about\": \"I love to go rock climbing\", \"interests\": [ \"sports\", \"music\" ] }} 将 HTTP 命令由 PUT 改为 GET 可以用来检索文档,同样的,可以使用 DELETE 命令来删除文档,以及使用 HEAD 指令来检查文档是否存在。如果想更新已存在的文档,只需再次 PUT 。 轻量搜索 搜索所有雇员: 1GET /megacorp/employee/_search 返回数据 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465{ \"took\": 46, \"timed_out\": false, \"_shards\": { \"total\": 1, \"successful\": 1, \"skipped\": 0, \"failed\": 0 }, \"hits\": { \"total\": { \"value\": 3, \"relation\": \"eq\" }, \"max_score\": 1, \"hits\": [ { \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"1\", \"_score\": 1, \"_source\": { \"first_name\": \"John\", \"last_name\": \"Smith\", \"age\": 25, \"about\": \"I love to go rock climbing\", \"interests\": [ \"sports\", \"music\" ] } }, { \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"2\", \"_score\": 1, \"_source\": { \"first_name\": \"Jane\", \"last_name\": \"Smith\", \"age\": 32, \"about\": \"I like to collect rock albums\", \"interests\": [ \"music\" ] } }, { \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"3\", \"_score\": 1, \"_source\": { \"first_name\": \"Douglas\", \"last_name\": \"Fir\", \"age\": 35, \"about\": \"I like to build cabinets\", \"interests\": [ \"forestry\" ] } } ] }} 返回结果包括三个文档,放在数据hits中。 搜索姓氏为 Smith 的雇员 1GET /megacorp/employee/_search?q=last_name:Smith 在请求路径中使用 _search 端点,并将查询本身赋值给参数 q= 。返回结果给出了所有的 Smith: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950{ \"took\": 23, \"timed_out\": false, \"_shards\": { \"total\": 1, \"successful\": 1, \"skipped\": 0, \"failed\": 0 }, \"hits\": { \"total\": { \"value\": 2, \"relation\": \"eq\" }, \"max_score\": 0.4700036, \"hits\": [ { \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"1\", \"_score\": 0.4700036, \"_source\": { \"first_name\": \"John\", \"last_name\": \"Smith\", \"age\": 25, \"about\": \"I love to go rock climbing\", \"interests\": [ \"sports\", \"music\" ] } }, { \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"2\", \"_score\": 0.4700036, \"_source\": { \"first_name\": \"Jane\", \"last_name\": \"Smith\", \"age\": 32, \"about\": \"I like to collect rock albums\", \"interests\": [ \"music\" ] } } ] }} 使用查询表达式搜索 Query-string 搜索通过命令非常方便地进行临时性的即席搜索 ,但它有自身的局限性。Elasticsearch 提供一个丰富灵活的查询语言叫做 查询表达式 , 它支持构建更加复杂和健壮的查询。 12345678GET /megacorp/employee/_search{ "query" : { "match" : { "last_name" : "Smith" } }} 返回效果与之前一样 更复杂的搜索 同样搜索姓氏为 Smith 的员工,但这次我们只需要年龄大于 30 的 1234567891011121314151617GET /megacorp/employee/_search{ "query" : { "bool": { "must": { "match" : { "last_name" : "smith" } }, "filter": { "range" : { "age" : { "gt" : 30 } } } } }} 全文搜索 搜索下所有喜欢攀岩(rock climbing)的员工: 12345678GET /megacorp/employee/_search{ "query" : { "match" : { "about" : "rock climbing" } }} 返回结果 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950{ \"took\": 13, \"timed_out\": false, \"_shards\": { \"total\": 1, \"successful\": 1, \"skipped\": 0, \"failed\": 0 }, \"hits\": { \"total\": { \"value\": 2, \"relation\": \"eq\" }, \"max_score\": 1.4167401, \"hits\": [ { \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"1\", \"_score\": 1.4167401, \"_source\": { \"first_name\": \"John\", \"last_name\": \"Smith\", \"age\": 25, \"about\": \"I love to go rock climbing\", \"interests\": [ \"sports\", \"music\" ] } }, { \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"2\", \"_score\": 0.4589591, \"_source\": { \"first_name\": \"Jane\", \"last_name\": \"Smith\", \"age\": 32, \"about\": \"I like to collect rock albums\", \"interests\": [ \"music\" ] } } ] }} 可以看到返回结果还带有相关性得分_score 短语搜索 精确匹配一系列单词或者短语 。 比如, 执行这样一个查询,短语 “rock climbing” 的形式紧挨着的雇员记录。 为此对 match 查询稍作调整,使用一个叫做 match_phrase 的查询 12345678GET /megacorp/employee/_search{ "query" : { "match_phrase" : { "about" : "rock climbing" } }} 高亮搜索 每个搜索结果中 高亮 部分文本片段 再次执行前面的查询,并增加一个新的 highlight 参数: 12345678910111213GET /megacorp/employee/_search{ "query" : { "match_phrase" : { "about" : "rock climbing" } }, "highlight": { "fields" : { "about" : {} } }} 返回结果 12345678910111213141516171819202122232425262728{ ... \"hits\": [ { \"_index\": \"megacorp\", \"_type\": \"employee\", \"_id\": \"1\", \"_score\": 1.4167401, \"_source\": { \"first_name\": \"John\", \"last_name\": \"Smith\", \"age\": 25, \"about\": \"I love to go rock climbing\", \"interests\": [ \"sports\", \"music\" ] }, \"highlight\": { \"about\": [ \"I love to go <em>rock</em> <em>climbing</em>\" ] } } ] }} 结果中还多了一个叫做 highlight 的部分。这个部分包含了 about 属性匹配的文本片段,并以 HTML 标签 <em> 封装 二、 Springboot整合ElasticSearch1. 概述SpringBoot默认支持两种技术来和ES交互; Jest(默认不生效) 需要导入jest的工具包(io.searchbox.client.JestClient) 从springboot 2.2.0以后被弃用 SpringData ElasticSearch 版本适配说明 Spring Data Elasticsearch Elasticsearch 3.2.x 6.8.1 3.1.x 6.2.2 3.0.x 5.5.0 2.1.x 2.4.0 2.0.x 2.2.0 1.3.x 1.5.2 Springboot 2.2.6对应于 Spring Data Elasticsearch 3.2.6,即适配Elasticsearch 6.8.1 2. 环境搭建编写文件对应Javabean,指定索引名和类型 123456789101112131415161718192021222324252627282930313233343536373839@Document(indexName = \"ustc\",type = \"book\")public class Book { private Integer id; private String bookName; private String author; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getBookName() { return bookName; } public void setBookName(String bookName) { this.bookName = bookName; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } @Override public String toString() { return \"Book{\" + \"id=\" + id + \", bookName='\" + bookName + '\\'' + \", author='\" + author + '\\'' + '}'; }} 3. ElasticSearch客户端 Transport Client 在ES7中已经被弃用,将在ES8被移除 High Level REST Client ES的默认客户端 Reactive Client 非官方驱动,基于WebClient 下面以REST客户端为例说明ES的使用 配置主机地址 方式一 配置类配置 注意:这种方式底层依赖于Http相关类,因此要导入web相关jar包 123456789101112@Configurationstatic class Config { @Bean RestHighLevelClient client() { ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo(\"localhost:9200\") .build(); return RestClients.create(clientConfiguration).rest(); }} 方式二 spring配置文件指定 1spring.elasticsearch.rest.uris=http://192.168.31.162:9200 在测试类中注入客户端 12@AutowiredRestHighLevelClient highLevelClient; 创建索引 123456IndexRequest request = new IndexRequest(\"ustc\", \"book\", UUID.randomUUID().toString()) .source(Collections.singletonMap(\"feature\", \"high-level-rest-client\")) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);IndexResponse index = highLevelClient.index(request, RequestOptions.DEFAULT);System.out.println(index.toString()); 下面为创建索引 123456789{ \"_index\": \"ustc\", \"_type\": \"book\", \"_id\": \"0dc9f47a-7913-481d-a36d-e8f034a6a3ac\", \"_score\": 1, \"_source\": { \"feature\": \"high-level-rest-client\" }} 得到索引 1234//分别指定要获取的索引、类型、idGetRequest getRequest = new GetRequest(\"ustc\",\"book\",\"0dc9f47a-7913-481d-a36d-e8f034a6a3ac\");GetResponse documentFields = highLevelClient.get(getRequest, RequestOptions.DEFAULT);System.out.println(documentFields); 4. ElasticsearchRestTemplateES有两个模板,分别为ElasticsearchRestTemplate和ElasticsearchTemplate 分别对应于High Level REST Client和Transport Client(弃用),两个模板都实现了ElasticsearchOperations接口,因此使用时我们一般使用ElasticsearchOperations,具体实现方式由底层决定。 由于在AbstractElasticsearchConfiguration中已经向容器中导入了ElasticsearchRestTemplate,因此我们使用时可以直接注入 注入模板 12@AutowiredElasticsearchOperations elasticsearchOperations; 保存索引 123456789Book book = new Book();book.setAuthor(\"路遥\");book.setBookName(\"平凡的世界\");book.setId(1);IndexQuery indexQuery = new IndexQueryBuilder() .withId(book.getId().toString()) .withObject(book) .build();String index = elasticsearchOperations.index(indexQuery); 查询索引 1Book book = elasticsearchOperations.queryForObject(GetQuery.getById(\"1\"), Book.class); 5. Elasticsearch Repositories编写相关Repository并继承Repository或ElasticsearchRepository,泛型分别为<查询类,主键> 123public interface BookRepository extends Repository<Book,Integer> { List<Book> findByBookNameAndAuthor(String bookName, String author);} 查询的方法仅需按照一定规则命名即可实现功能,无需编写实现,如上findByBookNameAndAuthor()方法相当于ES的json查询 12345678910{ \"query\": { \"bool\" : { \"must\" : [ { \"query_string\" : { \"query\" : \"?\", \"fields\" : [ \"bookName\" ] } }, { \"query_string\" : { \"query\" : \"?\", \"fields\" : [ \"author\" ] } } ] } }} 更多命名规则见本文档 @Query 此外,还可以使用@Query自定义请求json 1234interface BookRepository extends ElasticsearchRepository<Book, String> { @Query(\"{\\\"match\\\": {\\\"name\\\": {\\\"query\\\": \\\"?0\\\"}}}\") Page<Book> findByName(String name,Pageable pageable);} 若参数为John,相当于请求体为 123456789{ \"query\": { \"match\": { \"name\": { \"query\": \"John\" } } }} 更多ES与springboot整合内容见官方文档 (四) Spring boot与任务一、异步任务在Java应用中,绝大多数情况下都是通过同步的方式来实现交互处理的;但是在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务,springboot中可以用异步任务解决。 两个注解: @Async 在需要异步执行的方法上标注注解 @EnableAsync 在主类上标注开启异步任务支持 开启异步任务后,当controller层调用该方法会直接返回结果,该任务异步执行 123456789101112@Servicepublic class AsyncService { @Async public void sayHello() { try { Thread.sleep(3000); System.out.println(\"hello async task!\"); } catch (InterruptedException e) { e.printStackTrace(); } }} 二、 定时任务项目开发中经常需要执行一些定时任务,比如需要在每天凌晨时候,分析一次前一天的日志信息。Spring为我们提供了异步执行任务调度的方式,提供TaskExecutor 、TaskScheduler 接口。 两个注解: @EnableScheduling 标注在主类,开启对定时任务支持 @Scheduled 标注在执行的方法上,并制定cron属性 1234567@Servicepublic class ScheduleService { @Scheduled(cron = \"0,1,2,3,4,5,30,50 * * * * 0-7\") public void schedule() { System.out.println(\"I am executing..\"); }} cron表达式: second(秒), minute(分), hour(时), day of month(日), month(月), day of week(周几). 0 0/5 14,18 * * ? 每天14点整,和18点整,每隔5分钟执行一次 0 15 10 ? * 1-6 每个月的周一至周六10:15分执行一次 0 0 2 ? * 6L 每个月的最后一个周六凌晨2点执行一次 0 0 2 LW * ? 每个月的最后一个工作日凌晨2点执行一次 0 0 2-4 ? * 1#1 每个月的第一个周一凌晨2点到4点期间,每个整点都执行一次; 字段 允许值 允许的特殊字符 秒 0-59 , - * / 分 0-59 , - * / 小时 0-23 , - * / 日期 1-31 , - * ? / L W C 月份 1-12 , - * / 星期 0-7或SUN-SAT 0,7是SUN , - * ? / L C # 特殊字符 代表含义 , 枚举 - 区间 * 任意 / 步长 ? 日/星期冲突匹配 L 最后 W 工作日 C 和calendar联系后计算过的值 # 星期,4#2,第2个星期四 三、 邮件任务springboot自动配置包中MailSenderAutoConfiguration通过@Import注解向容器中导入了MailSenderJndiConfiguration,而MailSenderJndiConfiguration向容器中导入了JavaMailSenderImpl类,我们可以使用该类发送邮件 配置文件 123spring.mail.username=邮箱用户名spring.mail.password=邮箱密码或授权码spring.mail.host=smtp.example.com 自动注入 12@Autowiredprivate JavaMailSenderImpl javaMailSender; 简单邮件发送 123456789SimpleMailMessage message = new SimpleMailMessage();//设置主题和内容message.setSubject(\"今天开会\");message.setText(\"物质楼555开会,不要迟到\");//设置发送方和接收方message.setFrom(\"xxx@163.com\");message.setTo(\"xxx@qq.com\");javaMailSender.send(message); 复杂邮件发送 带有附件或html页面的邮件 两个设置 new MimeMessageHelper(message,true) 设置multipart=true,开启对内联元素和附件的支持 helper.setText("xxxx",true) html=ture,设置content type=text/html,默认为text/plain 12345678910111213141516MimeMessage message = javaMailSender.createMimeMessage();//multipart=true//开启对内联元素和附件的支持MimeMessageHelper helper = new MimeMessageHelper(message,true);helper.setSubject(\"今天开会\");//html=ture//设置content type=text/html,默认为text/plainhelper.setText(\"<b style='color:red'>物质楼555开会,不要迟到</b>\",true);helper.setFrom(\"hongshengmo@163.com\");helper.setTo(\"1043245239@qq.com\");//设置附件helper.addAttachment(\"2.png\",new File(\"D:\\\\Works\\\\Note\\\\images\\\\图片2.png\"));helper.addAttachment(\"3.png\",new File(\"D:\\\\Works\\\\Note\\\\images\\\\图片3.png\"));javaMailSender.send(message); (五) Spring boot与安全一、安全 应用程序的两个主要区域是“认证”和“授权”(或者访问控制),这两个主要区域是安全的两个目标。 身份验证意味着确认您自己的身份,而授权意味着授予对系统的访问权限 认证 身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。系统确定您是否就是您所说的使用凭据。在公共和专用网络中,系统通过登录密码验证用户身份。身份验证通常通过用户名和密码完成, 授权 另一方面,授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。简单来说,授权决定了您访问系统的能力以及达到的程度。验证成功后,系统验证您的身份后,即可授权您访问系统资源。 二、Spring SecuritySpring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型。他可以实现强大的web安全控制。对于安全控制,我们仅需引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理。 WebSecurityConfigurerAdapter:自定义Security策略 通过在配置类中继承该类重写configure(HttpSecurity http)方法来实现自定义策略 @EnableWebSecurity:开启WebSecurity模式 在配置类上标注@EnableWebSecurity开启WebSecurity模式 三、 Springboot整合security1. 导入依赖123456789101112<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> 导入spring security的包之后,默认情况所有应用访问认证授权,默认用户名user,密码为随机生成的uuid,启动时打印在控制台 2. 登录/注销1234567891011121314151617181920@EnableWebSecuritypublic class MySecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { //根目录允许所有人访问,其他目录都需要对应角色 http.authorizeRequests().antMatchers(\"/\").permitAll() .antMatchers(\"/level1/**\").hasRole(\"VIP1\") .antMatchers(\"/level2/**\").hasRole(\"VIP2\") .antMatchers(\"/level3/**\").hasRole(\"VIP3\"); //开启自动配置的登陆功能,效果,如果没有登陆,没有权限就会来到登陆页面 // /login来到登陆页 // 重定向到/login?error表示登陆失败 http.formLogin(); //开启自动配置的注销功能 //向/logout发送post请求表示注销 http.logout(); }} 此时除了主页,点击其他的页面都会自动跳转到security自动生成的登录页面,/login来到登陆页,重定向到/login?error表示登陆失败; http.logout()开启自动配置的注销功能,向/logout发送post请求表示注销,需要在欢迎页加上注销表单,默认注销后自动跳转到登录页面,若想改变转发路径,可以通过logoutSuccessUrl(url)设置路径 123<form th:action=\"@{/logout}\" method=\"post\"> <input type=\"submit\" value=\"注销\"></form> 3. 定义认证规则为了保证密码能安全存储,springboot内置PasswordEncoder对密码进行转码,默认密码编码器为DelegatingPasswordEncoder。在定义认证规则时,我们需要使用PasswordEncoder将密码转码,由于withDefaultPasswordEncoder()并非安全已被弃用,因此仅在测试中使用。 1234567891011121314151617181920@Beanpublic UserDetailsService users() { //使用默认的PasswordEncoder User.UserBuilder builder = User.withDefaultPasswordEncoder(); //定义账户用户名、密码、权限 UserDetails user1 = builder.username(\"zhangsan\") .password(\"123456\") .roles(\"VIP1\", \"VIP2\") .build(); UserDetails user2 = builder.username(\"lisi\") .password(\"123456\") .roles(\"VIP3\", \"VIP2\") .build(); UserDetails user3 = builder.username(\"wangwu\") .password(\"123456\") .roles(\"VIP1\", \"VIP3\") .build(); //使用内存保存用户信息 return new InMemoryUserDetailsManager(user1,user2,user3);} 4.自定义欢迎页导入依赖 1234<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency> 引入命名空间 123<!DOCTYPE html><html xmlns:th=\"http://www.thymeleaf.org\" xmlns:sec=\"http://www.thymeleaf.org/extras/spring-security\"> 根据是否登录显示游客或用户信息 1234567891011121314<!-- 未登录显示此div --><div sec:authorize=\"!isAuthenticated()\"> <h2 align=\"center\">游客您好,如果想查看武林秘籍 <a th:href=\"@{/userlogin}\">请登录</a></h2></div><!-- 登录显示此div --><div sec:authorize=\"isAuthenticated()\"> <!-- 显示用户名 --> <h2>尊敬的<span th:text=\"${#authentication.name}\"></span>,您好!您的角色有: <!-- 显示用户角色 --> <span th:text=\"${#authentication.authorities}\"></span></h2> <form th:action=\"@{/logout}\" method=\"post\"> <input type=\"submit\" value=\"注销\"> </form></div> 根据角色类型显示信息 123456789<!-- 具有VIP1的角色显示以下div --><div sec:authorize=\"hasRole('VIP1')\"> <h3>普通武功秘籍</h3> <ul> <li><a th:href=\"@{/level1/1}\">罗汉拳</a></li> <li><a th:href=\"@{/level1/2}\">武当长拳</a></li> <li><a th:href=\"@{/level1/3}\">全真剑法</a></li> </ul></div> 更多spring-security与thymeleaf整合教程 5. 自定义登录页/记住我123456789101112131415@Overrideprotected void configure(HttpSecurity http) throws Exception { ... //定制登录页 http.formLogin() .usernameParameter(\"user\") //表单用户名name .passwordParameter(\"pwd\") //表单密码name .loginPage(\"/userlogin\"); //定制登陆页路径 ... //开启记住我 http.rememberMe(). rememberMeParameter(\"rem\"); //设置表单记住我name值} 通过loginPage(url)设置登录页路径后,在定制的登录页发送post url即为登录请求,并设置表单的name属性都为对应值; 通过勾选记住我,session退出后依然能通过cookie保存用户信息,下次免登陆 123456<form th:action=\"@{/userlogin}\" method=\"post\"> 用户名:<input name=\"user\"/><br> 密码:<input name=\"pwd\"><br/> <input type=\"checkbox\" name=\"rem\">记住我<br> <input type=\"submit\" value=\"登陆\"></form> 更多spring-security参阅官方文档 (六) Spring boot与分布式一、分布式应用 分布式应用(distributed application)指的是应用程序分布在不同计算机上,通过网络来共同完成一项任务的工作方式。 为什么需要分布式? 单一应用架构当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。 垂直应用架构当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。 分布式服务架构当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。 流动计算架构当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。 在分布式系统中,国内常用zookeeper+dubbo组合,而Spring Boot推荐使用全栈的Spring,Spring Boot+Spring Cloud。 二、Zookeeper和Dubbo1. 概述ZooKeeperZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。 DubboDubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方(Provider)和服务消费方(Consumer)两个角色。 2. 整合springboot环境搭建 分别创建provider和consumer模块并分别导入依赖 1234567891011121314151617181920212223242526272829303132<dependencies> <!-- 导入dubbo与springboot整合启动器 --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.6</version> </dependency> <!-- 导入zookeeper客户端 --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <!-- 导入zookeeper客户端所需依赖:curator框架 --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.3.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.3.0</version> </dependency> </dependencies> provider配置文件 123456# 应用项目名dubbo.application.name=provider-ticket# zookeeper地址dubbo.registry.address=zookeeper://192.168.31.162:2181# dubbo扫描包路径 dubbo.scan.base-packages=cn.edu.ustc.service consumer配置文件 12dubbo.application.name=consumer-userdubbo.registry.address=zookeeper://192.168.31.162:2181 生产者服务 @EnableDubbo : 可以在指定的包名下(通过 scanBasePackages),或者指定的类中(通过 scanBasePackageClasses)扫描 Dubbo 的服务提供者(以 @Service 标注)以及 Dubbo 的服务消费者(以 Reference 标注)。 @Service: 表示服务的具体实现,被注解的类会被dubbo扫描 1234567891011121314import org.apache.dubbo.config.annotation.Service;import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;import org.springframework.stereotype.Component;@EnableDubbo //开启对dubbo支持@Component@Service //标记此类,表示服务的具体实现public class TicketServiceImpl implements TicketService{ @Override public String getTicket() { return \"Gxx:合肥-北京\"; }} 消费者服务 编写与分布式服务类相同的接口(不必实现),并保证包结构相同 123public interface TicketService { String getTicket();} @Reference 可以定义在类中的一个字段、方法上,表示一个服务的引用。通常 @Reference 定义在一个字段上 1234567891011@Servicepublic class UserService { @Reference TicketService ticketService; public void hello() { String ticket = ticketService.getTicket(); System.out.println(\"买到票了:\"+ticket); }} 此时若调用hello(),控制台将打印 1买到票了:Gxx:合肥-北京 有关dubbo更多 dubbo注解详细解释 dubbo与zookeeper官方整合案例 三、Spring Cloud1. 概述Spring Cloud是一个分布式的整体解决方案。Spring Cloud 为开发者提供了在分布式系统(配置管理,服务发现,熔断,路由,微代理,控制总线,一次性token,全局琐,leader选举,分布式session,集群状态)中快速构建的工具,使用Spring Cloud的开发者可以快速的启动服务或构建应用、同时能够快速和云平台资源进行对接。 SpringCloud分布式开发五大常用组件 服务发现——Netflix Eureka 客服端负载均衡——Netflix Ribbon 断路器——Netflix Hystrix 服务网关——Netflix Zuul 分布式配置——Spring Cloud Config 2. 入门Eureka注册中心 创建工程导入eureka-server模块 12345678910111213141516171819<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> ... </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 配置文件 12345678910server: port: 8761eureka: instance: hostname: eureka-server # eureka实例的主机名 client: register-with-eureka: false #不把自己注册到eureka上 fetch-registry: false #不从eureka上来获取服务的注册信息 service-url: defaultZone: http://localhost:8761/eureka/ 生产者模块 创建工程导入eureka-client和web模块 1234567891011121314151617181920212223<dependencies> <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> ...</dependencies><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement> 配置文件 12345678910111213server: port: 8002spring: application: name: provider-ticketeureka: instance: prefer-ip-address: true # 注册服务的时候使用服务的ip地址 client: service-url: defaultZone: http://localhost:8761/eureka/ 编写controller层和service层demo 123456789101112131415161718@Servicepublic class TicketService { public String getTicket(){ System.out.println(\"8002\"); return \"《厉害了,我的国》\"; }}@RestControllerpublic class TicketController { @Autowired TicketService ticketService; @GetMapping(\"/ticket\") public String getTicket(){ return ticketService.getTicket(); }} 消费者模块 创建工程导入eureka-client和web模块 配置文件 123456789101112spring: application: name: consumer-userserver: port: 8200eureka: instance: prefer-ip-address: true # 注册服务的时候使用服务的ip地址 client: service-url: defaultZone: http://localhost:8761/eureka/ 向容器中注入RestTemplate, 并使用@EnableDiscoveryClient开启发现服务功能 1234567891011121314@EnableDiscoveryClient //开启发现服务功能@SpringBootApplicationpublic class ConsumerUserApplication { public static void main(String[] args) { SpringApplication.run(ConsumerUserApplication.class, args); } @LoadBalanced //使用负载均衡机制 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); }} 编写controller并使用RestTemplate发现服务 123456789101112@RestControllerpublic class UserController { @Autowired RestTemplate restTemplate; @GetMapping(\"/buy\") public String buyTicket(String name){ String s = restTemplate.getForObject(\"http://PROVIDER-TICKET/ticket\", String.class); return name+\"购买了\"+s; }} 向http://localhost:8200/buy?username=zhangsan发请求,则会响应 1zhangsan购买了《厉害了,我的国》 并且在使用了@LoadBalanced之后实现了负载均衡,如果创建不同端口的provider应用,则访问会被均衡到各个应用 (七) Spring boot与热部署 在开发中我们修改一个Java文件后想看到效果不得不重启应用,这导致大量时间花费,我们希望不重启应用的情况下,程序可以自动部署(热部署)。有以下四种情况,如何能实现热部署。 一、模板引擎在Spring Boot中开发情况下禁用模板引擎的cache页面模板改变ctrl+F9可以重新编译当前页面并生效 二、Spring LoadedSpring官方提供的热部署程序,实现修改类文件的热部署 下载Spring Loaded(项目地址https://github.com/spring-projects/spring-loaded) 添加运行时参数; javaagent:C:/springloaded-1.2.5.RELEASE.jar –noverify 三、JRebel收费的一个热部署软件安装插件使用即可 四、 Spring Boot Devtools(推荐)引入依赖 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId></dependency> IDEA使用ctrl+F9重新编译实现热部署 (八) Spring Boot与监控管理通过引入spring-boot-starter-actuator,可以使用Spring Boot为我们提供的准生产环境下的应用监控和管理功能。我们可以通过HTTP,JMX,SSH协议来进行操作,自动得到审计、健康及指标信息等 一、 Actuator监控管理导入依赖 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency> 浏览器打开链接http://localhost:8080/actuator/,可以看到所有支持的连接,响应如下,默认只支持这些端点 1234567891011121314151617181920{ \"_links\": { \"self\": { \"href\": \"http://localhost:8080/actuator\", \"templated\": false }, \"health\": { \"href\": \"http://localhost:8080/actuator/health\", \"templated\": false }, \"health-path\": { \"href\": \"http://localhost:8080/actuator/health/{*path}\", \"templated\": true }, \"info\": { \"href\": \"http://localhost:8080/actuator/info\", \"templated\": false } }} 如果要看到所有支持的状态查询,需要配置 1management.endpoints.web.exposure.include=* bean加载情况http://localhost:8080/actuator/beans,显示了容器中各类各项属性 12345678910111213141516171819202122{ \"contexts\": { \"application\": { \"beans\": { \"endpointCachingOperationInvokerAdvisor\": { \"aliases\": [], \"scope\": \"singleton\", \"type\": \"org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor\", \"resource\": \"class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.class]\", \"dependencies\": [ \"environment\" ] }, \"defaultServletHandlerMapping\": { \"aliases\": [], \"scope\": \"singleton\", \"type\": \"org.springframework.web.servlet.HandlerMapping\", \"resource\": \"class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]\", \"dependencies\": [] }, } ... 二、 端点配置默认情况下,除shutdown以外的所有端点均已启用。要配置单个端点的启用,请使用management.endpoint.<id>.enabled属性。以下示例启用shutdown端点: 1management.endpoint.shutdown.enabled=true 另外可以通过management.endpoints.enabled-by-default来修改全局端口默认配置,以下示例启用info端点并禁用所有其他端点: 12management.endpoints.enabled-by-default=falsemanagement.endpoint.info.enabled=true 修改路径 1234# 修改根目录路径management.endpoints.web.base-path=/management# 修改/health路径management.endpoints.web.path-mapping.health=healthcheck 更多参阅spring-actuator官方文档","link":"/2020/04/18/springboot/"}],"tags":[{"name":"缓存","slug":"缓存","link":"/tags/%E7%BC%93%E5%AD%98/"},{"name":"消息","slug":"消息","link":"/tags/%E6%B6%88%E6%81%AF/"},{"name":"检索","slug":"检索","link":"/tags/%E6%A3%80%E7%B4%A2/"},{"name":"分布式","slug":"分布式","link":"/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"},{"name":"前端","slug":"前端","link":"/tags/%E5%89%8D%E7%AB%AF/"},{"name":"jquery","slug":"jquery","link":"/tags/jquery/"},{"name":"ajax","slug":"ajax","link":"/tags/ajax/"},{"name":"动态代理","slug":"动态代理","link":"/tags/%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86/"},{"name":"java","slug":"java","link":"/tags/java/"},{"name":"配置","slug":"配置","link":"/tags/%E9%85%8D%E7%BD%AE/"},{"name":"日志","slug":"日志","link":"/tags/%E6%97%A5%E5%BF%97/"},{"name":"web","slug":"web","link":"/tags/web/"},{"name":"docker","slug":"docker","link":"/tags/docker/"},{"name":"jpa","slug":"jpa","link":"/tags/jpa/"}],"categories":[{"name":"spring","slug":"spring","link":"/categories/spring/"},{"name":"java","slug":"java","link":"/categories/java/"},{"name":"前端","slug":"前端","link":"/categories/%E5%89%8D%E7%AB%AF/"}]}