玉树临风的小虾米 · 杭州私立国际学校一年多少钱?速看2024学费 ...· 2 月前 · |
失恋的铁板烧 · Oh好困扰啊主题,含义 - 快看漫画· 7 月前 · |
不羁的肉夹馍 · 【动漫篇】(熟肉drama)富士见二丁目交响 ...· 7 月前 · |
考研的馒头 · 为什么还有人用VIM? - 知乎· 11 月前 · |
八块腹肌的青椒 · 金鹰视后李媛媛已经离世18年了,曾恋焦晃,陈 ...· 1 年前 · |
概述参考:官方文档jsoup的使用JSoup教程jsoup 在 GitHub 的开源代码概念简介jsoup 是一款基于 Java 的 HTML 解析器,它提供了一套非常省力的 API,不但能直接解析某个 URL 地址、HTML 文本内容,而且还能通过类似于 DOM、CSS 或者 jQuery 的方法来操作数据,所以 jsoup 也可以被当做爬虫工具使用。jsoup 实现 WHATWG HTML5 规范,并将 HTML 解析为与现代浏览器相同的 DOM。从 URL,文件或字符串中提取并解析 HTML查找和提取数据,使用 DOM 遍历或 CSS 选择器操纵 HTML元素,属性和文本根据安全的白名单清理用户提交的内容,以防止 XSS 攻击输出整洁的 HTML相关概念简介Document :文档对象每份 HTML 页面都是一个文档对象,Document 是 jsoup 体系中最顶层的结构。Element:元素对象。一个 Document 中可以着包含着多个 Element 对象,可以使用 Element 对象来遍历节点提取数据或者直接操作 HTML。Elements:元素对象集合,类似于List<Element>。Node:节点对象。标签名称、属性等都是节点对象,节点对象用来存储数据。类继承关系:Document 继承自 Element ,Element 继承自 Node。一般执行流程:先获取 Document 对象,然后获取 Element 对象,最后再通过 Node 对象获取数据。依赖<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.15.4</version> </dependency>jsoup 的主要类虽然完整的类库中有很多类,但大多数情况下,需要重点了解下面给出3 个类即可org.jsoup.Jsoup 类Jsoup 类是任何 Jsoup 程序的入口点,并将提供从各种来源加载和解析 HTML 文档的方法。Jsoup 类的一些重要方法如下:// 创建并返回URL的连接。 static Connection connect(String url) // 将指定的字符集文件解析成文档。 static Document parse(File in, String charsetName) // 将给定的html代码解析成文档。 static Document parse(String html) // 从本地文件中加载文档对象 static Document parse(File in, String charsetName) // 从输入 HTML 返回安全的 HTML,通过解析输入 HTML 并通过允许的标签和属性的白名单进行过滤。 static String clean(String bodyHtml, Whitelist whitelist)org.jsoup.nodes.Document 类该类表示通过 Jsoup 库加载 HTML 文档。可以使用此类执行适用于整个 HTML 文档的操作。org.jsoup.nodes.Element 类HTML 元素是由标签名称,属性和子节点组成。 使用 Element 类,可以提取数据,遍历节点和操作 HTML。注:继承关系为 Document extends Element extends NodeAPIJsoup 类Jsoup 类的一些重要方法如下:// 创建并返回URL的连接。 static Connection connect(String url) // 将指定的字符集文件解析成文档。 static Document parse(File in, String charsetName) // 将给定的html代码解析成文档。 static Document parse(String html) // 从本地文件中加载文档对象 static Document parse(File in, String charsetName) // 从输入 HTML 返回安全的 HTML,通过解析输入 HTML 并通过允许的标签和属性的白名单进行过滤。 static String clean(String bodyHtml, Whitelist whitelist)Connection 接口// 将请求作为GET执行,并解析结果 Document get() throws IOException; // 将请求作为POST执行,并解析结果。 Document post() throws IOException; // 执行请求。获取内容包含响应码等 Connection.Response execute() throws IOException; // 将请求方法设置为使用GET或POST。默认为GET Connection method(Connection.Method method); // 创建一个新请求,使用此Connection作为会话状态并初始化连接设置(然后可以独立于返回的Connection.request对象) Connection newRequest(); // 设置要提取的请求URL。协议必须是HTTP或HTTPS Connection url(URL url); Connection url(String url); // 设置头部信息 Connection header(String name, String value); Connection headers(Map<String, String> headers); // 发出请求的用户信息 Connection userAgent(String userAgent); // 先前网页的地址,当前请求网页紧随其后,即来路 Connection referrer(String referrer); // 设置用于此请求的HTTP代理。设置为null禁用以前设置的代理。 Connection proxy(Proxy proxy); Connection proxy(String host, int port); // 设置总请求超时时间。如果发生超时SocketTimeoutException将被抛出。默认超时为30秒(30000ms)。零超时被视为无限超时。 // 注意,此超时指定连接时间和读取完整响应时间的组合最长持续时间 Connection timeout(int millis); // 在关闭连接和截断输入之前,设置从(未压缩)连接读取到正文的最大字节数(即正文内容将被修剪)。 // 默认最大值为2MB。最大大小为0被视为无限量(仅受机器上可用内存的限制)。 Connection maxBodySize(int bytes); // 设置是否遵循服务器重定向的连接。默认为true Connection followRedirects(boolean followRedirects); // 将连接配置为在发生HTTP错误时不引发异常(4xx-5xx,例如404或500)。默认为false;,如果遇到错误,将引发IOException。 // 如果设置为true,响应将填充错误正文,状态消息将反映错误。 Connection ignoreHttpErrors(boolean ignoreHttpErrors); // 分析响应时忽略文档的Content-Type。默认为false,未识别的内容类型将引发IOException。(例如,为了防止试图解析JPEG二进制图像而产生垃圾。)设置为true可强制解析尝试,而不管内容类型如何。 Connection ignoreContentType(boolean ignoreContentType); // 设置自定义SSL套接字工厂 Connection sslSocketFactory(SSLSocketFactory var1); // 添加请求数据参数。请求参数在GET的请求查询字符串中发送,在POST的请求正文中发送。一个请求可能有多个同名值。 Connection data(String key, String value); // 添加输入流作为请求数据参数。 Connection data(String key, String filename, InputStream inputStream); // 添加输入流作为请求数据参数。对于GET,没有影响,但对于POST,这将上载输入流。filename为文件名,不是路径 Connection data(String key, String filename, InputStream InputStream, String contentType); Connection data(Collection<Connection.KeyVal> data); Connection data(Map<String, String> data); Connection data(String... keyvals); // 获取此密钥的数据KeyVal(如果有) Connection.KeyVal data(String var1); // 设置要在请求中发送的cookie Connection cookie(String name, String value); Connection cookies(Map<String, String> cookies); // 获取此连接使用的cookie存储 Connection cookieStore(CookieStore cookieStore); // 提供自定义或预先填充的CookieStore,用于此连接发出的请求。 CookieStore cookieStore(); // 提供在分析文档响应时使用的备用解析器 Connection parser(Parser parser); // 设置请求的数据字符集,默认x-www-form-urlencoded Connection postDataCharset(String charset); // 设置连接请求 Connection request(Connection.Request var1); // 设置POST(或PUT)请求主体 Connection requestBody(String body); // 获取与此连接关联的请求对象 Connection.Request request(); // 设置连接的响应 Connection response(Connection.Response response); // 执行请求后,获取响应。 Connection.Response response();Element(元素)类org.jsoup.nodes.Element extends NodeElements 对象提供了一系列类似于 DOM 的方法来查找元素,抽取并处理其中的数据。查找元素// 通过 id 来查找元素 public Element getElementById(String id) // 通过标签来查找元素 public Elements getElementsByTag(String tag) // 通过类选择器来查找元素 public Elements getElementsByClass(String className) // 通过属性名称来查找元素,例如查找带有 href 元素的标签 public Elements getElementsByAttribute(String key) // 获取兄弟元素。如果元素没有兄弟元素,则返回一个空列表 public Elements siblingElements() // 获取第一个兄弟元素 public Element firstElementSibling() // 获取最后一个兄弟元素 public Element lastElementSibling() // 获取下一个兄弟元素 public Element nextElementSibling() // 获取上一个兄弟元素 public Element previousElementSibling() // 获取此节点的父节点 public Element parent() // 获取此节点的所有子节点 public Elements children() // 获取此节点的指定子节点 public Element child(int index) // 使用 CSS 选择器查找元素 public Elements select(String cssQuery)获取元素数据在获得文档对象并且指定查找元素后,就可以获取元素中的数据。注:这些访问器方法都有相应的 setter 方法来更改数据。// 获取单个属性值 public String attr(String key) // 获取所有属性值 public Attributes attributes() // 设置属性值 public Element attr(String key, String value) // 获取文本内容 public String text() // 设置文本内容 public Element text(String value) // 获取元素内的 HTML 内容 public String html() // 设置元素内的 HTML 内容 public Element html(String value) // 获取元素外 HTML 内容 public String outerHtml() // 获取数据内容(例如:script 和 style 标签) public String data() // 获得 id 值(例如:<p id="goods">衣服</p>) public String id() // 检查这个元素是否含有一个类选择器(不区分大小写) public boolean hasClass(String className) // 获得第一个类选择器值 public String className() // 获得所有的类选择器值 public Set<String>classNames() // 获取元素标签 public Tag tag() // 获取元素标签名(例如:`<p>`、`<div>` 等) public String tagName()修改数据在解析了一个 Document 对象之后,可能想修改其中的某些属性值,并把它输出到前台页面或保存到其他地方,jsoup 对此提供了一套非常简便的接口(支持链式写法)。设置属性的值以下方法当针对 Element 对象操作时,只有一个元素会受到影响。当针对 Elements 对象进行操作时,可能会影响到多个元素。// 设置标签的属性值 public Element attr(String key, String value) // 删除标签 public Element removeAttr(String key) // 增加类选择器选项 public Element addClass(String className) // 删除对应的类选择器 public Element removeClass(String className)代码示例:Document doc = Jsoup.connect("http://csdn.com").get(); // 复数,Elements Elements elements = doc.getElementsByClass("text"); // 单数,Element Element element = elements.first(); // 复数对象,所有 class="text" 的元素都将受到影响 elements.attr("name","goods"); // 单数对象,只有一个元素会受到影响(链式写法) element.attr("name","shop").addClass("red");修改元素的 HTML 内容可以使用 Element 中的 HTML 设置方法,具体如下:// 在末尾追加 HTML 内容 public Element append(String html) // 在开头追加 HTML 内容 public Element prepend(String html) // 在匹配元素内部添加 HTML 文本。这个方法将先清除元素中的 HTML 内容,然后用传入的 HTML 代替 public Element html(String value) // 对元素包裹一个外部 HTML 内容,将元素置于新增的内容中间 public Element wrap(String value)示例代码:Document doc = Jsoup.connect("http://csdn.com").get(); Element div = doc.select("div").first(); div.html("<p>csdn</p>"); div.prepend("<p>a</p>"); div.append("<p>good</p>"); // 输出:<div"> <p>a</p> <p>csdn</p> <p>good</p> </div> Element span = doc.select("span").first(); span.wrap("<li><a href='...'></a></li>"); // 输出: <li><a href="..."> <span>csdn</span> </a></li>修改元素的文本内容对于传入的文本,如果含有像 <, > 等这样的字符,将以文本处理,而非 HTML。// 清除元素内部的 HTML 内容,然后用提供的文本代替 public Element text(String text) // 在元素后添加文本节点 public Element prepend(String first) // 在元素前添加文本节点 public Element append(String last)示例代码: // <div></div> Element div = doc.select("div").first(); div.text(" one "); div.prepend(" two "); div.append(" three "); // 输出: <div> two one three </div>基本使用获取文档(Document)获得文档对象 Document 一共有 4 种方法,分别对应不同的获取方式。从URL中加载文档对象(常用)使用 Jsoup.connect(String url).get() 方法获取(只支持 http 和 https 协议):Document doc = Jsoup.connect("http://csdn.com/").get(); String title = doc.title();connect(String url) 方法创建一个新的 Connection 并通过 .get() 或者 .post() 方法获得数据。如果从该URL获取 HTML 时发生错误,便会抛出 IOException,应适当处理。Connection 接口还提供一个方法链来解决特殊请求,可以在发送请求时带上请求的头部参数。示例代码:Document doc = Jsoup.connect("http://csdn.com") .data("query", "Java") .userAgent("Mozilla") .cookie("auth", "token") .timeout(8000) .post();可以使用 execute() 方法获得完整的响应对象和响应码:// 获得响应对象 Connection.Response response = Jsoup.connect("http://csdn.com").execute(); // 获取状态码 int code = response.statusCode();从本地文件中加载文档对象可以使用静态的 Jsoup.parse(File in, String charsetName) 方法从文件中加载文档。其中 in 表示路径,charsetName 表示编码方式。示例代码:File input = new File("/tmp/input.html"); Document doc = Jsoup.parse(input, "UTF-8");从字符串文本中加载文档对象使用静态的 Jsoup.parse(String html) 方法可以从字符串文本中获得文档对象 Document 。示例代码:String html = "<html><head><title>First parse</title></head>" + "<body><p>Parsed HTML into a doc.</p></body></html>"; Document doc = Jsoup.parse(html);从 <body> 片断中获取文档对象使用 Jsoup.parseBodyFragment(String html) 方法,示例代码:String html = "<p>Lorem ipsum.</p>"; Document doc = Jsoup.parseBodyFragment(html); // doc 此时为:<body> <p>Lorem ipsum.</p></body> Element body = doc.body();parseBodyFragment 方法创建一个新的文档,并插入解析过的 HTML 到 body 元素中。虽然使用正常的 Jsoup.parse(String html) 方法,通常也能得到相同的结果,但是明确将用户输入作为 body 片段处理是个更好的方式。Document.body() 方法能够取得文档 body 元素的所有子元素,与 doc.getElementsByTag("body") 相同。选择元素(Element)解析文档对象并获取数据一共有 2 种方式,分别为 DOM方式、CSS 选择器方式,可以选择一种自己喜欢的方式去获取数据,效果一样。DOM 方式将 HTML 解析成一个 Document 之后,就可以使用类似于 DOM 的方法进行操作。常用方法详见 Element(元素)类 API示例代码:// 获取 csdn 首页所有的链接 Document doc = Jsoup.connect("http://csdn.com").get(); Elements elements = doc.getElementsByTag("body"); Elements contents = elements.first().getElementsByTag("a"); for (Element content : contents) { String linkHref = content.attr("href"); String linkText = content.text(); }CSS 选择器方式可以使用类似于 CSS 选择器的语法来查找和操作元素,常用的方法为 select(String cssQuery)示例代码:Document doc = Jsoup.connect("http://csdn.com").get(); // 获取带有 href 属性的 a 元素 Elements elements = doc.select("a[href]"); for (Element content : elements) { String linkHref = content.attr("href"); String linkText = content.text(); }select() 方法在 Document、Element 或 Elements 对象中都可以使用,而且是上下文相关的,因此可实现指定元素的过滤,或者采用链式访问。select() 方法将返回一个 Elements 集合,并提供一组方法来抽取和处理结果。select(String cssQuery) 方法参数简介tagname :通过标签查找元素,例如通过 "a" 来查找 <a> 标签。#id :通过 ID 查找元素,比如通过 #logo 查找 <p id="logo">.class :通过 class 名称查找元素,比如通过 .titile 查找 <p class="titile">ns|tag :通过标签在命名空间查找元素,比如使用 fb|name 来查找 <fb:name> [attribute] :利用属性查找元素,比如通过 [href]查找 <a href="...">[^attribute] :利用属性名前缀来查找元素,比如:可以用 [^data-] 来查找带有 HTML5 dataset 属性的元素[attribute=value] :利用属性值来查找元素,比如:[width=500][attribute^=value], [attribute$=value], [attribute*=value] :利用匹配属性值开头、结尾或包含属性值来查找元素,比如通过 [href*=/path/] 来查找 <a href="a/path/c.html">[attribute~=regex] :利用属性值匹配正则表达式来查找元素比如通过 img[src~=(?i)\.(png|jpe?g)] 来匹配所有的png 或者 jpg、jpeg 格式的图片* :通配符,匹配所有元素参数属性组合使用el#id :元素 + ID,比如:div#logoel.class :元素 + class,比如:div.mastheadel[attr] :元素 + class,比如:a[href]匹配所有带有 href 属性的 a 元素任意组合,比如:a[href].highlight 匹配所有带有 href 属性且 class="highlight" 的 a 元素。ancestor child :查找某个元素下子元素比如:可以用 .body p 查找在 "body" 元素下的所有 p 元素parent > child :查找某个父元素下的直接子元素比如:可以用 div.content > p 查找 p 元素,也可以用 body > * 查找 body 标签下所有直接子元素siblingA + siblingB :查找在 A 元素之前第一个同级元素 B,比如:div.head + divsiblingA ~ siblingX :查找 A 元素之前的同级 X 元素,比如:h1 ~ pel, el, el :多个选择器组合,查找匹配任一选择器的唯一元素,例如:div.masthead, div.logo特殊参数:伪选择器:lt(n) :查找哪些元素的同级索引值(它的位置在 DOM 树中是相对于它的父节点)小于 n比如:td:lt(3) 表示小于三列的元素:gt(n) :查找哪些元素的同级索引值大于 n,比如: div p:gt(2) 表示哪些 div 中有包含 2 个以上的 p 元素:eq(n) :查找哪些元素的同级索引值与 n 相等,比如:form input:eq(1) 表示包含一个 input 标签的 Form元素:has(seletor) :查找匹配选择器包含元素的元素,比如:div:has(p) 表示哪些 div 包含了 p 元素:not(selector) :查找与选择器不匹配的元素,比如: div:not(.logo) 表示不包含 class=logo 元素的所有 div 列表:contains(text) :查找包含给定文本的元素,搜索不区分大不写,比如: p:contains(jsoup):containsOwn(text) :查找直接包含给定文本的元素(不包含任何它的后代):matches(regex) :查找哪些元素的文本匹配指定的正则表达式,比如:div:matches((?i)login):matchesOwn(regex) :查找直接包含文本匹配指定正则表达式的元素(不包含任何它的后代)注意:上述伪选择器索引是从 0 开始的,即第一个元素索引值为 0,第二个元素 index 为 1 等其他功能一些常用数据的获取从 HTML 获取标题Document document = Jsoup.parse( new File("C:/Users/xyz/Desktop/yiibai-index.html"), "utf-8"); System.out.println(document.title());获取 HTML 页面的 Fav 图标假设 favicon 图像将是 HTML 文档的 <head> 部分中的第一个图像String favImage = "Not Found"; try { Document document = Jsoup.parse(new File("C:/Users/zkpkhua/Desktop/yiibai-index.html"), "utf-8"); Element element = document.head().select("link[href~=.*\\.(ico|png)]").first(); if (element == null) { element = document.head().select("meta[itemprop=image]").first(); if (element != null) { favImage = element.attr("content"); } else { favImage = element.attr("href"); } catch (IOException e) { e.printStackTrace(); System.out.println(favImage);获取 HTML 页面中的所有链接try { Document document = Jsoup.parse(new File("C:/Users/zkpkhua/Desktop/yiibai-index.html"), "utf-8"); Elements links = document.select("a[href]"); for (Element link : links) { System.out.println("link : " + link.attr("href")); System.out.println("text : " + link.text()); } catch (IOException e) { e.printStackTrace(); }获取 HTML 页面中的所有图像try { Document document = Jsoup.parse(new File("C:/Users/zkpkhua/Desktop/yiibai-index.html"), "utf-8"); Elements images = document.select("img[src~=(?i)\\.(png|jpe?g|gif)]"); for (Element image : images) { System.out.println("src : " + image.attr("src")); System.out.println("height : " + image.attr("height")); System.out.println("width : " + image.attr("width")); System.out.println("alt : " + image.attr("alt")); } catch (IOException e) { e.printStackTrace(); }获取 URL 的元信息元信息包括 Google 等搜索引擎用来确定网页内容的索引为目的。它们以 HTM L页面的 HEAD 部分中的一些标签的形式存在。try { Document document = Jsoup.parse(new File("C:/Users/zkpkhua/Desktop/yiibai-index.html"), "utf-8"); String description = document.select("meta[name=description]").get(0).attr("content"); System.out.println("Meta description : " + description); String keywords = document.select("meta[name=keywords]").first().attr("content"); System.out.println("Meta keyword : " + keywords); } catch (IOException e) { e.printStackTrace(); }在 HTML 页面中获取表单属性在网页中获取表单输入元素非常简单。 使用唯一 ID 查找 FORM 元素; 然后找到该表单中存在的所有 INPUT 元素。Document doc = Jsoup.parse(new File("c:/temp/yiibai-index.html"),"utf-8"); Element formElement = doc.getElementById("loginForm"); Elements inputElements = formElement.getElementsByTag("input"); for (Element inputElement : inputElements) { String key = inputElement.attr("name"); String value = inputElement.attr("value"); System.out.println("Param name: " + key + " \nParam value: " + value); }相对路径转绝对路径在 HTML 元素中,URLs 经常写成相对于文档位置的相对路径,如:<a href="/download">...</a>。当使用 .attr(String key) 方法来取得 a 元素的 href 属性时,它将直接返回在 HTML 源码中指定的值。如果需要取得一个绝对路径,有两种方式:方式1:在属性名前加 abs: 前缀,就可以返回包含根路径的 URL 地址 attr("abs:href")方式2:使用 absUrl(String key) 方法示例场景问题描述:有一个包含相对 URLs 路径的 HTML 文档,现在需要将这些相对路径转换成绝对路径的URLs解决方式:确保在解析文档时有指定 base URI 路径。然后使用 abs: 属性前缀来取得包含 base URI 的绝对路径。示例代码:Document doc = Jsoup.connect("http://www.open-open.com").get(); Element link = doc.select("a").first(); String relHref = link.attr("href"); // 输出:/ String absHref = link.attr("abs:href"); // 输出:http://www.open-open.com/消除不受信任的HTML (防止XSS攻击)假设在应用程序中,想显示用户提交的 HTML 片段。 例如用户可以在评论框中放入 HTML 内容。 这可能会导致非常严重的问题,如果允许直接显示此 HTML。 用户可以在其中放入一些恶意脚本,并将用户重定向到另一个脏网站。为了清理这个 HTML,Jsoup 提供 Jsoup.clean() 方法。 此方法期望 HTML 格式的字符串,并将返回清洁的 HTML。 要执行此任务,Jsoup 使用白名单过滤器。 jsoup 白名单过滤器通过解析输入 HTML(在安全的沙盒环境中)工作,然后遍历解析树,只允许将已知安全的标签和属性(和值)通过清理后输出。它不使用正则表达式,这对于此任务是不合适的。清洁器不仅用于避免 XSS,还限制了用户可以提供的元素的范围:可以使用文本,强元素,但不能构造 div 或表元素。示例场景问题描述:在某些网站中经常会提供用户评论的功能,但是有些不怀好意的用户,会搞一些脚本到评论内容中,而这些脚本可能会破坏整个页面的行为,更严重的是获取一些机要信息,此时需要清理该 HTML,以避免跨站脚本攻击(XSS)。解决方式:使用 clean() 方法清除恶意代码,但需要指定一个配置的 Safelist(旧版本中是 Whitelist),通常使用 Safelist.basic() 即可。Safelist 的工作原理是将输入的 HTML 内容单独隔离解析,然后遍历解析树,只允许已知的安全标签和属性输出。示例代码:String unsafe = "<p><a href='http://csdn.com/' οnclick='attack()'>Link</a></p>"; String safe = Jsoup.clean(unsafe, Safelist.basic()); // 输出: <p><a href="http://csdn.com/" >Link</a></p>注:jsoup 的 Safelist 不仅能够在服务器端对用户输入的 HTML 进行过滤,只输出一些安全的标签和属性,还可以限制用户可以输入的标签范围。jsoup 使用代理示例代码:Connection.Response execute = Jsoup.connect("http://csdn.net/") .proxy("12.12.12.12", 1080) // 使用代理 .execute();
tomcat参考:Tomcat的3个参数acceptCount、maxConnections、maxThreadsTomcat 的核心组件Tomcat 由 2 大核心组件组成:Connector、ContainerTomcat 处理请求的过程请求在 tomcat 服务器的处理过程(BIO 模式)客户端与服务端完成三次握手建立了连接,连接信息会存放在 ServerSocket 连接请求的队列中(队列长度为 acceptCount)如果提交到线程池的任务数没有超过 maxConnections,那么就 ServerSocket.accept() 返回 socket 对象,封装为任务提交到线程池;如果提交的任务数超过了 maxConnections,则阻塞任务提交到线程池后,分三种情况:线程数 <= minSpareThreads:不管有没有空闲线程,都新建线程来处理任务minSpareThreads < 线程数 < maxThreads:新任务会优先使用空闲线程,如果没有空闲线程就新建线程线程数 == maxThreads:新任务就会在 Connector 创建的 ServerSocket 队列中堆积起来,直到到达最大的配置值(acceptCount 属性值)若队列已满,任何再来的请求将会收到 connection refused 错误,直到有可用的资源来处理它们当任务被处理完后,则销毁任务以及任务中的 socket 对象,该连接被释放Connector 的 protocol(协议)Connector 在处理 HTTP 请求时,会使用不同的 protocol。不同的 Tomcat 版本支持的 protocol 不同,其中最典型的 protocol 包括BIO、NIO 和 APR(Tomcat7 中支持这 3 种,Tomcat8 增加了对 NIO2 的支持,而到了 Tomcat8.5 和 Tomcat9.0,则去掉了对 BIO 的支持)。BIO(Blocking IO):阻塞的 IONIO(Non-blocking IO):非阻塞的 IOAPR(Apache Portable Runtime):是 Apache 可移植运行库,利用本地库可以实现高可扩展性、高性能;Apr 是在 Tomcat 上运行高并发应用的首选模式,但需要安装 apr、apr-utils、tomcat-native 等包。BIO在 BIO 实现的 Connector 中,处理请求的主要实体是 JIoEndpoint 对象。JIoEndpoint 维护了 Acceptor 和 Worker:Acceptor 接收 socket,然后从 Worker 线程池中找出空闲的线程处理 socket,如果 worker 线程池没有空闲线程,则 Acceptor 将阻塞。Worker 是 Tomcat 自带的线程池,如果通过配置了其他线程池,原理与 Worker 类似。NIO在 NIO 实现的 Connector 中,处理请求的主要实体是 NIoEndpoint 对象。NIoEndpoint 中除了包含 Acceptor 和 Worker 外,还使用了 Poller,处理流程如下图所示:Acceptor 接收 socket 后,不是直接使用 Worker 中的线程处理请求,而是先将请求发送给了 Poller,而 Poller 是实现 NIO 的关键。Acceptor 向 Poller 发送请求通过队列实现,使用了典型的生产者-消费者模式。在 Poller 中,维护了一个 Selector 对象;当 Poller 从队列中取出 socket 后,注册到该 Selector 中;然后通过遍历 Selector,找出其中可读的 socket,并使用 Worker 中的线程处理相应请求。与 BIO 类似,Worker 也可以被自定义的线程池代替。在 NIoEndpoint 处理请求的过程中,无论是 Acceptor 接收 socket,还是线程处理请求,使用的仍然是阻塞方式;但在 ”读取socket并交给Worker中的线程” 的这个过程中,使用非阻塞的 NIO 实现,这是 NIO 模式与 BIO 模式的最主要区别(其他区别对性能影响较小,暂时略去不提)。而这个区别,在并发量较大的情形下可以带来 Tomcat 效率的显著提升。影响并发的 tomcat 参数maxConnections :Tomcat 在任意时刻接收和处理的最大连接数(可以提交给线程池的最大任务数)当 Tomcat 接收的连接数达到 maxConnections 时,Acceptor 线程不会读取 accept 队列中的连接(socket);这时 accept 队列中的线程会一直阻塞着,直到 Tomcat 接收的连接数小于 maxConnections。如果设置为 -1,则连接数不受限制。默认值与连接器使用的协议有关:NIO 的默认值是 10000APR/native 的默认值是 8192在windows下,APR/native 的 maxConnections 值会自动调整为设置值以下最大的 1024 的整数倍如设置为 2000,则最大值实际是 1024BIO 的默认值为 maxThreads(如果配置了 Executor,则默认值是 Executor 的 maxThreads)acceptCount :允许的最大并发连接数(瞬时连接、瞬时并发数),为 ServerSocket 连接请求的队列长度,默认值为 100请求连接在任务队列中时,客户端显示为浏览器显示“转圈”当 accept 队列中连接的个数达到 acceptCount 时,队列满,进来的请求一律被拒绝。实际场景中,常见的表象是 nginx 响应 502,Tomcat 中没有任何 access 日志,此时应该调大该值。minProcessors:服务器启动时,线程池创建的最少线程数maxProcessors(maxThreads ):线程池最大连接线程数。默认值为 200线程数小于此数时,每次来任务若有空闲线程,使用空闲线程处理,如果没有空闲线程则新建线程处理如果该 Connector 绑定了 Executor,这个值会被忽略,因为该 Connector 将使用绑定的 Executor,而不是内置的线程池来执行任务。注:maxThreads 规定的是最大的线程数目,并不是实际 running 的 CPU 数量;实际上,maxThreads 的大小比 CPU 核心数量要大得多。因为处理请求的线程真正用于计算的时间可能很少,大多数时间可能在阻塞,如等待数据库返回数据、等待硬盘读写数据等。因此,在某一时刻,只有少数的线程真正的在使用物理 CPU,大多数线程都在等待;故线程数远大于物理核心数才是合理的。换句话说,Tomcat 通过使用比 CPU 核心数量多得多的线程数,可以使 CPU 忙碌起来,大大提高 CPU 的利用率minSpareThreads :线程池最小空闲线程数(多余的空闲线程都将杀死)。默认值为 25线程数小于此数时,每次来任务都新建线程处理maxSpareThreads :线程池最大空闲线程数一旦创建的线程超过这个值,Tomcat 就会关闭不再需要的 socket 线程maxIdLeTime:一个线程空闲多久算是一个空闲线程,单位:毫秒connectionTimeout :网络连接超时。单位:毫秒。默认值为 60000ms(60秒)设置为 0 表示永不超时,但这样设置有隐患的。通常设置为 30000 毫秒或使用默认值disableUploadTimeout :禁用上传超时,主要用于大数据上传时,允许 Servlet 容器正在执行使用一个较长的连接超时值,以使 Servlet 有较长的时间来完成它的执行,默认值为 falseenableLookups :是否反查域名,取值为:true 或 false若为 true,则可以通过调用 request.getRemoteHost() 进行 DNS 查询来得到远程客户端的实际主机名若为 false,则不进行DNS查询,而是返回其 ip 地址为了提高处理能力,应设置为 false补充说明:maxThreads 和 acceptCount 属性对 tomcat 能同时处理的请求数和请求响应时间有直接的影响。无论 acceptCount 值为多少,maxThreads 直接决定了实际可同时处理的请求数。而不管 maxThreads 如何,acceptCount 则决定了有多少请求可等待处理。然而,不管是可立即处理请求还是需要放入等待区,都需要 tomcat 先接受该请求(即接受客户端的连接请求,建立socketchannel),那么 tomcat 同时可建立的连接数(maxConnections 属性值)也会影响可同时处理的请求数。如何设置 acceptCount 、maxConnections、maxThreads 的值:CPU 越不密集(或 IO 越密集),maxThreads 应该越大maxConnections 的设置与 Tomcat 的运行模式有关如果 tomcat 使用的是 BIO,那么 maxConnections 的值应该与 maxThreads 一致(默认值为 maxThreads)如果 tomcat使用的是 NIO,maxConnections 值应该远大于 maxThreads(默认值为 10000)Tomcat 能够接收的连接数 = maxThreads + acceptCount acceptCount 的设置,与应用在连接过高情况下希望做出什么反应有关系如果设置过大,后面进入的请求等待时间会很长如果设置过小,后面进入的请求立马返回 connection refused在线用户数、连接数、瞬时并发数、线程数的区别在线用户数 = 连接数 + 静态用户数(已登录,但连接已断开,只是在浏览静态数据)(有session对象,没有socket对象)连接数 = 已接受连接数 + 瞬时并发数(acceptCount:在连接队列里等待的socket对象数)已接受连接数 = 线程数(RUNNABLE状态)(正在处理)+ 任务队列中的任务数(已接受,待处理)影响并发的其他限制因素Tomcat 的运行模式BIO(阻塞式的 Socket 通信)模式Tomcat8 以下版本,默认的 HTTP 实现是采用 BIO 模式,每个请求都需要创建一个线程处理这种模式下的并发量受到线程数的限制,不大适合高并发,但技术成熟。每个进程中的线程数受制于操作系统的内核参数设置:Windows 主机每个进程中的线程数不允许超过 2000Linux 主机每个进程中的线程数不允许超过 1000NIO模式(非阻塞式的 Socket 通信)Tomcat8 以上版本,默认使用的就是 NIO 模式,在性能上高于阻塞式的,每个请求也不需要创建一个线程进行处理,并发能力比前者高。APR 模式(全称 Apache Portable Runtime)是 Tomcat 生产环境运行的首选方式。但必须要安装 APR 和 Native,直接启动就支持 APR。APR 是从操作系统级别解决异步 IO 问题。APR 的本质就是使用 JNI 技术调用操作系统底层的 IO 接口,所以需要提前安装所需要的依赖。如果操作系统未安装 APR 或者 APR 路径未指到 Tomcat 默认可识别的路径,则 APR 模式无法启动,自动切换启动 NIO 模式。注:APR 模式可以提升 Tomcat 对静态文件的处理性能,当然也可以采用动静分离。JVM 调优(tomcat 可以使用的内存)Tomcat 是运行在 JVM 上的,所以对 JVM 的调优也是非常有必要的在 Java 中每开启一个线程需要耗用 1MB 的 JVM 内存空间用于作为线程栈之用tomcat 默认可以使用的内存为128MB,在并发量较大的应用项目中,这点内存是不够的,需要修改 JVM 参数调优Unix下,在文件{tomcat_home}/bin/catalina.sh的前面,增加如下设置:JAVA_OPTS='-Xms【初始化内存大小】 -Xmx【可以使用的最大内存】'需要把这个两个参数值调大。例如:JAVA_OPTS='-Xms256m -Xmx512m'表示初始化内存为 256MB,可以使用的最大内存为 512MB一台主机允许的连接数、线程数、内存大小、硬件性能和 CPU 数量,都会限制实际并发数并发能力还与应用的逻辑密切相关,如果逻辑很复杂需要大量的计算,那并发能力势必会下降。如果每个请求都含有很多的数据库操作(或其他中间件的连接),那么对于数据库的性能要求也是非常高的。对于单台数据库服务器来说,允许客户端的连接数量是有限制的(数据库读写的并发能力)建议当某个应用拥有 250 个以上并发的时候,应考虑应用服务器的集群拓展tomcat 服务器 server.xml 文件<Server port="8005" shutdown="SHUTDOWN"> <!-- 属性说明 port: 指定一个端口,这个端口负责监听关闭Tomcat的请求 shutdown: 向以上端口发送的关闭服务器的命令字符串 --> <Listener className="org.apache.catalina.core.AprLifecycleListener" /> <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" /> <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> <Listener className="org.apache.catalina.storeconfig.StoreConfigLifecycleListener"/> <GlobalNamingResources> <Environment name="simpleValue" type="java.lang.Integer" value="30"/> <Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml" /> </GlobalNamingResources> <!-- 每个Service元素只能有一个Engine元素。元素处理在同一个<Service>中所有<Connector>元素接收到的客户请求 --> <Service name="Catalina"> <!-- 属性说明 name: Service的名称 --> <!-- Connector元素: 由Connector接口定义。<Connector>元素代表与客户程序实际交互的给件, 它负责接收客户请求,以及向客户返回响应结果 --> <Connector port="8080" maxHttpHeaderSize="8192" maxThreads="150" acceptCount="100" minSpareThreads="25" maxSpareThreads="75" connectionTimeout="20000" disableUploadTimeout="true" enableLookups="false" redirectPort="8443" /> <Connector port="8009" enableLookups="false" redirectPort="8443" protocol="AJP/1.3" /> <!--属性说明 port: 服务器连接器的端口号,该连接器将在指定端口侦听来自客户端的请求 maxThreads: 设定在监听端口的线程的最大数目,这个值也决定了服务器可以同时响应客户请求的最大数目,默认值为200 acceptCount: 当所有可以使用的处理请求的线程都被用光时,可以放到处理队列中的请求数 超过这个数的请求将不予处理,而返回Connection refused错误 minProcessors: 服务器启动时创建的处理请求的线程数,每个请求由一个线程负责 maxProcessors: 最多可以创建的处理请求的线程数 minSpareThreads: 最小备用线程 maxSpareThreads: 最大备用线程 connectionTimeout: 等待超时的时间数(以毫秒为单位) disableUploadTimeout: 禁用上传超时,主要用于大数据上传时 enableLookups: 如果为true,则可以通过调用request.getRemoteHost()进行DNS查询来得到远程客户端的实际主机名; 若为false则不进行DNS查询,而是返回其ip地址 redirectPort: 服务器正在处理http请求时收到了一个SSL传输请求后重定向的端口号 debug: 日志等级 protocol: 必须设定为AJP/1.3协议 address: 如果服务器有两个以上IP地址,该属性可以设定端口监听的IP地址,默认情况下,端口会监听服务器上所有IP地址 --> <Engine name="Catalina" defaultHost="localhost"> <!-- 属性说明 name: 对应$CATALINA_HOME/config/Catalina中的Catalina defaultHost: 缺省的处理请求的虚拟主机名 对应Host元素中的name属性,也就是$CATALINA_HOME/config/Catalina/localhost中的localhost 它至少与其中的一个Host元素的name属性值是一样的 debug: 日志等级 --> <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> <!-- 由Host接口定义。一个Engine元素可以包含多个<Host>元素,每个<Host>的元素定义了一个虚拟主机。它包含了一个或多个Web应用 --> <Host name="localhost" appBase="webapps" autoDeploy="true" unpackWARs="true" xmlValidation="false" xmlNamespaceAware="false"> <!-- 属性说明 name: 虚拟主机名。即 $CATALINA_HOME/config/Catalina/localhost中的localhost appBase: 默认的应用路径,此路径相对于$CATALINA_HOME/ (web应用的基本目录) 。 在autoDeploy为true的情况下,可自动部署应用此路径 autoDeploy: 默认为true,表示如果有新的WEB应用放入appBase并且Tomcat在运行的情况下,自动载入应用 debug: 是日志的调试等级 unpackWARs: 如果为true,在Web应用为*.war时,tomcat会自动将WAR文件解压; 否则不解压,直接从WAR文件中运行应用程序 --> <Context path="/demm" docBase="E:\\projects\\demm\\WebRoot" debug="0" reloadable="true" > </Context> <!-- 属性说明 path: 访问应用的上下文路由URI 如果http://localhost/是应用的根目录,访问此应用接口的url前缀为http://localhost/demm docBase: WEB应用的目录。即 web应用的文件存放路径或者是WAR文件存放路径 注意:此目录必须符合Java WEB应用的规范 debug: 日志等级 reloadable: 是否在程序有改动时自动重新载入。如果为true,则Tomcat将支持热部署,但会影响性能。 即可以自动检测web应用的/WEB-INF/lib和/WEB-INF/classes目录的变化,自动装载新的JSP和Servlet。 可以在不重起Tomcat的情况下改变web应用。 --> </Host> </Engine> </Service> </Server> 注:tomcat 启动后会默认占用 8080,8009和 8005 三个端口8080 端口负责建立 HTTP 连接。在通过浏览器访问 Tomcat服务器的 Web 应用时,使用的就是这个连接器8009 端口负责和其他 HTTP服务器建立连接。在把 Tomcat 与其他 HTTP 服务器集成时就需要用到这个连接器8005 端口用来向 Tomcat 发布 shutdown 命令的对网络端口的理解实际上,电脑在网卡上的硬件网络端口只有一个。常说的 1-65535 号端口,并不是真的有这么多个硬件端口,硬件端口实际上只有一个,访问所有端口的数据包都发往这个硬件端口。硬件端口接收到数据包之后进行解析,然后通知监听对应端口的socket对象来取数据。并发量计算参考: 并发量计算几个概念:业务并发用户数、最大并发访问数、系统用户数、同时在线用户数假设一个 OA 系统有 1000 用户,这就是系统用户数最高峰同时有 500 人在线,是“同时在线人数”,也称作“最大业务并发用户数”500 个同时使用系统用户中20%查看系统公告,不构成压力;20%填写表格(只在提交时才会请求,填写对服务器不构成压力);40%在发呆(什么都没做);20%用户不停从一个页面跳转另一个页面(只有这20%对服务器产生了压力)服务器实际压力(能承受的最大并发访问数),既取决于业务并发用户数,还取决于用户的业务场景,这些可以通过对服务器日志的分析得到,一般只需要分析典型业务(用户常用,最关注的业务操作)。估算业务并发用户数的公式(测试人员一般只关心业务并发用户数)C = nL / TC^ = C + 3 × (C的平方根)C 是平均的业务并发用户数n 是 login session 的数量L 是 login session 的平均长度T 是指考察的时间段长度C^ 是指业务并发用户数的峰值该公式的得出是假设用户的 login session 产生符合泊松分布而估算得到。假设:OA 系统有1000用户,每天400个用户发访问,每个登录到退出平均时间2小时,在1天时间内用户只在8小时内使用该系统。C = 400 × 2 / 8 = 100C^ = 100 + 3 × (100的平方根) = 100 + 3 × 10 = 130另外,如果知道平均每个用户发出的请求数 u,则系统吞吐量可以估算为 u × C注意:精确估算,还要考虑用户业务操作存在一定的时间集中性(比如上班后 1 小时内是 OA 系统高峰期),采用公式计算仍然会存在偏差针对例子 OA 系统可以把 1 小时设定为考察时间的粒度,将一天 8 小时划分为 8 个区间,这样可以解决业务操作存在集中性问题,更趋于精准,偏差更小。系统吞度量要素参考:系统吞吐量(TPS)、用户并发量、性能测试概念和公式系统吞吐量的几个重要参数:QPS(TPS)、并发数、响应时间QPS(TPS)= 并发数 / 平均响应时间QPS(TPS):每秒钟request / 事务数并发数: 系统同时处理的request / 事务数响应时间: 一般取平均响应时间(很多人经常会把并发数和 TPS 理解混淆)一个系统吞吐量通常由 QPS(TPS)、并发数两个因素决定。每套系统这两个值都有一个相对极限值,在应用场景访问压力下,只要某一项达到系统最高值,系统的吞吐量就上不去了如果压力继续增大,系统的吞吐量反而会下降,原因是系统超负荷工作,上下文切换、内存等等其它消耗导致系统性能下降。系统响应时间,由 CPU 运算、IO、外部系统响应等等组成。tomcat 高并发配置与优化参考:tomcat高并发配置与优化
内存溢出(OutofMemoryError)简述java doc 中对 Out Of Memory Error 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。JVM 提供的内存管理机制和自动垃圾回收极大的解放了用户对于内存的管理,由于 GC(垃圾回收)一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现内存泄漏和内存溢出问题。但是基本不会出现并不等于不会出现,所以掌握 Java 内存模型原理和学会分析出现的内存溢出或内存泄漏仍然十分重要。大多数情况下,GC 会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存,供应用程序继续使用。在抛出 OutofMemoryError 之前,通常垃圾收集器会被触发,尽其所能去清理出空间。例如:在引用机制分析中,涉及到 JVM 会去尝试回收软引用指向的对象等。在 java.nio.BIts.reserveMemory() 方法中,System.gc() 会被调用,以清理空间。当然,也不是在任何情况下垃圾收集器都会被触发的。比如,分配了一个超大对象,类似一个超大数组超过堆的最大值,JVM 可以判断出垃圾收集并不能解决这个问题,所以直接抛出 OutofMemoryError。内存溢出的常见情形不同的内存溢出错误可能会发生在内存模型的不同区域,因此,需要根据出现错误的代码具体分析来找出可能导致错误发生的地方,并想办法进行解决。栈内存溢出(StackOverflowError)栈内存可以分为虚拟机栈(VM Stack)和本地方法栈(Native Method Stack),除了它们分别用于执行 Java 方法(字节码)和本地方法,其余部分原理是类似的。以虚拟机栈为例说明,Java 虚拟机栈是线程私有的,当线程中方法被调度时,虚拟机会创建用于保存局部变量表、操作数栈、动态连接和方法出口等信息的栈帧(Stack Frame)。具体来说,当线程执行某个方法时,JVM 会创建栈帧并压栈,此时刚压栈的栈帧就成为了当前栈帧。如果该方法进行递归调用时,JVM 每次都会将保存了当前方法数据的栈帧压栈,每次栈帧中的数据都是对当前方法数据的一份拷贝。如果递归的次数足够多,多到栈中栈帧所使用的内存超出了栈内存的最大容量,此时 JVM 就会抛出 StackOverflowError。总之,不论是因为栈帧太大还是栈内存太小,当新的栈帧内存无法被分配时,JVM 就会抛出 StackOverFlowError。优化方案:可以通过设置 JVM 启动参数 -Xss 参数来改变栈内存大小。注:分配给栈的内存并不是越大越好,因为栈内存越大,线程多,留给堆的空间就不多了,容易抛出OOM。JVM的默认参数一般情况没有问题(包括递归)。递归调用要控制好递归的层级,不要太高,超过栈的深度。递归调用要防止形成死循环,否则就会出现栈内存溢出。堆内存溢出(OutOfMemoryError:java heap space)堆内存的唯一作用就是存放数组和对象实例,即通过 new 指令创建的对象,包括数组和引用类型。堆内存溢出又分为两种情况:Java 虚拟机的堆内存设置不够如果堆的大小不合理(没有显式指定 JVM 堆大小或者指定数值偏小),对象所需内存太大,创建对象时分配空间,JVM 就会抛出 OutOfMemoryError:java heap space 异常。优化方案:如果要处理比较可观的数据量,可以通过修改 JVM 启动参数 -Xms 、-Xmx 来调整。使用压力测试来调整这两个参数达到最优值。尽量避免大的对象的申请,例如文件上传,大批量从数据库中获取等。尽量分块或者分批处理,有助于系统的正常稳定的执行。尽量提高一次请求的执行速度,垃圾回收越早越好。否则,大量的并发来了的时候,再来新的请求就无法分配内存了,就容易造成系统的雪崩。堆内存泄露最终导致堆内存溢出当堆中一些对象不再被引用但垃圾回收器无法识别时,这些未使用的对象就会在堆内存空间中无限期存在,不断的堆积就会造成内存泄漏。不停的堆积最终会触发 java . lang.OutOfMemoryError。优化方案:如果发生了内存泄漏,则可以先找出导致泄漏发生的对象是如何被 GC ROOT 引用起来的,然后通过分析引用链找到发生泄漏的地方,进行代码优化。永久代溢出(OutOfMemoryError:PermGen sapce)对于老版本的 oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(例如常量池回收、卸载不再需要的类型)非常不积极,所以当不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 intern 字符串缓存占用太多空间,也会导致 OOM 问题,对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:PermGen space"。随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutofMemoryError:Metaspace"。元空间内存溢出(OutOfMemoryError: Metaspace)元空间的溢出,系统会抛出 java.lang.OutOfMemoryError: Metaspace出现这个异常的问题的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。优化方案:默认情况下,元空间的大小仅受本地内存限制。但是为了整机的性能,尽量还是要对该项进行设置,优化参数配置,以免造成整机的服务停机。慎重引用第三方包对第三方包,一定要慎重选择,不需要的包就去掉。这样既有助于提高编译打包的速度,也有助于提高远程部署的速度。关注动态生成类的框架对于使用大量动态生成类的框架,要做好压力测试,验证动态生成的类是否超出内存的需求会抛出异常。直接内存溢出如果直接或间接(很多 java NIO,例如在 netty 的框架中被封装为其他的方法)使用了 ByteBuffer 中的 allocateDirect() 方法,而又不做 clear 的时候,就会抛出 java.lang.OutOfMemoryError: Direct buffer memory 异常。如果经常有类似的操作,可以考虑设置 JVM 参数:-XX:MaxDirectMemorySize,并及时 clear 内存。创建本地线程内存溢出除了堆以外的区域,无法为线程分配一块内存区域了(线程基本只占用堆以外的内存区域),要么是内存本身就不够,要么堆的空间设置得太大了,导致了剩余的内存已经不多了,而由于线程本身要占用内存,所以就不够用了。优化方案:首先检查操作系统是否有线程数的限制,如果使用 shell 也无法创建线程,就需要调整系统的最大可支持的文件数。日常开发中尽量保证线程最大数的可控制的,不要随意使用可以无限制增长的线程池。数组超限内存溢出JVM 在为数组分配内存之前,会执行特定平台的检查:分配的数据结构是否在此平台是可寻址的。一般来说 java 对应用程序所能分配数组最大大小是有限制的,只不过不同的平台限制有所不同,但通常在1到21亿个元素之间。当应用程序试图分配大于 Java 虚拟机可以支持的数组时会报 Requested array size exceeds VM limit 错误。不过这个错误一般少见的,主要是由于 Java 数组的索引是 int 类型。 Java 中的最大正整数为 2 ^ 31 - 1 = 2,147,483,647。 并且平台特定的限制可以非常接近这个数字,例如:Jdk1.8 可以初始化数组的长度高达 2,147,483,645(Integer.MAX_VALUE-2)。若是在将数组的长度再增加 1 达到 nteger.MAX_VALUE-1 ,就会出现 OutOfMemoryError 了。优化方案:数组长度要在平台允许的长度范围之内。超出交换区内存溢出在 Java 应用程序启动过程中,可以通过 -Xmx 和其他类似的启动参数限制指定的所需的内存。而当 JVM 所请求的总内存大于可用物理内存的情况下,操作系统开始将内容从内存转换为硬盘。当应用程序向 JVM native heap 请求分配内存失败并且 native heap 也即将耗尽时, JVM 会抛出Out of swap space 错误, 错误消息中包含分配失败的大小(以字节为单位)和请求失败的原因。优化方案:增加系统交换区的大小。但如果使用了交换区,性能会大大降低,不建议采用这种方式。生产环境尽量避免最大内存超过系统的物理内存。其次,去掉系统交换区,只使用系统的内存,保证应用的性能。系统杀死进程内存溢出操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,称为“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer 被激活,检查当前谁占用内存最多然后将该进程杀掉。一般 Out of memory:Kill process or sacrifice child 报错会在当可用虚拟内存(包括交换空间)消耗到让整个操作系统面临风险内存不足时,会被触发。在这种情况下,OOM Killer 会选择“流氓进程”并杀死它。优化方案:增加交换空间的方式可以缓解 Java heap space 异常但还是建议最好的方案就是升级系统内存,让 java 应用有足够的内存可用,就不会出现这种问题。内存泄漏(memory leak)简述也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收它们的情况,才叫内存泄漏。但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,也可以叫做宽泛意义上的“内存泄漏”。尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutOfMemory 异常,导致程序崩溃。注意:这里的可用内存并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。Java 使用可达性分析算法,最上面的数据不可达,就是需要被回收的。后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开,从而导致没有办法被回收可达性分析算法可达性分析算法:判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏)。举例说明:对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长;那么当 Y 生命周期结束的时候,X 依然引用着 Y,这时候,垃圾回收期是不会回收对象 Y 的;如果对象 X 还引用着生命周期比较短的 A、B、C,对象 A 又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。Java 中内存泄漏的 8 种情况静态集合类,如 HashMap、LinkedList 等等。如果这些容器为静态的,那么它们的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简而言之,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。单例模式单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。内部类持有外部类的引用在 Java 中内部类的定义与使用一般为成员内部类与匿名内部类,他们的对象都会隐式持有外部类对象的引用,影响外部类对象的回收。可以通过反编译可以来验证这个理论:java 代码public class Outer { private String name; class Inner{ private String test; }反编译后的代码class Outer$Inner { private String test; final Outer this$0; Outer$Inner() { this.this$0 = Outer.this; super(); }可以清楚的发现,内部类的属性中有这个外部类,并且在内部类的构造函数中有这个外部类属性的初始化。如果一个外部类的实例对象的方法返回了一个内部类的实例对象,而这个内部类对象被长期引用了,那么即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象引用,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。各种连接,如数据库连接、网络连接和 IO 连接等在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对 Connection、Statement 或 ResultSet 不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。变量不合理的作用域一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为 null,很有可能导致内存泄漏的发生。class Outer$Inner { private String test; final Outer this$0; Outer$Inner() { this.this$0 = Outer.this; super(); }如上面这个伪代码,通过 readFromNet 方法把接受的消息保存在变量 msg 中,然后调用 saveDB 方法把 msg 的内容保存到数据库中,此时 msg 已经就没用了,由于 msg 的生命周期与对象的生命周期相同,此时 msg 还不能回收,因此造成了内存泄漏。优化方案:方案1:这个 msg 变量可以放在方法内部,当方法使用完,那么 msg 的生命周期也就结束,就可以回收了。方案2:在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。改变哈希值当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏。这也是 String 为什么被设置成了不可变类型,可以放心地把 String 存入 HashSet,或者把 String 当做 HashMap 的 key 值;当想把自己定义的类保存到散列表的时候,需要保证对象的 hashCode 不可变。/** * 演示内存泄漏 public class ChangeHashCode1 { public static void main(String[] args) { HashSet hs = new HashSet(); Point cc = new Point(); cc.setX(10);//hashCode = 41 hs.add(cc); cc.setX(20);//hashCode = 51 System.out.println("hs.remove = " + hs.remove(cc));//false hs.add(cc); System.out.println("hs.size = " + hs.size());//size = 2 class Point { int x; public int getX() return x; public void setX(int x) this.x = x; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + x; return result; @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Point other = (Point) obj; if (x != other.x) return false; return true; }对象缓存泄漏一旦把对象引用放入到缓存中,就很容易遗忘。比如:代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境则可能会有几百万的数据。优化方案:可以使用 WeakHashMap 代表缓存,此种 Map 的特点是,当除了自身有对 key 的引用外,此 key 没有其他引用那么此 map 会自动丢弃此值。/** * 演示内存泄漏 public class MapTest { static Map wMap = new WeakHashMap(); static Map map = new HashMap(); public static void main(String[] args) { init(); testWeakHashMap(); testHashMap(); public static void init() { String ref1 = new String("obejct1"); String ref2 = new String("obejct2"); String ref3 = new String("obejct3"); String ref4 = new String("obejct4"); wMap.put(ref1, "cacheObject1"); wMap.put(ref2, "cacheObject2"); map.put(ref3, "cacheObject3"); map.put(ref4, "cacheObject4"); System.out.println("String引用ref1,ref2,ref3,ref4 消失"); public static void testWeakHashMap() { System.out.println("WeakHashMap GC之前"); for (Object o : wMap.entrySet()) { System.out.println(o); try { System.gc(); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("WeakHashMap GC之后"); for (Object o : wMap.entrySet()) { System.out.println(o); public static void testHashMap() { System.out.println("HashMap GC之前"); for (Object o : map.entrySet()) { System.out.println(o); try { System.gc(); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("HashMap GC之后"); for (Object o : map.entrySet()) { System.out.println(o); * String引用ref1,ref2,ref3,ref4 消失 * WeakHashMap GC之前 * obejct2=cacheObject2 * obejct1=cacheObject1 * WeakHashMap GC之后 * HashMap GC之前 * obejct4=cacheObject4 * obejct3=cacheObject3 * Disconnected from the target VM, address: '127.0.0.1:51628', transport: 'socket' * HashMap GC之后 * obejct4=cacheObject4 * obejct3=cacheObject3 **/上面代码演示 WeakHashMap 如何自动释放缓存对象:当 init 函数执行完成后,局部变量字符串引用 weakd1,weakd2,d1,d2 都会消失,此时只有静态 map 中保存中对字符串对象的引用,可以看到,调用 gc 之后,HashMap 的没有被回收,而 WeakHashMap 里面的缓存被回收了。监听器和回调内存泄漏另一个常见来源是监听器和其他回调,如果客户端在实现的 API 中注册回调,却没有显式的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将它们保存成为 WeakHashMap 中的键。
JVM(Java虚拟机)JVM 内存模型 结构图jdk1.8 结构图(极简)jdk1.8 结构图(简单)JVM(Java虚拟机):是一个抽象的计算模型。如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。目的是为构建在其上运行的应用程序提供一个运行环境,能够运行 java 字节码。JVM 可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。jdk1.7 结构图(详细)JVM 内存模型 组成元素Java 内存模型主要包含线程私有的程序计数器、java虚拟机栈、本地方法栈和线程共享的堆空间、元数据区、直接内存。Java运行时数据区域Java 虚拟机在执行过程中会将所管理的内存划分为不同的区域,有的随着线程产生和消失,有的随着 Java 进程产生和消失。根据 JVM 规范,JVM 运行时区域大致分为程序计数器、虚拟机栈、本地方法栈、堆、方法区(jkd1.8废弃)五个部分。程序计数器(PC 寄存器、计数器)程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都已自己的程序计数器。可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。倘若执行的是 native 方法,则程序计数器中为空Java 虚拟机栈(JVM Stacks)虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。每个栈帧主要包含的内容如下:局部变量表存储着 java 基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用注意:这里的基本数据类型指的是方法内的局部变量局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。操作数栈动态连接方法返回地址虚拟机栈可能会抛出两种异常:栈溢出(StackOverFlowError):若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常内存溢出(OutOfMemoryError):若虚拟机栈的容量允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OOM 异常本地方法栈(Native Method Stacks)本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。虚拟机栈和本地方法栈的主要区别:虚拟机栈执行的是 java 方法本地方法栈执行的是 native 方法Java 堆(Java Heap)Java 堆中是 JVM 管理的最大一块内存空间。主要存放对象实例。Java 堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。Java 堆的分区:在 jdk1.8 之前,分为新生代、老年代、永久代在 jdk1.8 及之后,只分为新生代、老年代永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代Java 堆内存大小:堆内存大小 = 新生代 + 老年代(新生代占堆空间的1/3、老年代占堆空间2/3)既可以是固定大小的,也可以是可扩展的(通过参数 -Xmx 和 -Xms 设定)如果堆无法扩展或者无法分配内存时报 OOM主要存储的内容是:对象实例类初始化生成的对象基本数据类型的数组也是对象实例字符串常量池字符串常量池原本存放在方法区,jdk8 开始放置于堆中字符串常量池存储的是 string 对象的直接引用,而不是直接存放的对象,是一张 string table静态变量static 修饰的静态变量,jdk8 时从方法区迁移至堆中线程分配缓冲区(Thread Local Allocation Buffer)线程私有,但是不影响 java 堆的共性增加线程分配缓冲区是为了提升对象分配时的效率堆和栈的区别:管理方式,堆需要GC,栈自动释放大小不同,堆比栈大碎片相关:栈产生的碎片远小于堆,因为GC不是实时的分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配效率:栈的效率比堆高方法区(逻辑上)方法区是 JVM 的一个规范,所有虚拟机必须要遵守的。常见的 JVM 虚拟机有 Hotspot 、 JRockit(Oracle)、J9(IBM)方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭 JVM 就会释放这个区域的内存。Java8 以前是放在 JVM 内存中的,由堆空间中的永久代实现,受 JVM 内存大小参数限制Java8 移除了永久代和方法区,引入了元空间拓展:JDK版本方法区的实现运行时常量池所在的位置JDK6PermGen space(永久代)PermGen space(永久代)JDK7PermGen space(永久代)Heap(堆)JDK8Metaspace(元空间)Heap(堆)元空间(元数据区、Metaspace)元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受 JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:类元信息(Class)类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中运行时常量池(Runtime Constant Pool)运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法直接内存(Direct Memory)直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。常见于 NIO 操作时,用于数据缓冲区(比如 ByteBuffer 使用的就是直接内存)。分配、回收成本较高,但读写性能高。直接内存不受 JVM 内存回收管理(直接内存的分配和释放是 Java 会通过 UnSafe 对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。Java 程序内存 = JVM 内存 + 本地内存JVM 内存(JVM 虚拟机数据区)Java 虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机(JVM)内存。JVM 内存受虚拟机内存大小的参数控制,当大小超过参数设置的大小时会报 OOM本地内存(元空间 + 直接内存)对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。虽然不受参数的限制,如果所占内存超过物理内存,仍然会报 OOM堆外内存直接内存直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。可通过 -XX:MaxDirectMemorySize 调整大小,默认和 Java 堆最大值一样内存不足时抛出OutOf-MemoryError或 者OutOfMemoryError:Direct buffer memory;线程堆栈可通过 -Xss 调整大小内存不足时抛出StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)OutOfMemoryError(如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)Socket 缓存区每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。如果无法分配,可能会抛出 IOException:Too many open files异常JNI 代码如果代码中使用了 JNI 调用本地库,那本地库使用的内存也不在堆中,而是占用 Java 虚拟机的本地方法栈和本地内存虚拟机和垃圾收集器虚拟机、垃圾收集器的工作也是要消耗一定数量的内存JVM 堆及各种 GC 详解参考:Java 中的新生代、老年代、永久代和各种 GC结构图(新生代、老年代、永久代)JVM 中的堆,一般分为三大部分:新生代、老年代、永久代( Java8 中已经被移除)新生代、MinorGC(Young GC)新生代主要是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。新生代又分为 Eden、S0、S1(SurvivorFrom、SurvivorTo)三个区:Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。SurvivorFrom 区:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。SurvivorTo 区:保留了一次 MinorGC 过程中的幸存者。Eden 和 S0,S1 区的比例为 8 : 1 : 1幸存者 S0,S1 区:复制之后发生交换,谁是空的,谁就是 SurvivorTo 区JVM 每次只会使用 eden 和其中一块 survivor 来为对象服务,所以无论什么时候,都会有一块 survivor 是空的,因此新生代实际可用空间只有 90%当 JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁。MinorGCMinorGC 的过程(采用复制算法):首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,一般是 15,则赋值到老年代区)同时把这些对象的年龄 + 1(如果 ServicorTo 不够位置了就放到老年区)然后,清空 Eden 和 ServicorFrom 中的对象;最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。Minor GC 触发机制:当年轻代满(指的是 Eden 满,Survivor 满不会引发 GC)时就会触发 Minor GC(通过复制算法回收垃圾)对象年龄(Age)计数器虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值) 来设置。老年代、MajorGC(Old GC)老年代老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。MajorGC 采用标记-清除算法:首先扫描一次所有老年代,标记出存活的对象然后回收没有标记的对象。MajorGC 的耗时比较长(速度一般会比 Minor GC 慢10倍以上,STW 的时间更长),因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。永久代、元数据区(元空间)、常量池永久代(PermGen)是 JDK7 及之前, HotSpot 虚拟机基于 JVM 规范对方法区的一个落地实现,其他虚拟机如 JRockit(Oracle)、J9(IBM) 有方法区 ,但是没有永久代。在 JDK1.8 已经被移除,取而代之的是元数据区(元空间)内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域。和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。元数据区(元空间、Metaspace)元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:-XX:MetaspaceSize (初始空间大小):达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过 MaxMetaspaceSize时,适当提高该值。-XX:MaxMetaspaceSize(最大空间)默认是没有限制的。除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:-XX:MinMetaspaceFreeRatio :在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;-XX:MaxMetaspaceFreeRatio :在GC之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;类的元数据放入本地内存中,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由虚拟机的 MaxPermSize 控制,而由系统的实际可用空间来控制。元空间替换永久代的原因分析:字符串存在永久代中,容易出现性能问题和内存溢出。通常会使用 PermSize 和 MaxPermSize 设置永久代的大小就决定了永久代的上限,但是类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。当使用元空间时,可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。Oracle 可能会将HotSpot 与 JRockit 合二为一。类常量池、运行时常量池、字符串常量池类常量池在类编译过程中,会把类元信息存放到元空间(方法区),类元信息其中一部分便是类常量池主要存放字面量(字面量一部分便是文本字符)和符号引用运行时常量池在类加载时,会将字面量和符号引用解析为直接引用存储在运行时常量池(文本字符会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池)在 JDK6,运行时常量池 存在于 方法区在 JDK7,运行时常量池 存在于 Java 堆字符串常量池存储的是字符串对象的引用,而不是字符串本身字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中(JDK8 时,方法区就是元空间)拓展字面量java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:int a=1; // 这个1便是字面量 String b="iloveu"; // iloveu便是字面量符号引用由于在编译过程中并不知道每个类的地址,因为可能这个类还未加载,所以如果在一个类中引用了另一个类,被引用的类的全限定类名会作为符号引用,在类加载完后用这个符号引用去获取它的内存地址。比如:com.javabc.Solution 类中引用了 com.javabc.Quest,那么 com.javabc.Quest 作为符号引用就会存到类常量池,等类加载完后,就可以拿着这个引用去元空间找此类的内存地址Full GC 、Major GC(Old GC)Minor GC、Major GC、Full GC 的区别新生代收集(Minor GC/Young GC):只是新生代的垃圾收集老年代收集(Major GC/Old GC ):只是老年代的垃圾收集整堆收集(Full GC):收集整个 java 堆(young gen + old gen)和方法区的垃圾收集Full GC 触发机制:调用 System.gc 时,系统建议执行 Full GC,但是不必然执行老年代空间不足方法区空间不足通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载堆空间分成不同区的原因堆空间分为新生代和老年代的原因根据对象存活的时间,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。新生代分为了 eden、Survivor 区的原因为了更好的管理堆内存中的对象,方便GC算法(复制算法)来进行垃圾回收。如果没有 Survivor 区,那么 Eden 每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发 Full GC,而 Full GC 是非常耗时的。将 Eden 区满了的对象,添加到 Survivor 区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即 Survivor 相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。新生代的 Survivor 区又分为 s0 和 s1 区的原因:分两个区的好处就是解决内存碎片化。为什么一个 Survivor 区不行?假设现在只有一个survivor区,模拟一下流程:新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,此时进行 Minor GC,Eden和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。GC 优化的本质,也是为什么分代的原因:减少GC次数和GC时间,避免全区扫描。堆不是对象存储的唯一选择(逃逸分析)如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样无需在堆上分配内存。也无须进行垃圾回收了。逃逸分析概述: 一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,对象只在方法内部引用,则认为没有发生逃逸当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。GC(垃圾回收)System.gc()GC(Garbage Collection)垃圾回收。System.gc() 是用 Java,C#和许多其他流行的高级编程语言提供的API。当它被调用时,它将尽最大努力从内存中清除垃圾(即未被引用的对象)。在默认情况下,通过 System.gc() 或者Runtime.getRuntime().gc() 的调用,会显式触发 Full GC(完整的 GC 事件),对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。在 GC 完成之前,整个 JVM 将冻结(即正在运行的所有服务将被暂停),通常完整的 GC 需要很长时间才能完成。因此在不合适的时间运行 GC,将导致不良的用户体验,甚至是崩溃。JVM 具有复杂的算法,该算法始终在后台运行,进行所有计算以及有关何时触发 GC 的计算。当显式调用 System.gc() 调用时,所有这些计算都将被抛掉。system.gc() 调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)System.gc() 可以从应用程序堆栈的各个部分调用:开发的应用程序可以显式的调用 System.gc() 方法System.gc() 也可以由第三方库,框架触发可以由外部工具(如 VisualVM)通过使用 JMX 触发如果应用程序使用了RMI,RMI会定期调用 System.gc()GC 操作应该由 JVM 自行控制,在绝大部分的场景都不建议程序员手动写代码显式进行 System.gc() 操作。但是也不排除其中个别例外:在开发多个微服务时,每个服务都有多个备份节点。在非业务高峰时段,可以从微服务-负载均衡的节点池中取出其中一个 JVM 实例。然后通过该 JVM 上的 JMX 显式触发 System.gc() 调用,一旦 GC 事件完成并且从内存中清除了垃圾,将该 JVM 放回到微服务-负载均衡的节点池中。当然这个过程需要很好的微服务管理及服务发布机制配合,这样既能保证 JVM 垃圾内存的有效清理,又不影响业务的正常运行。如何检测应用程序正在进行 System.gc()?System.gc() 可以从多个渠道进行的调用,而不仅仅是从应用程序源代码进行的调用。因此,搜索应用程序代码System.gc() 字符串,不足以知道 GC 是否正在被调用。通过 GC 日志可以检测应用程序是否正在进行垃圾回收// java 8 启动 GC 日志:-XX:+PrintGCDetails -Xloggc:<gc-log-file-path> -XX:+PrintGCDetails -Xloggc:/opt/tmp/myapp-gc.log // java 9 启动 GC 日志:-Xlog:gc*:file=<gc-log-file-path> -Xlog:gc*:file=/opt/tmp/myapp-gc.log建议始终在所有生产服务器中始终启用 GC 日志,因为它有助于排除故障并优化应用程序性能。启用GC日志只会增加微不足道的开销。还可以将 GC 日志上传到垃圾收集日志分析器工具,例如GCeasy,HP JMeter等。这些工具将生成丰富的垃圾收集分析报告。如何禁止GC显式调用或调整调用GC的频率?如果就是想避免程序员显式调用GC,避免不成熟的程序员在不合适时间调用GC,避免人为造成的GC崩溃,可以通过如下方法:搜索和替换在代码库中搜索 System.gc() 和 Runtime.getRuntime().gc()如果看到匹配项,则将其删除。但是这种方法无法避免第三方库、框架或通过外部源进行调用。通过JVM参数强制禁止通过传递 JVM 参数 -XX:+DisableExplicitGC 来强制禁止显式调用。这种方式强制、有效,应用程序内的任何 GC 显式代码调用 System.gc() 都将被禁止生效。JVM 自身的 GC 策略不受此参数影响,只禁止人为的触发 GC。RMI如果应用程序正在使用 RMI,则可以控制 GC 调用的频率 。启动应用程序时,可以使用以下JVM参数配置该频率:-Dsun.rmi.dgc.server.gcInterval=n-Dsun.rmi.dgc.client.gcInterval=n这些属性的默认值在JDK 1.4.2 和 5.0 是 60000毫秒(即60秒)JDK 6 和更高版本是 3600000毫秒(即60分钟)如果应用主机内存资源非常富余,可以将这些属性设置为很高的值,以便可以将GC带来的对应用程序的影响最小化。这也是应用程序性能优化的一种方式之一。STW(Stop The World)事件stop-the-world,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿。分析工作必须在一个能确保一致性的快照中进行一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以需要减少 STW 的发生。STW 事件和采用哪款 GC 无关,所有的 GC 都有这个事件。哪怕是 G1 也不能完全避免 Stop-the-world 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。开发中除非特殊情况,不要用 system.gc() 进行手动 GC,会导致 stop-the-world 的发生。GC 常用算法分代收集算法(现在的虚拟机垃圾收集大多采用这种方式)它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以使用的是复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以使用的是标记-整理 或者 标记-清除。标记-清除算法每个对象都会存储一个标记位,记录对象的状态(活着或是死亡)。标记-清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。优点是可以避免内存碎片。标记-压缩(标记-整理)算法标记-压缩法是标记-清除法的一个改进版,和标记清除算法基本相同。不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩(整理),然后把剩下的所有对象全部清除,这样就可以解决内存碎片问题。复制算法复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。当有效内存空间耗尽时,JVM 将暂停程序运行,开启复制算法 GC 线程。接下来 GC 线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC 线程将更新存活对象的内存引用地址指向新的内存地址。此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。复制算法不会产生内存碎片。直接内存(Direct Memory)详解参考:JVM 直接内存文件的读写过程传统 io 方式Java 本身不具备磁盘的读写能力,要想实现磁盘读写,必须调用操作系统提供的函数(即本地方法)。在这里 CPU 的状态改变从用户态(Java)切换到内核态(system)【调用系统提供的函数后】。内存这边也会有一些相关的操作,当切换到内核态以后,就可以由 CPU 的函数,去真正读取磁盘文件的内容,在内核状态时,读取内容后,会在操作系统内存中划出一块儿缓冲区,其称之为系统缓冲区,磁盘的内容先读入到系统缓冲区中(分次进行读取);系统的缓冲区是不能被 Java 代码直接操作的,所以 Java 会先在堆内存中分配一块儿 Java 的缓冲区,即代码中的 new byte[大小],Java 的代码要能访问到刚才读取的那个流中的数据,必须先从系统缓冲区的数据间接读入到 Java 缓冲区,然后 CPU 的状态又切换到用户态了,然后再去调用 Java 的那个输出流的写入操作,就这样反复进行读写读写,把整个文件复制到目标位置。可以发现,由于有两块儿内存,两块儿缓冲区,即系统内存和 Java 堆内存都有缓冲区,那读取的时候必然涉及到这数据存两份,第一次先读到系统缓冲区还不行,因为 Java 代码不能直接访问系统缓冲区,所以需要先把系统缓冲区数据读入到 Java 缓冲区中,这样就造成了一种不必要的数据的复制,效率因而不是很高。directBuffer(直接缓存区)方式当 ByteBuffer 调用 allocateDirect 方法后,操作系统这边划出一块缓冲区,即 direct memory(直接内存),这段区域与之前不一样的地方在于这个操作系统划出来的内存可以被 Java 代码直接访问,即系统可以访问它,Java 代码也可以访问它,它是 java 代码和系统共享的一段内存区域,这就是直接内存。磁盘文件读到直接内存后,Java 代码直接访问直接内存,比传统 io 方式少了一次缓冲区里的复制操作,所以速度得到了成倍的提高。这也是直接内存带来的好处,适合做较大文件拷贝的这种 io 操作。演示案例(运行并比较时间后可以发现,尤其是读写大文件时使用 ByteBuffer 的读写性能非常高):// 演示ByteBuffer作用 public class Demo { static final String FORM = "D:\\asd\\asd.mp4"; // 选比较大的文件,比如200多兆 static final String TO = "D:\\asd.mp4"; static final int _1Mb = 1024 * 1024; public static void main(String[] args) { // io 用时:3187.41008(大概用了3秒),多跑几遍,多比较,跑一次不算。 io(); // directBuffer 用时:951.114625(不到1秒) derectBuffer(); private static void deirectBuffer() { long start = System.nanoTime(); try (FileChannel from = new FileInputStream(FROM).getChannel(); FileChannel to = new FileOutputStream(TO).getChannel(); ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb); // 读写的缓冲区(分配一块儿直接内存) while (true) { int len = from.read(bb); if (len == -1) { break; bb.flip(); to.write(bb); bb.clear(); }catch (IOException e) { e.printStackTrace(); long end = System.nanoTime(); print("directBuffer用时:" + (end - start) / 1000_000.0); // 用传统的io方式做文件的读写 private static void io() { long start = System.nanoTime(); try ( // 网友1:写到try()括号里就不用手动close了 FileInputStream from = new FileInputStream(FROM); FileOutPutStream to = new FileOutputStream(TO); byte[] buf = new byte[_1Mb];// byte数组缓冲区(与上面的读写缓冲区设置大小一致,比较时公平) while (true) { int len = from.read(buf);// 用输入流读 if (len == -1) { break; to.write(buf, 0, len);// 用输出流写 }catch(IOException e) { e.printStackTrace(); long end = System.nanoTime(); print("io用时:" + (end - start) / 1000_000.0); }直接内存的分配和回收直接内存的分配和释放是 Java 通过 UnSafe 对象来管理的,并且回收需要主动调用 freeMemory() 方法,不直接受 JVM 内存回收管理。ByteBuffer 底层分配和释放直接内存的大概情况:ByteBuffer 对象被创建时,调用 Unsafe 对象的 allocateMemory(_1Gb) 方法分配直接内存,返回 long base,即内存地址ByteBuffer 对象被销毁时,调用 unsafe 对象的 freeMemory(base) 方法释放直接内存。ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来检测 ByteBuffer 对象,一旦 ByteBuffer 对象被(Java)垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory() 方法来释放直接内存。演示案例(演示直接内存溢出)运行后,输出 36即循环 36 次(一次 100 兆,循环 36 次也算 3 个 G 多了)后,爆出直接内存溢出异常:Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory// 演示直接内存溢出 public class Demo { static int _100Mb = 1024 * 1024 * 100; public static void main(String[] args) { List<ByteBuffer> list = new ArrayList<>(); int i = 0; try { while (true) { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);// 每次分配100兆内存 list.add(byteBuffer);// 把这玩意放到List中,一直循环 }finally { print(i); }使用 System.gc() 间接进行直接内存的回收可能存在的问题代码案例public class Demo { static int _1Gb = 1024 * 1024 * 1024; public static void main(String[] args) throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb); print("分配完毕"); print("开始释放"); byteBuffer = null; System.gc(); // 显式的垃圾回收 }System.gc() 触发的是一次 Full GC,是比较影响性能的垃圾回收 ,不光要回收新生代,还要回收老年代,所以它造成的程序暂停时间比较长。为了防止一些程序员不小心在代码里经常写 System.gc() 以触发显式的垃圾回收,做一些 JVM 调优时经常会加上 JVM 虚拟机参数 -XX:+DisableExplicitGC,禁用这种显式的垃圾回收,也就是让 System.gc() 代码无效。但是加上这个虚拟机参数后,可能会间接影响到直接内存的回收机制。没加虚拟机参数的话,由于 byteBuffer 被 null 了,显式触发 Java 垃圾回收,byteBuffer 的堆内存被回收时,会调用 unsafe 对象的 freeMemory(base) 方法释放直接内存,所以也导致了直接内存也被释放掉。加虚拟机参数之后,System.gc() 代码失效,虽然 byteBuffer 被 null 了,但如果内存比较充足,那么它还会暂时存活着,其创建的直接内存(ByteBuffer.allocateDirect(-1Gb))也会在 byteBuffer 的堆内存被 JVM 自动进行垃圾回收前一直存在着。所以禁用 System.gc() 之后,会发现别的代码不受太大影响,但直接内存会受到影响,因为不能用显式的方法回收掉Bytebuffer,所以 ByteBuffer 只能等到 JVM 自动进行垃圾回收时,才会被清理,从而它所对应的那块儿直接内存在此之前也会一直不会被释放掉,这就会造成直接内存可能占用较大,长时间得不到释放这样一个现象。所以使用直接内存的情况比较多,由程序员直接手动的管理直接内存时,推荐用 Unsafe 的相关方法,直接调用 Unsafe 对象的 freeMemory() 方法来释放直接内存。JVM 的性能调优调优参数配置方式java [options] MainClass [arguments]options :JVM 启动参数。 配置多个参数的时候,参数之间使用空格分隔。参数命名: 常见为 -参数名参数赋值: 常见为 -参数名=参数值 或 -参数名:参数值内存参数:-Xms(s 为 strating):初始堆大小,JVM启动的时候,给定堆空间大小。可以设置与-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。示例:-Xms3550m :设置 JVM 初始内存为 3550M。-Xmx(x 为 max):最大堆大小,JVM运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。示例:-Xmx3550m :设置 JVM 最大可用内存为 3550M。-Xmn(n 为 new):新生代大小整个堆大小 = 新生代大小 + 老年代大小 + 持久代大小(jkd1.8废弃)持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的 3/8示例:-Xmn2g:设置年轻代大小为2G。-Xss:设置每个线程的 Java 栈大小。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。JDK5.0 以后每个线程 Java 栈大小为1M,以前每个线程堆栈大小为 256K。示例:-Xss128k :设置每个线程的堆栈大小为128k。-XX:NewSize=n:设置年轻代大小-XX:NewRatio=n:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值。示例:设置为 4 :年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。示例:设置为 3 :表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5。-XX:MaxPermSize=n:设置永久代大小示例:-XX:MaxPermSize=16m:设置持久代大小为16m。-XX:MaxTenuringThreshold=n:设置垃圾最大年龄如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。垃圾回收器参数JVM给了三种选择:串行收集器、并行收集器、并发收集器。串行收集器只适用于小数据量的情况。-XX:+UseSerialGC: 设置串行收集器。-XX:+UseParallelGC: 设置并行收集器,表示年轻代使用并行收集器。-XX:+UseParNewGC: 设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。-XX:+UseParallelOldGC: 设置并行年老代收集器JDK6.0 支持对年老代并行收集。-XX:+UseConcMarkSweepGC: 设置年老代并发收集器 CMS。-XX:+UseG1GC: 设置G1收集器-XX:ParallelGCThreads=n: 设置并行收集器收集时最大线程数使用的CPU数。并行收集线程数。-XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间,单位毫秒。可以减少STW时间。-XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n) 并发收集器设置-XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况。-XX:+UseAdaptiveSizePolicy: 设置此选项后,并行收集器会自动选择年轻代区大小和相应的 Survivor 区比例,以达到目标系统规定的最低相应时间或者收集频率等。此值建议使用并行收集器时,一直打开。-XX:CMSFullGCsBeforeCompaction=n: 此值设置运行多少次 GC 以后对内存空间进行压缩、整理。因为并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片。元空间参数:-XX:MetaspaceSize:初始化的 Metaspace 大小,该值越大触发 Metaspace GC 的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控 Metaspace 的大小,而上下浮动主要由 -XX:MaxMetaspaceFreeRatio 和 -XX:MinMetaspaceFreeRatio 两个参数控制。在默认情况下,这个值大小根据不同的平台在 12M 到 20M 浮动。使用 java -XX:+PrintFlagsInitial 命令查看本机的初始化参数。-XX:MinMetaspaceFreeRatio:当进行过 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增加 MetaspaceSize 的大小(为了避免过早引发一次垃圾回收)。默认值为40,也就是40%。设置该参数可以控制 Metaspace 的增长的速度,太小的值会导致 Metaspace 增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致 Metaspace 增长的过快,浪费内存。-XX:MaxMetaspaceFreeRatio:当进行过 Metaspace GC 之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会减小 MetaspaceSize 的大小。默认值为70,也就是70%。-XX:MaxMetaspaceExpansion :Metaspace 增长时的最大幅度。默认值大约为5MB。-XX:MinMetaspaceExpansion :Metaspace 增长时的最小幅度。默认值大约330KB。-XX:MaxMetaspaceSize:最大空间。默认是没有限制的。指定该值可以防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。辅助参数JVM提供了大量命令行参数,打印信息,供调试使用。商业项目上线的时候,不允许使用。一定使用 loggc。主要有以下一些:-XX:+PrintGC输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs][Full GC 121376K->10414K(130112K), 0.0650971 secs]-XX:+PrintGCDetails输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs][GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]-XX:+PrintGCTimeStamps -XX:+PrintGC:可与上面两个混合使用输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]-XX:+PrintGCApplicationConcurrentTime :打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用输出形式:Application time: 0.5291524 seconds-XX:+PrintGCApplicationStoppedTime: 打印垃圾回收期间程序暂停的时间。可与上面混合使用输出形式:Total time for which application threads were stopped: 0.0468229 seconds-XX:PrintHeapAtGC :打印GC前后的详细堆栈信息-Xloggc:filename :与上面几个配合使用,把相关日志信息记录到文件以便分析。调优建议年轻代大小选择响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达老年代的对象。吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8 CPU 以上的应用。老年代大小选择响应时间优先的应用: 老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:并发垃圾收集信息持久代并发收集次数传统GC信息花在年轻代和年老代回收上的时间比例减少年轻代和老年代花费的时间,一般会提高应用的效率吞吐量优先的应用: 一般吞吐量优先的应用都有一个很大的年轻代和一个较小的老年代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。较小堆引起的碎片问题因为老年代的并发收集器使用标记-清除算法,所以不会对堆进行压缩。当收集器回收时,它会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记-清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次 Full GC 后,对老年代进行压缩
简介Feign 可以把 Rest 的请求进行隐藏,伪装成类似 Spring MVC 的 Controller 一样。不用再自己拼接 url,拼接参数等等操作,一切都交给Feign去做。 入门案例在服务消费者导入依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>在启动类上添加@EnableFeignClients注解Feign中已经自动集成了Ribbon负载均衡,因此不需要自己定义RestTemplate了@SpringCloudApplication @EnableFeignClients // 开启Feign注解 public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); }编写Feign客户端@FeignClient(value = "user-service") // 添加FeignClient,指定服务ID public interface UserClient { * 声明一个feign的接口,它的实现是服务提供者的controller实现 @GetMapping("/user/{id}") User getById(@PathVariable("id") Long id); }代码中调用,使用userClient访问:@Autowired // 注入UserClient private UserClient userClient; public User getUserById(@PathVariable long id) { User user = userClient.getById(id); return user; }@FeignClient 注解说明@FeignClient:声明这是一个 Feign 客户端,同时通过 name / value 属性指定服务名称只能标注在远程服务调用的接口上(FeignClient注解被 @Target(ElementType.TYPE) 修饰,表示 FeignClient 注解的作用目标在接口上)当 SpringCloud 扫描到被 @FeignClient 标识的接口时,底层会为其创建实现类代理对象(jdk代理),并交给 spring容器管理(注册IOC容器);创建出来 Bean 对象的名字由注解的name或value属性指定,若对同一个服务创建两个远端服务调用接口时会报错接口中定义的方法,完全采用 SpringMVC 的注解,Feign 会根据注解生成 URL,并访问获取结果服务的启动类或配置类需要标注 @EnableFeignClients 注解才能使 Fegin 生效@FeignClient注解的常用属性如下:name / value:指定 FeignClient 的名称,如果项目使用了Ribbon(注册中心),name属性会作为微服务的名称,用于服务发现url:一般用于调试,可以手动指定@FeignClient调用的地址。默认为空url 可以从配置文件获取,如果有则通过 url 调用,没有则根据服务名调用。格式为 url = "${xxx.xxx.xxx: }"configuration:Feign配置类,可以自定义 Feign 的 Encoder、Decoder、LogLevel、Contract,可以为每一个 FeignClient 指定不同的配置fallback:定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口fallbackFactory:工厂类,用于生成 fallback 类示例,通过这个属性可以实现每个接口通用的容错逻辑,减少重复的代码path:定义当前 FeignClient 的统一前缀,当项目中配置了 server.context-path,server.servlet-path 时使用decode404:当发生 http 404 错误时,如果该字段位 true,会调用 decoder 进行解码,否则抛出 FeignException调用方式:方式一:接口提供方在注册中心如果服务提供方已经注册到注册中心了,那么name或者value的值为:服务提供方的服务名称。必须为所有客户端指定一个name或者value@FeignClient(value="run-product",fallback = ProductClientServiceFallBack.class)方式二:单独的一个http接口,接口提供方没有注册到注册中心@FeignClient(name="runClient11111",url="localhost:8001")此处name的值为:调用客户端的名称以上两种方式都能正常调用。name可以为注册中心的服务名称,加上url属性时,name的值就与注册中心服务名称无关。Feign Client 的配置feign 配置是在 ribbon 配置的基础上做了扩展,可以支持服务级超时时间配置,所以,feign 配置和 ribbon 配置的效果是⼀样的。SpringCloud 对配置的优先级顺序如下:Feign局部配置 > Feign全局配置 > Ribbon局部配置 > Ribbon全局配置配置文件属性和配置类的优先级顺序为:配置文件属性配置 > 配置类代码配置feign: client: config: default: # 全部服务配置 connectTimeout: 5000 # 建立连接的超时时长,单位:毫秒。默认为1000 readTimeout: 5000 # 指建立连接后从服务端读取到可用资源所用的超时时间,单位:毫秒。默认为1000 loggerLevel: FULL # 日志级别 errorDecoder: com.example.SimpleErrorDecoder # Feign的错误解码器,相当于代码配置方式中的ErrorDecoder retryer: com.example.SimpleRetryer # 配置重试,相当于代码配置方式中的Retryer requestInterceptors: # 配置拦截器,相当于代码配置方式中的RequestInterceptor - com.example.FooRequestInterceptor - com.example.BarRequestInterceptor decode404: false # 是否对404错误解码 encode: com.example.SimpleEncoder decoder: com.example.SimpleDecoder contract: com.example.SimpleContract serverName: # 单独给某⼀服务配置。serverName是服务名,使⽤的时候要⽤服务名替换掉这个 connectTimeout: 5000 readTimeout: 5000Feign 请求添加 headers方案一:方法上的 @RequestMapping 注解添加 headers 信息@RequestMapping 注解的属性中包含一个 headers 数组,可以在指定的方法上 @RequestMapping 注解中添加需要的 headers,可以是写死的,也可以读取配置=同理 @RequestMapping 一组的 @PostMapping,@GetMapping 注解等均适用@FeignClient(name = "server",url = "127.0.0.1:8080") public interface FeignTest { @RequestMapping(value = "/test",headers = {"app=test-app","token=${test-app.token}"}) String test(); }方案二:接口上的@RequestMapping注解添加headers信息如果同一个接口中所有的方法都需要同样的headers,可以在接口上的@RequestMapping注解中添加headers,使整个接口的方法均被添加同样的headers@FeignClient(name = "server",url = "127.0.0.1:8080") @RequestMapping(value = "/",headers = {"app=test-app","token=${test-app.token}"}) public interface FeignTest { @RequestMapping(value = "/test") String test(); }方案三:使用@Headers注解添加headers信息(不推荐)@FeignClient(name = "server",url = "127.0.0.1:8080") @Headers({"app: test-app","token: ${test-app.token}"}) public interface FeignTest { @RequestMapping(value = "/test") String test(); }查看openfeign官方文档发现其使用的是@Headers来添加headers,测试发现并没有生效,spring cloud使用了自己的SpringMvcContract来解析注解,所以需要自己实现一个Contract来实现对@Headers注解的支持,具体实现参照(https://juejin.im/post/6844903961653149709)方案四:自定义RequestInterceptor添加headers信息feign提供了一个拦截器接口RequestInterceptor,实现RequestInterceptor接口就可以实现对feign请求的拦截,接口提供了一个方法apply(),实现apply()方法实现apply()方法直接添加header会拦截所有的请求都加上headers,如果不是所有的feign请求都需要用到不建议此方法@Component public class FeignRequestInterceptor implements RequestInterceptor { @Value("${test-app.token}") private String token; @Override public void apply(RequestTemplate requestTemplate) { requestTemplate.header("app","test-app");//静态 requestTemplate.header("token",token);//读配置 }方案五:自定义RequestInterceptor实现添加动态数据到header以上方案都不适合把动态的数据放入headers中,而通常场景下可能经常需要把计算的签名,用户id等动态信息设置到headers,所以还需要一个更加完善的方案。方案1/2/3均不能设置动态的值,方案4可以设置动态值,但是没做到请求的区分,所以在方案4的基础上进行改进得到了较为完善的方案5。具体实现如下:在请求调用代码中,获取到HttpServletRequest对象,将需要添加到headers中的值封装成一个map后放入HttpServletRequest的attribute域中 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = Objects.requireNonNull(attributes).getRequest(); String signedMsg = getSignedMsg(reqJson); // 计算签名字符串 Map<String, String> reqMap = new HashMap<>(); reqMap.put("content-type", "application/json"); //常量字段 reqMap.put("accessKey", accessKey); //常量字段 reqMap.put("signedMsg", signedMsg); //动态计算/获取字段 request.setAttribute("customizedRequestHeader", reqMap);在自定义RequestInterceptor中获取到HttpServletRequest对象的attribute域中指定的key,将key对应map中的所有参数加入到headers。@Component public class FeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 设置自定义header // 设置request中的attribute到header以便转发到Feign调用的服务 Enumeration<String> reqAttrbuteNames = request.getAttributeNames(); if (reqAttrbuteNames != null) { while (reqAttrbuteNames.hasMoreElements()) { String attrName = reqAttrbuteNames.nextElement(); if (!"customizedRequestHeader".equalsIgnoreCase(attrName)) { continue; Map<String,String> requestHeaderMap = (Map)request.getAttribute(attrName); for (Map.Entry<String, String> entry : requestHeaderMap.entrySet()) { requestTemplate.header(entry.getKey(), entry.getValue()); break; }负载均衡 (Ribbon)Feign中本身已经集成了Ribbon依赖和自动配置,默认支持Ribbon。Fegin内置的ribbon默认设置了请求超时时长,默认是1000ms。因为Ribbon内部有重试机制,一旦超时,会自动重新发起请求可以通过配置来修改:全局配置 使用 ribbon.=ribbon: ReadTimeout: 2500 # 数据通信超时时长,单位:ms。默认为1000 ConnectTimeout: 500 # 连接超时时长,单位:ms。默认为1000 OkToRetryOnAllOperations: false # 是否对所有的异常请求(连接异常和请求异常)都重试。默认为false MaxAutoRetriesNextServer: 1 # 最多重试多少次连接服务(实例)。默认为1。不包括首次调用 MaxAutoRetries: 0 # 服务的单个实例的重试次数。默认为0。不包括首次调用 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 切换负载均衡策略为随机。默认为轮询策略指定服务配置 <服务名称>.ribbon. = serverName: # 单独给某⼀服务配置。serverName是服务名,使⽤的时候要⽤服务名替换掉这个 ribbon: connectTimeout: 5000 readTimeout: 5000容错机制Hystrix支持Feign默认也有对Hystix的集成,只不过,默认情况下是关闭的。需要通过下面的参数来开启:feign: hystrix: enabled: true # 开启hystrix熔断机制 hystrix: command: default: # 全局默认配置 execution: # 线程隔离相关 timeout: enabled: true # 是否给方法执行设置超时时间,默认为true。一般不改。 isolation: strategy: THREAD # 配置请求隔离的方式,这里是默认的线程池方式。还有一种信号量的方式semaphore, thread: timeoutlnMilliseconds: 10000 # 方式执行的超时时间,默认为1000毫秒,在实际场景中需要根据情况设置 circuitBreaker: # 服务熔断相关 requestVolumeThreshold: 10 # 触发熔断的最小请求次数,默认20 sleepWindowInMilliseconds: 10000 # 休眠时长,单位毫秒,默认是5000毫秒 errorThresholdPercentage: 50 # 触发熔断的失败请求最小占比,默认50% serverName: # 单独给某⼀服务配置 execution: timeout: enabled: true isolation: strategy: THREAD thread: timeoutlnMilliseconds: 10000注意:Hystix的超时时间应该比Ribbon重试的总时间要大 ,否则Hystrix命令超时后,该命令直接熔断,重试机制就没有任何意义了。Ribbon:总重试次数 = 访问的服务器数 * 单台服务器最大重试次数即 总重试次数 = (1+MaxAutoRetriesNextServer)*(1+MaxAutoRetries )Hystrix超时时间 > (Ribbon超时时间总和)* 重试次数故 建议hystrix的超时时间为:( ( 1+MaxAutoRetriesNextServer) * (1+MaxAutoRetries ) ) * (ReadTimeout + connectTimeout)MaxAutoRetries:Ribbon配置: 服务的单个实例的重试次数。不包括首次调用MaxAutoRetriesNextServer:Ribbon配置: 最多重试多少次连接服务(实例)。不包括首次调用ReadTimeout:Ribbon配置: 通信超时时间connectTimeout:Ribbon配置: 建立连接超时时间Sentinel支持Sentinel 可以通过并发线程数模式的流量控制来提供信号量隔离的功能。并且结合基于响应时间的熔断降级模式,可以在不稳定资源的平均响应时间比较高的时候自动降级,防止过多的慢调用占满并发数,影响整个系统。依赖 <!--Sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>在配置文件中开启Feign对Sentinel的支持feign: sentinel: enabled: trueFeign开启容错机制支持后的使用方式Feign 开启 Hystrix 或 Sentinel 容错机制支持后的使用方式均是如下两种:方案一:直接继承被容错的接口,并为每个方法实现容错方案方案二:实现FallbackFactory接口方案一:直接继承被容错的接口,并为每个方法实现容错方案定义一个类,作为fallback的处理类 。直接继承被容错的接口,并为每个方法实现容错方案@Component public class UserClientFallback implements UserClient { @Override public User getById(Long id) { return new User(1L, "我是备份-feign", 18, new Date()); }在 @FeignClient 注解中使用 fallback 属性指定自定义的容错处理类@FeignClient(value = "user-service",fallback = UserClientFallback.class) public interface UserClient { @GetMapping("/user/{id}") User getById(@PathVariable("id") Long id); }测试验证方案二:实现FallbackFactory接口。可以拿到具体的服务错误信息,便于后期排查问题@FeignClient(value="34-SPRINGCLOUD-SERVICE-GOODS", fallbackFactory = GoodsRemoteClientFallBackFactory.class) public interface GoodsRemoteClient { @RequestMapping("/service/goods") public ResultObject goods(); }@Component public class GoodsRemoteClientFallBackFactory implements FallbackFactory<GoodsRemoteClient> { @Override public GoodsRemoteClient create(Throwable throwable) { return new GoodsRemoteClient() { @Override public ResultObject goods() { String message = throwable.getMessage(); // message即为错误信息 System.out.println("feign远程调用异常:" + message); return new ResultObject(); }请求压缩(feign.compression)Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:feign: compression: request: enabled: true response: enabled: true也可以对请求的数据类型,以及触发压缩的大小下限进行设置,只有超过这个大小的请求才会对其进行压缩:feign: compression: request: enabled: true mime-types: text/xml,application/xml,application/json min-request-size: 2048日志级别通过logging.level.xx=debug来设置日志级别。然而这个对Fegin客户端而言不会产生效果。因为@FeignClient注解修改的客户端在被代理时,都会创建一个新的Fegin.Logger实例,所以需要额外指定这个日志的级别才可以。Feign支持4种日志级别:NONE:不记录任何日志信息,这是默认值。BASIC:仅记录请求的方法,URL以及响应状态码和执行时间HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。全局配置方式一:配置文件属性实现feign: client: config: default: # 将调用的微服务名称设置为default即为配置成全局 loggerLevel: FULL方式一:代码实现//在启动类上为@EnableFeignClients注解添加defaultConfiguration配置 @EnableFeignClients(defaultConfiguration = FeignConfig.class)细粒度(指定服务配置)方式一:配置文件实现feign: client: config: server-1: # 想要调用的微服务名称 loggerLevel: FULL方式二:代码实现1)编写配置类,定义日志级别@Configuration public class FeignConfig { @Bean Logger.Level level(){ return Logger.Level.FULL; }2)在FeignClient中指定配置类:(可以省略)@FeignClient(value = "user-service", configuration = FeignConfig.class) // 添加FeignClient,指定服务ID public interface UserClient { @GetMapping("/user/{id}") User getById(@PathVariable("id") Long id); }Feign每次访问的日志示例:
概述Nacos官方文档:https://nacos.io/zh-cn/docs/what-is-nacos.htmlNacos 致力于发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理:Kubernetes ServicegRPC & Dubbo RPC ServiceSpring Cloud RESTful ServiceNacos 的关键特性包括:服务发现和服务健康监测Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助根据健康状态管理服务的可用性及流量。动态配置服务动态配置服务以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。Nacos 提供了一个简洁易用的UI 管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,可以更安全地在生产环境中管理配置变更和降低配置变更带来的风险。动态 DNS 服务动态 DNS 服务支持权重路由,可以更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能更容易地实现以 DNS 协议为基础的服务发现,以消除耦合到厂商私有服务发现 API 上的风险。Nacos 提供了一些简单的 DNS APIs TODO 管理服务的关联域名和可用的 IP:PORT 列表.服务及其元数据管理Nacos 能从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。nacos 整合参考:Spring cloud alibaba--Nacos Config配置管理依赖引入 <!-- nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2.2.1.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.1.RELEASE</version> </dependency>注意:若使用示例的依赖版本号,Spring Boot 版本要低于 2.4,否则启动应用会报错。SpringBoot、SpringCloud 和 nacos 集成版本对应关系对照(版本若对应不上,应用可能会启动报错):配置 Nacos项目中默认配置文件是 application.properties ,Nacos 配置加在此配置文件中的话,应用启动会报连接 Nacos 失败,需要创建 bootstrap.properties 或 bootstrap.yml 配置文件(添加任意一个即可),下面以 bootstrap.yml 为例:spring: application: # 项目(微服务)名称 name: apm-mobile-android cloud: nacos: # nacos用户名 username: nacos # nacos用户密码 password: nacos # nacos服务器地址 server-addr: 10.0.7.115:18117 # nacos配置中心相关 config: # 开启nacos作为配置中心,默认值:true enabled: true # 作为配置中心的nacos服务器地址,默认值:${spring.cloud.nacos:server-addr} #server-addr: 10.0.7.115:18117 # 配置文件读取的nacos命名空间ID,默认值:public namespace: PROD # 配置文件在nacos命名空间中的分组,默认值:DEFAULT_GROUP group: apm # 配置文件的文件前缀(配置文件名称),默认值:${spring.application.name} prefix: ${spring.application.name} # 配置文件的文件后缀(文件类型),默认值:properties file-extension: properties # 配置内容的编码方式,默认值:UTF-8 encode: UTF-8 # 获取配置的超时时间,单位:ms,默认值:3000 timeout: 3000 # 开启监听和自动刷新,动态感知配置变化,默认值:true refresh-enabled: true # AccessKey #access-key: 123 # SecretKey #secret-key: 123 # 引入共享配置(同一分组) shared-configs: # 配置支持共享的 Data Id - data-id: comm.properties # 配置 Data Id 所在分组,缺省默认 DEFAULT_GROUP group: DEFAULT_GROUP # 配置Data Id 在配置变更时,是否动态刷新,缺省默认 false refresh: true # 引入扩展配置(同一分组或不同分组) extension-configs: # 配置支持共享的 Data Id - data-id: comm.properties # 配置 Data Id 所在分组,缺省默认 DEFAULT_GROUP group: DEFAULT_GROUP # 配置Data Id 在配置变更时,是否动态刷新,缺省默认 false refresh: true # nacos注册中心相关 discovery: # 开启nacos作为服务注册中心,默认值:true enabled: true # 作为注册中心的nacos服务器地址,默认值:${spring.cloud.nacos:server-addr} #server-addr: 10.0.7.115:18117 # 服务注册在nacso上的命名空间ID,默认值:public namespace: PROD加载 Nacos 配置中心配置项在初始化类中添加 @EnableDiscoveryClient 注解即可:import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient // 开启服务发现客户端,加载Nacos配置中心配置项 @SpringBootApplication public class SpringbootdemoApplication { public static void main(String[] args) { SpringApplication.run(SpringbootdemoApplication.class, args); new BootstrapManager(); }Nacos 配置中心配置项动态生效前置条件:bootstrap 配置文件中 nacos 的配置项 refresh-enabled: true(默认开启)# 开启监听和自动刷新,动态感知配置变化,默认值:true spring.cloud.nacos.config.refresh-enabled=true方式一:@Value + @RefreshScope 获取最新值@RestController @RefreshScope // 配置项动态生效 public class TestController { @NacosValue(value = "${test.data}", autoRefreshed = true) private String data; @Value(value = "${test.data}") private String datas; @GetMapping("test") public String test() { return "data :" + data + ",datas="+datas; }方式二:通过 applicationContext.getEnvironment.getProperty 获取最新值@SpringBootApplication public class NacosConfigSimpleApplication { public static void main(String[] args) throws InterruptedException { ConfigurableApplicationContext applicationContext = SpringApplication.run(NacosConfigSimpleApplication.class, args); //取到Spring的配置环境 while(true){ ConfigurableEnvironment environment = applicationContext.getEnvironment(); String username = environment.getProperty("user.name"); String age = environment.getProperty("user.age"); System.out.println("username:"+username+" | age:"+age); TimeUnit.SECONDS.sleep(1); }方式三:通过 @NacosValue 获取最新值standalone 使用,@NacosValue 获取最新值 nacos,配置信息需要写在配置类上@Configuration @EnableNacosConfig(globalProperties = @NacosProperties(serverAddr = "127.0.0.1:8848")) @NacosPropertySource(dataId = "example", group="test",autoRefreshed = true) public class NacosConfiguration { }@Controller public class ConfigController { @NacosValue(value = "${test.data}", autoRefreshed = true) private boolean data; @RequestMapping(value = "/test", method = GET) @ResponseBody public boolean get() { return data; } }引入公共的配置文件默认读取的配置文件是与服务名相同的配置文件的配置;若是还有其它公共的配置需要读取进来,就涉及到多个配置文件的读取。在 bootstrap 配置文件中引入公共配置文件的方式:方式一:拓展配置方式可以引入与默认配置文件同一分组或不同分组的公共配置文件使用 extension-configs 按数组的方式引入配置,设置data_id、group、refresh值# 配置支持共享的 Data Id spring.cloud.nacos.config.extension-configs[0].data-id=comm.properties # 配置 Data Id 所在分组,缺省默认 DEFAULT_GROUP spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP # 配置Data Id 在配置变更时,是否动态刷新,缺省默认 false spring.cloud.nacos.config.extension-configs[0].refresh=true使用 ext-configs 按数组的方式引入配置,设置data_id、group、refresh值(ext-config 官方标记废弃;取而代之的是 extension-configs)方式二:共享配置方式只能引入与默认配置文件同一分组的公共配置文件使用 shared-configs 按数组的方式引入配置,设置data_id、group、refresh值# 配置支持共享的 Data Id spring.cloud.nacos.config.shared-configs[0].data-id=comm.properties # 配置 Data Id 所在分组,缺省默认 DEFAULT_GROUP spring.cloud.nacos.config.shared-configs[0].group=DEFAULT_GROUP # 配置Data Id 在配置变更时,是否动态刷新,缺省默认 false spring.cloud.nacos.config.shared-configs[0].refresh=true使用 shared-dataids 来指定要读取共享配置文件的 DataID ,多个文件用 , 分隔使用 refreshable-dataids 指定共享配置文件,支持自动刷新(shared-dataids 官方标记废弃;取而代之的是 shared-configs)spring.cloud.nacos.config.shared-dataids=shareconfig1.yml,shareconfig2.yml spring.cloud.nacos.config.refreshable-dataids=shareconfig1.yml,shareconfig2.yml配置文件读取优先级:profile > 默认 > extension-configs(数组下标越大优先级越大) > shared-configs(数组下标越大优先级越大)数组下标越大优先级越大 即 数组中后面引入的会覆盖前面引入的相同项支持 profile 粒度的配置spring-cloud-starter-alibaba-nacos-config 在加载配置的时候,不仅加载了以 dataid 为 ${spring.application.name}.${file-extension:properties} 为前缀的文件,还加载了 dataid 为 ${spring.application.name}-${profile}.${file-extension:properties} 的基础配置。在日常开发环境中,若是遇到多套配置的情况,可以使用 spring 提供的 ${spring.profiles.active} 这个配置项配置选择哪个配置#选择加载的文件为application-dev.properties spring.profiles.active=dev注意:只有默认的配置文件才能结合 profile 使用;除了默认配置文件,其它配置文件(例如:dev)都需要写上后缀。并且后缀必须跟配置的 boostrap.properties 中配置的扩展名(spring.cloud.nacos.config.file-extension=?)一致默认配置文件:跟服务名spring.application.name相同的DataId的配置文件(无文件扩展名),称之为默认配置文件部署 nacosdocker stack 部署 nacos 集群docker stack 部署 nacos 集群的部署模板:docker-compose-swarm-nacos.yml注:@nacos_image、@mysql_image 替换为实际的镜像@nacos_logs_path、@nacos_data_path替换为实际的存储路径version: "3.5" services: nacos1: hostname: nacos1 container_name: nacos1 image: @nacos_image volumes: - @nacos_logs_path/nacos1:/home/nacos/logs ports: # 8848端口是Nacos对客户端提供服务的端口 - "32101:8848" expose: # 7848是Nacos集群通信端口,用于Nacos集群间进行选举,检测等 - "7848" environment: NACOS_REPLICAS: 3 MYSQL_SERVICE_HOST: nacos-mysql MYSQL_SERVICE_DB_NAME: nacos_devtest MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_USER: nacos MYSQL_SERVICE_PASSWORD: nacos MODE: cluster NACOS_SERVER_PORT: 8848 PREFER_HOST_MODE: hostname NACOS_SERVERS: nacos1:8848 nacos2:8848 nacos3:8848 restart: always networks: - apps_net depends_on: - nacos-mysql deploy: labels: name: nacos1 placement: constraints: - node.role == manager nacos2: hostname: nacos2 image: @nacos_image container_name: nacos2 volumes: - @nacos_logs_path/nacos2:/home/nacos/logs expose: - "8848" - "7848" environment: NACOS_REPLICAS: 3 MYSQL_SERVICE_HOST: nacos-mysql MYSQL_SERVICE_DB_NAME: nacos_devtest MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_USER: nacos MYSQL_SERVICE_PASSWORD: nacos MODE: cluster NACOS_SERVER_PORT: 8848 PREFER_HOST_MODE: hostname NACOS_SERVERS: nacos1:8848 nacos2:8848 nacos3:8848 restart: always networks: - apps_net depends_on: - nacos-mysql deploy: labels: name: nacos2 placement: constraints: - node.role == worker nacos3: hostname: nacos3 image: @nacos_image container_name: nacos3 volumes: - @nacos_logs_path/nacos3:/home/nacos/logs expose: - "8848" - "7848" environment: NACOS_REPLICAS: 3 MYSQL_SERVICE_HOST: nacos-mysql MYSQL_SERVICE_DB_NAME: nacos_devtest MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_USER: nacos MYSQL_SERVICE_PASSWORD: nacos MODE: cluster NACOS_SERVER_PORT: 8848 PREFER_HOST_MODE: hostname NACOS_SERVERS: nacos1:8848 nacos2:8848 nacos3:8848 restart: always networks: - apps_net depends_on: - nacos-mysql deploy: labels: name: nacos3 placement: constraints: - node.role == worker nacos-mysql: hostname: nacos-mysql container_name: nacos-mysql image: @mysql_image volumes: - @nacos_data_path:/var/lib/mysql ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: nacos_devtest MYSQL_USER: nacos MYSQL_PASSWORD: nacos restart: always networks: - apps_net deploy: labels: name: mysql placement: constraints: - node.role == manager networks: apps_net: # 引用已创建的网络。手动创建命令:docker network create -d overlay --attachable apps_net external: truek8s 部署 nacos 集群使用 k8s 的 helm 工具部署 nacos 集群的部署模板:values.yamlmysqlImage: "harbor.paic.com.cn/library/nacos-mysql:latest" nacosImage: "harbor.paic.com.cn/library/nacos-service:1.4.1" busyboxImage: "harbor.paic.com.cn/library/busybox:1.30.0" mysqlHost: "mysql" mysqlPort: "3306" mysqlDatabase: "nacos_devtest" mysqlRootPassword: password mysqlUser: nacos mysqlPassword: nacos nfsPath: /home/nacos/ nacosDataPath: /home/nacos/data service_name: nacos namespace: publicChart.yamlapiVersion: v1 appVersion: "1.0" description: A Helm chart for Kubernetes name: nacos version: 0.1.0 maintainers: - name: nametemplates 文件夹下 configmap.yaml--- apiVersion: v1 kind: ConfigMap metadata: name: nacos-cm namespace: {{ .Values.namespace }} data: mysql.host: mysql mysql.port: "3306" mysql.db.name: {{ .Values.mysqlDatabase }} mysql.user: {{ .Values.mysqlUser }} mysql.password: {{ .Values.mysqlPassword }}templates 文件夹下 mysql.yamlapiVersion: v1 kind: ReplicationController metadata: name: mysql namespace: {{ .Values.namespace }} labels: name: mysql spec: replicas: 1 selector: name: mysql template: metadata: labels: name: mysql spec: containers: - name: mysql image: {{ .Values.mysqlImage }} ports: - containerPort: 3306 volumeMounts: - name: mysql-data mountPath: /var/lib/mysql - name: MYSQL_ROOT_PASSWORD value: {{ .Values.mysqlRootPassword }} - name: MYSQL_DATABASE value: {{ .Values.mysqlDatabase }} - name: MYSQL_USER value: {{ .Values.mysqlUser }} - name: MYSQL_PASSWORD value: {{ .Values.mysqlPassword }} volumes: - name: mysql-data hostPath: path: {{ .Values.nacosDataPath }} nodeSelector: nacos: nacos-mysql apiVersion: v1 kind: Service metadata: name: mysql namespace: {{ .Values.namespace }} labels: name: mysql spec: ports: - port: 3306 targetPort: 3306 selector: name: mysqltemplates 文件夹下 nacos.yaml###使用自建数据库;使用Ingress发布配置后台### apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: {{ .Values.namespace }} labels: caas_service: nacos-headless spec: ports: - port: 8848 name: server protocol: TCP targetPort: 8848 - port: 7848 name: rpc targetPort: 7848 clusterIP: None sessionAffinity: None type: ClusterIP publishNotReadyAddresses: true selector: caas_service: {{ .Values.service_name }} apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ .Values.service_name }} namespace: {{ .Values.namespace }} spec: serviceName: nacos-headless replicas: 3 template: metadata: labels: caas_service: {{ .Values.service_name }} annotations: pod.alpha.kubernetes.io/initialized: "true" spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: "caas_service" operator: In values: - nacos-headless topologyKey: "kubernetes.io/hostname" containers: - name: k8snacos imagePullPolicy: Always image: {{ .Values.nacosImage }} resources: requests: memory: "2Gi" cpu: "500m" ports: - containerPort: 8848 name: client - containerPort: 7848 name: rpc livenessProbe: httpGet: path: /nacos port: 8848 initialDelaySeconds: 10 periodSeconds: 3 timeoutSeconds: 1 failureThreshold: 5 readinessProbe: httpGet: path: /nacos port: 8848 initialDelaySeconds: 10 periodSeconds: 3 timeoutSeconds: 1 failureThreshold: 5 - name: NACOS_REPLICAS value: "3" - name: MYSQL_SERVICE_HOST valueFrom: configMapKeyRef: name: nacos-cm key: mysql.host - name: MYSQL_SERVICE_DB_NAME valueFrom: configMapKeyRef: name: nacos-cm key: mysql.db.name - name: MYSQL_SERVICE_PORT valueFrom: configMapKeyRef: name: nacos-cm key: mysql.port - name: MYSQL_SERVICE_USER valueFrom: configMapKeyRef: name: nacos-cm key: mysql.user - name: MYSQL_SERVICE_PASSWORD valueFrom: configMapKeyRef: name: nacos-cm key: mysql.password - name: MODE value: "cluster" - name: NACOS_SERVER_PORT value: "8848" - name: PREFER_HOST_MODE value: "hostname" - name: NACOS_SERVERS value: {{ .Values.service_name }}-0.nacos-headless.{{ .Values.namespace }}.svc.cluster.local:8848 {{ .Values.service_name }}-1.nacos-headless.{{ .Values.namespace }}.svc.cluster.local:8848 {{ .Values.service_name }}-2.nacos-headless.{{ .Values.namespace }}.svc.cluster.local:8848 initContainers: - command: - sleep 10; mkdir /wls/logs/nacos-0 /wls/logs/nacos-1 /wls/logs/nacos-2 -p;chown -R 798:5682 /wls/logs/nacos-0 /wls/logs/nacos-1 /wls/logs/nacos-2 ; echo init finished image: {{ .Values.busyboxImage }} imagePullPolicy: IfNotPresent name: init volumeMounts: - mountPath: /wls/logs/ name: logs volumes: - name: logs hostPath: path: {{ .Values.nfsPath }}/logs selector: matchLabels: caas_service: {{ .Values.service_name }} # ------------------- App Service ------------------- # apiVersion: v1 kind: Service metadata: name: {{ .Values.service_name }}-nodeport annotations: {{- range $key, $value := .Values.annotations }} {{ $key }}: {{ $value | quote }} {{- end }} labels: caas_service: {{ .Values.service_name }} spec: ports: - name: "nodeport" port: 8848 protocol: TCP targetPort: 8848 nodePort: 32101 publishNotReadyAddresses: true selector: caas_service: {{ .Values.service_name }} type: NodePort检查 nacos 集群是否健康启动nacos 集群启动后,可能会出现容器状态是 health/running,但实际并未健康启动的情况,会导致微服务注册有问题,需要手动检查下进入到nacos实例的容器内后执行命令以下,查看响应信息curl -X PUT '127.0.0.1:8848/nacos/v1/ns/instance/beat?serviceName=nacos'若nacos实例未健康启动,重启nacos的实例容器后再次检查(一般重启一次后就能健康启动)# nacos实例健康启动的响应信息 {"clientBeatInterval":5000,"code":20404}
概述简介Java 程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。Druid 已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。Spring Boot 2.0 以上默认使用 Hikari 数据源,Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源。Druid 的 Github地址Druid 基本配置参数介绍com.alibaba.druid.pool.DruidDataSource 基本配置参数name :数据源名称如果存在多个数据源,监控的时候可以通过名字来区分开来如果没有配置,将会生成一个名字,格式是"DataSource-"+System.identityHashCode(this)jdbcUrl :连接数据库的 url,不同数据库不一样username :连接数据库的用户名password :连接数据库的密码driverClassName :数据库驱动类可配可不配,如果不配置 druid 会根据 url 自动识别 dbType,然后选择相应的 driverClassName(建议配置下)initialSize :初始化时建立物理连接的个数,初始化发生在显示调用 init 方法,或者第一次 getConnection 时maxActive :最大连接池数量maxIdle :已经不再使用,配置了也没效果minIdle :最小连接池数量maxWait :获取连接时最大等待时间,单位毫秒配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降(可以通过配置 useUnfairLock=true 使用非公平锁)poolPreparedStatements :是否缓存 preparedStatement,即 PsCachePSCache 对支持游标的数据库性能提升巨大,比如说 oracle,而 mysql 则建议关闭maxOpenPreparedStatements :要启用 PSCache,必须配置大于0当大于 0 时,poolPreparedStatements 自动触发修改为 true在 Druid 中,不会存在 Oracle 下 PSCache 占用内存过多的问题,可以把这个数值配置大一点,比如 100validationQuery :用来检测连接是否有效的 sql,要求是一个查询语句如果 validationQuery 为null,testOnBorrow、testOnReturn 、testWhileIdle 都不会起作用testOnBorrow :申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能testOnReturn :归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能testWhileIdle :建议配置为 true,不影响性能,并且保证安全性申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunMills,执行 validationQuery 检测连接是否有效timeBetweenEvictionRunMillis :间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒Destory 线程会检测连接的间隔时间testWhileIdle 的判断依据(详见 testWhileIdele 属性的说明)numTestsPerEvictionRun :废弃,一个 DruidDataSource 只支持一个 EvicationRunminEvictableIdleTimeMillis :一个连接在池中最小生存的时间,单位是毫秒connectionInitSqls :物理连接初始化的时候执行 sqlexceptionSorter :当数据库抛出一些不可恢复的异常时,抛弃连接filters :通过别名的方式配置扩展插件,属性类型是字符串常用的插件有:监控统计用的 filter(stat:监控统计,log:4:日志记录,wall:防御sql注入)proxyFilters :类型是 List<com.alibaba.druid,filter.Filter>,如果同时配置 filter 和 proxyFilters,是组合关系(并非)Druid 集成方式(含依赖)Spring Boot 方式(推荐)依赖 <!--引入druid数据源--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency>yml 配置文件切换数据源方式1:spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/springboot_test?characterEncoding=utf8&serverTimezone=UTC username: root password: root # druid-spring-boot-starter 依赖自动生效 druid,可以不配置 type 属性,但建议配置 type: com.alibaba.druid.pool.DruidDataSource方式2:druid 专用配置(需要 druid-spring-boot-starter 依赖)spring: datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/springboot_test?characterEncoding=utf8&serverTimezone=UTC username: root password: rootSpring 方式依赖 <!--引入druid数据源--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency>yml 配置文件切换数据源Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以通过 spring.datasource.type 指定数据源。spring: datasource: username: root password: 123456 url: jdbc:mysql://192.168.10.132:3306/testdb driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # druid 数据源专有配置 # 不是druid-spring-boot-starter依赖,SpringBoot默认是不注入druid数据源专有属性值的,需要自己绑定 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 maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计。stat:监控统计 log4:日志记录 wall:防御sql注入 # 如果运行时报错:ClassNotFoundException:orgapache.log4j.Priority,则导入log4j依赖即可 filters: stat,wall,log4j # 自动往数据库建表 #schema: #- classpath:department.sql配置类:添加 DruidDataSource 组件到容器中,并绑定属性@Configuration public class DruidDataSourceConfig { * 添加 DruidDataSource 组件到容器中,并绑定属性 @Bean @ConfigurationProperties(prefix = "spring.datasource") @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.alibaba.druid.pool.DruidDataSource") public DataSource druid(){ return new DruidDataSource(); }Druid 密码回调现在很多项目都是把数据库的密码明文放在配置文件中,这样其实是不安全的,应该将密码加密后再放到配置中,这样可以一定程度的保护数据库密码的安全。Druid 可以通过配置参数 passwordCallBack 来指定一个密码接口回调类进行密文密码解密操作。实现步骤:自定义一个密码接口回调类需要实现 DruidPasswordCallback 接口并重写接口方法 setProperties()import com.alibaba.druid.util.DruidPasswordCallback; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.util.Properties; @Slf4j public class DruidCustomPasswordCallback extends DruidPasswordCallback { @Override public void setProperties(Properties properties) { super.setProperties(properties); // 获取配置文件中的已经加密的密码(spring.datasource.druid.connect-properties.password) String pwd = (String)properties.get("password"); if (StringUtils.isNotEmpty(pwd)) { try { // 这里的代码是将密码进行解密,并设置 String password = "解密后的明文密码"; setPassword(password.toCharArray()); } catch (Exception e) { e.printStackTrace(); }yml 配置文件中配置密码接口回调类注:可以通过配置文件中的 password-callback-class-name 或 password-callback 属性来配置密码接口回调类推荐使用 spring.datasource.druid.password-callback-class-name若是使用 spring.datasource.druid.password-callback 属性,则需要搭配相应的类型转换器才能用,不然报错:No converter found capable of converting from type [java.lang.String] to type [javax.security.auth.callback.PasswordCallback]根据密码接口回调类中的密文密码解密的逻辑,必须配置 spring.datasource.druid.connect-properties.password 属性才会进行密文密码解密操作并重置密码为解密后的明文密码若不配置 connect-properties.password ,则默认使用的 spring.datasource(.druid).password 的属性值若是 Spring 集成 Druid,则可以在初始化 DruidDatabaseSource 时,手动配置其 passwordCallBack 属性值spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/leyou?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root11 # type: com.alibaba.druid.pool.DruidDataSource druid: # 配置密码接口回调类的全限定类名 password-callback-class-name: com.duran.ssmtest.config.DruidCustomPasswordCallback # 配置密码接口回调类中方法入参Properties的值(自定义Map<String, String>) connect-properties: # 配置密文密码(传参到配置密码接口回调类中解密) password: aaaDruid 数据源监控Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看。设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @Configuration public class DruidDataSourceConfig { * 添加 DruidDataSource 组件到容器中,并绑定属性 @Bean @ConfigurationProperties(prefix = "spring.datasource") @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.alibaba.druid.pool.DruidDataSource") public DataSource druid(){ return new DruidDataSource(); * 配置 Druid 监控管理后台的Servlet; * 内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式 @Bean @ConditionalOnClass(DruidDataSource.class) public ServletRegistrationBean statViewServlet(){ // 这些参数可以在 http.StatViewServlet 的父类 ResourceServlet 中找到 Map<String,String> initParams = new HashMap<>(); initParams.put("loginUsername","admin"); initParams.put("loginPassword","123456"); // allow:Druid 后台允许谁可以访问。默认就是允许所有访问。 initParams.put("allow",""); // 后面参数为空则所有人都能访问,一般会写一个具体的ip或ip段 // deny:Druid 后台禁止谁能访问 // initParams.put("deny","192.168.10.132"); // 注册一个servlet,同时表明/druid/* 这个请求会走到这个servlet,而druid内置了这个请求的接收 ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); bean.setInitParameters(initParams); return bean; * 配置一个web监控的filter @Bean @ConditionalOnClass(DruidDataSource.class) public FilterRegistrationBean webStatFilter(){ Map<String,String> initParams = new HashMap<>(); // 这些不进行统计 initParams.put("exclusions","*.js,*.css,/druid/*"); FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebStatFilter()); bean.setInitParameters(initParams); bean.setUrlPatterns(Arrays.asList("/*")); return bean; }运行应用,浏览器访问 :http://localhost:8080/druid/login.html
参考:SpringBoot2异常处理回滚事务详解(自动回滚/手动回滚/部分回滚)概念事务定义事务,就是一组操作数据库的动作集合。事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。事务特点原子性:一个事务中所有对数据库的操作是一个不可分割的操作序列,要么全做要么全不做一致性:数据不会因为事务的执行而遭到破坏隔离性:一个事务的执行,不受其他事务的干扰,即并发执行的事务之间互不干扰持久性:一个事务一旦提交,它对数据库的改变就是永久的。事务实现机制Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。编程式事务管理: 编程式事务管理使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。声明式事务管理: 建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务管理不需要入侵代码,更快捷而且简单,推荐使用。声明式事务有两种方式:一种是在配置文件(xml)中做相关的事务规则声明另一种是基于 @Transactional 注解的方式。注释配置是目前流行的使用方式,推荐使用。在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,这个代理对象决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑,最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务。Spring AOP 代理有 CglibAopProxy 和 JdkDynamicAopProxy 两种,以 CglibAopProxy 为例,对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptor 的 intercept 方法。对于 JdkDynamicAopProxy,需要调用其 invoke 方法。开启事务Spirng Boot 默认开启事务,无需做任何事情,直接使用@Transactional即可Spring 开启事务的方式:方式1:Spring中 纯XML 配置事务<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="pooledDataSource"/> </bean> <aop:config> <aop:pointcut expression="execution(* cn.yuanyu.crud.service..*(..))" id="txPoint"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPoint"/> </aop:config> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*"/> <tx:method name="get*" read-only="true"/> </tx:attributes> </tx:advice>方式2:Spring中 XML+注解 配置事务一般xml里面配置粗粒度的控制,然后使用注解<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="pooledDataSource"/> </bean> <aop:config> <aop:pointcut expression="execution(* cn.yuanyu.crud.service..*(..))" id="txPoint"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPoint"/> </aop:config> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*"/> <tx:method name="get*" read-only="true"/> </tx:attributes> </tx:advice> <!-- --> <tx:annotation-driven transaction-manager="transactionManager"/>方式3:Spring中 纯注解 配置事务,使用@EnableTransactionManagement 注解也可以启用事务管理功能@Configuration //声明配置类 @MapperScan("cn.yuanyu.tx.mapper") @EnableTransactionManagement // 开启事务注解,等同于配置文件<tx:annotation-driven/> public class MybatisPlusConfiguration {注解@Transactional的使用注解@Transactional常用配置参 数 名 称功 能 描 述readOnly用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)rollbackFor用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class);指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})transactionManager / value多个事务管理器托管在 Spring 容器中时,指定事务管理器的 bean 名称rollbackForClassName用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”})noRollbackFor用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})noRollbackForClassName用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”, ”Exception”})propagation用于设置事务的传播行为。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED, readOnly=true)isolation用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置timeout该属性用于设置事务的超时秒数,默认值为-1表示永不超时事物超时设置:@Transactional(timeout=30) ,设置为30秒Propagation的属性(事务的传播行为)例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)Propagation属性含义REQUIRED默认值 在有transaction状态下执行;如当前没有transaction,则创建新的transaction;SUPPORTS如当前有transaction,则在transaction状态下执行;如果当前没有transaction,在无transaction状态下执行;MANDATORY必须在有transaction状态下执行,如果当前没有transaction,则抛出异常IllegalTransactionStateException;REQUIRES_NEW创建新的transaction并执行;如果当前已有transaction,则将当前transaction挂起;NOT_SUPPORTED在无transaction状态下执行;如果当前已有transaction,则将当前transaction挂起;NEVER在无transaction状态下执行;如果当前已有transaction,则抛出异常IllegalTransactionStateException。事务5种隔离级别例如:@Transactional(isolation = Isolation.READ_COMMITTED)隔离级别含义DEFAULT这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别另外四个与JDBC的隔离级别相对应;READ_UNCOMMITTED最低的隔离级别。事实上我们不应该称其为隔离级别,因为在事务完成前,其他事务可以看到该事务所修改的数据。而在其他事务提交前,该事务也可以看到其他事务所做的修改。可能导致脏,幻,不可重复读READ_COMMITTED大多数数据库的默认级别。在事务完成前,其他事务无法看到该事务所修改的数据。遗憾的是,在该事务提交后,你就可以查看其他事务插入或更新的数据。这意味着在事务的不同点上,如果其他事务修改了数据,你就会看到不同的数据。可防止脏读,但幻读和不可重复读仍可以发生。REPEATABLE_READ比ISOLATION_READ_COMMITTED更严格,该隔离级别确保如果在事务中查询了某个数据集,你至少还能再次查询到相同的数据集,即使其他事务修改了所查询的数据。然而如果其他事务插入了新数据,你就可以查询到该新插入的数据。可防止脏读,不可重复读,但幻读仍可能发生。SERIALIZABLE完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。代价最大、可靠性最高的隔离级别,所有的事务都是按顺序一个接一个地执行。避免所有不安全读取。使用注意事项(防止事务失效)在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。@Transactional 注解应该只被应用在 public 修饰的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 该注解,它也不会报错(IDEA会有提示), 但事务并没有生效。被外部调用的公共方法A有两个进行了数据操作的子方法B和子方法C的事务注解说明:被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是其他类的方法且各自声明事务,则事务由子方法B和C各自控制被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是本类的方法,则无论子方法B和C是否声明事务,事务均不会生效被外部调用的公共方法A声明事务@Transactional,无论子方法B和C是不是本类的方法,无论子方法B和C是否声明事务,事务均由公共方法A控制被外部调用的公共方法A声明事务@Transactional,子方法运行异常,但运行异常被子方法自己 try-catch 处理了,则事务回滚是不会生效的!如果想要事务回滚生效,需要将子方法的事务控制交给调用的方法来处理:方案1:子方法中不用 try-catch 处理运行异常方案2:子方法的catch里面将运行异常抛出【throw new RuntimeException();】默认情况下,Spring会对unchecked异常进行事务回滚,也就是默认对 RuntimeException() 异常或是其子类进行事务回滚。如果是checked异常则不回滚,例如空指针异常、算数异常等会被回滚;文件读写、网络问题Spring就没法回滚。若想对所有异常(包括自定义异常)都起作用,注解上面需配置异常类型:@Transactional(rollbackFor = Exception.class数据库要支持事务,如果是mysql,要使用innodb引擎,myisam不支持事务事务@Transactional由spring控制时,它会在抛出异常的时候进行回滚。如果自己使用try-catch捕获处理了,是不生效的。如果想事务生效可以进行手动回滚或者在catch里面将异常抛出【throw new RuntimeException();】方案一:手动抛出运行时异常(缺陷是不能在catch代码块自定义返回值) try{ }catch(Exception e){ logger.error("",e); throw new RuntimeException(e); }方案二:手动进行回滚【 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 】 try{ }catch(Exception e){ log.error("fail",e); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return false; }@Transactional可以放在Controller下面直接起作用,看到网上好多同学说要放到@Component下面或者@Service下面,经过试验,可以不用放在这两个下面也起作用。@Transactional引入包问题,它有两个包:import javax.transaction.Transactional; import org.springframework.transaction.annotation.Transactional; // 推荐这两个都可以用,对比了一下他们两个的方法和属性,发现后面的比前面的强大。建议使用后面的。使用场景自动回滚直接抛出,不try/catch@Override @Transactional(rollbackFor = Exception.class) public Object submitOrder() throws Exception { success(); //假如exception这个操作数据库的方法会抛出异常,方法success()对数据库的操作会回滚。 exception(); return ApiReturnUtil.success(); }手动回滚进行try/catch,回滚并抛出@Override @Transactional(rollbackFor = Exception.class) public Object submitOrder (){ success(); try { exception(); } catch (Exception e) { e.printStackTrace(); // 手动回滚事务 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return ApiReturnUtil.error(); return ApiReturnUtil.success(); }回滚部分异常使用【Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint(); 】设置回滚点。使用【TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);】回滚到savePoint。@Override @Transactional(rollbackFor = Exception.class) public Object submitOrder (){ success(); //只回滚以下异常, Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint(); try { exception(); } catch (Exception e) { e.printStackTrace(); // 手工回滚事务 TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint); return ApiReturnUtil.error(); return ApiReturnUtil.success(); }手动创建、提交、回滚事务PlatformTransactionManager 这个接口中定义了三个方法 getTransaction创建事务,commit 提交事务,rollback 回滚事务。它的实现类是 AbstractPlatformTransactionManager。@Autowired priDataSourceTransactionManager dataSourceTransactionManager; @Autowired TransactionDefinition transactionDefinition; // 手动创建事务 TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition); // 手动提交事务 dataSourceTransactionManager.commit(transactionStatus); // 手动回滚事务。(最好是放在catch 里面,防止程序异常而事务一直卡在哪里未提交) dataSourceTransactionManager.rollback(transactionStatus);事务失效不回滚的原因及解决方案异常被捕获导致事务失效在spring boot 中,使用事务非常简单,直接在方法上面加入@Transactional 就可以实现。@GetMapping("delete") @ResponseBody @Transactional public void delete(@RequestParam("id") int id) { try { //delete country this.repository.delete(id); if(id == 1){ throw Exception("测试事务"); //delete city this.repository.deleteByCountryId(id); }catch (Exception e){ logger.error("delete false:" + e.getMessage()); }发现事务不回滚,即 this.repository.delete(id);成功把数据删除了。原因:默认spring事务只在发生未被捕获的 RuntimeException 时才回滚。 spring aop 异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获 RuntimeException 的异常,但可以通过配置来捕获特定的异常并回滚。换句话说在service的方法中不使用 try catch 或者在 catch 中最后加上throw new RuntimeExcetpion()抛出运行异常,这样程序异常时才能被aop捕获进而回滚。解决方案:方案1:例如service层处理事务,那么service中的方法中不做异常捕获,或者在catch语句中最后增加throw new RuntimeException(); 语句,以便让aop捕获异常再去回滚,并且在service的上层要继续捕获这个异常。方案2:在service层方法的catch语句中进行手动回滚,这样上层就无需去处理异常。@GetMapping("delete") @ResponseBody @Transactional public Object delete(@RequestParam("id") int id){ if (id < 1){ return new MessageBean(101,"parameter wrong: id = " + id) ; try { //delete country this.countryRepository.delete(id); //delete city this.cityRepository.deleteByCountryId(id); return new MessageBean(200,"delete success"); }catch (Exception e){ logger.error("delete false:" + e.getMessage()); // 手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return new MessageBean(101,"delete false"); }自调用导致事务失效问题描述及原因在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理,否则会造成自调用问题。若同一类中的 没有@Transactional 注解的方法 内部调用 有@Transactional 注解的方法,有@Transactional 注解的方法的事务被忽略,不会发生回滚。见 示例代码展示。自调用问题示例:@Service public class OrderService { private void insert() { insertOrder(); @Transactional public void insertOrder() { //insert log info //insertOrder //updateAccount // insertOrder() 尽管有@Transactional 注解,但它被内部方法 insert()调用,事务被忽略,出现异常事务不会发生回滚,并且会报错类似于:org.springframework.transaction.NoTransactionException: No transaction aspect-managed TransactionStatus in scope(翻译:没有Transaction无法回滚事务。自调用导致@Transactional 失效。)自调用失效原因:spring里事务是用注解配置的,当一个方法没有接口,单单只是一个内部方法时,事务的注解是不起作用的,需要回滚时就会报错。出现这个问题的根本原因是:@Transactional 的实现原理是AOP,AOP的实现原理是动态代理,而自调用时并不存在代理对象的调用,也就不会产生基于AOP 的事务回滚操作虽然可以直接从容器中获取代理对象,但这样有侵入之嫌,不推荐。解决方案方案1、在类上(或者最外层的公共方法)加事务@Service @Slf4j public class MyTransactional { // 最外层公共方法。自动回滚事务方式,insertOrder()方法报错后事务回滚,且线程中止,后续逻辑无法执行 @Transactional public void test1() { this.insertOrder(); System.out.println("11111111111111111"); // 最外层公共方法。手动回滚事务方式,insertOrder()方法报错后事务回滚,可以继续执行后续逻辑 @Transactional public void test2() { try { insertOrder(); } catch (Exception e) { log.error("faild to ...", e); // 手动回滚事务 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 其他操作 // 其他操作 // 进行数据库操作的方法(private 或 public 均可) private void insertOrder() { //insert log info //insertOrder //updateAccount }方案 2、使用AspectJ 取代 Spring AOP 代理上面的两个问题@Transactional 注解只应用到 public 方法和自调用问题,是由于使用 Spring AOP 代理造成的。为解决这两个问题,可以使用 AspectJ 取代 Spring AOP 代理。需要将下面的 AspectJ 信息添加到 xml 配置信息中。AspectJ 的 xml 配置信息<tx:annotation-driven mode="aspectj" /> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> </bean class="org.springframework.transaction.aspectj.AnnotationTransactionAspect" factory-method="aspectOf"> <property name="transactionManager" ref="transactionManager" /> </bean> 同时在 Maven 的 pom 文件中加入 spring-aspects 和 aspectjrt 的 dependency 以及 aspectj-maven-plugin。AspectJ 的 pom 配置信息<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>4.3.2.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.9</version> </dependency> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.9</version> <configuration> <showWeaveInfo>true</showWeaveInfo> <aspectLibraries> <aspectLibrary> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </aspectLibrary> </aspectLibraries> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>test-compile</goal> </goals> </execution> </executions> </plugin>其他事务提交方式默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring 会将底层连接的【自动提交特性】设置为 false 。也就是在使用 spring 进行事务管理的时候,spring 会将【是否自动提交】设置为false,等价于JDBC中的 connection.setAutoCommit(false); ,在执行完之后在进行提交 connection.commit(); 。事务回滚规则指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。事务并发会产生的问题术语含义脏读A事务读取到了B事务还未提交的数据,如果B未提交的事务回滚了,那么A事务读取的数据就是无效的,这就是数据脏读不可重复读在同一个事务中,多次读取同一数据返回的结果不一致,这是由于读取事务在进行操作的过程中,如果出现更新事务,它必须等待更新事务执行成功提交完成后才能继续读取数据,这就导致读取事务在前后读取的数据不一致的状况出现幻读A事务读取了几行记录后,B事务插入了新数据,并且提交了插入操作,在后续操作中A事务就会多出几行原本不存在的数据,就像A事务出现幻觉,这就是幻读第一类丢失更新在没有事务隔离的情况下,两个事务都同时更新一行数据,但是第二个事务却中途失败退出, 导致对数据的两个修改都失效了。例如:张三的工资为5000,事务A中获取工资为5000,事务B获取工资为5000,汇入100,并提交数据库,工资变为5100; 随后,事务A发生异常,回滚了,恢复张三的工资为5000,这样就导致事务B的更新丢失了。 脏读脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。例如:张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。 与此同时,事务B正在读取张三的工资,读取到张三的工资为8000。 随后,事务A发生异常,回滚了事务,张三的工资又回滚为5000。 最后,事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。 不可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。例如:在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。 与此同时,事务B把张三的工资改为8000,并提交了事务。 随后,在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。第二类丢失更新不可重复读的特例。有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。例如:在事务A中,读取到张三的存款为5000,操作没有完成,事务还没提交。 与此同时,事务B存入1000,把张三的存款改为6000,并提交了事务。 随后,在事务A中,存储500,把张三的存款改为5500,并提交了事务,这样事务A的更新覆盖了事务B的更新。幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。例如:目前工资为5000的员工有10人,事务A读取到所有的工资为5000的人数为10人。 此时,事务B插入一条工资也为5000的记录。 这时,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。不可重复读和幻读的区别不可重复读的重点是修改,同样的条件,你读取过的数据,再次读取出来发现值不一样了幻读的重点在于新增或者删除,同样的条件,第 1 次和第 2 次读出来的记录数不一样
自动配置原理概述Spring Boot 内部集成了大量第三方服务,提供了默认配置,而默认配置生效的步骤:@EnableAutoConfiguration 注解会去所有包下寻找 META-INF/spring.factories 文件,读取其中以 EnableAutoConfiguration为 key 的所有类的名称,这些类就是提前写好的自动配置类这些类都声明了 @Configuration注解,并且通过 @Bean 注解提前配置了所需要的一切实例但是,这些配置不一定生效,因为有 @Conditional注解,满足一定条件才会生效只需要引入了相关依赖(启动器),上面的配置需要的条件成立,自动配置生效如果自己配置了相关Bean,那么会覆盖默认的自动配置的Bean还可以通过配置 application.properties(yml) 文件,来覆盖自动配置中的属性Spring Boot 能够快速开发的原因就在于内置了很多第三方组件的默认配置,使用的步骤如下:使用启动器如果不想自己配置,只需要引入启动器依赖即可,而依赖版本也不用操心,因为只要引入了 Spring Boot 提供的 stater(启动器),就会自动管理依赖及版本了。替换默认配置SpringBoot 的配置,都会有默认属性,而这些属性可以通过修改 application.properties 文件来进行覆盖。关键注解@Conditional@Condition 是在 Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建 Bean 操作@Conditional 标注在配置类上或配置类的方法上,和 @Configuration 配合使用,@Conditional 指定的条件成立,配置类里面的内容才生效 SpringBoot常用条件注解:@ConditionalOnBean:容器中存在指定Bean@ConditionalOnMissingBean :容器中不存在指定Bean@ConditionalOnProperty:系统中指定的属性是否有指定的值@ConditionalOnClass :系统中有指定的类@ConditionalOnMissingClass: 系统中没有指定的类@ConditionalOnExpression :满足SpEL表达式指定@ConditionalOnSingleCandidate :容器中只有一个指定的Bean,或者这个Bean是首选Bean@ConditionalOnResource :类路径下是否存在指定资源文件@ConditionalOnWebApplication :当前是web环境@ConditionalOnNotWebApplication :当前不是web环境@ConditionalOnJava:系统的java版本是否符合要求示例:User类@Data public class User { private String username; private Integer age; }配置类@Configuration @ConditionalOnProperty(value = "user.enable") // 配置文件存在该配置项时该配置类才生效 public class UserConfig { @Bean public User user(){ User user = new User(); user.setUsername("tom"); user.setAge(18); return user; }配置文件中添加如下配置:user: enable: true添加测试用例@RunWith(SpringRunner.class) @SpringBootTest public class RedisTests { @Autowired private User user; @Test public void testUser(){ System.out.println(user); }SpringBoot自动配置流程图使用的时候只需要引入相应的starter,starter是一个指示信号,引入这个依赖之后,相应的条件满足了,就会注册相应的Bean。@Import参考:@Import注解的作用@Import注解的作用@Import 注解:把类导入 Spring IOC容器有多种方式能让类加 IOC容器管理,如@Bean、@Component等,@Import 是另外一种方式,更加快捷。支持三种方式:带有 @Configuration 的配置类(4.2版本之前只可以导入配置类,4.2版本之后 也可以导入普通类)ImportSelector 的实现ImportBeanDefinitionRegistrar 的实现主要用法:直接填 class 数组方式@Configuration @Import({User.class}) // 大括号中可以添加多个类,使用逗号分隔,例如 {User.class,UserInfo.class} public class UserConfig {}ImportSelector 方式(Spring Boot 底层采用比较得多的方式)// 自定义ImportSelector public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 返回的是需要加载的类名数组 注意这里需要的是类的全路径 return new String[]{"com.itheima.redis.entity.User"}; }@Configuration @Import(MyImportSelector.class) public class UserConfig {}ImportBeanDefinitionRegistrar 方式这种方式和 ImportSelector 方式类似,不过这种方式可以自定义Bean在容器中的名称// 自定义ImportBeanDefinitionRegistrar public class TestImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(TestD.class); //自定义注册bean registry.registerBeanDefinition("testD1111",rootBeanDefinition); }@Import({TestImportBeanDefinitionRegistrar.class}) @Configuration public class ImportConfig {}注意:三种用法方式都可在一个@Import中使用,需要注意的是class 数组方式、ImportSelector 方式在IOC容器中bean名称是类的全限定类名,而ImportBeanDefinitionRegistrar 方式是自定义的名称@SpringBootApplication流程图查看源码重点的注解有3个:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan@SpringBootConfiguration源码:@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration @Indexed public @interface SpringBootConfiguration { @AliasFor( annotation = Configuration.class boolean proxyBeanMethods() default true; }这个注解上面有一个 @Configuration 注解,@Configuration 注解的作用为声明当前类是一个配置类,然后 Spring 会自动扫描到添加了@Configuration的类,并且读取其中的配置信息。@SpringBootConfiguration 是来声明当前类是 SpringBoot 应用的配置类 。@EnableAutoConfiguration源码:@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {}; }借助@Import(AutoConfigurationImportSelector.class),@EnableAutoConfiguration 可以帮助 Spring Boot 应用将所有符合条件的 @Configuration 配置都加载到当前 Spring Boot 创建并使用的IOC容器。AutoConfigurationImportSelector 源码,查看selectImports方法:继续跟进 getAutoConfigurationEntry 方法在getCandidateConfigurations方法中有一段提示:跟进loadFactoryNames方法:可以看到从这个位置加载配置:找到配置文件这里有自动配置:在自动配置包中内置了大量的第三方中间件的配置类:总结:@EnableAutoConfiguration 从 classpath 中搜寻所有 META-INF/spring.factories 配置文件,并将其中org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的配置项通过反射实例化为对应的标注了 @Configuration 的 JavaConfig 形式的 IOC 容器配置类,然后汇总为一个,并加载到IOC容器。@ComponentScan开启组件扫描。提供了类似与 <context:component-scan> 标签的作用 通过 basePackageClasses 或者 basePackages 属性来指定要扫描的包。如果没有指定这些属性,那么将从声明这个注解的类所在的包开始,扫描包及子包 而@SpringBootApplication注解声明的类就是 main 函数所在的启动类,因此扫描的包是该类所在包及其子包。因此,一般启动类会放在一个比较前的包目录中 SpringBoot 监控Actuator概念SpringBoot 自带监控功能 Actuator,可以帮助实现对程序内部运行情况监控,比如监控状况、Bean加载情况、配置属性、日志信息等;同时也可以通过Actuator管理应用程序,例如通过Actuator去做一个shutdown功能(默认是不开启的),另外还可以在运行的过程中对日志进行调整。Endpoints:端点可以把 Endpoints 理解为一个功能模块,功能模块可以监控 Spring Boot 应用,甚至可以与 Spring Boot 进行交互(比如读取信息,关闭应用等操作)。Spring Boot内置了很多 Endpoints,最重要的 Endpoints 是 health,即健康检查模块。Actuator端点说明端点说明默认开启默认 HTTP默认 JMXauditevents公开当前应用程序的审查事件信息YNYbeans显示Spring loC容器关于Bean的信息YNYcaches显示应用中的缓存YNYconditions显示自动配置类的评估和配置条件,并且显示他们匹配或者不匹配的原因YNYconfigprops显示当前项目的属性配置信息(通过@ConfigurationProperties配置)YNYenv显示当前Spring应用环境配置属性(ConfigurableEnvironment)YNYflyway显示已经应用于flyway数据库迁移的信息YNYhealth显示当前应用健康状态YYYhttptrace显示最新追踪信息(默认为最新100次 HTTP请求)YNYinfo显示当前应用信息YYYloggers显示并更新应用程序中记录器的配置YNYliquibase显示已经应用于liquibase数据库迁移的信息YNYmetrics显示当前配置的各项“度量”指标YNYmappings显示由@RequestMapping (@GetMapping和@PostMapping 等)配置的映射路径信息YNYscheduledtasks显示当前应用的调度任务计划YNYsessions允许从Spring会话支持的会话存储库检索和删除用户会话,只是Spring 会话对响应式Web 应用还暂时不能支持YNYshutdown允许当前应用被优雅地进行关闭(在默认的情况下不启用这个端点)NNYthreaddump显示线程泵YNYheapdump返回 Heap Dump 文件,格式为 HPROFYNN/Aprometheus返回可供 Prometheus 抓取的信息YNN/A注意:shutdown 端点默认不启用只有health和 info 默认是可以通过 http 进行访问的快速入门依赖坐标<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>如果想看更多的信息,在配置文件中配置:management: endpoint: health: show-details: always endpoints: exposure: include: "*"Actuator监控接口:http://localhost:8080/actuator/beans http://localhost:8080/actuator/env http://localhost:8080/actuator/mappings 具体详细的解释:路径描述/beans描述应用程序上下文里全部的Bean,以及它们的关系/env获取全部环境属性/env/{name}根据名称获取特定的环境属性值/health报告应用程序的健康指标,这些值由HealthIndicator的实现类提供/info获取应用程序的定制信息,这些信息由info打头的属性提供/mappings描述全部的URI路径,以及它们和控制器(包含Actuator端点)的映射关系/metrics报告各种应用程序度量信息,比如内存用量和HTTP请求计数/metrics/{name}报告指定名称的应用程序度量值/trace提供基本的HTTP请求跟踪信息(时间戳、HTTP头等)Actuator 的详细配置management: server: # Actuator的管理端口号,默认跟服务端口号(server.port)一致 port: 8085 endpoint: health: # 是否开启health端点,默认为true enabled: true # 展示健康检查中所有指标的详细信息 show-details: always endpoints: # web:http方式 exposure: # 暴露可以通过web访问的端点。默认"health,info",用 * 可以包含全部端点 include: "*" # 不暴露的端点 exclude: # 自定义监控路径前缀。默认 /actuator base-path: /actuator # 修改端点的访问路径(映射),例如将 /beans 更改为 /request_beans path-mapping: beans: request_beans # 所有端点是否默认启动,默认true。若设置为false,则默认情况下所有端点都不启用,此时需要按需启用端点 enabled-by-default: trueSpring Boot Admin概述及快速入门Actuator 的监控内容够详细,但是阅读性比较差,所以可以使用 Spring Boot Admin 提供一个可视化的界面查阅信息,Spring Boot Admin 是一个第三方提供的开源社区项目,用于管理和监控 SpringBoot 应用程序。源码库:https://github.com/codecentric/spring-boot-adminSpring Boot Admin 有两个角色,客户端(Client)和服务端(Server):应用程序作为 Spring Boot Admin Client 向为 Spring Boot Admin Server 注册Spring Boot Admin Server 的界面将展示 Spring Boot Admin Client 的监控信息开发步骤如下:admin-server:创建 admin-server 模块导入依赖坐标 admin-starter-server <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.3.0</version> </dependency>在启动类上标注 @EnableAdminServer 注解,启用 Admin 监控功能配置相关信息server: port: 8888启动 server 服务,访问:http://localhost:8888/#/applicationsadmin-client: 自己创建的项目就是所谓的client端创建 admin-client 模块导入依赖坐标 admin-starter-client<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.3.0</version> </dependency>配置相关信息:server地址等spring: boot: admin: client: url: http://localhost:8888 management: endpoint: health: show-details: always endpoints: exposure: include: "*"启动 client 服务Admin Server 的详细配置参考:https://www.jianshu.com/p/b0528b52772cspring: boot: admin: # server端的访问路径。默认为 / context-path: / monitor: # 更新client端状态的时间间隔,默认为10000,单位是毫秒 period: 10000 # client端状态的生命周期,该生命周期内不会更新client状态,默认为100000,单位是毫秒 status-lifetime: 100000 # 查询client状态信息时的连接超时时间,默认为2000,单位是毫秒 # 如果2秒内没有获取到client的状态信息,则认为连接已经断开 connect-timeout: 2000 # 查询client状态信息时的读取超时时间,默认为2000,单位是毫秒(如果2秒内没有获取到client的状态信息,则认为读取失败) read-timeout: 2000 # 要被过滤掉的元数据(当与正则表达式相匹配时,这些数据会在输出的json数据中过滤掉) # 默认值是".password", ".secret",".∗secret", ".key", ".",".token", ".credentials.", ".*vcap_services", ".credentials.", ".∗vcap services" metadata-keys-to-sanitize: # 要获取的client的端点信息 # 默认是 "health", "env", "metrics", "httptrace:trace", "threaddump:dump", "jolokia", "info", "logfile", "refresh", "flyway", "liquibase", "heapdump", "loggers", "auditevents" probed-endpoints: instance-proxy: # 向client发起请求时不会被转发的headers信息。默认值是"Cookie", "Set-Cookie", "Authorization" ignored-headers: # 在导航栏中显示的brand值 brand: # 显示的页面标题。默认是"Spring Boot Admin" title: Admin Client 的详细配置参考:https://www.jianshu.com/p/b0528b52772cspring: boot: admin: client: # 是否启用spring boot Admin客户端,默认为true enabled: true # 要注册的Admin Server端的url地址。如果要同时在多个server端口注册,则用逗号分隔各个server端的url地址 url: http://localhost:8888 # server端获取client信息的路径,默认情况下server通过访问/instances请求来获取到client端的信息。(client端向server端注册,注册成功后server端会给该client创建一个唯一的clientID值。当server端需要获取client的信息,比如health信息时,server端会发送http://IP:PORT/instances/clientID/actuator/health即可,这里的http://IP:PORT是client所在服务器的IP地址,instances就是该属性的值) api-path: instances # 如果server端需要进行认证时,该属性用于配置用户名 username: user # 如果server端需要进行认证时,该属性用于配置密码 password: 123456 # 注册时间间隔,默认10000,单位是毫秒(client通过持续不断地向server端进行注册来保持client端与server端的连接) period: 10000 # 注册连接超时时间,默认5000,单位是毫秒。当client向server进行注册时,如果5秒钟没有注册完成则认为本次注册失败 connect-timeout: 5000 # 注册读取超时,默认5000,单位是毫秒 read-timeout: 5000 # 是否开启自动注册,默认为true auto-registration: true # 是否开启自动注销,如果服务端运行在云平台,默认值是true auto-deregistration: null # 默认为true,client只会在一个server端进行注册(按照spring.boot.admin.client.url中设置的server的顺序),如果该server端宕机,会自动在下一个server端进行注册。如果该属性值为false,则会在所有的server端进行注册 egister-once: true instance: # 注册的management-url,如果可用的url不同的话,可以重写该值。 # 默认该属性值与management-base-url 和 management.context-path两个属性值有关 management-url: # 用于计算management-url的基本URL。该路径值在运行时进行获取并赋值给 base url # 默认该属性值与management.port, service-url 以及server.servlet-path有关 management-base-url: # 用于计算service-url的基本URL。该路径值在运行时进行获取并赋值给 base url service-base-url: # 注册的health-url地址,如果可用的url不同可以重写该值 health-url: # 注册的service-url值 service-url: http://192.168.0.66:22586 # 客户端工程的名字。默认值是配置的spring.application.name的值 name: # 是否使用注册的ip地址来取代上述各个url中hostname的值,默认为false prefer-ip: true自定义封装 Starter 启动器启动器一般都是以spring-boot-starter-开头,这种方式一般都是 Spring 官方的命名方式。其他企业开发的启动器命名方式类似于:XXX-boot-starter启动器一般都是一个空壳子,里面没有代码,主要用于管理依赖开发步骤:创建 starter 项目,示例:test-hello-spring-boot-starter创建自动配置项目,示例:test-spring-boot-autoconfigure(1)添加基本的springboot依赖(因为要用@Bean等注解)<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies>(2)定义自动配置类,示例:在配置类中将HelloTemplate定义为一个Bean@Configuration public class HelloAutoConfiguration { @Bean //实例化HelloTemplate,交给Spring IOC 容器管理 public HelloTemplate helloTemplate(){ return new HelloTemplate(); }(3)在resources下创建META-INF目录,在META-INF目录下创建 spring.factories 文件,在 spring.factories 中编辑要扫描的配置类(HelloAutoConfiguration),这样spring就会扫描到 HelloAutoConfiguration 了# 第一行固定写法,第二行这个是我们写的配置类的类名全路径 这样spring就能扫描到了 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.itheima.hellospringbootdemo.util.HelloAutoConfiguration在 starter 项目的 pom 文件中添加自动配置项目的依赖<dependencies> <dependency> <groupId>com.test</groupId> <artifactId>test-spring-boot-autoconfigure</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>在其它工程中添加 stater 项目的依赖,即可通过 @Autowired 使用自动配置项目中注入到Spring容器的 BeanSpringBoot 项目的部署项目打包在SpringBoot项目中,都是通过将项目打成 jar 包,然后直接运行(因为内置了tomcat)准备一个开发完的 SpringBoot 项目,因为是 SpringBoot 项目,所以一定要有启动类在准备打包的项目中的 pom 文件中,添加如下内容<build> <!--finalName 可以不写,写了就是要打包的项目的名称 --> <finalName>demo</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>找到 idea 右侧的 maven 区域,找到想打包的项目,点击clean,清除缓存,如图如果点击test,会将在 test/java 下所有的测试用例都跑一边,所有测试用例都通过后才会执行后面的流程,这里的流程都是下一步会包含上一步。如果在打包时不想运行测试用例,有两种方式跳过:方式1:可以在 pom 中添加如下设置:<properties> <!--打包时跳过测试用例--> <skipTests>true</skipTests> </properties>方式2:在IDEA中配置如下点击 package 进行打包操作,最后显示 BUILD SUCCESS 即为打包成功注意:如果项目有自定义的依赖,则需要先点击 install,将依赖包安装到本地仓库;否则若直接点击package,会报找不到类等错误在 idea 左侧项目的 target 目录下,可以找到刚打好的 jar 包在 jar 包文件夹地址栏上输入cmd,打开命令窗口,在命令行中输入java -jar 项目名.jar,回车即可启动应用Spring Boot 多环境配置切换一般在一个项目中,总是会有好多个环境。比如:开发环境 -> 测试环境 -> 预发布环境 -> 生产环境。每个环境上的配置文件总是不一样的,甚至开发环境中每个开发者的环境可能也会有一点不同。在 Spring Boot中,多环境配置的文件名需要满足 application-{profile}.yml 或者 application-{profile}.properties的格式,其中{profile} 对应环境,比如:application-dev.yml:开发环境application-test.yml:测试环境application-prod.yml:生产环境配置环境的常用方式:方式1:直接在 application.yml中配置:spring: profiles: active: dev方式2:在 IDEA 中指定配置生效:方式3:在启动时添加参数来指定使用哪个配置# 使用测试环境配置文件 java -jar xxx.jar --spring.profiles.active=testSpring Boot 配置加载顺序Spring Boot 若在优先级更高的位置找到了相同的配置,那么它就会无视低级的配置。如果不同的配置是可以同时生效的。根据 Spring Boot 的文档,配置使用的优先级从高到低的顺序,具体如下所示:命令行参数通过 System.getProperties() 获取的 Java 系统参数操作系统环境变量从 java:comp/env 得到的 JNDI 属性通过 RandomValuePropertySource 生成的 "random.*" 属性应用 Jar 文件之外的属性文件(application-{profile}.properties/yml)应用 Jar 文件内部的属性文件(application-{profile}.properties/yml)应用 Jar 文件之外的属性文件(application.properties/yml)应用 Jar 文件内部的属性文件(application.properties/yml)在应用配置 Java 类(包含“@Configuration”注解的 Java 类)中通过 "@PropertySource" 注解声明的属性文件通过 "SpringApplication.setDefaultProperties" 声明的默认属性Spring Boot 加载外部的配置文件方式1:通过环境变量 spring.config.location 指定注意:使用 location 参数指定配置文件后,会使项目默认配置文件(application.properties 或 application.yml )失效,Spring Boot 将只加载指定的外部配置文件java -jar springbootdemo-0.0.1-SNAPSHOT.jar --spring.config.location=./my-application.yml方式2:通过环境变量 spring.config.additional-location 指定注意:使用 additional-location 参数不会使项目默认的配置文件失效,外部配置文件会与项目默认的配置文件共同生效,形成互补配置,且其优先级比所有默认配置文件的优先级都高java -jar springbootdemo-0.0.1-SNAPSHOT.jar --spring.config.additional-location=./my-application.yml
概述Spring官网:https://spring.io/projectsSpring Boot 是在Spring框架基础上创建的一个全新框架,是Spring项目中的一个子工程,与Spring-framework 同属于Spring的产品。Spring Boot 称为搭建程序的脚手架 。其最主要作用是快速的构建庞大的Spring项目,并且尽可能的减少一切xml配置,做到开箱即用,迅速上手,让开发者关注于业务而非配置。Spring Boot 简化了基于Spring的应用开发,为Spring平台及第三方库提供开箱即用的设置(提供默认设置,存放默认配置的包就是启动器starter),多数Spring Boot应用只需要很少的Spring配置。 Spring Boot 内置了tomcat,无需再单独配置tomcat。Spring Boot 设计的目的是简化 Spring 应用的搭建和开发过程,它不但具有Spring的所有优秀特性,而且具有如下显著的特点:为 Spring 开发提供更加简单的使用和快速开发的技巧具有开箱即用的默认配置功能,能根据项目依赖自动配置具有功能更加强大的服务体系,包括嵌入式服务、安全、性能指标,健康检查等绝对没有代码生成,可以不再需要 XML 配置,即可让应用更加轻巧和灵活Spring Boot 对于一些第三方技术的使用,提供了非常完美的整合,使用简单。SpringBoot 项目搭建基础依赖引入Spring Boot 官网版本列表:https://spring.io/projects/spring-boot#learnRELEASE GA:General Availability,正式发布的版本,官方推荐使用此版本。SNAPSHOT:快照版,可以稳定使用,且仍在继续改进版本。PRE:预览版,内部测试版,主要是给开发人员和测试人员测试和找BUG用的,不建议使用。<?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.test</groupId> <artifactId>spring-boot-ssm</artifactId> <version>1.0-SNAPSHOT</version> <!-- 使用SpringBoot框架必须引入 spring-boot-starter-parent 作为父项目 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> </parent> <properties> <!-- 设置JDK版本 --> <java.version>1.8</java.version> </properties> <dependencies> <!-- web启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 单元测试启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!---- 数据库相关 ----> <!-- jdbc启动器,spring-boot-starter-jdbc默认集成了HikariCP连接池 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- druid连接池启动器 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 持久层框架启动器 --> <!---- 工具类 ----> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>启动类App.javaimport org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class,args); }Spring Boot 配置Spring 加载配置的注解@Configuration :声明一个类作为配置类,代替 xml 文件@Bean : 声明在方法上,将方法的返回值加入 Bean 容器,代替 <bean>标签@Value :属性注入,替代 xml 中的属性注入格式: @Value("${属性}")@PropertySource :加载指定的属性文件(*.properties)到 Spring 的 Environment 中可以配合 @Value 和 @ConfigurationProperties 使用@PropertySource 和 @Value 组合使用:可以将自定义属性文件中的属性变量值注入到当前类的使用@Value注解的成员变量中。@PropertySource 和 @ConfigurationProperties 组合使用:可以将属性文件与一个Java类绑定,将属性文件中的变量值注入到该Java类的成员变量中。@ConfigurationProperties:自动配置绑定@ConfigurationProperties:用于自动配置绑定 yml 配置文件的属性和 java 对象的属性支持属性:value / prefix 属性:配置文件配置项前缀ignoreInvalidFields 属性:默认为false,值类型不匹配将会爆出异常ignoreUnknownFields 属性:默认为true,忽略掉对象中未知的字段用法1:标注在类上,转换配置文件配置项为bean对象注意:需要将标注了 @ConfigurationProperties 注解的类注册到spring容器中,方式有两种:方式1:在标注了@ConfigurationProperties 注解的类上使用 @componet 等 IOC 注解方式2:在标注了@componet 等 IOC 注解的类上或配置类上标注 @EnableConfigurationProperties(bean类名.class)需要有无参构造方法、getter和setter方法@Data @AllArgsConstructor @NoArgsConstructor @Component @ConfigurationProperties(prefix = "app.mycar") public class Car { private int price; private String brand; }@Configuration @EnableConfigurationProperties(Car.class) public class Config { }用法2:标注在配置类的方法上,搭配 @bean 使用,绑定第三方属性@Configuration public class DbConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.druid") public DataSource datasource(){ return new DruidDataSource(); }Environment:获取运行环境变量Spring中的Environment用来表示整个应用运行时的环境,可以使用Environment类获取整个运行环境中的配置信息。方法:public String getProperty(String key) public <T> T getProperty(String key, Class<T> targetType) // 注:key为配置文件中的key示例:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; @Configuration // 声明这个类是一个配置类 @PropertySource(value = "classpath:user.properties") //加载配置文件 public class UserConfig { @Autowired private Environment environment; @Bean //创建User对象,交给spring容器 User对象中的值从配置文件中获取 public User getUser() { User user = new User(); user.setUsername(environment.getProperty("user.username")); user.setPassword(environment.getProperty("user.password")); return user; }支持的配置文件类型及优先级支持的配置文件类型:application.propertiesapplication.ymlapplication.yaml配置文件被加载的优先级是:properties > yml > yaml若相同的配置在多种配置文件类型中都配置了,则优先级高的配置生效默认配置和常用基础配置Spring Boot 封装了大量的默认配置:Spring boot 各版本文档目录:https://docs.spring.io/spring-boot/docs/SpringBoot 2.3.0.RELEASE版本的默认配置列表(其他版本类似):Common Application propertiesSpring Boot 常用基础配置:server: # 服务端口号,默认8080 port: 8080 logging: level: # 微服务的日志配置。格式:包路径: 日志级别 com.test: debug spring: application: name: test-service datasource: # 数据源配置 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/leyou?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC username: root password: root type: com.alibaba.druid.pool.DruidDataSourceSpring MVC 配置端口配置设置web访问端口server: port: 8080日志配置日志级别分为 FATAL、ERROR、WARN、INFO、DEBUG、ALL 或者自定义的级别。Log4j 建议只使用四个级别,优先级从高到低分别是 ERROR、WARN、INFO、DEBUG企业生产环境,一般设置为 INFO 级别,表示打印 INFO 及以上级别的日志,不打印 DEBUG 及以下级别的日志开发和测试环境,一般设置为 DEBUG 级别,表示所有的级别的日志都能输出日志级别控制:logging: level: com.test: debug org.springframework.web: debug说明:logging.level:固定写法,说明下面是日志级别配置,日志相关其它配置也可以使用com.test 和 org.springframework 是指定包名,后面的配置仅对这个包有效静态资源默认的静态资源路径为:classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public只要静态资源放在这些目录中任何一个,SpringMVC都会处理。一般把静态资源放在classpath:/static/目录下。拦截器如果想要保持 Spring Boot 的一些默认 MVC 特征,同时又想自定义一些 MVC 配置(包括:拦截器,格式化器, 视图控制器、消息转换器 等等),可以让一个类实现 WebMvcConfigurer 接口,并且添加 @Configuration 注解,但不能添加@EnableWebMvc 注解如果想要自定义 HandlerMapping、HandlerAdapter、ExceptionResolver 等组件,可以创建一个 WebMvcRegistrationsAdapter 实例来提供以上组件如果想要完全自定义 Spring MVC,不保留 Spring Boot 提供的一切特征,可以自己定义类并且添加 @Configuration 注解和 @EnableWebMvc 注解实现步骤:自定义拦截器(实现 HandlerInterceptor 接口)@Slf4j public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.debug("preHandle方法执行..."); return true; @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.debug("postHandle方法执行..."); @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.debug("afterCompletion方法执行..."); }添加配置类(实现 WebMvcConfigurer 接口),注册拦截器@Configuration public class MyWebConfig implements WebMvcConfigurer { * 注册自定义拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { // 通过registry来注册拦截器,通过addPathPatterns来添加拦截路径 registry.addInterceptor(new MyInterceptor()) .addPathPatterns("/user/**") .excludePathPatterns("/user/login"); // 这个地址的不拦截 }ApplicationRunner 接口方法:只定义了一个 run(ApplicationArguments args) 方法run 方法的参数 ApplicationArguments 可以获取到当前项目执行的命令参数(比如把这个项目打成 jar 执行的时候,带的参数可以通过 ApplicationArguments 获取到)。由于该方法是在容器启动完成之后,才执行的,所以,这里也可以从 spring 容器中拿到其他已经注入的 bean。使用场景:springBoot 项目启动时,若想在启动之后直接执行某一段代码,就可以自定义一个类实现 ApplicationRunner 这个接口,并重写接口的 run 方法。@Component // 此类必须要交给spring管理才生效 public class ConsumerRunner implements Application{ @Oberride public void run(ApplicationArgumers args) throws Exception{ System.out.println("需要在springBoot项目启动时执行的代码---"); }在同一个项目中,可以定义多个 ApplicationRunner 的实现类。如果有多个实现类,同时又需要它们按一定顺序执行,可以通过在实现类上加上 @Order 注解或者实现 Ordered 接口来实现。SpringBoot 会按照 @Order 中的 value 值从小到大依次执行。即值越小拥有越高的优先级,值越小越先被加载。注:值可为负数。
MVC 模式和 Spring MVC 介绍MVC : 是一种用于设计创建web应用表现层的模式,主要作用是将视图展示和业务控制代码分离开来MVC 使用了三种角色来分别处理不同的功能:Model(模型):数据模型(封装对象)View(视图):负责数据的展示(html,jsp)Controller(控制器):负责调度,用于程序业务逻辑处理MVC架构跟三层架构的关系:MVC把三层架构中的表现层再度进行了分化,分成了控制器、视图、模型。三层架构的目的是解耦,mvc的目的是实现web系统的职责划分。MVC架构在三层架构中的位置图示:Spring MVC 介绍SpringMVC是Spring产品对MVC模式的一种具体实现,属于轻量级的WEB框架。它通过一套简单的注解,让一个普通的 Java 类成为控制器,而无须实现任何接口。同时还支持RestFul风格的编程风格。SpringMVC的功能就是封装了原来Servlet中的共有功能,例如请求参数解析处理、请求结果封装返回等。SpringMVC是 Spring 框架(Spring Framework)的子模块,也存在容器的概念(支持 Spring 的注解)。Spring MVC 入门案例(xml)SpringMVC依赖 <dependencies> <!--springmvc--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.6.RELEASE</version> </dependency> <!--前端控制器 servlet--> <dependency> <groupId>javax.servlet</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> </dependencies>开启SpringMVC注解支持<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 包扫描 --> <context:component-scan base-package="cn.test"></context:component-scan> <!-- 配置springmvc的注解驱动。内置了处理器映射器和处理器适配器 --> <mvc:annotation-driven></mvc:annotation-driven> <!-- 视图解析器 --> <!-- 配置返回页面的前缀和后缀。当需要返回一个视图的时候,只需写视图的名称,视图解析器会自动在该名称上拼接前后缀。 前缀 + 控制器返回值 + 后缀 /WEB-INF/jsps/ + 控制器返回值 + .jsp --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsps/"></property> <property name="suffix" value=".jsp"></property> </bean> </beans>web.xml 中配置前端控制器<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <!-- 配置SpringMVC的前端控制器 DispatcherServlet --> <servlet> <servlet-name>mvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--自动加载Springmvc的配置文件--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>mvc</servlet-name> <!-- 处理所有请求,不处理.jsp为后缀的请求 --> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>Controller:import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; * 控制器:需要交给容器管理 * 方法:接受请求,调用service,完成响应和页面跳转 @Controller public class HelloController { * 控制器方法: * 接受请求,调用service,完成响应和页面跳转 * 1、返回值String (跳转的页面路径) * 2、需要在方法上通过注解@RequestMapping配置进入此方法的url @RequestMapping(value="/hello") public String hello() { System.out.println("hello heima23"); return "success"; // /WEB-INF/jsps/ + 控制器返回值 + .jsp }Spring MVC 原理web 工程执行过程浏览器发出请求 http://localhost/helloTomcat接收请求,经过请求解析封装出Request和Response对象,然后转交给应用程序配置的DispatcherServlet会拦截到请求的路径DispatcherServlet经过一番分析处理,会将请求转发到自定义的Controller上(@RequestMapping)Controller经过处理,给出了一个返回路径DispatcherServlet拿到这个路径会找到对应的 JSP 进行视图渲染。Spring MVC 执行流程用户通过浏览器发送请求至DispatcherServletDispatcherServlet收到请求调用HandlerMappingHandlerMapping找到具体的处理器链返回给DispatcherServletDispatcherServlet会根据返回的处理器链调用HandlerAdapterHandlerAdapter经过适配调用具体的Handler(controller)Controller执行完成返回一个执行结果HandlerAdapter将Handler的结果ModelAndView对象返回给DispatcherServletDispatcherServlet将ModelAndView对象传给ViewResloverViewResolver解析后得到具体View,并返回给DispatcherServletDispatcherServlet根据View进行视图渲染(即将模型数据填充至视图中)DispatcherServlet会将渲染后的视图响应给浏览器Spring MVC 的四大组件前端控制器(DispatcherServlet):SpringMVC的核心组件(DispathcherServlet),协调所有组件的运行处理器映射器(HandlerMapping):负责根据URL请求找到对应的的处理器(Controller中的方法)处理器适配器(HandlerAdapter):统一适配器接口,对处理器进行了一个封装,可以统一调用。真正的去调用处理方法(执行Controller中的方法)视图解析器 (ViewReslover):根据逻辑视图匹配到真正的物理视图物理视图:jsp页面的完整路径Spring MVC 请求和响应中各组件的执行顺序Controller 层映射请求@RequestMapping 注解@RequestMapping:用于建立请求URL和处理方法之间的映射关系,也可以通过它的属性对请求做出各种限制可标注在类、方法上,若用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。属性:value / path 属性:指定映射的请求地址,指定的地址可以是URI Template 模式method 属性:指定请求的 method 类型, GET、POST、PUT、DELETE等,示例:RequestMethod.postparams 属性:指定 request 中必须包含的参数,若请求没有携带指定参数,则抛出异常headers 属性:指定 request 中必须包含某些指定的header值,才能让该方法处理请求。consumes 属性:指定处理请求的提交内容类型(Content-Type),如application/json、text/html;produces 属性:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回@GetMapping、@PostMapping 等注解@GetMapping、@PostMapping、@PutMapping、@DeleteMapping 等注解Spring MVC 注解 @RequestMapping(method = RequestMethod.GET/POST/PUT/DELETE...) 的缩写用于将 HTTP 映射到特定的处理方法上,简化常用的 HTTP 方法的映射,并更好地表达被注解方法的语义请求参数的接收与处理接收请求参数在SpringMVC中可以使用多种类型来接收前端传入的地址栏参数简单类型(8种基本类型 \ 8种基本类型的包装类型 \ 字符串)使用方式:只需要保证前端传递的参数名称跟方法的形参名称一致即可//处理器中属性名必须和请求参数名一致 @RequestMapping("/simpleParam") public String simpleParam(Integer id, String name){ System.out.println(id); System.out.println(name); return "success"; }对象(pojo)类型使用方式:只需要保证前端传递的参数名称跟 pojo 的属性名称(set方法)一致即可,所有的请求参数自动地封装到 java 对象中@RequestMapping("/dtoParam") public String voParam(UserDTO dto){ System.out.println(dto); return "success"; }数组类型使用方式:只需要保证前端传递的参数名称跟方法中的数组形参名称一致即可同名参数传递,自动的将请求参数封装到数组种集合类型将集合包装到一个对象中即可,自动的将前端传入的数据封装到对象中的集合属性日期类型在 Spring MVC 中内置了一系列的类型转化器,可以自动的将请求参数的 String 类型转化为某种格式(例如:Integer)对于一些常见的类型, Spring MVC 是内置了类型转换器的,但是对于一些格式比较灵活的参数(日期 时间),Spring MVC 无法完成类型转换,就需要自定义类型转换器处理请求参数@RequestParam 注解标注在 Controller 方法参数之前,用于对请求中 url 传入的普通参数做一些限制,支持三个属性:value / name 属性:默认属性,用于绑定请求传入的参数名称。一般在请求传参和 Controller 方法入参不一致时使用required 属性:用于指定此参数是否必传@RequestParam 修饰的参数默认必须传值,可以用 required = false 来指定非必传值defaultValue 属性:指定参数默认值(当参数为非必传参数且请求没有传入该参数时,使用默认值)@RequestMapping("/list1") public String test1(int userId) { return "list"; @RequestMapping("/list2") public String test2(@RequestParam int userId) { return "list"; }不加 @RequestParam 注解:前端的参数名需要和后端控制器的变量名保持一致才能生效参数为非必传接受请求头信息在控制器中获取当前请求的请求头的方式:@RequestHeader 注解 配置到方法参数上 : 前端控制器自动获取头信息@RequestHeader Map map : 获取所有的请求头@RequestHeader("cookie") String cookie : 根据key从所有头信息中获取指定头信息@CookieValue("key") :从cookie中根据key获取指定value数据 /** * 在控制器中获取,当前请求的请求头 @RequestMapping(value="/demo12") public String demo12(@RequestHeader Map map, @RequestHeader("cookie") String cookie, @CookieValue("JSESSIONID") String sessionId) { System.out.println(map); System.out.println(cookie); System.out.println(sessionId); return "success"; }文件上传客户端文件上传的三要素form表单的 method = postform表单的 enctype=“multipart/form-data”from表单中的input的 type = filespringmvc中服务端文件上传依赖<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency>需要在SpringMVC的配置文件中,配置文件上传解析器(自动的将上传的内容,转化为MultipartFile对象)<!-- 文件解析器 id:固定值(multipartResolver) property:其中指定上传文件的大小规则 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!--文件的大小规则 5M = 1024 * 1024 * 5 --> <property name="maxUploadSize" value="5242880"></property> </bean>在控制器方法上,直接使用MultipartFile对象参数封装上传的文件import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; @Controller public class FileUploadController { * 上传文件到服务端。服务端将文件转存到E:\file * MultipartFile :封装上传的文件 * 参数名 :和上传文件的input的name属性一致 @RequestMapping("/upload") public String upload(MultipartFile myFile, String name) throws IOException { System.out.println("name=" + name); System.out.println(myFile.getOriginalFilename()); //获取上传文件名 System.out.println(myFile.getSize()); //获取上传文件大小 File file = new File(new File("E:\\file"), myFile.getOriginalFilename()); myFile.transferTo(file); //写入文件内容 return "success"; }@RequestBody、@ResponseBody 注解Ajax + json 实现异步交互在 Spring MVC 中进行 ajax 的数据交互,可以通过两个注解简化开发@RequestBody : 自动的将请求的 json 字符串,转化为指定 java 对象(处理请求)@ResponseBody :自动的将 java 对象,通过转换器转换为指定的格式(通常为 json 字符串))并响应(处理响应)@RestController 注解 = @ResponseBody + @Controller 组合 @RequestMapping("/testAjax") @ResponseBody public User testAjax(@RequestBody User user) { System.out.println("ajax请求的数据对象=" + user); //调用业务逻辑 User user2 = new User(); user2.setUsername("张三"); return user2; }@RestController 注解: @ResponseBody + @Controller 组合@RestController 中包含 @RessponseBody 的注解效果,故该 Controller 中的方法,就无法返回 jsp、html 界面,配置的 InternalResourceViewResolver 不工作,只能返回 return 的内容注意:Spring MVC 默认使用 MappingJackson2HttpMessageConverter 对 json 数据进行转换,需要加入 jackson 的包 <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency>Spring Boot 默认集成了 jackson ,不再需要添加 jackson 相关依赖Spring MVC 操作 Servlet 对象Servlet 原生 API 对象:HttpServletRequestHttpSevletResponseHttpSession语法规则:方式1(推荐):将对象以方法参数的形式配置到 Controller 方法中 /** * 获取Servlet原生API对象(request,response,session) @RequestMapping("/demo2") public String demo2(HttpServletRequest request, HttpServletResponse response, HttpSession session) { request.setAttribute("user", "1"); System.out.println(request); System.out.println(response); System.out.println(session); return "success"; }方式2:将需要的 API 对象通过 @Autowired 的方式注入Spring MVC 操作 session 域使用传统方式操作session域@RequestMapping("/hello13.action") public String hello13(HttpSession session){ session.setAttribute("prod", "电视机"); return "hello"; @RequestMapping("/hello14.action") public String hello14(HttpSession session){ String prod = (String) session.getAttribute("prod"); System.out.println(prod); return "hello"; }使用 @SessionAttribute("name") ,从session里面根据name来获取对应的参数的值@GetMapping("/test-mvc-get-session") public String testMvcGetSession(@SessionAttribute("girl") String girl) { System.out.println("testMvcGetSession -----> girl in session is " + girl); return "success"; }使用ModelMap对象从session域中获取值@RequestMapping("/getValue") public String getValue(ModelMap modelMap){ System.out.println(modelMap.get("value")); return "success"; }清空session域@RequestMapping("/delValue") public String delValue(SessionStatus sessionStatus){ //清空session sessionStatus.setComplete(); return "success"; } 页面跳转页面跳转之转发请求转发:只发送一起请求,不会丢失数据(SpringMVC的默认页面跳转形式)方式1:直接返回逻辑视图底层请求转发经过视图解析器,添加前后缀组成物理视图(页面的完成路径)渲染并跳转方式2:使用 forward 转发语法规则(返回值): forward: 物理视图forward:关键字后面的路径表示不再经过视图解析器 @RequestMapping("/demo3") public String demo3() { System.out.println("forward转发"); return "forward:/WEB-INF/jsps/success.jsp"; }方式3:使用servlet原生api注意:控制器方法返回值:void @RequestMapping("/demo3") public void demo3(HttpServletRequest request, HttpServletResponse response) throws Exception { System.out.println("servlet原生API对象"); request.getRequestDispatcher("/WEB-INF/jsps/success.jsp").forward(request, response); }请求转发时携带数据转发时携带数据:返回响应数据。方式1(推荐):将数据绑定到request域 @RequestMapping("/demo4") public String demo4(HttpServletRequest request){ // 将数据绑定到request域 request.setAttribute("username", "张三") return "result" }页面通过 el 表达式获取响应数据并展示方式2:绑定到Model对象Model:SpringMVC中的Model配置到参数上,底层通过Request实现。可以用于替换request完成数据响应 @RequestMapping("/demo4") public String demo4(Model model){ // 将数据绑定到Model对象 model.addAttribute("username", "张三") return "result" }方式3(官方):通过ModelAndView返回ModelAndView : 模型视图对象,通过此对象可以指定返回的视图地址和数据绑定语法规则:方法的返回值:ModelAndView在方法中通过ModelAndView的setViewName方式指定跳转的页面在方法中通过ModelAndView的addObject方式指定需要存入request域中的数据 @RequestMapping("/demo4") public ModelAndView demo4() { ModelAndView mv = new ModelAndView(); // 指定响应数据 mv.addObject("username", "传智播客"); // 配置返回页面视图地址 mv.setViewName("result"); // 支持逻辑视图,支持forward:物理视图 return mv; }页面跳转之重定向重定向:通过各种方法将各种网络请求重新定个方向转到其它位置(如:网页重定向、域名的重定向、路由选择的变化也是对数据报文经由路径的一种重定向)。实现原理:客户端浏览器发送http请求,web服务器接收请求后发送302状态码响应及对应新的location给客户端,客户端发现是302请求,则自动再发送一个新的http请求,请求url是新的location,服务器再根据新的http请求响应客户端。在这里的location可以定义到任意的url,既然是浏览器重新发送了请求,则就没有什么request传递的概念了,在客户端浏览器的路径栏显示的是其重定向的路径,客户端可以观察到路径的变化。特点:是客户端的行为是浏览器至少做了两次访问请求的浏览器的地址发生改变两次跳转之间的传输的数据丢失(request范围),即 无法从重定向后的页面获取到表单的数值可以重定向到任意的url重定向的方式:方式1(推荐):使用 redirect 重定向 @RequestMapping("/demo5") public void demo5(HttpServletRequest request, HttpServletResponse response) throws IOException { System.out.println("第一次请求"); return "redirect:/demo6"; @RequestMapping("/demo6") public String demo6() { System.out.println("重定向到demo6"); return "success"; }方式2:使用servlet原生API @RequestMapping("/demo5") public void demo5(HttpServletRequest request, HttpServletResponse response) throws IOException { System.out.println("第一次请求"); response.sendRedirect("/demo6"); @RequestMapping("/demo6") public String demo6() { System.out.println("重定向到demo6"); return "success"; }释放静态资源(不交给 Spring MVC 处理)当有静态资源需要加载时,比如 jquery.js,通过谷歌开发者工具抓包发现,没有加载到 jquery.js 的原因:现在 Spring MVC 的前端控制器 DispatcherServlet 的 url-pattern 配置的是 /(缺省),代表除了jsp请求不拦截, 其他的所有请求都会拦截,包括一些静态文件(js、html、css、jpg等等),而拦截住之后, 它又找不到对应的处理器方法来处理, 因此报错释放静态资源方式方式1(推荐):在Springmvc的配置文件中配置统一释放所有的静态资源文件当SpringMVC处理静态资源时,委托给默认的Servlet处理默认Servlet:tomcat中的默认Servlet<mvc:default-servlet-handler/>方式2:在SpringMVC的配置文件中配置释放静态资源mvc:resources 标签mapping 标签:请求路径的URL的映射规则location 标签:静态资源的物理目录当静态资源请求到SpringMVC的前端控制器时,根据释放资源的配置不再查找具体的controller处理从location路径中查找匹配的资源 <mvc:resources mapping="/js/*" location="/js/"></mvc:resources> <mvc:resources mapping="/image/*" location="/image/"></mvc:resources> <mvc:resources mapping="/css/*" location="/css/"></mvc:resources>方式3:修改web.xml中前端控制器的URL映射规则,以特殊字符串结尾的请求交给前端控制器处理servlet-mapping 标签url-pattern 标签:/* :对所有请求有效(包含 jsp 页面)/ :对所有请求有效(不包含 jsp 页面)需要进入SpringMVC的所有请求,都需要以 .do 结尾控制器方法中 @RequestMapping 不需要再做额外的配置 <servlet-mapping> <servlet-name>mvc</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping>
概述Spring 体系概述Spring 是于2003年兴起的一个 full-stack 轻量级的 Java 开源框架,由 Rod Johnson 创建,使用 Spring 可以更快、更轻松、更安全地进行 Java 编程Spring 是一个生态体系,或一个超级粘合平台,常见的 Spring 项目有:Spring Boot,Spring Framework,Spring Data,Spring Cloud,Spring Cloud Data Flow,Spring Security,Spring GraphQL,Spring Session 和 Spring Web Services 等Spring 提供了展现层 Spring MVC、持久层 Spring JDBC、业务层事务管理等众多的企业级应用技术,Spring 还能整合开源世界众多的第三方框架和类库,逐渐成为使用最多的 Java EE企业应用开源框架Spring 并不等同于 Spring 框架(Spring Framework),这是常见的误区Spring 框架(Spring Framework)概述Spring 框架,即 Spring Framework 框架,是 Spring 生态的其中一个重要项目,也是其他 Spring 全家桶(SpringMVC、SpringBoot、SpringCloud、SpringData等)的基础和核心Spring 框架分为多个模块,应用程序可以选择需要的模块。Spring 框架的核心是 Core Container(核心容器)模块,包括配置模型和依赖注入机制Spring 框架为不同的应用程序架构提供基础支持,包括消息传递、事务数据和持久性以及 WebSpring 框架还包括基于 Servlet 的 Spring MVC Web 框架,以及并行的 Spring WebFlux 反应式 Web 框架Spring 框架是分层的JavaSE/EE一站式轻量级开源框架,以 IOC(控制反转)和 AOP (面向切面编程)为核心Spring 框架的特点方便解耦,简化开发:将所有对象的创建和依赖关系维护交给Spring管理方便集成各种优秀框架:Spring内部提供了对各种优秀框架(Struts2、Hibernate、MyBatis)的直接支持降低了Java EE API 使用难度:对 JAVA EE开发中一些API( JDBC、JavaMail、远程调用)都提供了封装方便程序测试:支持 JUnit4,可以通过注解方便地测试 Spring 程序AOP变成支持:面向切面编程,可以方便地实现对程序进行权限拦截和运行监控等功能声明式事务:通过配置就可以完成对事务的管理,无需编程Spring 框架的架构和模块说明Spring 框架的主要模块Core Container(核心容器)是其他模块建立的基础,由 Beans 模块、Core 核心模块、Context 上下文模块和 SpEL 表达式语言模块组成Core(spring-core):封装了Spring框架的底层部分,包括资源访问、类型转换以及Spring 框架基本的核心工具类Spring 其它组件要都要使用到这个包里的类,是其它组件的基本核心,也可以在自己的应用系统中使用这些工具类外部依赖:Commons Logging, (Log4J)jar包:org.springframework spring-core 4.3.7.RELEASEBeans(spring-beans):提供了框架的基础部分,包括访问配置文件、控制反转(IOC)和依赖注入(DI)外部依赖:spring-corejar包:org.springframework spring-beans 4.3.7.RELEASESpEL(spring-expression):提供了强大的表达式语言支持,用于在运行时查询和处理对象图。该语言支持设置和获取属性值;属性赋值,方法调用,访问数组的内容,收集和索引器,逻辑和算术运算,命名变量,并从Spring的IOC容器的名字对象检索,它也支持列表选择和投影以及常见的列表聚合jar包:org.springframework spring-expression 4.3.7.RELEASEContext(spring-context):上下文模块,建立在 Core 和 Beans 模块的基础之上,集成Beans模块并添加资源绑定、数据验证、国际化、Java EE支持、容器生命周期、事件传播等为Spring 核心提供了大量扩展。可以找到使用Spring ApplicationContext特性时所需的全部类,JDNI 所需的全部类,instrumentation组件以及校验Validation 方面的相关类。外部依赖:spring-core,spring-beans,spring-expression,spring-aopjar包:org.springframework spring-context 4.3.7.RELEASE依赖关系:Data Access/Integration(数据访问与集成)包含模块:JDBC、ORM、OXM、JMS、TransactionJDBC(spring-jdbc):包含对Spring 对JDBC 数据访问进行封装的所有类jar包:org.springframework spring-jdbc 4.3.7.RELEASEORM(spring-orm):提供了与“对象-关系”映射框架集成的API,包括 JPA、JDO、Hibernate、MyBatis等。OXM(spring-oxm):提供了 Object/XML 映射的抽象层实现,如 JAXB、Castor、XMLBean、JiBX、XStream将 java对象映射成为XML数据,或者将 XML 数据映射成java对象JMS(spring-jms):java消息服务,提供了一套“消息生产者、消息消费者”模板,JMS用于两个应用程序之间,或者分布式系统中发送消息,进行异步通信Transaction(spring-tx):事务控制,为JDBC、Hibernate、JDO、JPA、Beans等提供的一致的声明式和编程式事务管理支持jar包:org.springframework spring-tx 4.3.7.RELEASE依赖关系:Web包括模块:Web、servlet、websocket、portletWeb(spring-web):提供了基本web集成特性,例如:多文件上传、使用 Servlet 监听器的 IOC 容器初始化以及 web 应用上下文包含Web 应用开发时,用到Spring 框架时所需的核心类,包括自动载入Web ApplicationContext 特性的类、Struts 与JSF 集成类、文件上传的支持类、Filter 类和大量工具辅助类jar包:org.springframework spring-web 4.3.7.RELEASEservlet(spring-webmvc):提供了 SpringMVC Web 框架实现。SpringMVC 提供了基于注解的请求资源注入、更简单的数据绑定、数据验证等以及一套非常简易的 JSP 标签包含Spring MVC 框架相关的所有类,包括框架的Servlets,Web MVC框架,控制器和视图支持如果应用使用了独立的MVC 框架,则无需这个JAR 文件里的任何类jar包:org.springframework spring-webmvc 4.3.7.RELEASEWebSocket:提供了简单的接口,用户只需要实现接口就可以快速搭建 websocket server,从而实现双向通讯Portlet(spring-webmvc-portlet):提供了Portlet环境中MVC实现依赖关系:AOP、aspects、spring-instrument 、messagingaop部分包含4个模块AOP(spring-aop):提供了面向切面编程,提供了比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,并且能动态把这些功能添加到需要的代码中jar包:org.springframework spring-aop 4.3.7.RELEASEAspects(spring-aspects):提供与AspectJ的集成,是一个功能强大且成熟的面向切面编程框架以便可以方便的将面向方面的功能集成进IDE中,比如Eclipse AJDTjar包:org.springframework spring-aspects 4.3.7.RELEASEInStrumentation(spring-instrument):检测,提供了类工具的支持和类加载器的实现Messaging(消息处理):提供了对消息传递体系结构和协议的支持依赖关系:Test:Spring 支持 Junit 和 TestNG 测试框架,而且还额外提供了一些基于 Spring 的测试功能。Spring 框架 的 API 体系Spring Framework 的API体系异常庞大,暂时只介绍 BeanFactory 和 ApplicationContext:BeanFactoryBeanFactory 是 Spring 的"心脏",IOC 容器的核心接口,定义了IOC的基本功能Spring 使用它来配置文档,管理bean的加载,实例化并维护bean之间的依赖关系,负责bean的声明周期ApplicationContextApplicationContext 由 BeanFactory 派生而来,可以比喻为Spring的躯体ApplicationContext 在 BeanFactory 的基础上添加了很多功能:支持了 aop 功能和 web 应用MessageSource,提供国际化的消息访问通过配置来实现 BeanFactory 中很多编码才能实现的功能ApplicationContext 的常用实现类:ClassPathXmlApplicationContext:从classpath目录读取配置文件FileSystemXmlApplicationContext:从文件系统或者url中读取配置文件AnnotationConfigApplicationContext:读取注解。当使用注解配置容器对象时,需要使用此类来创建 spring容器BeanFactory 和 ApplicationContext 的区别:beanFactory 主要是面向 Spring 框架的基础设施,也就是供 spring 自身内部调用Applicationcontext 主要面向 Spring 的使用者BeanFactroy 是在第一次使用到某个Bean(调用getBean()方法)时,才对该Bean进行加载实例化ApplicationContext 是在容器启动时,一次性创建并加载了所有的BeanSpring Boot 框架概述Spring Boot 也是 Spring 生态中一个极其重要的项目,Spring Boot 是对 Spring Framework 的扩展,也可以说它是一个服务于框架的框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程Spring Boot 的服务范围是简化配置文件,它消除了设置 Spring 应用程序所需的XML配置,即尽可能的自动配置 Spring 应用Spring Boot 直接嵌入了 Tomcat、Jetty 或 Undertow(无需部署WAR文件),并且提供生产就绪功能,例如指标、运行状况检查和外部化配置等等,为更快和更高效的开发应用程序铺平了道路IOC(控制反转)IOC概述、IOC 容器工作原理IOC(控制反转)是一种设计思想,目的是指导设计出更加松耦合的程序:控制:指的是对象控制权,在 java 中可以简单理解为对象的控制权限(比如对象的创建、销毁等权限)反转:指的是将对象的控制权由原来的程序员在类中主动控制反转到由 Spring 容器来控制主要功能:解耦Spring IOC 容器 是对 IOC 思想的一种实现:对象的创建交由 Spring 框架管理,需要对象时从 Spring IOC 容器中获取即可底层原理:反射Spring 的 IOC容器 工作原理:当 Spring 的 IOC 容器加载时,会读取配置文件中的诸多 bean 配置根据 bean 的 class 的值寻找对应的 Class 字节码文件通过反射技术,创建出一个个对象创建的对象会被存放到内部的一个 Map 结构中,等待被使用当需要使用具体的对象时就无须手动创建,而是直接从 Spring 的IOC容器中Spring IOC 的入门案例(xml)案例:通过Spring中内置的容器获取对象操作步骤:创建工程导入依赖配置接口和实现类编写Spring的配置文件测试:从容器中获取对象创建工程导入依赖 <dependencies> <!--spring的坐标--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.6.RELEASE</version> </dependency> <!--单元测试--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies>创建Dao接口和实现类(略)创建Spring配置文件spring配置文件约定俗称:applicationContext.xmlspring配置文件放置到:resource目录下spring配置文件,需要引入名称空间(约束)在spring的配置文件中通过 <bean>标签,定义对象id和实现类全类名在resource目录下创建applicationContext.xml配置文件<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 定义配置信息 id:唯一标志(获取的时候,调用容器的getBean("id")) class:实现类的全限定类名 --> <!--把数据库连接池对象放入IOC容器--> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql:///spring"/> <property name="user" value="root"/> <property name="password" value="root"/> </bean> <!--把QueryRunner放入到IOC容器中--> <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner"> <constructor-arg name="ds" ref="dataSource"/> </bean> <!--把dao对象交给IOC容器--> <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"> <property name="queryRunner" ref="queryRunner"/> </bean> <!--把service交给IOC容器--> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"/> </bean> </beans>测试import cn.test.dao.UserDao; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; * 测试从容器中获取对象 public class UserDaoTest { public static void main(String[] args) { //1、根据配置文件获取容器 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); //2、调用容器的方法,获取对象 UserDao userDao = (UserDao)ac.getBean("userDao"); //3、测试 userDao.save(); }执行过程分析初始化 Spring Bean 对象(xml)方式1:默认无参构造函数IOC容器通过反射调用,根据默认无参构造函数来创建类对象。如果 bean 中没有默认无参构造函数,将会创建失败。<bean id="userDao" class="cn.test.dao.impl.UserDaoImpl"></bean>方式2:工厂模式创建对象在Spring中还可以通过工厂模式来创建对象。工厂模式又分两种:静态工厂:不产生工厂的实例,直接调用工厂的静态方法创建对象实例工厂:先产生工厂的实例,再调用工厂实例的方法创建对象(1)java代码编写工厂类public class FactroyCreateBean { // 静态工厂 public static UserDao createUserDao(){ return new UserDaoImpl(); // 实例工厂 public UserDao createUserDaoSimple(){ return new UserDaoImpl(); }(2)xml配置文件配置<!--使用静态工厂创建对象--> <bean id="userDao1" class="cn.test.factory.FactroyCreateBean" factory-method="createUserDao"/> <!--使用实例工厂创建对象--> <bean id="factroyCreateBean" class="cn.test.factory.FactroyCreateBean"/> <bean id="userDao2" factory-bean="factroyCreateBean" factory-method="createUserDaoSimple"/>Spring Bean 的生命周期概述、生命周期流程图Bean对象生命周期指的是Bean创建到销毁的这么一段时间。粗粒度生命周期:spring中单例对象的生命周期为:出生:IOC容器加载时出生存活:IOC容器运行时存活死亡:IOC容器销毁时死亡spring中多例对象的生命周期为:出生:使用对象时出生存活:一直存活死亡:由java垃圾回收器负细粒度生命周期:出生过程实例化bean对象【IOC】为对象属性赋值【DI】处理实现的Aware接口① 如果这个Bean已经实现了 BeanNameAware 接口,会调用它实现的 setBeanName(String beanId) 方法,此处传递的就是Spring配置文件中Bean的id值。② 如果这个Bean已经实现了 BeanFactoryAware 接口,会调用它实现的 setBeanFactory() 方法,传递的是Spring工厂自身。③ 如果这个Bean已经实现了 ApplicationContextAware 接口,会调用 setApplicationContext(ApplicationContext) 方法,传入Spring上下文。通过 BeanPostProcessor 接口的 postProcessBeforeInitialization 方法对bean对象进行预处理通过 InitializingBean 接口的 afterPropertiesSet 方法对bean对象进行处理通过指定 init-method 方法对bean对象进行处理通过 BeanPostProcessor 接口的 postProcessAfterInitialization 方法对bean对象进行后处理,这一步bean对象已经彻底创建成功了,可以做一些类似缓存的工作死亡过程如果Bean实现了 DisposableBean 接口,会调用其实现的 destroy() 方法。如果指定 destroy-method 方法,可以在bean对象销毁前自动执行。生命周期流程图测试案例:public class UserDaoImpl implements UserDao, BeanNameAware, BeanFactoryAware,ApplicationContextAware, InitializingBean, DisposableBean { public UserDaoImpl() { System.out.println("IOC"); private Integer id; public void setId(Integer id) { System.out.println("DI"); this.id = id; @Override public void setBeanName(String s) { System.out.println("BeanNameAware:"+s); @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("BeanFactoryAware"); @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("ApplicationContextAware"); @Override public void afterPropertiesSet() throws Exception { System.out.println("InitializingBean的afterPropertiesSet"); public void initMethod(){ System.out.println("init-method"); @Override public void save() { System.out.println("保存成功!"); @Override public void destroy() throws Exception { System.out.println("DisposableBean的destroy"); public void destroyMethod(){ System.out.println("destroy-method"); }public class MyBeanProcessor implements BeanPostProcessor { public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("BeanPostProcessor的before"); return bean; public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("BeanPostProcessor的after"); return bean; }<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl" init-method="initMethod" destroy method="destroyMethod"> <property name="id" value="1"/> </bean> <bean class="com.itheima.processor.MyBeanProcessor"/> </beans>// 测试类 public class UserDaoImplTest { @Test public void save() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); UserDao userDao = ac.getBean(UserDao.class); userDao.save(); ac.close(); }Spring Bean 的作用域参考:https://blog.csdn.net/weixin_38676276/article/details/90382350singleton:单例。在Spring IOC容器中仅存在一个共享的Bean实例。默认值当spring创建applicationContext容器的时候,spring会初始化所有的该作用域实例,加上lazy-init则可以避免预处理prototype:原型(多例)每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()相当于执行new XxxBean()创建后spring将不再对其管理(下面是在web项目下才用到的作用域)request:仅在当前HTTP request内有效每次HTTP请求都会创建一个新的Beanspring创建后会继续监听,当处理请求结束后销毁实例session:仅在当前HTTP Session内有效同一个HTTP Session共享一个Bean,不同Session使用不同Beanspring创建后会继续监听,当HTTP Session最终被废弃时,在该HTTP Session作用域内的bean也会被废弃掉global-session:全局的web域。类似于servlet中的application。一般用于Portlet应用环境定义 Bean 的配置信息(xml)id:唯一标志(获取的时候,调用容器的getBean("id"))class:实现类的全限定类名scope:对象作用域(singleton(默认) | prototype | request | session | global-session)init-method:对象创建成功之后,指定的初始化方法destroy-method:容器关闭对象销毁之前,执行的销毁方法只有在scope=singleton(单例)的时候,才有效<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl" scope="prototype" init-method="init" destroy-method="destroy"/>依赖注入(xml)依赖注入:Dependency Injection(DI)。 它是 spring 框架核心 IOC 的具体实现。在编写程序时, 通过控制反转,把对象的创建交给了 spring,然后在代码中需要对象时,进行依赖对象的注入。IOC 具有两个功能:依赖查找,依赖注入本质:向对象中的私有属性赋值构造方法set方法调用构造方法注入向对象中添加有参构造方法public class UserServiceImpl implements UserService { private String name; private Integer age; public UserServiceImpl(String name, Integer age) { this.name = name; this.age = age; @Override public void save() { System.out.println(name + age); }在spring的配置文件中,通过bean标签配置对象创建(需要添加构造方法参数) <!-- constructor-arg 设置对象的构造方法参数 (一个参数配置一个constructor-arg标签) name:构造参数名 type:构造参数类型 index:构造参数,参数索引位置(从0开始) 以上三个属性,用于定位构造方法参数位置(三选一即可) value: 对基本数据类型的参数赋值(8大数据类型 + String) ref: 对对象属性的参数赋值。即必须得是在配置文件中配置过的bean 以上两个属性,用于对构造参数赋值 --> <bean id="userService" class="cn.test.service.impl.UserServiceImpl"> <constructor-arg name="age" value="12"></constructor-arg> <constructor-arg name="name" value="王者荣耀"></constructor-arg> </bean>set 方法注入提供属性的set方法在spring配置文件中,通过bean结合property配置set方法调用<bean id="book" class="com.itheima.spring.Book"> <!-- name:找的是类中 set 方法后面的部分 ref:给属性赋值(其他bean类型) value:给属性赋值(8大基本数据类型 + string类型) --> <property name="name" value="程序员" /> <property name="price" value="1" /> <property name="publishDate" ref="date" /> </bean>注入复杂类型(集合)给类中的集合成员传值,它用的也是 set方法注入的方式,只不过变量的数据类型都是集合。 这里介绍注入数组、List、Set、Map、Properties。(1) 注入数组数据配置set方法public class UserServiceImpl implements UserService { * 注入数组(array数组,list集合,set集合) private String [] names; private List<String> lists; private Set<String> sets; public void setNames(String[] names) { this.names = names; public void setLists(List<String> lists) { this.lists = lists; public void setSets(Set<String> sets) { this.sets = sets; @Override public void save() { }spring配置文件<bean id="book" class="com.itheima.spring.Book"> <!-- List --> <property name="list"> <list> <value>1</value> <value>2</value> </list> </property> <!--Set--> <property name="set"> <set> <value>3</value> <value>4</value> </set> </property> <!--数组--> <property name="array"> <array> <value>5</value> <value>6</value> </array> </property> <!--Map--> <property name="map"> <map> <entry key="7" value="7-1" /> <entry key="8" value="8-1" /> </map> </property> <!--Properties--> <property name="properties"> <props> <prop key="9">9-1</prop> <prop key="10">10-1</prop> </props> </property> </bean>Spring 框架中的注解配置介绍、注解和 xml 配置对应关系注解和 xml 是Spring提供的两种配置形式,二者的功能是完全一样的,都是要降低程序间的耦合注解的好处是配置简单,xml的好处是修改配置不用改动源码,企业开发中两种方式灵活使用Spring中注解配置注解 + xml 配置(开启注解的功能支持)纯注解(Spring Boot + cloud))注意:在spring中使用注解开发,需要开启注解的功能支持(ioc注解,aop注解,事务注解)注解和xml配置对应关系:xml配置注解配置说明< bean id="" class="" >@Component @Controller @Service @Repository将类的实例化对象放入Spring容器管理< property name="" ref="">@Autowired @Qualifier @Resource从Spring容器中获取对象注入属性< property name="" value="">@Valuebean的简单属性注入< bean scope="">@Scope控制bean的作用范围< bean init-method="init" destroy method="destory" />@PostConstruct @PreDestroybean创建之后和销毁之前分别调用的方法开启包扫描(开启 IOC 注解支持)开启包扫描(即 开启对 IOC 注解的支持):扫描指定包下的所有 java 类中的 Spring 注解如果扫描到类上有 IOC 注解,就会把当前类交给 IOC 容器管理,当容器启动时,自动创建对象存入容器如果扫描到属性上有 DI 注解,则依据依赖注入的规则,给属性注入值方式1:xml配置方式 开启包扫描<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 开启包扫描 - base-package:包名(自动扫描此包以及此包下的所有子包) --> <context:component-scan base-package="cn.test"></context:component-scan> </beans>方式2:配置类注解方式 开启包扫描/** * 1、声明配置类:@Configuration * 2、开启包扫描:@ComponentScan @Configuration @ComponentScan(basePackages = "cn.test") public class SpringConfig { }IOC 注解(对象创建的注解)IOC 注解(对象创建的注解)说明:标注在想要被 IOC 容器管理的类上,表明创建对象交给容器管理当 Spring 容器启动时,根据包扫描配置自动的扫描到 IOC 注解,反射创建注解标注的对象,存入容器,委托容器管理默认存入容器的 id(唯一标识)为当前类名首字母小写。可以通过 value 属性自定义存入容器中对象的 id一共有四个:@Controller:一般标注在表现层(web层)的类上@Service: 一般标注在业务层(service层)的类上@Repository: 一般标注在持久层(dao层)的类上@Component:组件, 非三层模式范围的类上使用相当于xml中的如下配置<bean id="userDaoImpl" class="cn.test.dao.impl.UserDaoImpl"></bean>生命周期的相关注解@Scope : 在类上配置对象的作用域通过value属性指定作用范围(singleton|prototype),默认为 singleton(单例)@PostConstruct : 配置对象创建后的触发动作当对象创建完成之后,自动执行的方法。相当于 xml 配置文件中的 init-method@PreDestroy :配置对象销毁前的触发动作(仅在 Scope = singleton 时有效)容器关闭,对象销毁之前执行的方法。相当于 xml 配置文件中的destory-method示例:@Repository @Scope(value="singleton") public class UserDaoImpl implements UserDao { public UserDaoImpl() { System.out.println("创建UserDaoImpl"); @Override public void save() { System.out.println("调用dao保存数据"); //初始化方法:在对象创建完成之后执行 @PostConstruct public void init() { System.out.println("执行init方法"); //销毁方法:在容器关闭对象销毁之前执行 @PreDestroy public void destory() { System.out.println("执行destory方法"); }DI 注解(依赖注入的注解)DI 注解都相当于直接给属性赋值,而无需借助于 set 方法或构造方法@Autowired使用方式1:标注在属性上直接给属性赋值(通过 @Autowired 依赖注入,不需要配置 set 方法)默认是通过 by Type(根据类型,即接口类型)的形式从 IOC 容器中查找对象并给属性注入值如果 IOC 容器中存在多个与属性同类型的对象(一个接口有多个实现类),则会按照属性名(by Name)作为唯一标志从容器中查找对象并给属性注入值也可以和 @Qualifier 注解共同使用,指定唯一标志从容器中查找对象并给属性注入值value:指定 IOC 容器中对象唯一标志(id)注意:@Qualifier 只能结合 @Autowired 一起使用使用方式2:标注在方法上表示自动执行当前方法,如果方法有参数,会自动从IOC容器中寻找同类型的对象给参数传值也可以在参数上添加 @Qualifier("IOC容器中对象id") 注解按照名称寻找对象给参数传值@Autowired 使用注意事项只能在被 Spring 容器托管(标注了 @Controller 等 IOC 注解)的类中使用 @Autowired 注解自动注入与权限修饰符无关,即使是 private 修饰的字段也可以自动注入默认情况下,使用 @Autowired 注解进行自动注入的属性一定要被装配( Spring 容器托管)如果在容器中找不到该类型的bean 进行注入,就会报错如果允许不被装配,可以将 @Autowired 的 required 属性为 false@Autowired 是基于类型的注入,如果当前类型属性在容器中只有一个 Bean,那么属性名不限制,但一般建议遵循类名首字母小写的规则如果当前属性类型在容器中有个多个Bean,那么必须要通过属性名或者同时标注 @Qualifier 注解指定 Bean name示例:@Service public class UserServiceImpl implements UserService { @Autowired @Qualifier(value = "userDao2") // 从容器中获取指定唯一标志的对象 private UserDao userDao; @Override public void save() { userDao.save(); }@value配置在属性上。可用于简单数据类型的注入,相当于 < property name="" value="" > ,但通常不这么使用一般用于解析 properties 配置文件或注册中心的配置文件中的内容根据key值获取对应的 value,语法规则: @Value("${key}")使用步骤:properties 配置文件交给Spring容器管理通过 @Value,从容器中得到配置项,并注入示例:(1) 配置jdbc.properties文件jdbc.username=root jdbc.password=root jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql:///heima23(2) 配置文件交给Spring修改spring配置文件applicationContext.xml <!--将properties文件,交给spring管理--> <context:property-placeholder location="jdbc.properties"></context:property-placeholder>(3) 属性注入@Repository(value = "userDao") public class UserDaoImpl implements UserDao { @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; public void save() { System.out.println(username); System.out.println(password); System.out.println(driver); System.out.println(url); System.out.println("调用dao11111完成保存"); }@Reource(了解)jdk提供的依赖注入的注解,此注解在jdk9及以上版本已经移除只能放在属性上表示先按照属性名匹配 IOC 容器中对象id给属性注入值【by name】若没有成功,会继续根据当前属性的类型匹配IOC容器中同类型对象来注入值 【by type】若指定了 name 属性 @Resource(name = "对象id"),则只能按照对象 id 注入值示例:@Service public class UserServiceImpl implements UserService { @Resource(name = "userDao1") private UserDao userDao; @Override public void save() { userDao.save(); }Configuration 配置类(注解)配置类中的常用注解:@Configuration:标注在类上,声明该类为 Spring 配置类Spring 在启动的时候会自动扫描并加载所有配置类,配置 Spring 容器(应用上下文),将配置类中的 Bean 放入容器管理@Bean:标注在 Spring 配置类中的方法上,注册 bean 对象到 IOC 容器name 属性:给生成的bean指定唯一标志在 Spring 容器启动的时候,自动的扫描并执行所有配置了 @Bean 的方法,并将返回值存入Spring容器注意:被标注的方法,需要返回某个实例被标注的方法,可以配置依赖的属性参数,Spring 会自动从容器中获取到依赖的对象,自动调用方法@ComponentScan:开启包扫描(Spring IOC注解支持),默认扫描当前包及子包下所有类basePackage 属性:指定扫描的包路径。可以减少加载时间如果扫描到类上有 IOC 注解,就会把当前类交给 IOC 容器管理,当容器启动时,自动创建对象存入容器如果扫描到属性上有 DI 注解,则依据依赖注入的规则,给属性注入值@PropertySource:加载本地 properties 文件交给 Spring 容器管理value 属性:指定本地 properties 路径@Import:在一个配置类中导入其它配置类的内容value 属性:指定其他的配置类的 class 类路径Spring 支持多配置类(配置类的模块),若配置类臃肿,可以拆分配置类,然后在主配置类中引入子配置类(子配置类上不用配置注解)示例:import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; @Configuration // 声明配置类 @ComponentScan(basePackages = "cn.test") // 开启包扫描,并指定包扫描路径 @PropertySource(value="jdbc.properties") // 通过注解将此文件交给spring容器管理 @Import(value=DataSourceConfig.class) // 引入其他的配置类 public class SpringConfig { @Bean public QueryRunner getQueryRunner(DataSource dataSource) { QueryRunner qr = new QueryRunner(dataSource); return qr; }import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; // 子配置类 public class DataSourceConfig { @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Value("${jdbc.url}") private String url; @Value("${jdbc.driver}") private String driver; @Bean public DataSource getDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setUrl(url); dataSource.setDriverClassName(driver); return dataSource; }# properties文件 jdbc.username=root jdbc.password=root jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql:///heima23
Mybatis 映射文件深入动态sqlif 标签if 标签:判断语句(单条件分支)。必须结合 test 属性联合使用。常用场景:在 WHERE 条件中使用 if 标签。根据条件判断动态拼接查询条件。在 UPDATE 更新列中使用 if 标签。只更新有变化的字段, 空值不更新。在 INSERT 动态插入中使用 if 标签。只有非空属性才插入。在 SELECT 动态查询字段使用 if 标签。根据条件动态确定查询字段。mapper接口方法:public List<User> findByIdAndUsernameIf(User user);xml文件:<select id="findByIdAndUsernameIf" parameterType="user" resultType="user"> select * from user <!-- where 1=1 是多条件拼接时的小技巧,后面的条件查询就可以都用and了 --> <!-- where 1 = 1 --> <!-- where 标签:相当于 where 1=1 功能, 当if条件都不满足时,where 将不会拼接在sql语句中 当if有条件满足时,where 将会拼接在sql语句中,且去掉第一个成立的 if 标签下的 and | or 等 --> <where> <if test="id != null"> and id = #{id} </if> <if test="username != null"> and username like concat('%',#{username}),'%') </if> </where> </select>choose 标签choose when otherwise 标签可以帮我们实现 if else 的逻辑(或 java 中的 switch 语句)。一个 choose 标签至少有一个 when,最多一个otherwise。mapper接口方法:public List<User> findByIdAndUsernameChoose(User user);xml文件:<!-- choose标签 相当于 switch语句 when标签 相当于 case+break语句 otherwise 相当于 default语句 --> <select id="findByIdAndUsernameChoose" parameterType="user" resultType="user"> select * from user <where> <choose> <when test="id != null"> and id = #{id} </when> <when test="username != null"> and username like concat(concat('%',#{username}),'%') </when> <otherwise> and 1 = 1 </otherwise> </choose> </where> </select>set 标签可以去掉 set 标签中的最后一个条件的 逗号mapper接口方法:public void updateIf(User user);xml方法:<update id="updateIf" parameterType="user"> update user <set> <if test="username != null"> username=#{username}, </if> <if test="birthday != null"> birthday=#{birthday}, </if> <if test="sex != null"> sex = #{sex}, </if> <if test="address != null"> address = #{address}, </if> </set> where id = #{id} </update>foreach 标签做数据的循环遍历* 例如: select * from user where id in (1,2,3) 在这样的语句中,传入的参数部分必须依靠 foreach遍历才能实现。foreach 标签中的属性:collection:必填, 被遍历的集合(list)/数组(array)/Map的名称item:变量名。即从迭代的对象中取出的每一个值index:索引的属性名。当迭代的对象为 Map 时, 该值为 Map 中的 Key.open:遍历前拼接的字符串close:遍历后拼接的字符串separator:分隔符where 中使用 foreachmapper接口方法:// foreach标签 遍历 public List<User> findByList(List<Integer> ids); public List<User> findByArray(Integer[] ids); // 传递引用数据类型 list属性或者array属性 public List<User> findByPojo(QueryVo queryVo);xml文件:<!-- 传递 普通类型list集合 collection="" 取值:collection、list --> <select id="findByList" parameterType="list" resultType="user"> select * from user where <foreach collection="list" open="id in (" close=")" item="id" separator=","> #{id} </foreach> </select> <!-- 传递 普通类型array数组 collection="" 取值:array --> <select id="findByArray" parameterType="int[]" resultType="user"> select * from user where <foreach collection="array" open="id in (" close=")" item="id" separator=","> #{id} </foreach> </select> <!-- 传递 引用数据类型list集合属性 collection="" 取值 集合或数组的 属性名 --> <select id="findByPojo" parameterType="QueryVo" resultType="user"> select * from user where <foreach collection="ids" open="id in (" close=")" item="id" separator=","> #{id} </foreach> </select>foreach 实现批量插入mapper接口方法: // 批量插入学生 int insertList(List<Student> students);xml文件: <insert id="insertList"> insert into student(name, phone, email, sex, locked) values <foreach collection="list" item="student" separator=","> #{student.name}, #{student.phone},#{student.email}, #{student.sex},#{student.locked} </foreach> </insert>trim 标签trim 标签的属性prefix: 当 trim 元素包含有内容时, 增加 prefix 所指定的前缀prefixOverrides: 当 trim 元素包含有内容时, 去除 prefixOverrides 指定的前缀suffix: 当 trim 元素包含有内容时, 增加 suffix 所指定的后缀suffixOverrides:当 trim 元素包含有内容时, 去除 suffixOverrides 指定的后缀、set 和 where 其实都是 trim 标签的一种类型, 该两种功能都可以使用 trim 标签进行实现。trim 标签来实现 where 标签当 trim 标签中含有内容时,添加 where,且第一个为 and 或 or 时,会将其去掉;而如果没有内容,则不添加 where<trim prefix="where" prefixOverrides="AND |OR"> </trim>trim 标签来实现 set 标签当 trim 标签中含有内容时,添加 set,且最后的内容为 , 时,会将其去掉;而没有内容,不添加 set<trim prefix="SET" suffixOverrides=","> </trim>bind 标签bind 元素允许在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。如在 selectByStudentSelective 方法中,有如下<if test="name != null and name !=''"> and name like concat('%', #{name}, '%') </if>在 MySQL 中,concat 函数支持多参数,但在 Oracle 中只支持两个参数。通过使用 bind 来让该 SQL 达到支持两个数据库的作用<if test="name != null and name !=''"> <bind name="nameLike" value="'%'+name+'%'"/> and name like #{nameLike} </if>include 标签(引入sql片段)<!-- 可以将公共部分进行抽取 --> <sql id="userSelect"> select * from user </sql> <!-- 引入sql片段 --> <select id="findByArray" parameterType="int[]" resultType="user"> <include refid="userSelect"></include> where <foreach collection="array" open="id in (" close=")" item="id" separator=","> #{id} </foreach> </select> <select id="findByPojo" parameterType="QueryVo" resultType="user"> <include refid="userSelect"></include> where <foreach collection="ids" open="id in (" close=")" item="id" separator=","> #{id} </foreach> </select>生成并返回主键往数据库中插入记录时,生成主键并将主键返回到原实体对象中。方式1:<selectKey>【复杂,但通用】既支持主键自增类型,也支持 UUID 类型的主键的生成与返回方式2:useGeneratedKeys【简单,但不通用】注意:这种方式仅支持主键自增类型的数据库,如 MySQL 和 sqlServer,oracle不支持xml文件:<!-- 方式1:<selectKey> keyColumn 数据库表的主键字段 keyProperty 指定实体的主键属性名。selectKey语句结果应该被设置的目标属性 resultType 指定实体主键的类型。 可以不写,Mybatis通常可以推算出来。Mybatis允许任何简单类型作为主键的类型,包括字符串。 order="AFTER" selectKey的sql语句是在insert执行之前(执行),还是之后(执行) AFTER 之后执行。适合返回自增id的情况(主键是自增列,插入之后才能获知)。SELECT LAST_INSERT_ID() FROM DUAL BEFORE 之前执行。适合返回UUID的情况(主键不是自增列,需要预先生成)。SELECT UUID() FROM DUAL --> <insert id="save2" parameterType="user"> <selectKey keyColumn="id" keyProperty="id" resultType="int" order="AFTER"> SELECT LAST_INSERT_ID() FROM DUAL </selectKey> insert into user(username, birthday, sex, address) values(#{username}, #{birthday}, #{sex}, #{address}) </insert> <!-- 方式2:useGeneratedKeys useGeneratedKeys="true" 开启返回主键功能 keyColumn 数据库表的主键字段 keyProperty 指定实体的主键属性名 --> <insert id="save1" parameterType="user" useGeneratedKeys="true" keyColumn="id" keyProperty="id"> insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address}) </insert>多表查询表关系及xml标签配置介绍一对一 订单与用户。查询站在订单角度:一个订单只能属于一个用户 一对多 用户和订单。查询站在用户角度:一个用户可以拥有多个订单 多对一 订单和用户。多个订单,可以被一个用户拥有 多对多 用户和角色。 - 站在用户角度:一个用户可以拥有多个角色 - 站在角色角度:一个角色可以被多个用户拥有 mybatis:一个表是一个实体的映射(user orders) 实体: 实体属性、集合属性 1 v 1: 实体属性 1 v N: 1的一方有多的一方的集合属性 多的一方有1的一方的实体对象 * 一对一配置:使用<resultMap>+<association>做配置 association: property:关联的实体属性名 javaType:关联的实体类型(使用别名) * 一对多配置:使用<resultMap>+<collection>做配置 collection: property:关联的集合属性名 ofType:关联的集合泛型类型(使用别名) * 多对多配置:使用<resultMap>+<collection>做配置 collection: property:关联的集合属性名 ofType:关联的集合泛型类型(别名) * 多对多的配置跟一对多很相似,区别在于SQL语句的编写。多表查询和嵌套查询的实体类Orders(订单)实体类@Data public class Orders { private Integer id; private Date ordertime; private Double money; // 订单所属的用户 private User user; }User(用户)实体类@Data public class User { private Integer id; private String username; private Date birthday; private String sex; private String address; // 一个用户有多个角色 private List<Role> roleList; // 一个用户有多个订单 private List<Orders> orders; }Role(角色)实体类@Data public class Role { private Integer id; private String roleName; private String roleDesc; }一对一(多对一)一对一查询的实例:需求:查询一个订单,与此同时查询出该订单所属的用户(1 v 1)OrdersMapper.xml映射文件(一对一映射关系)<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.test.dao.OrdersDao"> <!--手动封装--> <resultMap id="findOrderswithUserResultMap" type="orders"> <!--订单orders--> <id column="iid" property="id"></id> <result column="ordertime" property="ordertime"></result> <result column="money" property="money"></result> <!--订单中的用户User(一对一的封装) association标签 property:订单实体类中的用户属性名 javaType:要封装数据的属性属于什么类型。不写则自动映射 --> <association property="user" javaType="cn.test.domain.User"> <id column="uid" property="id"></id> <result column="username" property="username"></result> <result column="birthday" property="birthday"></result> <result column="sex" property="sex"></result> <result column="address" property="address"></result> </association> </resultMap> <!-- 查询当前订单以及所属用户的信息 --> <select id="findOrderswithUser" parameterType="int" resultMap="findOrderswithUserResultMap"> select *, o.id iid from orders o left join user u on o.uid=u.id where o.id=#{id} </select> </mapper>一对多一对多查询的实例:需求:查询一个用户,与此同时查询出该用户具有的所有订单(1 v N) UserMapper.xml映射文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.test.dao.UserDao"> <!--手动封装--> <resultMap id="findUserwithOrdersResultMap" type="user"> <!--封装user数据--> <id column="id" property="id"></id> <result column="username" property="username"></result> <result column="birthday" property="birthday"></result> <result column="sex" property="sex"></result> <result column="address" property="address"></result> <!--封装user的list集合属性(一对多) collection标签 property:用户的订单集合属性名 ofType:要封装的list集合中的泛型类型 --> <collection property="list" ofType="orders"> <id column="iid" property="id"></id> <result column="ordertime" property="ordertime"></result> <result column="money" property="money"></result> </collection> </resultMap> <!-- 查询一个用户及订单的信息 --> <select id="findUserwithOrders" parameterType="int" resultMap="findUserwithOrdersResultMap"> select *, o.id as iid from user u left join orders o on u.id=o.uid where u.id=#{id} </select> </mapper>多对多(二个一对多)多对多的查询的实例:需求: 查询某个用户的所有角色或者是某个角色的所有用户站在用户角度,一个用户可以拥有多个角色站在角色角度,一个角色可以被多个用户拥有所以 mybatis框架多对多的实现,跟一对多的步骤是一样的,区别点在于sql语句UserMapper.xml映射文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.test.dao.UserDao"> <resultMap id="findUserwithRoleResult" type="user"> <!--用户数据--> <id column="id" property="id"></id> <result column="username" property="username"></result> <result column="birthday" property="birthday"></result> <result column="sex" property="sex"></result> <result column="address" property="address"></result> <!--用户的角色集合(一对多) collection标签 property:用户的角色集合属性名 oftype:要封装的类型 --> <collection property="roleList" ofType="Role"> <id column="rid" property="id"></id> <result column="role_name" property="roleName"></result> <result column="role_desc" property="roleDesc"></result> </collection> </resultMap> <!-- 查询某个用户及角色信息 --> <select id="findUserwithRole" parameterType="int" resultMap="findUserwithRoleResult"> select * from user u left join user_role ur on u.id=ur.uid left join role r on r.id=ur.rid where u.id=#{id} </select> </mapper>嵌套查询(了解)Mybatis嵌套查询 介绍:将多表查询的复杂sql语句拆分多个单表查询,再由mybatis框架进行嵌套组合优点:减少sql复杂性缺点:需要编写多次sql和多个配置,代码麻烦。故一般不推荐使用简单示例:* 需求:查询一个订单,与此同时查询出该订单所属的用户 1v1 # 关联查询 select * from orders o left join user u on o.uid=u.id where o.id=1; # 嵌套查询 将一条sql语句能完成的事情拆分成多条单表查询去完成 #1 查询订单号为1的订单 #2 在根据查询的订单的uid去查用户表 #3 mybatis进行组装(映射文件中组装)因最终要返回的是订单 所以去订单映射文件中组装 一对一(多对一)需求:查询一个订单,与此同时查询出该订单所属的用户xml映射文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.test.mapper.OrdersMapper"> <resultMap id="ordersResultMap" type="orders"> <id column="id" property="id"></id> <result column="ordertime" property="ordertime"></result> <result column="address" property="address"></result> <!--mybatis框架嵌套查询(一对一映射) association标签 property:要封装的对象属性名 select:数据来源的方法名 column:方法需要的参数(uid) --> <association property="user" select="cn.test.mapper.UserMapper.findUserById" column="uid"> </association> </resultMap> <!-- 查询当前订单 --> <select id="findOrderById" parameterType="int" resultMap="ordersResultMap"> select * from orders where id=#{id} </select> </mapper><?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.test.mapper.UserMapper"> <!-- 查询当前用户 --> <select id="findUserByUId" parameterType="int" resultType="user"> select * from user where id=#{id} </select> </mapper>一对多需求:查询一个用户,与此同时查询出该用户具有的订单xml映射文件:<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.test.mapper.UserMapper"> <resultMap id="userResultMap" type="user"> <id column="id" property="id"></id> <result column="username" property="username"></result> <result column="birthday" property="birthday"></result> <result column="sex" property="sex"></result> <result column="address" property="address"></result> <!-- 嵌套查询(一对多的映射) collection标签 property;要封装的对象集合属性 ofType:泛型类型 select:数据的来源方法 column:方法需要的参数数据(用户的id) --> <collection property="orders" ofType="Orders" select="cn.test.mapper.OrdersMapper.findBylist" column="id"> </collection> <!-- 查询当前用户 --> </resultMap> <select id="findUserById" parameterType="int" resultMap="userResultMap"> select * from user where id=#{id} </select> </mapper><?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.test.mapper.OrdersMapper"> <!--查询多个订单--> <select id="findBylist" parameterType="int" resultType="Orders"> select * from orders where uid=#{id} </select> </mapper>多对多(由二个一对多组成)需求:查询用户同时查询出该用户的所有角色xml映射文件:<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.test.mapper.UserMapper"> <resultMap id="userResultMap" type="user"> <id column="id" property="id"></id> <result column="username" property="username"></result> <result column="birthday" property="birthday"></result> <result column="sex" property="sex"></result> <result column="address" property="address"></result> <!-- 嵌套查询(一对多的映射) collection标签 property;要封装的对象集合属性 ofType:泛型类型 select:数据的来源方法 column:方法需要的参数数据(用户的id) --> <collection property="roleList" ofType="role" select="cn.test.mapper.RoleMapper.findRoles" column="id"> </collection> </resultMap> <!--查询当前用户--> <select id="findUserById" parameterType="int" resultMap="userResultMap"> select * from user where id=#{id} </select> </mapper><?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.test.mapper.RoleMapper"> <resultMap id="RoleResultMap" type="role"> <id column="id" property="id"></id> <result column="role_name" property="roleName"></result> <result column="role_desc" property="roleDesc"></result> </resultMap> <!--查询当前用户的角色--> <select id="findRoles" resultMap="RoleResultMap" parameterType="int"> SELECT * FROM user_role ur INNER JOIN role r ON ur.rid=r.id WHERE ur.uid=#{id} </select> </mapper>调用存储过程参考:MyBatis 调用存储过程(详解)在mapper文件中可以使用statementType标记使用什么的对象操作SQL语句。statementType:标记操作SQL的对象取值说明:STATEMENT:直接操作sql,不进行预编译,获取数据。$—StatementPREPARED:预处理,参数,进行预编译,获取数据。#—PreparedStatement。默认CALLABLE:执行存储过程。CallableStatement示例:创建insert_user存储过程(MySql)CREATE PROCEDURE insert_user(OUT u_id INTEGER,IN u_name VARCHAR(20),IN u_sex VARCHAR(20),IN u_age INTEGER) BEGIN INSERT INTO t_user (name,sex,age) VALUES (u_name,u_sex,u_age); SET u_id=LAST_INSERT_ID(); END在UserMapper.xml中调用insert_user存储过程<!-- 添加用户 --> <insert id="addUser" parameterType="com.po.User" statementType="CALLABLE"> {call insert_user(#{id,mode=OUT,jdbcType=INTEGER},#{name,mode=IN},#{sex,mode=IN},#{age,mode=IN})} </insert>创建deleteUser存储过程(MySql)CREATE PROCEDURE deleteUser(IN u_id INTEGER) BEGIN DELETE FROM t_user WHERE id=u_id; END在UserMapper.xml中调用deleteUser存储过程<!-- 删除用户 --> <delete id="deleteUser" parameterType="Integer" statementType="CALLABLE"> {call deleteUser(#{id,mode=IN})} </delete>创建updateUser存储过程(MySql)CREATE PROCEDURE updateUser(IN u_id INTEGER,IN u_name VARCHAR(20),IN u_sex VARCHAR(20),IN u_age INTEGER) BEGIN UPDATE t_user SET name=u_name,sex=u_sex,age=u_age WHERE id=u_id; END在UserMapper.xml中调用updateUser存储过程<!-- 更新用户 --> <update id="updateUser" parameterType="user" statementType="CALLABLE"> {call updateUser(#{id,mode=IN},#{name,mode=IN},#{sex,mode=IN},#{age,mode=IN})} </update>创建getUserById存储过程(MySql)CREATE PROCEDURE getUserById(IN u_id INTEGER) BEGIN SELECT id,name,sex,age FROM t_user WHERE id=u_id; END在UserMapper.xml中调用getUserById存储过程<!-- 根据id查询用户 --> <select id="getUserById" parameterType="Integer" resultType="user" statementType="CALLABLE"> {call getUserById(#{id,mode=IN})} </select>拓展了解Mybatis 核心文件概述1)environments标签数据库环境配置其中,事务管理器(transactionManager)类型有两种:JDBC:这个配置就是直接使用了JDBC 的提交和回滚设置,由mybatis自己手动控制事务MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期。例如:mybatis与spring整合后,事务交给spring容器管理。其中,数据源(dataSource)常用类型有二种:UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。使用的是mybatis自带的2)properties标签第三方属性配置实际开发中,习惯将数据源的配置信息单独抽取成一个properties文件 <properties resource="jdbc.properties"></properties> <environments default="mysql"> <!--mysql数据库环境--> <environment id="mysql"> <!-- 使用JDBC类型事务管理器 --> <transactionManager type="JDBC"></transactionManager> <!-- 数据源(连接池)配置 POOLED--> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"></property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </dataSource> </environment> </environments>3)typeAliases标签实体类型别名配置 <typeAliases> <!--指定一个实体类和对应的别名--> <!--<typeAlias type="com.test.domain.User" alias="user"></typeAlias>--> <!--指定当前包下所有的实体设置别名 默认: 别名(类名) --> <package name="com.test.domain"></package> </typeAliases>4)mappers标签映射关系配置(加载映射配置文件) <!-- 加载指定的src目录下的映射文件 --> <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>核心配置文件标签顺序Mybatis 的 API 概述1)Resources专门用于加载mybatis核心配置文件,返回的 io流InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");2)SqlSessionFactoryBuilder专门用于生产SqlSessionFactory工厂对象SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);3)SqlSessionFactory一个项目只有一个工厂对象,生产sqlSession对象// 需要手动提交事务,DML语句才会持久化到数据库中 SqlSession openSession(); // 【推荐】 // 设置是否开启自动提交,如果设置为true,开启自动提交事务 SqlSession openSession(boolean autoCommit); true:每一条DML类型语句都会自动提交事务 false(默认值):需要手动提交事务4)SqlSession是mybatis核心对象,可以对数据库进行CRUD基本操作// 方法 <T> T selectOne(String statement, Object parameter); <E> List<E> selectList(String statement, Object parameter); int insert(String statement, Object parameter); int update(String statement, Object parameter); int delete(String statement, Object parameter); // 事务 void commit(); void rollback();MyBatis 延迟加载(懒加载)Mybatis的延迟加载是针对嵌套查询而言的,是指在进行查询的时候先只查询最外层的SQL,对于内层SQL将在需要使用的时候才查询出来。使用延迟加载的场景:一对多、多对多不推荐使用延迟加载的场景:一对一(使用 立即加载)特别注意:延迟加载是基于 嵌套查询 组合使用1)局部延迟加载* 需要手动在每一个select相关标签中单独配置 <association>、<collection> fetchType="lazy" 懒加载 fetchType="eager" 立即加载2)全局延迟加载lazyLoadingEnabled:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载,特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。默认值为false。 <!-- 在核心配置文件SqlMapConfig.xml,设置全局参数 --> <settings> <!--开启懒加载--> <setting name="lazyLoadingEnabled" value="true"/> </settings>若是pringboot集成,在yaml配置文件中配置开启延迟加载。mybatis: configuration: lazy-loading-enabled: true #开启延时加载开关注意:如果全局配置了延迟加载,那么一对一也会起作用可以为一对一设置局部的立即加载,因为局部优先级高于全局延迟加载<association property="user" javaType="User" column="uid" select="com.itheima.mapper.UserMapper.findById" fetchType="eager"> </association>3)指定触发延迟加载的方法配置lazyLoadTriggerMethods 默认情况下仅仅支持自动将 equals,clone,hashCode,toString 这几个方法定义为延迟加载的加载触发方法。* 在SqlMapConfig.xml核心配置文件,设置全局参数 <settings> <!-- 如果将 Person 的 doLazyLoadingNow()方法加入这个列表中, 则调用 doLazyLoadingNow()方法将会导致 Person 上的所有延迟加载属性的关联对象被执行加载。 --> <setting name="lazyLoadTriggerMethods" value="doLazyLoadingNow,equals,clone,hashCode,toString"/> </settings>若是springboot集成,在yaml配置文件中配置mybatis: configuration: lazy-loading-enabled: true # 开启延时加载开关 aggressive-lazy-loading: false # 将积极加载改为消极加载(即按需加载),默认值就是false # 指定触发延迟加载的方法 lazy-load-trigger-methods: "doLazyLoadingNow,equals,clone,hashCode,toString"MyBatis 缓存参考:https://blog.csdn.net/jinhaijing/article/details/84230810缓存是服务器内存的一块区域。经常访问但又不会时时发生变化的数据适合使用缓存。mybatis也支持缓存:提高查询速度,减轻数据库访问压力。一级缓存(本地缓存)MyBatis自带一级缓存且不可卸载当执行查询以后,查询的结果会同时缓存到SqlSession为我们提供的一块区域中,该区域的结构是一个Map,当再次查询同样的数据时,mybatis会先去sqlsession中查询缓存,有的话直接拿出来用。当SqlSession对象消失时,mybatis的一级缓存也就消失了,同时一级缓存是SqlSession范围的缓存,当调用SqlSession的修改、添加、删除、commit(),close等方法时,就会清空一级缓存。特点:随着sqlSession的创建而存在,随着SqlSession的销毁而销毁简称:sqlSession级别的缓存一级缓存失效的四种情况:sqlSession不同(会话不同)sqlSession相同,查询缓存中没有的数据sqlSession相同,但两次查询之间执行了增删改操作说明:为了防止增删改对当前数据的影响,即使查的同一个对象,也会重新查数据库原因:每个增删改标签都有默认刷新缓存配置:flushCache="true"(刷新一级和二级缓存)sqlSession相同,但手动清楚了一级缓存(缓存清空)手动清空一级缓存:openSession.clearCache();没有使用到当前一级缓存的情况,就会再次向数据库发出查询二级缓存(全局缓存)基于 mapper.xml 中 namespace 命名空间级别的缓存,即同一个 namespace 对应一个二级缓存。两个mapper.xml的namespace如果相同,则这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。也称为SqlSessionFactory级别的缓存,由同一个SqlSessionFactory对象创建的多个SqlSession共享其缓存,但是其中缓存的是数据而不是对象,所以从二级缓存再次查询出得结果的对象与第一次存入的对象是不一样的。注意:不是程序自带的,需要配置。仅做了解,一般不推荐使用(一般使用Redis缓存)。配置流程:在核心配置文件 SqlMapConfig.xml 中开启二级缓存(可以省略)<settings> <!--开启对二级缓存的支持 默认是支持的可以不用配置--> <setting name="cacheEnabled" value="true"/> </settings>若是springboot集成,在yaml配置文件中配置开启二级缓存(可以省略)mybatis: configuration: cache-enabled: true #打开全局缓存开关(二级环境),默认值就是true在mapper配置文件中开启使用二级缓存<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.atguigu.mybatis.dao.EmployeeMapper"> <!-- 声明使用二级缓存 --> <cache><cache/> <!-- <cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache> --> <select id="getEmpByLastNameLikeReturnMap" resultType="com.atguigu.mybatis.bean.Employee"> select * from tbl_employee where last_name like #{lastName} </select> </mapper> <cache>中可以配置一些参数:eviction:缓存的回收策略:LRU – 最近最少使用的:移除最长时间不被使用的对象。默认FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。flushInterval:缓存刷新间隔。缓存多长时间清空一次,默认不清空,可以设置一个毫秒值readOnly:是否只读:true:只读;mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。mybatis为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快false:非只读:mybatis觉得获取的数据可能会被修改。mybatis会利用序列化&反序列的技术克隆一份新的数据给你。安全,速度慢size:缓存存放多少元素type="":指定自定义缓存的全类名实体类对象需要实现序列化接口public class User implements Serializable操作的过程中需要提交之后才会存入到二级缓存查询数据提交到二级缓存中:sqlsession.commit | sqlsession.close注意:如果 openSession.close(); 在第二次查询之后才关闭,则第二次查询会从一级缓存中查,如果不是一个session,则查询不到数据,然后就会发送sql去查数据库;@Test public void testSecondLevelCache() throws IOException{ SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); SqlSession openSession = sqlSessionFactory.openSession(); SqlSession openSession2 = sqlSessionFactory.openSession(); EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class); EmployeeMapper mapper2 = openSession2.getMapper(EmployeeMapper.class); Employee emp01 = mapper.getEmpById(1); System.out.println(emp01); Employee emp02 = mapper2.getEmpById(1); System.out.println(emp02); openSession.close(); openSession2.close(); }工作机制:一个会话,查询一条数据,这个数据会默认先被放在当前会话的一级缓存中;当会话关闭或者提交后,一级缓存中的数据会被保存到二级缓存中;然后当有新的会话查询数据,若是同一个 namespace 就可以获取二级缓存中的内容。sqlSession ==> EmployeeMapper ==> Employee不同namespace查出的数据会放在自己对应的缓存中(map)效果:数据会从二级缓存中获取缓存有关的设置/属性mybatis全局配置文件中配置全局缓存开启和清空1)控制二级缓存的开启和关闭 <setting name="cacheEnabled" value="true"/> <!-- cacheEnabled=true:开启缓存;false:关闭缓存(二级缓存关闭)(一级缓存一直可用的) -->2)控制一级缓存的开启和关闭 <setting name="localCacheScope" value="SESSION"/> <!-- localCacheScope:本地缓存作用域(一级缓存SESSION); 当前会话的所有数据保存在会话缓存中;STATEMENT:可以禁用一级缓存 -->注意:一级缓存关闭后,二级缓存自然也无法使用;mapper.xml 中也可以配置一级和二级缓存开启和使用1)每个select标签都默认配置了useCache="true",表示会将本条语句的结果进行二级缓存。 如果useCache= false:则表示不使用缓存(一级缓存依然使用,二级缓存不使用)2)每个 增删改 标签都默认配置了 flushCache="true",表示增删改执行完成后就会刷新一级二级缓存。 注意:查询标签 <select> 默认 flushCache="false":如果 flushCache=true; 则每次查询之后都会清空缓存;一级和二级缓存都无法使用。多数据库支持如果配置了 DatabaseIdProvider,可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句。示例:<insert id="insert"> <selectKey keyProperty="id" resultType="int" order="BEFORE"> <if test="_databaseId == 'oracle'"> select seq_users.nextval from dual </if> <if test="_databaseId == 'db2'"> select nextval for seq_users from sysibm.sysdummy1" </if> </selectKey> insert into users values (#{id}, #{name}) </insert>DatabaseIdProvider 配置 @Bean public DatabaseIdProvider databaseIdProvider() { DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider(); Properties p = new Properties(); p.setProperty("Oracle", "oracle"); p.setProperty("TiDB", "tidb"); p.setProperty("PostgreSQL", "postgresql"); p.setProperty("DB2", "db2"); p.setProperty("SQL Server", "sqlserver"); databaseIdProvider.setProperties(p); return databaseIdProvider; }动态 SQL 中的插入脚本语言MyBatis 从 3.2 版本开始支持插入脚本语言,这允许插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。可以通过实现以下接口来插入一种语言:public interface LanguageDriver { ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql); SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType); SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType); }实现自定义语言驱动后,就可以在 mybatis-config.xml 文件中将它设置为默认语言:<typeAliases> <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/> </typeAliases> <settings> <setting name="defaultScriptingLanguage" value="myLanguage"/> </settings>或者,也可以使用 lang 属性为特定的语句指定语言:<select id="selectBlog" lang="myLanguage"> SELECT * FROM BLOG </select>或者,在 mapper 接口上添加 @Lang 注解:public interface Mapper { @Lang(MyLanguageDriver.class) @Select("SELECT * FROM BLOG") List<Blog> selectBlog(); }MyBatis 注解开发单表注解* @Insert:实现新增,代替了<insert></insert> * @Update:实现更新,代替了<update></update> * @Delete:实现删除,代替了<delete></delete> * @Select:实现查询,代替了<select></select> * @Result:实现结果集封装,代替了<result></result> * @Results:可以与@Result 一起使用,封装多个结果集,代替了<resultMap></resultMap>注解对用户表实现增删改查操作import cn.test.doman.User; import org.apache.ibatis.annotations.*; import java.util.List; // 注解的单表crud操作 @Mapper public interface UserMapper { //1 根据id做查询 @Select("select * from user where id =#{id}") public User findById(int id); //2 全查 @Select("SELECT id AS iid,username AS name,birthday AS bir,sex AS se,address AS addr FROM USER") @Results({ /*column:字段 * property:对象属性 * id:默认是false 代表当前不是主键 @Result(column = "iid",property = "id",id=true), @Result(column = "name",property = "username"), @Result(column = "bir",property = "birthday"), @Result(column = "se",property = "sex"), @Result(column = "addr",property = "address") public List<User> findAll(); //3 新增 @Insert("insert into user(username,birthday,sex,address) " + "values(#{username},#{birthday},#{sex},#{address})") public void save(User user); //4 修改 @Update("update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} " + "where id=#{id}") public void update(User user); //5 删除 @Delete("delete from user where id=#{id}") public void delete(int id); }多表注解注解开发的多表查询是嵌套查询方式@Results 代替的是标签<resultMap>。该注解中可以使用单个@Result注解,也可以使用@Result集合。 使用格式:@Results({@Result(), @Result()})或@Results(@Result()) @Result 代替<id>标签和<result>标签 @Result中属性介绍: column:数据库的列名 property:需要装配的属性名 one:需要使用@One注解(@Result(one=@One ())) many:需要使用@Many注解(@Result(many=@Many ())) @One(一对一) 代替了<assocation>标签,是多表查询的关键,在注解中用来指定子查询返回单一对象。 @One注解属性介绍: select:指定用来多表查询的 sqlmapper 使用格式:@Result(column=" ", property=" ", one=@One(select="")) @Many(一对多) 代替了<collection>标签,是多表查询的关键,在注解中用来指定子查询返回对象集合。 使用格式:@Result(column=" ", property=" ", many=@Many(select=""))1)一对一import org.apache.ibatis.annotations.One; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; @Mapper public interface OrdersMapper { // 查询订单 @Select("select * from orders where id=#{id}") @Results({ @Result(column = "id",property = "id",id = true), @Result(column = "ordertime",property = "ordertime"), @Result(column = "money",property = "money"), /* 组装用户 * 一对一 one=@one * select:代表要封装的数据方法来源 * column:方法需要的参数 * property:要封装的对象属性名 * javaType:对象属性名的字节码(.class)类型 @Result(one=@One(select = "cn.test.mapper.UserMapper.findById") ,property = "user",javaType = User.class, column = "uid") public Orders findOrders(int id); }@Mapper public interface UserMapper { //1 根据id做查询 @Select("select * from user where id =#{id}") public User findById(int id); }2)一对多@Mapper public interface UserMapper { //1 根据id做查询 @Select("select * from user where id =#{id}") @Results({ @Result(column = "id",property = "id",id=true), @Result(column = "username",property = "username"), @Result(column = "birthday",property = "birthday"), @Result(column = "sex",property = "sex"), @Result(column = "address",property = "address"), /*组装订单orders * 一对多 * many=@many * select:代表要封装的数据方法来源 * column:方法需要的参数 * property:要封装的对象属性名 * javaType:对象属性名的字节码(.class)类型 @Result(many = @Many(select = "cn.test.mapper.OrdersMapper.findOrdersList"), property = "ordersList",javaType =List.class,column = "id") public User findById(int id); } @Mapper public interface OrdersMapper { /*根据用户id获取订单集合*/ @Select("select * from orders where uid=#{用户的id}") public List<Orders> findOrdersList(int id); }3)多对多@Mapper public interface UserMapper { //1 根据id做查询 @Select("select * from user where id =#{id}") @Results({ @Result(column = "id",property = "id",id=true), @Result(column = "username",property = "username"), @Result(column = "birthday",property = "birthday"), @Result(column = "sex",property = "sex"), @Result(column = "address",property = "address"), // 组装订单orders @Result(many = @Many(select = "cn.test.mapper.OrdersMapper.findOrdersList" , fetchType = FetchType.LAZY) , property = "ordersList",javaType =List.class,column = "id"), // 组装角色 @Result(many = @Many(select = "cn.test.mapper.RoleMapper.findRoles") ,property = "roleList",javaType = List.class,column = "id") public User findById(int id); } @Mapper public interface RoleMapper { /*查询指定用户的角色*/ @Select("SELECT * FROM user_role ur INNER JOIN role r ON ur.rid=r.id WHERE ur.uid=#{id}") @Results({ @Result(column = "id",property = "id",id=true), @Result(column = "role_name",property = "roleName"), @Result(column = "role_desc",property = "roleDesc") public List<Role> findRoles(int id); }报错处理使用 foreach 批量操作,报错 ORA-00911: 无效字符的错误连接数据库:Oracle错误写法 <delete id="deleteEntityAll" parameterType="java.lang.reflect.Array"> <foreach collection="array" item="item" index="index" separator=";"> delete from GKCP_TC_JL where JH = #{item.jh} and to_char(TCRQ,'YYYY-MM-DD') = #{item.tcrq} </foreach> </delete>这样写生成的SQL语句用 ; 隔开,在sql工具中可以识别,但是mybatis不识别解决方案:在foreach中增加节点操作符可以解决(Oracle的匿名块)正确写法: <delete id="deleteEntityAll" parameterType="java.lang.reflect.Array"> <foreach collection="array" item="item" index="index" open="begin" close=";end;" separator=";"> delete from GKCP_TC_JL where JH = #{item.jh} and to_char(TCRQ,'YYYY-MM-DD') = #{item.tcrq} </foreach> </delete>
Querydsl-JPA 框架(推荐)官网:http://www.querydsl.com/static/querydsl/4.1.3/reference/html_single/参考:JPA整合Querydsl入门篇SpringBoot环境下QueryDSL-JPA的入门及进阶概述及依赖、插件、生成查询实体1.Querydsl支持代码自动完成,因为是纯Java API编写查询,因此主流Java IDE对起的代码自动完成功能支持几乎可以发挥到极致(因为是纯Java代码,所以支持很好) 2.Querydsl几乎可以避免所有的SQL语法错误(当然用错了Querydsl API除外,因为不写SQL了,因此想用错也难) 3.Querydsl采用Domain类型的对象和属性来构建查询,因此查询绝对是类型安全的,不会因为条件类型而出现问题 4.Querydsl采用纯Java API的作为SQL构建的实现可以让代码重构发挥到另一个高度5.Querydsl的领一个优势就是可以更轻松的进行增量查询的定义 使用在Spring环境下,可以通过两种风格来使用QueryDSL。一种是使用JPAQueryFactory的原生QueryDSL风格, 另一种是基于Spring Data提供的QueryDslPredicateExecutor<T>的Spring-data风格。使用QueryDslPredicateExecutor<T>可以简化一些代码,使得查询更加优雅。 而JPAQueryFactory的优势则体现在其功能的强大,支持更复杂的查询业务。甚至可以用来进行更新和删除操作。依赖 <dependencies> <!-- QueryDSL框架依赖 --> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> </dependency> </dependencies>添加maven插件(pom.xml)添加这个插件是为了让程序自动生成query type(查询实体,命名方式为:"Q"+对应实体名)。上文引入的依赖中querydsl-apt即是为此插件服务的。注:在使用过程中,如果遇到query type无法自动生成的情况,用maven更新一下项目即可解决(右键项目->Maven->Update Project)。<project> <build> <plugins> <plugin> <!--因为QueryDsl是类型安全的,所以还需要加上Maven APT plugin,使用 APT 自动生成Q类:--> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>补充:QueryDSL默认使用HQL发出查询语句。但也支持原生SQL查询。若要使用原生SQL查询,你需要使用下面这个maven插件生成相应的query type。<project> <build> <plugins> <plugin> <groupId>com.querydsl</groupId> <artifactId>querydsl-maven-plugin</artifactId> <version>${querydsl.version}</version> <executions> <execution> <goals> <goal>export</goal> </goals> </execution> </executions> <configuration> <jdbcDriver>org.apache.derby.jdbc.EmbeddedDriver</jdbcDriver> <jdbcUrl>jdbc:derby:target/demoDB;create=true</jdbcUrl> <packageName>com.mycompany.mydomain</packageName> <targetFolder>${project.basedir}/target/generated-sources/java</targetFolder> </configuration> <dependencies> <dependency> <groupId>org.apache.derby</groupId> <artifactId>derby</artifactId> <version>${derby.version}</version> </dependency> </dependencies> </plugin> </plugins> </build> </project>生成查询实体idea工具为maven project自动添加了对应的功能。添加好依赖和plugin插件后,就可以生成查询实体了。打开右侧的Maven Projects,如下图所示:双击clean清除已编译的target双击compile命令执行,执行完成后会在我们pom.xml配置文件内配置生成目录内生成对应实体的QueryDSL查询实体。生成的查询实体如下图所示:JPAQueryFactory 风格QueryDSL在支持JPA的同时,也提供了对Hibernate的支持。可以通过HibernateQueryFactory来使用。装配 与 注入SpringBoot注解方式装配/** * 方式一。使用Spring的@Configuration注解注册实例进行容器托管 @Configuration public class QueryDslConfig { @Bean public JPAQueryFactory jpaQueryFactory(EntityManager em){ return new JPAQueryFactory(em); * 方式二。在Dao类中初始化 // 实体管理 @Autowired private EntityManager entityManager; // 查询工厂 private JPAQueryFactory queryFactory; // 初始化JPA查询工厂 @PostConstruct // Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法) public void init(){ queryFactory = new JPAQueryFactory(entityManager); }注入 @Autowired private JPAQueryFactory queryFactory;更新、删除JPAQueryFactory 更新在Querydsl JPA中,更新语句是简单的 update-set/where-execute 形式。execute()执行后返回的是被更新的实体的数量。注意:使用QueryDsl更新实体时需要添加事务@Test @Transactional public void testUpdate() { QStudent qStudent = QStudent.student; Long result = queryFactory.update(qStudent) .set(qStudent.name, "haha") // 可以用if条件判断更新值来确定字段是否.set() .setnull(qStudent.age) // 设置null值 .where(qStudent.id.eq(111L)).execute(); assertThat(result, equalTo(1L)); }JPAQueryFactory 删除删除语句是简单的 delete-where-execute 形式。注意:使用QueryDsl删除实体时需要添加事务@Test @Transactional public void testDelete() { QStudent qStudent = QStudent.student; //删除指定条件的记录 Long result = queryFactory.delete(qStudent) .where(qStudent.id.eq(111L)) .execute(); assertThat(result, equalTo(1L)); //删除所有记录。即不加where条件 Long totalResult = queryFactory.delete(qStudent).execute(); System.out.println(totalResult); }查询表达式工具类Expressions 表达式工具类// when-then 条件表达式函数。when传参必须为名为eqTrue或eqFalse的Predicate T cases().when(Predicate).then(T a).otherwise(T b) DateExpression<Date> currentDate() // 返回当前日历(年-月-日)的 DateExpression TimeExpression<Time> currentTime() // 返回当前时刻(时:分:秒)的 TimeExpression DateTimeExpression<Date> currentTimestamp() // 返回当前时间(年-月-日 时:分:秒)的 DateTimeExpression // exprs 均为名为eqTrue的Predicate ,则返回名为eqTrue的Predicate,否则返回eqFalse的Predicate BooleanExpression allOf(BooleanExpression... exprs) // exprs 至少存在一个名为eqTrue的Predicate,则返回名为eqTrue的Predicate,否则返回eqFalse的Predicate BooleanExpression anyOf(BooleanExpression... exprs) // 转类型为 BooleanExpression。特别注意:asBoolean(Boolean).isTrue() 才是可用Predicate BooleanExpression asBoolean(Boolean) // asBoolean(true) <==等价==> booleanPath("true") NumberExpression asNumber(T) StringrExpression asString(T) DateExpression asDate(T) TimeExpression asTime(T) DateTimeExpression asDateTime(T) // 自定义语法 StringTemplate stringTemplate(String template, Object... args) NumberTemplate<T> numberTemplate(Class<? extends T> cl, String template, Object... args) BooleanTemplate booleanTemplate(String template, ImmutableList<?> args)MathExpressions 数学表达式工具类NumberExpression<A> round(Expression<A> num) // 四舍五入取整 NumberExpression<A> round(Expression<A> num, int s) // 四舍五入保留 s 位小数 NumberExpression<Double> asin(Expression<A> num) // 返回num的反正弦值。-1 <= num <= 1,否则返回null NumberExpression<Double> acos(Expression<A> num) // 返回num的反余弦值。-1 <= num <= 1,否则返回null // 慎用!qdsl-jpa底层是调用random()函数,MySQL没有该函数,只有rand()函数,会报错,解决方案为使用QDSL-SQL查询 NumberExpression<Double> random() // 返回0到1内的随机值 NumberExpression<Double> random(int seed) // 返回一个指定的0到1内的随机值表达式方法注意:在select()中查询出的结果使用表达式方法处理过后,若要封装到实体类中,则都需要使用 .as(alias) 起别名指定封装到实体类中的哪个字段。SimpleExpression 简单表达式 extends DslExpression extends Expression// 给查询字段取别名 T as(alias) BooleanExpression eq(T right) // 等于 equal BooleanExpression eqAll(T... right) BooleanExpression eqAny(T... right) BooleanExpression ne(T right) // 不等于 not equal BooleanExpression neAll(T... right) BooleanExpression neAny(T... right) BooleanExpression in(T... right) BooleanExpression notIn(T... right) BooleanExpression isNotNull() BooleanExpression isNull() // 相当于java中的switch语句。两种写法 T when(A).then(B).otherwise(C) // 该字段的查询结果等于参数则返回null,不等于则返回查询结果。Field == A ? null : Field SimpleExpression<T> nullif(A) // 符合过滤条件的的总条数。 select count(table.id) from table NumberExpression<Long> count()ComparableExpressionBase extends SimpleExpression// 设置默认值。返回 Field, A, B ... 顺序中第一个非null的值,若都为null则返回null // 注意:使用该方法兜底Oracle数据库的null为空字符串时会失效,因为Oracle会把空字符串当作null T coalesce(A, B ...)NumberExpression 数值表达式 extends ComparableExpressionBaseNumberExpression<T> add(A) // 加 NumberExpression<T> subtract(A) // 减 NumberExpression<T> multiply(A) // 乘 NumberExpression<T> divide(A) // 除 NumberExpression<T> mod(A) // 返回余数 NumberExpression<T> floor() // 向下取整 NumberExpression<T> ceil() // 向上取整 NumberExpression<T> round() // 四舍五入取整 NumberExpression<T> max() // 返回指定数值列的最大值 NumberExpression<T> min() // 返回指定数值列的最小值 NumberExpression<T> sqrt() // 返回指定数值列的平方根 NumberExpression<T> sum() // 返回指定数值列(或分组相同数值列)的总数 NumberExpression<T> avg() // 返回指定数值列(或分组相同数值列)的平均数 NumberExpression<T> abs() // 返回指定数值列的绝对值 NumberExpression<T> negate() // 返回指定数值列的相反数 StringExpression stringValue() // 返回字符串表达式 // 数据类型转换为数字类型。type为数字基本类型的包装类.class。实体类接收字段需与type的类型一致。 NumberExpression<T> castToNum(Class<A> type)ComparableExpression extends ComparableExpressionBaseBooleanExpression lt(T right) // 小于 less than BooleanExpression ltAll(T... right) BooleanExpression ltAny(T... right) BooleanExpression gt(T right) // 大于 greater than BooleanExpression gtAll(T... right) BooleanExpression gtAny(T... right) BooleanExpression loe(T right) // 小于等于 less than or equal BooleanExpression loeAll(T... right) BooleanExpression loeAny(T... right) BooleanExpression goe(T right) // 大于等于 greater than or equal BooleanExpression goeAll(T... right) BooleanExpression goeAny(T... right) BooleanExpression between(from, to) // from和to之间 [from, to] BooleanExpression notBetween(from, to)BooleanExpression 布尔表达式 extends LiteralExpression (extends ComparableExpression) implements PredicateBooleanExpression isTrue() // 计算结果若为true,则返回名为eqTrue的Predicate,否则返回名为eqFalse的Predicate BooleanExpression isFalse() // 计算结果若为false,则返回名为eqTrue的Predicate,否则返回名为eqFalse的Predicate BooleanExpression not() // 返回相反的结果 BooleanExpression eq(Boolean right) BooleanExpression and(Predicate right) BooleanExpression andAnyOf(Predicate... predicates) BooleanExpression or(Predicate right) BooleanExpression orAllOf(Predicate... predicates)StringExpressions 字符串表达式 extends LiteralExpression extends ComparableExpressionStringExpression contains(String str) // 包含参数字符串 BooleanExpression isEmpty() // 判断是否为空 BooleanExpression isNotEmpty() // 正则匹配查询 BooleanExpression matches(Expression<String> regex) // 模糊查询。% 为通配符,_ 表一个字符,可以传参escape指定转义字符 BooleanExpression like(String str) BooleanExpression like(String str, char escape) BooleanExpression endsWith(str) // 判断字符串的后缀是否为str。注意:必须使用boolean数据类型的字段接收 BooleanExpression startsWith(str) // 判断字符串的前缀是否为str。注意:必须使用boolean数据类型的字段接收 // 将字母转换大小写 StringExpression toLowerCase() StringExpression toUpperCase() StringExpression lower() StringExpression upper() StringExpression trim() // 去掉字符串两端的空格 StringExpression substring(int beginIndex) // 截取子字符串从索引位置至末尾 StringExpression concat(str) // 拼接 str StringExpression append(str) // 在末尾添加 str StringExpression prepend(str) // 在前面添加 str NumberExpression<Integer> length() // 返回字符串长度 NumberExpression<Integer> locate(str) // 返回 str 的位置(从1开始),没有返回0 NumberExpression<Integer> indexOf(str) // 返回 str 索引(从0开始),没有返回-1 SimpleExpression<Character> charAt(int i) // 返回参数索引位置的字符。实体类接收字段需为char或CharacterSSselect() 和 fetch() 的常用写法注意:使用fetch()查询时,数据库没有符合该条件的数据时,返回的是空集合,而不是null。QMemberDomain qm = QMemberDomain.memberDomain; //查询字段-select() List<String> nameList = queryFactory .select(qm.name) .from(qm) .fetch(); //查询实体-selectFrom() List<MemberDomain> memberList = queryFactory .selectFrom(qm) .fetch(); //查询并将结果封装至dto中 List<MemberFavoriteDto> dtoList = queryFactory .select( Projections.bean(MemberFavoriteDto.class, qm.name, qf.favoriteStoreCode)) .from(qm) .leftJoin(qm.favoriteInfoDomains, qf) .fetch(); //去重查询-selectDistinct() List<String> distinctNameList = queryFactory .selectDistinct(qm.name) .from(qm) .fetch(); //获取首个查询结果-fetchFirst() MemberDomain firstMember = queryFactory .selectFrom(qm) .fetchFirst(); //获取唯一查询结果-fetchOne() //当fetchOne()根据查询条件从数据库中查询到多条匹配数据时,会抛`NonUniqueResultException`。 MemberDomain anotherFirstMember = queryFactory .selectFrom(qm) .fetchOne();where 子句查询条件的常用写法//查询条件示例 List<MemberDomain> memberConditionList = queryFactory.selectFrom(qm) //like示例 .where(qm.name.like('%'+"Jack"+'%') //contain示例 .and(qm.address.contains("厦门")) //equal示例 .and(qm.status.eq("0013")) //between .and(qm.age.between(20, 30))) .fetch();使用QueryDSL提供的BooleanBuilder来进行查询条件管理。BooleanBuilder builder = new BooleanBuilder(); // like builder.and(qm.name.like('%'+"Jack"+'%')); // contain builder.and(qm.address.contains("厦门")); // equal示例 builder.and(qm.status.eq("0013")); // between builder.and(qm.age.between(20, 30)); List<MemberDomain> memberConditionList = queryFactory.selectFrom(qm).where(builder).fetch(); // 复杂的查询关系 BooleanBuilder builder2 = new BooleanBuilder(); builder2.or(qm.status.eq("0013")); builder2.or(qm.status.eq("0014")); builder.and(builder2); List<MemberDomain> memberConditionList = queryFactory.selectFrom(qm).where(builder2).fetch();自定义封装查询的结果集方法一:使用Projections的Bean方法JPAQueryFactory查询工厂的select方法可以将Projections方法返回的QBean作为参数,通过Projections的bean方法来构建返回的结果集映射到实体内,有点像Mybatis内的ResultMap的形式,不过内部处理机制肯定是有着巨大差别的!bean方法第一个参数需要传递一个实体的泛型类型作为返回集合内的单个对象类型,如果QueryDSL查询实体内的字段与DTO实体的字段名字不一样时,可以采用as方法来处理,为查询的结果集指定的字段添加别名,这样就会自动映射到DTO实体内。return queryFactory .select( Projections.bean(PersonIDCardDto.class, QIDCard.iDCard.idNo, QPerson.person.address, QPerson.person.name.as("userName"))) // 使用别名对应dto内的userName .from(QIDCard.iDCard, QPerson.person) .where(predicate) .fetch();底层原理:使用数据封装类的无参构造方法创建对象(如果类上有使用@Builder注解导致@Data无参构造方法被覆盖,则会报错,可以再加上 @AllArgsConstructor,@NoArgsConstructor 注解)使用setter方法封装数据给字段(会校验数据封装字段和Entity对应字段的数据类型是否一致,不一致则会报错)常见问题:Entity中时间字段的数据类型为 java.util.Date,数据封装类中时间字段的数据类型为 java.sql.Date 或具有指定时间格式的String类型,数据类型不一致,导致数据无法封装成功方案1:修改数据封装类或Entity中时间的数据类型,使其类型一致方案2:数据封装类中新增util.Date类型字段,手动重写其setter方法,处理数据后赋值到原sql.Date类型字段上。注意:查询封装数据到字段时 as("新增的util.Date字段")方案3:数据封装类中新增util.Date类型字段,先将数据封装到该字段,再通过getter、setter方法处理数据后赋值到原sql.Date类型字段上。方法二:使用Projections的fields方法return queryFactory .select( Projections.fields(PersonIDCardDto.class, QIDCard.iDCard.idNo, QPerson.person.address, QPerson.person.name)) .from(QIDCard.iDCard, QPerson.person) .where(predicate) .fetch();方法三:使用Projections的constructor方法,注意构造方法中参数的顺序return queryFactory .select( Projections.constructor(PersonIDCardDto.class, QPerson.person.name, QPerson.person.address, QIDCard.iDCard.idNo)) .from(QIDCard.iDCard, QPerson.person) .where(predicate) .fetch();方式四:使用集合的stream转换从方法开始到fetch()结束完全跟QueryDSL没有任何区别,采用了最原始的方式进行返回结果集,但是从fetch()获取到结果集后处理的方式就有所改变了。fetch()方法返回的类型是泛型List(List),List继承了Collection,完全存在使用Collection内非私有方法的权限,通过调用stream方法可以将集合转换成Stream泛型对象,该对象的map方法可以操作集合内单个对象的转换,具体的转换代码可以根据业务逻辑进行编写。在map方法内有个lambda表达式参数tuple,我们通过tuple对象get方法就可以获取对应select方法内的查询字段。注意:tuple只能获取select内存在的字段,如果select内为一个实体对象,tuple无法获取指定字段的值。 /** * 使用java8新特性Collection内stream方法转换dto public List<GoodDTO> selectWithStream() { //商品基本信息 QGoodInfoBean goodBean = QGoodInfoBean.goodInfoBean; //商品类型 QGoodTypeBean goodTypeBean = QGoodTypeBean.goodTypeBean; return queryFactory .select( goodBean.id, goodBean.price, goodTypeBean.name, goodTypeBean.id) .from(goodBean,goodTypeBean) //构建两表笛卡尔集 .where(goodBean.typeId.eq(goodTypeBean.id)) //关联两表 .orderBy(goodBean.order.desc()) //倒序 .fetch() .stream() //转换集合内的数据 .map(tuple -> { //创建商品dto GoodDTO dto = new GoodDTO(); //设置商品编号 dto.setId(tuple.get(goodBean.id)); //设置商品价格 dto.setPrice(tuple.get(goodBean.price)); //设置类型编号 dto.setTypeId(tuple.get(goodTypeBean.id)); //设置类型名称 dto.setTypeName(tuple.get(goodTypeBean.name)); //返回本次构建的dto return dto; //返回集合并且转换为List<GoodDTO> .collect(Collectors.toList()); }排序、分页排序.asc() // 升序 .desc() // 降序 .asc().nullsFirst() // 升序,空值放前面 .asc().nullsLast() // 降序,空值放前面//排序 List<MemberDomain> orderList = queryFactory.selectFrom(qm) .orderBy(qm.name.asc()) .fetch();分页.limit(long limit) // 限制查询结果返回的数量。即一页多少条记录(pageSize) .offset(long offset) // 跳过多少行。offset = ( pageNum - 1 ) * pageSize // pageNum:第几页 QMemberDomain qm = QMemberDomain.memberDomain; //写法一 JPAQuery<MemberDomain> query = queryFactory .selectFrom(qm) .orderBy(qm.age.asc()); // 查询总条数。fetchCount时,orderBy不会被执行 long total = query.fetchCount(); // 获取过滤后的查询结果集 List<MemberDomain> list0= query.offset(2).limit(5).fetch(); //写法二。fetchResults()自动实现count查询和结果查询,并封装到QueryResults<T>中 QueryResults<MemberDomain> results = queryFactory .selectFrom(qm) .orderBy(qm.age.asc()) .offset(2) .limit(5) .fetchResults(); List<MemberDomain> list = results.getResults(); // 过滤后的查询结果集 logger.debug("total:"+results.getTotal()); // 符合过滤条件的的总条数 logger.debug("offset:"+results.getOffset()); // 跳过多少条符合过滤条件的查询结果 logger.debug("limit:"+results.getLimit()); // 限制查询结果返回的条数写法一和二都会发出两条sql进行查询,一条查询count,一条查询具体数据。写法二的getTotal()等价于写法一的fetchCount。无论是哪种写法,在查询count的时候,orderBy、limit、offset这三个都不会被执行。可以大胆使用。子查询 // 子查询作为where条件内容 @Test public void selectJPAExpressions() { List<MemberDomain> subList = queryFactory .selectFrom(qm) .where(qm.status.in( JPAExpressions.select(qm.status).from(qm))) .fetch(); // 子查询作为select查询字段 @Test public void selectJPAExpressions() { QUserAddress ua = QUserAddress.userAddress; QUser u = QUser.user; List<UserAddressDTO> list = queryFactory .select( Projections.bean(UserAddressDTO.class , ua.addressee , Expressions.asNumber( JPAExpressions .select(u.id.count()) .from(u) .where(u.id.ne(ua.userId)) .longValue() // asNumber接收子查询结果后需要指定数值的数据类型 .as("lon") // , Expressions.asString( // asString接收子查询结果后不用指定数据类型 // JPAExpressions. // select(u.username) // .from(u) // .where(u.id.eq(ua.userId)) // ) // .as("password") .from(ua) .where(ua.id.eq(38)) .fetch(); }联表动态查询 // JPA查询工厂 @Autowired private JPAQueryFactory queryFactory; * 关联查询示例,查询出城市和对应的旅店 @Test public void findCityAndHotel() { QTCity qtCity = QTCity.tCity; QTHotel qtHotel = QTHotel.tHotel; JPAQuery<Tuple> jpaQuery = queryFactory .select(qtCity, qtHotel) .from(qtCity) .leftJoin(qtHotel) .on(qtHotel.city.longValue().eq(qtCity.id.longValue())); // 分离式 添加查询条件 jpaQuery.where(QTCity.tCity.name.like("shanghai")); // 获取查询结果 List<Tuple> result = jpaQuery.fetch(); // 对多元组取出数据,这个和select时的数据相匹配 for (Tuple row : result) { System.out.println("qtCity:" + row.get(qtCity)); System.out.println("qtHotel:" + row.get(qtHotel)); System.out.println("--------------------"); }联表一对多查询封装方式一:查询结果返回类型为ListList<UserAddressDTO> list = queryFactory .from(u) .join(ua) .on(ua.userId.eq(u.id)) .where(u.id.eq(31)) .transform(GroupBy.groupBy(u.id) .list( Projections.bean(UserAddressDTO.class, u.id, u.username, GroupBy.list( Projections.bean(UserAddress.class, ua.address, ua.city, ua.district )).as("userAddresses"))) );方式二:查询结果返回类型为Mapmap的key为分组字段,一般为主键IDMap<Integer, UserAddressDTO> map = queryFactory .from(u) .join(ua) .on(ua.userId.eq(u.id)) .where(u.id.eq(31)) .transform(GroupBy.groupBy(u.id) Projections.bean(UserAddressDTO.class, u.id, u.username, GroupBy.list(Projections.bean(UserAddress.class, ua.address, ua.city, ua.district )).as("userAddresses"))) );实体类:@Data @Builder @AllArgsConstructor @NoArgsConstructor public class UserAddressDTO { private Integer id; private String username; private String password; private String phone; private List<UserAddress> userAddresses; }使用聚合函数//聚合函数-avg() Double averageAge = queryFactory .select(qm.age.avg()) .from(qm) .fetchOne(); //聚合函数-concat() String concat = queryFactory .select(qm.name.concat(qm.address)) .from(qm) .fetchOne(); //聚合函数-date_format() String date = queryFactory .select( Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate)) .from(qm) .fetchOne();当用到DATE_FORMAT这类QueryDSL似乎没有提供支持的Mysql函数时,我们可以手动拼一个String表达式。这样就可以无缝使用Mysql中的函数了。使用 Template 实现自定义语法QueryDSL并没有对数据库的所有函数提供支持,好在它提供了Template特性。可以使用Template来实现各种QueryDSL未直接支持的语法。Template的局限性: 由于Template中使用了{}来作为占位符(内部序号从0开始),而正则表达式中也可能使用了{},因而会产生冲突。QMemberDomain qm = QMemberDomain.memberDomain; //使用booleanTemplate充当where子句或where子句的一部分 List<MemberDomain> list = queryFactory .selectFrom(qm) .where(Expressions.booleanTemplate("{0} = \"tofu\"", qm.name)) .fetch(); //上面的写法,当booleanTemplate中需要用到多个占位时 List<MemberDomain> list1 = queryFactory .selectFrom(qm) .where( Expressions.booleanTemplate("{0} = \"tofu\" and {1} = \"Amoy\"", qm.name, qm.address)) .fetch(); //使用stringTemplate充当查询语句的某一部分 String date = queryFactory .select( Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate)) .from(qm) .fetchFirst(); //在where子句中使用stringTemplate String id = queryFactory .select(qm.id) .from(qm) .where( Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate).eq("2018-03-19")) .fetchFirst();QueryDslPredicateExecutor 风格通常使用Repository来继承QueryDslPredicateExecutor<T>接口。通过注入Repository来使用。Repository 接口Spring Data JPA中提供了QueryDslPredicateExecutor接口,用于支持QueryDSL的查询操作。public interface tityRepository extends JpaRepository<City, Integer>, QuerydslPredicateExecutor<city> {}QueryDslPredicateExecutor<T>接口提供了findOne(),findAll(),count(),exists()四个方法来支持查询。并可以使用更优雅的BooleanBuilder 来进行条件分支管理。count()会返回满足查询条件的数据行的数量exists()会根据所要查询的数据是否存在返回一个boolean值findOne()、findAll()findOne从数据库中查出一条数据。没有重载方法。Optional<T> findOne(Predicate var1);和JPAQuery的fetchOne()一样,当根据查询条件从数据库中查询到多条匹配数据时,会抛NonUniqueResultException。使用的时候需要慎重。findAll()findAll是从数据库中查出匹配的所有数据。提供了以下几个重载方法。Iterable<T> findAll(Predicate var1); Iterable<T> findAll(Predicate var1, Sort var2); Iterable<T> findAll(Predicate var1, OrderSpecifier<?>... var2); Iterable<T> findAll(OrderSpecifier<?>... var1); Page<T> findAll(Predicate var1, Pageable var2);使用示例:QMemberDomain qm = QMemberDomain.memberDomain; // QueryDSL 提供的排序实现 OrderSpecifier<Integer> order = new OrderSpecifier<>(Order.DESC, qm.age); Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"),order); QMemberDomain qm = QMemberDomain.memberDomain; // Spring Data 提供的排序实现 Sort sort = new Sort(new Sort.Order(Sort.Direction.ASC, "age")); Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"), sort);单表动态分页查询单表动态查询示例://动态条件 QTCity qtCity = QTCity.tCity; //SDL实体类 //该Predicate为querydsl下的类,支持嵌套组装复杂查询条件 Predicate predicate = qtCity.id.longValue().lt(3).and(qtCity.name.like("shanghai")); //分页排序 Sort sort = new Sort(new Sort.Order(Sort.Direction.ASC,"id")); PageRequest pageRequest = new PageRequest(0,10,sort); //查找结果 Page<TCity> tCityPage = tCityRepository.findAll(predicate, pageRequest);Querydsl SQL 查询Querydsl SQL 模块提供与 JDBC API 的集成。可以使用更多的 JDBC SQL方法。比如可以实现 from 的查询主体为子查询出来的临时表、union、union All 等Querydsl-JPA限制的相关操作。还可以根据 JDBC API 获取数据库的类型使用不同的数据库语法模板。依赖及配置依赖: <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-sql</artifactId> <version>${querydsl.version}</version> </dependency> <!-- joda-time为querydsl-sql中必需的新版本的时间日期处理库 --> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.10.5</version> </dependency>yaml配置:logging: level: com.querydsl.sql: debug # 打印日志SQLQuery 的 Q 类需要自己创建编写(可以基于apt 插件生成的 JPA 的Q类改造),并放到主目录(src)启动类下的包里。使用 extends RelationalPathBase<Entity> 的Q类。推荐需要将数据库表名传入构造方法的table参数里,path 可以传别名,所有的property参数为实体类的属性名(驼峰命名),addMetadata() 中ColumnMetadata.named("FeildNmae") 的 FeildNmae 为数据库字段名。使用该Q类查询所有字段数据时(即select(Q类))可以自动映射封装结果集。使用extends EntityPathBase<Entity>的Q类。需要将传入构造方法的variable参数改成数据库表名,并且将所有的property参数改成相对应的数据库字段名。注意:使用 extends EntityPathBase<Entity> 的实体Q类,直接 select(Q类) 会报错,无法自动映射封装结果集,需要使用Projections.bean(Entity.class,Expression<?>... exprs) 手动封装结果集。/** * extends RelationalPathBase<Entity> 的Q类示例 public class QEmployee extends RelationalPathBase<Employee> { private static final long serialVersionUID = 1394463749655231079L; public static final QEmployee employee = new QEmployee("EMPLOYEE"); public final NumberPath<Integer> id = createNumber("id", Integer.class); public final StringPath firstname = createString("firstname"); public final DatePath<java.util.Date> datefield = createDate("datefield", java.util.Date.class); public final PrimaryKey<Employee> idKey = createPrimaryKey(id); public QEmployee(String path) { super(Employee.class, PathMetadataFactory.forVariable(path), "PUBLIC", "EMPLOYEE"); addMetadata(); public QEmployee(PathMetadata metadata) { super(Employee.class, metadata, "PUBLIC", "EMPLOYEE"); addMetadata(); protected void addMetadata() { addMetadata(id, ColumnMetadata.named("ID").ofType(Types.INTEGER)); addMetadata(firstname, ColumnMetadata.named("FIRSTNAME").ofType(Types.VARCHAR)); addMetadata(datefield, ColumnMetadata.named("DATEFIELD").ofType(Types.DATE)); * extends EntityPathBase<Entity> 的Q类示例 public class QUserAddressS extends EntityPathBase<UserAddress> { private static final long serialVersionUID = -1295712525L; public static final QUserAddressS userAddress = new QUserAddressS("tb_user_address"); public final NumberPath<Integer> id = createNumber("id", Integer.class); public final StringPath address = createString("address"); public final DateTimePath<java.util.Date> createTime = createDateTime("create_time", java.util.Date.class); public QUserAddressS(String variable) { super(UserAddress.class, forVariable(variable)); public QUserAddressS(Path<? extends UserAddress> path) { super(path.getType(), path.getMetadata()); public QUserAddressS(PathMetadata metadata) { super(UserAddress.class, metadata); }SQLQueryFactory 方式装配及基本使用装配@Configuration @Slf4j public class QueryDslConfig { @Bean public SQLQueryFactory sqlQueryFactory(DataSource druidDataSource){ SQLTemplates t; try(Connection connection = druidDataSource.getConnection()){ t = new SQLTemplatesRegistry().getTemplates(connection.getMetaData()); }catch (Exception e){ log.error("", e); t = SQLTemplates.DEFAULT; com.querydsl.sql.Configuration configuration = new com.querydsl.sql.Configuration(t); configuration.addListener(new SQLBaseListener(){ @Override public void end(SQLListenerContext context) { if (context != null && !DataSourceUtils.isConnectionTransactional(context.getConnection(), druidDataSource)){ // 若非事务连接 SQLCloseListener.DEFAULT.end(context); configuration.setExceptionTranslator(new SpringExceptionTranslator()); // 创建SQLQueryFactory,且数据库连接由spring管理 return new SQLQueryFactory(configuration, () -> DataSourceUtils.getConnection(druidDataSource)); }注入 @Autowired private SQLQueryFactory sqlQueryFactory;SQLQueryFactory 基本使用 /** * 子查询作为临时表传入from()中 @Test public void selectBySqlQueryFactory(){ // 使用 extends RelationalPathBase<Entity> 的QEntity,自动映射封装 QUserAddressSql uaSql = QUserAddressSql.userAddress; // 子查询 SQLQuery<Tuple> q = SQLExpressions .select( // 查询字段须是数据库表中的字段名(不是实体属性名),且类型一致 uaSql.addressee , uaSql.userId .from(uaSql); List<Tuple> fetch = sqlQueryFactory .select( // 查询字段须是临时表中的字段别名,且类型一致 Expressions.template(String.class, "q.addressee").as("addressee") , Expressions.numberTemplate(Integer.class, "q.user_id").as("userId") .from(q, Expressions.stringPath("q")) // 子查询作为临时表 .fetch(); System.out.println(fetch); * 子查询结果集 union @Test public void selectBySqlQueryFactory(){ // 使用 extends EntityPathBase<Entity> 的改造版QEntity,结果集如需封装到实体类,必须手动指定实体类来接收 QUserAddressSql uaSql = QUserAddressSql.userAddress; QUserSql uSql = QUserSql.user; SQLQuery<Tuple> a = SQLExpressions .select(uaSql.userId.as("user_id") , uaSql.phone) .from(uaSql) .where(uaSql.userId.eq(30)); SQLQuery<Tuple> b = SQLExpressions .select(uSql.id.as("user_id") , uSql.phone) .from(uSql) .where(uSql.id.eq(29).or(uSql.id.eq(30))); Union<Tuple> union = sqlQueryFactory.query().union(a, b); long count = sqlQueryFactory .from(union, Expressions.stringPath("q")).fetchCount(); List<UserAddressDTO> list = sqlQueryFactory .from(union, Expressions.stringPath("q")) .orderBy(Expressions.numberPath(Integer.class, "user_id").desc() , Expressions.stringTemplate("phone").desc()) .offset(0) .limit(5) .transform( GroupBy.groupBy(Expressions.template(String.class, "q.user_id")).list( Projections.bean(UserAddressDTO.class , Expressions.template(Integer.class, "q.user_id").as("userId") , GroupBy.list(Projections.bean(UserAddress.class , Expressions.stringTemplate("q.phone").as("phone") )).as("userAddresses") System.out.println(count); list.forEach(s -> System.out.println(JSON.toJSONString(s))); }SQLExpression 表达式工具类// 合并多张表记录。union为去重合并,unionAll为不去重合并 static <T> Union<T> union(SubQueryExpression<T>... sq) static <T> Union<T> union(List<SubQueryExpression<T>> sq) static <T> Union<T> unionAll(SubQueryExpression<T>... sq) static <T> Union<T> unionAll(List<SubQueryExpression<T>> sq) // 调用函数查询序列 static SimpleExpression<Long> nextval(String sequence) static <T extends Number> SimpleExpression<T> nextval(Class<T> type, String sequence) // 使用示例:SQL写法:select seq_process_no.nextval from dual; Long nextvalReturn = sqlQueryFactory.select(SQLExpressions.nextval("序列名")).fetchOne; // 将多列记录聚合为一列记录。delimiter为分隔符。Oracle数据库专属,其他数据库报错 static WithinGroup<Object> listagg(Expression<?> expr, String delimiter) // 使用示例: SQLExpression.listagg(qEntity.name, ",").withinGroup.OrderBy(qEntity.name.asc()).getValue.as("Name") // 将多列记录聚合为一列记录。separator为分隔符。MySQL、PostgreSQL都可用,PostgreSQL会根据模板翻译成String_agg函数 static StringExpression groupConcat(Expression<String> expr, String separator) static StringExpression groupConcat(Expression<String> expr) static <T> RelationalFunctionCall<T> relationalFunctionCall(Class<? extends T> type, String function, Object... args) static <D extends Comparable> DateExpression<D> date(DateTimeExpression<D> dateTime) static <D extends Comparable> DateExpression<D> date(Class<D> type, DateTimeExpression<?> dateTime) static <D extends Comparable> DateTimeExpression<D> dateadd(DatePart unit, DateTimeExpression<D> date, int amount) static <D extends Comparable> DateExpression<D> dateadd(DatePart unit, DateExpression<D> date, int amount) // 获取两个日期的时间间隔(end-start) static <D extends Comparable> NumberExpression<Integer> datediff(DatePart unit, DateTimeExpression<D> start, DateTimeExpression<D> end)JPASQLQuery 方式使用 JPASQLQuery 作为查询引擎时,使用的QEntity(extends EntityPathBase<Entity>),传入构造方法的variable参数可以不为数据库表名(因为 JPASQLQuery可以找到映射的真实表名,仅把此参数作为表别名),但所有的property参数仍必需为相对应的数据库字段名。故并不能直接使用 apt 插件生成 的 jpa 使用的Q类,仍需要使用改造版的 Q类(extends EntityPathBase<Entity>)。 @Test public void selectBySqlQueryFactory(){ // 使用 extends EntityPathBase<Entity> 的改造版QEntity,结果集如需封装到实体类,必须手动指定实体类来接收 QUserAddress ua = QUserAddress.userAddress; // jpa+sql的查询工具,本例使用的oracle的sql模板 JPASQLQuery<?> jpasqlQuery = new JPASQLQuery<Void>(em, new OracleTemplates()); // 子查询 SQLQuery<Tuple> q = SQLExpressions .select( // 查询字段须是数据库表中的字段名(不是实体属性名),且类型一致。如直接不使用QEntity的属性,则需手动指定 Expressions.stringPath("addressee").as("addressee") , Expressions.numberPath(Integer.class, "user_id").as("user_id") .from(ua); List<Tuple> fetch = jpasqlQuery .select( // 查询字段须是临时表中的字段名或别名,且类型一致。结果集字段需添加别名手动映射封装 Expressions.template(String.class, "q.addressee").as("addressee") , Expressions.numberTemplate(Integer.class, "q.user_id").as("userId") .from(q, Expressions.stringPath("q")) // 子查询作为临时表 .fetch(); System.out.println(fetch); }拓展了解获取数据库的类型import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @Component @Slf4j public class DataSourceUtil { @Autowired private DataSource dataSource; // 数据库类型名称 private static String databaseProductName; * 初始化静态成员变量 @PostConstruct public void init(){ try(Connection connection = dataSource.getConnection()) { databaseProductName = connection.getMetaData().getDatabaseProductName(); } catch (SQLException e) { e.printStackTrace(); * 获取数据库类型名称 public static String getDatabaseProductName(){ return databaseProductName; }Jpa 表名大小写转换、字段名规避数据库关键字在linux下,mysql的表名是区分大小写的,如果不能通过修改mysql配置取消表名区分大小写,则可以通过在Hibernate将转化的SQL语句发送给数据库执行之前转换大小写。如果数据库字段名与数据库关键字(保留字)同名导致sql语句执行失败,也可通过该自定义类处理import com.duran.ssmtest.utils.DataSourceUtil; import org.apache.commons.lang3.StringUtils; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; import org.hibernate.cfg.ImprovedNamingStrategy; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.springframework.stereotype.Component; import java.util.Arrays; @Component public class MyPhysicalNamingStrategyStandardImpl extends PhysicalNamingStrategyStandardImpl { private static final long serialVersionUID = 1L; // mysql关键字列表 private static final String mysqlKey = "ADD,ALL,ALTER,ANALYZE,AND,AS,ASC,ASENSITIVE" + ",BEFORE,BETWEEN,BIGINT,BINARY,BLOB,BOTH,BY,CALL,CASCADE,CASE,CHANGE,CHAR,CHARACTER" + ",CHECK,COLLATE,COLUMN,CONDITION,CONNECTION,CONSTRAINT,CONTINUE,CONVERT,CREATE,CROSS" + ",CURRENT_DATE,CURRENT_TIME,CURRENT_TIMESTAMP,CURRENT_USER,CURSOR,DATABASE,DATABASES" + ",DAY_HOUR,DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DEC,DECIMAL,DECLARE,DEFAULT,DELAYED" + ",DELETE,DESC,DESCRIBE,DETERMINISTIC,DISTINCT,DISTINCTROW,DIV,DOUBLE,DROP,DUAL,EACH,ELSE" + ",ELSEIF,ENCLOSED,ESCAPED,EXISTS,EXIT,EXPLAIN,FALSE,FETCH,FLOAT,FLOAT4,FLOAT8,FOR,FORCE" + ",FOREIGN,FROM,FULLTEXT,GOTO,GRANT,GROUP,HAVING,HIGH_PRIORITY,HOUR_MICROSECOND,HOUR_MINUTE" + ",HOUR_SECOND,IF,IGNORE,IN,INDEX,INFILE,INNER,INOUT,INSENSITIVE,INSERT,INT,INT1,INT2,INT3" + ",INT4,INT8,INTEGER,INTERVAL,INTO,IS,ITERATE,JOIN,KEY,KEYS,KILL,LABEL,LEADING,LEAVE,LEFT" + ",LIKE,LIMIT,LINEAR,LINES,LOAD,LOCALTIME,LOCALTIMESTAMP,LOCK,LONG,LONGBLOB,LONGTEXT,LOOP" + ",LOW_PRIORITY,MATCH,MEDIUMBLOB,MEDIUMINT,MEDIUMTEXT,MIDDLEINT,MINUTE_MICROSECOND,MINUTE_SECOND" + ",MOD,MODIFIES,NATURAL,NOT,NO_WRITE_TO_BINLOG,NULL,NUMERIC,ON,OPTIMIZE,OPTION,OPTIONALLY,OR" + ",ORDER,OUT,OUTER,OUTFILE,PRECISION,PRIMARY,PROCEDURE,PURGE,RAID0,RANGE,READ,READS,REAL" + ",REFERENCES,REGEXP,RELEASE,RENAME,REPEAT,REPLACE,REQUIRE,RESTRICT,RETURN,REVOKE,RIGHT,RLIKE" + ",SCHEMA,SCHEMAS,SECOND_MICROSECOND,SELECT,SENSITIVE,SEPARATOR,SET,SHOW,SMALLINT,SPATIAL" + ",SPECIFIC,SQL,SQLEXCEPTION,SQLSTATE,SQLWARNING,SQL_BIG_RESULT,SQL_CALC_FOUND_ROWS" + ",SQL_SMALL_RESULT,SSL,STARTING,STRAIGHT_JOIN,TABLE,TERMINATED,THEN,TINYBLOB,TINYINT" + ",TINYTEXT,TO,TRAILING,TRIGGER,TRUE,UNDO,UNION,UNIQUE,UNLOCK,UNSIGNED,UPDATE,USAGE,USE" + ",USING,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VALUES,VARBINARY,VARCHAR,VARCHARACTER,VARYING,WHEN" + ",WHERE,WHILE,WITH,WRITE,X509,XOR,YEAR_MONTH,ZEROFILL"; @Override public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) { // 驼峰命名策略转换表名 String tableName = ImprovedNamingStrategy.INSTANCE.tableName(name.getText()); // 将entity中的表名全部转换成大写 tableName = tableName.toUpperCase(); return Identifier.toIdentifier(tableName); @Override public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment context) { // 驼峰命名策略转换字段名 String colnumName = ImprovedNamingStrategy.INSTANCE.columnName(name.getText()); // 将entity中的字段名全部转换成大写 colnumName = colnumName.toUpperCase(); // 如果entity字段名与mysql关键字同名,则在entity字段名加上`` if ("mysql".equalsIgnoreCase(DataSourceUtil.getDatabaseProductName())){ if (Arrays.asList(StringUtils.split(mysqlKey, ",")).contains(colnumName.toUpperCase())){ colnumName = "`" + colnumName + "`"; return Identifier.toIdentifier(colnumName); }如果自定义的MyPhysicalNamingStrategyStandardImpl类未加@Component注解(将类交给spring容器管理),则需要在 application.properties里面添加如下配置(hibernate版本 >= 5.0):# 值为自定义的MyPhysicalNamingStrategyStandardImpl类的全限定类名 spring.jpa.hibernate.naming.physical-strategy=com.test.config.MyPhysicalNamingStrategyStandardImpl需要注意的是,如果hibernate版 < 5.0,则配置里的内容应是spring.jpa.hibernate.naming-strategy=com.test.config.strategy.MyImprovedNamingStrategy同时自定义的MyImprovedNamingStrategy类继承ImprovedNamingStrategy,并且重写相应的方法。Spring JPA Junit 关闭自动回滚使用 JPA 配合Hibernate ,采用注解默认是开启了LayzLoad也就是懒加载,所以当操作为增删改时需在Junit的单元测试上加上@Transactional注解,这样Spring会自动为当前线程开启Session,这样在单元测试里面懒加载才不会因为访问完Repository之后,出现session not found.但若在单元测试上面加了@Transactional 会自动回滚事务,需要在单元测试上面加上@Rollback(false),才能修改数据库。@Test @Transactional @Rollback(false) //关闭自动回滚 public void saveTest() { ProductCategory category = new ProductCategory(); category.setCategoryname("快乐"); category.setCategorytype(6); ProductCategory save = categoryRepository.save(category); System.out.println(save.toString());延迟加载与立即加载(FetchType)通常可以在@OneToMany中用LAZY、在@ManyToOne/Many中用EAGER,但不绝对,看具体需要。FetchType.LAZY:延迟加载,在查询实体A时,不查询出关联实体B,在调用getxxx方法时,才加载关联实体,但是注意,查询实体A时和getxxx必须在同一个Transaction中,不然会报错:no session。即会表现为两次单独的SQL查询(非联表查询)FetchType.EAGER:立即加载,在查询实体A时,也查询出关联的实体B。即会表现为一次查询且是联表查询默认情况下,@OneToOne、@ManyToOne是LAZY,@OneToMany、@ManyToMany是EAGER。有两个地方用到延迟加载:relationship(@OneToMany等)、attribute(@Basic)。后者一般少用,除非非常确定字段很少访问到。时间类型的精度问题如MySQL的DATETIME类型,默认是精确到秒的,故存入的时间戳的毫秒会被舍弃并根据四舍五入加入到秒(如1s573ms变成2s、1s473ms变成1s),从而保存进去与查出来的也会不一致。外键关联、关联删除外键关联相关注解:@ManyToOne/@OneToMany/@OneToOne 、 @JoinColumn/@PrimaryKeyJoinColumn、@MapsId,用法及区别见:https://www.cnblogs.com/chiangchou/p/mappedBy.html(1)@JoinColumn用来指定外键,其name属性指定该注解所在Entity对应的表的一个列名(2)@ManyToOne等用来指定对应关系是多对一等数量对应关系通过@ManyToOne等注解指定数量对应关系时,须在多的一方标注(@ManyToOne),一的一方注不注均可。(以下以School、Student为例,为一对多关系)若只用 @ManyToOne等注解 没用 @JoinColumn注解,则在生成表时会自动生成一张关联表来关联School、Student,表中包含School、Studeng的id若在用了 @ManyToOne等注解 的基础上用了 @JoinColumn注解 则不会自动生成第三张表,而是会在多的一方生成一个外键列。列名默认为 ${被引用的表名}_id (可以通过 @JoinColumn 的name属性指定列名)。上法的缺点是在 insert 多的一方后会再执行一次 update 操作来设置外键的值(即使 在inser t时已经指定了),避免额外update 的方法:在一的一方不使用 @JoinColumn ,而是改为指定 @OneToMan等注解 的mappedBy属性。注意:@JoinColumn注解 和 @ManyToOne等注解的mappedBy属性不能同时存在,会报错。关联删除假设有user、admin两表,admin.user_id 与 user.id 对应。当要删除userId为"xx"一条admin表记录时:1.若业务逻辑中未使用 JPA软删除:1.1 若后者通过外键关联前者,则直接从user删除id为"xx"的记录即可,此时会级联删除admin表的相应记录。当然要分别从两表删除记录也可,此时须保证先从admin表再从user表删除;1.2 若无外键关联,则需要分别从user、admin删除该记录,顺序先后无关紧要;2.若使用了软删除,对于软删除操作外键将不起作用(因为物理上并未删除记录),因此此时也只能分别从两表软删除记录。但不同的是,此时须先从admin再从user表删除记录。若顺序相反,会发现user表的记录不会被软删除。猜测原因为:内存中存在userEntity、adminEntity且adminEntity.userByUserId引用了userEntity,导致delete userEntity时发现其被adminEntity引用了从而内部取消执行了delete操作。在实际业务中一般都会启用软删除,所以物理删除的场景很少,从而上面1.1、1.2的场景很少。综上,在涉及到关联删除时,最好按拓扑排序的顺序(先引用者再被引用者)依次删除各Entity记录。示例:进行如下设置后,JPA会自动生成为student表生成两个外键约束:student表school_id关联school表id自动、student表id字段关联user表id字段。//StudentEntity //get set ... @Column(name = "id") private String sId; @Column(name = "school_id") private String schoolId; @ManyToOne @JoinColumn(name = "school_id", referencedColumnName = "id", insertable = false, updatable = false)//school.school_id字段外键关联到school.id字段;多个字段对应数据库同一字段时会报错,通过添加insertable = false, updatable = false即可 private SchoolEntity schoolBySchoolId; @OneToOne @JoinColumn(name = "id", referencedColumnName = "id", insertable = false, updatable = false) //student.id字段外键关联到user.id字段。也可用@PrimaryKeyJoinColumn @MapsId(value = "id") private UserEntity userByUserId;对于外键属性(如上面student表的school_id),当该属性不是当前表的主键时,通过 @OneToOne/@ManyToOne + @JoinColumn 定义即可成功地在数据库中自动生成产生外键约束。但当该属性也是当前表的主键时(如为student.id定义外键来依赖user.id字段),单靠@OneToOne + @JoinColumn并不能自动产生外键约束,此时可通过加@MapIds来解决。总结:通过@ManyToOne/@OneToMany/@OneToOne + @JoinColumn/@PrimaryKeyJoinColumn定义外键,是否需要@MapsId视情况而定。外键场景有两种:外键属性不是当前表的主键(如上面student表的school_id字段不是主键)外键属性也是当前表的属性(如上面student表的id字段是主键)基于这两种场景,各注解使用时的组合及效果如下:说明:使用注解组合后是否会自动为表生成外键约束?打钩的表示会、打叉的表示不会、半勾半叉的表示会但是生成的不是预期的(如场景1中期望school_id关联了school id自动,但一种结果是id关联了user id、另一种是自动产生了school_by_school_id字段并关联到了school id,显然都不符合期望)。结论:1、外键属性不是主键的场景(第一种),用 @OneToOne/@ManyToOne + @JoinColumn 即可,为了简洁推荐不用@MapIds,示例见上面的school_id关联school id设置。2、外键属性是主键的场景(第二种),用 @OneToOne + @JoinColumn + @MapsId,示例见上面的student id关联user id设置。虽从表中可见场景二有三种组合都可以达到目标,但为了符合业务语义(主键嘛,当然是唯一的,因此是一对一)且为了和场景一的尽可能统一,我们采用这个的组合。实践发现,使用@MapsId时,要求外键字段、被关联的字段 的数据库列名得相同且都得为"id"。why?如何避免?TODO参考资料:https://stackoverflow.com/questions/4756596/jpa-hibernate-unidirectional-one-to-one-mapping-with-shared-primary-key通过 JPA 定义表结构的关联关系(如共用部分字段等)这里以实际项目中课程、实验、步骤与其翻译数据的表结构关联方案设计为例:多语言表(翻译表)与原表(主表)关联方案设计,需求:字段(列)复用以免重复代码定义、同一个列的定义如是否为空在不同表中可不一样(如有些字段主表中非空但翻译表中可空),有如下方案:无关联,重复定义。pass有关联通过@MappeSuperclass,不同子类可以完全继承父类列定义且分别对应不同表,表结构完全相同,但不能覆盖父类的定义。pass通过@Inheritance,三种策略:SINGLE_TABLE:父、子类对应同一张表。源课程和翻译课程id一样,违背主键唯一约束。passJOINED:父、子类对应不同表且子类自动加与父类主键一样的字段与父类主键关联,但父表中除主键之外的所有字段无法在子表中再出现。passTABLE_PER_CLASS:父、子类对应不同表且表定义完全相同,无外键,但同一字段在不同表中字段定义无法不同。pass定义个普通父类,子类继承父类并分别进行@Column定义:不同子类对应不同表,不同表含有的字段及定义可不一样。selectedWeb 支持参阅:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.webBasic Web Support(Domain class、Pageable、Sort)domain类(即被Spring Data CrudRepository管理的domain类,如Entity类)及Pageable、Sort可以直接作为handler方法的形参,框架会自动解析请求参数组装成相应的实参,示例:@Controller @RequestMapping("/users") class UserController { @RequestMapping("/{id}") String showUserForm(@PathVariable("id") User user, Model model) { model.addAttribute("user", user); return "userForm"; @Controller @RequestMapping("/users") class UserController { private final UserRepository repository; UserController(UserRepository repository) { this.repository = repository; @RequestMapping String showUsers(Model model, Pageable pageable) { model.addAttribute("users", repository.findAll(pageable)); return "users"; }对于domain类,会自动根据request的"id"参数调用repository的findById查得对象。request示例:/user?id=2对于Pageable,会根据request"page"、"size"参数组装对象;request示例:/users?page=0&size=2对于Sort,会根据request的"sort"参数组装对象,该参数值须遵循规则: property,property(,ASC|DESC)(,IgnoreCase) 。request示例:/users?sort=firstname&sort=lastname,asc&sort=city,ignorecase内部原理:第一者是由 DomainClassConverter 类完成的,后两者是由 HandlerMethodArgumentResolver 完成的。Querydsl Web Support可以直接将Querydsl的Predicate作为handler方法的形参,框架会自动(默认只要Querydsl在classpath上就会生效)根据请求参数组装创建Predicate实例。示例:@Controller class UserController { @Autowired UserRepository repository; @RequestMapping(value = "/", method = RequestMethod.GET) String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) { model.addAttribute("users", repository.findAll(predicate, pageable)); return "index"; }SpringDataJpa 和 mybatis 的比较spring data jpa实现了jpa(java persistence api)功能,即可以实现pojo转换为关系型数据库记录的功能,通俗来讲就是可以不写任何的建表sql语句了。jpa是spring data jpa功能的一个子集。而mybatis并没有jpa功能,建表语句还是要自己写的。spring data jpa是全自动框架,不需要写任何sql。而mybatis是半自动框架,需要自己写sql,mybatis-plus为mybatis赋能,使其也可以基本上不需要写任何模板sql。debug模式下看生成的sql,mybatis下的sql可读性很好,而spring data jpa下的查询sql可读性并不好。spring data jpa的insert与update都调用同一个方法save,如果带有主键id(如果启用了乐观锁,那么还有version字段),那么就是更新,否则就是新增,所以addOrUpdate是一个接口;而mybatis中提供insert方法和updateById方法。由于spring data jpa调用同一个方法,所以其要执行两条sql,先执行查询,再执行插入/更新。另外就是返回值,spring data jpa的返回值是Employee对象,而mybatis的返回值是影响的行数,当然mybatis也可以得到新增后的id,返回新增后的对象spring data jpa的dynamic sql是使用JpaSpecificationExecutor,而mybatis中是使用xml来构造dynamic sql。 当执行分页查询的时候,spring data jpa实际上是调用了两个sql语句,通过count获得总记录数,即当用到Pageable的时候会执行一条count语句,这可能是很昂贵的操作,因为count操作在innodb中要扫描所有的叶子节点,通过limit来获得分页记录mybatis获得总记录数好像并不是通过执行count语句来获得的,可能是通过游标cursor的方式来获得的,通过druid监控,其只执行一条sql语句spring data jpa支持自己来写sql语句,有两种方式:1)@Query或@Modifying配合@Query2)通过entityManager但要注意的是:如果自己写sql语句,那么有些拦截器可能并不能起作用,如@PreUpdate相对来说,mybatis就比较简单,直接在mapper xml中写sql就可以了
参考:JPA -- EntityManager常用API详解EntityManager基本概念基本概念及获得 EntityManager 对象基本概念在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在 JPA 中叫做EntityManager,在MyBatis中叫做SqlSession,通过这个对象来操作数据库。EntityManager是 JPA 中用于增删改查的接口,连接内存中的 java 对象和数据库的数据存储。Hibernate EntityManager是围绕提供JPA编程接口实现的Hibernate Core的一个包装,支持JPA实体实例的生命周期,并允许用标准的Java Persistence查询语言编写查询。EntityManager称为实体管理器,它由EntityManagerFactory所创建。EntityManagerFactory,作为EntityManager的工厂,包含有当前O-R映射的元数据信息,每个EntityManagerFactory,可称为一个持久化单元(PersistenceUnit),每个持久化单元可认为是一个数据源的映射(所谓数据源,可理解为一个数据库,可以在应用服务器中配置多个数据源,同时使用不同的PersistenceUnit来映射这些数据源,从而能够很方便的实现跨越多个数据库之间的事务操作!)PersistenceContext,称为持久化上下文,它一般包含有当前事务范围内的,被管理的实体对象(Entity)的数据。每个EntityManager,都会跟一个PersistenceContext相关联。PersistenceContext中存储的是实体对象的数据,而关系数据库中存储的是记录,EntityManager正是维护这种OR映射的中间者,它可以把数据从数据库中加载到PersistenceContext中,也可以把数据从PersistenceContext中持久化到数据库,EntityManager通过Persist、merge、remove、refresh、flush等操作来操纵PersistenceContext与数据库数据之间的同步!EntityManager是应用程序操纵持久化数据的接口。它的作用与hibernate session类似。为了能够在一个请求周期中使用同一个session对象,在hibernate的解决方案中,提出了currentSession的概念,hibernate中的current session,可以跟JTA事务绑定,也可以跟当前线程绑定。在hibernate中,session管理着所有的持久化对象的数据。而在EJB3中,EntityManager管理着PersistenceContext,PersistenceContext正是被管理的持久化对象的集合。在 Java EE 环境下,一个 JTA 事务通常会横跨多个组件的调用(比如多个 EJB 组件的方法调用)。这些组件需要能够在单个事务范围内访问到同样的PersistenceContext。为了满足这种情况的需要,当EntityManager被注入或通过 JNDI 被查询的时候,它的 PersistenceContext 将会在当前事务范围内自动传播,引用到同一个 Persistence unit 的EntityManager将使用同样的 PersistenceContext。这可以避免在不同的组件之间传递EntityManager引用。通过容器来传递PersistenceContext,而不是应用程序自己来传递EntityManager。这种方式(由容器管理着PersistenceContext,并负责传递到不同的EntityManager)称为容器管理的实体管理器(Container-Managed EntityManager),它的生命周期由容器负责管理,编程人员不需要考虑EntityManger的连接,释放以及复杂的事务问题等。有一种不常见的情况是,应用程序自身需要独立访问PersistenceContext。即每次创建一个EntityManager都会迫使创建一个新的PersistenceContext。这些PersistenceContext即使在同一个事务范围内也不会跟其它EntityManager共享!这个创建过程可以由EntityManagerFactory的createEntityManager方法来创建。这被称为应用管理的实体管理器(application-managed entity manager)。获得EntityManager对象常用方式:SpringBoot容器托管对象方式:依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>对象注入:@Autowired private EntityManager entityManager;实体状态和转换实体状态详解:临时状态实际上就是new了一个普通的 JavaBean 对象。托管状态临时状态在调用 persist() 后,即可将一般的 JavaBean 做为了托管状态的Bean,该Bean的任何属性改动都会牵涉到数据库记录的改动。一旦该记录flush到数据库之后,并且事务提交了,那么此对象不在持久化上下文中,即:变为了游离(没人管的孩子)状态了。在游离状态的时候调用更新、刷新方法后,游离状态对象就变为了在持久化上下文的托管状态了。通过管理器的find方法,将实体从数据库查询出来后,该实体也就变为了托管形态。持久化状态当处在托管状态的实体Bean被管理器flush了,那么就在极短暂的时间进入了持久化状态,事务提交之后,立刻变为了游离状态。可以把持久化状态当做实实在在的数据库记录。游离状态游离状态就是提交到数据库后,事务commit后实体的状态,因为事务已经提交了,此时实体的属性任你如何改变,也不会同步到数据库,因为游离是没人管的孩子,不在持久化上下文中。销毁对象一般要删除一个持久化对象的时候都是先find出来,之后调用remove方法删之,此时这个对象就是销毁对象,实际上就是瞬时对象的另一种形态罢了。常用的 APISELECT、DELETESELECTØ find() :返回指定的 OID 对应的实体类对象,如果这个实体存在于当前的持久化环境,则返回一个被缓存的对象;否则会创建一个新的 Entity, 并加载数据库中相关信息;若 OID 不存在于数据库中,则返回一个 null。Ø getReference()<T> T find(Class<T> entityClass, Object primaryKey); <T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> var3); <T> T find(Class<T> entityClass, Object primaryKey, LockModeType var3); <T> T find(Class<T> entityClass, Object primaryKey, LockModeType var3, Map<String, Object> var4); <T> T getReference(Class<T> entityClass, Object primaryKey); // 参数说明: entityClass // 被查询的实体类类型 primaryKey // 待查找实体的主键值异同:当在数据库中没有找到记录时,find()方法会返回null,而getReference() 方法会抛出javax.persistence.EntityNotFoundException异常,调用getReference()方法,返回的其实并不是实例对象,而是一个代理。当你要使用实体时,才会真正的调用查询语句来查询实例对象另外getReference()方法不保证 entity Bean已被初始化。如果传递进getReference()或find()方法的参数不是实体Bean,都会引发 IllegalArgumentExceptionDELETEØ Remove()void remove(Object entity);如果级联关系cascade=CascadeType.ALL,在删除person 时候,也会把级联对象删除。把cascade属性设为cascade=CascadeType.REMOVE 有同样的效果。Person person = em.find(Person.class, 2); em.remove (person);如果传递进remove ()方法的参数不是实体Bean,会引发一个IllegalArgumentExceptionremove()方法不能移除游离对象,只能移除持久化对象Order order = new Order(); order.setId(140); entityManager.remove(order);上面这段代码会抛出异常,因为order是我们自己创建的对象,也就是游离对象。必须这样写:Order order = new Order(); order = entityManager.find(Order.class,140); entityManager.remove(order);这段代码中的order是从数据库中获取的,也就是持久化对象hibernate的delete()方法,只要对象有Id,就可以删除INSERT、UPDATEINSERTØ persist(): 将临时状态的实体持久化到数据库void persist(Object entity);persist方法:使对象由临时状态变为托管状态。进而变为持久化状态,就是执行INSERT操作。如果传递进persist()方法的参数不是实体Bean,会引发IllegalArgumentException和hibernate的save()方法有些不同:当Entity实体类中设置了主键自动生成时,如果传入对象有id值,则会抛出异常特殊场景及处理方案:特殊场景:Entity上使用自动生成ID值,但有些又需要插入的主键值是指定的ID值,而非自动生成的ID值处理方案1:Bean主键为Null,persist()后自动生成ID值,然后再使用QueryDsl-Jpa的update()方法,根据自动生成的ID值为条件,更新新增的实体的ID值为指定值@Test @Transactional @Rollback(false) public void addByEntityManager(){ User bean = User.builder() .addressee("孙六") .build(); entityManager.persist(u1); QUser entity = QUser.user; long execute = queryFactory.update(entity) .set(entity.id, "11111") .where(entity.id.eq(bean.getId())) .execute(); entityManager.flush(); entityManager.clear(); }处理方案2:使用Querydsl-SQL中,SqlQueryFactory.insert()方法。UPDATEØ 当实体正在被容器管理,即托管状态,你可以调用实体的set方法对数据进行修改,在容器决定flush时(这个由Container自行判断),更新的数据才会同步到数据库,而不是在调用了set方法对数据进行修改后马上同步到数据库。如果希望修改后的数据马上同步到数据库,可以调用 EntityManager.flush() 方法。// 使用示例 @tranational publicvoid updatePerson() { Person person = entityManager.find(Person.class, 1); person.setName("lihuoming"); //方法执行完后即可更新数据 entityManager.merge(person); }Ø Merge<T> T merge(T entity);传入的对象没有id在这种情况下,调用merge方法,将返回一个新的对象(有id),并对这个新的对象执行insert操作。传入的对象有id,entityManager的缓存中没有该对象,数据库中没有该记录:在这种情况下,调用merge方法,将返回一个新的对象,并对该对象执行insert操作。注意:如果Entity的主键设置的是自动生成,则新对象的id并不是原传入对象的id,而是自动生成的(比如自增长的id)。(其实和情况1的结果是一样的)传入的对象有id,entityManager的缓存没有该对象,数据库中有该记录在这种情况下,调用merge方法,将会从数据库中查询对应的记录,生成新的对象,然后将我们传入的对象复制到新的对象,最后执行update操作。简单来说,就是更新操作。传入的对象有id,entityManager的缓存有该对象在这种情况下,调用merge方法,JPA会把传入的对象赋值到entityManager的缓存中的对象,然后对entityManager缓存中的对象执行update操作。(和情况3的结果一样)总结:执行merge时,如果实体ID为空,则进行insert操作。 如果有ID则进行update操作。flush()、clear()flush()将实体的改变立刻刷新到数据库中当EntityManager对象在一个session bean 中使用时,它是和服务器的事务上下文绑定的。EntityManager 在服务器的事务提交时提交并且同步它的内容。在一个session bean 中,服务器的事务默认地会在调用堆栈的最后提交(如:方法的返回)。// 例子1:在方法返回时才提交事务 public void updatePerson(Person person) { try { Person person = em.find(Person.class, 2); person.setName("lihuoming"); em.merge(person); //后面还有众多修改操作 } catch (Exception e) { e.printStackTrace(); //更新将会在这个方法的末尾被提交和刷新到数据库中 }默认只在当事务提交时才将改变更新到数据库中,容器将所有数据库操作集中到一个批处理中,这样就减少了代价昂贵的与数据库的交互。当调用 persist( ),merge( )或 remove( ) 这些方法时,更新并不会立刻同步到数据库中,直到容器决定刷新到数据库中时才会执行,默认情况下,容器决定刷新是在 “相关查询” 执行前或事务提交时发生。当然 “相关查询” 除 find() 和 getreference() 之外,这两个方法是不会引起容器触发刷新动作的,默认的刷新模式是可以改变的。如果你需要在事务提交之前将更新刷新到数据库中,你可以直接地调用EntityManager.flush()方法。ORM框架执行的一些更新数据库的方法,其实质是在更新缓存,只有调用了 flush() 后才会将缓存同步到数据库,即真正执行SQL语句,但是这时并没有真正将数据保存进数据库,需要事务commit后才能全部保存。一般 flush 后立刻就会进行事务的提交。public void updatePerson(Person person) { try { Person person = em.find(Person.class, 2); person.setName("lihuoming"); em.merge(person); em.flush();//手动将更新立刻刷新进数据库 //后面还有众多修改操作 } catch (Exception e) { e.printStackTrace(); }clear()分离所有当前正在被管理的实体1.清除持久上下文环境,断开所有关联的实体。如果这时还有未提交的更新则会被撤消。2.在处理大量实体的时候,如果你不把已经处理过的实体从EntityManager中分离出来,将会消耗你大量的内存。3.调用EntityManager 的clear()方法后,所有正在被管理的实体将会从持久化内容中分离出来。4.有一点需要说明下,在事务没有提交前(事务默认在调用堆栈的最后提交,如:方法的返回),如果调用clear()方法,之前对实体所作的任何改变将会丢失,所以建议在调用clear()方法之前先调用flush()方法保存更改。JcreateQuery() – PQL创建查询对象除了使用 find() 或 getReference() 方法来获得Entity Bean之外,你还可以通过 JPQL 得到实体Bean。要执行 JPQL 语句,你必须通过 EntityManager 的 createQuery() 或 createNamedQuery() 方法创建一个Query 对象。注:JPQL 没有插入语句。即不能执行insert语句。Ø getResultList()Query query = em.createQuery("select p from Person p where p.name=’黎明’"); List result = query.getResultList(); Iterator iterator = result.iterator(); while( iterator.hasNext() ){ //处理Person }Ø getSingleResult()返回查询的第一条数据,可以进行强转,如:// 查询数目: Query query = em.createQuery("select count(1) from Person p"); Long num = (Long)query. getSingleResult (); // 强转为实体: Query query = em.createQuery("select p from Person p"); User user = (User)query. getSingleResult ();Ø executeUpdate()// 执行更新和删除操作,返回受影响的记录数。 Query query = em.createQuery("delete from Person"); int result =query.executeUpdate(); //影响的记录数Ø 关于 JPQL 和SQL 中参数的问题:使用标识符Query query =em.createQuery("delete from Person p where p.name := name"); query.setParameter("name","张三"); int result =query.executeUpdate(); //影响的记录数使用索引下标Query query =em.createQuery("delete from Person p where p.id = ?1 "); query.setParameter(1, "张三"); int result =query.executeUpdate(); //影响的记录数 createNaiveQuery() – SQL用法基本同createQuery(),只不过这里使用的不是 JPQL 而是SQLØ 将查询到的数据映射成实体:Query query = em.createNativeQuery("select * from person", Person.class); List result = query.getResultList(); if (result != null){ Iterator iterator = result.iterator(); while( iterator.hasNext() ){ Person person= (Person)iterator.next(); }refresh()如果怀疑当前被管理的实体已经不是数据库中最新的数据,则可以通过 refresh() 方法刷新实体,容器会把数据库中的新值重写进实体。这种情况一般发生在获取了实体之后,有人更新了数据库中的记录,这时需要得到最新的数据。当然再次调用 find() 或 getReference() 方法也可以得到最新数据,但这种做法并不优雅。User user = em.find(User.class, 1); //第二次同样的查询不会访问数据库 user = em.find(User.class, 1); // 运行以上代码,发现调用了两次find,但是只执行了一次select语句,这是缓存导致的。 // 执行refresh()方法刷新缓存,容器会把数据库中的新值重写进实体。 User user = em.find(User.class, 1); em.refresh(user);其他方法contains()判断实体是否还在EntityManage的管理下,或者说是否属于当前持久上下文环境。contains() 方法使用一个实体作为参数,如果这个实体对象当前正被持久化内容管理,返回值为true,否则为false。如果传递的参数不是实体 Bean,将会引发一个IllegalArgumentException.User user = em.find(User.class, 1); if (em.contains(user)){ //正在被持久化内容管理 }else{ //已经不受持久化内容管理 }getFlushMode ():获取持久上下文环境的Flush模式。返回FlushModeType类的枚举值。setFlushMode()改变实体管理器的Flush模式setFlushMode()的Flush模式有2种类型:AUTO 和 COMMIT。AUTO为默认模式。// 改变实体管理器的Flush模式 em.setFlushMode(FlushModeType.COMMIT);FlushModeType.AUTO默认情况下除了在事务提交时 flush,在进行查询时(除了 find() 和 getreference() 查询)也会进行一次 flush ,比如使用JPQL查询前会进行一次flush。使用场合:在大量更新数据的过程中没有任何查询语句(除了 find() 和 getreference() 查询)的执行。FlushModeType.COMMIT刷新只有在事务提交时才发生,查询不触发。使用场合:在大量更新数据的过程中存在查询语句(除了find() 和 getreference() 查询)的执行。其实上面两种模式最终反映的结果是:JDBC 驱动跟数据库交互的次数。JDBC 性能最大的增进是减少JDBC 驱动与数据库之间的网络通讯。FlushModeType.COMMIT 模式使更新只在一次的网络交互中完成,而 FlushModeType.AUTO 模式可能需要多次交互(触发了多少次Flush 就产生了多少次网络交互)isOpen()判断当前的实体管理器是否是打开状态close()关闭实体管理器。之后若调用实体管理器实例的方法或其派生的查询对象的方法都将抛出 IllegalstateException 异常,除了 getTransaction 和 isOpen方法(返回 false)。不过,当与实体管理器关联的事务处于活动状态时,调用 close() 方法后持久上下文将仍处于被管理状态,直到事务完成。getTransaction()返回资源层的事务对象。EntityTransaction实例可以用于开始和提交多个事务EntityTransaction 接口用来管理资源层实体管理器的事务操作通过调用实体管理器的getTransaction方法 获得其实例。① begin ()用于启动一个事务,此后的多个数据库操作将作为整体被提交或撤消。若这时事务已启动则会抛出 IllegalStateException 异常。② commit () 用于提交当前事务。即将事务启动以后的所有数据库更新操作持久化至数据库中。③ rollback () 撤消(回滚)当前事务。即撤消事务启动后的所有数据库更新操作,从而不对数据库产生影响。④ setRollbackOnly () 使当前事务只能被撤消。⑤ getRollbackOnly () 查看当前事务是否设置了只能撤消标志。⑥ isActive () 查看当前事务是否是活动的。如果返回true则不能调用begin方法,否则将抛出 IllegalStateException 异常;如果返回 false 则不能调用 commit、rollback、setRollbackOnly 及 getRollbackOnly 方法,否则将抛出 IllegalStateException 异常。需要注意的是:// 在JPA里面,先需要 getTransaction,再 begin EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); // 在 hibernate 里面呢,直接 begin,然后进行 commit EntityTransaction transaction = session.beginTransaction(); transaction.commit();JPA 调用存储过程参考:https://www.cnblogs.com/zhuang229/p/12227938.html定义存储过程及调用方式定义一个简单的存储过程传入一个int参数,返回这个参数+1CREATE DEFINER=`root`@`localhost` PROCEDURE `plus1inout`(IN ARG INT, OUT res INT) BEGIN SET res = ARG + 1; END注意:IN参数个数没有限制。如果out参数类型为sys_refcursor,那么最好只定义这 一个out参数(JPA API限制);sys_refcursor 类型的 out 参数,在 JPA 中统一注册为 Void.class 类型,参数模式定义为 ParameterMode.REF_CURSOR;使用getResultList()方法获取游标fetch到的多行数据,返回结果为List<Object[]>,一个Object[]对应一行数据。如果使用Oracle 存储包,只需在定义存储过程名字时加个对应的package名前缀即可(例如:包名.存储过程名)。JPA调用存储过程的两种方式基于Entity实体类,在实体类上使用@NamedStoredProcedureQuery注解(需要数据库中有对应的表,可自动映射结果集)EntityManager创建createNamedStoredProcedureQuery,传参调用SpringDataJpa中Repository自定义方法,传参调用直接使用EntityManager进行自定义(不需要数据库中有对应的表,需要自己处理结果集)EntityManager创建StoredProcedureQuery对象,注册 IN/OUT 参数模式实体类(使用注解声明存储过程)@NamedStoredProcedureQuery注解(解析详见注解目录)name为唯一名称,调用时使用;procedureName 为存储过程名;@Entity注解必须有@Entity要求必须有主键id属性(存储过程可返回id任意值即可)@Entity要求必须对应数据库表必须存在(JPA表检查用)(若用EntityManager调用存储过程,此条存疑)import lombok.Data; import javax.persistence.*; @Data @Entity // 存储过程使用了注解@NamedStoredProcedureQuery,并绑定到一个随意一个JPA表 @NamedStoredProcedureQueries({ @NamedStoredProcedureQuery( name = "User.plus1", procedureName = "plus1inout", parameters = { @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class), @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) @NamedStoredProcedureQuery( name = "User.mytest", procedureName = "mytest") }) class user{ private Integer id; private String name; private String age; }EntityManager直接调用存储过程@Autowired private EntityManager em; @Test public void test01(){ StoredProcedureQuery query = em .createStoredProcedureQuery("plus1inout") // 创建StoredProcedureQuery对象,传入被调用的存储过程名称 .registerStoredProcedureParameter("ARG", Integer.class, ParameterMode.IN) // 注册参数 .registerStoredProcedureParameter("res", Integer.class, ParameterMode.OUT) .setParameter("ARG", 20); // 传参 query.execute(); // 执行存储过程调用 String result = query.getOutputParameterValue("res").toString(); // 获取存储过程中的返回值 System.out.println(result); }调用基于实体类注解的存储过程使用EntityManager调用基于Entity实体类注解的存储过程。实体类详见 JPA调用存储过程@Autowired private EntityManager em; @Test public void test02(){ StoredProcedureQuery query = em // 创建NamedStoredProcedureQuery对象,传入实体类上@NamedStoredProcedureQuery注解中name的值 .createNamedStoredProcedureQuery("proKQAttendanceRecord") // IN模式的参数可以在实体类上注解,此处相应注释掉 // .registerStoredProcedureParameter("PRM_ID", Integer.class, ParameterMode.IN) // 使用EntityManager调用基于Entity实体类注解的存储过程时,OUT或INOUT模式的参数,必须要在此处注册,并删掉原实体类上相应的参数注解。不然运行报错。 .registerStoredProcedureParameter("PRM_APPCODE", Integer.class, ParameterMode.OUT) .registerStoredProcedureParameter("PRM_ERRMSGE", Integer.class, ParameterMode.OUT) .setParameter("PRM_ID", attId); // 传参 query.execute(); List resultList = query.getResultList(); Object code = query.getOutputParameterValue("PRM_APPCODE"); Object msg = query.getOutputParameterValue("PRM_ERRMSGE");
概述JPA官方文档:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#prefaceJPA(Java Persistence API)是 Java 标准中的一套ORM规范(提供了一些编程的 API 接口,具体实现由 ORM 厂商实现,如Hiernate、TopLink 、Eclipselink等都是 JPA 的具体实现),借助 JPA 技术可以通过注解或者XML描述【对象-关系表】之间的映射关系,并将实体对象持久化到数据库中(即Object Model与Data Model间的映射)。JPA是Java持久层API,由Sun公司开发,希望规范、简化Java对象的持久化工作,整合ORM技术,整合第三方ORM框架,建立一种标准的方式,目前也是在按照这个方向发展,但是还没能完全实现。在ORM框架中,Hibernate框架做了较好的 JPA 实现,已获得Sun的兼容认证。JPA 的优势:1.开发者面向 JPA 规范的接口,但底层的JPA实现可以任意切换:觉得Hibernate好的,可以选择Hibernate JPA实现;觉得TopLink好的,可以选择TopLink JPA实现。2.开发者可以避免为使用Hibernate学习一套ORM框架,为使用TopLink又要再学习一套ORM框架。JPA为我们提供了以下规范:ORM映射元数据:JPA支持XML和注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中JPA的Criteria API:提供API来操作实体对象,执行CRUD操作,框架会自动将之转换为对应的SQL,使开发者从繁琐的 JDBC、SQL中解放出来。JPQL查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。Hibernate介绍Hibernate对数据库结构提供了较为完整的封装,Hibernate的O/R Mapping实现了POJO 和数据库表之间的映射,以及SQL 的自动生成和执行。往往只需定义好了POJO 到数据库表的映射关系,即可通过Hibernate 提供的方法完成持久层操作。甚至不需要对SQL 的熟练掌握, Hibernate/OJB 会根据制定的存储逻辑,自动生成对应的SQL 并调用JDBC 接口加以执行。Hibernate框架(3.2及以上版本)对JPA接口规范做了较好的实现,主要是通过以下三个组件来实现的:hibernate-annotation:是Hibernate支持annotation方式配置的基础,它包括了标准的 JPA annotation以及Hibernate自身特殊功能的annotation。hibernate-core:是Hibernate的核心实现,提供了Hibernate所有的核心功能。hibernate-entitymanager:实现了标准的 JPA,可以把它看成hibernate-core和 JPA 之间的适配器,它并不直接提供ORM的功能,而是对hibernate-core进行封装,使得Hibernate符合 JPA 的规范。hibernate对 JPA 的支持,不是另提供了一套专用于 JPA 的注解。一些重要的注解如@Column, @OneToMany等,hibernate并没有提供,这说明 JPA 的注解已经是hibernate 的核心,hibernate只提供了一些补充,而不是两套注解。JPA 和hibernate都提供了的注解(例如@Entity),若 JPA 的注解够用,就直接用,若 JPA 的注解不够用,直接使用hibernate的即可。Spring Data JPA介绍Spring Data JPA是在实现了 JPA 规范的基础上封装的一套 JPA 应用框架(Criteria API还是有些复杂)。虽然ORM框架都实现了 JPA 规范,但是在不同的ORM框架之间切换仍然需要编写不同的代码,而使用Spring Data JPA能够方便的在不同的ORM框架之间进行切换而不需要更改代码。Spring Data JPA旨在通过统一ORM框架的访问持久层的操作,来提高开发人的效率。Spring Data JPA是一个 JPA 数据访问抽象。也就是说Spring Data JPA不是一个实现或 JPA 提供的程序,它只是一个抽象层,主要用于减少为各种持久层存储实现数据访问层所需的样板代码量。但是它还是需要JPA提供实现程序,其实Spring Data JPA底层就是使用的 Hibernate实现。Spring Data JPA 其实并不依赖于 Spring 框架。Spring Data JPA 通过Repository来支持上述功能,默认提供的几种Repository已经满足了绝大多数需求:JpaRepository( 为Repository的子接口:JpaRepository -> PagingAndSortingRepository -> CrudRepository -> Repository)QueryByExampleExecutorJpaSpecificationExecutorQuerydslPredicateExecutor后三者用于更复杂的查询,如动态查询、关联查询等;第一种用得最多,提供基于方法名(query method)的查询,用户可基于第一种继承创建自己的子接口(只要是Repository的子接口即可),并声明各种基于方法名的查询方法。Repository 的实现类:SimpleJpaRepositoryQueryDslJpaRepositoryJPA与springDataJpa、Hibernate 之间的关系Querydsl-JPA 介绍Springdata-JPA是对 JPA 使用的封装,Querydsl-JPA也是基于各种ORM之上的一个通用查询框架,使用它的API类库可以写出“Java代码的sql”,不用去手动接触sql语句,表达含义却如sql般准确。更重要的一点,它能够构建类型安全的查询,这比起 JPA 使用原生查询时有很大的不同,可以不必再对恶心的“Object[]”进行操作了。SpringDataJPA + Querydsl-JPA联合使用方案是使用 JPA 操作数据库的最佳方案,它们之间有着完美的相互支持,以达到更高效的编码。JPA 注解Entity 常用注解参考:JPA & Spring Data JPA学习与使用小记指定对象与数据库字段映射时注解的位置:如@Id、@Column等注解指定Entity的字段与数据库字段对应关系时,注解的位置可以在Field(属性)或Property(属性的get方法上),两者统一用其中一种,不能两者均有。推荐用前者。@Entity、@Table@Entity(必需)标注在实体类上。映射实体类。指出该 Java 类为实体类,将映射到指定的关系数据库表。应用了此注解后,将会自动将类名映射作为数据库表名、将类内的字段名映射为数据库表的列名。映射策略默认是按驼峰命名法拆分将类名或字段名拆分成多部分,然后以下划线连接,如StudentEntity -> student_entity、studentName -> student_name。若不按默认映射,则可通过@Table、@Column指定,见下面。@Table(可选)标注在实体类上。映射数据库表名。当实体类与其映射的数据库表名不同名时需要使用 @Table 标注说明,该标注与 @Entity 标注并列使用schema属性:指定数据库名name属性:指定表名,不指定时默认按驼峰命名法拆分将类名,并以下划线连接@DynamicInsert、@DynamicUpdate@DynamicInsert(可选)标注在实体类上。设置为true,表示insert对象的时候,生成动态的insert语句,如果这个字段的值是null就不会加入到insert语句中,默认false。@DynamicUpdate(可选)标注在实体类上。设置为true,表示执行update对象时,在生成动态的update语句前,会先查询该表在数据库中的字段值,并对比更新使用的对象中的字段值与数据库中的字段值是否相同,若相同(即该值没有修改),则该字段就不会被加入到update语句中。默认false,表示无论更新使用实体类中的字段值与数据库中的字段值是否一致,都加入到update语句中,即都使用对象中所有字段的值覆盖数据库中的字段值。比如只想更新某个属性,但是却把整个属性都更改了,这并不是我们希望的结果,我们希望的结果是:我更改了哪写字段,只要更新我修改的字段就够了。注意:@DynamicUpdate的动态更新的含义是,比较更新要使用的实体类中的所有字段值与从数据库中查询出来的所有字段值,判断其是否有修改,不同则加入到update语句中更新字段值。看这个例子,数据库中id=1的记录所有字段都是非空的,但是实体类中只有name有值,也就是所有字段都变了,只是其他字段被更新为了新的空值。所以 jpa 更新数据库字段值,无论是否有 @DynamicUpdate注解,均需要手动先select对象,然后通过set更新对象的属性值,然后再save对象,实现更新操作@Id、@GeneratedValue@Id(必需)标注在实体类成员变量或getter方法之上。映射生成主键。用于声明一个实体类的属性映射为数据库的一个主键列。若同时指定了下面的@GeneratedValue则存储时会自动生成主键值,否则在存入前用户需要手动为实体赋一个主键值。主键值类型可以是:Primitive types:boolean, byte, short, char, int, long, float, doubleEquivalent wrapper classes from package java.lang:Byte,Short, Character, Integer, Long, Float, Doublejava.math.BigInteger, java.math.BigDecimaljava.lang.Stringjava.util.Date, java.sql.Date, java.sql.Time, java.sql.TimestampAny enum typeReference to an entity objectcomposite of several keys above指定联合主键,有@IdClass、@EmbeddedId两种方法。@GeneratedValue@GeneratedValue 用于标注主键的生成策略,通过 strategy 属性指定。默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer 对应 identity,MySQL 对应 auto incrementAUTO: JPA自动选择合适的策略,是默认选项IDENTITY:采用数据库 ID自增长的方式来自增主键字段,Oracle 不支持这种方式TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。SEQUENCE:通过序列产生主键,通过 @SequenceGenerator 注解指定序列名,MySql 不支持这种方式@Column、@Basic、@Transient@Column(可选)标注在实体类成员变量或getter方法之上。映射表格列。当实体的属性与其映射的数据库表的列不同名时需要使用 @Column 标注说明。类的字段名在数据库中对应的字段名可以通过此注解的name属性指定,不指定则默认为将属性名按驼峰命名法拆分并以下划线连接,如 createTime 对应 create_time。注意:即使name的值中包含大写字母,对应到db后也会转成小写,如@ Column(name="create_Time") 在数据库中字段名仍为create_time。可通过SpringBoot配置参数【spring.jpa.hibernate.naming.physical-strategy】配置对应策略,如指定name值是什么,数据库中就对应什么名字的列名。默认值为:【org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy】@Column注解一共有10个属性,这10个属性均为可选属性,各属性含义分别如下:name:定义了被标注字段在数据库表中所对应字段的名称。unique:该字段是否为唯一标识,默认为false。也可以使用@Table标记中的@UniqueConstraint。nullable:该字段是否可以为null值,默认为true。length:字段的长度,当字段的类型为varchar时,该属性才有效,默认为255个字符。insertable:在使用“INSERT”脚本插入数据时,是否插入该字段的值。默认为true。updatable:在使用“UPDATE”脚本插入数据时,是否更新该字段的值。默认为true。insertable = false 和updatable = false 一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。columnDefinition:建表时创建该字段的DDL语句,一般用于通过Entity生成表定义时使用。(如果DB中表已经建好,该属性没有必要使用)precision和scale:表示精度,当字段类型为double时,precision表示数值的总长度,scale表示小数点所占的位数。table:定义了包含当前字段的表名。@Basic(可选)表示一个简单的属性到数据表的字段的映射,对于没有任何标注的属性或getter方法,默认标注为 @Basicfetch 表示属性的读取策略,有 EAGER 和 LAZY 两种,分别为立即加载和延迟加载optional 表示该属性是否允许为 null,默认为 true@Transient:忽略属性定义暂态属性。表示该属性并非一个到数据库表的字段的映射,ORM 框架将忽略该属性。如果一个属性并非数据库表的字段映射,就必须将其标识为 @Transient,否则ORM 框架默认为其注解 @Basic,例如工具方法不需要映射。@Temporal:时间日期精度标注在实体类成员变量或getter方法之上。可选。在 JavaAPI 中没有定义 Date 类型的精度,而在数据库中表示 Date 类型的数据类型有 Date(年月日),Time(时分秒),TimeStamp(年月日时分秒) 三种精度,进行属性映射的时候可以使用 @Temporal 注解调整精度。目前此注解只能用于修饰JavaAPI中的【java.util.Date】、【java.util.Calendar】类型的变量,TemporalType 取 DATE、TIME、TIMESTAMP 时在MySQL中分别对应的 DATE、TIME、DATETIME 类型。示例: @Temporal(TemporalType.TIMESTAMP) @CreationTimestamp //org.hibernate.annotations.CreationTimestamp,用于在JPA执行insert操作时自动更新该字段值 @Column(name = "create_time", updatable=false ) //为防止手动set,可设false以免该字段被更新 private Date createTime; @Temporal(TemporalType.TIMESTAMP) @UpdateTimestamp //org.hibernate.annotations.UpdateTimestamp,用于在JPA执行update操作时自动更新该字段值 private Date updateTime;时间日期自动更新:1、Hibernate的注解: @CreationTimestamp(创建时间)、@UpdateTimestamp(更新时间) 用法:在时间日期类型属性上加上注解即可2、SpringDataJPA的注解:(可参阅https://blog.csdn.net/tianyaleixiaowu/article/details/77931903) @CreatedDate(创建时间)、@LastModifiedDate(更新时间)、@CreatedBy、@LastModifiedBy用法:在实体类加上注解 @EntityListeners(AuditingEntityListener.class)在启动类上加上注解 @EnableJpaAuditing在实体类中属性中加上面四种注解示例:@Data @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { @GeneratedValue(strategy = GenerationType.AUTO) protected Integer id; // 创建时间 @CreatedDate @Column(name = "create_time", updatable=false ) //为防止手动set,可设false以免该字段被更新 private Long createTime; // 更新时间 @LastModifiedDate @Column(name = "update_time") private Long updateTime; }其他注解@MappedSuperClass:共有字段超类标注在实体类上。共有字段超类中声明了各Entity共有的字段,即数据库中多表中共有的字段,如create_time、update_time、id等。标注为@MappedSuperclass的类将不是一个完整的实体类,将不会映射到数据库表,但是其属性都将映射到其子类的数据库字段中。标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。允许多级继承注解的类继承另一个实体类或标注@MappedSuperclass类,可以使用 @AttributeOverride 或 @AttributeOverrides 注解重定义其父类属性映射到数据库表中字段。@IdClass:指定联合主键类标注在实体类上。指定联合主键类。如:@IdClass(StudentExperimentEntityPK.class)主键类StudentExperimentEntityPK需要满足:实现Serializable接口有默认的public无参数的构造方法重写equals和hashCode方法。equals方法用于判断两个对象是否相同,EntityManger通过find方法来查找Entity时,是根据equals的返回值来判断的。hashCode方法返回当前对象的哈希码它的类型和名称必须与使用 @Id 进行标注的实体主键字段的类型和名称一致。示例:/** * 实体类 @Data @Entity @Table(name = "customer_course") @IdClass(CustomerCourseEntityPK.class) // 指定联合主键类 public class CustomerCourseEntity { @Column(name = "customer_id", length = ColumnLengthConstrain.LEN_ID_MAX) private String customerId; @Column(name = "course_id", length = ColumnLengthConstrain.LEN_ID_MAX) private String courseId; @Column(name = "max_number") private Integer maxNumber; @ManyToOne @JoinColumn(name = "course_id", referencedColumnName = "id", nullable = false, insertable = false, updatable = false) private CourseEntity courseByCourseId; * 联合主键类 @Data public class CustomerCourseEntityPK implements Serializable { private static final long serialVersionUID = 1L; private String customerId; private String courseId; }@EmbeddedId:联合主键标注在实体类成员变量或getter方法上。功能与@IdClass一样用于指定联合主键。不同的是其标注在实体内的主键类变量上,且主键类应该标注 @Embeddable 注解。此外在主键类内指定的字段在实体类内可以不再指定,若再指定则需为@Column加上insertable = false, updatable = false属性示例:@Data @Entity @Table(name = "customer_course") @IdClass(CustomerCourseEntityPK.class) // 指定联合主键类 public class CustomerCourseEntity { @EmbeddedId private CustomerCourseEntityPK id; @Column(name = "max_number") private Integer maxNumber; * 联合主键类 @Data @Embeddable public class CustomerCourseEntityPK implements Serializable { private static final long serialVersionUID = 1L; @Column(name = "customer_id", length = ColumnLengthConstrain.LEN_ID_MAX) private String customerId; @Column(name = "course_id", length = ColumnLengthConstrain.LEN_ID_MAX) private String courseId; }@Inheritance:表结构复用标注在实体类上。用于表结构复用。指定被该注解修饰的类被子类继承后子类和父类的表结构的关系。通过strategy属性指定关系,有三种策略:SINGLE_TABLE:适用于共同字段多独有字段少的关联关系定义。子类和父类对应同一个表且所有字段在一个表中,还会自动生成(也可通过 @DiscriminatorColumn 指定)一个字段 varchar 'dtype' 用来表示一条数据是属于哪个实体的。为默认值。(未使用@Inheritance或使用了但没指定strategy属性时默认采用此策略)。JOINED:子类和父类对应不同表,父类属性对应的列(除了主键)不会且无法再出现在子表中。子表自动产生与父表主键对应的外键与父表关联。同样地也可通过@DiscriminatorColumn为父类指定一个字段用于标识一条记录属于哪个子类。TABLE_PER_CLASS:子类和父类对应不同表且各类自己的所有字段(包括继承的)分别都出现在各自的表中;表间没有任何外键关联。此策略最终效果与@MappedSuperClass等同。@Inheritance与@MappedSuperclass的区别:@MappedSuperclass子类与父类没有外键关系、不会对应同一个表@Inheritance适用于表关联后者适用于定义公共字段两者是可以混合使用@Inheritance、@MappedSuperClass可用于定义Inheritance关系。这些方式的一个缺点是子类中无法覆盖从父类继承的字段的定义(如父类中name是not null,但子类中允许为null)。除了 @Inheritance、@MappedSuperClass外,还有一种Inheritance方法(此法可解决上述不足):先定义一个Java POJO(干净的POJO,没有任何对该类使用任何的ORM注解),然后不同子类继承该父类,并分别在不同子类中进行ORM定义即可。此法下不同子类拥有父类的公共字段且该字段在不同子类中对应的数据库列定义可不同。@Embedded、@Embeddable 当一个实体类要在多个不同的实体类中进行使用,而其不需要生成数据库表 @Embeddable:标注在类上,表示此类是可以被其他类嵌套 @Embedded:标注在属性上,表示嵌套被@Embeddable注解的同类型类@Enumerated:映射枚举 使用此注解映射枚举字段,以String类型存入数据库 注入数据库的类型有两种:EnumType.ORDINAL(Interger)、EnumType.STRING(String)@TableGenerator:主键值生成器TableGenerator定义一个主键值生成器,在 @GeneratedValue的属性strategy = GenerationType.TABLE时,generator属性中可以使用生成器的名字。生成器可以在类、方法或者属性上定义。 生成器是为多个实体类提供连续的ID值的表,每一行为一个类提供ID值,ID值通常是整数。属性说明:name:生成器的唯一名字,可以被Id元数据使用。table:生成器用来存储id值的Table定义。pkColumnName:生成器表里用来保存主键名字的字段valueColumnName:生成器表里用来保存主键值的字段pkColumnValue:生成器表里用来保存主键名字的字段的值initialValue:id值的初始值。allocationSize:id值的增量示例:@Entity public class Employee { @Column(name = "id") @TableGenerator(name = "hf_opert_id_gen", // 此处的名字要和下面generator属性值一致 table = "mcs_hibernate_seq", // 主键保存到数据库的表名 pkColumnName = "sequence_name", // 表里用来保存主键名字的字段 valueColumnName = "sequence_next_hi_value", // 表里用来保存主键值的字段 pkColumnValue = "user_id", // 表里名字字段对应的值 allocationSize = 1) // 自动增长,设置为1 @GeneratedValue(strategy = GenerationType.TABLE, generator = "hf_opert_id_gen") private Integer id; }@JoinColumn、@JoinColumns@JoinColumn:指定外键如果在实体类的某个属性上定义了联表关系(OneToOne或OneTOMany等),则使用@JoinColumn注解来定义关系的属性。JoinColumn的大部分属性和Column类似。属性说明:name:主表的列名。若不指定,默认为 关联表的名称 + “_” + 关联表主键的字段名,例如 address_idreferencedColumnName:关联表作为外键的列名。若不指定,默认为关联表的主键作为外键。unique:是否唯一 ,默认falsenullable:是否允许为空,默认trueinsertable:是否允许插入,默认trueupdatable:是否允许更新,默认truecolumnDefinition:定义建表时创建此列的DDLsecondaryTable:从表名。如果此列不建在主表上(默认建在主表),该属性定义该列所在从表的名字。foreignKey():外键。默认@ForeignKey(ConstraintMode.PROVIDER_DEFAULT);@Data @Entity public class Person { // Person和Address是一对一关系。Address表中名为id_address的列作为外键指向Person表中名为address_id的列 @OneToOne @JoinColumn(name="address_id", referencedColumnName="id_address", unique=true) private Address address; @Data @Entity public class Address { @column(name ="id_address") private Integer idAddress; }@JoinColumns如果在实体类的某个属性上定义了联表关系(OneToOne或OneTOMany等),并且关系存在多个JoinColumn,则使用@JoinColumns注解定义多个JoinColumn的属性。属性说明:value:定义JoinColumn数组,指定每个JoinColumn的属性@Data @Entity public class Custom { // Custom和Order是一对一关系。Order表中一个名为CUST_ID的列作为外键指向Custom对应表中名为ID_CUST的列,另一名为CUST_NAME的列作为外键指向Custom对应表中名为NAME_CUST的列 @OneToOne @JoinColumns({ @JoinColumn(name="CUST_ID", referencedColumnName="ID_CUST"), @JoinColumn(name="CUST_NAME", referencedColumnName="NAME_CUST") private Order order; }@OneToOne、@OneToMany@OneToOne描述一个 一对一的关联属性说明:fetch:表示抓取策略,默认为FetchType.LAZYcascade:表示级联操作策略。CascadeType.ALL,当前类增删改查改变之后,关联类跟着增删改查。@Data @Entity public class Person { // Person和Address是一对一关系。Address表中名为id_address的列作为外键指向Person表中名为address_id的列 @OneToOne @JoinColumn(name="address_id", referencedColumnName="id_address", unique=true) private Address address; }@OneToMany描述一个 一对多的关联,该属性应该为集体类型,在数据库中并没有实际字段。属性说明:fetch:表示抓取策略,默认为FetchType.LAZY,因为关联的多个对象通常不必从数据库预先读取到内存cascade:表示级联操作策略,对于OneToMany类型的关联非常重要,通常该实体更新或删除时,其关联的实体也应当被更新或删除例如:实体User和Order是OneToMany的关系,则实体User被删除时,其关联的实体Order也应该被全部删除@ManyToOne、@ManyToMany@ManyToOne表示一个多对一的映射,该注解标注的属性通常是数据库表的外键属性说明:optional:是否允许该字段为null,该属性应该根据数据库表的外键约束来确定,默认为truefetch:表示抓取策略,默认为FetchType.EAGERcascade:表示默认的级联操作策略,可以指定为ALL,PERSIST,MERGE,REFRESH和REMOVE中的若干组合,默认为无级联操作targetEntity:表示该属性关联的实体类型。该属性通常不必指定,ORM框架根据属性类型自动判断 targetEntity@ManyToMany描述一个多对多的关联。多对多关联上是两个一对多关联,但是在ManyToMany描述中,中间表是由ORM框架自动处理属性说明:targetEntity:表示多对多关联的另一个实体类的全名,例如:package.Book.classmappedBy:表示多对多关联的另一个实体类的对应集合属性名称两个实体间相互关联的属性必须标记为@ManyToMany,并相互指定targetEntity属性, 需要注意的是,有且只有一个实体的@ManyToMany注解需要指定mappedBy属性,指向targetEntity的集合属性名称,利用ORM工具自动生成的表除了User和Book表外,还自动生成了一个User_Book表,用于实现多对多关联@NamedStoredProcedureQuery定义在一个实体上面声明存储过程。有多个存储过程,可以用@NamedStoredProcedureQueries。name:自定义存储过程在java中的唯一别名,调用时使用;procedureName:数据库中的存储过程名;parameters:存储过程的参数@StoredProcedureParameter:定义存储过程的参数属性name:参数名。和数据库里的参数名字一样mode:参数模式。ParameterMode.IN、OUT、INOUT、REF_CURSORtype:参数数据类型。String.class、Integer.class,Long.class等@JsonFormat、@DateTimeFormat@JsonFormat后端到前端的时间格式的转换。注意:该注解并非 JPA 注解。// 出参时间格式化 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime;也可以在配置文件中配置进行时间戳统一转换:spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8@DateTimeFormat前端到后端的时间格式的转换。注意:该注解并非 JPA 注解。// 入参出参时间格式化。请求报文只需要传入"yyyy-MM-dd HH:mm:ss"格式字符串进来,则自动转换为Date类型数据 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private Date createTime;主键生成策略1. 通用策略参考:https://blog.csdn.net/chenlong220192/article/details/46678461通过annotation来映射hibernate实体,基于annotation的hibernate主键标识@Id,由@GeneratedValue设定其生成规则。GeneratedValue注解用于标注主键的生成策略,通过 strategy 属性指定。默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer对应identity,MySQL 对应 auto increment 。源码定义:@Target({METHOD,FIELD}) @Retention(RUNTIME) public @interface GeneratedValue{ GenerationType strategy() default AUTO; String generator() default ""; } 其中GenerationType:public enum GenerationType{ TABLE, SEQUENCE, IDENTITY, } JPA提供的四种标准用法为:AUTO:JPA自动选择合适的策略,是默认选项IDENTITY:采用数据库ID自增长的方式来自增主键字段。Oracle 不支持这种方式TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植SEQUENCE:通过序列产生主键,通过@SequenceGenerator 注解指定序列名。MySql不支持这种方式1、AUTO 用法// 使用示例 @GeneratedValue(strategy = GenerationType.AUTO) 在指定主键时,如果不指定主键生成策略,默认为AUTO。// 使用示例 @Id 此时主键生成策略,为默认值AUTO。以下指定主键生成策略为AUTO,效果同上:// 使用示例 @GeneratedValue(strategy = GenerationType.AUTO) 2、IDENTITY 用法// 使用示例 @GeneratedValue(strategy = GenerationType.IDENTITY) 3、TABLE 用法// 使用示例。 @GeneratedValue(strategy = GenerationType.TABLE, generator="pk_gen") @TableGenerator( name = "pk_gen", table="tb_generator", pkColumnName="gen_name", valueColumnName="gen_value", pkColumnValue="PAYABLEMOENY_PK", allocationSize=1 ) 使用此策略需要数据库存在相应的表。此处应用表 tb_generator,定义为:CREATE TABLE tb_generator ( id NUMBER NOT NULL, gen_name VARCHAR2(255) NOT NULL, gen_value NUMBER NOT NULL, PRIMARY KEY(id) ) 插入纪录,供生成主键使用INSERT INTO tb_generator(id, gen_name, gen_value) VALUES (1,PAYABLEMOENY_PK', 1); 在主键生成后,这条纪录的value值,按allocationSize递增。// @TableGenerator的源码定义: @Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface TableGenerator { String name(); String table() default ""; String catalog() default ""; String schema() default ""; String pkColumnName() default ""; String valueColumnName() default ""; String pkColumnValue() default ""; int initialValue() default 0; int allocationSize() default 50; UniqueConstraint[] uniqueConstraints() default {}; } 以上属性说明如下:name 表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中;table 表示表生成策略所持久化的表名,例如,这里表使用的是数据库中的“tb_generator”;catalog 属性和 schema属性具体指定表所在的目录名或是数据库模式名;pkColumnName 属性的值表示在持久化表中,该主键生成策略所对应键值的名称。例如在“tb_generator”中将“gen_name”作为数据库表中主键的键值对的名称;valueColumnName 属性的值表示在持久化表中,该主键当前所生成的值,它的值将会随着每次创建累加。例如,在“tb_generator”中将“gen_value”作为数据库表中主键的键值对的键值;pkColumnValue 属性的值表示在持久化表中,该生成策略所对应的主键。例如在“tb_generator”表中,将“gen_name”的值为“CUSTOMER_PK”;initialValue 表示主键初始值,默认为0;allocationSize 表示每次主键值增加的大小。例如设置成1,则表示每次创建新记录后自动加1,默认为50;UniqueConstraint 与@Table标记中的用法类似;4、SEQUENCE 用法// 使用示例 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="aaa") @SequenceGenerator(name="aaa", sequenceName="seq_payment") @SequenceGenerator 源码定义:@Target({TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface SequenceGenerator { String name(); String sequenceName() default ""; int initialValue() default 0; int allocationSize() default 50; } 以上属性说明如下:name 表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中。sequenceName 表示生成策略用到的数据库序列名称;initialValue 表示主键初识值,默认为0;allocationSize 表示每次主键值增加的大小。例如设置成1,则表示每次创建新记录后自动加1,默认为50;2. hibernate策略hibernate提供多种主键生成策略,有点是类似于JPA,基于Annotation的方式通过@GenericGenerator实现以下是hibernate特有的:native:对于 oracle 采用 Sequence 方式,对于MySQL 和 SQL Server 采用identity(自增主键生成机制),native就是将主键的生成工作交由数据库完成,hibernate不管(常用);uuid:采用128位的uuid算法生成主键,uuid被编码为一个32位16进制数字的字符串,占用空间大(字符串类型);hilo:使用hilo生成策略,要在数据库中建立一张额外的表,默认表名为hibernate_unique_key,默认字段为integer类型,名称是next_hi(比较少用);assigned:在插入数据的时候主键由程序处理(很常用),这是 <generator>元素没有指定时的默认生成策略。等同于JPA中的AUTO;identity:使用SQL Server 和 MySQL 的自增字段,这个方法不能放到 Oracle 中,Oracle 不支持自增字段,要设定sequence(MySQL 和 SQL Server 中很常用),等同于JPA中的INDENTITY;increment:插入数据的时候hibernate会给主键添加一个自增的主键,但是一个hibernate实例就维护一个计数器,所以在多个实例运行的时候不能使用这个方法;select:使用触发器生成主键(主要用于早期的数据库主键生成机制,少用);sequence:调用底层数据库的序列来生成主键,要设定序列名,不然hibernate无法找到;seqhilo:通过hilo算法实现,但是主键历史保存在Sequence中,适用于支持 Sequence 的数据库,如 Oracle(比较少用);foreign:使用另外一个相关联的对象的主键,通常和联合起来使用;guid:采用数据库底层的guid算法机制,对应MYSQL的uuid()函数,SQL Server的newid()函数,ORACLE的rawtohex(sys_guid())函数等;uuid.hex:看uuid,建议用uuid替换;sequence-identity:sequence策略的扩展,采用立即检索策略来获取sequence值,需要JDBC3.0和JDK4以上(含1.4)版本;对于这些hibernate主键生成策略和各自的具体生成器之间的关系,在org.hibernate.id.IdentifierGeneratorFactory中指定了:static { GENERATORS.put("uuid", UUIDHexGenerator.class); GENERATORS.put("hilo", TableHiLoGenerator.class); GENERATORS.put("assigned", Assigned.class); GENERATORS.put("identity", IdentityGenerator.class); GENERATORS.put("select", SelectGenerator.class); GENERATORS.put("sequence", SequenceGenerator.class); GENERATORS.put("seqhilo", SequenceHiLoGenerator.class); GENERATORS.put("increment", IncrementGenerator.class); GENERATORS.put("foreign", ForeignGenerator.class); GENERATORS.put("guid", GUIDGenerator.class); GENERATORS.put("uuid.hex", UUIDHexGenerator.class); //uuid.hex is deprecated GENERATORS.put("sequence-identity", SequenceIdentityGenerator.class); // 上面十二种策略,加上native,hibernate一共默认支持十三种生成策略。使用示例:// uuid @GeneratedValue(generator = "paymentableGenerator") @GenericGenerator(name = "paymentableGenerator", strategy = "uuid") // 除以下所列特殊适配格式外,其他策略均采用上面第一种格式 // select @GeneratedValue(generator = "paymentableGenerator") @GenericGenerator(name="select", strategy="select", parameters = { @Parameter(name = "key", value = "idstoerung") }) // sequence @GeneratedValue(generator = "paymentableGenerator") @GenericGenerator(name = "paymentableGenerator", strategy = "sequence", parameters = { @Parameter(name = "sequence", value = "seq_payablemoney") }) // seqhilo @GeneratedValue(generator = "paymentableGenerator") @GenericGenerator(name = "paymentableGenerator", strategy = "seqhilo", parameters = { @Parameter(name = "max_lo", value = "5") }) // foreign @GeneratedValue(generator = "idGenerator") @GenericGenerator(name = "idGenerator", strategy = "foreign", parameters = { @Parameter(name = "property", value = "employee") }) // sequence-identity @GeneratedValue(generator = "paymentableGenerator") @GenericGenerator(name = "paymentableGenerator", strategy = "sequence-identity", parameters = { @Parameter(name = "sequence", value = "seq_payablemoney") })hibernate每种主键生成策略提供接口org.hibernate.id.IdentifierGenerator的实现类,如果要实现自定义的主键生成策略也必须实现此接口。IdentifierGenerator提供一generate方法,generate方法返回产生的主键。// 源码展示 public interface IdentifierGenerator { * The configuration parameter holding the entity name public static final String ENTITY_NAME = "entity_name"; * Generate a new identifier. * @param session * @param object the entity or toplevel collection for which the id is being generated * @return a new identifier * @throws HibernateException public Serializable generate(SessionImplementor session, Object object) throws HibernateException; } 3. 自定义策略由@GenericGenerator实现GenericGenerator注解源码定义:@Target({PACKAGE, TYPE, METHOD, FIELD}) @Retention(RUNTIME) public @interface GenericGenerator { * unique generator name String name(); * Generator strategy either a predefined Hibernate * strategy or a fully qualified class name. String strategy(); * Optional generator parameters Parameter[] parameters() default {}; } 以上属性说明如下:name 属性指定生成器名称;strategy 属性指定具体生成器的类名;parameters 得到strategy指定的具体生成器所用到的参数;通过@GenericGenerator自定义主键生成策略import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.id.UUIDHexGenerator; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.Type; import java.io.Serializable; import java.util.Properties; * 自定义主键生成策略。实现自己设置ID,同时保留原来的主键生成策略(32位UUID)不变。 * 调用的保存方法需为Repository.save()或EntityManager.merge() * 若调用的保存方法为EntityManager.persist(),且传入对象有id值时,仍会报错! public class CustomUUIDGenerator extends UUIDHexGenerator { private String entityName; @Override public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException { entityName = params.getProperty(ENTITY_NAME); if (entityName == null) { throw new MappingException("no entity name"); super.configure(type, params, serviceRegistry); @Override public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException { Serializable id = session.getEntityPersister(entityName, object).getIdentifier(object, session); if (id != null) { return id; return super.generate(session, object); * Entity实体类中使用 @GeneratedValue(generator = "paymentableGenerator") @GenericGenerator(name = "paymentableGenerator", strategy = "{自定义主键生成策略的全限定类名}") private String id;Repository 相关注解Repository相关注解主要在SpringDataJpa的Repository中使用。@Query:自定义 JPQL 或原生Sql 查询,摆脱命名查询的约束 @Query("select u from User u where u.firstname = :firstname") // JPQL User findByLastnameOrFirstname(@Param("lastname") String lastname); @Query(value = "SELECT * FROM USERS WHERE X = ?1", nativeQuery = true) // 原生sql User findByEmailAddress(String X);Ø 关于@Query中参数的占位符:方式一:标识符 : :参数名可以定义好参数名,赋值时采用@Param(“参数名”),而不用管顺序。方式二:使用索引下标:?索引值索引值从1开始,查询中 ”?X” 个数需要与方法定义的参数个数相一致,并且顺序也要一致。@Modifying:DELETE和UPDATE操作必须加上@modifying注解,以通知Spring Data 这是一个DELETE或UPDATE操作。@Transactional:UPDATE或者DELETE操作需要使用事务@Async:异步操作@NoRepositoryBean:避免Spring容器为此接口创建实例。可用于定义公共Repository/** * 可以将业务中用到的公共方法抽离到公共Repository中 @NoRepositoryBean //避免Spring容器为此接口创建实例。不被Service层直接用到的Repository(如base repository)均应加此声明 public interface BaseRepository<T, ID> { @Modifying @Query("update #{#entityName} set isDelete='N' where id in ?1 ") Integer myUpdateAsNotDeleted(Collection<String> ids); }SpringDataJpa 框架使用官方文档:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#preface基本使用基本使用步骤及依赖基本使用步骤将 spring-data-jpa 包,数据库驱动包等添加为项目依赖;配置文件定义相应的数据源;定义Entity实体类;定义自己业务相关的的 JPA repository 接口,继承自 JpaRepository 或者JpaSpecificationExecutor;为应用添加注解@EntityScan、@EnableJpaRepositories,此步不是必须的;将自定义的 JPA repository 接口注入到服务层并使用它们进行相应的增删改查;依赖 <!-- Spring Boot JPA 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- mysql 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- pgsql 驱动--> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency>Entity 类与 Respository 接口示例Entity类@Data @Builder @AllArgsConstructor @NoArgsConstructor @Table(name="CUSTOMERS") @Entity @DynamicInsert @DynamicUpdate public class Customer { @GeneratedValue(strategy=GenerationType.AUTO) @Column(name = "id",insertable = false, updatable = false, length = 32) private Integer id; @Column(name = "name", nullable = false, length = 10) private String name; @Column(name = "age") private Integer age; @Temporal(TemporalType.TIMESTAMP) @CreationTimestamp @Column(name = "create_date", columnDefinition = "timestamp(6)") private Date createDate; @Temporal(TemporalType.TIMESTAMP) @UpdateTimestamp @Column(name = "update_date", columnDefinition = "timestamp(6)") private Date updateTime; }Respository接口// Customer 为该respository对应的实体类,Long为实体类的主键的类型 public interface CustomerRespository extends JpaRespository<Customer, Long>{ }注解扫描及 JPA、JDBC 常用配置注解扫描在SpringBoot中:默认情况下,当Entity类、Repository类与主类在同一个包下或在主类所在包的子类时,Entity类、Repository类会被自动扫描到并注册到Spring容器,此时使用者无需任何额外配置。当不在同一包或不在子包下时,需要分别通过在主类上加注解@EntityScan( basePackages = {"xxx.xxx"}) 来指定Entity的位置可多处使用@EntityScan。它们的basePackages可有交集,但必须覆盖到所有被Resository使用到的Entity,否则会报错。@EnableJpaRepositories( basePackages = {"xxx.xxx"}) 来指定Repository类的位置可多处使用@EnableJpaRepositories。它们的basePackages不能有交集否则会报重复定义的错(除非配置允许覆盖定义),必须覆盖到所有被使用到的Resository。JPA和JDBC常用配置在利用SpringBoot框架进行开发时,大部分服务避不开用数据库进行数据存储和使用。SpringBoot里面一般有两种方式进行数据表的创建和数据存储。Spring JDBC,我们需要在application.yml或者application.properties中配置JDBC相关属性,主要是spring.datasource.xxx属性配置。当然,使用jpa也需要用到spring.datasource.url/username/password等属性配置进行数据库地址、用户名、密码等配置。Spring Boot JPA,我们需要在application.yml或者application.properties中配置jpa相关属性spring.jpa.xxx属性配置。配置模板:spring: datasource: # driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC username: root password: root type: com.alibaba.druid.pool.DruidDataSource # 数据源配置 schema: classpath:db/schema.sql # 建表语句脚本的存放路径 data: classpath:db/data.sql # 数据库初始化数据的存放路径 sql-script-encoding: UTF-8 # 设置脚本的编码 database: mysql # 配置数据库方言。使用JPA访问数据库时,必需配置。 hibernate: ddl-auto: none # 每次程序启动时的数据库初始化策略 database-platform: org.hibernate.dialect.MySQL5InnoDBDialect # 配置数据库引擎,不配置则默认为myisam引擎 show-sql: true # 日志打印执行的SQL properties: hibernate: # show_sql: true # 日志打印执行的SQL。与spring.jpa.show-sql配置效果相同,两者使用其一即可。 format_sql: true # 格式化sql语句配置说明:spring.datasource.xxxspring.datasource.driver-class-name:配置driver的类名,默认是从JDBC URL中自动探测spring.datasource.url:配置数据库JDBC连接串spring.datasource.username:配置数据库连接用户名spring.datasource.password:配置数据库连接用户名对应的密码spring.datasource.type:连接池配置spring.datasource.schema:使用脚本创建表的语句的存放路径,classpath/db表示在工程的resource层级下的db目录中存放spring.datasource.data:使用脚本初始化数据库数据的语句的存放路径spring.datasource.sql-script-encoding:设置脚本的编码,默认常用设置为UTF-8使用上述方式建表时,spring.jpa.hibernet.ddl-auto设置成none,否则有啥问题,我也没尝试过。这样配置可以避免两种方式一起使用spring.jpa.xxxspring.jpa.hibernet.ddl-auto 值说明:create: 服务程序重启后,加载hibernate时都会删除上一次服务生成的表,然后根据服务程序中的model(entity)类再重新生成表,这个值慎用,会导致数据库中原表数据丢失。create-drop :服务服务程序重启后,加载hibernate时根据model(entity)类生成表,当sessionFactory关闭时,创建的表就自动删除。update:默认常用属性,第一次加载hibernate时根据model(entity)类会自动建立表结构,后面服务程序重启时,加载hibernate会根据model(entity)类自动更新表结构,如果表结构改变了,但是表行仍然存在,不会删除以前的行(对于表结构行只增不减)。validate :服务程序重启后,每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,如果不同,就会报错。不会创建新表,但是会插入新值。none :什么也不做。spring.jpa.database:配置数据库方言,使用JPA访问数据库时,必需配置。spring.jpa.database-platform:配置数据库引擎。SpringBoot 2.0后使用JPA、Hibernate来操作MySQL,Hibernate默认使用MyISM存储引擎而非InnoDB,前者不支持外键故会忽略外键定义。使用JPA访问数据库的注意事项:spring.jpa.database和spring.jpa.database-platform 这两项配置至少要配置一个来指明数据库方言访问的是MySQL数据库时,spring.datasource.driver-class-name 需配置为 com.mysql.cj.jdbc.DriverMySQL jdbc 6.0 版本以上 spring.datasource.url 中地址必须要配置 “serverTimezone”参数UTC代表的是全球标准时间若我们使用的时间是北京时区也就是东八区,领先UTC八个小时。url的时区使用中国标准时间。也是就serverTimezone=Asia/ShanghaRespository 接口核心方法查询所有数据List<T> findAll()根据id查询Optional<T> findOne()根据实体类属性查询(需命名方法)findByProperty (type Property) // 例如:findByAge(int age)分页查询Page<S> findAll(Example<S> example, Pageable pageable)修改 or 添加数据S save(S entity)底层逻辑为:当entity的id为null,则直接新增,不为null,则先select,如果数据库存在,则update。如果不存在,则insert批量保存List<T> saveAll(Iterable<S> list)注意:当批量保存大量数据时,效率会很慢!因为 saveAll 本质是循环集合调用save方法。优化方案见 批量保存优化删除void delete(T entity)计数 查询long count()或者 根据某个属性的值查询总数countByAge(int age)是否存在boolean existsById(ID primaryKey)查询API自定义命名查询及查询关键字通过方法名来指定查询逻辑,而不需要自己实现查询的SQL逻辑,示例:List<Student> getByName(String name)方法名解析原理:对方法名中除了保留字(findBy、top、within等)外的部分以 and 为分隔符提取出条件单词,然后解析条件获取各个单词并看是否和Entity中的属性对应(不区分大小写进行比较)。注意:get/find 与 by之间的会被忽略,所以 getNameById 与 getById 是等价的,会根据id查出整个Entity而不会只查name字段(指定部分字段的查询见后面条目)。查询条件解析原理:假设School和Student是一对多关系,Student中有个School school字段、School有个String addressCode字段,以如下查询为例:// 查询student表 Studetn getByNameAndSchoolAddressCode(String studentName, String addressCode) // JPA会自动生成条件studentName和关联条件student.school.addressCode进行查询 // 查询student表,推荐写法 Studetn getByNameAndSchool_AddressCode(String studentName, String addressCode)由And分割得到studentName、SchoolAddressCode;分别看Student中是否有上述两属性,显然前者有,后者没有,则后者需要进一步解析;JPA 按驼峰命名格式从后往前尝试分解 SchoolAddressCode :先得到 SchoolAdress、Code,由于 Student 没有SchoolAddress属性,故继续尝试分解,得到School、AdressCode;由于 Student 有 School 属性且 School 有 addressCode 属性,故满足,最终得到条件student.school.addressCode。注:若Student中有个 SchoolAdress schoolAddress 属性,但 schoolAddress 中没有 code 属性,则会因找不到 student.schoolAdress.code 而报错,所以可通过下划线显示指定分割关系,即写成:getByNameAndSchool_AddressCode查询字段解析原理:默认会查出 Entity 的所有字段且返回类型为该Entity类型,有两种情况可查询部分字段(除此外都会查出所有字段):通过 @Query 写自定义查询逻辑中只查部分字段。这种不属于直接通过方法名指定查询(详见后面查询指定部分字段的条目)。返回类型为自定义接口或该接口列表,接口中仅包含部分字段的get方法,此时会根据接口方法名查询部分字段。示例:/** * 此方法在 CourseRepository 中 * 注:find和By间的部分在解析时会被忽略。但为了见名知意,最好加上字段信息,如 findVersionByGroupId List<MyCustomColumns> findCustomColumnsByGroupId(String groupId); * 封装查询结果的接口 public interface MyCustomColumns { //JPA生成查询语句时只会查下面get方法中指定的字段名。需要确保Entity中有该字段名否则会报错 public String getId(); public String getVersion(); public String getGroupId(); }JPA 集合类型查询参数List<StudentEntity> getByIdInAndSchoolId(Collection<String> studentIdList, String schoolId); 关键在于 In 关键字。参数用Collection类型,当然也可以用List、Set等,但用Collection更通用,因为此时实际调用可以传List、Set等实参。查询关键字在查询时,通常需要同时根据多个属性进行查询,且查询的条件也格式各样(大于某个值、在某个范围等等),SpringDataJPA 为此提供了一些表达条件查询的关键字,官方文档如下:keywordSampleJPQL snippetDistinctfindDistinctByLastnameAndFirstnameselect distinct … where x.lastname = ?1 and x.firstname = ?2AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2Is, EqualsfindByFirstnamefindByFirstnameIsfindByFirstnameEquals… where x.firstname = ?1BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2LessThanfindByAgeLessThan… where x.age < ?1LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1GreaterThanfindByAgeGreaterThan… where x.age > ?1GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1AfterfindByStartDateAfter… where x.startDate > ?1BeforefindByStartDateBefore… where x.startDate < ?1IsNull, NullfindByAge(Is)Null()… where x.age is nullIsNotNull, NotNullfindByAge(Is)NotNull()… where x.age not nullLikefindByFirstnameLike… where x.firstname like ?1NotLikefindByFirstnameNotLike… where x.firstname not like ?1StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname descNotfindByLastnameNot… where x.lastname <> ?1InfindByAgeIn(Collection ages)… where x.age in ?1NotInfindByAgeNotIn(Collection ages)… where x.age not in ?1TruefindByActiveTrue()… where x.active = trueFalsefindByActiveFalse()… where x.active = falseIgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstname) = UPPER(?1)Example(动态实例)查询Example查询翻译过来叫 ” 按例查询(QBE)”。是一种用户界面友好的查询技术。 它允许动态创建查询,并且不需要编写包含字段名称的查询。 而且按示例查询不需要使用特定的数据库的查询语言来编写查询语句。优势:可以使用动态或者静态的限制去查询在重构你的实体的时候,不用担心影响到已有的查询可以独立地工作在数据查询API之外劣势:不支持组合查询,比如:firstname = ?0 or (firstname = ?1 and lastname = ?2)只支持字符串的starts/contains/ends/regex匹配,对于非字符串的属性,只支持精确匹配。换句话说,并不支持大于、小于、between等匹配。对一个要进行匹配的属性(如:姓名 name),只能传入一个过滤条件值Example(动态实例)查询的原理从生成的SQL语句可以看到,它的判断条件是根据实体的属性来生成查询语句的。如果实体的属性是null,它就会忽略它;如果不是,就会取其值作为匹配条件。注意:如果一个字段是不是包装类型,而是基本类型,它也会参与where条件中,其值是默认值。所以在定义实体时,基本数据类型的字段应尽量使用包装类型。使用示例:@Test public void test01() { User user = User.builder().name("Bob").build(); Example<User> example = Example.of(user); List<User> list = userRepository.findAll(example); list.foreach(System.out::println) Optional<User> userOptional = userRepository.findOne(Example.of(user)); userOptional.ifPresent(x -> System.out.println(x.getName()).isEqualTo("Bob")); }Example(动态实例)查询的概念定义介绍:// 示例:Example对象,由customer和matcher共同创建 Example<Customer> ex = Example.of(customer, matcher);实体对象:在持久化框架中与Table对应的域对象,一个对象代表数据库表中的一条记录,如上例中Customer对象。在构建查询条件时,一个实体对象代表的是查询条件中的“数值”部分。如:要查询名字是“Dave”的客户,实体对象只能存储条件值“Dave”匹配器:ExampleMatcher对象,它是匹配“实体对象”的,表示了如何使用“实体对象”中的“值”进行查询,它代表的是“查询方式”,解释了如何去查的问题。如:要查询FirstName是“Dave”的客户,即名以“Dave"开头的客户,该对象就表示了“以什么开头的”这个查询方式,如上例中:withMatcher("name", GenericPropertyMatchers.startsWith())实例:即Example对象,代表的是完整的查询条件。由实体对象(查询条件值)和匹配器(查询方式)共同创建。实例查询:就是通过一个例子来查询。要查询的是Customer对象,查询条件也是一个Customer对象,通过一个现有的客户对象作为例子,查询和这个例子相匹配的对象自定匹配器规则ExampleMatcher,不传时会使用默认的匹配器。 @Test public void test02() { //创建查询条件数据对象 User user = new User(); user.setUsername("y"); user.setAddress("sh"); user.setPassword("admin"); //创建匹配器,即如何使用查询条件 ExampleMatcher matcher = ExampleMatcher.matching() //模糊查询匹配开头,即{username}% // .withMatcher("username", ExampleMatcher.GenericPropertyMatchers.startsWith()) .withMatcher("username", match -> match.startsWith()) //全部模糊查询,即%{address}% .withMatcher("address" ,ExampleMatcher.GenericPropertyMatchers.contains()) //忽略字段,即不管password是什么值都不加入查询条件 .withIgnorePaths("password"); //创建实例 Example<User> example = Example.of(user ,matcher); List<User> list = userRepository.findAll(example); System.out.println(list); 打印的sql语句如下: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.password as password4_0_, user0_.phone as phone5_0_, user0_.username as username6_0_ t_user user0_ where user0_.username like ? and ( user0_.address like ? 参数如下: 2018-03-24 13:26:57.425 TRACE 5880 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [y%] 2018-03-24 13:26:57.425 TRACE 5880 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [%sh%] @Test public void test03() { //创建查询条件数据对象 Customer customer = new Customer(); customer.setName("zhang"); customer.setAddress("河南省"); customer.setRemark("BB"); //创建匹配器,即如何使用查询条件 ExampleMatcher matcher = ExampleMatcher.matching() //构建对象 .withStringMatcher(StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询 .withIgnoreCase(true) //改变默认大小写忽略方式:忽略大小写 .withMatcher("address", GenericPropertyMatchers.startsWith()) //地址采用“开始匹配”的方式查询 .withIgnorePaths("focus"); //忽略属性:是否关注。因为是基本类型,需要忽略掉 //创建实例与查询 List<Customer> ls = dao.findAll(Example.of(customer, matcher)); @Test public void test04() { List<Customer> ls = dao.findAll(Example.of( new Customer(), ExampleMatcher.matching() //构建对象 .withIncludeNullValues() //改变“Null值处理方式”:包括 //忽略其他属性 .withIgnorePaths("id","name","sex","age","focus","addTime","remark","customerType") }StringMatcher 参数Matching生成的语句说明DEFAULT (case-sensitive)firstname = ?0默认(大小写敏感)DEFAULT (case-insensitive)LOWER(firstname) = LOWER(?0)默认(忽略大小写)EXACT (case-sensitive)firstname = ?0精确匹配(大小写敏感)EXACT (case-insensitive)LOWER(firstname) = LOWER(?0)精确匹配(忽略大小写)STARTING (case-sensitive)firstname like ?0 + ‘%’前缀匹配(大小写敏感)STARTING (case-insensitive)LOWER(firstname) like LOWER(?0) + ‘%’前缀匹配(忽略大小写)ENDING (case-sensitive)firstname like ‘%’ + ?0后缀匹配(大小写敏感)ENDING (case-insensitive)LOWER(firstname) like ‘%’ + LOWER(?0)后缀匹配(忽略大小写)CONTAINING (case-sensitive)firstname like ‘%’ + ?0 + ‘%’模糊查询(大小写敏感)CONTAINING (case-insensitive)LOWER(firstname) like ‘%’ + LOWER(?0) + ‘%’模糊查询(忽略大小写)说明:在默认情况下(没有调用withIgnoreCase())都是大小写敏感的。总结在使用springdata jpa时可以通过Example来快速的实现动态查询,同时配合Pageable可以实现快速的分页查询功能对于非字符串属性的只能精确匹配,比如想查询在某个时间段内注册的用户信息,就不能通过Example来查询ExampleMatcher的使用 :基本类型的处理。如客户Customer对象中的年龄age是int型的,当页面不传入条件值时,它默认是0,是有值的,那是否参与查询呢?实体对象中,基本数据类型无论是否传值,都会参与查询(因为有默认值),故应避免使用基本数据类型,采用包装器类型(默认值是null)。如果已经采用了基本类型,而这个属性查询时若不需要进行过滤,则需把它添加到忽略列表(ignoredPaths)中。Null值的处理。当某个条件值为Null,是应当忽略这个过滤条件呢,还是应当去匹配数据库表中该字段值是Null的记录?当条件值为null时,默认是忽略此过滤条件,一般业务也是采用这种方式就可满足。当需要查询数据库表中属性为null的记录时,可将值设为 include,这时,对于不需要参与查询的属性,都必须添加到忽略列表(ignoredPaths)中,否则会出现查不到数据的情况(见上面实例 test04)。忽略某些属性值。一个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?若属性值为null,默认忽略该过滤条件;若属性值为基本数据类型,默认参与查询,若需忽略,则需添加至则需把它添加到忽略列表(ignoredPaths)中。不同的过滤方式。同样是作为String值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?默认创建匹配器时,字符串采用的是精确匹配、不忽略大小写,可以通过操作方法改变这种默认匹配,以满足大多数查询条件的需要,如将“字符串匹配方式”改为CONTAINING(包含,模糊匹配),这是比较常用的情况。对于个别属性需要特定的查询方式,可以通过配置“属性特定查询方式”来满足要求(见上面实例 test02)。大小写匹配。字符串匹配时,有时可能希望忽略大小写,有时则不忽略,如何做到?忽略大小的生效与否,是依赖于数据库的。例如 MySql 数据库中,默认创建表结构时,字段是已经忽略大小写的,所以这个配置与否,都是忽略的。如果业务需要严格区分大小写,可以改变数据库表结构属性来实现,具体可百度(见上面实例 test03)。JPQL 与 nativeQuery (原生SQL)查询JPQL 是专门为 Java 应用程序访问和导航实体实例设计的。Java Presistence Query Language( JPQL ),java持久性查询语言。它是JPA规范的重要组成部分,其实它就是一种查询语言,语法类似于SQL语法,但是有着本质的区别。JPQL 与 SQL 的区别JPQL 是面向对象的查询语言,因此它可以完全理解继承、多态和关联等特征。而且 JPQL 内置了大量函数,极大地方便了 JPQL 查询的功能。当然 JPQL 底层依然是基于SQL的,但 JPQL 到 SQL 的转换无须开发者关心,JPQL解析器会负责完成这种转换,并负责执行这种转换的SQL语句来更新数据库。SQL是面向关系数据库的查询语言,因此SQL操作的对象是数据表、数据列;而 JQPL 操作的对象是实体对象,对象属性。代码对比// 原生的SQL语句。对t_user table表执行查询,查询name、age、user_id三个数据列 select name,age,user_id from t_user // 面向对象的JPQL语句。对User实体执行查询,查询的是User实体的name、age、userId 属性 select name,age,userId from User 比较项SQLJPQL面向处理关系数据处理JPA实体关联实体的方式内连接、外连接、左连接、右连接内连接和左外连接支持的操作增(Insert)、删(Delete)改(Update)、查(Select)Delete(remove)Update(merge)、Select(find)JPQL基本语法select 实体别名.属性名,实体别名.属性名…… from 实体名 [as] 实体别名 where 实体别名.实体属性 op 比较值使用 @Query 注解创建查询,将该注解标注在Repository的方法上,然后提供一个需要的 JPQL 语句即可,如:@Query("select p from Person p where name like %?1%") Person findByName(String name);JPQL 查询时,可以使用SpEL表达式:#{#entityName} (取数据库实体名称 )好处是当修改类名后,不需要再单独去修改 JPQL 中的实体类名称@Query("select p from #{#entityName} p where name like %?1%") Person findByName(String name;SpEL表达式了解:SpEL(Spring Expression Language),即Spring表达式语言。它是一种类似JSP的EL表达式、但又比后者更为强大有用的表达式语言。SpEL表达式可以在spring容器内实时查询和操作数据,尤其是操作List列表型、Array数组型数据。所以使用SpEL可以有效缩减代码量,优化代码结构。@Query注解查询时候,条件查询如何使用占位符:(1)?+ 数字若使用这种方式,则参数列表中参数的入参顺序必须与 @Query注解当中标注的顺序相同@Query("SELECT s from Student s where s.email=?1 and s.age=?2") Student findStudentByEmailAndAge(String email , Integer age);(2):+ 参数名称这种方式可以自定义参数的名称。需要在参数列表当中用 @Param 注解标注参数名称。不用考虑顺序,是根据参数名称进行绑定@Query("SELECT s from Student s where s.email=:email and s.age=:age") Student findStudentByEmailAndAge2(@Param("age") Integer age, @Param("email") String email);nativeQuery(原生SQL查询)Repository中应尽可能避免使用nativeQuery,使得与数据库字段的耦合限制在Entity内而不扩散到Repository内,更易于维护尽可能避免在 JPQL、nativeQuery中进行联表查询,而是在Service层通过 JPA Specification 进行动态关联查询nativeQuery返回Entity使用 nativeQuery 时SQL语句查询的字段名若没有取别名,则默认是数据库中的字段名(例如school_id),而API返回值通常是schoolId,可以在SQL里通过 school_id as schoolId 取别名返回。然而若查询很多个字段值则得一个个通过as取别名,很麻烦,可以直接将返回值指定为数据库表对应的Entity,不过此法要求查询的是所有字段名,如:// nativeQuery返回类型可以声明为Entity,会自动进行匹配,要求查回与Entitydb中字段对应的所有db中的字段 @Query(value = " select t.* from teacher t where t.school_id=?1 "// 以下为搜索字段 + "and (?4 is NULL or name like %?4% ) " + "order by job_number limit ?2, x?3 ", nativeQuery = true) List<TeacherEntity> myGetBySchoolIdOrderByJobNumber(String schoolId, int startIndex, Integer size, String searchName);排序查询、分页查询排序查询静态方式:直接在方法体现(如 getByNameOrderByIdDesc),也可以在 JPQL 的@Query的逻辑中使用order by进行排序动态方式:可以在Repository的方法的最后加一个Sort 或者 Pageable 类型的参数,便可动态生成排序或分页语句(编译后会自动在语句后加order by或limit语句)List<User> findByAndSort(String name, Sort sort); // 调用 UserRepository.findByAndSort("bolton", Sort.by(Direction.Desc, "age"));进阶(了解)——可以通过 JpaSort.unsafe 实现待 function(函数计算) 的 sort(排序):public interface UserRepository extends JpaRepository<User, Long> { @Query("select u from User u where u.lastname like ?1%") List<User> findByAndSort(String lastname, Sort sort); @Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%") List<Object[]> findByAsArrayAndSort(String lastname, Sort sort); userRepository.findByAndSort("lannister", new Sort("firstname")); //userRepository.findByAndSort("stark", new Sort("LENGTH(firstname)")); //报错:invalid 无效 userRepository.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); 分页查询动态方式:在Repository的方法的最后加一个 Pageable 类型的参数,便可动态生成分页语句(编译后会自动在语句后加limit语句)// 不写@Query语句也可以加Pageable。另外若这里声明为List则不会分页,总是返回所有数据 @Query("select se from StudentExperimentEntity se " + "where se.studentId= ?2 and se.experimentId in " + "( select e.id from ExperimentEntity e where e.courseId= ?1 ) ") List<StudentExperimentEntity> myGetByCourseIdAndStudentId(String courseId, String studentId, Pageable pageable); * 编译后会在myGetByCourseIdAndStudentId所写SQL后自动加上 order by studentexp0_.lastopertime desc limit ? repository.myGetByCourseIdAndStudentId(courseId, studentId, PageRequest.of(0, 10, newSort(Sort.Direction.DESC, "lastopertime")));注:上述用法也支持nativeQuery,示例:@Query(value = "select d.*, u.username from developer d inner join user u on d.id=u.id " + " where (?1 is null or d.nick_name like %?1% ) ", nativeQuery = true) List<DeveloperEntity> myGetByNicknameOrPhoneOrEmailOrBz(String searchNicknameOrPhoneOrEmailOrBz, Pageable pageable); // 如果要同时返回分页对象,则可用Page<XX>返回类型 Page<DeveloperEntity> myGetByNicknameOrPhoneOrEmailOrBz(String searchNicknameOrPhoneOrEmailOrBz, Pageable pageable); 需要注意的是,只有元素是Entity类型时才支持直接将返回值声明为Page对象,否则会报错:Convert Exception。自定义封装查询的结果集(投影)查询一个表的部分字段,称为投影(Projection)对于只返回一个字段的查询,方法返回类型直接声明为该字段的类型或类型列表@Query(value = "select p.name from PserSon p where p.id=?1") String findNameById(String id); @Query(value = "select name from PserSon where age=?1") Set<String> findNameByAge(String id);对于返回多个字段的查询:如果是查询所有字段,则使用Entity实体类接收查询的结果集,支持 JPA,JPQL,原生sql查询。如果是查询部分字段,自定义查询的结果集有3种方法:使用自定义接口来映射结果集,支持 JPA,JPQL,原生sql查询。使用自定义对象来接收结果集,支持 JPQL查询。使用List<Object[]> 或 Map<String, Object> 来接收结果集,只支持原生sql查询。1.使用自定义接口来映射结果集直接通过方法名命名指定返回对象为包含部分字段getter方法的自定义接口(interface),只需要定义属性的getter方法,jdk动态代理封装数据。注意:如果不用 @Query 则需要确保接口中getter方法名中的字段与Entity中的一致,而如果使用@Query则不需要,因为可以通过as 取别名/** * Repository自定义查询方法 List<IdAndLanguageType> getLanguagesTypeByCourseIdIn(Collection<String> courseIdCollection); * 自定义接口来映射结果 public interface IdAndLanguageType { String getIdx(); String getLanguageType(); default String getAll() { return getIdx() + ", " + getLanguageType(); public interface PersonSummary { String getFirstname(); String getLastname(); List<AddressSummary> getAddress(); public interface AddressSummary { String getCity(); * 投影接口中的 getter 可以使用可为空的包装器来提高空安全性。 * 如果基础投影值不是null,则使用包装器类型的当前表示返回值。如果支持值是null,则 getter 方法返回所用包装器类型的空表示。 * 当前支持的包装器类型有: * java.util.Optional、com.google.common.base.Optional、scala.Option、io.vavr.control.Option interface NamesOnly { Optional<String> getFirstname(); }内部原理:根据自定义接口中的getter方法解析出要查询的字段:idx、languageTypeJPA 内部转成了用@Query注解的查询:// 注意:这里第一个字段名为 idx 而不是 id ,因为是根据接口getter方法产生的。 @Query("select new map(idx as idx, languageType as languageType) from CourseEntity where id in ?1 ") 2.使用自定义对象来接收结果集返回对象为自定义的class,可定义toString方法,打印日志方便。自定义的class须包含查询的字段的属性,且要封装的字段由公开的构造方法确定,对象属性不要求名称一致,只需要构造方法参数位置正确。JPQL 语法须为:select new + 对象全限定类名/** * Repository自定义查询方法 @Query(select new com.xx.yy.PersonResult(p.id,p.name,p.age) from Person p) List<PersonResult> findPersonResult(); * 自定义class类来映射结果 * 自定义class类中属性若全为JPQL中的查询字段且顺序一致,使用@AllArgsConstructor全参构造方法即可,否则需手写相应构造方法。 @Data @AllArgsConstructor @NoArgsConstructor public class IdAndLanguageType { String id; String name; String age; }3.使用Map<String, Object> 或 List<Object[]>来接收结果集注意:指定为Map时,实际类型是org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap,该类型只能读不能改或写(1)nativeQuery 查询,即 原生SQL查询直接select部分字段即可,结果集默认会自动包装为Map。缺点是sql里用的直接是数据库字段名,导致耦合大,数据库字段名一变,所有相关sql都得相应改变。/** * Repository自定义查询方法 @Query(value = "select g.id, g.school_id as schoolId, g.name from grade g " + "left join student s on g.name=s.grade " + " where g.school_id=(select a.school_id from admin a where a.id=?1)" + " and (?4 is null or g.name like %?4% or g.bz like %?4% ) " + " group by g.id limit ?2,?3", nativeQuery = true) List<Map<String, Object>> myGetGradeList(String adminId, Integer page, Integer size, String searchGradeNameOrGradeBz);(2)JPQL 查询可以手动指定包装为map,此时map的key为字段序号,故最通过as指定key为字段名。默认会将结果包装为List而不是Map,可以手动指定包装为map,此时map的key为字段序号(0、1、2...),也可以通过as指定key为字段名。注意:由于声明为Map时并不知道数据的返回类型是什么,故默认会用最大的类型(例如对于数据库中的整型列,查出时Map中该字段的类型为BigInteger)/** * Repository自定义查询方法 * 注意是 'map',不是 jdk 中的 'Map' ! @Query("select new map(g.name as name, count(s.id) as stuCount) from GradeEntity g, StudentEntity s " + "where g.name=s.grade and g.schoolId=?1 group by g.id") List<Map<String, Object>> myGetBySchoolId(String schoolId); @Query("select new map(g.name as name, count(s.id) as stuCount) from GradeEntity g, StudentEntity s " + "where g.name=s.grade and g.schoolId=?1 group by g.id") List<Object[]> myGetBySchoolId(String schoolId);动态投影/** * Repository自定义查询方法 * 动态投影方式。泛型根据需要传入Entity实体类或封装部分字段的自定义接口 public interface PersonRepository extends Repository<Person, UUID> { <T> Collection<T> findByLastname(String lastname, Class<T> type); }count查询、In查询count查询Integer countByName(String name);In查询不管是否是@Query都可以用 in 查询,如:@Query( "select * from student where id in ?1", nativeQuery=true) //@Query( "select s from StudentEntity s where s.id in ?1") List<StudentEntity> myGetByIdIn(Collection<String> studentIds ); //复杂查询,自定义查询逻辑 List<StudentEntity> getByIdIn( Collection<String> studentIds ); //简单查询,声明语句即可不管是否自己写查询语句、不管是否是nativeQuery,都要求调用该方法时所传的id列表必须至少有一个元素,否则执行时会报错。原因:运行时动态生成sql语句,如果id列表为null或空列表,则最终生成的sql语句中"where id in null"不符合sql语法。联表查询1、Entity内未定义关联实体时的联表查询,示例:@Query("select cd from CourseDeveloperEntity cd join Developer d where d.nickName='stdeveloper'") 2、Entity内定义的关联实体的关联查询,示例:@Query("select cd, d from CourseDeveloperEntity cd join cd.developer d where d.nickName='stdeveloper'") || (等价于) @Query("select cd, cd.developer from CourseDeveloperEntity cd where cd.developer.nickName='stdeveloper'") 若将一个对象的关联对象指定为延迟加载LAZY,则每次通过该对象访问关联对象时(如courseDeveloper.developer)都会执行一次SQL来查出被关联对象,显然如果被关联对象访问频繁则此时性能差。解决:法1:改为 EAGER 加载;法2:使用 join fetch 查询,其会立即查出被关联对象。示例:@Query("select cd from CourseDeveloperEntity cd join fetch cd.developer where cd.id='80'") join Fetch 其实就是使用 inner join,可以显示指定用其他关联方式,例如 left join fetch join fetch的缺点之一在于有可能导致“Duplicate Data and Huge Joins”,例如多个实验关联同一课程,则查询两个实验时都关联查出所属课程,后者重复查询。延迟加载与立即加载(FetchType)默认情况下,@OneToOne、@ManyToOne是LAZY,@OneToMany、@ManyToMany是EAGER。但不绝对,看具体需要。FetchType.LAZY:延迟加载。在查询实体A时,不查询出关联实体B,在调用getxxx方法时,才加载关联实体。但是注意,查询实体A时和getxxx必须在同一个 Transaction 中,不然会报错:“no session”,即会表现为两次单独的SQL查询(非联表查询)FetchType.EAGER:立即加载。在查询实体A时,也查询出关联的实体B。即会表现为一次查询且是联表查询有两个地方用到延迟加载:relationship(@OneToMany等)、attribute(@Basic)。后者一般少用,除非非常确定字段很少访问到。增删改API保存 与 更新Repository方法核心方法// 添加 or 修改数据 S save(S entity)底层逻辑为:当entity的id为null,则直接新增,不为null,则先select,如果数据库存在,则update。如果不存在,则insert注意:若 JPA 启用了逻辑删除(软删除)功能,使用save方法则可能会出现 主键冲突 或 唯一索引冲突 等问题原因:若数据库启用了逻辑删除功能,记录逻辑删除后,该条记录实际仍存在于数据库中,但是 JPA 根据Entity的主键查询数据库判断该执行insert还是update时,查询语句会自动加上逻辑删除的判断条件,从而查不到数据而最终执行insert,进而可能会导致报主键冲突或唯一索引冲突。update方式1:通过Repository的save方法方式2:通过Repository中注解@@Query、@Modifying、@Query组合自定义方法(注:update不支持直接通过方法名声明)删除 与 逻辑删除1、删除记录Repository接口核心方法void delete(T entity)Repository自定义删除方法需要加@Modefying、@Transactional@Transactional //也可以只标注在上层调用者方法上 @Modifying @Query("delete from EngineerServices es where es.engineerId = ?1")//update与此类似 int deleteByEgId(String engineerId); // 直接通过方法名声明(注:update不支持这样写) @Transactional @Modifying int deleteByEgId(String engineerId);注:JPA中非nativeQuery的删除操作(如deleteByName)内部实际上是先分析出方法名中的条件、接着按该条件查询出所有Entity,然后根据这些Entity的id执行SQL删除操作。也正因为这样,软删除功能中指定 @SQLDelete("update student set is_delete='Y' where id=? ") 即可对所有非nativeQuery起作用。方法名包含条件的删除操作(例如 Integer deleteByNameAndSId(String name, String uuid); ),其执行时与save类似,也是先根据条件查出目标Entity再执行删除操作。对于 void delete(T entity);, 则直接根据Entity的主键操作而不用先查。2、逻辑删除使用org.hibernate.annotations(不是JPA的标准)的 @Where、@SQLDelete、@SQLDeleteALL 三个注解来实现。// 对非nativeQuery 旳delete起作用,包括形如deleteByName等,下同。 @SQLDelete(sql = "update " + StudentEntity.tableName + " set " + constant.ISDELETE_COLUMN_NAME + " =true where sid=?") @SQLDeleteAll(sql = "update " + StudentEntity.tableName + " set " + constant.ISDELETE_COLUMN_NAME + " =true where sid=?") // 对非nativeQuery的select起作用(如count、非nativeQuery的String myGetNameByName等,前者本质上也是select) @Where(clause = constant.ISDELETE_COLUMN_NAME + " = false") @Data @Entity @Table(name = StudentEntity.tableName) public class StudentEntity extends BaseEntity { public static final String tableName = "student"; @Column(name = constant.ISDELETE_COLUMN_NAME, nullable = false) private Boolean isDelete = false; }需要注意的是:@Where会自动在查询语句后拼接@Where中指定的条件;该注解对所有的非nativeQuery的查询起作用,如count、自己写的非nativeQuery的查询语句(例如:myGetByName)等。@SQLDelete会自动将删除语句替换为@SQLDelete中指定的sql操作;该注解对所有非nativeQuery的删除操作起作用,如delete(StudenEntity entity)、deleteBySId、deleteByName等,但由于指定的sql操作中条件不得不写死,所以要按期望起作用的话,@SQLDelete中的sql操作应以Entity的主键为条件,且自定义的删除方法必须按delete(StudenEntity entity)、deleteBySId两种写法写(delete(StudenEntity entity)会自动取entity的主键给sid),而不能用deleteByName(会将name参数值传给sid)通过 JPQL 的方法名指定删除操作(如 Integer deleteByName(String name))时背后是先根据条件查出Entity然后根据Entity的主键删除该Entity。所以通过@SQLDelete、@SQLDeleteALL实现逻辑删除时,由于其语句是写死的,故:@SQLDelete、@SQLDeleteALL同时存在时会按后者来执行软删除逻辑@SQLDeleteALL并不会批量执行软删除逻辑(因为一来不知具体有几个数据,二来in中只指定了一个元素),而是一个个删,即有多条待删除时也会一条条执行软删除逻辑,每条语句中in中只有一个元素。故其效果与@SQLDelete的一样,然而 “in” 操作效率比 “=” 低,故推荐使用@SQLDelete关于软删除:对于关联表(一对一、一对多、多对多),若要启用软删除,则须为多对多关联表定义额外的主键字段而不能使用联合外键作为主键,否则软删除场景下删除关联关系再重新关联时会主键冲突。另外,特殊情况下多对多关联表可以不启用软删除(被关联表、一对多或多对一关联表则需要,因为它们侧重的信息往往不在于关联关系而是重要的业务信息)批量保存优化原生的saveAll()方法可以保证程序的正确性,但是如果数据量比较大时效率低。源码逻辑原理是:for 循环集合调用save方法;save方法逻辑为,当entity的id为null,则直接新增,不为null,则先select,如果数据库存在,则update。如果不存在,则insert。 @Transactional public <S extends T> List<S> saveAll(Iterable<S> entities) { Assert.notNull(entities, "Entities must not be null!"); List<S> result = new ArrayList(); Iterator var3 = entities.iterator(); while(var3.hasNext()) { S entity = var3.next(); result.add(this.save(entity)); //save方法是核心逻辑 return result; @Transactional public <S extends T> S save(S entity) { if (this.entityInformation.isNew(entity)) { this.em.persist(entity); return entity; } else { return this.em.merge(entity); }解决方案:批量插入优化方案为:当保存大量数据时,直接使用em进行持久化插入,省了一步查询操作。并且考虑到如果最后才提交所有数据,数据库的负载可能会比较大,故每100条记录就提交(flush)一次。 @Autowired private EntityManager entityManager; private final int BATCH_SIZE = 1000; @Transactional(rollbackFor = Exception.class) public void addBatch(List<S> list) { int num = 0; for (S s : list) { entityManager.persist(s); // insert插入操作(变成托管状态) int num += 1; if (i % BATCH_SIZE == 0) { entityManager.flush(); // 变成持久化状态 entityManager.clear(); // 变成游离状态 }批量更新在确保数据已经存在的情况下,如果是批量更新可以如下代码代替上面的entityManager.persist(projectApplyDO);语句:entityManager.merge(projectApplyDO); //update更新操作自动提交问题JPA事务内Entity变更会自动更新到数据库若启用了事务,则对于managed状态的entity,若在事务内该entity有字段的值发生了变化,则即使未调save方法,该entity的变化最后也会被自动同步到数据库,即sql update操作。即相当于在Persist Context flush时自动对各engity执行 save 方法。(org.hibernate.event.internal.AbstractFlushingEventListener中)详情可参阅:https://blog.csdn.net/qq_38658642/article/details/90729827实现自定义的 Repository 实现类1.写一个与Repository接口同名的类,加上后缀 Impl,标注@Repository注解;这个类不需要实现任何接口,可以自动被扫描到。2.在Repository接口中加入自定义的方法,比如:public interface MyRepository extends JpaRespository<UserEntity, String>{ // 自定义的方法 public Page<Object[]> getByCondition(UserQueryModel u); }3.在实现类中,去实现在Repository接口中加入的自定义方法,会被自动找到@Repository public class MyRepositoryImpl{ @Autowired private EntityManager em; // 实现在Repository接口中加入的自定义方法 public Page<Object[]> getByCondition(UserQueryModel u){ String hql = "select o.uuid,o.name from UserEntity o where 1=1 and o.uuid=:uuid"; Query q = em.createQuery(hql); q.setParameter("uuid", u.getUuid()); q.setFirstResult(0); q.setMaxResults(1); Page<Object[]> page = new PageImpl<Object[]>(q.getResultList(), new PageRequest(0,1), 3); return page; }Repository 方式调用存储过程Repository方式调用存储过程需要基于Entity实体类,在实体类上使用@NamedStoredProcedureQuery注解(需要数据库中有对应的表,可自动映射结果集)。详解 Hibernate EntityManger专题-JPA调用存储过程 条目。import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import com.labofjet.entity.A; import com.labofjet.entity.APK; @Repository public interface ARepository extends JpaRepository<A, APK>{ // 方式1。若用这种方式,方法名要与存储过程名一样。【推荐】 @Procedure Integer plus1inout(Integer arg); @Procedure Object[] mytest(); // 方式2。Procedure的name为实体类上@NamedStoredProcedureQuery注解中name的值 @Procedure(name="User.plus1") Integer alias2(@Param("arg")Integer argAlias); // @Param必须匹配@StoredProcedureParameter注释的name参数 // 方式3。Procedure的procedureName参数必须匹配实体类上@NamedStoredProcedureQuery的procedureName的值 @Procedure(procedureName="plus1inout") Integer alias3(Integer arg); }注意:返回类型必须匹配。in_only类型的存储过程返回是void,in_and_out类型的存储过程返回相应数据类型JpaSpecificationExecutor 接口spring data jpa 提供了 JpaSpecificationExecutor 接口,只要简单实现toPredicate方法就可以实现复杂的动态查询。Specification是Spring对Criteria的封装。JpaSpecificationExecutor提供了以下接口public interface JpaSpecificationExecutor<T> { T findOne(Specification<T> spec); List<T> findAll(Specification<T> spec); Page<T> findAll(Specification<T> spec, Pageable pageable); List<T> findAll(Specification<T> spec, Sort sort); long count(Specification<T> spec); //其中Specification就是需要我们传入查询方法的参数,它是一个接口 public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb); // root:根参数,代表了可以查询和操作的实体对象的根,如果将实体对象比喻成表名,那root里面就是这张表里面的字段,是JPQL的实体字段,通过Path<Y>get(String var0)来获得操作的字段 // criteriaQuery:代表一个specific的顶层查询对象,它包含着查询的各个部分,如: select、form、where、group by、order by 等,它提供了查询的的方法,常用的有 where、select、having // criteriaBuilder:用来构建CriteriaQuery的构建器对象,其实就相当于条件或条件组合 }提供唯一的一个方法toPredicate,我们只要按照JPA 2.0 criteria api写好查询条件就可以了。关于JPA 2.0 criteria api的介绍和使用,欢迎参考:http://blog.csdn.net/dracotianlong/article/details/28445725 http://developer.51cto.com/art/200911/162722.htmRepository 继承 JpaSpecificationExecutor接口public interface TaskResposity extends JpaRespository<Task, Long>, JpaSpecificationExecutor<Task>{ }调用@Service public class TaskService { @Autowired private TaskRepository taskRepository ; * 多条件 + 分页排序 查询 public Page<Task> findBySepc(Task task, int page, int size){ // 分页排序请求 PageRequest pageReq = new PageRequest(page, size, new Sort(Direction.DESC,"createTime")); Page<Task> tasks = taskRepository.findAll(new Specification<Task>(){ // 匿名内部类 @Override public Predicate toPredicate(Root<Task> root, CriteriaQuery<?> query, CriteriaBuilder builder) { //1.混合条件查询 Path<String> exp1 = root.get("taskName"); Path<Date> exp2 = root.get("createTime"); Path<String> exp3 = root.get("taskDetail"); Predicate pre = builder.and( builder.like(exp1, "%" + task.getTaskName + "%"), builder.lessThan(exp2, new Date())); return builder.or(pre, builder.equal(exp3, task.getTaskDetail)); /* 生成的sql语句为: Hibernate: select count(task0_.id) as col_0_0_ tb_task task0_ where task0_.task_name like ? and task0_.create_time<? or task0_.task_detail=? //2.多表查询 Join<Task,Project> join = root.join("project", JoinType.INNER); Path<String> exp4 = join.get("projectName"); return cb.like(exp4, "%"+ task.getProjectName +"%"); /* 生成的sql语句为: Hibernate: select count(task0_.id) as col_0_0_ tb_task task0_ inner join tb_project project1_ on task0_.project_id=project1_.id where project1_.project_name like ? }, pageReq); return tasks; // 多条件 + 排序 查询 // 将多个的条件封装成数组的形式传递给接收多个参数的方法完成多条件查询 public List<Task> findBySepc(Task task){ /*Specification<UserEntity> spc = (root, query, builder)->{ ArrayList<Predicate> list = new ArrayList<>(); list.add(builder.equal(root.get("username"), task.getUsername)); list.add(builder.equal(xrootget("password"), task.getPassword)); return builder.and(list.toArray(new Predicate[list.size()])); Specification<UserEntity> spc = (root, query, builder) -> builder.and( builder.equal(root.get("username"), task.getUsername), builder.equal(xrootget("password"), task.getPassword) List<UserEntity> list = userRepository.findAll(spc, Sort.by(Sort.Direction.DESC,"createTime")); list.forEach(System.out::println); return list; }每次都要写一个类来实现Specification比较麻烦,可以将查询条件封装在专门的一个类中,使用时调用静态方法public class TaskSpec { // 封装查询条件的静态方法 public static Specification<Task> method1(Task task){ return new Specification<Task>(){ @Override public Predicate toPredicate(Root<Task> root, CriteriaQuery<?> query, CriteriaBuilder cb) { // 示例,未写具体的查询条件,入参从 task 中获取 return null; // 使用 Page<Task> tasks = this.taskDao.findAll(TaskSpec.method1(), pageReq);
cron 时间表达式(七子表达式)cron 表达式,又称时间表达式(七子表达式),是一个字符串,以5或者6个空格隔开,字符串被切割为6个或者7个域,每个域都代表不同的含义// 从左到右分别表示:秒 分 时 日 月 周 年;参数以空格隔开,其中 年 不是必须参数,可以省略。 {Seconds} {Minutes} {Hours} {DayofMonth} {Month} {DayofWeek} {Year}序号时间元素是否必填入参范围可填通配符1秒是0-59, - * /2分是0-59, - * /3时是0-23, - * /4日是1-31, - * ? / L W5月是1-12 或 JAN - DEC, - * /6周(周一 ~ 周日)是1-7(1=SUN )<br/>或 SUN,MON,TUE,WED,THU,FRI,SAT, - * ? / L #8年否1970-2099, - * /常用通配符:* 表示匹配该域的任意值,比如 Minutes 域使用 * 表示每分钟都会触发 - 表示范围,比如 Minutes 域使用 10-20 表示从10分钟到20分钟每分钟都会触发一次 , 表示列出枚举值,比如 Minutes 域使用 1,3 表示1分钟和3分钟都会触发一次 / 表示间隔时间触发(开始时间/时间间隔),例如在 Minutes 域使用 5/10 表示从第5分钟开始,每隔10分钟触发一次 ? 表示不指定值,简单理解就是忽略该字段的值,直接根据另一个字段的值触发执行,仅被用于 DayofMonth 域和 DayofWeek 域中 当这两个域其中之一被指定了值以后,为了避免冲突,需要将另一个域的值设为“?” L 表示最后,是单词"last"的缩写(最后一天或最后一个星期几);仅被用于 DayofMonth 域和 DayofWeek 域中 用在 DayofMonth 域 表示该月的最后一天 用在 DayofWeek 域 表示一个星期的最后一天,也就是周六 在使用'L'时,不要指定列表','或范围'-',否则易导致出现意料之外的结果 如果在“L”前有具体的内容,它就具有其他的含义了 例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五 注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题 # 表示该月第n个星期x(x#n),仅用 DayofWeek 域,如:6#3 表示该月的第三个星期五 W 仅用在 DayofMonth 域中,表示距离当月给定日期最近的工作日(周一到周五),是单词"weekday"的缩写注意:天数和星期不能同时指定值!!!示例:// 每天凌晨零点执行 @Scheduled(cron ="0 0 0 * * * ?") // 每隔五分钟执行 @Scheduled(cron ="0 */5 * * * * ?") // 在每天下午2点到下午2:55期间的每5分钟触发 @Scheduled(cron ="0 0/5 14 * * ?") // 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 @Scheduled(cron ="0 0/5 14,18 * * ?") // 在每天下午2点到下午2:05期间的每1分钟触发 @Scheduled(cron ="0 0-5 14 * * ?") // 每个星期三中午12点 @Scheduled(cron ="0 0 12 ? * WED") // 每月的第三个星期五上午10:15触发 @Scheduled(cron ="0 15 10 ? * 6#3") // 每年三月的星期三的下午2:10和2:44触发 @Scheduled(cron ="0 10,44 14 ? 3 WED") // 每月最后一日的上午10:15触发 @Scheduled(cron ="0 15 10 L * ?") // 每月的最后一个星期五上午10:15触发 @Scheduled(cron ="0 15 10 ? * 6L")<br/>spring 自带调度器:@Scheduled介绍定时调度其实有很多的第三方平台可以接入,但是其实在 SpringBoot 有自带的定时任务注解 @Scheduled。@Scheduled 可以通过注解配置快速实现方法的定时调度,直接在方法加上 @Scheduled 注解即可。定时任务开启条件:@Scheduled 标注的方法所在类交给 Spring 容器管理(即标注 Spring IOC 注解)在配置类或 @Scheduled 标注的方法所在类上添加 @EnableScheduling 注解开启定时任务功能@EnableScheduling 注解的作用是发现注解@Scheduled的任务并后台执行<br/>@Scheduled注解参数:cron 参数:数据类型为 String最经常使用的参数,表示接收一个时间表达式(七子表达式),最多接收7个时间元素zone 参数指定获取的时区,默认是空,表示使用服务器所在时区,比如 Asia/BeiJingi 或者 Asia/ShanghaifixedDelay 参数指定上次执行结束后到再次执行的固定时间,单位是毫秒。示例:@Scheduled(fixedDelay= 3000) 表示距离上次调用后三秒再执行fixedDelayString 参数fixedDelayString 与 fixedDelay 是几乎一样的,唯一的差异是 fixedDelayString 是支持占位符的fixedRate 参数指定多久执行一次,单位是毫秒。与 cron 参数的 / 通配符效果相似示例:@Scheduled(fixedRate= 3000) 表示每三秒执行一次fixedRateString 参数fixedRate 参数的升级,支持占位符initialDelay 参数表示第一次延迟多少毫秒执行,单位是毫秒示例:@Scheduled(initialDelay= 3000) 表示第一次执行时,延迟3秒执行initialDelayString 参数initialDelay 参数的升级,支持占位符<br/>注意若微服务是多实例部署,则每一个微服务实例都会同时执行定时调度任务,可能产生很多重复数据或者导致系统出现其他的业务逻辑BUG,所以在使用 @Scheduled 进行任务调度时,需要配合 redis 的分布式锁来使用,让定时调度任务只在一个微服务实例上执行,避免BUG出现。<br/>使用示例import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component @Slf4j public class Task { // 每5秒执行一次 @Scheduled(cron = "0/5 * * * * ?") public void task2(){ log.warn("现在时间:" + LocalDateTime.now()); // 上⼀次开始执⾏时间点之后2秒再执⾏ @Scheduled(fixedRate = 2000) public void task(){ log.warn("现在时间:" + LocalDateTime.now()); }<br/>JDK 原生定时器介绍定时器,可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件事情java.util.Timer 类:是一个描述定时器的类一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。构造方法:public Timer() // 创建一个新计时器。成员方法:// 终止此计时器,丢弃所有当前已安排的任务 void cancel() // 注意,在此计时器调用的计时器任务的 run 方法内调用此方法,就可以绝对确保正在执行的任务是此计时器所执行的最后一个任务。 // 在指定的毫秒值之后,执行指定的任务,只会执行一次 void schedule(TimerTask task, long delay) // 在指定的毫秒值之后,执行指定的任务,之后每隔固定的毫秒数重复执行定时任务 void schedule(TimerTask task, long delay, long period) // 安排在指定的时间执行指定的任务,只会执行一次 void schedule(TimerTask task, Date time) // 安排指定的任务在指定的时间开始进行重复的固定延迟执行 void schedule(TimerTask task, Date firstTime, long period) /* 参数: task 所要安排的任务。定时器到时间之后要执行的任务 delay 执行任务前的延迟时间,单位是毫秒。 多个毫秒之后开始执行TimerTask任务 period 执行各后续任务之间的时间间隔,单位是毫秒。定时器开始执行之后,每隔多少毫秒重复执行 time 执行任务的时间。从什么日期开始执行任务 20020-07-06 15:25:13 period 执行各后续任务之间的时间间隔,单位是毫秒。定时器开始执行之后,每隔多少毫秒重复执行 */<br/>TimerTask 类:由 Timer 执行的一次执行或重复执行的任务java.util.TimerTask implements Runnable 接口TimerTask 类是一个抽象类,无法直接创建成员抽象方法:// 此计时器任务要执行的操作。重写run方法,设置线程任务 void run()<br/>使用示例import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class Demo01Timer { public static void main(String[] args) throws ParseException { show04(); 在指定的毫秒值之后,执行指定的任务,只会执行一次 void schedule(TimerTask task, long delay) private static void show01() { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("c4爆炸了!"); timer.cancel(); }, 5000); 在指定的毫秒值之后,执行指定的任务,之后每隔固定的毫秒数重复执行定时任务 void schedule(TimerTask task, long delay, long period) private static void show02() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Timer timer =new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println(sdf.format(new Date())); }, 5000, 1000); 安排在指定的时间执行指定的任务,只会执行一次 void schedule(TimerTask task, Date time) private static void show03() throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = sdf.parse("2020-07-06 15:32:50"); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("哈哈!"); timer.cancel(); }, date); 安排指定的任务在指定的时间开始进行重复的固定延迟执行 void schedule(TimerTask task, Date firstTime, long period) private static void show04() throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = sdf.parse("2020-07-06 15:37:30"); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("嘿嘿,你中招了!"); }, date, 10);
jasypt 加解密参考:jasypt 的 GitHub 官方网址jasypt加密Jasypt 开源加密库使用教程SpringBoot配置文件中密码属性加密概述Jasypt 全称 Java Simplified Encryption ,是 Sourceforge.net 上的一个开源项目。Jasypt 可用于加密任务与应用程序,例如加密密码、敏感信息和数据通信,还包括高安全性、基于标准的加密技术、可同时单向和双向加密的加密密码、文本、数字和二进制文件。Jasypt 还符合 RSA 标准的基于密码的加密,并提供了无配置加密工具以及新的、高可配置标准的加密工具、加密属性文件(encryptable properties files)、Spring work 集成、加密 Hibernate 数据源配置、新的命令行工具、UR L加密的 Apache wicket 集成以及升级文档。Jasypt 也可以与 Acegi Security 整合,即 Spring Security。Jasypt 亦拥有加密应用配置的集成功能,而且提供一个开放的 API 从而任何一个 Java Cryptography Extension 都可以使用 Jasypt出于安全考虑,Spring boot 配置文件中的敏感信息通常需要对它进行加密/脱敏处理,尽量不使用明文。开源安全框架 Jasypt Spring Boot 为 Spring Boot 应用程序中的属性源提供加密支持,专门用于处理 Spring boot 属性加密,在配置文件中使用特定格式直接配置密文,然后应用启动的时候,Jasypt 会自动将密码解密成明文供程序使用。Jasypt 加密属性配置格式:secret.property=ENC(nrmZtkF7T0kjG/VodDvBw93Ct8EgjCA+),ENC() 就是它的标识,程序启动的时候,会自动解密其中的内容,如果解密失败,则会报错。所以获取这些属性值和平时没有区别,直接使用如 @Value("${secret.property}") 获取即可,取值并不需要特殊处理。Jasypt 同一个密钥(secretKey)对同一个内容执行加密,每次生成的密文都是不一样的,但是根据根据这些密文解密成原内容都是可以的.jasypt 集成方式(含依赖)在项目中集成 jasypt-spring-boot 有三种方式:方式1:如果是 Spring Boot 应用程序,使用了注解 @SpringBootApplication 或者 @EnableAutoConfiguration,那么只需添加 jasypt-spring-boot-starter 依赖,此时整个 Spring 环境就会支持可加密属性配置(这意味着任何系统属性、环境属性、命令行参数,yaml、properties 和任何其他自定义属性源可以包含加密属性)<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>方式2:如果没有使用 @SpringBootApplication 或者 @EnableAutoConfiguration,则将 jasypt-spring-boot 添加到类路径1)依赖<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot</artifactId> <version>3.0.3</version> </dependency>2)将 @EnableEncryptableProperties 添加到配置类中,以便在整个 Spring 环境中启用可加密属性:@Configuration @EnableEncryptableProperties public class MyApplication { }方式3:如果不使用 @SpringBootApplication 或者 @EnableAutoConfiguration 自动配置注解,并且不想在整个 Spring 环境中启用可加密的属性,则可以使用本方式1)将依赖项添加到项目中<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot</artifactId> <version>3.0.3</version> </dependency>2)配置文件中添加任意数量的 @EncryptablePropertySource 注解(就像使用 Spring 的 @PropertySource 注解一样)@Configuration @EncryptablePropertySource(name = "EncryptedProperties", value = "classpath:encrypted.properties") public class MyApplication { }2)或者还可以使用 @EncryptablePropertySources 注解来对 @EncryptablePropertySource 类型的注解进行分组@Configuration @EncryptablePropertySources({@EncryptablePropertySource("classpath:encrypted.properties"), @EncryptablePropertySource("classpath:encrypted2.properties")}) public class MyApplication { }注:从 1.8 版起,@EncryptablePropertySource 支持 YAML 文件yml 配置及参数说明yml 配置# jasypt 配置加密 jasypt: encryptor: # 自定义加密盐值(密钥) password: jasypt # 加密算法设置 algorithm: PBEWithMD5AndDES iv-generator-classname: org.jasypt.iv.RandomIvGenerator salt-generator-classname: org.jasypt.salt.RandomSaltGenerator参数说明KeyRequiredDefault Value说明jasypt.encryptor.passwordTrue-自定义加密盐值(密钥)jasypt.encryptor.algorithmFalsePBEWITHHMACSHA512ANDAES_256加密算法, 必须由 JCE 提供程序支持jasypt.encryptor.key-obtention-iterationsFalse1000获取加密密钥的哈希迭代次数jasypt.encryptor.pool-sizeFalse1要创建的加密程序池的大小jasypt.encryptor.provider-nameFalseSunJCE请求加密算法的安全提供程序的名称jasypt.encryptor.provider-class-nameFalsenull jasypt.encryptor.iv-generator-classnameFalseorg.jasypt.iv.RandomIvGeneratorIV 发生器jasypt.encryptor.salt-generator-classnameFalseorg.jasypt.salt.RandomSaltGeneratorSal 发生器jasypt.encryptor.string-output-typeFalsebase64字符串输出的编码形式可用的编码类型有:base64、hexadecimal(16进制)jasypt.encryptor.proxy-property-sourcesFalsefalse jasypt.encryptor.skip-property-sourcesFalseempty list jasypt.encryptor.property.prefixFalseENC(设置密文前缀jasypt.encryptor.property.suffixFalse)设置密文后缀注:唯一需要的属性是 jasypt.encryptor.password ,其余的可以使用默认值。虽然所有这些属性都可以在属性文件中声明,但为了安全 password 属性官方不推荐存储在属性文件中,而应作为系统属性、命令行参数或环境变量传递。官网默认加解密算法为 "PBEWITHHMACSHA512ANDAES_256",它是 sha512 加 AES 高级加密,需要 Java JDK 1.9 及以上支持,或者添加 JCE 无限强度权限策略文件,否则运行会报错:”加密引发异常,一个可能的原因是您正在使用强加密算法,并且您没有在这个Java虚拟机中安装Java加密扩展(JCE)无限强权限策略文件“。默认使用 com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor 进行加解密标准 StringEncryptor 的所有属性,都可以在全局配置文件中进行配置。也可以在后台添加 StringEncryptor bean,此时默认的加密程序将被忽略。import org.jasypt.encryption.StringEncryptor; import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JasyptConfig { * 自定义 StringEncryptor,覆盖默认的 StringEncryptor * bean 名称是必需的,从 1.5 版开始按名称检测自定义字符串加密程序,默认 bean 名称为:jasyptStringEncryptor @Bean("jasyptStringEncryptor") public StringEncryptor jasyptStringEncryptor() { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); SimpleStringPBEConfig config = new SimpleStringPBEConfig(); config.setPassword("jasypt"); config.setAlgorithm("PBEWithMD5AndDES"); config.setKeyObtentionIterations("1000"); config.setPoolSize("1"); config.setProviderName("SunJCE"); config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator"); config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator"); config.setStringOutputType("base64"); encryptor.setConfig(config); return encryptor; }注意:bean 名称是必需的,因为 jasypt spring boot 从 1.5 版开始按名称检测自定义字符串加密程序,默认 bean 名称为:jasyptStringEncryptor但也可以通过定义属性来覆盖,例如 jasypt.encryptor.bean=encryptorBean,然后使用该名称定义自定义加密程序:@Bean("encryptorBean") public StringEncryptor stringEncryptor() { ... }加解密工具类要获取密文,就是将需要加密的数据进行加密,方法多样,可以通过命令行操作 jar 包获取,也可以直接使用代码进行加密。推荐使用代码加密,下面提供一个工具类进行加密,注意事项:Jasypt 默认使用 StringEncryptor 解密属性,所以加密时默认也得使用 StringEncryptor 加密,否则启动时解密失败报错StringEncryptor 接口有很多的实现类,常用 PooledPBEStringEncryptor加密与解密对 StringEncryptor 设置的属性必须要一致,比如加密时使用什么算法,那么解密时也得一样,否则启动时解密失败报错常用的加密算法为 "PBEWithMD5AndDES",官网默认的是 "PBEWITHHMACSHA512ANDAES_256",前者是 md5 加 des 标准加密,后者是 sha512 加 AES 高级加密,但需要 Java JDK 1.9 及以上支持,或者添加 JCE 无限强度权限策略文件。import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig; public class JasyptUtils { * @param password 加密盐值 * @param text 需要加密的字符串 * @return 加密后的字符串 public static String encrypt(String password, String text) { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); encryptor.setConfig(cryptor(password)); return encryptor.encrypt(text); * @param password 加密盐值 * @param text 需要解密的字符串 * @return 解密后的字符串 public static String decrypt(String password, String text) { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); encryptor.setConfig(cryptor(password)); return encryptor.decrypt(text); * 配置(对应yml中的配置) * @param password 加密盐值 * @return SimpleStringPBEConfig public static SimpleStringPBEConfig cryptor(String password) { SimpleStringPBEConfig config = new SimpleStringPBEConfig(); //设置盐值 config.setPassword(password); //设置算法配置信息 config.setAlgorithm("PBEWithMD5AndDES"); config.setKeyObtentionIterations("1000"); config.setProviderName("SunJCE"); config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator"); config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator"); config.setStringOutputType("base64"); config.setPoolSize("1"); return config; public static void main(String[] args) { // 加密 String encryptStr = encrypt("jasypt", "root"); // 解密 String decryptStr = decrypt("jasypt", encryptStr); System.out.println("加密后:" + encryptStr); System.out.println("解密后:" + decryptStr); }基本使用全局配置文件中配置如下,必须设置 jasypt.encryptor.password 属性,algorithm 算法需要与加密时使用的算法一致。想要对哪个属性进行加密,则使用 ENC() 包裹起来,然后里面放置密文即可,应用启动时会自动被解密。密文在 yml 配置文件中的使用格式:ENC(密文)示例:username: ENC(vyxdS47pJdWBF38TFdmjKmMm4zEO0FQP) password: ENC(vyxdS47pJdWBF38TFdmjKmMm4zEO0FQP)拓展:SpringBoot 项目(有正常使用@SpringBootApplication 或者@EnableAutoConfiguration注解)中可以在代码中使用@Value注解来直接获取解密后的配置值Jasypt 默认使用 StringEncryptor 解密属性,所以它默认就已经放置在了 Spring 容器中,可以直接获取使用,比如除了对配置文件中的属性加解密后,还可以做其它任何加解密操作,比如提供一个 Controller 接口用于在线加解密。因为浏览器地址栏对于特殊字符比较敏感,所以不使用默认的 base64、而改为使用 16 进制字符串jasypt: encryptor: password: wangmaox # 加密的密钥,自定义即可,必填项 algorithm: PBEWithMD5AndDES # 指定解密算法 string-output-type: hexadecimal # 设置加密内容输出的编码形式,可用的编码类型有 base64、hexadecimal(16进制)想要使用 StringEncryptor 的地方直接获取使用即可 @Resource private StringEncryptor stringEncryptor; * http://localhost:8080/jasypt/encryptor?message=12日下午17点执行任务&isEncrypt=true * http://localhost:8080/jasypt/encryptor?message=702EAA3755766C567F62E83273681A90DC684B6AFADD5CD84691778DAF4A1466E13CE0720E8BABC06081A5D6DBD90EA1&isEncrypt=false * 在线使用 {@link StringEncryptor} 加解密消息。 * @param message 加/解密的内容 * @param isEncrypt true 表示加密、false 表示解密 @GetMapping("jasypt/encryptor") public ObjectNode encrypt(@RequestParam String message, @RequestParam boolean isEncrypt) { JsonNodeFactory nodeFactory = JsonNodeFactory.instance; String encrypt = isEncrypt ? stringEncryptor.encrypt(message) : stringEncryptor.decrypt(message); ObjectNode objectNode = nodeFactory.objectNode(); objectNode.put("code", 200); objectNode.put("data", encrypt); return objectNode; }密钥(盐值)存储说明加解密过程本身都是通过盐值进行处理的,所以正常情况下盐值和加密串是分开存储的。盐值应该放在系统属性、命令行或是环境变量来使用,而不是放在配置文件。密钥传递方式:# 方式1:启动参数 java -jar jasypt-spring-boot-demo.jar --jasypt.encryptor.password=password # 方式2:系统属性 java -Djasypt.encryptor.password=password -jar jasypt-spring-boot-demo.jar # 方式3:环境变量 jasypt: encryptor: password: ${JASYPT_ENCRYPTOR_PASSWORD:password} # 也可以先设置环境变量 export JASYPT_ENCRYPTOR_PASSWORD=password java -jar jasypt-spring-boot-demo.jar去掉配置文件中设置的盐值后:在 idea 的启动配置项 “VM options” 添加:Djasypt.encryptor.password=盐值。打包后启动方式:java -jar -Djasypt.encryptor.password=盐值 xxx.jar使用 jar 包方式-加密 @echo off set/p input=待加密的明文字符串: set/p password=加密密钥(盐值): echo 加密中...... java -cp jasypt-1.9.2.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=%input% password=%password% algorithm=PBEWithMD5AndDES pause使用 jar 包方式-解密java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringDecryptionCLI password=123456 algorithm=PBEWithMD5AndDES ivGeneratorClassName=org.jasypt.iv.RandomIvGenerator input=BwNPdUi+syCTKFj/nlbI5fAtGUKuhN8r发现器、解析器EncryptablePropertyDetector:发现器该接口提供了两个方法:isEncrypted 方法:判断是否是 jasypt 约定规则加密的属性unwrapEncryptedValue 方法:密文预处理。可以自定义返回去除掉前缀和后缀的真正加密的值该接口默认的实现是 DefaultPropertyDetectorEncryptablePropertyResolver:分解器该接口中只提供了一个方法:resolvePropertyValue 方法:遍历配置文件属性,判断是否是加密属性,然后进行解密返回明文。在默认实现 DefaultPropertyResolver 中,依赖 EncryptablePropertyDetector 以及 StringEncryptor,真正解密的方法是写在StringEncryptor该接口默认的实现是 DefaultPropertyResolver自定义分解器(一般不用自定义,此处仅作示例)public class JasyptEncryptablePropertyResolver implements EncryptablePropertyResolver { //自定义解密方法 @Override public String resolvePropertyValue(String s) { if (null != s && s.startsWith(MyEncryptablePropertyDetector.ENCODED_PASSWORD_HINT)) { return PasswordUtil.decode(s.substring(MyEncryptablePropertyDetector.ENCODED_PASSWORD_HINT.length())); return s; }自定义加密算法如果不想要使用 jasypt 工具中的加密算法,或者内部要求使用某种特定的加密算法,jasypt-spring-boot 组件提供了自定义加解密的实现方式。可以通过自己实现 EncryptablePropertyDetector、EncryptablePropertyResolver 接口,并且交给 Spring 管理,设置 bean 名称为 encryptablePropertyDetector 和 encryptablePropertyResolver 来覆盖框架提供的默认实现,完成加密算法和前缀后缀的自定义。在 yml 文件指定自定义的加解密实现类注入 Spring 容器的 bean 名称#数据库配置文件加密 jasypt: encryptor: ## 实现jasypt加密解密的类 bean: customJasyptStringEncryptor自定义加解密实现类import com.test.ssmtest.encryption.utils.AESUtils; import com.test.ssmtest.encryption.utils.Sm4Utils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jasypt.encryption.StringEncryptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; * 自定义 jasypt 加解密实现类 @Slf4j public class JasyptCustomStringEncryptor implements StringEncryptor { @Autowired private AESUtils aesUtils; @Autowired private Sm4Utils sm4Utils; @Value("${jasypt.encryption.encryptedMethod:aes}") private String encryptedMethod; @Override public String encrypt(String s) { return s; @Override public String decrypt(String s) { log.info("get encrypted text:" + s); String encryptedText = null; if (StringUtils.isNotBlank(s)){ try { String encodedPrefix = s.substring(0, s.indexOf("(")).toUpperCase(); if (JasyptEncryptableDetector.ENCODED_HINT_ENC.equalsIgnoreCase(encodedPrefix)){ encodedPrefix = encryptedMethod; String ciphertext = s.substring(s.indexOf("(") + 1, s.lastIndexOf(")")); if (AESUtils.AES.equalsIgnoreCase(encodedPrefix)){ encryptedText = aesUtils.decryptAESAndDecode(ciphertext); } else if (Sm4Utils.SM4.equalsIgnoreCase(encodedPrefix)){ encryptedText = sm4Utils.decryptSM4AndDecode(ciphertext); log.info("generate decrypted text:"+ encryptedText); if (StringUtils.isNotBlank(encryptedText)){ log.info("decrypt text success!"); } else { log.error("decrypt text failed!"); } catch (Exception e) { log.error("decrypt text error!", e); return encryptedText; }自定义发现器为了支持自定义的加密属性前缀,需要自己实现 EncryptablePropertyDetector 接口import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyDetector; import java.util.Arrays; * 自定义 jasypt 发现器实现类 public class JasyptEncryptableDetector implements EncryptablePropertyDetector { private static final String[] ENCODED_HINTS = {"ENC", "AES", "SM4"}; public static final String ENCODED_HINT_ENC = "ENC"; // 判断是否是 jasypt 约定规则加密的属性 @Override public boolean isEncrypted(String s) { if (null != s && s.contains("(") && s.contains(")")) { s = s.trim(); return Arrays.asList(ENCODED_HINTS).contains(s.substring(0, s.indexOf("(")).toUpperCase()); return false; // 密文预处理。可以自定义返回去除掉前缀和后缀的真正加密的值。 // 因解密方法需要加密提示判断加密的算法,此处不去除前缀和后缀 @Override public String unwrapEncryptedValue(String s) { return s.trim(); }注册 beanimport org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration // 配置添加该注解,开启属性自动解密功能。若用jasypt-spring-boot-starter依赖包,可以不用配置该注解 // @EnableEncryptableProperties public class JasyptConfig { @Bean @ConditionalOnProperty(name = "jasypt.encryptor.bean", havingValue = "jasyptCustomStringEncryptor") public JasyptCustomStringEncryptor jasyptCustomStringEncryptor(){ return new JasyptCustomStringEncryptor(); @Bean("encryptablePropertyDetector") @ConditionalOnProperty(name = "jasypt.encryptor.bean", havingValue = "jasyptCustomStringEncryptor") public JasyptEncryptableDetector jasyptEncryptableDetector(){ return new JasyptEncryptableDetector();
加密技术概述加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:对称加密,如 AES、SM4(国密)基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。优势:算法公开、计算量小、加密速度快、加密效率高缺陷:双方都使用同样密钥,安全性得不到保证非对称加密,如 RSA、git的ssh公钥和私钥基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端 私钥是用于加密的,公钥是用于解密的。私钥加密,持有私钥或公钥才可以解密公钥加密,持有私钥才可解密优点:安全,难以破解缺点:算法比较耗时非对称算法一般是用来传送对称加密算法的密钥不可逆加密,如 MD5,SHA基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。编码器Base64原理剖析:跳转网址Base64 是网络上最常见的用于传输 8Bit字节码的编码方式之一,Base64 就是一种基于64个可打印字符来表示二进制数据的方法。Base64 一般用于在 HTTP 协议下传输二进制数据,由于 HTTP 协议是文本协议,所以在 HTTP 协议下传输二进制数据需要将二进制数据转换为字符数据。然而直接转换是不行的。因为网络传输只能传输可打印字符。可打印字符:在ASCII码中规定,0~31、127 这 33 个字符属于控制字符,32~126 这 95 个字符属于可打印字符,也就是说网络传输只能传输这 95 个字符,不在这个范围内的字符无法传输。那么该怎么才能传输其他字符呢?其中一种方式就是使用 Base64。大多数编码都是由字符串转化成二进制的过程,而 Base64 的编码则是从二进制转换为字符串。与常规恰恰相反。Base64 编码主要用在传输、存储、表示二进制领域,不能算得上加密,只是无法直接看到明文。也可以通过打乱 Base64 编码来进行加密。中文有多种编码(比如:utf-8、gb2312、gbk等),不同编码对应 Base64 编码结果都不一样。java.util.Base64 工具类:该类仅由用于获得 Base64 编码方案的编码器和解码器的静态方法组成。作用:使用 Base64 里边的编码器对数据进行编码(加密)使用 Base64 里边的解码器对数据进行解码(解密)Base64 工具类 静态方法:// 使用Basic型base64编码方案 static Base64.Encoder getEncoder() // 获取加密器(编码器) static Base64.Decoder getDecoder() // 获取解密器(解码器) // 使用MIME型base64编码方案 static Base64.Encoder getMineEncoder() // 获取加密器(编码器) static Base64.Decoder getMineDecoder() // 获取解密器(解码器) static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator) // 指定行长度和行分隔符获取加密器 // 使用 URL and Filename safe 型base64编码方案 static Base64.Encoder getUrlEncoder() // 获取加密器(编码器) static Base64.Decoder getUrlDecoder() // 获取解密器(解码器)java.util.Base64.Encoder:是Base64的内部类,用于对数据进行加密Encoder 的成员方法:String encodeToString(byte[] src) // 使用Base64编码方案将指定的字节数组编码为字符串。java.util.Base64.Decoder:是Base64的内部类,用于对数据进行解密Decoder 的成员方法: byte[] decode(String src) // 使用Base64编码方案将Base64编码的字符串解码为新分配的字节数组。Hex(十六进制)十六进制,英文名称:Hex number system(简写为 hex)是一种基数为 16 的计数系统,是一种逢 16 进 1 的进位制。通常由 0~9,A~F 组成,其中:A~F表示 10~15,这些称作十六进制数字。自定义工具类:EncoderUtilsimport java.util.Base64; public class EncoderUtils { public static final String UTF_8 = "UTF-8"; public static final String BASE_64 = "base64"; public static final String HEX = "hex"; * base64 编码 public static String encodeByteToBase64(byte[] byteArray) { return Base64.getEncoder().encodeToString(byteArray); * base64 解码 public static byte[] decodeBase64ToByte(String base64EncodedString) { return Base64.getDecoder().decode(base64EncodedString); * 将二进制转换成十六进制(Hex) public static String parseByte2HexStr(byte[] buf){ StringBuilder sb = new StringBuilder(); for (byte b : buf) { String hex = Integer.toHexString(b & 0xFF); if (hex.length() == 1) { hex = '0' + hex; sb.append(hex.toUpperCase()); return sb.toString(); * 将十六进制(Hex)转换成二进制 public static byte[] parseHexStr2Byte(String hexStr){ if (hexStr.length() < 1){ return null; } else { int half = hexStr.length() / 2; byte[] result = new byte[half]; for (int i = 0; i < half; i++) { int high = Integer.parseInt(hexStr.substring(i*2, i*2 + 1), 16); int low = Integer.parseInt(hexStr.substring(i*2 + 1, i*2 + 2), 16); result[i] = (byte) (high * 16 + low); return result; }对称加密AES 算法概述AES 加密算法全称 Advanced Encryption Standard(高级加密标准),是最为常见的对称加密算法之一。AES 加密需要:明文 + 密钥 + 偏移量(IV)+ 密码模式(算法/模式/填充)AES 解密需要:密文 + 密钥 + 偏移量(IV)+ 密码模式(算法/模式/填充)AES 的密码模式一般为 AES/CBC/PKCS5PaddingAES :加解密算法CBC :数据分组模式PKCS5Padding :数据按照一定的大小进行分组,最后分剩下那一组,不够长度,就需要进行补齐, 也可以叫 补齐模式实际工作中的加密流程在实际的工作中,客户端跟服务器交互一般都是字符串格式,所以一个比较好的加密流程是:加密流程 :明文通过 密钥 (有时也需要 偏移量 ),利用 AES 加密算法,然后通过 Base64 转码,最后生成加密后的字符串。解密流程 :加密后的字符串通过 密钥 (有时也需要 偏移量 ),利用 AES 解密算法,然后通过 Base64 转码,最后生成解密后的字符串。数据分组模式AES 的数据分组模式有以下几种:电码本模式(Electronic codebook,ECB):需要加密的消息按照块密码的块大小被分为数个块,并对每个块进行独立加密。计算器模式(CTR)密码反馈模式(CFB)输出反馈模式(OFB)密码分组链接模式(CBC)将整段明文切成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。在 CBC 模式下,使用 AES 加解密方式进行分组加解密时,需要用到的两个参数:初始化向量,也就是偏移量加解密秘钥填充模式在对数据进行加解密时,通常将数据按照固定的大小(block size)分成多个组,那么随之就产生了一个问题,如果分到最后一组,不够一个 block size 了,就需要进行补齐操作。例如电子密码本(ECB)和密文块链接(CBC)为对称密钥加密设计的块密码工作模式要求输入明文长度必须是块长度的整数倍,因此信息必须填充至满足要求。常见填充模式:算法/模式/填充16字节加密后数据长度不满16字节加密后长度AES/CBC/NoPadding16不支持AES/CBC/PKCS5Padding3216AES/CBC/ISO10126Padding3216AES/CFB/NoPadding16原始数据长度AES/CFB/PKCS5Padding3216AES/CFB/ISO10126Padding3216AES/ECB/NoPadding16不支持AES/ECB/PKCS5Padding3216AES/ECB/ISO10126Padding3216AES/OFB/NoPadding16不支持AES/OFB/PKCS5Padding3216AES/OFB/ISO10126Padding3216AES/PCBC/NoPadding16不支持AES/PCBC/PKCS5Padding3216AES/PCBC/ISO10126Padding3216偏移量一般是为了增加 AES 加密的复杂度,增加数据的安全性。一般在 AES_256 中会使用到偏移量 ,而在 AES_128 加密中不会使用到字符集在 AES 加密中,特别也要注意到字符集的问题。一般用到的字符集是 utf-8 和 gbk 。AES 具体的加密流程介绍明文 P:没有经过加密的数据密钥 K:用来加密明文的密码,在对称加密算法中,加密与解密的密钥是相同的密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,通常是通过非对称加密算法加密密钥,然后再通过网络传输给对方,或者直接面对面商量密钥。密钥是绝对不可以泄漏的,否则会被攻击者还原密文,窃取机密数据。AES 加密函数:把明文 P 和密钥 K 作为加密函数的参数输入,则加密函数E会输出密文 C密文 C:经加密函数处理后的数据AES 解密函数:把密文 C 和密钥 K 作为解密函数的参数输入,则解密函数会输出明文 PAES加密方法AES为分组加密,分组加密也就是把明文分成一组一组的,每组的长度相等,每次加密一组数据,直到加密完整个明文。AES密钥长度(32位比特字)分组长度(32位比特字)加密轮数AES-1284410AES-1926412AES-2568414在AES标准规范中,分组长度只能是128位,也就是说,每个分组为16个字节,每个字节8位,密钥的长度可以使用128位、192位或者258位。密钥的长度不同,推荐加密轮数也不同,比如 AES-128 也就是密钥的长度为128位,加密轮数为10轮,AES-192为12轮,AES-256为14轮。以AES-128为例,加密中一轮的4个操作:字节代换、行位移、列混合、轮密钥加字节代换:AES的字符代换其实就是一个简单的查表操作,AES定义了一个S盒和一个逆S盒。行位移:就是一个简单的左循环移位操作。列混合:是通过矩阵相乘来实现的,经过移位后的状态矩阵与固定的矩阵相乘,得到混淆后的状态矩阵。轮密钥加:是将128位轮密钥Ki同状态矩阵中的数据进行逐位异或操作。自定义工具类:AESUtilsimport lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; * AES-128-CBC加密模式 @Slf4j @NoArgsConstructor @AllArgsConstructor @Data @Component public class AESUtils { public static final String AES = "AES"; // 填充模式,"算法/模式/补码方式" public static final String PADDING = "AES/CBC/PKCS5Padding"; // 密钥,长度16位,可以用26个字母和数字组成 private String key = "hj7x89H$yuBI0456"; // 偏移量,长度16位。增加加密算法的强度 private String iv = "NIfb&95GUY86Gfgh"; * @Description AES算法加密明文 * @param data 明文 * @return 密文字节数组 public byte[] encryptAES(String data) throws Exception { // "算法/模式/补码方式" Cipher cipher = Cipher.getInstance(PADDING); SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES); // CBC模式,需要一个向量iv,可增加加密算法的强度 IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes()); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); return cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); public String encryptAESAndEncoder(String data, String strOutType) throws Exception { byte[] encrypted = encryptAES(data); if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){ // BASE64做转码。同时能起到2次加密的作用。 return EncoderUtils.encodeByteToBase64(encrypted).trim(); } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){ // 将二进制转换成十六进制 return EncoderUtils.parseByte2HexStr(encrypted).trim(); } else { log.warn("can not match strOutType, default use hex"); // 将二进制转换成十六进制 return EncoderUtils.parseByte2HexStr(encrypted).trim(); public String encryptAESAndHexEncoder(String data) throws Exception { return encryptAESAndEncoder(data, EncoderUtils.HEX); * @Description AES算法解密密文 * @param data 密文 * @return 明文 public String decryptAES(byte[] data) throws Exception { // "算法/模式/补码方式" Cipher cipher = Cipher.getInstance(PADDING); SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes()); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); return new String(cipher.doFinal(data)); public String decryptAESAndDecode(String data, String strOutType) throws Exception { byte[] decodeData = null; if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){ decodeData = EncoderUtils.decodeBase64ToByte(data); } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){ decodeData = EncoderUtils.parseHexStr2Byte(data); } else { log.warn("can not match strOutType, default use hex"); decodeData = EncoderUtils.parseHexStr2Byte(data); return decryptAES(decodeData); public String decryptAESAndHexDecode(String data) throws Exception { return decryptAESAndDecode(data, EncoderUtils.HEX); public boolean verifyAES(String cipherText, String paramStr, String strOutType) throws Exception { byte[] decodeData = null; if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){ decodeData = EncoderUtils.decodeBase64ToByte(cipherText); } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){ decodeData = EncoderUtils.parseHexStr2Byte(cipherText); } else { log.warn("can not match strOutType, default use hex"); decodeData = EncoderUtils.parseHexStr2Byte(cipherText); return paramStr.equalsIgnoreCase(decryptAES(decodeData)); public boolean verifyAES(String cipherText, String paramStr) throws Exception { return verifyAES(cipherText, paramStr, EncoderUtils.HEX); public static void main(String[] args) throws Exception { // 待加密的字串 String data = "password123"; AESUtils aesUtils = new AESUtils(); String encryptedData = aesUtils.encryptAESAndHexEncoder(data); System.out.println("加密结果:" + encryptedData); String decryptedData = aesUtils.decryptAESAndHexDecode(encryptedData); System.out.println("解密结果:" + decryptedData); }SM4 算法概述SM4 分组加密算法是中国无线标准中使用的分组加密算法,在2012年被国家商用密码管理局确定为国家密码行业标准,标准编号 GM/T 0002-2012 并且改名为 SM4 算法,与 SM2 椭圆曲线公钥密码算法,SM3 密码杂凑算法共同作为国家密码的行业标准,在我国密码行业中有着极其重要的位置。SM4 算法的密钥长度和分组长度均为 128 bit,加解密算法均采用 32 轮非平衡 Feistel 迭代结构,该结构最先出现在分组密码 LOKI 的密钥扩展算法中。SM4 通过 32 轮非线性迭代后加上一个反序变换,这样只需要解密密钥是加密密钥的逆序,就能使得解密算法与加密算法保持一致。故 SM4 算法的加解密过程中使用的算法是完全相同的,唯一不同点在于该算法的解密密钥是由它的加密密钥进行逆序变换后得到的。基本运算:SM4 密码算法使用模2加和循环移位作为基本运算。基本密码部件:SM4 密码算法使用了 S 盒、非线性变换 τ、线性变换部件 L、合成变换 T 基本密码部件。轮函数:SM4 密码算法采用对基本轮函数进行迭代的结构。利用上述基本密码部件,便可构成轮函数。SM4 密码算法的轮函数是一种以字为处理单位的密码函数。加密算法:SM4 密码算法是一个分组算法。数据分组长度为 128 比特,密钥长度为128比特。加密算法采用32轮迭代结构,每轮使用一个轮密钥。解密算法:SM4 密码算法是对合运算,因此解密算法与加密算法的结构相同,只是轮密铝的使用顺序相反,解密轮密钥是加密轮密钥的逆序。密钥扩展算法:SM4 密码算法使用 128 位的加密密钥,并采用 32 轮法代加密结构,每一轮加密使用一个 32 位的轮密钥,共使用 32 个轮密钥。因此需要使用密钥扩展算法,从加密密钥产生出 32 个轮密钥。SM4 的安全性:SM4 密码算法经过我国专业密码机构的充分分析测试,可以抵抗差分攻击、线性攻击等现有攻击,因此是安全的。注:S 盒是一种利用非线性变换构造的分组密码的一个组件,主要是为了实现分组密码过程中的混淆的特性和设计的。SM4 算法中的 S 盒在设计之初完全按照欧美分组密码的设计标准进行,它采用的方法是能够很好抵抗差值攻击的仿射函数逆映射复合法。自定义工具类:Sm4Utilsimport javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.security.Security; import java.util.Arrays; import java.util.Base64; import java.util.Base64.Encoder; import java.util.Base64.Decoder; @Slf4j @NoArgsConstructor @AllArgsConstructor @Data @Component public class Sm4Utils { static { // 注册 BouncyCastle Security.addProvider(new BouncyCastleProvider()); public static final String SM4 = "SM4"; // 加密算法/分组加密模式/分组填充方式 // PKCS5Padding-以8个字节为一组进行分组加密 // 定义分组加密模式使用:PKCS5Padding public static final String PADDING = "SM4/ECB/PKCS5Padding"; // 128-32位16进制;256-64位16进制 public static final int DEFAULT_KEY_SIZE = 128; // 密钥,长度16位,可以用26个字母和数字组成 private String key = "hj7x89H$yuBI0456"; private static Cipher generateEcbCipher(int mode, byte[] key) throws Exception { Cipher cipher = Cipher.getInstance(Sm4Utils.PADDING, BouncyCastleProvider.PROVIDER_NAME); cipher.init(mode, new SecretKeySpec(key, SM4)); return cipher; public byte[] encryptSM4(byte[] data) throws Exception { Cipher cipher = generateEcbCipher(Cipher.ENCRYPT_MODE, key.getBytes(StandardCharsets.UTF_8)); return cipher.doFinal(data); public String encryptSM4AndEncoder(String data, String strOutType) throws Exception { byte[] encrypted = encryptSM4(data.getBytes(StandardCharsets.UTF_8)); if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){ // BASE64做转码。同时能起到2次加密的作用。 return EncoderUtils.encodeByteToBase64(encrypted).trim(); } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){ // 将二进制转换成十六进制 return EncoderUtils.parseByte2HexStr(encrypted).trim(); } else { log.warn("can not match strOutType, default use hex"); // 将二进制转换成十六进制 return EncoderUtils.parseByte2HexStr(encrypted).trim(); public String encryptSM4AndHexEncoder(String data) throws Exception { return encryptSM4AndEncoder(data, EncoderUtils.HEX); public byte[] decryptSM4(byte[] cipherText) throws Exception { Cipher cipher = generateEcbCipher(Cipher.DECRYPT_MODE, key.getBytes(StandardCharsets.UTF_8)); return cipher.doFinal(cipherText); public String decryptSM4AndDecode(String data, String strOutType) throws Exception { byte[] decodeData = null; if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){ decodeData = EncoderUtils.decodeBase64ToByte(data); } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){ decodeData = EncoderUtils.parseHexStr2Byte(data); } else { log.warn("can not match strOutType, default use hex"); decodeData = EncoderUtils.parseHexStr2Byte(data); return new String(decryptSM4(decodeData)); public String decryptSM4AndHexDecode(String data) throws Exception { return decryptSM4AndDecode(data, EncoderUtils.HEX); public boolean verifySM4(String cipherText, String paramStr, String strOutType) throws Exception { byte[] decodeData = null; if (EncoderUtils.BASE_64.equalsIgnoreCase(strOutType)){ decodeData = EncoderUtils.decodeBase64ToByte(cipherText); } else if (EncoderUtils.HEX.equalsIgnoreCase(strOutType)){ decodeData = EncoderUtils.parseHexStr2Byte(cipherText); } else { log.warn("can not match strOutType, default use hex"); decodeData = EncoderUtils.parseHexStr2Byte(cipherText); return Arrays.equals(decryptSM4(decodeData), paramStr.getBytes(StandardCharsets.UTF_8)); public boolean verifySM4(String cipherText, String paramStr) throws Exception { return verifySM4(cipherText, paramStr, EncoderUtils.HEX); public static void main(String[] args) throws Exception { // 待加密的字串 String data = "张三"; Sm4Utils sm4Utils = new Sm4Utils(); String encryptedData = sm4Utils.encryptSM4AndHexEncoder(data); System.out.println("加密结果:" + encryptedData); String decryptedData = sm4Utils.decryptSM4AndHexDecode(encryptedData); System.out.println("解密结果:" + decryptedData); }非对称加密RSA 算法概述RSA是基于大数因子分解难题。目前各种主流计算机语言都支持RSA算法的实现java6及以上版本支持RSA算法RSA算法可以用于数据加密和数字签名RSA算法相对于 DES/AES 等对称加密算法,速度要慢的多总原则:公钥加密,私钥解密 / 私钥加密,公钥解密使用方式RSA算法构建密钥对简单,这里以甲乙双方发送数据为模型甲方在本地构建密钥对(公钥+私钥),并将公钥公布给乙方甲方将数据用私钥进行加密,发送给乙方乙方用甲方提供的公钥对数据进行解密如果乙方向传送数据给甲方:乙方用公钥对数据进行加密,然后传送给甲方甲方用私钥对数据进行解密自定义工具类:RSAUtilsimport org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.HashMap; import java.util.Map; * 非对称加密算法RSA算法组件 public class RSAUtils { //非对称密钥算法 public static final String KEY_ALGORITHM = "RSA"; * 密钥长度,DH算法的默认密钥长度是1024 * 密钥长度必须是64的倍数,在512到65536位之间 private static final int KEY_SIZE = 512; private static final String PUBLIC_KEY = "RSAPublicKey"; private static final String PRIVATE_KEY = "RSAPrivateKey"; * 初始化密钥对 * @return Map 甲方密钥的Map public static Map<String, Object> initKey() throws Exception { //实例化密钥生成器 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM); //初始化密钥生成器 keyPairGenerator.initialize(KEY_SIZE); //生成密钥对 KeyPair keyPair = keyPairGenerator.generateKeyPair(); //甲方公钥 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); //甲方私钥 RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); //将密钥存储在map中 Map<String, Object> keyMap = new HashMap<String, Object>(); keyMap.put(PUBLIC_KEY, publicKey); keyMap.put(PRIVATE_KEY, privateKey); return keyMap; * 私钥加密 * @param data 待加密数据 * @param key 密钥 * @return byte[] 加密数据 public static byte[] encryptByPrivateKey(byte[] data, byte[] key) throws Exception { //取得私钥 PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(key); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); //生成私钥 PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec); //数据加密 Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE, privateKey); return cipher.doFinal(data); * 公钥加密 * @param data 待加密数据 * @param key 密钥 * @return byte[] 加密数据 public static byte[] encryptByPublicKey(byte[] data, byte[] key) throws Exception { //实例化密钥工厂 KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); //初始化公钥 //密钥材料转换 X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(key); //产生公钥 PublicKey pubKey = keyFactory.generatePublic(x509KeySpec); //数据加密 Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE, pubKey); return cipher.doFinal(data); * 私钥解密 * @param data 待解密数据 * @param key 密钥 * @return byte[] 解密数据 public static byte[] decryptByPrivateKey(byte[] data, byte[] key) throws Exception { //取得私钥 PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(key); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); //生成私钥 PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec); //数据解密 Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(data); * 公钥解密 * @param data 待解密数据 * @param key 密钥 * @return byte[] 解密数据 public static byte[] decryptByPublicKey(byte[] data, byte[] key) throws Exception { //实例化密钥工厂 KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); //初始化公钥 //密钥材料转换 X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(key); //产生公钥 PublicKey pubKey = keyFactory.generatePublic(x509KeySpec); //数据解密 Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); cipher.init(Cipher.DECRYPT_MODE, pubKey); return cipher.doFinal(data); * 取得私钥 * @param keyMap 密钥map * @return byte[] 私钥 public static byte[] getPrivateKey(Map<String, Object> keyMap) { Key key = (Key) keyMap.get(PRIVATE_KEY); return key.getEncoded(); * 取得公钥 * @param keyMap 密钥map * @return byte[] 公钥 public static byte[] getPublicKey(Map<String, Object> keyMap) throws Exception { Key key = (Key) keyMap.get(PUBLIC_KEY); return key.getEncoded(); public static void main(String[] args) throws Exception { //初始化密钥 //生成密钥对 Map<String, Object> keyMap = RSACoder.initKey(); byte[] publicKey = RSACoder.getPublicKey(keyMap); byte[] privateKey = RSACoder.getPrivateKey(keyMap); System.out.println("公钥:" + Base64.encodeBase64String(publicKey)); System.out.println("私钥:" + Base64.encodeBase64String(privateKey)); System.out.println("==========密钥对构造完毕,甲方将公钥公布给乙方,开始进行加密数据的传输=========="); String str = "RSA密码交换算法"; System.out.println("===========甲方向乙方发送加密数据=============="); System.out.println("原文:" + str); //甲方进行数据的加密 byte[] code1 = RSACoder.encryptByPrivateKey(str.getBytes(), privateKey); System.out.println("加密后的数据:" + Base64.encodeBase64String(code1)); System.out.println("===========乙方使用甲方提供的公钥对数据进行解密=============="); //乙方进行数据的解密 byte[] decode1 = RSACoder.decryptByPublicKey(code1, publicKey); System.out.println("乙方解密后的数据:" + new String(decode1)); System.out.println("===========反向进行操作,乙方向甲方发送数据=============="); str = "乙方向甲方发送数据RSA算法"; System.out.println("原文:" + str); //乙方使用公钥对数据进行加密 byte[] code2 = RSACoder.encryptByPublicKey(str.getBytes(), publicKey); System.out.println("===========乙方使用公钥对数据进行加密=============="); System.out.println("加密后的数据:" + Base64.encodeBase64String(code2)); System.out.println("=============乙方将数据传送给甲方======================"); System.out.println("===========甲方使用私钥对数据进行解密=============="); //甲方使用私钥对数据进行解密 byte[] decode2 = RSACoder.decryptByPrivateKey(code2, privateKey); System.out.println("甲方解密后的数据:" + new String(decode2)); }拓展javax.crypto.Cipher 类javax.crypto.Cipher 类是从 jdk1.4 就开始引入,所属 jdk 拓展包 Java Cryptographic Extension(JCE)框架,该框架主要用于加密解密和密码功能。目前已支持的算法总共支持以下加密算法: AES / DES / DESede / RSA类型值密码长度说明AES/CBC/NoPadding128AES算法的CBC模式实现AES/CBC/PKCS5Padding128AES算法的CBC模式实现, 并用PKCS5Padding规则填充AES/ECB/NoPadding128AES算法的ECB模式实现AES/ECB/PKCS5Padding128AES算法的ECB模式实现, 并用PKCS5Padding规则填充DES/CBC/NoPadding56 DES/CBC/PKCS5Padding56 DESede/CBC/NoPadding168 DESede/CBC/PKCS5Padding168 DESede/ECB/NoPadding168 DESede/ECB/PKCS5Padding168 RSA/ECB/PKCS1Padding(1024, 2048)密码长度有范围可选RSA/ECB/OAEPWithSHA-1AndMGF1Padding(1024, 2048)密码长度有范围可选RSA/ECB/OAEPWithSHA-256AndMGF1Padding(1024, 2048)密码长度有范围可选常见的 加密模式 有以下几种:ECB(Electronic Codebook Book,电码本模式):将明文分成若干小段,然后对每小段进行加密CBC(Cipher Block Chaining,密码分组链接模式):先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密常见的 填充规则 有以下几种:大部分情况下,明文并非刚好 N 位的倍数。对于最后一个分组,如果长度小于N位,则需要用数据填充至 N 位Cipher 类里面常用方法:// 获取 Cipher 实现指定转换的对象 public static final Cipher getInstance(String transformation, String provider) // 使用密钥和一组算法参数初始化此密码 public final void init(int opmod, Key key); public final void init(int opmod, Certificate certificate); public final void init(int opmod, Key key, SecureRandom random); public final void init(int opmod, Certificate certificate, SecureRandom random); public final void init(int opmod, Key key, AlgorithmParameterSpec params); public final void init(int opmod, Key key, AlgorithmParameterSpec params, SecureRandom random); public final void init(int opmod, Key key, AlgorithmParameters params); public final void init(int opmod, Key key, AlgorithmParameters params,SecureRandom random); // 完成多部分加密或解密操作,具体取决于此密码的初始化方式 public final byte[] doFinal(byte[] input); public final byte[] doFinal(byte[] input, int inputOffset, int inputLen); public final int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output); public final int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output,int outputOffset); // 在多步加密或解密数据时,首先需要一次或多次调用update方法,用以提供加密或解密的所有数据 public byte[] update(byte[] input); public byte[] update(byte[] input, int inputOffset, int inputLen); public int update(byte[]input, int inputOffset, int inputLen, byte[] output); public int update(byte[]input, int inputOffset, int inputLen, byte[] output, int outputOffset); // 如果还有输入数据,多步操作可以使用前面提到的doFinal方法之一结束。如果没有数据,多步操作可以使用下面的doFinal方法之一结束 public final byte[] doFinal(); public final int doFinal(byte[] output, int outputOffset);其中:通过调用 Cipher 类中的 getInstance() 静态工厂方法得到 Cipher 对象。参数 transformation :密码学算法名称,比如 DES,也可以在后面包含模式和填充方式两种写法:"算法/模式/填充",示例:"DES/CBC/PKCS5Padding""算法",示例:"DES"如果没有指定模式或填充方式,就使用特定提供者指定的默认模式或默认填充方式。例如,SunJCE 提供者使用 ECB 作为 DES、DES-EDE 和 Blowfish 等 Cipher 的默认模式,并使用 PKCS5Padding 作为它们默认的填充方案。这意味着在 SunJCE 提供者中,下列形式的声明是等价的:Cipherc1=Cipher.getInstance("DES/ECB/PKCS5Padding");Cipher c1=Cipher.getInstance("DES");getInstance() 工厂方法返回的对象没有进行初始化,因此在使用前必须进行初始化。通过 getInstance() 得到的 Cipher 对象必须使用下列四个模式之一进行初始化,这四个模式在 Cipher 类中被定义为 finalinteger常数,可以使用符号名来引用这些模式:ENCRYPT_MODE :加密数据DECRYPT_MODE :解密数据WRAP_MODE :将一个 Key 封装成字节,可以用来进行安全传输UNWRAP_MODE :将前述已封装的密钥解开成 java.security.Key 对象每个 Cipher 初始化方法使用一个模式参数 opmod,并用此模式初始化 Cipher 对象。此外还有其他参数,包括密钥 key、包含密钥的证书 certificate、算法参数 params 和随机源 random。必须指出的是,加密和解密必须使用相同的参数。当 Cipher 对象被初始化时,它将失去以前得到的所有状态。如果在 transformation 参数部分指定了 padding 或 unpadding 方式,则所有的 doFinal 方法都要注意所用的 padding 或unpadding 方式。调用 doFinal 方法将会重置 Cipher 对象到使用 init 进行初始化时的状态,就是说,Cipher 对象被重置,使得可以进行更多数据的加密或解密,至于这两种模式,可以在调用 init 时进行指定。包裹 wrap 密钥必须先使用 WRAP_MODE 初始化 Cipher 对象,然后调用以下方法:public final byte[] wrap(Key key);如果将调用 wrap 方法的结果(wrap 后的密钥字节)提供给解包裹 unwrap 的人使用,必须给接收者发送以下额外信息:密钥算法名称密钥算法名称可以调用 Key 接口提供的 getAlgorithm 方法得到:public String getAlgorithm();被包裹密钥的类型(Cipher.SECRET_KEY,Cipher.PRIVATE_KEY,Cipher.PUBLIC_KEY)为了对调用 wrap 方法返回的字节进行解包,必须先使用 UNWRAP_MODE 模式初始化 Ciphe r对象,然后调用以下方法:public final Keyunwrap(byte[] wrappedKey,String wrappedKeyAlgorithm,int wrappedKeyType));其中:参数 wrappedKey 是调用wrap方法返回的字节参数 wrappedKeyAlgorithm 是用来包裹密钥的算法参数 wrappedKeyType 是被包裹密钥的类型,该类型必须是Cipher.SECRET_KEY, Cipher.PRIVATE_KEY, Cipher.PUBLIC_KEY 三者之一
概述参考:squirrel-foundation状态机的使用细节有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型状态机可以描述核心业务规则,核心业务内容,总结一下就是 a 实体,在 b 状态下,由 c 角色,在满足 d 条件时,执行 e 操作成功后,迁移到f状态下,并产生 g 事件,通常用于业务复杂的订单等业务里。生命周期状态机创建过程StateMachineStateMachine 实例由 StateMachineBuilder 创建不被共享对于使用 annotation 方式(或fluent api)定义的 StateMachine,StateMachine 实例即根据此定义创建,相应的 action 也由本实例执行,与 spring 的集成最终要的就是讲 spring 的 bean 实例注入给由 builder 创建的状态机实例;StateMachineBuilder本质上是由 StateMachineBuilderFactory 创建的动态代理。被代理的 StateMachineBuilder 默认实现为 StateMachineBuilderImpl,内部描述了状态机实例创建细节包括 State、Event、Context 类型信息、constructor 等,同时也包含了 StateMachine 的一些全局共享资源包括 StateConverter、EventConverter、MvelScriptManage r等。StateMachineBuilder 可被复用,使用中可被实现为 singleton;StateMachineBuilderFactory:为StateMachineBuilder创建的动态代理实例;事件处理过程状态正常迁移TransitionBegin--(exit->transition->entry)-->TransitionComplete-->TransitionEnd状态迁移异常TransitionBegin--(exit->transition->entry)-->TransitionException-->TransitionEnd状态迁移事件拒绝TransitionBegin-->TransitionDeclined-->TransitionEnd状态持久化与事务状态持久化从 StateMachine 的事件响应流程中可以看到,TransitionBegin --(exit -> transition -> entry) --> TransitionComplete -->TransitionEnd,在 TransitionComplete 发生一个状态已从 source 迁移到了 target 状态,所以可以选择在这个时间点进行了状态的持久化(没有选择 TransitionEnd 做持久化,因为某些场景在持久化完成后还会存在一些外部动作的触发,例如通知第三方系统当前状态已完成变更)。代码详见声明式状态机TransitionComplete()方法。分布式锁 + 事务由于 StateMachine 实例不是由 Spring 容器创建,所以这个过程中无法通过注解方式开启事务(Spring没有机会去创建事务代理),采用了编程式事务,在 AbstractStateMachineEngine 的 fire 函数中隐式的实现(代码详见:声明式状态机AbstractStateMachineEngine#fire)案例依赖<dependency> <groupId>org.squirrelframework</groupId> <artifactId>squirrel-foundation</artifactId> <version>0.3.8</version> </dependency>声明式无类型状态机参考:squirrel-foundation-demoSpring基于状态机squirrel-foundation简单使用Squirrel(松鼠)状态机的使用利用状态机模拟一个订单的支付过程状态机引擎/** * 创建无类型化状态机,简化状态机,防止过多泛化导致代码不易阅读 * 通过Spring创建StateMachineBuilder实例,通过buidler创建状态机(单例) * 业务函数中通过StateMachineBuilder实例创建StateMachine实例,并向StateMachine暴露SpringApplicationContext,以便于StateMachine通过ApplicationContext获取数据层的对象 @Slf4j public class AbstractStateMachineEngine <T extends UntypedStateMachine> implements ApplicationContextAware { private ApplicationContext applicationContext; protected UntypedStateMachineBuilder stateMachineBuilder = null; @SuppressWarnings("unchecked") public AbstractStateMachineEngine() { //识别泛型参数 Class<T> genericType = (Class<T>) GenericTypeResolver.resolveTypeArgument(getClass(), AbstractStateMachineEngine.class); stateMachineBuilder = StateMachineBuilderFactory.create(genericType, ApplicationContext.class); //注入applicationContext,并在创建StateMachine实例时注入 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; * 可以通过向OrderContext 上下文传递一些业务参数,比如orderId等等 public boolean fire(EOrderEvents event, OrderContext context) { T stateMachine = stateMachineBuilder.newUntypedStateMachine( context.geteOrder().getOrderStatus(),applicationContext); // 添加监听器 // stateMachine.addStateMachineListener(new StateMachineListener<UntypedStateMachine, Object, Object, Object>() { // @Override // public void stateMachineEvent(StateMachineEvent<UntypedStateMachine, Object, Object, Object> event) { // log.info("lastState: " + event.getStateMachine().getLastState()); // } // }); // stateMachine.addDeclarativeListener(new DeclarativeEventListener()); // 源码中的日志 demo // StateMachineLogger logger = new StateMachineLogger(stateMachine); // logger.startLogging(); //由于StateMachine实例不是由Spring容器创建,所以这个过程中无法通过注解方式开启事务(Spring没有机会去创建事务代理),因此采用了编程式事务 DataSourceTransactionManager transactionManager = (DataSourceTransactionManager)applicationContext.getBean("transactionManager"); DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransaction(def); try { stateMachine.fire(event, context); transactionManager.commit(status); //这里会返回状态机是否出错,如果出错可用于通知Controller层 return stateMachine.isError(); } catch (Exception ex) { //用于事务回滚 transactionManager.rollback(status); return true; }继承AbstractStateMachineEngine,并加入@Service注解,便可以在项目中注入。(单例)@Service public class OrderStateMachineEngine extends AbstractStateMachineEngine<SubmitOrderStateMachine>{ }定义状态机(基于注解的方式)import lombok.extern.slf4j.Slf4j; import org.squirrelframework.foundation.fsm.*; import org.squirrelframework.foundation.fsm.annotation.StateMachineParameters; import org.squirrelframework.foundation.fsm.impl.AbstractUntypedStateMachine; * 定义 触发事件、状态变化时,调用的方法 * @States 定义状态列表,里面可以包含多个状态 * @State定义每个状态,name状态名称,entryStateInit进入状态时调用的方法,exitCallMethod 离开状态是调用的方法,initialState 为true时,为默认状态。 @States({ @State(name = "INIT", entryCallMethod = "entryStateInit", exitCallMethod = "exitStateInit", initialState = true), @State(name = "WAIT_PAY", entryCallMethod = "entryStateWaitPay", exitCallMethod = "exitStateWaitPay"), @State(name = "WAIT_SEND", entryCallMethod = "entryStateWaitSend", exitCallMethod = "exitStateWaitSend"), @State(name = "PART_SEND", entryCallMethod = "entryStatePartSend", exitCallMethod = "exitStatePartSend"), @State(name = "WAIT_RECEIVE", entryCallMethod = "entryStateWaitReceive", exitCallMethod = "exitStateWaitReceive"), @State(name = "COMPLETE", entryCallMethod = "entryStateComplete", exitCallMethod = "exitStateComplete") @Transitions({ @Transit(from = "INIT", to = "WAIT_PAY", on = "SUBMIT_ORDER", callMethod = "submitOrder"), @Transit(from = "WAIT_PAY", to = "WAIT_SEND", on = "PAY", callMethod = "pay"), @Transit(from = "WAIT_SEND", to = "PART_SEND", on = "PART_SEND", callMethod = "partSend"), @Transit(from = "PART_SEND", to = "WAIT_RECEIVE", on = "SEND", callMethod = "send"), @Transit(from = "WAIT_RECEIVE", to = "COMPLETE", on = "COMPLETE", callMethod = "complete") // @StateMachineParameters用来声明状态机泛型参数类型,向AbstractStateMachine传递参数 @StateMachineParameters(stateType = OrderState.class, eventType = OrderEvent.class, contextType = OrderContext.class) @Slf4j public class SubmitOrderStateMachine extends AbstractStateMachine<UntypedStateMachine, Object, Object, Object> implements UntypedStateMachine { private OrderService orderService; protected ApplicationContext applicationContext; //定义构造函数接受ApplicationContext注入 //([参看New State Machine Instance](http://hekailiang.github.io/squirrel/)) public SubmitOrderStateMachine(ApplicationContext applicationContext) { this.applicationContext = applicationContext; // 通过applicationContext注入orderService this.orderService = applicationContext.getBean(OrderService.class); // 状态转换时调用的方法,需要将方法名配置在 callMethod 内 // 若【方法名】符合 transitFrom[fromStateName]To[toStateName] 格式,不需要配置 callMethod public void submitOrder(OrderState fromState, OrderState toState, OrderEvent Event, OrderContext Context) { log.info("转换事件 {}=>{} on {} with {}.", fromState, toState, event, context); orderService.submitOrder(toState); public void complete(OrderState fromState, OrderState toState, OrderEvent Event, OrderContext Context) { log.info("转换事件 {}=>{} on {} with {}.", fromState, toState, event, context); System.out.println("complete"); // 符合 entry[StateName] 格式,不需要配置 callMethod public void entryStateInit(OrderState fromState, OrderState toState, OrderEvent Event, OrderContext Context) { log.info("进入状态 {}=>{} on {} with {}.", from, to, event, context); System.out.println("entryStateInit"); // 符合 exit[StateName] 格式,不需要配置 callMethod public void exitStateInit(OrderState fromState, OrderState toState, OrderEvent Event, OrderContext Context) { log.info("退出状态 {}=>{} on {} with {}.", from, to, event, context); System.out.println("exitStateInit"); // ========================================================================================== // 如果不想用 DeclarativeEventListener 这种声明在单独类里的方法,可以直接重写以下方法,效果是一样的 // 两者同时用也可以,为了代码方便最好别这样 // ========================================================================================== @Override protected void afterTransitionCausedException(Object fromState, Object toState, Object event, Object context) { * 当状态转换过程中出现异常,已执行的action列表将失效并且状态机会进入error状态,意思就是状态机实例不会再处理任何event。假如用户继续向状态机发送event,便会抛出IllegalStateException异常。所有状态转换过程中发生的异常,包括action执行和外部listener调用,会被包装成TransitionException(未检查异常)。目前,默认的异常处理策略非常简单并且粗暴的连续抛出异常,可以参阅AbstractStateMachine.afterTransitionCausedException方法。 log.info("Override 发生错误 {}", getLastException().getMessage()); Throwable targeException = getLastException().getTargetException(); // recover from IllegalArgumentException thrown out from state 'A' to 'B' caused by event 'ToB' if(targeException instanceof IllegalArgumentException && fromState.equals("A") && toState.equals("B") && event.equals("ToB")) { // do some error clean up job here // ... // after recovered from this exception, reset the state machine status back to normal setStatus(StateMachineStatus.IDLE); } else if(...) { // recover from other exception ... } else { // afterTransitionCausedException 默认的实现是直接抛异常 super.afterTransitionCausedException(fromState, toState, event, context); @Override protected void beforeTransitionBegin(Object fromState, Object event, Object context) { // 转换开始时被调用 System.out.println(); // super.beforeTransitionBegin(fromState, event, context); log.info("Override beforeTransitionBegin"); @Override protected void afterTransitionCompleted(Object fromState, Object toState, Object event, Object context) { // 转换完成时被调用 // super.afterTransitionCompleted(fromState, toState, event, context); log.info("Override afterTransitionCompleted"); if (context instanceof StateMachineContext && toState instanceof State) { StateMachineContext stateMachineContext = (StateMachineContext)context; //从上下文中获取需要持久化的数据,例如订单ID等 Rma rma = stateMachineContext.get(MessageKeyEnum.RMA); //持久化 rma.setStatus((State)toState); this.applicationContext.get("rmaRepository").updateRma(rma); } else { throw new Exception("type not support, context expect " + StateMachineContext.class.getSimpleName() + ", actually " + context.getClass().getSimpleName() + ", state expect " + State.class.getSimpleName() + ", actually " + toState.getClass().getSimpleName()); @Override protected void afterTransitionEnd(Object fromState, Object toState, Object event, Object context) { // 转换结束时被调用 // super.afterTransitionEnd(fromState, toState, event, context); log.info("Override afterTransitionEnd"); @Override protected void afterTransitionDeclined(Object fromState, Object event, Object context) { // 当转换被拒绝时被调用。实际是调用 callMethod 中的方法被调用时,抛出异常时被调用 // super.afterTransitionDeclined(fromState, event, context); log.info("Override afterTransitionDeclined"); @Override protected void beforeActionInvoked(Object fromState, Object toState, Object event, Object context) { // 当转换开始时被调用。实际是 callMethod 中的方法被调用时,先调用该方法。类似于 AOP 的效果 // super.beforeActionInvoked(fromState, toState, event, context); log.info("Override beforeActionInvoked"); @Override protected void afterActionInvoked(Object fromState, Object toState, Object event, Object context) { // 当转换结束时被调用。实际是 callMethod 被调用后,调用该方法。类似于 AOP 的效果 // super.afterActionInvoked(fromState, toState, event, context); log.info("Override afterActionInvoked"); }定义状态枚举、事件枚举、上下文定义状态枚举@Get public enum OrderState { INIT(1,"开始"), WAIT_PAY(2,"待支付"), WAIT_SEND(3,"待发送"), PART_SEND(4,"配送"), WAIT_RECEIVE(5,"待接收"), COMPLETE(6,"完成"), CANCELED(7,"取消"); private String desc; private int code; public static OrderState getState(String state) { for (OrderState orderState : OrderState.values()) { if (orderState.name().equalsIgnoreCase(state)) { return orderState; return null; public static OrderStates getState(int code) { for (OrderStates orderState : OrderStates.values()) { if (orderState.ordinal()+1 == code) { return orderState; return null; }定义事件枚举public enum OrderEvent { SUBMIT_ORDER, //提交订单;INIT ==> WAIT_PAY PAY, //支付完成;WAIT_PAY ==> WAIT_SEND PART_SEND, //等待配送;WAIT_SEND ==> PART_SEND SEND, //配送中;PART_SEND ==> WAIT_RECEIVE COMPLETE //完成;WAIT_RECEIVE ==> COMPLETE }定义上下文@Data @AllArgsConstructor @NoArgsConstructor public class OrderContext { public OrderDTO orderDTO; }业务相关代码Service的实现,更新订单状态@Service public class OrderService { @Autowired OrderDTOMapper orderDTOMapper; public int submitOrder(OrderState state) { OrderDTO orderDTO = new OrderDTO(); orderDTO.setState(state); orderDTOMapper.insert(orderDTO); return 1; }mapper@Mapper public interface OrderDTOMapper { int insert(OrderDTO orderDTO); }订单的实体@Data @AllArgsConstructor @NoArgsConstructor public class OrderDTO { private Integer id; private OrderState state; private Date createTime; private Date updateTime; }定义监听器可以不用 DeclarativeEventListener 这种声明在单独类里的形式import lombok.extern.slf4j.Slf4j; import org.squirrelframework.foundation.exception.TransitionException; import org.squirrelframework.foundation.fsm.Action; import org.squirrelframework.foundation.fsm.annotation.*; * 声明式监听 @Slf4j public class DeclarativeEventListener { * 转换事件开始时进行调用 @OnTransitionBegin public void transitionBegin(FSMEvent event) { System.out.println(); log.info("transitionBegin event {}", event); * 转换事件开始时进行调用 * 可以加入条件 * 'event'(E), 'from'(S), 'to'(S), 'context'(C) and 'stateMachine'(T) can be used in MVEL scripts @OnTransitionBegin(when="event.name().equals(\"ToB\")") public void transitionBeginConditional() { log.info("transitionBeginConditional"); * 转换事件结束时进行调用 * 这个方法必须是 public 并且返回值是 void @OnTransitionEnd @ListenerOrder(10) public void transitionEnd() { log.info("transitionEnd"); System.out.println(); @OnTransitionComplete public void transitionComplete(String from, String to, FSMEvent event, Integer context) { log.info("transitionComplete {}=>{} on {} with {}", from, to, event, context); @OnTransitionException public void transitionException(String from, String to, FSMEvent event, Integer context) { log.info("transitionException"); * 当转换被拒绝时,将调用注有TransitionDecline的方法 @OnTransitionDecline public void transitionDeclined(String from, FSMEvent event, Integer context) { log.info("transitionDeclined {}=>??? on {} with {}", from, event, context); * 带有 OnAfterActionExecuted 注释的方法将在调用操作之前被调用 * 实际是 callMethod 中的方法被调用钱执行这个方法。类似于 AOP 的效果,运行一下即可知道 @OnBeforeActionExecuted public void onBeforeActionExecuted(Object sourceState, Object targetState, Object event, Object context, int[] mOfN, Action<?, ?, ?,?> action) { log.info("onBeforeActionExecuted"); * 带有 OnAfterActionExecuted 注释的方法将在调用操作之后被调用 * 实际是 callMethod 中的方法被调用后执行这个方法。类似于 AOP 的效果,运行一下即可知道 @OnAfterActionExecuted public void onAfterActionExecuted(Object sourceState, Object targetState, Object event, Object context, int[] mOfN, Action<?, ?, ?,?> action) { log.info("onAfterActionExecuted"); * 带有 OnActionExecException 注释的方法将在调用操作异常之后被调用 * 实际是 callMethod 中的方法被调用时抛异常了之后执行这个方法。类似于 AOP 的效果,运行一下即可知道 @OnActionExecException public void onActionExecException(Action<?, ?, ?,?> action, TransitionException e) { log.info("onActionExecException"); }Controller 类测试注入OrderStateMachineEngine ,初始化状态为INIT,通过OrderStateMachineEngine 触发SUBMIT_ORDER事件,发生状态变化,并将转换后的状态插入数据库@RestController @RequestMapping(value = "/order/") public class OrderController { // 注入状态机 @Autowired OrderStateMachineEngine orderStateMachineEngine; @RequestMapping("/test") public void test(){ OrderDTO orderDTO = new OrderDTO(OrderState.INIT); OrderContext orderContext = new OrderContext(orderDTO); // 启动状态机,触发某事件,发生状态变化 orderStateMachineEngine.fire(OrderEvent.SUBMIT_ORDER,orderContext); }流式 API 式状态机参考:Squirrel使用状态机引擎StateMachine 接口需要以下 4 种泛型参数。T 代表实现的状态机类型。S 代表实现的状态类型。E 代表实现的事件类型。C 代表实现的外部上下文类型。/** * 创建状态机 * 通过Spring创建StateMachineBuilder实例,通过buidler创建状态机(单例) * 业务函数中通过StateMachineBuilder实例创建StateMachine实例,并向StateMachine暴露SpringApplicationContext,以便于StateMachine通过ApplicationContext获取数据层的对象 @Slf4j @Componet public class StateMachineEngine implements ApplicationContextAware { private ApplicationContext applicationContext; protected StateMachineBuilder<MyStateMachine, MyState, MyEvent, MyContext> builder; @SuppressWarnings("unchecked") public StateMachineEngine() { builder = StateMachineBuilderFactory.create( MyStateMachine.class, MyState.class, MyEvent.class, MyContext.class); // 状态机builder创建之后,可以使用流失API来定义状态机的state/transition/action // 创建一个状态从状态A到B,并且MyEvent.GoToB事件触发,调用MyCallMethod动作方法的externalTransition builder.externalTransition().from(MyState.A).to(MyState.B).on(MyEvent.GoToB) .callMethod = "MyCallMethod"; //注入applicationContext,并在创建StateMachine实例时注入 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; * 可以通过向OrderContext 上下文传递一些业务参数,比如orderId等等 public boolean fire(EOrderEvents event, OrderContext context) { MyStateMachine stateMachine = stateMachineBuilder.newStateMachine( context.geteOrder().getOrderStatus(),applicationContext); //由于StateMachine实例不是由Spring容器创建,所以这个过程中无法通过注解方式开启事务(Spring没有机会去创建事务代理),因此采用了编程式事务 DataSourceTransactionManager transactionManager = (DataSourceTransactionManager)applicationContext.getBean("transactionManager"); DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransaction(def); try { stateMachine.fire(event, context); transactionManager.commit(status); //这里会返回状态机是否出错,如果出错可用于通知Controller层 return stateMachine.isError(); } catch (Exception ex) { //用于事务回滚 transactionManager.rollback(status); return true; }定义状态机import lombok.extern.slf4j.Slf4j; import org.squirrelframework.foundation.fsm.*; import org.squirrelframework.foundation.fsm.annotation.StateMachineParameters; import org.squirrelframework.foundation.fsm.impl.AbstractUntypedStateMachine; * 定义 触发事件、状态变化时,调用的方法 @Slf4j public class MyStateMachine extends AbstractStateMachine<MyStateMachine, MyState, MyEvent, MyContext> { private MyService myService; protected ApplicationContext applicationContext; //定义构造函数接受ApplicationContext注入 public SubmitOrderStateMachine(ApplicationContext applicationContext) { this.applicationContext = applicationContext; // 通过applicationContext注入orderService this.myService = applicationContext.getBean(MyService.class); // 状态转换时调用的方法,需要将方法名配置在 callMethod 内 // 若【方法名】符合 transitFrom[fromStateName]To[toStateName] 格式,不需要配置 callMethod public void MyCallMethod(MyState fromState, OrderState MyState,MyEvent Event, MyContext Context) { log.info("转换事件 {}=>{} on {} with {}.", fromState, toState, event, context); myService.myMethod(toState); // ========================================================================================== // 如果不想用 DeclarativeEventListener 这种声明在单独类里的方法,可以直接重写以下方法,效果是一样的 // 两者同时用也可以,为了代码方便最好别这样 // ========================================================================================== @Override protected void afterTransitionCausedException(Object fromState, Object toState, Object event, Object context) { * 当状态转换过程中出现异常,已执行的action列表将失效并且状态机会进入error状态,意思就是状态机实例不会再处理任何event。假如用户继续向状态机发送event,便会抛出IllegalStateException异常。所有状态转换过程中发生的异常,包括action执行和外部listener调用,会被包装成TransitionException(未检查异常)。目前,默认的异常处理策略非常简单并且粗暴的连续抛出异常,可以参阅AbstractStateMachine.afterTransitionCausedException方法。 log.info("Override 发生错误 {}", getLastException().getMessage()); Throwable targeException = getLastException().getTargetException(); // recover from IllegalArgumentException thrown out from state 'A' to 'B' caused by event 'ToB' if(targeException instanceof IllegalArgumentException && fromState.equals("A") && toState.equals("B") && event.equals("ToB")) { // 在这里做一些错误清理工作 // ... // 恢复此异常后,将状态机状态恢复为正常状态 setStatus(StateMachineStatus.IDLE); } else if(...) { // 从其他异常中恢复... } else { // afterTransitionCausedException 默认的实现是直接抛异常 super.afterTransitionCausedException(fromState, toState, event, context); @Override protected void beforeTransitionBegin(Object fromState, Object event, Object context) { // 转换开始时被调用 System.out.println(); // super.beforeTransitionBegin(fromState, event, context); log.info("Override beforeTransitionBegin"); @Override protected void afterTransitionCompleted(Object fromState, Object toState, Object event, Object context) { // 转换完成时被调用 // super.afterTransitionCompleted(fromState, toState, event, context); log.info("Override afterTransitionCompleted"); @Override protected void afterTransitionEnd(Object fromState, Object toState, Object event, Object context) { // 转换结束时被调用 // super.afterTransitionEnd(fromState, toState, event, context); log.info("Override afterTransitionEnd"); @Override protected void afterTransitionDeclined(Object fromState, Object event, Object context) { // 当转换被拒绝时被调用。实际是调用 callMethod 中的方法被调用时,抛出异常时被调用 // super.afterTransitionDeclined(fromState, event, context); log.info("Override afterTransitionDeclined"); @Override protected void beforeActionInvoked(Object fromState, Object toState, Object event, Object context) { // 当转换开始时被调用。实际是 callMethod 中的方法被调用时,先调用该方法。类似于 AOP 的效果 // super.beforeActionInvoked(fromState, toState, event, context); log.info("Override beforeActionInvoked"); @Override protected void afterActionInvoked(Object fromState, Object toState, Object event, Object context) { // 当转换结束时被调用。实际是 callMethod 被调用后,调用该方法。类似于 AOP 的效果 // super.afterActionInvoked(fromState, toState, event, context); log.info("Override afterActionInvoked"); }定义状态枚举、事件枚举、上下文略(同声明式状态机)
概述OpenAPI 介绍随着互联网技术的发展,现在的网站架构基本都由原来的后端渲染,变成了:前端渲染、前后端分离的形态,而且前端技术和后端技术在各自的道路上越走越远。 前端和后端的唯一联系,变成了API接口;API 文档变成了前后端开发人员联系的纽带,变得越来越重要。没有 API 文档工具之前,大家都是手写 API 文档的,在什么地方书写的都有,而且 API 文档没有统一规范和格式,每个公司都不一样。这无疑给开发带来了灾难。OpenAPI 规范(OpenAPI Specification 简称OAS)是 Linux 基金会的一个项目,试图通过定义一种用来描述 API 格式或 API 定义的语言,来规范 RESTful 服务开发过程。目前 V3.0 版本的 OpenAPI 规范已经发布并开源在 github 上 。源码:https://github.com/OAI/OpenAPI-Specificationswagger 介绍OpenAPI 是一个编写 API 文档的规范,然而如果手动去编写 OpenAPI 规范的文档,是非常麻烦的。而 Swagger 就是一个实现了OpenAPI 规范的工具集。官网:https://swagger.io/Swagger 包含的工具集:Swagger编辑器: Swagger Editor允许在浏览器中编辑YAML中的OpenAPI规范并实时预览文档。Swagger UI: Swagger UI是HTML,Javascript和CSS资产的集合,可以从符合OAS标准的API动态生成漂亮的文档。Swagger Codegen:允许根据OpenAPI规范自动生成API客户端库(SDK生成),服务器存根和文档。Swagger Parser:用于解析来自Java的OpenAPI定义的独立库Swagger Core:与Java相关的库,用于创建,使用和使用OpenAPI定义Swagger Inspector(免费): API测试工具,可让您验证您的API并从现有API生成OpenAPI定义SwaggerHub(免费和商业): API设计和文档,为使用OpenAPI的团队构建。快速入门案例SpringBoot 已经集成了 Swagger,使用简单注解即可生成 swagger 的 API 文档。引入依赖<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.8.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.8.0</version> </dependency> 编写配置import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .host("localhost:8085")//可不要 .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("controller包路径")) .paths(PathSelectors.any()) .build(); private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("乐优商城用户中心")//微服务名称 .description("乐优商城用户中心接口文档") .version("1.0") .build(); 启动测试重启服务,访问:http://localhost:8085/swagger-ui.html就能看到 swagger-ui 提供的API页面了:可以看到编写的4个接口,任意点击一个,即可看到接口的详细信息:可以看到详细的接口声明,包括:请求方式:请求路径请求参数响应等信息点击右上角的try it out!还可以测试接口:填写参数信息,点击execute,可以发起请求并测试:自定义接口概述swagge-ui 根据接口自动生成文档说明,不够详细。如果有需要,可以通过注解自定义接口声明。常用的注解包括:@Api:修饰整个类,描述Controller的作用@ApiOperation:描述一个类的一个方法,或者说一个接口@ApiParam:单个参数描述@ApiModel:用对象来接收参数@ApiProperty:用对象接收参数时,描述对象的一个字段@ApiResponse:HTTP响应其中1个描述@ApiResponses:HTTP响应整体描述@ApiIgnore:使用该注解忽略这个API@ApiError:发生错误返回的信息@ApiImplicitParam:一个请求参数@ApiImplicitParams:多个请求参数示例:/** * 用户名和手机号唯一性校验功能 @GetMapping("/check/{data}/{type}") @ApiOperation(value = "校验用户名、手机号数据是否可用,如果不存在则可用") @ApiResponses({ @ApiResponse(code = 200, message = "校验结果有效,true或false代表可用或不可用"), @ApiResponse(code = 400, message = "请求参数有误,比如type不是指定值") public ResponseEntity<Boolean> checkUser( @ApiParam(value = "要校验的数据", example = "lisi") @PathVariable("data") String data, @ApiParam(value = "数据类型,1:用户名,2:手机号", example = "1") @PathVariable(value = "type") Integer type) { return ResponseEntity.ok(userService.checkUser(data, type)); }查看文档:常用注解说明@Api用在请求的类上,表示对类的说明常用参数: value="描述类的作用"其他参数: tags="说明该类的作用,非空时将覆盖value的值,可以在UI界面上看到的注解" description 对api资源的描述,在1.5版本后不再支持 basePath 基本路径可以不配置,在1.5版本后不再支持 position 如果配置多个Api 想改变显示的顺序位置,在1.5版本后不再支持 produces 设置MIME类型列表(output),例:"application/json, application/xml",默认为空 consumes 设置MIME类型列表(input),例:"application/json, application/xml",默认为空 protocols 设置特定协议,例:http, https, ws, wss。 authorizations 获取授权列表(安全声明),如果未设置,则返回一个空的授权值。 hidden 默认为false, 配置为true 将在文档中隐藏示例:@Api(tags="登录请求") @Controller @RequestMapping(value="/highPregnant") public class LoginController {}@ApiOperation用在请求的方法上,说明方法的用途、作用常用参数: value="说明方法的用途、作用" notes="方法的备注说明"其他参数: tags 操作标签,非空时将覆盖value的值 response 响应类型(即返回对象) responseContainer 声明包装的响应容器(返回对象类型)。有效值为 "List", "Set" or "Map"。 responseReference 指定对响应类型的引用。将覆盖任何指定的response()类 httpMethod 指定HTTP方法,"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS" and "PATCH" position 如果配置多个Api 想改变显示的顺序位置,在1.5版本后不再支持 nickname 第三方工具唯一标识,默认为空 produces 设置MIME类型列表(output),例:"application/json, application/xml",默认为空 consumes 设置MIME类型列表(input),例:"application/json, application/xml",默认为空 protocols 设置特定协议,例:http, https, ws, wss。 authorizations 获取授权列表(安全声明),如果未设置,则返回一个空的授权值。 hidden 默认为false, 配置为true 将在文档中隐藏 responseHeaders 响应头列表 code 响应的HTTP状态代码。默认 200 extensions 扩展属性列表数组示例:@ResponseBody @PostMapping(value="/login") @ApiOperation(value = "登录检测", notes="根据用户名、密码判断该用户是否存在") public UserModel login(@RequestParam(value = "name", required = false) String account, @RequestParam(value = "pass", required = false) String password){}@ApiImplicitParams用在请求的方法上,表示一组参数说明@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面常用参数: name:参数名,参数名称可以覆盖方法参数名称,路径参数必须与方法参数一致 value:参数的汉字说明、解释 required:参数是否必须传,默认为false [路径参数必填] paramType:参数放在哪个地方 · header --> 请求参数的获取:@RequestHeader · query --> 请求参数的获取:@RequestParam · path(用于restful接口)--> 请求参数的获取:@PathVariable · body(不常用) · form(不常用) dataType:参数类型,默认String,其它值dataType="Integer" defaultValue:参数的默认值其他参数: allowableValues 限制参数的可接受值。1.以逗号分隔的列表 2、范围值 3、设置最小值/最大值 access 允许从API文档中过滤参数。 allowMultiple 指定参数是否可以通过具有多个事件接受多个值,默认为false example 单个示例 examples 参数示例。仅适用于BodyParameters示例:@ResponseBody @PostMapping(value="/login") @ApiOperation(value = "登录检测", notes="根据用户名、密码判断该用户是否存在") @ApiImplicitParams({ @ApiImplicitParam(name = "name", value = "用户名", required = false, paramType = "query", dataType = "String"), @ApiImplicitParam(name = "pass", value = "密码", required = false, paramType = "query", dataType = "String") public UserModel login(@RequestParam(value = "name", required = false) String account, @RequestParam(value = "pass", required = false) String password){}@ApiModel、@ApiModelProperty@ApiModel用在请求的类上,表示对类的说明用于响应类上,表示一个返回响应数据的信息这种一般用在 post 请求,使用 @RequestBody 这样的场景,请求参数无法使用 @ApiImplicitParam 注解进行描述的时候@ApiModelProperty:用在属性上,描述响应类的属性常用参数: value 此属性的简要说明。 name 允许覆盖属性名称 其他参数: allowableValues 限制参数的可接受值。1.以逗号分隔的列表 2、范围值 3、设置最小值/最大值 access 允许从API文档中过滤属性。 notes 目前尚未使用。 dataType 参数的数据类型。可以是类名或者参数名,会覆盖类的属性名称。 required 参数是否必传,默认为false position 允许在类中对属性进行排序。默认为0 hidden 允许在Swagger模型定义中隐藏该属性。 example 属性的示例。 readOnly 将属性设定为只读。 reference 指定对相应类型定义的引用,覆盖指定的任何参数值示例:@ApiModel(value="用户登录信息", description="用于判断用户是否存在") public class UserModel implements Serializable{ private static final long serialVersionUID = 1L; /**用户名 */ @ApiModelProperty(value="用户名") private String account; /**密码 */ @ApiModelProperty(value="密码") private String password; }@ApiResponses用在请求的方法上,表示一组响应@ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息常用参数: code:数字,例如400 message:信息,例如"请求参数没填好" response:抛出异常的类示例:@ResponseBody @PostMapping(value="/update/{id}") @ApiOperation(value = "修改用户信息",notes = "打开页面并修改指定用户信息") @ApiResponses({ @ApiResponse(code=400,message="请求参数没填好"), @ApiResponse(code=404,message="请求路径没有或页面跳转路径不对") public JsonResult update(@PathVariable String id, UserModel model){}@ApiParam用在请求方法中,描述参数信息常用参数: name:参数名称,参数名称可以覆盖方法参数名称,路径参数必须与方法参数一致 value:参数的简要说明。 defaultValue:参数默认值 required:属性是否必填,默认为false [路径参数必须填]其他参数: allowableValues 限制参数的可接受值。1.以逗号分隔的列表 2、范围值 3、设置最小值/最大值 access 允许从API文档中过滤参数。 allowMultiple 指定参数是否可以通过具有多个事件接受多个值,默认为false hidden 隐藏参数列表中的参数。 example 单个示例 examples 参数示例。仅适用于BodyParameters示例:@ResponseBody @PostMapping(value="/login") @ApiOperation(value = "登录检测", notes="根据用户名、密码判断该用户是否存在") public UserModel login(@ApiParam(name = "name", value = "用户名", required = false) @RequestParam(value = "name", required = false) String account, @ApiParam(name = "pass", value = "密码", required = false) @RequestParam(value = "pass", required = false) String password){} //或以实体类为参数: @ResponseBody @PostMapping(value="/login") @ApiOperation(value = "登录检测", notes="根据用户名、密码判断该用户是否存在") public UserModel login(@ApiParam(name = "model", value = "用户信息Model") UserModel model){}
参考:Runtime.exec详解java调用外部程序---- Runtime.getRuntime().exec概述Runtime.getRuntime().exec 用于调用外部可执行程序或系统命令,并重定向外部程序的标准输入、标准输出和标准错误到缓冲池。功能和windows“运行”类似。格式:Process process = Runtime.getRuntime().exec( ".//p.exe "); process.waitfor();第一行的“.//p.exe”是要执行的程序名,Runtime.getRuntime() 返回当前应用程序的Runtime对象,该对象的 exec() 方法指示Java虚拟机创建一个子进程执行指定的可执行程序,并返回与该子进程对应的Process对象实例。通过Process可以控制该子进程的执行或获取该子进程的信息。第二条语句的目的等待子进程完成再往下执行。方法APIRuntime.getRuntime().exec共有六个重载方法:// 在单独的进程中执行指定的外部可执行程序的启动路径或字符串命令 public Process exec(String command) // 在单独的进程中执行指定命令和变量 public Process exec(String[] cmdArray) // 在指定环境的独立进程中执行指定命令和变量 public Process exec(String command, String[] envp) // 在指定环境的独立进程中执行指定的命令和变量 public Process exec(String[] cmdArray, String[] envp) // 在指定环境和工作目录的独立进程中执行指定的字符串命令 public Process exec(String command, String[] envp, File dir) // 在指定环境和工作目录的独立进程中执行指定的命令和变量 public Process exec(String[] cmdarray, String[] envp, File dir) // 参数说明: cmdarray // 包含所调用命令及其参数的数组。数组第一个元素是命令,其余是参数 envp // 字符串数组,其中每个元素的环境变量的设置格式为 name=value,如果子进程应该继承当前进程的环境,则该参数为null dir // 子进程的工作目录;如果子进程应该继承当前进程的工作目录,则该参数为null // 参数cmdArray 示例:shutdown -s -t 3600 String arr[] = {"shutdown","-s","-t","3600"}; Process process = Runtime.getRuntime().exec(arr[]); 在调用这个方法时,不能将命令和参数放在一起,eg:String arr[] = {"shutdown -s -t 3600"}; 这样会导致程序把“shutdown -s -t 3600”当成是一条命令的名称,然后去查找“shutdown -s -t 3600”这条命令,它当然会找不到,所以就会报错 */注意:Runtime.exec() 不是cmd或shell环境,因此无法直接调用dir等命令,需要在程序中读取运行的操作系统平台,以调用不同的命令解释器(NT:cmd.exe,windows 95/98:command.exe,linux:/bin/sh)Procss类将持有该程序返回 Java VM 的引用。这个procss类是一个抽象类,具体子类的实现依赖于不同的底层操作系统。Process 的常用方法:// 导致当前线程等待,如有必要,一直要等到由该 Process 对象表示的进程已经终止。 int waitFor() /* 如果已终止该子进程,此方法立即返回。 如果没有终止该子进程,调用的线程将被阻塞,直到退出子进程,0 表示正常终止 */ // 杀掉子进程 void destroy() // 返回子进程的出口值,值 0 表示正常终止 int exitValue() // 获取子进程的错误流 InputStream getErrorStream() // 获取子进程的输入流 InputStream getInputStream() // 获取子进程的输出流 OutputStream getOutputStream()程序阻塞问题通过 Process实例.getInputStream() 和 Process实例.getErrorStream() 获取的输入流和错误信息流是缓冲池向当前Java程序提供的,而不是直接获取外部程序的标准输出流和标准错误流。而缓冲池的容量是一定的,因此若外部程序在运行过程中不断向缓冲池输出内容,当缓冲池填满,那么外部程序将暂停运行直到缓冲池有空位可接收外部程序的输出内容为止。(采用xcopy命令复制大量文件时将会出现该问题)解决办法:当前的Java程序不断读取缓冲池的内容,从而为腾出缓冲池的空间。Runtime r = Runtime.getRuntime(); Process proc = r.exec("cmd /c dir"); // 假设该操作为造成大量内容输出 // 采用字符流读取缓冲池内容,腾出空间 BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream(), "gbk"))); String line = null; while ((line = reader.readLine()) != null){ System.out.println(line); /* 或采用字节流读取缓冲池内容,腾出空间 ByteArrayOutputStream pool = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int count = -1; while ((count = proc.getInputStream().read(buffer)) != -1){ pool.write(buffer, 0, count); buffer = new byte[1024]; System.out.println(pool.toString("gbk")); int exitVal = proc.waitFor(); System.out.println(exitVal == 0 ? "成功" : "失败"); catch(Exception e){ e.printStackTrace(); }注意:外部程序在执行结束后需自动关闭,否则不管是字符流还是字节流均由于既读不到数据,又读不到流结束符,从而出现阻塞Java进程运行的情况。cmd的参数 “/c” 表示当命令执行完成后关闭自身。实例在当前目录执行dir命令,并将结果保存到 c:\dir.txt 文本文件中:前提:假设当前用户的家目录为c:\user\fsjohnhuangc:\user\fsjohnhuang下的目录结构c:\user\fsjohnhuang |--jottings |--test.txtd:\test下的目录结构d:\test |--movies |--readme.txt代码片段Runtime r = Runtime.getRuntime(); Process proc = r.exec("cmd /c dir > %dest%", new String[]{"dest=c:\\dir.txt", new File("d:\\test")}); int exitVal = proc.waitFor(); // 阻塞当前线程,并等待外部程序中止后获取结果码 System.out.println(exitVal == 0 ? "成功" : "失败"); catch(Exception e){ e.printStackTrace(); }执行代码后查看c:\dir.txt文件内容如如下:驱动器 D 中的卷没有标签。 卷的序列号是 8074-B214 D:\test 的目录 2014/09/22 14:45 <DIR> movies 2014/03/31 17:14 8,642 readme.txt拓展1,调用一次exec方法执行多条cmd命令,使用 && 分隔命令,该方法的局限性是只能在cmd里面使用Runtime.getRuntime().exec("cmd /c set CLASSPATH=D:\\ && javac D:\\a.java && java a"); // 方法重载:public Process exec(String[] cmdarray, String[] envp, File dir) String arr[] = {"CLASSPATH=D://","Path=C:\\Program Files\\Java\\jdk1.8.0_131\\bin"}; Runtime.getRuntime().exec("cmd /c javac a.java && java a", arr, new File("D://"));2,执行Runtime.exec()需要注意的陷阱:https://www.cnblogs.com/fpqi/p/9679039.html
概述无论是微服务还是SOA,都面临着服务间的远程调用。常见的远程调用方式有以下2种:RPC: Remote Produce Call远程过程调用,类似的还有RMI(remote method invoke)。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表。Http: http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。 现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。现在热门的Rest风格,就可以通过http协议来实现。如果项目全部采用 Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。如果项目的技术栈多样化,主要采用了Spring和SpringBoot框架,那么SpringCloud搭建微服务是不二之选,使用Http方式来实现服务间调用。 java开发中,使用http连接,访问第三方网络接口,通常使用的连接工具为RestTemplate、HttpClient和OKHttp。RestTemplate概述及依赖HttpClient和OKHttp两种连接工具,使用起来比较复杂,如果使用spring框架,可以使用restTemplate来进行http连接请求。restTemplate默认的连接方式是java中的HttpConnection,可以使用ClientHttpRequestFactory指定不同的HTTP连接方式。依赖<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency>配置类基础配置@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(ClientHttpRequestFactory factory) { return new RestTemplate(factory); @Bean public ClientHttpRequestFactory simpleClientHttpRequestFactory() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setReadTimeout(150 * 1000); // ms factory.setConnectTimeout(150 * 1000); // ms return factory; }进阶配置import org.apache.http.client.HttpClient; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class RestTemplateConfig { * http连接管理器 @Bean public HttpClientConnectionManager poolingHttpClientConnectionManager() { /*// 注册http和https请求 Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(); PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);*/ PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(); // 最大连接数 poolingHttpClientConnectionManager.setMaxTotal(500); // 同路由并发数(每个主机的并发) poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); return poolingHttpClientConnectionManager; * HttpClient * @param poolingHttpClientConnectionManager @Bean public HttpClient httpClient(HttpClientConnectionManager poolingHttpClientConnectionManager) { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); // 设置http连接管理器 httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager); /*// 设置重试次数,默认是3次,没有开启 httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true));*/ // 设置默认请求头 /*List<Header> headers = new ArrayList<>(); headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36")); headers.add(new BasicHeader("Accept-Encoding", "gzip,deflate")); headers.add(new BasicHeader("Accept-Language", "zh-CN")); headers.add(new BasicHeader("Connection", "Keep-Alive")); headers.add(new BasicHeader("Content-type", "application/json;charset=UTF-8")); httpClientBuilder.setDefaultHeaders(headers);*/ return httpClientBuilder.build(); * 请求连接池配置 * @param httpClient @Bean public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) { HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); // httpClient创建器 clientHttpRequestFactory.setHttpClient(httpClient); // 连接超时时间/毫秒(连接上服务器(握手成功)的时间,超出抛出connect timeout) clientHttpRequestFactory.setConnectTimeout(5 * 1000); // 数据读取超时时间(socketTimeout)/毫秒(服务器返回数据(response)的时间,超过抛出read timeout) clientHttpRequestFactory.setReadTimeout(10 * 1000); // 从连接池获取请求连接的超时时间,不宜过长,必须设置/毫秒(超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool) clientHttpRequestFactory.setConnectionRequestTimeout(10 * 1000); return clientHttpRequestFactory; * rest模板 @Bean public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) { // 配置请求工厂 return new RestTemplate(clientHttpRequestFactory); }使用实体类@Data @Builder @NoArgsConstrutor @AllArgsConstrutor public class BaseResponse<TempUser> implements Serializable { private static final long serialVersionUID = 1L; private String responseCode; private String responseMessage; private List<TempUser> responseData; }@Data @Builder @NoArgsConstrutor @AllArgsConstrutor public class TempUser implements Serializable { private static final long serialVersionUID = 1L; private String userName; private Integer age; }GET请求普通访问BaseResponse result = restTemplate.getForObject( "http://localhost:8080/cs-admin/rest/getUser?userName=张三&age=18", BaseResponse.class);返回HTTP状态ResponseEntity<BaseResponse> responseEntity = restTemplate.getForEntity( "http://localhost:8080/cs-admin/rest/getUser?userName=张三&age=18", TempUser.class); // 获取状态对象 HttpStatus httpStatus = responseEntity.getStatusCode(); // 获取状态码 int statusCodeValue = responseEntity.getStatusCodeValue(); // 获取headers HttpHeaders httpHeaders = responseEntity.getHeaders(); // 获取body BaseResponse result = responseEntity.getBody();映射请求参数Map<String, Object> paramMap = new HashMap<>(); paramMap.put("userName", "张三"); paramMap.put("age", 18); BaseResponse result = restTemplate.getForObject( "http://localhost:8080/cs-admin/rest/getUser?userName={userName}&age={age}", BaseResponse.class, paramMap);POST请求普通访问接口TempUser param = new TempUser(); param.setUserName("张三"); param.setAge(18); BaseResponse result = restTemplate.postForObject("http://localhost:8080/cs-admin/rest/getPostUser", param, BaseResponse.class);带HEAD访问接口// 请求头信息 HttpHeaders headers = new HttpHeaders(); //headers.setContentType(MediaType.valueOf("application/json;charset=UTF-8")); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); //headers.add("headParam1", "headParamValue"); // 请求体内容 TempUser param = new TempUser(); param.setUserName("张三"); param.setAge(18); // 组装请求信息 HttpEntity<TempUser> httpEntity=new HttpEntity<>(param, headers); BaseResponse result = restTemplate.postForObject("http://localhost:8080/cs-admin/rest/getPostUser", httpEntity, BaseResponse.class);无请求体的访问:仅method为post,传参方式仍然为get的param方式Map<String, Object> paramMap = new HashMap<>(); paramMap.put("userName", "张三"); paramMap.put("age", 18); BaseResponse result = restTemplate.postForObject( "http://localhost:8080/cs-admin/rest/getPostUserNoBody?userName={userName}&age={age}", null, BaseResponse.class, paramMap); System.out.println(result);上传文件后台接口代码:@RequestMapping("uploadFile") public TempUser uploadFile(HttpServletRequest request, TempUser form) { MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request; //获取文件信息 MultipartFile multipartFile = multipartHttpServletRequest.getFile("file"); TempUser tempUser = new TempUser(); if (multipartFile != null) { tempUser.setUserName(form.getUserName() + " " + multipartFile.getOriginalFilename()); if(form!=null){ tempUser.setAge(form.getAge()); return tempUser; }访问方式:// 文件 FileSystemResource file=new FileSystemResource("D:\\Elasticsearch权威指南(中文版).pdf"); // 设置请求内容 MultiValueMap<String, Object> param=new LinkedMultiValueMap<>(); param.add("file", file); // 其他参数 param.add("userName", "张三"); param.add("age", 18); // 组装请求信息 HttpEntity<MultiValueMap<String, Object>> httpEntity=new HttpEntity<>(param); // 发送请求 TempUser result = restTemplate.postForObject("http://localhost:8080/cs-admin/rest/uploadFile", httpEntity, TempUser.class);HttpClient概述HttpClient 通过连接池创建连接:管理连接的基本单位是Route(路由),每个路由上都会维护一定数量的HTTP连接每次调用后必须执行 releaseConnection路由可以理解为客户端机器到目标机器的一条线路如果不给 httpclient配置指定的连接管理器,httpclient会自动使用PoolingHttpClientConnectionManager作为连接管理器。PoolingHttpClientConnectionManager默认的maxConnPerRoute和maxConnTotal分别是是2和20。也就是对于每个服务器最多只会维护2个连接,看起来有点少。所以,在日常使用时我们尽量使用自己配置的连接管理器比较好。连接池:连接池技术作为创建和管理连接的缓冲池技术。连接池管理的对象是长连接有长连接的优势长连接:是指客户端与服务器端一旦建立连接以后,可以进行多次数据传输而不需重新建立连接,优势:省去了每次数据传输连接建立的时间开销资源的访问控制短连接:每次数据传输都需要客户端和服务器端建立一次连接使用使用HttpClient发送请求、接收响应很简单,一般需要如下几步即可:创建HttpClient对象创建请求方法的实例,并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象。如果需要发送请求参数,可调用HttpGet、HttpPost共同的setParams(HetpParams params)方法来添加请求参数;对于HttpPost对象而言,也可调用setEntity(HttpEntity entity)方法来设置请求参数,参数则必须用NameValuePair[]数组存储调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容释放连接。无论执行方法是否成功,都必须释放连接依赖:<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.2</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient-cache</artifactId> <version>4.5.2</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpmime</artifactId> <version>4.5.2</version> </dependency>java工具类import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.Consts; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; * HttpClient 工具类 @Slf4j public class HttpClientUtil { public static final String APPLICATION_JSON_VALUE = "application/json"; private static final Logger logger = log; private static final Integer CONN_TIME_OUT = 3000;// 超时时间豪秒 private static final Integer SOCKET_TIME_OUT = 10000; /** 每个路由的最大请求数,默认2 */ private static final Integer DEFAULT_MAX_PER_ROUTE = 40; /** 最大连接数,默认20 */ private static final Integer MAX_TOTAL = 400; private static HttpClient httpClient; static { // 请求配置 RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(CONN_TIME_OUT) .setConnectionRequestTimeout(CONN_TIME_OUT) .setSocketTimeout(SOCKET_TIME_OUT) .build(); // 管理 http连接池 PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE); cm.setMaxTotal(MAX_TOTAL); httpClient = HttpClients.custom() .setConnectionManager(cm) .setDefaultRequestConfig(requestConfig) .build(); * Get请求 public static String requestGet(String url, Map<String, String> paramsMap) throws Exception { logger.info("GET request url:{} params:{}", url, paramsMap); Long start = System.currentTimeMillis(); List<NameValuePair> params = initParams(paramsMap); // Get请求 HttpGet httpGet = new HttpGet(url); try { // 设置参数 String str = EntityUtils.toString(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8)); String uriStr = StringUtils.isEmpty(str) ? httpGet.getURI().toString() : httpGet.getURI().toString() + "?" + str; httpGet.setURI(new URI(uriStr)); // 发送请求 HttpResponse response = httpClient.execute(httpGet); logger.info("GET request url:{} response:{} time:{}", url, response, System.currentTimeMillis() - start); // 获取返回数据 return getSuccessRetFromResp(response, url, JSON.toJSONString(paramsMap)); } finally { // 必须释放连接,不然连接用完后会阻塞 httpGet.releaseConnection(); * Post请求,Map格式数据 public static String requestPost(String url, Map<String, String> paramsMap) throws Exception { logger.info("POST request url:{} params:{}", url, paramsMap); Long start = System.currentTimeMillis(); List<NameValuePair> params = initParams(paramsMap); HttpPost httpPost = new HttpPost(url); try { httpPost.setEntity(new UrlEncodedFormEntity(params, Consts.UTF_8)); HttpResponse response = httpClient.execute(httpPost); logger.info("POST request url:{} response:{} time:{}", url, response, System.currentTimeMillis() - start); String retStr = getSuccessRetFromResp(response, url, JSON.toJSONString(paramsMap)); return retStr; } finally { httpPost.releaseConnection(); * Post请求,json格式数据 public static String requestPostJsonStr(String url, String json) throws Exception { logger.info("POST request url:{} params:{}", url, json); long start = System.currentTimeMillis(); HttpPost httpPost = new HttpPost(url); try { StringEntity entity = new StringEntity(json, Consts.UTF_8); entity.setContentType(APPLICATION_JSON_VALUE); httpPost.setEntity(entity); HttpResponse response = httpClient.execute(httpPost); logger.info("POST request url:{} response:{} time:{}", url, response, System.currentTimeMillis() - start); return getSuccessRetFromResp(response, url, json); } finally { // 资源释放 httpPost.releaseConnection(); * post Object格式数据 public static String requestPostJson(String url, Object obj) throws Exception { String params = JSON.toJSONString(obj); return requestPostJsonStr(url, params); private static String getSuccessRetFromResp(HttpResponse response, String url, String params) throws Exception { String retStr = ""; // 检验状态码,如果成功接收数据 int code = response.getStatusLine().getStatusCode(); if (code == 200) { retStr = EntityUtils.toString(response.getEntity(), Consts.UTF_8); } else { throw new RuntimeException(String.format("Http request error:%s, url:%s, params:%s", response, url, params)); logger.info("Http request retStr:{}. url:{}", retStr, url); return retStr; private static List<NameValuePair> initParams(Map<String, String> paramsMap) { List<NameValuePair> params = new ArrayList<NameValuePair>(); if (paramsMap == null) return params; for (Map.Entry<String, String> entry : paramsMap.entrySet()) { params.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); return params;
参考:手把手教你解决.git目录过大问题.git文件过大解决概述git 概述Git 是一个分布式版本控制软件,最初由 「林纳斯·托瓦兹」 创作,于 2005 年发布。最初目的是为更好地管理 Linux 内核开发。Git 在本地磁盘上就保存着所有有关当前项目的历史更新,处理速度快。Git 中的绝大多数操作都只需要访问本地文件和资源,不用实时联网。「Git LFS」(Large File Storage - 大文件存储)是可以把⾳乐、图⽚、视频等指定的任意文件存在 Git仓库之外,而在 Git 仓库中用一个占用空间 1KB 不到的文本指针来代替的小⼯具。通过把大文件存储在 Git 仓库之外,可以减小 Git 仓库本身的体积,使克隆 Git 仓库的速度加快,也使得 Git 不会因为仓库中充满大⽂件而损失性能。使用 Git LFS ,在默认情况下,只有当前签出的 commit 下的 LFS 对象的当前版本会被下载。此外,也可以做配置,只取由 Git LFS 管理的某些特定文件的实际内容,而对于其他由 Git LFS 管理的⽂件则只保留文件指针,从而节省带宽,加快克隆仓库的速度;也可以配置⼀次获取大文件的最近版本,从而能方便地检查大文件的近期变动。.git 目录虽然 .git 这个隐藏目录并不算在代码体积之后,但是拉代码的时候,是需要拉下来的,因为里面包含之前的提交记录等信息。这就会导致下载速度变的很慢。├── HEAD ├── branches ├── index ├── logs │ ├── HEAD │ └── refs │ └── heads │ └── master ├── objects │ ├── 88 │ │ └── 23efd7fa394844ef4af3c649823fa4aedefec5 │ ├── 91 │ │ └── 0fc16f5cc5a91e6712c33aed4aad2cfffccb73 │ ├── 9f │ │ └── 4d96d5b00d98959ea9960f069585ce42b1349a │ ├── info │ └── pack └── refs ├── heads │ └── master └── tagsdescription:用于GitWeb程序config:配置特定于该仓库的设置hooks:放置客户端或服务端的hook脚本HEAD:指明当前处于哪个分支objects:Git对象存储目录refs:Git引用存储目录branches:放置分支引用的目录.git 目录变大原因当使用 git add 和 git commit 命令的过程中,Git 都会生成一个 Git 对象,称为 blob对象,存放在 objects 目录下,然后更新 index 索引,再然后创建 tree 对象,最后创建出了 commit 对象。这些 commit 对象指向了顶层 tree 对象以及先前的 commit 对象。而上述创建出来的对象,都以文件的方式保存在 .git/objects 目录下。所以,当在使用的过程中,提交了一个体积特别大的文件,就会被 Git 追踪记录在 .git/objects 文件夹下面。即使后面删除了这个体积特别大的文件,但其实 Git 只会记录删除的这个操作,并不会把文件从 .git 文件夹下面真正的删除,即 .git 文件夹完全不会变小。解决方案方案1:重建仓库重建仓库的这种做法,算是一种比较一劳永逸且相对而言比较简单的方式。但是,这种做法一般情况下,都是不可行,除非是自己的本地项目。方案2:删除大文件直接找到 .git 目录下的大文件,将其删除掉,之后推送到远程代码库里面。这样做的前提是,删除所有其他分支,保留 master 或者 main 分支,并清空 git 服务器的当前项目所有分支,重新推送。这里需要注意的是,操作有风险,后果请自负。使用在 git 项目的根目录鼠标右击使用 Git Bash Here 调出 Git 的命令窗口找出 git项目 中较大的五个提交记录文件git verify-pack -v .git/objects/pack/pack-*.idx | sort -k 3 -g | tail -5 # 命令说明: # verify-pack 显示已打包的内容(找大文件) # sort -k 3 -g 以第三列排序执行结果(当前以查询最大的五次做介绍):dbad6eb20d31a5aefe132b74b2137cd10105c574 blob 16684712 7287784 86721282 6c858bc93421b2db41dafc2bfd4eb82c77c50266 blob 17504576 8257957 47764046 416088453a2514ada98ba639af3ff298510b4246 blob 22216104 10854126 75719527 f0d8d3b476526af42b4e06f390a1b4925580e99b blob 22435760 10589160 16678516 553ba826b92c9d42acb1e586774ac697661588c9 blob 29814552 12998052 60871184第一行的字母其实相当于文件的 id,用以下命令可以找出 id 对应的文件名git rev-list --objects --all | grep dbad6eb20d31a5aefe132b74b2137cd10105c574 # 命令说明: # rev-list 列出Git仓库中的所有提交记录 # --objects 列出该提交涉及的所有文件ID # --all 所有分支的提交(位于/refs下的所有引用) 执行结果dbad6eb20d31a5aefe132b74b2137cd10105c574 Pods/UMengUShare/UShareSDK/SocialLibraries/WeChat/WechatSDK/libWeChatSDK.a删除大文件记录git filter-branch --force --prune-empty --index-filter 'git rm -rf --cached --ignore-unmatch Pods/UMengUShare/UShareSDK/SocialLibraries/WeChat/WechatSDK/libWeChatSDK.a' --tag-name-filter cat -- --all # 命令说明 # filter-branch 重写Git仓库中的提交 # --index-filter 指定后面命令进行删除 # --all 所有分支的提交(位于/refs下的所有引用)上面代码执行完毕后有可能会报以下错误WARNING: git-filter-branch has a glut of gotchas generating mangled history rewrites. Hit Ctrl-C before proceeding to abort, then use an alternative filtering tool such as 'git filter-repo' (https://github.com/newren/git-filter-repo/) instead. See the filter-branch manual page for more details; to squelch this warning, set FILTER_BRANCH_SQUELCH_WARNING=1. Proceeding with filter-branch... Cannot rewrite branches: You have unstaged changes.如果出现以上错误可执行以下命令 (不报错则跳过此步)git stash然后重新执行移除命令执行结果WARNING: git-filter-branch has a glut of gotchas generating mangled history rewrites. Hit Ctrl-C before proceeding to abort, then use an alternative filtering tool such as 'git filter-repo' (https://github.com/newren/git-filter-repo/) instead. See the filter-branch manual page for more details; to squelch this warning, set FILTER_BRANCH_SQUELCH_WARNING=1. Proceeding with filter-branch... Rewrite 967ff01a8b2bc7d7f6c90630ede38a2135ba5813 (1/41) (0 seconds passed, remaiRewrite 9674de7f9f8ed029785421f2246bb9d41786cc49 (2/41) (0 seconds passed, remaiRewrite e04ab331167f24b7d26d69e169386e690081af74 (3/41) (0 seconds passed, remaiRewrite 997e93fdaff75162eda8e5c6ee6d3265ce61fe96 (4/41) (0 seconds passed, Rewrite 429d321e2ee4c54d8d96b35a8e35d021bd8930f1 (35/41) (3 seconds passed, remaining 0 predicted) rm 'Pods/UMengUShare/UShareSDK/SocialLibraries/WeChat/WechatSDK/libWeChatSDK.a' Ref 'refs/heads/master' was rewritten Ref 'refs/remotes/origin/master' was rewritten WARNING: Ref 'refs/remotes/origin/master' is unchanged Ref 'refs/stash' was rewritten彻底清除rm -rf .git/refs/original/ # 删除本地仓库引用 git reflog expire --expire=now --all # 设置所有reflog条目现在过期 git gc --prune=now # 回收空间,移除无效或异常的文件强制推送远程git push --force --all # 让远程仓库变小 git remote prune origin方案3:使用 git repo-clean 工具清理(亲测,推荐)git repo-clean是用 Golang 开发的具备 Git 仓库大文件扫描,清理,并重写 commit 提交记录功能的 Git 拓展工具。源码及下载安装详见:https://gitee.com/oschina/git-repo-clean推荐使用二进制包安装,简单方便使用:有两种使用方式,一种是命令行,一种是交互式。目前选项有如下: -v, --verbose 显示处理的详细过程 -V, --version 显示 git-repo-clean 版本号 -h, --help 显示使用信息 -p, --path 指定Git仓库的路径, 默认是当前目录,即'.' -s, --scan 扫描Git仓库数据,默认是扫描所有分支中的数据 -f, --file 直接指定仓库中的文件或目录,与'--scan'不兼容 -b, --branch 设置需要删除文件的分支, 默认是从所有分支中删除文件 -l, --limit 设置扫描文件阈值, 比如: '--limit=10m' -n, --number 设置显示扫描结果的数量 -t, --type 设置扫描文件后缀名,即文件类型 -i, --interactive 开启交互式操作 -d, --delete 执行文件删除和历史重写过程 -L, --lfs 将大文件转换为Git LFS指针文件命令行式用法:git repo-clean --verbose --scan --limit=1G --type=tar.gz --number=5加上 --delete 选项,则会批量删除扫描出的文件,并重写相关提交历史(包括HEAD)git repo-clean --verbose --scan --limit=1G --type=tar.gz --number=5 --delete 注意:若直接在git项目的根目录进入 bash 命令窗口使用 repo-clean 命令时,显示无 repo-clean 命令,则可以通过左下角【开始】输入 cmd 进入命令窗口,然后使用命令切换至 git 项目的根目录,再使用 repo-clean 命令目前扫描操作和删除操作都是默认在所有分支上进行,而--branch 选项只是指定删除时的分支,不能指定扫描时的分支。因此如果使用了这个选项指定了某个分支,可能从扫描结果中选择了另一个分支中的文件,因此不会有文件真正被删除。方案4:使用 migrate 命令优化 .git 目录迁移已有的 git 仓库,使用 git lfs 来进行管理。重写历史后的提交需执行 git commit --force,请确认在本地的操作合适无误后再进行提交。如有迁移至 git lfs 前的仓库有多份拷贝,其他拷贝可能需要执行 git reset --hard origin/master 来重置其本地的分支,注意执行 git reset --hard 命令将会丢失本地的改动git lfs migrate :用来将当前已经被 GIT 储存库(.git)保存的文件以 LFS 文件的形式保存# 重写master分⽀ # 将历史提交(指的是.git目录)中的*.zip都⽤lfs进⾏管理 $ git lfs migrate import --include-ref=master --include="*.zip" # 重写所有分⽀及标签 # 将历史提交(指的是.git目录)中的*.rar,*.zip都⽤lfs进⾏管理 $ git lfs migrate import --everything --include="*.rar,*.zip" # 切换后需要把切换之后的本地分支提交到远程仓库了,需要手动push更新远程仓库中的各个分支 $ git lfs push --force # 切换成功后,GIT仓库的大小可能并没有变化 # 主要原因可能是之前的提交还在,因此需要做一些清理工作 # 如果不是历史记录非常重要的仓库,建议不要像上述这么做,而是重新建立一个新的仓库 $ git reflog expire --expire-unreachable=now --all $ git gc --prune=now拓展使用 lfs 管理大文件比较好的避免上述问题的出现,就是及时使用 lfs 来追踪、记录和管理大文件。这样大文件既不会污染我们的 .git目录,也可以让我们更方便的使用。# 1.开启lfs功能 $ git lfs install # 2.追踪所有后缀名为“.psd”的文件 $ git lfs track "*.iso" # 3.追踪单个文件 git lfs track "logo.png" # 4.提交存储信息文件 $ git add .gitattributes # 5.提交并推送到GitHub仓库 $ git add . $ git commit -m "Add some files" $ git push origin master同时,还有一个方法,就是灵活使用 .gitignore 文件,及时排除仓库不需要的特殊目录或者文件,从而不会让不应该存在的文件,出现在代码仓库里面.DS_Store node_modules /dist *.zip *.tar.gz
概述web 服务器、项目、资源概述web 服务器:可以被浏览器访问到的服务器常见的 web 服务器:tomcat:中小型的服务器软件,免费开源,支持 JSP 和 Servlet apache 公司的产品WebLogic:Oracle 公司的产品,是目前应用最广泛的 Web 服务器,支持 J2EE 规范。WebLogic 是用于开发、集成、部署和管理大型分布式 Web 应用、网络应用和数据库应用的 Java 应用服务器。收费的大型的服务器软件WebSphere:IBM 公司的产品,支持 JavaEE 规范。WebSphere 是随需应变的电子商务时代的最主要的软件平台,可用于企业开发、部署和整合新一代的电子商务应用。收费的大型的服务器软件web 项目: 部署在 web 服务器上面,可以被浏览器直接访问到的项目,特点:项目的结构得按照指定规则web 资源:静态资源:指在页面写死的、始终不变的数据,比如:HTML、CSS、JS、图片、音频、视频。动态资源:指web页面中浏览的数据是由程序产生的,不同时间点访问web页面看到的内容各不相同。 比如:在不同时间搜索微博的热门话题内容是不一样的。这些数据由程序生成,/Servlet、ASP、PHP等技术都可以完成。软件的架构网络中有很多的计算机,它们直接的信息交流,称之为:交互。 在互联网交互的过程的有两个非常典型的交互方式 —— B/S 交互模型和 C/S 交互模型。C/S架构 Client/Server 客户端/服务器特点:需要安装一个客户端,例如:QQ客户端 游戏各种客户端 迅雷客户端优点:效果比较炫,好看缺点:占用硬盘空间,服务器只要升级就要求客户端跟着升级B/S架构 Browser/Server 浏览器/服务器特点:不需要安装客户端,一个浏览器足矣,例如:网页淘宝 网页京东 网页12306优点:不会占用硬盘空间,服务器只要升级不要求跟着升级缺点:效果不炫,所有压力都在服务器相同点: 都需要和服务器进行数据交互,都是先有请求,后给响应,一定是请求和响应成双成对Tomcat 服务器Tomcat 的目录结构bin 里面存放的都是tomcat的二进制命令 关注点:开启服务: startup.bat 关闭服务:shutdown.bat conf 里面存放的都是tomcat的配置文件 关注点:server.xml(配置端口) web.xml lib 里面存放的都是tomcat运行过程中提供支撑的jar包 logs 里面存放的都是日志文件 关注点:错误信息的日志文件查看 catalina.2020-xx-xx.log temp 里面存放的是tomcat运行过程中创建的临时文件 由tomcat维护 work 里面存放的内容和jsp相关 webapps(核心) 用来存放web项目 存放在这个里面的项目可以被浏览器直接访问到 http://localhost:8080 --> webapps文件夹下虚拟路径发布项目方式1:配置独立xml文件在 tomcat/conf 目录下新建一个 Catalina 目录(如果已经存在无需创建)在 Catalina 目录下创建 localhost 目录(如果已经存在无需创建)在 localhost 中创建 xml 配置文件,名称为:xxx.xml(xxx名就是项目的浏览器访问别名)xxx.xml中代码如下:<Context docBase="项目所在的硬盘位置" />好处:使用配置文件对项目的部署和卸载不用重启 tomcat 了,也不影响 tomcat 整体的配置文件方式2:配置 server.xml,添加 context 标签在 server.xml 配置文件的最后加上如下代码:<Context path="项目的浏览器访问别名" docBase="项目所在的硬盘位置" />注意:谨慎使用(最好单独独立出来一个配置文件)电脑安装 Tomcat(了解)在电脑安装一个 tomcat 软件,将电脑变成 web 服务器,步骤:下载一个 tomcat 服务器软件,官网地址 http://tomcat.apache.org解压下载好的 tomcat。注意:将解压后的文件 copy 到一个没有中文和空格的路径下启动 tomcat 服务:进入 bin 路径,双击 startup.bat 服务器就可以启动(默认占用电脑的8080端口)测试:在页面输入地址访问 http://localhost:8080关闭 tomcat 服务:直接关闭 或者去 bin 路径双击 shutdown.batTomcat启动的常见问题总结1、一闪而过 原因:没有配置环境变量JAVA_HOME 或者配置错误 解决:配置环境变量JAVA_HOME,因为tomcat在启动的时候会去找环境变量JAVA_HOME Tomcat的底层需要JDK的支撑 JAVA_HOME=jdk的安装路径(不要加bin目录) path=%JAVA_HOME%\bin 2、报错 java.net.BindException: Address already in use: bind 原因:端口号被占用 方式1:结束正在占用端口的进程 1) netstat -ano 查看端口对应的pid 2) 打开任务管理器 结束pid对应的进程 方式2:改变自己的端口号 修改conf文件夹中的server.xml 大概在69行的位置 或者修改端口号为80: 80是默认端口号.可以不写idea 集成 Tomcat 环境(了解)idea 自动用的是虚拟路径的方式给 tomcat 管理的idea集成tomcat步骤:idea创建web项目:idea发布项目:Http协议概念及作用概念:协议:规定了被约束对象都需要去遵守的规则http协议:是互联网上运用最为广泛的一种浏览器和服务器之间的协议,规定了浏览器和服务器之间要遵守的规则只要浏览器访问服务器或者是服务器响应浏览器必须要遵循一些规则,这些规则都是由http协议制定好的http协议就是咱们浏览器和服务器之间的合同,双方都要遵循这份合同的规定内容作用:规定了浏览器去请求服务器时候必须带哪些数据,以及这些数据都要以什么格式进行传递规定了服务器去响应浏览器的时候必须响应哪些数据回去,以及这些数据都要以什么格式进行传递请求内容:请求行 请求头 请求体请求内容分为:请求行(第一行)POST /web02_1/demo1.html HTTP/1.1 提交方式 要请求的服务器资源 使用的是http协议的那个版本(固定)请求头(key:value)数据的格式:键值对的数据常用头数据:Referer User-AgentReferer:当前页面的来源地址,可以解决防盗链User-Agent:用户的浏览器版本信息,可以解决下载请求体(要传递给服务器的页面数据)只有 post 提交才有 是页面表单的数据要传递给服务器的内容响应内容:响应行 响应头 响应体响应内容分为:响应行// 组成部分: HTTP/1.1 200 固定协议版本 状态码 // 常见的响应状态码: 200 OK 请求已成功,响应也没问题。出现此状态码是表示正常状态。 302 资源重定向 304 去找缓存数据 403 服务器拒绝执行 (文件或文件夹加了权限) 404 请求的资源没找到 405 请求的方法不存在 500 服务器错误 (代码写的有问题)响应头常用响应头:location:重定向操作,通常告知浏览器马上向该地址发送请求refresh:定时刷新操作,指定时间后跳转到指定页面content-disposition:通知浏览器以何种方式获取数据(直接展示数据或者以附件方式)--下载content-type:通知浏览器返回的资源类型以及编码格式 --处理响应数据的中文乱码响应体浏览器页面要解析显示的内容即是响应体的内容get 提交和 post 提交的区别get 提交的数据不安全,因为所有的页面表单数据都在请求行中,意味着资源都在地址栏暴露了post 提交的数据安全,因为所有的页面表单数据都在请求体中,意味着资源都不会在地址栏暴露了get 提交有着大小的限制,post 提交没有大小的限制get 提交没有请求体,所有的表单的数据都在请求行中post 有请求体,所有的表单数据都在请求体中Java web 项目项目的结构在 JavaEE 规范中,WEB 项目存在一定的目录结构,具体结构如 web项目(myweb) || ------ 静态资源(如html、css、js、img等)、二级目录等 || ------ WEB-INF目录(放在该目录下的资源浏览器是不能直接访问到) || ------ classess目录:放的都是java代码的 .class || ------ lib目录:放入项目需要的jar || ------ web.xml:配置文件(配置的是项目的内容) 是tomcat首先会去加载的配置文件 servlet如果是2.5的版本这个web.xml文件必须有 servlet如果是3.0的版本这个web.xml文件可以不要 用注解代替三大技术:Servlet、Filter、Listenerjava web 项目服务器有三大技术:Servlet、Filter、Listenerservlet:本质就是一个类,这个类实现了 java 提供的 Servlet 规范。可以对浏览器的请求和响应进行处理filter:本质上是一个类,这个类需要实现 java 提供的 Filter 规范。可以对浏览器访问服务器资源时的一种拦截,可以让符合条件放行,不符合条件不放行listener:本质上是一个类,实现了 java 的 Listener 规范,可以监听其它对象(域)的状态变化Servlet概述Servlet 运行在服务端的 Java 小程序,是 sun 公司提供一套规范,用来处理客户端请求、响应给浏览器的动态资源。 servlet 的实质就是 java 代码,通过 java 的 API 动态的向客户端输出内容。servlet 的作用:在服务器端接受浏览器带来的请求数据(请求行、请求头、请求体)给浏览器做响应数据(响应行、响应头、响应体)servlet 与普通的 java 程序的区别:必须实现 servlet 接口必须在 servlet 容器(服务器)中运行servlet 程序可以接收用户请求参数以及向浏览器输出数据快速入门servlet2.5 的方式:必须有 web.xml 配置文件三个步骤创建一个 servlet:创建一个类实现 servlet 接口重写 service 方法在 web.xml 文件中配置创建的类(将请求路径和 java 程序的对应关系建立起来)tomcat 只要一启动,就会去加载 web.xml 文件,只加载一次只要这个 web.xml 配置文件修改了任何一处,都要重新 tomcat 重新加载servlet 的创建和服务方法、service 的执行都是由 tomcat 自动完成servlet3.0 的方式:没有web.xml配置文件步骤:创建 JavaEE6(含6) 以上的工程创建一个类实现 servlet 接口,并在实现类上标注 @WebServle 注解@WebServlet 注解的属性:name 属性:servlet名称,相当于web.xml中的<servlet‐name>urlPatterns 属性:请求路径示例:@WebServlet(name = "HelloServlet", urlPatterns = "/hello")servlet 的生命周期一个对象从创建到消亡的过程,就是生命周期。和 servlet 的生命周期相关的 3 个方法:// Servlet对象只要一创建出来就会调用的方法 void init(ServletConfig var1) // 处理浏览器交互的方法 void service(ServletRequest var1, ServletResponse var2) // 销毁的方法 void destroy()Servlet 的生命周期:浏览器第一次访问该 servlet 的时候,创建该 servlet 的对象,执行 init 初始化方法,只创建一次,请求使用的都是同一个 servlet 对象(单例对象),线程是不安全的浏览器每次访问的时候都会执行 service 服务方法,访问一次执行一次当服务器关闭的时候,会销毁该 servlet,执行 destory 方法servlet 的体系结构servlet 的继承体系:HttpServlet(功能最多)extends GenericServlet implements Servlet接口在 servlet 中,真正执行程序逻辑的是 service 方法时,对于 servlet 的初始化和销毁,一般由服务器调用执行,开发者本身不需要关心。企业开发中:继承 HttpServlet,只需要复写 doget 和 dopost 方法,而开发工具已经提供好了模板,直接用即可Servlet 的配置url-pattern(路径规则)配置在创建 servlet 后,如果想要这个 servlet 可以被访问到,必须在 web.xml 文件中对其进行配置。url-pattern 标签就是用于确定访问一个 servlet 的路径示例: <servlet> <servlet-name>ServletDemo1</servlet-name> <servlet-class>cn.itcast.web.ServletDemo1</servlet-class> </servlet> <servlet-mapping> <servlet-name>ServletDemo1</servlet-name> <url-pattern>/sd1</url-pattern> <url-pattern>/sd2</url-pattern> <url-pattern>/sd3</url-pattern> <url-pattern>/sd</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ServletDemo2</servlet-name> <!-- 完全匹配 --> <url-pattern>/aaa</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ServletDemo3</servlet-name> <!-- 扩展名匹配 --> <url-pattern>*.do</url-pattern> </servlet-mapping>注:一个servlet也可以被不同的路径映射,但一般在开发中,只需要配置一个有效路径即可路径映射的配置方式:完全匹配:必须以 / 开始例如: /ms1、 /aaa/ms1、/aaa/bb/cc/ms1目录匹配:必须以 / 开始,以 * 结束例如: /*、/aaa/*扩展名(后缀名)匹配:以 *.xx 结束(xx 代表的是后缀名)。注意:不能以 / 开始例如:*.action、*.do多种匹配的优先级:完全匹配 > 目录匹配 > 扩展名匹配只有一个 servlet 会执行,执行的结果是按照优先级去执行的load-on-startup(优先级)配置load-on-startup 标签可以设置 servlet 的加载优先级别和容器是否在启动时加载该 servlet当值为 0 或者大于 0 时,代表容器启动时加载该 servlet。正数的值越小,启动时加载该 servlet 的优先级越高。如果为负数,则容器启动时不会加载该 servlet,只有该 servlet 被选择时才会加载。servlet 的默认创建是在第一次访问的时候示例:<servlet> <servlet-name>ServletDemo1</servlet-name> <servlet-class>cn.itcast.web.ServletDemo1</servlet-class> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ServletDemo1</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>注:数字 1 被 tomcat 默认的 servlet 给占用了,在 tomcat 的 web.xml 中可以看到也使用了一个 servlettomcat 一启动会加载 2 个 web.xml 文件,一个是项目自定义的 web.xml,一个 tomcat 的 web.xml<servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>该 servlet 是 tomcat 创建的,主要用来处理其它的 servlet 处理不了的请求比如在当前下访问不到的资源都会走该 servlet,该 servlet 底层默认走写好的页面(404 500...)Servlet 的路径servlet 的访问路径分为 2 种:绝对路径带协议的绝对路径地址栏的地址方式,一般用于外部资源的访问,例如:http://localhost:8080/web02/ms5不带协议的绝对路径把协议和端口名省略,永远只能访问到本机的资源,一般用于内部资源的访问, 例如:/web02/ms5,特点:以/开头 以资源名结尾相对路径:相对比较的是地址栏的地址特点:同级目录及以下 以 ./ 开头、以资源名结尾(也可省略 ./ )父级目录 以 ../ 开头,返回父级目录Servlet 相关对象HttpServletRequest 请求对象概述HttpServletRequest 对象代表浏览器端的请求,当浏览器端通过 HTTP 协议访问服务器时,HTTP 请求中的所有信息都封装在这个对象中,开发人员通过这个对象的 api,可以获得客户通过浏览器传递过来的数据。HttpServletRequest 对象的作用:封装浏览器的请求数据(请求行、请求头 、请求体)APIHttpServletRequest extends ServletRequest获取请求行数据(请求方式、访问的资源、协议版本)// 获取请求方式 String getMethod() // 获取请求行中的资源名部分 String getRequestURI() // 获取客户端请求完整URL StringBuffer getRequestURL() // 获取请求协议和版本 String getProtocol() // 获取端口 int getLocalPort() // 获取请求者的ip地址 String getRemoteAddr() // 获取当前的项目名路径 String getContextPath()获取请求头数据(键值对 key/value 的数据)// 根据请求头的key获取value String getHeader(String key) // 例如:Referer可以获取到来源地址(没有来源为null:直接访问) --防盗链 // 例如:User-Agent可以获取用户的浏览器版本信息 --下载必备、处理下载乱码的 // 返回此请求包含的所有头名称 Enumeration getHeaderNames()获取请求体数据(所有浏览器提交的表单数据)// 获取单一name对应的value值 String getParameter(String name) // 获取多个name对应的多个value值 String[] getParameterValues(String name) // 获取页面所有的value值 Map<String,String[]> getParameterMap() // 注意:key:对应的是表单中name属性名,value对应的的是表单的name属性的value值 // 设置编码。一般设置为 utf-8,否则请求中中文可能会乱码 void setCharacterEncoding(String var1)作为容器数据存取删(request 也被称为域对象)// 存储数据 void setAttribute(String name, Object o) // 获取数据 Object getAttribute(String name) // 移除数据 void removeAttribute(String name)注意:若想要在多个 servlet 之间的使用 request 传递数据,则需要保证多个 servlet 之间使用的是同一个 request 对象,可以使用请求转发。request.getRequestDispatcher("/servlet的地址").forward(request, response);HttpServletResponse 对象概述HttpServletResponse 对象:服务器用来给浏览器写内容的一个对象服务器有数据如果想给浏览器:只能按照 http 协议的规定通过3个方向给:响应行、响应头、响应体API操作响应行格式:协议/版本 状态码示例:HTTP/1.1 200状态码含义200 请求响应已成功 302 重定向 304 去找缓存数据 403 服务器拒绝执行 (文件或文件夹加了权限) 404 请求的资源没找到 405 请求的方法不存在 500 服务器错误 (代码写的有问题)操作响应头格式:key:valuAPI 方法:// 设置键值对形式的响应头 setHeader(String key,String value)常用响应头:// content-type:通知浏览器响应的内容是什么类型的,并且用什么编码解析内容。解决响应回去的中文乱码问题 response.setHeader("content-type","文件的类型;charset=utf-8"); // 简写 response.setContentType("文件的类型;charset=utf-8"); // location:重定向 response.setHeader("location", "/day31/sd4"); response.setStatus(302); // 简写 response.sendRedirect("url"); // refresh:定时刷新 response.setHeader("refresh","秒数;url=跳转的路径"); // content-disposition:通知浏览器写回去的东西要以附件形式打开 (只用于下载)。默认情况下都是页面直接展示写回去的数据 response.setHeader("content-disposition","attachment;filename=" + aaa.jpg);操作响应体页面上要展示的内容API 方法:// 字符流 PrintWriter getWriter() // 字节流 (二进制) ServletOutputStream getOutputStream()特点:若是能写的出来的内容用字符流,其他全用字节流(下载专用)不能同时出现服务器会自动关闭这2个流ServletContext 对象概述ServletContext:servlet 的上下文对象(全局管理者对象),是一个全局的储存信息的空间。一个 web 项目只有一个全局管理者(SevletContext对象)生命周期:当 web 应用被加载时,ServletContext 生命周期开始(创建)当 web 应用被移除容器时,ServletContext 生命周期结束(销毁)作用范围:整个 web 应用作用:可以在整个 web 应用(多个 servlet 之间)共享数据可以获取项目的地址和项目资源流(路径和流)API原生获取 ServletContext 对象在继承 HttpServlet 或 GenericServlet 的自定义类中,直接通过父类方法 getServletContext() 获取 ServletContext 对象public ServletContext getServletContext()Spring 项目获取 ServletContext 对象通过 spring 注入自动注入(推荐)@Autowired private ServletContext servletContext;通过 HttpServletRequest 对象@GetMapping("/test") public String test(HttpServletRequest request) { ServletContext servletContext = request.getServletContext(); return "success"; }实现 WebApplicationInitializer 接口,重写 onStartup() 方法import javax.servlet.ServletContext; import javax.servlet.ServletException; import org.springframework.web.WebApplicationInitializer; public class ApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { System.out.println(servletContext); }创建一个实现 ServletContextListener 的监听器// 必不可少,声明为监听器,注册到web容器中 @WebListener public class InitContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) { ServletContext servletContext = servletContextEvent.getServletContext(); System.out.println(servletContext); @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { }通过 ContextLoader 类ServletContext servletContext = ContextLoader.getCurrentWebApplicationContext().getServletContext();注意:使用这种方式需要 webApplicationContext 完成初始化后才能获取到 servletContext 对象,否则获取的是空值导致空指针异常操作 ServletContext 对象数据存取操作:保存在 ServletContext 中的数据是项目全局共享的数据,可以用来在多个 servlet 之间传递信息数据// 存。特点:可以存多对 但是存在key值覆盖 void setAttribute(String name, Object value) // 取。特点: \没取到返回null Object getAttribute(String name) void removeAttribute(String name)读取项目资源的方法// 获取WEB项目的磁盘路径。场景:上传 String getRealPath(String path) // 根据WEB项目的磁盘资源获取流。场景:下载 InputStream getResourceAsStream(String path) // 获取web.xml中<context>标签的参数内容 String getInitParameter(String name)
文件传输tftp:上传及下载文件tftp 命令用于传输文件。ftp 让用户得以下载存放于远端主机的文件,也能将文件上传到远端主机放置。tftp 是简单的文字模式 ftp 程序,它所使用的指令和 ftp 类似。语法格式:tftp [参数] # 常用参数: connect 连接到远程tftp服务器 mode 文件传输模式 put 上传文件 get 下载文件 quit 退出 verbose 显示详细的处理信息 trace 显示包路径 status 显示当前状态信息 binary 二进制传输模式 ascii ascii 传送模式 rexmt 设置包传输的超时时间 timeout 设置重传的超时时间参考实例:# 连接远程服务器”218.28.188.288″: [root@linuxcool ~]$ tftp 218.28.188.288 tftp> get file # 远程下载file文件: getting from 218.28.188.288 to /dir Recived 168236 bytes in 1.5 seconds[112157 bit/s] tftp> quit # 退出tftp: curl:文件传输工具curl 命令是一个利用 URL 规则在 shell 终端命令行下工作的文件传输工具;它支持文件的上传和下载,所以是综合传输工具,但按传统,习惯称 curl 为下载工具。作为一款强力工具,curl 支持包括 HTTP、HTTPS、ftp 等众多协议,还支持 POST、cookies、认证、从指定偏移处下载部分文件、用户代理字符串、限速、文件大小、进度条等特征;做网页处理流程和数据检索自动化。语法格式:curl [参数] [网址] # 常用参数: -O 把输出写到该文件中,保留远程文件的文件名 -u 通过服务端配置的用户名和密码授权访问参考实例:# 将下载的数据写入到文件,必须使用文件的绝对地址: curl https://www.linuxcool.com/abc.txt --silent -O # 访问需要授权的页面时,可通过-u选项提供用户名和密码进行授权: $ curl -u root https://www.linuxprobe.com/ Enter host password for user 'root':scp:与远程主机上的文件/目录拷贝# 从远程复制到本地 # 语法: scp [-r] 用户名@主机ip:绝对路径 存放路径 # 选项: -r :递归复制目录 # 参考实例:复制远程主机上的test.zip文件到当前目录下 scp root@30.23.17.201:/home/test.zip ./ # 从本地复制到远程 scp [-r] 存放路径 用户名@主机ip:绝对路径 # 选项: -r :递归复制目录rsync:远程数据同步rsync 命令是一个功能非常强大的远程数据同步工具,可通过 LAN/WAN 快速同步多台主机间的文件。rsync 使用所谓的“rsync算法”来使本地和远程两个主机之间的文件达到同步,这个算法只传送两个文件的不同部分,而不是每次都整份传送,因此速度相当快。语法:rsync [OPTION]... SRC DEST rsync [OPTION]... SRC [USER@]host:DEST rsync [OPTION]... [USER@]HOST:SRC DEST rsync [OPTION]... [USER@]HOST::SRC DEST rsync [OPTION]... SRC [USER@]HOST::DEST rsync [OPTION]... rsync://[USER@]HOST[:PORT]/SRC [DEST]使用示例:# 拷贝本地文件。当SRC和DES路径信息都不包含有单个冒号":"分隔符时就启动这种工作模式 rsync -a /data /backup # 将本地机器的内容拷贝到远程机器。当DST路径地址包含单个冒号":"分隔符时启动该模式。 rsync -avz *.c foo:src # 将远程机器的内容拷贝到本地机器。当SRC地址路径包含单个冒号":"分隔符时启动该模式。 rsync -avz foo:src/bar /data # 从远程rsync服务器中拷贝文件到本地机。当SRC路径信息包含"::"分隔符时启动该模式。 rsync -av root@192.168.78.192::www /databack # 从本地机器拷贝文件到远程rsync服务器中。当DST路径信息包含"::"分隔符时启动该模式。 rsync -av /databack root@192.168.78.192::www # 列出远程机的文件列表。这类似于rsync传输,不过只要在命令中省略掉本地机信息即可。 rsync -v rsync://192.168.78.192/www参数选项:-v, --verbose # 详细模式输出。 -q, --quiet # 精简输出模式。 -c, --checksum # 打开校验开关,强制对文件传输进行校验。 -a, --archive # 归档模式,表示以递归方式传输文件,并保持所有文件属性,等于-rlptgoD。 -r, --recursive # 对子目录以递归模式处理。 -R, --relative # 使用相对路径信息。 -b, --backup # 创建备份,也就是对于目的已经存在有同样的文件名时,将老的文件重新命名为~filename。 # 可以使用--suffix选项来指定不同的备份文件前缀。 --backup-dir # 将备份文件(如~filename)存放在在目录下。 -suffix=SUFFIX 定义备份文件前缀。 -u, --update # 仅仅进行更新,也就是跳过所有已经存在于DST,并且文件时间晚于要备份的文件,不覆盖更新的文件。 -l, --links # 保留软链结。 -L, --copy-links # 想对待常规文件一样处理软链结。 --copy-unsafe-links # 仅仅拷贝指向SRC路径目录树以外的链结。 --safe-links # 忽略指向SRC路径目录树以外的链结。 -H, --hard-links # 保留硬链结。 -p, --perms # 保持文件权限。 -o, --owner # 保持文件属主信息。 -g, --group # 保持文件属组信息。 -D, --devices # 保持设备文件信息。 -t, --times # 保持文件时间信息。 -S, --sparse # 对稀疏文件进行特殊处理以节省DST的空间。 -n, --dry-run # 现实哪些文件将被传输。 -w, --whole-file # 拷贝文件,不进行增量检测。 -x, --one-file-system # 不要跨越文件系统边界。 -B, --block-size=SIZE # 检验算法使用的块尺寸,默认是700字节。 -e, --rsh=command # 指定使用rsh、ssh方式进行数据同步。 --rsync-path=PATH # 指定远程服务器上的rsync命令所在路径信息。 -C, --cvs-exclude # 使用和CVS一样的方法自动忽略文件,用来排除那些不希望传输的文件。 --existing # 仅仅更新那些已经存在于DST的文件,而不备份那些新创建的文件。 --delete # 删除那些DST中SRC没有的文件。 --delete-excluded # 同样删除接收端那些被该选项指定排除的文件。 --delete-after # 传输结束以后再删除。 --ignore-errors # 及时出现IO错误也进行删除。 --max-delete=NUM # 最多删除NUM个文件。 -P, --partial # 保留那些因故没有完全传输的文件,以是加快随后的再次传输。 --force # 强制删除目录,即使不为空。 --numeric-ids # 不将数字的用户和组id匹配为用户名和组名。 --timeout=time # ip超时时间,单位为秒。 -I, --ignore-times # 不跳过那些有同样的时间和长度的文件。 --size-only # 当决定是否要备份文件时,仅仅察看文件大小而不考虑文件时间。 --modify-window=NUM # 决定文件是否时间相同时使用的时间戳窗口,默认为0。 -T --temp-dir=DIR # 在DIR中创建临时文件。 --compare-dest=DIR # 同样比较DIR中的文件来决定是否需要备份。 --progress # 显示备份过程。 -z, --compress # 对备份的文件在传输时进行压缩处理。 --exclude=PATTERN # 指定排除不需要传输的文件模式。 --include=PATTERN # 指定不排除而需要传输的文件模式。 --exclude-from=FILE # 排除FILE中指定模式的文件。 --include-from=FILE # 不排除FILE指定模式匹配的文件。 --version # 打印版本信息。 --address # 绑定到特定的地址。 --config=FILE # 指定其他的配置文件,不使用默认的rsyncd.conf文件。 --port=PORT # 指定其他的rsync服务端口。 --blocking-io # 对远程shell使用阻塞IO。 -stats # 给出某些文件的传输状态。 --progress # 在传输时现实传输过程。 --log-format=formAT # 指定日志文件格式。 --password-file=FILE # 从FILE中得到密码。 --bwlimit=KBPS # 限制I/O带宽,KBytes per second。 -h, --help # 显示帮助信息。网络通讯ssh:安全连接客户端ssh 命令是 openssh 套件中的客户端连接工具,可以给予 ssh 加密协议实现安全的远程登录服务器,实现对服务器的远程管理。语法格式:ssh [参数] [远程主机] # 常用参数: -1 强制使用ssh协议版本1 -2 强制使用ssh协议版本2 -4 强制使用IPv4地址 -6 强制使用IPv6地址 -A 开启认证代理连接转发功能 -a 关闭认证代理连接转发功能 -b<IP地址> 使用本机指定的地址作为对位连接的源IP地址 -C 请求压缩所有数据 -F<配置文件> 指定ssh指令的配置文件,默认的配置文件为“/etc/ssh/ssh_config” -f 后台执行ssh指令 -g 允许远程主机连接本机的转发端口 -i<身份文件> 指定身份文件(即私钥文件) -l<登录名> 指定连接远程服务器的登录用户名 -N 不执行远程指令 -o<选项> 指定配置选项 -p<端口> 指定远程服务器上的端口 -q 静默模式,所有的警告和诊断信息被禁止输出 -X 开启X11转发功能 -x 关闭X11转发功能 -y 开启信任X11转发功能参考实例:# 登录远程服务器: ssh 用户名@ip # 用test用户连接远程服务器: ssh -l test 202.102.220.88 # 查看分区列表: ssh 202.102.220.88 /sbin/fdisk # 强制使用ssh协议版本1: ssh -1 # 开启认证代理连接转发功能: ssh -Aping:测试主机间网络连通性ping 命令主要用来测试主机之间网络的连通性,也可以用于执行 ping 指令会使用 ICMP 传输协议,发出要求回应的信息若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。不过值得注意的是:Linux 系统下的 ping 命令与 Windows 系统下的 ping 命令稍有不同。Windows 下运行 ping 命令一般会发出 4 个请求就结束运行该命令;Linux 下运行 ping 命令不会自动终止,需要按 CTR+C 终止或者使用 -c 参数为ping命令指定发送的请求数目。语法格式:ping [参数] [目标主机] # 常用参数: -c 指定发送报文的次数 -i 指定收发信息的间隔时间 -I 使用指定的网络接口送出数据包 -l 设置在送出要求信息之前,先行发出的数据包 -n 只输出数值 -p 设置填满数据包的范本样式 -q 不显示指令执行过程 -R 记录路由过程 -s 设置数据包的大小 -t 设置存活数值TTL的大小 -v 详细显示指令的执行过程 -d 使用Socket的SO_DEBUG功能参考实例:# 检测与linuxcool网站的连通性: ping www.linuxcool.com # 连续ping4次: ping -c 4 www.linuxcool.com # 设置次数为4,时间间隔为3秒: ping -c 4 -i 3 www.linuxcool.com # 利用ping命令获取指定网站的IP地址: $ ping -c 1 linuxcool.com | grep from | cut -d " " -f 4 220.181.57.216ifconfig:显示或设置网络设备ifconfig 命令的英文全称是“network interfaces configuring”,即用于配置和显示 Linux 内核中网络接口的网络参数。ifconfig 命令可以配置的网卡信息,但在网卡重启后机器重启后,配置就不存在。要想将上述的配置信息永远的存的电脑里,那就要修改网卡的配置文件了。语法格式:ifconfig [参数] # 常用参数: add<地址> 设置网络设备IPv6的IP地址 del<地址> 删除网络设备IPv6的IP地址 down 关闭指定的网络设备 up 启动指定的网络设备 IP地址 指定网络设备的IP地址参考实例:# 显示网络设备信息: ifconfig # 启动关闭指定网卡: ifconfig eth0 down ifconfig eth0 up # 为网卡配置和删除IPv6地址: ifconfig eth0 add 33ffe:3240:800:1005::2/64 ifconfig eth0 del 33ffe:3240:800:1005::2/64 # 用ifconfig修改MAC地址: ifconfig eth0 down ifconfig eth0 hw ether 00:AA:BB:CC:DD:EE ifconfig eth0 up ifconfig eth1 hw ether 00:1D:1C:1D:1E ifconfig eth1 up # 配置IP地址: ifconfig eth0 192.168.1.56 ifconfig eth0 192.168.1.56 netmask 255.255.255.0 ifconfig eth0 192.168.1.56 netmask 255.255.255.0 broadcast 192.168.1.255软件包下载与安装rpm:RPM 软件包管理器rpm 命令是 Red-Hat Package Manager(RPM软件包管理器)的缩写, 该命令用于管理 Linux 下软件包的软件。在 Linux 操作系统下,几乎所有的软件均可以通过RPM 进行安装、卸载及管理等操作。概括的说,rpm 命令包含了五种基本功能:安装、卸载、升级、查询和验证。语法格式:rpm [参数] [软件包名] # 常用参数: # 查询/验证 软件包选项: -a, --all 查询所有的软件包 -f, --file 查询文件或命令属于哪个软件包 -p, --package 查询指定的rpm软件包 # 查询软件包选项(with -q or --query): -q, --query 查询软件包是否安装 -l, --list 显示软件包的文件列表 -c, --configfiles 列出组态配置文件,本参数需配合”-l”参数使用 -d, --docfiles 列出文本文件,本参数需配合”-l”参数使用 -s, --state 显示文件状态,本参数需配合”-l”参数使用 -R 显示软件包的依赖关系 # 安装/升级/删除选项: --test 不真正安装,只是判断下是否能安装 -i, --install 安装软件包 -h, --hash 安装软件包时列出标记(和 -v 一起使用效果更好) -e, --erase 卸载软件包 -U, --upgrade 升级软件包 -v, --verbose 提供更多的详细信息输出 --force 强制参考示例:# 查询指定软件包是否安装 rpm -q 软件包名 # 列出所有已安装的软件包: rpm -qa # 查询软件包的详细信息 rpm -qi 软件包名 # 命令查询软件包的文件列表 rpm -ql 软件包名 # 命令查询系统文件属于哪个RPM包 rpm -qf 系统文件名 # 查询软件包的依赖关系 rpm -qR 软件包名 # 直接安装软件包: rpm -ivh 软件包名 # 忽略报错,强制安装: rpm --force -ivh 软件包名 # 卸载rpm包: rpm -e 软件包名 # 强力删除模式,如果使用上面命令删除时,提示有依赖的其它文件,则用该命令可以对其进行强力删除 rpm -e --nodeps 软件名 # 升级软件包: pm -U 软件包名yum:基于 RPM 的软件包管理器yum 命令是在 Fedora 和 RedHat 以及 SUSE 中基于 rpm 的软件包管理器,它可以使系统管理人员交互和自动化地更新与管理 RPM软件包,能够从指定的服务器自动下载RPM包并且安装,可以自动处理依赖性关系,并且一次安装所有依赖的软体包,无须繁琐地一次次下载、安装。yum 提供了查找、安装、删除某一个、一组甚至全部软件包的命令,而且命令简洁而又好记。语法格式:yum [options] [command] [package ...] # 常用命令: list # 列出所有已安装的和可安裝的软件包清单 list installed # 列出所有已安装的软件包清单 list updates # 列出所有可更新的软件包清单 install # 安装rpm软件包 update # 更新rpm软件包 check-update # 检查是否有可用的更新rpm软件包 remove # 删除指定的rpm软件包 search # 检查软件包的信息 info # 显示指定的rpm软件包的描述信息和概要信息 clean # 清理yum过期的缓存 shell # 进入yum的shell提示符 resolvedep # 显示rpm软件包的依赖关系 localinstall # 安装本地的rpm软件包 localupdate # 显示本地rpm软件包进行更新 deplist # 显示rpm软件包的所有依赖关系 # 常用参数: -y 对所有的提问都回答“yes” -c 指定配置文件 -q 安静模式。不显示安装的过程 -v 详细模式 -t 检查外部错误 -d 设置调试等级(0-10) -e 设置错误等级(0-10) -R 设置yum处理一个命令的最大等待时间 -C 完全从缓存中运行,而不去下载或者更新任何头文件 -h 显示帮助信息参考实例:# 列出所有已安装的和可安裝的软件包清单 yum list # 列出以mysql开头的已安装的和可安裝的软件包清单 yum list mysql* # 列出匹配到“mysql*”的可用的数据包: yum list available 'mysql*' # 自动搜索安装最快镜像插件: yum install yum-fastestmirror # 安装yum图形窗口插件: yum install yumex # 查看可能批量安装的列表: yum grouplist # 安装常用工具软件及依赖 yum groupinstall base # 卸载/删除vim-common: yum remove vim-common.x86_64 # 清除缓存目录下的软件包: yum clean packages # 显示httpd安装包信息: yum info httpd.x86_64离线安装 rpm 包自动解决依赖参考:centos7离线安装rpm包自动解决依赖当生产环境由于安全原因处于断网状态时,要安装 linux 软件只能通过本地源的方式:外网主机通过 downloadonly 或 yumdownloade 插件获取该软件及其依赖包,供本地源使用。使用 yum localinstall 自动安装软件,并且自动处理好依赖关系。# 安装 yum-plugin-downloadonly插件(外网主机)。一般yum自带此插件,不用安装 yum install -y yum-plugin-downloadonly # 使用yum配合插件下载软件包(rpm)到本地目录 yum install --downloadonly --downloaddir=DIR 软件包名 # 说明: --downloadonly # 仅下载,不安装 --downloaddir=DIR # 指定下载存储路径 # 本地安装软件包(自动解决依赖) yum localinstall *.rpm -ywget:从 Web 下载文件参考:wget命令详解支持 HTTP、HTTPS及FTP协议下载文件,而且wget还提供了很多选项,例如下载多个文件、后台下载,使用代理等等语法格式:wget [options] [url]常用参数选项 [options] :-O # 选项以其他名称保存下载的文件 -N # 只获取比本地文件新的文件 -P # 选项将文件下载到指定目录 -c # 选项断点续传。 # 当下载一个大文件时,如果中途网络断开导致没有下载完成,可以使用命令的-c选项恢复下载,让下载从断点续传,无需从头下载 -b # 选项在后台下载 -i # 选项下载多个文件。 # 如果先要一次下载多个文件,首先需要创建一个文本文件,并将所有的url添加到该文件中,每个url都必须是单独的一行。 # 然后使用-i选项,后跟该文本文件 -U # 选项设定模拟下载。如果远程服务器阻止wget下载资源,我们可以通过-U选项模拟浏览器进行下载,例如下面模拟谷歌浏览器下载。 --limit-rate # 选项限制下载速度Shell 脚本编程常用命令echo:输出字符串或提取Shell变量的值echo命令用于在终端设备上输出字符串或变量提取后的值,这是在 Linux 系统中最常用的几个命令之一。一般使用在变量前加上$符号 的方式提取出变量的值,例如:$PATH,然后再用 echo 命令予以输出。或者直接使用 echo 命令输出一段字符串到屏幕上,起到给用户提示的作用。语法格式:echo [参数] [字符串] # 常用参数: -n # 不输出结尾的换行符 -E # 禁用反斜杠转译 -e # 开启输出字串中对反斜杠的转译,即支持 \n 换行 # 只有开启-e参数的时候,下面的命令才能起作用: \0NNN # 输出NNN(一个八进制数)在ASCII码表中对应的字符,如:a->97(十进制)->141(八进制),echo -e "\0141" 结果:a \\ # 输出反斜杠 \a # 报鸣 \b # 退格,即删除前面的一个字符 \c # 抑制当前行的换行符(但是结果是\c后面的字符都没有显示) \f # 填表格(就是新起一行打印,但是纵向的位置不变) \n # 换行,光标移至行首 \r # 光标移至行首,但不换行 \t # 水平制表符 \v # 垂直制表符(效果好像和\f一样)注意:echo 后面使用双引号的话,双引号里面的内容会翻译,输出value单引号的话,双引号里面的内容不会翻译,输出$key$key=value echo "$key" # 输出结果:value echo '$key' # 输出结果:$key参考示例:# 输出一段字符串: echo "LinuxCool.com" 输出:LinuxCool.com # 返回上一条命令的执行结果。0:成功;非0:失败 echo $? # 输出变量提取后的值: echo $PATH 输出:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin # 对内容进行转义,不让$符号的提取变量值功能生效: echo \$PATH 输出:$PATH # 查看自己linux系统的默认解析器 echo $SHELL # 结合输出重定向符,将字符串信息导入文件中: echo "It is a test" > linuxcool # 使用反引号符执行命令,并输出其结果到终端: echo `date` # 输出带有换行符的内容: echo -e "a\nb\nc" # 输出信息中删除某个字符,注意看数字3消失了: echo -e "123\b456" 输出:12456awk:文本和数据进行处理的编程语言参考:Linux:“awk”命令的妙用awk 是一个强大的文本分析工具,简单来说 awk 就是把 管道 或文件逐行读入,(空格,制表符)为默认分隔符将每行切片,切开的部分再进行各种分析处理语法格式:awk [参数] 'COMMAND' [文件] # 注意:COMMAND(命令)只能使用单引号包裹 # 常用参数: -F '分隔符' # 指定字段分隔符。不指定默认使用空格、制表符作为分隔符。单引号、双引号包裹均可 -v # 自定义变量 -f # 从脚本中读取awk命令 -m # 对val值设置内在限制 -v var=$val, --assign=var=$val # 给COMMAND中传脚本变量值 # 先将脚本变量赋值awk内部自定义变量(var),再在awk命令中使用内部变量var) # awk 会根据分隔符将行分成若干个字段,$0为整行,$1为第一个字段,$2 为第2个地段... # 为打印一个字段或所有字段,使用 print 命令。这是一个 awk 动作 # 参考实例 # 打印管道输入的第1个字段 echo "this is a test" | awk '{print $1}' # 输出: # 打印每一行的第二和第三个字段 awk '{print $2,$3}' file awk 取行,匹配文件内容awk -F ":" '{print $1 " " $3}' /etc/passwd # $1与$3之间手动添加空格分隔 awk '/ljc/' file # 显示文件file中包含ljc的匹配行。 awk '!/ljc/' file # 显示文件file中不包含ljc的匹配行。 awk '/起始内容/,/终止内容/' file # 取包含内容的区间行区间匹配 awk '/halt|sync/' file # 匹配halt或者sync的行 awk 'NR>=5 && NR<=10' file # 取5到10行,逻辑与 awk -F ": '$1~/mail/ || $3>1 {print }' /ljc # 取$1或者mail或者$3>1的行,逻辑或awk 中的替换# 替换但不修改文件内容 gsub(/目标/,"替换为什么",第几列) gsub(/目标/,"替换为什么") 等同 gsub(/目标/,"替换为什么",$0) awk 'gsub(/halt/,"tihuang")' liangjc.txt awk 内置变量# 变量名 解释 FILENAMEawk # 浏览的文件名 FS # 设置输入字段分隔符,等价于命令行-F选项 NF # 浏览记录的字段个数 NR # 已读的记录数 $n # awk会根据分隔符将行分成若干个字段,$0为整行,$1为第一个字段,$2 为第2个地段... # 参考实例 # 打印行号在第一列 echo "this is a test" | awk '{print NR " " $1}' # 输出: 1 this # 打印每行列数 echo "this is a test" | awk '{print NF}' # 输出: 4awk 内置函数# 函数名 作用 toupper(s) # 返回s的大写 tolower(s) # 返回s的小写 length(s) # 返回s长度 substr(s,p) # 返回字符串s中从p开始的后缀部分awk 条件操作符# 操作符 描述 < # 小于 <= # 小于等于 == # 等于 != # 不等于 ~ # 匹配正则表达式 !~ # 不匹配正则表达式 awk 正则表达式匹配# 符号 # 表达含义 * # 与前面的正则表达式的零个或多个出现匹配 . # 匹配任何单个字符 \ # 转义随后的特殊字符 + # 匹配前面的正则表达式的一次或多次出现 ^ # 作为正则表达式的第一个字符,表示匹配行的开始,以什么开头 $ # 作为正则表达式的最后一个字符,表示匹配行的结尾 ? # 匹配前面的正则表达式的零次或一次出现 {n,m} # 匹配它前面某个范围内单个字符出现的次数,{n}将匹配n次出现,{n,}至少匹配n次出现,{n,m}匹配n和m之间的任意次出现支持 BEGIN 和 END 模块awk 读取文件之后执行,先计算,最后END{}显示结果# 参考实例: # 打印管道输入的第1个字段,行前面加列名,最后一行添加 end echo "this is a test" | awk '{print "name"} {print $1} END{print "end"}' # 输出: endawk 流程控制语句(类C语言,java语言)参考:https://www.cnblogs.com/minseo/p/13684207.html条件判断语句# if语句 { if(条件) { 动作1; 动作2 } } # 多个动作语句使用 ; 分隔 { if(条件) 动作 } # 动作只有一条命令时,大括号可以省略 # if...else 语句 { if(条件) { 动作 } else { 动作 } } # if...else if...else 语句 { if(条件1) { 动作 } else if (条件2) { 动作 } else { 动作 } } awk -F ':' '{ if ($1 > "d") { print $1 } else { print "-" } }' /etc/passwd # 可以把流程控制语句放到一个脚本中,然后调用脚本执行,如test.sh的内容如下 if ($1 > "d") { print $1 } else { print "-" # 用如下方式执行,效果一样 awk -F ':' -f test.sh /etc/passwd循环控制语句break 用以结束循环Continue 用于在循环体内部结束本次循环,从而直接进入下一次循环迭代Exit 用于结束脚本程序的执行# for语句。动作命令只有一条时,可以省略 { } { for (初始化动作; 条件; 递增/递减操作 ) { 动作 } } awk '{ for (i = 1; i <= 5; ++i) print i }' # while语句。动作命令只有一条时,可以省略 { } { while(条件) { 动作 } }xargs:给命令传递参数xargs(英文全拼: eXtended ARGuments)是给命令传递参数的一个过滤器,也是组合多个命令的一个工具。xargs 可以将管道或标准输入(stdin)数据转换成命令行参数,也能够从文件的输出中读取数据。xargs 也可以将单行或多行文本输入转换为其他格式,例如多行变单行,单行变多行。xargs 默认的命令是 echo,这意味着通过管道传递给 xargs 的输入将会包含换行和空白,不过通过 xargs 的处理,换行和空白将被空格取代。xargs 是一个强有力的命令,它能够捕获一个命令的输出,然后传递给另外一个命令。xargs 一般是和管道一起使用。命令格式:SOME_COMMAND | xargs [OPTION]... COMMAND INITIAL-ARGS... # 选项: -n NUM, --max-args=NUM # 从管道每次读取num个值作为一个参数传给 command 命令,默认是用所有的 -L LINES, -l LINES, --max-lines=LINES # 从管道每次读取 LINES 行传给 command 命令 -d STRING, --delimiter=STRING # 指定参数的分隔符(禁用引号和反斜杠)。 # 默认argument(参数)的分隔符是空格,xargs分隔符是回车 -i R, --replace=[R], -I R # 从管道读取的参数替换初始参数中的R。若R未指定,默认R为{}。看linux具体支持哪个参数 -r, --no-run-if-empty # 当xargs的输入为空的时候则停止xargs,不用再去执行了 -t, --verbose # 表示先打印命令,然后再执行 -p, --interactive # 每次执行一个管道参数的时候询问一次用户 -a FILE, --arg-file=FILE # 从文件中读入作为 stdin -s NUM, --max-chars=NUM # 命令行的最大字符数,指的是 xargs 后面那个命令的最大命令行字符数 -x, --exit # 如果超出大小(参见-s),则退出。主要是配合-s使用 --process-slot-var=VAR # 在子进程中设置环境变量VAR -e [END], --eof[=END], -E [END] # END必须是一个以空格分隔的标志,当xargs分析到含有END这个标志的时候就停止参考实例:# 遍历打印当前文件夹的文件名和目录名 ls | xargs -n1 echo # 遍历打印当前文件夹的文件名和目录名,使用预定义传参 ls | xargs -n1 -i echo "file name: {}, 1" # 多行输入单行输出 cat test.txt | xargs # 指定参数分隔符 $ echo "nameXnameXnameXname" | xargs -dX name name name nameread:接收键盘或其它文件描述符的输入read 命令接收标准输入(键盘)的输入,或者其他文件描述符的输入。得到输入后,read 命令将数据放入一个标准变量中。格式:read [选项] [变量名...] # 选项: -p str # “提示信息”:在等待read输入时,输出提示信息 -t int # 秒数。read命令会一直等待用户输入,使用此选项可以指定等待时间 -n int # 字符数。read命令只接收指定的字符数就会执行 -s # 隐藏输入的数据,适用于机密信息的输入注意:变量名可以自定义。如果不指定变量名,则会把输入保存到默认变量 REPLY 中;如果只提供了一个变量名,则将整个输入行赋予该变量;如果提供了一个以上的变量名,则输入行以空格分隔的若干字段会一个接一个地赋值各个变量,若输入的字段数大于变量数,则命令行上的最后一个变量被赋值剩余的所有字段若输入的字段数少于变量数,则未被赋值的变量值为空使用示例#读取从键盘的输入,并赋值给website变量 read website # cat 命令的输出作为read命令的输入,read读到的值放在line变量中 cat test.txt | read linetee:打印标准输出并储存到文件中tee指令会从标准输入设备读取数据,将其内容输出到标准输出设备,同时保存成文件语法:tee [options] [文件...] # 选项: -a, --append # 附加到既有文件的后面,而非覆盖它 -i-i, --ignore-interrupts # 忽略中断信号输入/输出重定向大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端。重定向命令列表如下:命令说明command > file将输出重定向到 file。command < file将输入重定向到 file。command >> file将输出以追加的方式重定向到 file。n > file将文件描述符为 n 的文件重定向到 file。n >> file将文件描述符为 n 的文件以追加的方式重定向到 file。n >& m将输出文件 m 和 n 合并。n <& m将输入文件 m 和 n 合并。<< tag将开始标记 tag 和结束标记 tag 之间的内容作为输入。注意:文件描述符: 0 是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。(1)输出重定向重定向一般通过在命令间插入特定的符号来实现。特别的,这些符号的语法如下所示:command1 > file1上面这个命令执行command1然后将输出的内容存入file1。注意:任何file1内的已经存在的内容将被新内容替代。如果要将新内容添加在文件末尾,请使用 >> 操作符。实例:# 执行下面的 who 命令,它将命令的完整的输出重定向在用户文件中(users): $ who > users # 执行后,并没有在终端输出信息,这是因为输出已被从默认的标准输出设备(终端)重定向到指定的文件。 # 使用 cat 命令查看文件内容: $ cat users _mbsetupuser console Oct 31 17:35 tianqixin console Oct 31 17:35 tianqixin ttys000 Dec 1 11:33 # 输出重定向会覆盖文件内容,请看下面的例子: $ echo "菜鸟教程:www.runoob.com" > users $ cat users 菜鸟教程:www.runoob.com # 如果不希望文件内容被覆盖,可以使用 >> 追加到文件末尾,例如: $ echo "菜鸟教程:www.runoob.com" >> users $ cat users 菜鸟教程:www.runoob.com 菜鸟教程:www.runoob.com(2)输入重定向和输出重定向一样,Unix 命令也可以从文件获取输入,语法为:command1 < file1这样,本来需要从键盘获取输入的命令会转移到文件读取内容。注意:输出重定向是大于号(>),输入重定向是小于号(<)。实例:# 接着以上实例,我们需要统计 users 文件的行数,执行以下命令: $ wc -l users 2 users # 也可以将输入重定向到 users 文件: $ wc -l < users # 注意:上面两个例子的结果不同:第一个例子,会输出文件名;第二个不会,因为它仅仅知道从标准输入读取内容。 # 同时替换输入和输出,执行command1,从文件infile读取内容,然后将输出写入到outfile中。 command1 < infile > outfile(3)重定向深入讲解一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。默认情况下,command > file 将 stdout 重定向到 file,command < file 将stdin 重定向到 file。如果希望 stderr 重定向到 file,可以这样写:# stderr 重定向到 file $ command 2>file # stderr 追加到 file 文件末 $ command 2>>file # 将 stdout 和 stderr 合并后重定向到 file $ command > file 2>&1 或 $ command >> file 2>&1 # stdin 和 stdout 都重定向。command 命令将 stdin 重定向到 file1,将 stdout 重定向到 file2。 $ command < file1 >file2(4)Here DocumentHere Document 是 Shell 中的一种特殊的重定向方式,用来将输入重定向到一个交互式 Shell 脚本或程序。它的基本的形式如下:command << delimiter document delimiter它的作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command。注意:结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。开始的delimiter前后的空格会被忽略掉。实例# 在命令行中通过 wc -l 命令计算 Here Document 的行数: $ wc -l << EOF www.runoob.com 3 # 输出结果为 3 行可以将 Here Document 用在脚本中,例如:#!/bin/bash # author:菜鸟教程 # url:www.runoob.com cat << EOF www.runoob.com EOF执行以上脚本,输出结果:欢迎来到 www.runoob.com(5)dev/null 文件如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:$ command > /dev/null/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。如果希望屏蔽 stdout 和 stderr,可以这样写:$ command > /dev/null 2>&1注意:0 是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。这里的 2 和 > 之间不可以有空格,2> 是一体的时候才表示错误输出。2>&1 的含义解释参考:https://blog.csdn.net/zhaominpro/article/details/82630528(1)在Linux系统中 0 1 2 是一个文件描述符名称代码操作符Java中表示Linux 下文件描述符(Debian 为例)标准输入(stdin)0< 或 <<System.in/dev/stdin -> /proc/self/fd/0 -> /dev/pts/0标准输出(stdout)1\>, >>, 1> 或 1>>System.out/dev/stdout -> /proc/self/fd/1 -> /dev/pts/0标准错误输出(stderr)22> 或 2>>System.err/dev/stderr -> /proc/self/fd/2 -> /dev/pts/0如上表所示,以下两条命令其实是等价的:echo "hello" > t.log 《===》 echo "hello" 1> t.log(2)关于 2>&1 的含义:含义:将标准错误输出重定向到标准输出符号 >& 是一个整体,不可分开,分开后就不是上述含义了。2>1 的写法其实是将标准错误输出重定向到名为"1"的文件里去了。2 和 > 之间不可以有空格,2> 是一体的时候才表示错误输出。写成 2&>1也是不可以的(3)为什么 2>&1 一定要写到 >log 后面,才表示标准错误输出和标准输出都定向到log中?一条shell命令如下:nohup java -jar app.jar >log 2>&1 & # 注:最后一个&表示把条命令放到后台执行 # 解析如下:(1 和 2 都理解是一个指针) 1.本来 1 -----> 屏幕 (1指向屏幕) 2.执行 >log 后, 1 -----> log (1指向log) 3.执行 2>&1 后, 2 -----> 1(2指向1,而1指向log,因此2也指向了log) # 命令结果:标准错误输出和标准输出都定向到log中另一条shell命令如下:nohup java -jar app.jar 2>&1 >log & # 解析如下: 1.本来 1 -----> 屏幕 (1指向屏幕) 2.执行 2>&1 后, 2 -----> 1 (2指向1,而1指向屏幕,因此2也指向了屏幕) 3.执行 >log 后, 1 -----> log (1指向log,2还是指向屏幕) # 命令结果不是我们想要的结果简单测试如下:// java代码如下: public class Htest { public static void main(String[] args) { System.out.println("out1"); System.err.println("error1"); // javac编译后运行下面指令: java Htest 2>&1 >log // 终端上只显示输出了"error1",log文件中则只有"out1"(4)>log 2>&1 的简写有以下两种简写方式:&>log | >&log # 第一种简写方式完整示例: nohup java -jar app.jar &>log &上面两种简写方式都和 >log 2>&1 一个语义,没有任何区别,但是第一种方式是最佳选择,一般使用第一种。seq:输出序列化的东西seq: squeue 是一个序列的缩写,主要用来输出序列化的东西# 用法: seq [选项]... 尾数 seq [选项]... 首数 尾数 # 或:以指定增量从首数开始打印数字到尾数 seq [选项]... 首数 增量 尾数 -s, --separator=字符串 # 使用指定字符串分隔数字(默认使用:\n) -w, --equal-width # 在列前添加0 使得宽度相同【自动补位】 -f, --format=格式 # 使用 printf 样式的浮点格式 # 格式:%3g:表示宽度为3,不足用0补足;%前面还可以指定字符串 --help # 显示此帮助信息并退出 --version # 显示版本信息并退出示例# 以空格作为分格,且输出单数 $ seq -s ' ' 10 1 2 3 4 5 6 7 8 9 10 # 默认补位操作 $ seq -w 8 10 # # 产生-2~10内的整数,增量为2 $ seq -2 2 2 # 使用指定格式。宽度为3,不足用0补足。 $ seq -f 'dir%03g' 1 3 dir001 dir002 dir003
系统管理systemctl:管理系统服务Centos7 之后从 init 完全换成了 systemd 的启动方式,systemd 启动服务的机制主要是通过 systemctl 的这个系统服务管理指令来处理。systemctl 在用法上也囊括 service / chkconfig / setup / init 的大部分功能。语法格式:systemctl [参数] [服务] # 常用参数: start 启动服务 stop 停止服务 restart 重启服务 enable 使某服务开机自启 disable 关闭某服务开机自启 status 查看服务状态 list -units --type=service 列举所有已启动服务参考实例:# 启动httpd服务: systemctl start httpd.service # 停止httpd服务: systemctl stop httpd # 重启httpd服务: systemctl restart httpd.service # 查看httpd服务状态: systemctl status httpd.service # 使httpd开机自启: systemctl enable httpd.service # 取消httpd开机自启: systemctl disable httpd.service # 列举所有已启动服务(unit单元) : systemctl list-units --type=serviceps:显示进程状态ps 命令是“process status”的缩写,ps命令用于显示当前系统的进程状态。可以搭配 kill 指令随时中断、删除不必要的程序。ps 命令是最基本同时也是非常强大的进程查看命令,使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。语法格式:ps [参数] # 常用参数: -A, -e # 显示所有进程 -p, p, --pid <PID> # 指定程序识别码,并列出该程序的状况 -u, U, --user <UID> # 列出属于该用户的进程的状况,也可使用用户名称来指定 -C <命令> # 列出指定命令的状况 -f # 显示完整格式的输出。显示UID,PPIP,C与STIME栏位 -aux # 显示所有进程,除了阶段作业领导者之外参考实例:# 查看所有进程 ps -ef # 查询指定进程 ps -ef | grep 进程名/进程号 # 杀死进程命令:kill kill -9 pid(进程号) # 搜索命令 grep :用于过滤搜索指定内容 # 格式:在指定文件中查找带有指定内容的信息 grep [参数] 指定内容 指定文件 -i # 不区分大小写的查找指定内容信息 ## 管道运算符 | :将多个命令串起来,处理模版输出的内容。常与grep组合使用,查所有文件(/进程/软件)的指定文件(/进程/软件)等 # 格式: 把命令1的输出作为命令2的输入 命令1 | 命令2 # 实例:当前目录下从所有的文件资源中查找1.txt文件资源 ll | grep 1.txt # 先把root目录下的所有资源查出来 | 在所有资源中搜索1.txt资源列出 PID 与相关信息各相关信息的意义为:F :代表这个程序的旗标 (flag), 4 代表使用者为 superuser;S :代表这个程序的状态 (STAT);UID :代表执行者身份PID :进程的ID号!PPID :父进程的ID;C :CPU使用的资源百分比PRI :指进程的执行优先权(Priority的简写),其值越小越早被执行;NI :这个进程的nice值,其表示进程可被执行的优先级的修正数值。ADDR :这个是内核函数,指出该程序在内存的那个部分。如果是个执行 的程序,一般就是『 - 』SZ :使用掉的内存大小;WCHAN :目前这个程序是否正在运作当中,若为 - 表示正在运作;TTY :登入者的终端机位置;TIME :使用掉的 CPU 时间。CMD :所下达的指令名称列出目前所有的正在内存当中的程序USER:该进程属于那个使用者账号。PID :该进程的进程ID号。%CPU:该进程使用掉的 CPU 资源百分比;%MEM:该进程所占用的物理内存百分比;VSZ :该进程使用掉的虚拟内存量 (Kbytes)RSS :该进程占用的固定的内存量 (Kbytes)TTY :该进程是在那个终端机上面运作,若与终端机无关,则显示 ?。另外, tty1-tty6 是本机上面的登入者程序,若为 pts/0 等等的,则表示为由网络连接进主机的程序。STAT:该程序目前的状态,主要的状态有:R :该程序目前正在运作,或者是可被运作;S :该程序目前正在睡眠当中,但可被某些讯号(signal) 唤醒。T :该程序目前正在侦测或者是停止了;Z :该程序应该已经终止,但是其父程序却无法正常的终止他,造成 zombie (疆尸) 程序的状态START:该进程被触发启动的时间;TIME :该进程实际使用 CPU 运作的时间。COMMAND:该程序的实际指令。kill:杀死进程kill 命令用来删除执行中的程序或工作。kill 命令可将指定的信号发送给相应的进程或工作。kill 命令默认使用信号为 15,用于结束进程或工作。如果进程或工作忽略此信号,则可以使用信号 9,强制杀死进程或作业。语法格式:kill [参数] [进程号] # 常用参数: -l 列出系统支持的信号 -s 指定向进程发送的信号 -a 处理当前进程时不限制命令名和进程号的对应关系 -p 指定kill命令只打印相关进程的进程号,而不发送任何信号参考实例:# 列出系统支持的信号列表: kill -l # 查找进程,并用kill杀掉 : PID TTY TIME CMD 1951 pts/0 00:00:00 bash 2446 pts/0 00:00:00 ps # 查看bash的进程ID为1951,然后用kill强制杀掉 kill -9 1951find:查找和搜索文件find 命令可以根据给定的路径和表达式查找的文件或目录。find 参数选项很多,并且支持正则,功能强大。和管道结合使用可以实现复杂的功能,是系统管理者和普通用户必须掌握的命令。find 如不加任何参数,表示查找当前路径下的所有文件和目录,如果服务器负载比较高尽量不要在高峰期使用 find 命令,find命令模糊搜索还是比较消耗系统资源的。语法格式:find [参数] [路径] [查找和搜索范围] # 常用参数: -name 按名称查找 -size 按大小查找 -user 按属性查找 -type 按类型查找 -iname 忽略大小写参考示例:# 列出当前目录及子目录下所有文件和文件夹: find . # 使用-name参数查看根目录下面所有的.conf结尾的配置文件: find / -name "*.conf" # 使用-size参数查看/etc目录下面大于1M的文件: find /etc -size +1M # 查找当前用户主目录下的所有文件: find $HOME -print # 在/home目录下查找以.txt结尾的文件名: find /home -name "*.txt" # 在/var/log目录下忽略大小写查找以.log结尾的文件名: find /var/log -iname "*.log" # 搜索超过七天内被访问过的所有文件: find . -type f -atime +7 # 搜索访问时间超过10分钟的所有文件: find . -type f -amin +10 # 找出/home下不是以.txt结尾的文件: find /home ! -name "*.txt"crontab:定时执行任务crontab 是英文“cron table”的简写。该命令被用来提交和管理用户的需要周期性执行的任务,与windows下的计划任务类似,当安装完成操作系统后,默认会安装此服务工具,并且会自动启动crond进程,crond进程每分钟会定期检查是否有要执行的任务,如果有要执行的任务,则自动执行该任务。Linux下的任务调度分为两类,系统任务调度和用户任务调度。系统任务调度:系统周期性所要执行的工作,比如写缓存数据到硬盘、日志清理等。/etc/crontab文件是系统任务调度的配置文件用户任务调度:用户定期要执行的任务,比如用户数据备份、定时邮件提醒等。用户可以使用 crontab 工具来定制自己的计划任务。/var/spool/cron/ 目录下存放的是每个用户包括root的crontab任务,其文件名与用户名一致。/etc/crontab 这个文件负责调度各种管理和维护任务。/etc/cron.d/ 这个目录用来存放任何要执行的crontab文件或脚本。还可以把脚本放在/etc/cron.hourly、/etc/cron.daily、/etc/cron.weekly、/etc/cron.monthly目录中,让它每小时/天/星期、月执行一次。语法格式:crontab [参数] # 常用参数: -e 编辑该用户的计时器设置 -l 列出该用户的计时器设置 -r 删除该用户的计时器设置 -u 指定要设定计时器的用户名称参考实例:# 查看当前定时任务: crontab -l # 创建、编辑定时任务: crontab -e # 删除本用户所有的定时任务: crontab -r 使用 crond 服务设置任务的参数格式:minute hour day month week command 分 时 日 月 周 命令 # 参数字段说明: minute 表示分钟,是从0到59之间的任何整数 hour 表示小时,是从0到23之间的任何整数 day 表示日期,是从1到31之间的任何整数 month 表示月份,是从1到12之间的任何整数 week 表示星期,是从0到7之间的任何整数,其中0或7代表星期日 command 要执行的命令,可以是系统命令,也可以是自己编写的脚本文件 # 时间的操作符有 * # 取值范围内的所有数字 / # 每隔n单位时间。例如:*/10 表示 每10分钟 - # 一个时间范围段。例如:8-10点 表示 8到10点 , # 分隔时段,散列数字。例如:6,0,1 表示 周六,周天,周一 # 注意事项:注意事项:在 crontab 命令中只有 “绝对路径”,不存在相对路径,故执行任何命令都需要写绝对路径如果有些时间字段没有设置,则需要使用星号(*)占位如果使用cron运行脚本,请将脚本执行的结果写入指定日志文件, 观察日志内容是否正常。命令或脚本使用bash命令,防止脚本没有增加执行权限(/usr/bin/bash)定时任务实例:# 每小时的第5分钟执行 ls 命令 5 * * * * ls # 每5分钟执行 ls 命令 */5 * * * * ls # 每天的 4:30 执行 ls 命令 30 4 * * * ls # 每小时执行 ls 命令 0 * * * * ls # 每天执行 ls 命令 0 0 * * * ls # 每周执行 ls 命令 0 0 * * 0 ls # 每年执行 ls 命令 0 0 1 1 * ls # 每月 8号 的 7:20 执行 ls 命令 20 7 8 * * ls # 每年的 6月28号 5:30 执行 ls 命令 30 5 28 6 * ls # 每星期日的 6:30 执行 ls 命令 30 6 * * 0 ls # 每月 10号和20号 的 4:30 执行 ls 命令 30 4 10,20 * * ls # 每天 8~11点 的第 25 分钟执行 ls 命令 25 8-11 * * * ls # 每个月中每隔 10天 的 5:30 执行 ls 命令。即:每月的 1、11、21、31日 在 5:30 执行一次 ls 命令 30 5 */10 * * lsuname:显示系统信息uname 命令的英文全称即“Unix name”。用于显示系统相关信息,比如主机名、内核版本号、硬件架构等。如果未指定任何选项,其效果相当于执行”uname -s”命令,即显示系统内核的名字。语法格式:uname [参数] # 常用参数: -a 显示系统所有相关信息 -m 显示计算机硬件架构 -n 显示主机名称 -r 显示内核发行版本号 -s 显示内核名称 -v 显示内核版本 -p 显示主机处理器类型 -o 显示操作系统名称 -i 显示硬件平台参考实例:# 显示系统主机名、内核版本号、CPU类型等信息: $ uname -a Linux linuxcool 3.10.0-123.el7.x86_64 #1 SMP Mon May 5 11:16:57 EDT 2014 x86_64 x86_64 x86_64 GNU/Linux # 仅显示系统主机名: $ uname -n linuxcool # 显示当前系统的内核版本 : $ uname -r 3.10.0-123.el7.x86_64 # 显示当前系统的硬件架构: $ uname -i x86_64sudo:以系统管理者的身份执行指令sudo 是一种权限管理机制,管理员可以授权于一些普通用户去执行一些 root 执行的操作,而不需要知道 root 的密码。sudo 允许一个已授权用户以超级用户或者其它用户的角色运行一个命令。当然,能做什么不能做什么都是通过安全策略来指定的。sudo 支持插件架构的安全策略,并能把输入输出写入日志。第三方可以开发并发布自己的安全策略和输入输出日志插件,并让它们无缝的和 sudo 一起工作。默认的安全策略记录在 /etc/sudoers 文件中。而安全策略可能需要用户通过密码来验证他们自己。也就是在用户执行 sudo 命令时要求用户输入自己账号的密码。如果验证失败,sudo 命令将会退出。语法格式:sudo [参数] # 常用参数: -v 因为 sudo 在第一次执行时或是在 N分钟内没有执行(N 预设为五)会问密码,这个参数是重新做一次确认,如果超过N分钟,也会问密码 -k 强迫使用者在下一次执行 sudo 时问密码(不论有没有超过 N 分钟) -b 将要执行的指令放在背景执行 -p prompt 可以更改问密码的提示语,其中 %u 会代换为使用者的帐号名称,%h 会显示主机名称 -s 执行环境变数中的SHELL 所指定的shell ,或是 /etc/passwd 里所指定的 shell command 要以系统管理者身份(或以 -u 更改为其他人)执行的指令参考实例:# 切换到root用户: sudo su # 指定用户执行命令: sudo -u userb ls -l # 以root权限执行上一条命令: sudo !! # 列出目前的权限: sudo -l # 列出 sudo 的版本资讯: sudo -Vdate:显示日期与时间date 命令可以用来显示或设定系统的日期与时间,在显示方面,可以设定欲显示的格式,格式设定为一个加号后接数个标记。若是不以加号作为开头,则表示要设定时间,而时间格式 MMDDhhmm[[CC]YY][.ss],其中 MM 为月份,DD 为日,hh 为小时,mm 为分钟,CC 为年份前两位数字,YY 为年份后两位数字,ss 为秒数。语法格式:date [选项] [+输出形式] # 常用参数: -d datestr 显示 datestr 中所设定的时间 (非系统时间) -s datestr 将系统时间设为 datestr 中所设定的时间 -u 显示目前的格林威治时间参考实例:# 显示当前时间: $ date 三 4月 12 14:08:12 CST 2019 $ date '+%c' 2019年04月17日 星期三 14时09分02秒 # 按自己的格式输出: $ date '+usr_time: $1:%M %P -hey' usr_time: $1:16 下午 -hey # 显示时间后跳行,再显示目前日期: $ date '+%T%n%D' # 只显示月份与日数: $ date '+%B %d' # 显示日期与设定时间(12:34:56): $ date --date '12:34:56'hostnamectl:修改主机名称hostnamectl 可用于查询和更改系统主机名和相关设置。此工具区分三种不同的主机名:高级“漂亮”主机名,其中可能包括特殊字符(例如 “lennart’s laptop”)静态主机名,用于在引导时初始化内核主机名(例如 “lennarts膝上型电脑”)默认瞬时主机名(从网络配置接收到的)如果是静态的主机名已设置且有效(不是 localhost),则不使用临时主机名语法格式:hostnamectl [参数] # 常用参数: -H 操作远程主机 status 显示当前主机名设置 set-hostname 设置系统主机名参考实例:# 显示当前主机名称的配置信息: hostnamectl status # 使用set-hostname命令来设置或修改主机名称: hostnamectl set-hostname linuxprobesource:在当前Shell环境中从指定文件读取和执行命令source 命令(从 C Shell 而来)是 bash shell 的内置命令。点命令(就是个点符号,从 Bourne Shell 而来)是 source 的另一名称。source 命令通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录。source 返回文件最后一个命令的返回值,如果文件不能读取则会失败。语法格式:source [文件]参考实例:# 读取和执行/root/.bash_profile文件: source ~/.bash_profile # 执行刚修改的初始化文件,使之立即生效: source /etc/bash_profile # 在一些工具的执行过程中,会把环境变量设置以”export XXX=XXXXXX”或”declare XXX=XXXXXX”的形式导出到 一个文件中,然后用source加载该文件内容到执行环境中: [root@linuxcool ~]# vi /etc/profile [root@linuxcool ~]# source /etc/profile如果把一些命令做成一个文件,让它自动顺序执行,对于需要多次反复编译系统核心的用户来说会很方便,而用 source 命令就可以做到这一点,它的作用就是把一个文件的内容当成 shell 来执行。先在 linux 的源代码目录下(如/usr/src/linux-2.4.20)建立一个文件,如 make_command,在其中输入一下内容:make mrproper && make menuconfig && make dep && make clean && make bzImage && make modules && make modules_install && cp arch/i386/boot/bzImage /boot/vmlinuz_new && cp System.map /boot && vi /etc/lilo.conf && lilo -v文件建立好之后,每次编译核心的时候,只需要在 /usr/src/linux-2.4.20 下输入:source make_commandulimit:设置用户可使用的资源ulimit :设置用户可使用的资源,如 CPU、内存、句柄等(临时修改,重启后失效)。用法:ulimit [参数] [限制]参数详解:-a :列出系统所有资源限制的值-S:设置软限制,超出设定的值会告警-H :设置硬限制,超出设定的值会报错-c:限制每个核心文件的最大容量,单位为 blocks核心文件(core file):当某些程序发生错误时,系统可能会将该程序在内存中的信息写成文件(核心文件)-d:进程数据段的最大值,单位为 Kbytes-f:当前 shell 进程可创建的最大文件容量,单位为 blocks-l:最大可加锁的物理内存大小,单位为 Kbytes-m:可以使用的最大常驻内存大小,单位为 Kbytes-n:每个进程可以同时打开的最大文件句柄数-p:管道缓冲区的最大值,单位为 Kbytes-s:线程栈的最大值,单位为 Kbytes-t:进程可以使用(占用) CPU 的最大时间,单位为秒-u:用户可运行的最大进程并发数-v:当前 shell 进程可使用的最大虚拟内存,单位为 Kbytes使用示例在命令 [限制] 处,设置值,即可调整限制值,只对当前 shell 有效S表示软限制;H表示硬限制;如果不指明,则表示软硬皆设置;[root@localhost solr-7.7.3]# ulimit -u [root@localhost solr-7.7.3]# ulimit -u 65535 [root@localhost solr-7.7.3]# ulimit -u 65535永久生效 ulimit修改 /etc/security/limits.conf 文件,更新或新增内容如下(示例):[root@localhost solr-7.7.3]# vim /etc/security/limits.conf * soft nofile 65536 * hard nofile 65536 * soft nproc 65536 * hard nproc 65536/etc/security/limits.conf 配置详解格式:<domain> <type> <item> <value>参数详解:domain 是指生效实体用户名也可以通过 @group 指定用户组使用 * 表示默认值type 指限制类型soft 软限制hard 硬限制item 限制资源core :同 ulimit -cdata :同 ulimit -dfsize :同 ulimit -fmemloc :同 ulimit -lnofile :同 ulimit -nstack :同 ulimit -scpu :同 ulimit -tnproc :同 ulimit -usigpengding :同 ulimit -imsgqueue :同 ulimit -qmaxlogins:指定用户可以同时登陆的数量maxsyslogins :系统可以同时登陆的用户数priority :用户进程运行的优先级locks :用户可以锁定的文件最大值防火墙常用命令参考:Linux查看、开启、关闭防火墙操作防火墙的区别:CentOS6 自带的防火墙是 iptables,CentOS7自带的防火墙是 firewall。iptables:用于过滤数据包,属于网络层防火墙。firewall:底层还是使用 iptables 对内核命令动态通信包过滤的,简单理解就是 firewall 是 centos7 下管理 iptables 的新命令。iptables 防火墙安装 iptables 防火墙:# 通过 yum install 命令联网下载安装iptables yum install iptables-services 常用命令:# 查看防火墙状态 service iptables status | systemctl status iptables # 启动防火墙 service iptables start | systemctl start iptables # 重启防火墙 service iptables restart | systemctl restart iptables # 停止防火墙 service iptables stop | systemctl stop iptables # 将iptables设置为开机启动 systemctl enable iptables.service # 永久关闭防火墙 chkconfig iptables off # 永久关闭后重启 chkconfig iptables on开启 80 端口# 编辑iptales vim /etc/sysconfig/iptables # 加入以下代码然后保存退出 -A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT #重启防火墙 service iptables restart查看防火墙策略、开放的端口iptables -nL 或 /sbin/iptables -L -nfirewall 防火墙查看 firewall 服务状态systemctl status firewalld # 出现 Active: active (running)绿色高亮显示则表示是启动状态。 # 出现 Active: inactive (dead)灰色表示停止状态。查看 firewall 的状态firewall-cmd --state开启、重启、关闭firewall服务# 开启 service firewalld start service firewalld restart service firewalld stop查看防火墙规则firewall-cmd --list-all查看、开放、关闭端口# 查询端口是否开放 firewall-cmd --query-port=8080/tcp # 开放80端口 firewall-cmd --permanent --add-port=80/tcp # 移除端口 firewall-cmd --permanent --remove-port=8080/tcp #重启防火墙(修改配置后要重启防火墙) firewall-cmd --reloadfirewall其他命令# 查看防火墙状态,是否是running firewall-cmd --state # 重新载入配置,比如添加规则之后,需要执行此命令 firewall-cmd --reload # 列出支持的zone firewall-cmd --get-zones # 列出支持的服务,在列表中的服务是放行的 firewall-cmd --get-services # 查看ftp服务是否支持,返回yes或者no firewall-cmd --query-service ftp # 临时开放ftp服务 firewall-cmd --add-service=ftp # 永久开放ftp服务 firewall-cmd --add-service=ftp --permanent # 永久移除ftp服务 firewall-cmd --remove-service=ftp --permanent # 永久添加80端口 firewall-cmd --add-port=80/tcp --permanent # 查看规则,这个命令和iptables的相同 iptables -L -n # 查看帮助 man firewall-cmd查看 cpu、内存环境信息2C2G,4C4G,8C16G,16C32G这里 C 指 cpu 物理核数,G 指总内存大小# 查看物理CPU个数 cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l # 查看每个物理CPU中core的个数(即核数) cat /proc/cpuinfo| grep "cpu cores"| uniq # 查看逻辑CPU的个数 cat /proc/cpuinfo| grep "processor"| wc -l # C=CPU总核数 = 物理CPU个数 X 每颗物理CPU的核数 # 总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数 # 查看CPU信息(型号) cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c # 查看内存信息。MemTotal键值即为内存大小,单位kb cat /proc/meminfo查看内存使用信息# 以合适的单位显示内存使用情况 free -h Mem # (第二行)是内存的使用情况。 Swap # (第三行)是交换空间的使用情况。 total # 列显示系统总的可用物理内存和交换空间大小。 used # 列显示已经被使用的物理内存和交换空间。 free # 列显示还有多少物理内存和交换空间可用使用。 shared # 列显示被共享使用的物理内存大小。 buff/cache # 列显示被 buffer 和 cache 使用的物理内存大小。 available # 列显示还可以被应用程序使用的物理内存大小。 # 释放缓存 # 在清理缓存先要先把buffe中的数据先写入到硬盘中 # 手动释放内存的命令 echo 1 > /proc/sys/vm/drop_caches drop_caches的值可以是0-3之间的数字,代表不同的含义: 0:不释放(系统默认值)。改为其他值后,只能系统重启恢复为0,无法手动改为0 1:释放页缓存 2:释放dentries和inodes 3:释放所有缓存top:实时显示系统中各个进程的资源占用状况参考:Linux top命令详解查看 GPU 使用信息参考:【GPU】nvidia-smi命令Nvidia 显卡自带一个命令行工具可以查看显存的使用情况:# 查看显存的使用情况快照 nvidia-smi # 周期性(10s)的输出显卡的使用情况 watch -n 10 nvidia-smi表格参数详解:GPU:本机中的GPU编号(有多块显卡的时候,从0开始编号)图上GPU的编号是:0Fan:风扇转速(0%-100%),N/A表示没有风扇Name:GPU类型,图上GPU的类型是:Tesla T4Temp:GPU的温度(GPU温度过高会导致GPU的频率下降)Perf:GPU的性能状态,从P0(最大性能)到P12(最小性能),图上是:P0Persistence-M:持续模式的状态,持续模式虽然耗能大,但是在新的GPU应用启动时花费的时间更少,图上显示的是:offPwr:Usager/Cap:能耗表示,Usage:用了多少,Cap总共多少Bus-Id:GPU总线相关显示,domain:bus:device.functionDisp.A:Display Active ,表示GPU的显示是否初始化Memory-Usage:显存使用率Volatile GPU-Util:GPU使用率Uncorr. ECC:关于ECC的东西,是否开启错误检查和纠正技术,0/disabled,1/enabledCompute M:计算模式,0/DEFAULT,1/EXCLUSIVE_PROCESS,2/PROHIBITEDProcesses:显示每个进程占用的显存使用率、进程号、占用的哪个GPU用户管理passwd:修改用户账户密码passwd 命令用于设置用户的认证信息,包括用户密码、账户锁定、密码失效等。直接运行 passwd 命令修改当前的用户密码,对其他用户的密码操作需要管理员权限。常用格式:passwd [参数] 用户名 # 常用参数: -d 删除密码,使账号无口令 -l 锁定用户密码,无法被用户自行修改。仅 root 用户可用 -u 解开已锁定用户密码,允许用户自行修改。仅 root 用户可用 -e 密码立即过期,下次登陆强制修改密码 -k 保留即将过期的用户在期满后能仍能使用 -S 查询密码状态。仅 root 用户可用 --stdin 可以将通过管道符输出的数据作为用户的密码。主要在批量添加用户时使用参考实例:# 修改当前登陆的账户密码: passwd # 修改其他用户密码(假设有linuxcool用户): passwd linuxcool # 锁定密码不允许用户修改: passwd -l linuxcool # 解除锁定密码,允许用户修改: passwd -u linuxcool # 下次登陆强制改密码: passwd -e linuxcool # 清除登录密码。清除之后登录时无需密码,风险极大,不推荐使用: passwd -d linuxcool # 查询密码状态: passwd -S linuxcool查看用户列表和用户组# 查看所有用户的列表 cat /etc/passwd # 说明: # /etc/passwd 中一行记录对应着一个用户,每行记录又被冒号(:)分隔为7个字段,其格式和具体含义如下: # 用户名:口令:用户标识号:组标识号:注释性描述:主目录:登录Shell # 查看用户组 cat /etc/group # 删除用户组 groupdel 用户组 # 查看当前登录用户的用户组 groups # 查看当前登录用户名 whoami创建/删除/修改/切换用户useradd:创建用户useradd 命令用来创建新的用户或更改用户的信息。useradd 可用来建立用户帐号。帐号建好之后,再用 passwd 设定帐号的密码。使用 useradd 指令所建立的帐号,实际上是保存在 /etc/passwd 文本文件中。语法格式:useradd [参数] [用户名] # 常用参数: -u, -uid UID 指定用户id -g, -gid GROUP 指定用户对应的用户组名称或ID -d, -home-dir HOME_DIR 新用户每次登陆时所使用的家目录 -m 用户目录不存在时则自动创建 -M 不建立用户家目录,优先于/etc/login.defs文件设定 -D 改变新建用户的预设值 -c 添加备注文字 -e 用户终止日期,日期的格式为YYYY-MM-DD -f 用户过期几日后永久停权。当值为0时用户立即被停权,而值为-1时则关闭此功能,预设值为-1 -G 定义此用户为多个不同组的成员 -n 取消建立以用户名称为名的群组 -r, -system 建立系统帐号 -p, -password PASSWORD 新帐户的加密密码 -l, -no-log-init 不要将用户添加到lastlog和faillog数据库 -s, -shell SHELL 新帐户的登录shell -o, -non-unique 允许创建具有重复(非唯一)UID的用户 -U, -user-group 创建与用户同名的组参考实例:# 添加新用户linuxcool: useradd linuxcool # 不创建家目录,并且禁止登陆: useradd -M -s /sbin/nologin linuxcool # 添加新用户linuxcool,指定UID为888,指定归属用户组为root,cool成员,其shell类型为/bin/sh: useradd -u 888 -s /bin/sh -G root,cool linuxcool # 添加新用户linuxcool,设置家目录为/home/linuxcool,用户过期时间为2019/05/01.过期后两天停权: useradd -e "2019/05/01" -f 2 -d /home/linuxcool linuxcooluserdel:删除用户userdel 命令用于删除指定的用户及与该用户相关的文件,英文全称即“user delete”。其实 userdel 命令实际上是修改了系统的用户账号文件 /etc/passwd、/etc/shadow以及/etc/group文件。这与Linux系统”一切操作皆文件”的思想正好吻合。值得注意的是,但是如果有该要删除用户相关的进程正在运行,userdel 命令通常不会删除一个用户账号。如果确实必须要删除,可以先终止用户进程,然后再执行userdel命令进行删除。但是 userdel 命令也提供了一个面对该种情况的参数,即”-f”选项。语法格式:userdel [参数] [用户名] # 常用参数: -f 强制删除用户账号 -r 删除用户主目录及其中的任何文件参考实例:# 删除用户,但不删除其家目录及文件: userdel linuxcool # 删除用户,并将其家目录及文件一并删除: userdel -r linuxcool # 强制删除用户: userdel -f linuxcoolusermod:修改用户属性语法格式:usermod [参数] [用户名] # 常用参数: -L 锁定用户密码,使密码无效。 -s 修改用户登入后所使用的shell -u 修改用户ID。 -U 解除密码锁定。su:切换用户语法格式:su USER_NAME创建/删除工作组groupadd:新建工作组groupadd 命令用于创建一个新的工作组,新工作组的信息将被添加到系统文件中。语法格式:groupadd [参数] 常用参数: -g 指定新建工作组的id -r 创建系统工作组,系统工作组的组ID小于500 -K 覆盖配置文件“/ect/login.defs” -o 允许添加组ID号不唯一的工作组参考实例:# 使用-g参数新建linuxcool工作组名,1005是工作组id: groupadd -g 1005 linuxcool # 使用-r创建系统工作组: groupadd -r -g 368 linuxcoolgroupdel:删除用户组groupdel命令用于删除指定的工作组,本命令要修改的系统文件包括/ect/group和/ect/gshadow。userdel修改系统账户文件,删除与 GROUP 相关的所有项目。给出的组名必须存在。若该群组中仍包括某些用户,则必须先删除这些用户后,方能删除群组。语法格式:groupdel [参数] [群组名称] # 常用参数: -h 显示帮助信息 -R 在chroot_dir目录中应用更改并使用chroot_dir目录中的配置文件参考实例:# 使用groupdel命令删除linuxcool工作组: groupdel linuxcool # 查看linuxcool组是否删除成功。通过查看/etc/group配置文件里面不存在linuxcool组,说明已经被删除了。 more /etc/group | grep linuxcool将用户加入到用户组# 将指定用户加入到用户组 gpasswd -a USER_NAME GROUP_NAME # 将当前用户加入到用户组 gpasswd -a $USER GROUP_NAME # 更新用户组 newgrp GROUP_NAME磁盘管理df:显示磁盘空间使用情况df 命令的英文全称即“Disk Free”,是用于显示系统上可使用的磁盘空间。默认显示单位为KB,建议使用“df -h”的参数组合,根据磁盘容量自动变换合适的单位,更利于阅读。日常普遍用该命令可以查看磁盘被占用了多少空间、还剩多少空间等信息。语法格式: df [参数] [指定文件] # 常用参数: -a 显示所有系统文件 -h 以容易阅读的方式显示。根据磁盘容量自动变换合适的单位,更利于阅读。 -H 以1000字节为换算单位来显示 -i 显示索引字节信息 -l 只显示本地文件系统 -t <文件系统类型> 只显示指定类型的文件系统 -T 输出时显示文件系统类型 -B <块大小> 指定显示时的块大小 -k 指定块大小为1KB -sync 在取得磁盘使用信息前,先执行sync命令参考实例:# 以容易阅读的方式显示磁盘分区使用情况: [root@linuxcool ~]$ df -h 文件系统 容量 已用 可用 已用% 挂载点 devtmpfs 1.9G 0 1.9G 0% /dev tmpfs 2.0G 0 2.0G 0% /dev/shm tmpfs 2.0G 1.1M 2.0G 1% /run tmpfs 2.0G 0 2.0G 0% /sys/fs/cgroup /dev/mapper/fedora_linuxhell-root 15G 2.0G 14G 13% / tmpfs 2.0G 4.0K 2.0G 1% /tmp /dev/sda1 976M 126M 784M 14% /boot tmpfs 390M 0 390M 0% /run/user/0 # 显示指定文件所在分区的磁盘使用情况: [root@linuxcool ~]$ df /etc/dhcp 文件系统 1K-块 已用 可用 已用% 挂载点 /dev/mapper/fedora_linuxcool-root 15718400 2040836 13677564 13% / # 显示文件类型为ext4的磁盘使用情况: [root@linuxcool ~]$ df -t ext4 文件系统 1K-块 已用 可用 已用% 挂载点 /dev/sda1 999320 128264 802244 14% /bootlsblk:查看系统的磁盘lsblk 命令的英文是“list block”,即用于列出所有可用块设备的信息,而且还能显示它们之间的依赖关系,但是它不会列出 RAM 盘的信息。lsblk 命令包含在 util-linux-ng 包中,现在该包改名为 util-linux。语法格式:lsblk [参数] # 常用参数: -a 显示所有设备 -b 以bytes方式显示设备大小 -d 不显示 slaves 或 holders -D 显示弃置的性能 -e 排除设备 -f 显示文件系统信息 -h 显示帮助信息 -i 只使用ASCII字符 -m 显示权限信息 -l 使用列表格式显示 -n 不显示标题 -o 输出列 -P 使用key=”value”格式显示 -r 使用原始格式显示 -t 显示拓扑结构信息参考实例:# lsblk命令默认情况下将以树状列出所有块设备: [root@linuxcool ~ ]$ lsblk lsblk NAME MAJ:MIN rm SIZE RO type mountpoint sda 8:0 0 232.9G 0 disk ├─sda1 8:1 0 46.6G 0 part / ├─sda2 8:2 0 1K 0 part ├─sda5 8:5 0 190M 0 part /boot ├─sda6 8:6 0 3.7G 0 part [SWAP] ├─sda7 8:7 0 93.1G 0 part /data └─sda8 8:8 0 89.2G 0 part /personal sr0 11:0 1 1024M 0 rom # 默认选项不会列出所有空设备: [root@linuxcool ~]$ lsblk -a # 也可以用于列出一个特定设备的拥有关系,同时也可以列出组和模式: [root@linuxcool ~]$ lsblk -m # 要获取SCSI设备的列表,你只能使用-S选项,该选项是用来以颠倒的顺序打印依赖的: [root@linuxcool ~]$ lsblk -S # 例如,你也许想要以列表格式列出设备,而不是默认的树状格式。可以将两个不同的选项组合,以获得期望的输出: [root@linuxcool ~]$ lsblk -nl定位较大文件目录并清理硬盘参考:LINUX下查找大文件及大的文件夹# 搜索根目录下超过指定大小的文件(按大小降序排序,单位MB) find / -type f -size +500M -print0 | xargs -0 du -hm | sort -gr # (进入根目录 / )循环定位最大文件目录: du -h --max-depth=1设备管理mount:文件系统挂载mount 命令用于加载文件系统到指定的加载点。此命令的最常用于挂载 cdrom,使可以访问cdrom中的数据,因为将光盘插入cdrom 中,Linux 并不会自动挂载,必须使用 Linux mount 命令来手动完成挂载。语法格式:mount [参数] 常用参数: -t 指定挂载类型 -l 显示已加载的文件系统列表 -a 加载文件“/etc/fstab”中描述的所有文件系统 -n 加载没有写入文件“/etc/mtab”中的文件系统 -r 将文件系统加载为只读模式 -V 显示程序版本 -h 显示帮助信息并退出参考实例:# 显示已加载的文件系统列表 mount -l # 启动所有挂载: mount -a # 挂载 /dev/cdrom 到 /mnt: mount /dev/cdrom /mnt # 挂载nfs格式文件系统: mount /123 /mnt -t nfs # 挂载第一块盘的第一个分区到/etc目录 : mount /dev/sda1 /etc -t ext4 -o loop,default # 判断路径是否已挂载 mountpoint -q file_path # 取消挂载 umount /mynfs
[root@node00 ~]# hostname node00命令提示符的组成由PS1环境变量控制# 默认的PS1设置 echo $PS1 # 结果为:[\u@\h \W]\$ # 可以通过修改/etc/bashrc文件的内容来设置提示信息,支持的配置如下: PS1变量 含义 \d # 代表日期,格式为weekday month day,例如"Mon Aug 1" \H # 完整的主机名 \h # 仅取主机的第一个名字 \t # 显示24小时制的时间,格式为HH:mm:ss \T # 显示12小时制的时间,格式为hh:mm:ss \A # 显示24小时制的时间,格式为HH:mm \u # 当前用户的名称 \v # BASH版本信息 \w # 显示完整的路径,家目录用"~"代替 \W # 利用basename获取工作目录名称,只会列出最后一个目录 \# # 执行的第几个命令 \$ # 提示字符,如果是root,则提示符为#,普通用户为$ ## 临时设置PS1变量 PS1='[\u@\h \w]\$ ' cd /etc/sysconfig/network-scripts/ # \w控制的显示全部路径已经生效了:[root@node00 /etc/sysconfig/network-scripts]# ## 永久设置PS1变量 vi /etc/bashrc # 这是文件中默认的内容 [ "$PS1" = "\\s-\\v\\\$ " ] && PS1="[\u@\h \W]\\$ " # 修改为你自己喜欢的格式,一个美观的PS1变量的格式如下 PS1="[\[\[\e[34;1m\]\t \[\[\e[34;1m\]\u@\[\e[0m\]\[\e[32;1m\]\h\[\e[0m\]\[\e[31;1m\] \W\[\e[0m\]]\\$ " # 保存退出之后 source /etc/bashrc命令格式# command [arg] [path] # 命令 可选的参数选项(可选) 文件或目录(可选) # 示例: rm -rf /tmp/test.txt # 命令和参数、参数和要操作的文件或目录之间必须有至少一个空格linux的目录结构/ # 根目录(只有一个根盘符--类似就是一个C盘符) /bin # Binaries (二进制文件) 的缩写, 这个目录存放着最经常使用的命令。这个目录已经包含在“path”系统变量里面 /boot # 这里存放的是启动 Linux 时使用的一些核心文件,包括一些连接文件以及镜像文件。 /dev # Device(设备)的缩写, 该目录下存放的是 Linux 的外部设备,在 Linux 中访问设备的方式和访问文件的方式是相同的。 /etc # Etcetera(等等) 的缩写,这个目录用来存放所有的系统管理所需要的配置文件和子目录。 /etc/profile # 存储系统环境变量的文件 /etc/passwd # 存储所有用户列表的文件 /home # 用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的。 /lib # Library(库) 的缩写。这个目录里存放着系统最基本的动态连接共享库和内核模块,其作用类似于 Windows 里的 DLL 文件。 几乎所有的应用程序都需要用到这些共享库。 /lost+found # 这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。 /media # linux 系统会自动识别一些设备,例如U盘、光驱等等,当识别后,Linux 会把识别的设备挂载到这个目录下。 /mnt # 系统提供该目录是为了让用户临时挂载别的文件系统的。将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了。 /opt # optional(可选) 的缩写,这是给主机额外安装软件所摆放的目录。比如安装一个ORACLE数据库则就可以放到这个目录下。 默认是空的。安装到/opt目录下的程序,它所有的数据、库文件等等都是放在同个目录下面。 /proc # Processes(进程) 的缩写,/proc 是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件。 这个目录是一个虚拟的目录,它是系统内存的映射,可以通过直接访问这个目录来获取系统信息。 这个目录的内容不在硬盘上而是在内存里,可以直接修改里面的某些文件,比如可以通过下面的命令来屏蔽主机的ping命令, 使别人无法ping你的机器:echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all /root # 该目录为系统管理员,也称作超级权限者的用户主目录。 /sbin # Superuser Binaries (超级用户的二进制文件) 的缩写,这里存放的是系统管理员使用的系统管理程序。 /selinux # 这个目录是 Redhat/CentOS 所特有的目录,Selinux 是一个安全机制,类似于 windows 的防火墙, 但是这套机制比较复杂,这个目录就是存放selinux相关的文件的。 /srv # 该目录存放一些服务启动之后需要提取的数据。 /sys # 这是 Linux2.6 内核的一个很大的变化。该目录下安装了 2.6 内核中新出现的一个文件系统 sysfs 。 # sysfs 文件系统集成了下面3种文件系统的信息: 针对进程信息的 proc 文件系统、针对设备的 devfs 文件系统以及针对伪终端的 devpts 文件系统。 该文件系统是内核设备树的一个直观反映。当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。 /tmp # temporary(临时) 的缩写这个目录是用来存放一些临时文件的。 linux系统会定期自动对这个目录进行清理,因此,千万不要把重要的数据放在这里。 /usr # unix shared resources(共享资源) 的缩写,这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下, 类似于 windows 下的 program files 目录。 /usr/local # 存放用户手动安装的软件 /usr/bin # 系统用户使用的应用程序。 /usr/share # 用于存放一些共享的数据,比如音乐文件或者图标等等 /usr/share/fonts # 是字体目录 /usr/sbin # 超级用户使用的比较高级的管理程序和系统守护程序。 /usr/src # 内核源代码默认的放置目录。 /usr/lib # 目录用于存放那些不能直接 运行的,但却是许多程序运行所必需的一些函数库文件 /var # variable(变量) 的缩写,这个目录中存放着在不断扩充着的东西,习惯将那些经常被修改的目录放在这个目录下。 包括各种日志文件。 /var/log # 系统日志存放,分析日志要看这个目录的东西 /run # 是一个临时文件系统,存储系统启动以来的信息。当系统重启时,这个目录下的文件应该被删掉或清除。 如果你的系统上有 /var/run 目录,应该让它指向 run。 ### 在 Linux 系统中,有几个目录是比较重要的,平时需要注意不要误删除或者随意更改内部文件。 /etc # 这个是系统中的配置文件,如果你更改了该目录下的某个文件可能会导致系统不能启动。 /bin, /sbin, /usr/bin, /usr/sbin # 这是系统预设的执行文件的放置目录,比如 ls 就是在 /bin/ls 目录下的。 # /bin, /usr/bin 是给系统用户使用的指令(除root外的通用户),而/sbin, /usr/sbin 则是给 root 使用的指令。 /var # 这是一个非常重要的目录,系统上跑了很多程序,那么每个程序都会有相应的日志产生,而这些日志就被记录到这个目录下,具体在 /var/log 目录下,另外 mail 的预设放置也是在这里。命令行常用快捷键参考:Linux 命令行快捷键# 控制命令 Ctrl + C :停止当前进程并返回提示符 # 常用 Ctrl + L :进行清屏操作。等同于命令(clear) Ctrl + Z :暂停执行在终端运行任务 # 移动光标 Ctrl + A :跳到行的开头 Ctrl + E :跳到行尾 # 常用 Ctrl + B :往回(左)移动一个字符(相当于左键) Ctrl + F :往后(右)移动一个字符(相当于右键) Ctrl + XX :(两个X)在行的开头和光标之间移动 Alt + B :往回(左)移动一个单词 Alt + F :往后(右)移动一个单词 # 编辑命令 Ctrl + H :删除一个光标左方位置的字符 Ctrl + W :由光标位置开始,往左删除单词。往行首删 # 常用 Alt + D :由光标位置开始,往右删除单词。往行尾删 Ctrl + U :删除当前光标前面的所有文字(还有剪切功能) # 常用 Ctrl + K :删除当前光标后面的所有文字(还有剪切功能) Ctrl + Y :粘贴Ctrl + U或Ctrl + K剪切的内容到光标后 Ctrl + _ :回复之前的状态。撤销操作 # 常用 Ctrl + A + K 或 Ctrl + E + U 或 Ctrl + K + U :组合可删除整行 # 常用 Tab :自动补全内容 # 常用 Ctrl + r :搜索执行过的命令 Ctrl + g :从搜索历史命令的UI中退出 Esc + . :获取上一条命令使用空格分隔后的最后一部分 Ctrl + l :清除屏幕内容 !! :执行上一条命令 !pw :执行最近以pw开头的命令 !pw:p :打印最近以pw开头的命令,但不执行Z !num :执行历史命令列表中第num条命令 Ctrl + R :搜索历史命令,随着输入会显示历史命令中的一条匹配命令,Enter键执行匹配命令;ESC键在命令行显示而不执行匹配命令终端/客户端:登出 关机 重启终端每次打开一个命令行窗口,都是开启一个新的终端,包括远程连接的窗口,使用 tty 命令来查看当前窗口的终端# 查看当前窗口的终端的命令 tty # 输出为:/dev/pts/0 # 终端与终端的通信 /dev/pts/0终端发出通信信息:echo hello > /dev/pts/1 /dev/pts/1终端收到通信信息:hello # 使用wall命令手动广播消息 wall 'hello everybody'退出登录仅退出当前用户(终端)的登录状态,并不关机命令:exit命令:logout快捷键:crtl + d关机重启主机命令 halt :立即关机命令 poweroff :立即关机命令 reboot :立即重启命令 initinit 0 # 立即关机 init 6 # 立即重启shutdown:可以安全的关闭或者重启系统,并广播关机或者重启的消息给全部的终端# 默认在一分钟之后关机 shutdown [arg] # arg参数: -r # 重启系统 -h # 关闭系统,可以再后面指定时间 -H # 关闭系统,不常用 -P # 关闭系统,不常用 -c # 取消正在执行的shutdown命令 -k # 只发送关机消息给所有终端,但并不会真正关机,极少使用 shutdown -h now # 立刻关机 shutdown -h 0 # 立刻关机 shutdown -h 5 # 5分钟之后关机,最大支持15min的延迟 shutdown -h 14:00 # 在14:00关机,最大支持15min的延迟 shutdown -r now # 立刻重启 shutdown -r 0 shutdown -r 5 # 5分钟之后重启,最大支持15min的延迟 shutdown -r 14:00 # 取消计划 shutdown -cCentOS7的关机、重启命令# shutdown、reboot、poweroff、halt在CentOS7中实际上是systemctl命令的参数,CentOS7还有以下系统关机重启相关的命令: systemctl reboot # 重启系统 systemctl halt # 关闭系统,CPU停止工作 systemctl poweroff # 关闭系统并切断电源 systemctl suspend # 暂停系统运行 systemctl hibernate # 系统休眠 systemctl hybrid-sleep # 系统进入交互式休眠状态 systemctl rescue # 启动救援状态bash:shell 脚本解释器bash 是一个为 GNU 计划编写的 Unix shell。bash 是大多数 Linux 系统以及Mac OSX 默认的 shell,它能运行于大多数类 Unix 风格的操作系统之上,甚至被移植到了 Microsoft Windows 上的 Cygwin 系统中,以实现 Windows 的 POSIX 虚拟接口。此外,它也被 DJGPP 项目移植到了 MS-DOS 上。语法格式:bash [参数] [文件] # 常用参数: -n 检测脚本是否正确,并不执行脚本 -x 执行脚本,输出执行过程 -c bash从字符串中读入命令,如果字符串后还有变量就被设定为从$0开始的位置参数参考实例:# 使用-n参数检查脚本语法时候正确: bash -n linuxcool.sh # 使用-x参数执行linuxcool.sh脚本并输出执行过程: bash -x linuxcool.shsh 和 bash 的区别sh 是一种 POSIX 标准,它有很多种实现,包括 ksh88,dash,bash 等。因为 sh 是一种规范,并不是实现,sh 通常是一个软链接,链接到系统默认 shell。大多数情况下,/bin/sh 会链接到/bin/bash所以执行 sh xx.sh 等价于执行 bash xx.sh 但是在一些系统中,/bin/sh 并没有指向 /bin/bash,比如在一些现代的 Debian 和 Ubuntu 系统中,sh 指向的是dash文件管理chown chmod:文件权限设置命令用户对文件具有什么操作权限root管理员linux的文件操作权限:读权限(r):对文件表示可读取此文件中的实际内容,对目录表示读取目录结构列表的权限写权限(w):对文件表示可以编辑、新增或者修改文件中的内容,对目录表示可以新建,删除,修改,移动子目录和目录中的文件执行权限(x):对文件表示该文件具有被系统执行的权限。对目录赋予 x 权限,代表用户可以进入目录- rw- r-- r-- # 文件类型 -:文件 d:文件夹 l:链接文件(快捷方式) b:可供存储周边设备 c:一次性读取装置 # 文件所有者(用户root):读权限,写权限(对超级管理员root无效) # 文件所属组(用户组root):读权限 # 其他用户:读权限chown ==> 修改文件或目录所有者,所有组命令# 语法 chown [-R] 所有者[:所属组] 文件或目录 -R, --recursive # 递归处理,将指令目录下的所有文件及子目录一并处理 # 参考实例 # 修改 test1.txt 文件的所有者为 www chown www test1.txt # 结果:-rw-r--r--. 1 www root 0 3月 31 10:47 test1.txt # 修改 text2.txt 文件的所有者和所有组为 www chown www:www test2.txt # 结果:-rw-r--r--. 1 www www 0 3月 31 10:47 test2.txt # 修改 test3.txt 文件的所有组为 www chown :www test3.txt # 结果:-rw-r--r--. 1 root www 0 3月 31 10:47 test3.txtchmod ==> 修改文件或目录权限命令使用数字设置权限# 语法 chmod [-R] 权限值 文件名 -R, --recursive # 递归处理,将指令目录下的所有文件及子目录一并处理 # 各个权限用数字表示规则: r --> 4 w --> 2 x --> 1 - --> 0 # 示例:rwxr-xr-x = 765 所有者的权限值为:rwx = 4 2 1 = 4+2+1 = 7 所属组的权限值为:rw- = 4 2 0 = 4+2+0 = 6 其他人的权限值为:r-x = 4 0 1 = 4+0+1 = 5 # 参考示例 chmod 当前登录用户的权限/组中用户的权限/其他用户的权限 test1.txt chmod 777 1.txt # 结果:-rwxrwxrwx. test1.txt chmod 000 1.txt # 结果:----------. test1.txt chmod 536 1.txt # 结果:-r-x-wxrw-. test1.txt使用字母设置权限# 语法 chmod [-R] [u,g,o,a][ ,-,=][r,w,x] # 使用字母设置权限 -R, --recursive # 递归处理,将指令目录下的所有文件及子目录一并处理 # 参数说明 [u,g,o,a] u # 所属者身份 g # 所属组身份 o # 其他用户身份 a # 所有身份(不加身份参数,默认所有身份) [+,-,=] + # 加入某个权限 - # 减少某个权限 = # 设置权限 [r,w,x] r # 读权限 w # 写权限 x # 执行权限 # 参考实例 -rwxrwXrwx 1 www root 0 Dec 6 09:45 test1.txt -rwxr-Xr-x 1 www www 0 Dec 6 09:45 test2.txt -rw-r--r-- 1 root www 0 Dec 6 09:45 test3.txt [root@centos test]# chmod o-wx test1.txt # 其他用户 减少 写(w)和执行(x)权限 [root@centos test]# chmod g=rwx test2.txt # 所属组 设置权限为 rwx [root@centos test]# chmod u+x test3.txt # 所属者 添加 执行(x)权限 [root@centos test]# ll total 0 -rwxrwxr-- 1 www root 0 Dec 6 09:45 test1.txt -rwxrwxr-x 1 www www 0 Dec 6 09:45 test2.txt -rwxr--r-- 1 root www 0 Dec 6 09:45 test3.txtdu:查看显示文件和目录占用空间du 命令的英文全称是 “Disk Usage”,即用于查看磁盘占用空间的意思。但是与 df 命令不同的是 du 命令是对文件和目录磁盘使用的空间的查看,而不是某个分区。语法格式:# 默认遍历展示各级目录(包含隐藏目录)的占用空间大小,默认单位kb du [参数] [文件/目录] # 注:若不指定文件或目录则默认当前目录 # 常用参数: -h # 以易读方式显示占用空间大小 -s # 仅显示总计,不展示各级目录和文件的大小。 -c # 追加显示总计占用空间大小 -a # 显示所有文件和目录的占用空间大小(即文件、文件夹、隐藏文件和文件夹) -k # 以KB为单位显示占用空间大小。默认 -m # 以MB为单位显示占用空间大小 -g # 以GB为单位显示占用空间大小参考实例:# 查询当前目录总大小 du -sh # 降序排列展示当前目录及一级子目录的占用空间 du -h --max-depth=1 | sort -gr # 以易读方式显示当前目录及多级子路径的占用空间 du -h # 以易读方式显示文件夹内所有文件大小 du -ah # 输出当前目录及一级子目录的占用空间,并显示总计 du -ch --max-depth=1 # 显示指定文件的所占空间 du log2012.log ls:显示指定工作目录下的内容及属性信息ls 命令是 Linux 下最常用的指令之一。ls 命令为英文单词 list 的缩写,正如英文单词 list 的意思,其功能是列出指定目录下的内容及其相关属性信息。默认状态下,ls 命令会列出当前目录的内容。语法格式:ls [选项] [文件] # 常用参数: -a 显示所有文件及目录 (包括以“.”开头的隐藏文件) -l 使用长格式列出文件及目录信息。ls -l 简写 ll -r 将文件以相反次序显示(默认依英文字母次序) -t 根据最后的修改时间排序 -A 同 -a ,但不列出 “.” (当前目录) 及 “..” (父目录) -S 根据文件大小排序 -R 递归列出所有子目录 -h 以易读方式展示大小参考实例:# 列出所有文件(包括隐藏文件): ls -a # 列出文件的详细信息 ls -l # 根据占用空间大小排序展示文件和文件夹 ls -lhS # 列出当前工作目录下所有名称是 “s” 开头的文件 ll s* # 列出根目录(/)下的所有目录: ls / cp:复制文件或目录cp 命令可以理解为英文单词 copy 的缩写,其功能为复制文件或目录。cp 命令可以将多个文件复制到一个具体的文件名或一个已经存在的目录下,也可以同时复制多个文件到一个指定的目录中。语法格式:cp [参数] 源文件 目录 # 常用参数: -r # 递归复制文件和目录。 -f # 若目标文件已存在,则会直接覆盖原文件。 # 默认cp命令是有别名(alias cp='cp -i')的,无法强制覆盖,即使使用 -f 参数也无法强制覆盖文件 # 可以使用 \cp 执行cp命令时不走alias -i # 若目标文件已存在,则会询问是否覆盖 -p # 保留源文件或目录的所有属性 -d # 当复制符号连接时,把目标文件或目录也建立为符号连接,并指向与源文件或目录连接的原始文件或目录 -l # 对源文件建立硬连接,而非复制文件 -s # 对源文件建立符号连接,而非复制文件 -b # 覆盖已存在的文件目标前将目标文件备份 -v # 详细显示cp命令执行的操作过程 -a # 等价于“dpr”选项参考实例:# 复制目录 cp -r dir1 dir2/ # 将文件test1复制到目录dir2,并改名为test2 cp -f test1 dir2/test2 # 复制多个文件 cp -r file1 file2 file3 dir # 交互式地将目录 /usr/linuxcool 中的所有.c文件复制到目录 dir 中 cp -r /usr/linuxcool/*.c dirmv:移动或改名文件mv 命令是“move”单词的缩写,其功能大致和英文含义一样,可以移动文件或对其改名。这是一个使用频率超高的文件管理命令,需要特别留意它与复制的区别:mv 与 cp 的结果不同。mv 命令好像文件“搬家”,文件名称发生改变,但个数并未增加。cp 命令是对文件进行复制操作,文件个数是有增加的。语法格式:mv [参数] 源文件 目录 # 常用参数: -i 若存在同名文件,则向用户询问是否覆盖 -f 覆盖已有文件时,不进行任何提示 -b 当文件存在时,覆盖前为其创建一个备份 -u 当源文件比目标文件新,或者目标文件不存在时,才执行移动此操作参考示例:# 将文件file_1重命名为file_2 mv file_1 file_2 # 将文件file移动到目录dir中 mv file /dir # 将目录dir1移动目录dir2中(前提是目录dir2已存在,若不存在则改名) mv dir1 /dir2 # 将目录dir1下的所有文件移动到当前目录下 mv /dir1/* .mkdir:创建目录mkdir 命令是“make directories”的缩写,用来创建目录。注意:默认状态下,如果要创建的目录已经存在,则提示已存在,而不会继续创建目录。 所以在创建目录时,应保证新建的目录与它所在目录下的文件没有重名。语法格式 :mkdir [参数] [目录] # 常用参数: -p 递归创建多级目录 -m 建立目录的同时设置目录的权限 -z 设置安全上下文 -v 显示目录的创建过程参考示例:# 在工作目录下,建立一个名为 dir 的子目录: mkdir dir # 在目录/usr/linuxcool下建立子目录dir,并且设置文件属主有读、写和执行权限,其他人无权访问 mkdir -m 700 /usr/linuxcool/dir # 同时创建子目录dir1,dir2,dir3: mkdir dir1 dir2 dir3 # 递归创建目录: mkdir -p linuxcool/dirtouch:创建文件touch 命令有两个功能:一是创建新的空文件,二是改变已有文件的时间戳属性。touch 命令会根据当前的系统时间更新指定文件的访问时间和修改时间。如果文件不存在,将会创建新的空文件,除非指定了”-c”或”-h”选项。注意:在修改文件的时间属性的时候,用户必须是文件的属主,或拥有写文件的访问权限。语法格式:touch [参数] [文件] 常用参数: -a 改变档案的读取时间记录 -m 改变档案的修改时间记录 -r 使用参考档的时间记录,与 --file 的效果一样 -c, --no-create 不创建新文件 -d 设定时间与日期,可以使用各种不同的格式 -t 设定档案的时间记录,格式与 date 命令相同参考实例:# 创建空文件: touch file.txt # 批量创建文件: $ touch file{1..5}.txt rm:移除文件或目录rm 是常用的命令,该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除。对于链接文件,只是删除了链接,原有文件均保持不变。rm 是一个危险的命令,使用的时候要特别当心,尤其对于新手,否则整个系统就会毁在这个命令(比如在 /(根目录)下执行 rm * -rf)所以,在执行 rm 之前最好先确认一下在哪个目录,到底要删除什么东西,操作时保持高度清醒的头脑。语法格式:rm [参数] [文件] # 常用参数: -f 忽略不存在的文件,不会出现警告信息 -i 删除前会询问用户是否操作 -r/R 递归删除 -v 显示指令的详细执行过程参考实例:# 直接删除,不会有任何提示: rm -f 文件名或空目录 # 递归删除目录及目录下所有文件: rm -rf /文件名或目录 # 删除当前目录下所有文件: rm -rf * # 清空系统中所有的文件(谨慎): rm -rf /*rmdir:删除空目录rmdir 命令作用是删除空的目录,英文全称:“remove directory”。注意:rmdir 命令只能删除空目录。当要删除非空目录时,就要使用带有“-R”选项的 rm 命令。rmdir 命令的“-p”参数可以递归删除指定的多级目录,但是要求每个目录也必须是空目录。语法格式 :rmdir [参数] [目录名称] # 常用参数: -p 用递归的方式删除指定的目录路径中的所有父级目录,非空则报错 -v 显示命令的详细执行过程 -- ignore-fail-on-non-empty 忽略由于删除非空目录时导致命令出错而产生的错误信息参考实例:# 删除空目录: rmdir dir # 递归删除指定的目录树: rmdir -p dir/dir_1/dir_2pwd:显示当前路径pwd 命令是“print working directory”中每个单词的首字母缩写,其功能正如所示单词一样,为打印工作目录,即显示当前工作目录的绝对路径。在实际工作中,经常会在不同目录之间进行切换,为了防止“迷路”,可以使用 pwd 命令快速查看当前所在的目录路径。语法格式:pwd [参数] # 常用参数: -L # 显示逻辑路径 # 获取当前文件夹的名称 basename `pwd`ln:创建软/硬链接语法格式:ln -s target source # 参数说明: -s # 软链接,不加该参数默认为硬链接 target # 表示目标文件(夹)【即被指向的文件(夹)】 source # 表示当前目录的软链接名。【即源文件(夹)】。注意,该目录名不能已存在,不然会在该目录下创建软链接! # 示例: ln -s /data/aaa/bbb bbb软连接的删除rm -rf ./test_chk_ln/ # 会删除文件夹下的所有内容,但是没有删除这个链接; rm -rf ./test_chk_ln # 则是仅删除这个软链接,不会删除下面的内容。文档编辑文件内容浏览命令less 文件名 # 分页查看所有内容 # 常用 # 箭头上: 上一行 # 箭头下: 下一行 # 空格:下一页 # b:上一页 # q:退出查看 # 参数: -m # 显示百分比 -N # 显示行号 cat 文件名 # 查看文件局部内容(查看的只是文件最后一部分的内容) more 文件名 # 百分比分页查看所有内容 # 回车:下一行 # 空格:下一页 # q :退出 # 查看文件的头部/末尾指定行树 (默认显示文件的头部/末尾10行内容) head -n 数字 文件名 # 查看文件头部内容 tail -n 数字 文件名 # 查看文件末尾内容 # -f 循环读取。常用 # -q 不显示处理信息 # -v 显示详细的处理信息 # -c<数目> 显示的字节数 # -n<行数> 显示文件的尾部 n 行内容 # --pid=PID 与-f合用,表示在进程ID,PID死掉之后结束 # -q, --quiet, --silent 从不输出给出文件名的首部 # -s, --sleep-interval=S 与-f合用,表示在每次反复的间隔休眠S秒 tail -f FileName # 把 FileName 文件里的最尾部的内容显示在屏幕上,并且不断刷新文件内容编辑命令编辑模式:对具体的内容进行操作命令模式:对内容行进行操作 复制整行内容 粘贴整行内容 删除整行内容底行模式:保存 退出# 打开文件 vi 文件名 #或 vim 文件名 # 注:vim可以编辑jar包 # 编辑文件内容操作流程 vi 文件名 ==>打开文件进入 命令模式 ----> 按 i 或 o ==>进入 编辑模式 ----> (编辑内容) ----> 按 Esc ==>进入 命令模式(锁定内容,禁止编写,但是可以复制粘贴) ----> 按 Shift+: ==>进入 底行模式(保存 退出) # 命令模式 常用的快捷键 dd # 删除当前行 yy # 复制当前行 p # 粘贴 /要搜索的内容 # 搜索指定内容。按n键:下一个;按N键:上一个 # 底行模式 :wq # 保存并退出 :q # 退出 :q! # 强制退出不保存注:编写文件的过程中,如果卡死,下次进来需要将隐藏文件删除 .文件名.swpcat:在终端设备上显示文件内容Linux 系统中有很多个用于查看文件内容的命令,每个命令又都有自己的特点,比如 cat 命令就是用于查看内容较少的纯文本文件的。注意:当文件内容较大时,文本内容会在屏幕上快速闪动(滚屏),用户往往看不清所显示的具体内容。因此对于较长文件内容:按 Ctrl+S 键,可以停止滚屏按 Ctrl+Q 键,可以恢复滚屏按Ctrl+C(中断)键,可以终止该命令的执行或者对于大文件,干脆用 more 命令吧!语法格式:cat [参数] [文件] # 常用参数: -n 显示行数(空行也编号) -s 显示行数(多个空行算一个编号) -b 显示行数(空行不编号) -E 每行结束处显示$符号 -T 将TAB字符显示为 ^I符号 -v 使用 ^ 和 M- 引用,除了 LFD 和 TAB 之外 -e 等价于”-vE”组合 -t 等价于”-vT”组合 -A 等价于 -vET组合参考示例:# 查看文件的内容: cat filename.txt # 查看文件的内容,并显示行数编号: cat -n filename.txt # 查看文件的内容,并添加行数编号后输出到另外一个文件中: cat -n linuxcool.log > linuxprobe.log # 清空文件的内容: cat /dev/null > /root/filename.txt # 持续写入文件内容,碰到EOF符后结束并保存: cat > filename.txt <<EOF Hello, World Linux! # 将软盘设备制作成镜像文件: cat /dev/fd0 > fdisk.isotail:查看文件尾部内容tail 用于显示文件尾部的内容,默认在屏幕上显示指定文件的末尾 10 行。如果给定的文件不止一个,则在显示的每个文件前面加一个文件名标题。如果没有指定文件或者文件名为“-”,则读取标准输入。语法格式:tail [参数] # 常用参数: -f, --follow<name|descriptor> 显示文件最新追加的内容 -c, --bytes=N 输出文件尾部的N(N为整数)个字节内容 -n, --line=N 输出文件的尾部N(N位数字)行内容 --pid=PID 与“-f”选项连用,当指定的进程号的进程终止后,自动退出tail命令 --retry 即使在tail命令启动时,文件不可访问或者文件稍后变得不可访问,都始终尝试打开文件。 使用此选项时需要与选项“——follow=name”连用 -F 与选项“--follow=name --retry”连用时功能相同参考示例:# 显示文件file的最后10行: tail file # 显示文件file的内容,从第20行至文件末尾: tail +20 file # 显示文件file的最后10个字符: tail -c 10 file # 一直变化的文件总是显示后10行: tail -f 10 fileless:分页显示工具浏览文字档案的内容。用 less 命令显示文件时,PageUp 键向上翻页,PageDown 键向下翻页,要退出 less 程序,应按 Q 键。less 的作用与 more 十分相似,不同点为 less 命令允许用户向前或向后浏览文件,而 more 命令只能向前浏览 。语法格式:less [参数] [文件] # 常用参数: -b 置缓冲区的大小 -e 当文件显示结束后,自动离开 -f 强迫打开特殊文件,例如外围设备代号、目录和二进制文件 -g 只标志最后搜索的关键词 -i 忽略搜索时的大小写 -m 显示类似more命令的百分比 -N 显示每行的行号 -o 将less 输出的内容在指定文件中保存起来 -Q 不使用警告音 -s 显示连续空行为一行 -S 在单行显示较长的内容,而不换行显示 -x 将TAB字符显示为指定个数的空格字符实际实例:# 查看文件 : less test.php # ps查看进程信息并通过less分页显示: ps -ef | less # 查看命令历史使用记录并通过less分页显示: history | less # 浏览多个文件 : less log2018.log log2019.log # 当正在浏览一个文件时,也可以使用 :e命令 打开另一个文件: less file1 :e file2 命令内部操作:b 向后翻一页d 向后翻半页h 显示帮助界面Q 退出less 命令u 向前滚动半页y 向前滚动一行空格键 滚动一页回车键 滚动一行grep:过滤查找参考:linux grep命令grep 命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。grep 全称是 Global Regular Expression Print,表示全局正则表达式版本,它的使用权限是所有用户。格式grep [选项]... PATTERN [FILE]... # 主要参数: # 正则表达式控制参数 -w, --word-regexp # 强制 PATTERN 仅完全匹配字词 -x, --line-regexp # 强制 PATTERN 仅完全匹配一行 -i, --ignore-case # 忽略大小写 -E, --extended-regexp # PATTERN 是一个可扩展的正则表达式(缩写为 ERE),识别逻辑操作符 与(|) -e, --regexp=PATTERN # 用 PATTERN 来进行匹配操作 -G, --basic-regexp # PATTERN 是一个基本正则表达式(缩写为 BRE) -A, --after-context=NUM # 打印含匹配词的后 NUM 行 -B, --before-context=NUM # 打印含匹配词的前 NUM 行 -C, --context=NUM # 打印含匹配词的前后 NUM 行 # 输出控制参数 -v # 对过滤结果取反 -c # 只输出匹配行的计数 -h # 查询多文件时不显示文件名前缀 -l # 查询多文件时只输出包含匹配字符的文件名 -n # 输出的同时打印行号,行号在前 -m, --max-count=NUM # NUM 次匹配后停止 -s # 不显示不存在或无匹配文本的错误信息 -v # 显示不包含匹配文本的所有行。示例:grep -v '^$' 文件名 # pattern正则表达式主要参数: | # 逻辑操作符:与 \ # 忽略正则表达式中特殊字符的原有含义 ^ # 匹配正则表达式的开始行 $ # 匹配正则表达式的结束行 \< # 从匹配正则表达 式的行开始 \> # 到匹配正则表达式的行结束。 [ ] # 单个字符,如[A]即A符合要求 [ - ] # 范围,如[A-Z],即A、B、C一直到Z都符合要求 。 # 所有的单个字符 \* # 有字符,长度可以为0使用实例:# 显示所有以d开头的文件中包含 test的行 $ grep ‘test’ d* # 显示在aa,bb,cc文件中匹配test的行 $ grep ‘test’ aa bb cc # 显示所有包含每个字符串至少有5个连续小写字符的字符串的行 $ grep ‘[a-z]\{5\}’ aa # 如果west被匹配,则es就被存储到内存中,并标记为1,然后搜索任意个字符(.*),这些字符后面紧跟着 另外一个es(\1),找到就显示该行。如果用egrep或grep -E,就不用”\”号进行转义,直接写成’w(es)t.*\1′就可以了。 $ grep ‘w\(es\)t.*\1′ aased:流编辑器参考:Linux中sed的用法sed 流编辑器,实现对文字的增删改替换查(过滤、取行),能同时处理多个文件多行的内容,可以不对原文件改动,把整个文件输入到屏幕,可以把只匹配到模式的内容输入到屏幕上。还可以对原文件改动,但是不会再屏幕上返回结果。流程:Sed软件从文件或管道中读取一行,处理一行,输出一行;再读取一行,再处理一行,再输出一行……模式空间:sed软件内部的一个临时缓存,用于存放读取到的内容。命令格式:sed [选项] [命令] [输入文件] # 常用选项: -i # 直接修改文件内容。如:sed -i s#old#new#g -n # 只打印模式匹配的行,一般与 p 一起使用。 # 如:sed -n '2p' /data.text 表示输出/data的第二行 # 如:sed -n '20,30p' ./data.text 显示 /data/boy的20到30行 -e # 进行多项编辑,即对输入行应用多条sed命令时使用,此为默认选项 # 如:sed -e '/^#/d' -e '/^$/d' ./data.text 表示删除空格的行和#开头的行 -r # 支持扩展表达式 -f filename # 将sed的动作写在一个脚本文件内,执行filename内的sed动作 # 常用命令。注:如果命令中要传变量,则需要使用双引号,不能使用单引号 s/pattern/replaces/替换标记 # 查找pattern用replaces替换;分隔符可自行指定,常用的分隔符有/, #, @等 # 替换标记: # g:全局替换。示例:sed -i 's/pattern/replaces/g' test.text # w /file:将替换的结果保存至指定文件中; # 实例:sed -i 's/pattern/replaces/w my.txt' test 将替换后的结果保存到my.txt中 # p:显示替换成功的行 d # delete, 删除匹配到的行; # 示例:sed -i '/xxx/d' filename # 更换分隔符为#,示例:sed -i '\#xxx#d' filename p # print, 显示匹配到的行;通常 p 会与参数 sed -n 一起用 a text # append, 在指定行后追加文本text,支持使用\n实现多行追加。 # 示例:sed '/hello/a world' hello.sh # hello行追加world行 i text # insert, 在指定行前插入文本text,支持使用\n实现多行插入 # sed '/hello/i world\n westos' hello.sh # 在hello行前插入 world行 和 westos行 # sed '4i abc' test.txt # 在第4行插入abc c text # 将指定行的内容替换为文本text # 示例:sed '/hello/c hello world' hello.sh # 将hello行替换为 hello world w file # write, 保存模式空间中匹配到的行至指定的文件中; r file # read, 将指定文件的内容读取至当前模式空间中被匹配到的行后面,常用于实现文件合并 # 示例:sed -i '/Ethernet/r myfile' test # 匹配Ethernet的行,读进来另一个文件的内容,读进来的文件的内容会插入到匹配Ethernet的行后 y # 用于(对应)转换字符 = # 打印行号 ! # 匹配后取反 l # 打印行号,并显示控制字符 q # 读取匹配到的行后退出sed在文件中查询文本的方式格式描述示例x/p查询第x行sed -n '2p ' /datax,y/p查询从x到y行sed -n '1,3p ' /data/pattern/p查询包含pattern的行sed -n '/pattern/p' /data/pattern 1/,/pattern 2/p查询包含pattern 1或pattern 2的行sed -n '/pn1/,/pn2/p' /data/pattern/,xp查询从包含pattern的行到x行sed -n '/pn/,5p' /datax,/pattern/p查询从x到包含pattern的行sed -n '5,/pn/p' /datax,y!p查询不包含指定行号x和y的行sed -n '5,8!p' /data10{sed-commands} # 对第10行操作 10,20{sed-commands} # 对10到20行操作,包括第10,20行 10,+20{sed-commands} # 对10到30(10+20)行操作,包括第10,30行 1~2{sed-commands} # 对1,3,5,7,……行操作 10,${sed-commands} # 对10到最后一行($代表最后一行)操作,包括第10行 /oldboy/{sed-commands} # 对匹配oldboy的行操作 /oldboy/,/Alex/{sed-commands} # 对匹配oldboy的行到匹配Alex的行操作 /oldboy/,${sed-commands} # 对匹配oldboy的行到最后一行操作 /oldboy/,10{sed-commands} # 对匹配oldboy的行到第10行操作 # 注意:如果前10行没有匹配到oldboy,sed软件会显示10行以后的匹配oldboy的行,如果有。 1,/Alex/{sed-commands} # 对第1行到匹配Alex的行操作 /oldboy/,+2{sed-commands} # 对匹配oldboy的行到其后的2行操作### 正则 #去除空行 sed '/^$/d' # 文件末尾添加新行 sed -i '$a 数据' 文件名 # 指定行后面追加新行(在第4行后面追加) sed '4 a 哈工大' data.txt # 指定匹配模式后追加数据(在包含 小王 的行后面追加 哈工大) sed '/小王/a 哈工大' data.txt # 替换12行的匹配值 sed '12s/小王/哈工大/g' data.txt # 替换前缀为 小王 的任意匹配值 sed 's/小王*/哈工大/g' data.txt备份压缩tar:打包/解包文件tar 命令可以为 linux 的文件和目录创建档案。利用 tar,可以为某一特定文件创建档案(备份文件),也可以在档案中改变文件,或者向档案中加入新的文件。tar 最初被用来在磁带上创建档案,现在,用户可以在任何设备上创建档案。利用tar命令,可以把一大堆的文件和目录全部打包成一个文件,这对于备份文件或将几个文件组合成为一个文件以便于网络传输是非常有用的。语法格式:tar [参数] [文件或目录] # 常用参数: -A 新增文件到以存在的备份文件 -B 设置区块大小 -c 建立新的备份文件 -C <目录> 切换工作目录,先进入指定目录再执行压缩/解压缩操作,可用于仅压缩特定目录里的内容或解压缩到特定目录 -d 记录文件的差别 -x 从归档文件中提取文件 -t 列出备份文件的内容 -z 通过gzip指令压缩/解压缩文件,文件名最好为*.tar.gz -Z 通过compress指令处理备份文件 -f<备份文件> 指定备份文件 -v 显示指令执行过程 -r 添加文件到已经压缩的文件 -u 添加改变了和现有的文件到已经存在的压缩文件 -j 通过bzip2指令压缩/解压缩文件,文件名最好为*.tar.bz2 -v 显示操作过程 -l 文件系统边界设置 -k 保留原有文件不覆盖 -m 保留文件不被覆盖 -w 确认压缩文件的正确性 -p 保留原来的文件权限与属性 -P 使用文件名的绝对路径,不移除文件名称前的“/”号 -N <日期格式> 只将较指定日期更新的文件保存到备份文件里 -- -exclude=<范本样式> 排除符合范本样式的文件 -- -remove-files 归档/压缩之后删除源文件参考实例:# 将所有.jpg的文件打成一个名为file.tar的包: tar -cvf file.tar *.jpg # 打包文件之后删除源文件: tar -cvf file.tar file --remove-files # .tar解包到当前目录 tar -xvf file.tar # .tar解包到指定目录 tar -xvf file.tar -C /usr/temp/ # 打包文件以后,以 gzip 压缩: tar -zcvf log.tar.gz file.log # 解包文件以后,以 gzip 解压缩: tar -zxvf file.tar.gzzip:压缩文件zip 程序将一个或多个压缩文件与有关文件的信息(名称、路径、日期、上次修改的时间、保护和检查信息以验证文件完整性)一起放入一个压缩存档中。可以使用一个命令将整个目录结构打包到 zip 存档中。语法格式:zip [参数] [文件名.zip] [文件/目录] # 常用参数: -r 递归处理,将指定目录下的所有文件和子目录一并处理 -d 删除压缩包里的条目 -q 不显示指令执行过程 -z 替压缩文件加上注释 -v 显示指令执行过程或显示版本信息 -n<字尾字符串> 不压缩具有特定字尾字符串的文件参考实例:# 将 /home/html/ 这个目录下所有文件和文件夹打包为当前目录下的 html.zip: zip -r html.zip /home/html # 把abc文件夹和123.txt压缩成为abc123.zip: zip -r abc123.zip abc 123.txt # 压缩文件 cp.zip 中删除文件 a.txt: zip -dv cp.zip a.txtunzip:解压缩zip文件unzip 命令是用于 .zip 格式文件的解压缩工具 unzip 命令将列出、测试或从 zip 格式存档中提取文件,这些文件通常位于 MS-DOS 系统上。默认行为(就是没有选项)是从指定的 ZIP 存档中提取所有的文件到当前目录(及其下面的子目录)。语法格式:unzip [参数] [文件] # 常用参数: -d 解压到的目录 -o:不必先询问用户,unzip执行后覆盖原有的文件 -l 不解压,显示压缩文件内所包含的文件的简单信息 -v 不解压,显示压缩文件内所包含的文件的详细信息 -c 将解压缩的结果显示到屏幕上,并对字符做适当的转换 -n 解压缩时不要覆盖原有的文件 -j 不处理压缩文件中原有的目录路径 -t 验证压缩包是否完整参考实例:# 把wwwroot.zip直接解压到当前目录里面: unzip wwwroot.zip # 把mydata.zip解压到mydatabak目录里面: unzip mydata.zip -d mydatabak # 把abc12.zip、abc23.zip、abc34.zip同时解压到当前目录里面: unzip abc\*.zip # 查看wwwroot.zip里面的内容: unzip -v wwwroot.zip # 验证/home目录下面的wwwroot.zip是否完整: unzip -t wwwroot.zip tar:归档/还原tar包语法格式:tar [参数] [文件/目录] # 常用参数: -c, --create # 创建新的归档文件 -x, --extract # 解压缩已归档的指定(或所有)文件 -z, --gzip # 调用gzip对tar归档文件进一步压缩成tar.gz格式,或者对tar.gz格式完成解压缩 -v, --verbose # 生成详细输出 -f, --file # 指定归档文件名 -r # 向归档文件末尾追加文件 -C # 改变目录参考实例:## .tar:打包文件 tar -cvf file.tar file # .tar打包命令 tar -xvf file.tar # .tar解包命令 ## .tar.gz:打包压缩文件 # .tar.gz打包命令 tar -zcvf file.tar.gz file # .tar.gz解包命令(掌握) tar -zxvf file.tar.gz # 解压缩到当前目录下 tar -zxvf file.tar.gz -C /usr/temp/ # 解压缩到指定目录下dump:备份文件系统dump 命令用于备份文件系统 dump 为备份工具程序,可将目录或整个文件系统备份至指定的设备,或备份成一个大文件。语法格式:dump [参数] # 常用参数: -0123456789 备份的层级 -b 指定区块的大小,单位为KB -B 指定备份卷册的区块数目 -c 修改备份磁带预设的密度与容量 -d 设置磁带的密度。单位为BPI -f 指定备份设备 -h 当备份层级等于或大于指定的层级时,将不备份用户标示为”nodump”的文件 -n 当备份工作需要管理员介入时,向所有”operator”群组中的使用者发出通 -s 备份磁带的长度,单位为英尺 -T 指定开始备份的时间与日期 -u 备份完毕后,在/etc/dumpdates中记录备份的文件系统,层级,日期与时间等 -w 与-W类似,但仅显示需要备份的文件 -W 显示需要备份的文件及其最后一次备份的层级,时间与日期参考实例:# 备份文件到/home目录: dump -0 -u /dev/tape /home/ # 备份文件系统/boot到 SCSI 磁带设备: dump -0f /dev/nst0 /bootmysqldump:MySQL 数据库备份mysqldump 命令是 MySQL 数据库中备份工具,用于将MySQL服务器中的数据库以标准的sql语言的方式导出,并保存到文件中。语法格式:mysqldump [选项] 数据库名 [表名] > 脚本名 mysqldump [选项] --数据库名 [选项 表名] > 脚本名 mysqldump [选项] --all-databases [选项] > 脚本名 # 常用参数: -h, --host 服务器IP地址 -P. --port 服务器端口号 -u, --user MySQL 用户名 -p, --pasword MySQL 密码 --databases 指定要备份的数据库 --all-databases 备份mysql服务器上的所有数据库 --compact 压缩模式,产生更少的输出 --comments 添加注释信息 --complete-insert 输出完成的插入语句 --lock-tables 备份前,锁定所有数据库表 --no-create-db/--no-create-info 禁止生成创建数据库语句 --force 当出现错误时仍然继续备份操作 --default-character-set 指定默认字符集 --add-locks 备份数据库表时锁定数据库表 --ignore-table 备份数据时库排除某表参考实例:# 导出整个数据库: mysqldump -u linuxcool -p smgp_apps_linuxcool > linuxcool.sql # 导出指定的数据库: mysqldump -u linuxcool -p smgp_apps_linuxcool users > linuxcool_users.sql # 备份指定数据库指定表(多个表以空格间隔) mysqldump -uroot -p mysql db event > /backup/mysqldump/2table.sql # 导出一个数据库结构: mysqldump -u linuxcool -p -d --add-drop-table smgp_apps_linuxcool > linuxcool_db.sql # 备份指定数据库排除某些表 mysqldump -uroot -p test --ignore-table=test.t1 --ignore-table=test.t2 > /backup/mysqldump/test2.sql备份还原命令:mysqladmin -uroot -p create db_name mysql -u root -p password db_name < /backup/mysqldump/db_name.db # 注:在导入备份数据库前,db_name如果没有,是需要创建的; 而且与db_name.db中数据库名是一样的才可以导入。k8s部署 定时备份mysql数据库及还原命令:# 创建定时任务 crontab -e # 定时任务(每天0点执行): 0 0 * * * /usr/local/bin/kubectl exec `/usr/local/bin/kubectl get po | grep mysql | awk '{print $1}'` -- mysqldump -u user -p password db_name > /root/mysql-bak/`date + \%d`.sql 2>&1 # 注:2>&1 的意思就是将标准错误重定向到标准输出。这里标准输出已经重定向到了~.sql。那么标准错误也会输出到~.sql文件里 # 还原命令: /usr/local/bin/kubectl exec `/usr/local/bin/kubectl get po | grep mysql | awk '{print $1}'` -- mysql -u user -p password db_name < /root/mysql-bak/`date + \%d`.sql
概述kubeconfig 文件保存了 k8s 集群的集群、用户、命名空间、认证的信息。kubectl 命令使用 kubeconfig 文件来获取集群的信息,然后和API server进行通讯。注意:用于配置对集群的访问的文件称为 kubecconfig 文件。也就是说,kubeconfig 文件中包含的内容是集群的配置。但是,并不是必须有个文件名字叫 kubeconfig默认情况下,kubectl命令从 $HOME/.kube 目录下查找一个名字叫做 config 的文件。可以通过 KUBECONFIG 环境变量或者--kubeconfig 参数来指定其他的 kubeconfig 文件。kubeconfig中主要由如下部分组成:clusters (集群)users(用户)context(上下文)kubeconfig支持多集群、多用户、多认证在实际的使用中的如下场景:kubelet使用证书认证(kubelet和api server进行认证)用户使用token进行认证管理员为不同的用户提供不同的证书都可以使用kubeconfig来组织起集群、用户、命名空间的信息。同样,也可以使用context在集群和命名空间之间进行切换。Context的定义在kubeconfig中,context中将访问一个集群的参数进行分组。访问这个context名称就是访问这个参数组。context就是一组信息的别名,举例:当在高德中使用家的地址,公司的地址就是一个别名,就能迅速的定位到具体的地址信息。每个context都有3个参数:cluster (集群)namespace(命名空间)user(用户)默认情况下,kubectl命令从current context中来获取参数,然后与集群进行通讯。查看kubeconfig的配置如果有KUBECONFIG环境变量,看到的配置是合并的配置[root@nccztsjb-node-11 ~]# kubectl config view apiVersion: v1 clusters: - cluster: certificate-authority-data: DATA+OMITTED server: https://k8s.apiserver.io:6443 name: cluster.local contexts: - context: cluster: cluster.local user: kubernetes-admin-cluster.local name: kubernetes-admin-cluster.local@cluster.local current-context: kubernetes-admin-cluster.local@cluster.local kind: Config preferences: {} users: - name: kubernetes-admin-cluster.local user: client-certificate-data: REDACTED client-key-data: REDACTED [root@nccztsjb-node-11 ~]# 通过kubectl config view得到的配置可能是来自一个kubeconfig文件,也可能是来自多个kubeconfig文件合并的结果。kubeconfig设置设置集群(1)集群有CA认证,并且将证书的输入嵌入到配置文件中kubectl config --kubeconfig=config-demo set-cluster development --server=https://1.2.3.4 --embed-certs --certificate-authority=ca.crt指定--kubeconfig参数后,所有的配置都会写到对应的文件中。如果不指定--kubeconfig参数那么就会将配置文件写到~/.kube/config这个文件中。--certificate-authority后面是ca证书的路径,需要确保这个证书是存在的。查看config-demo文件的内容[root@nccztsjb-node-11 config-exercise]# cat config-demo apiVersion: v1 clusters: - cluster: certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2VENDQWRHZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQ0FYRFRJeE1USXdNakE0TURjMU5sb1lEekl4TVRFeE1URXhNRGd3TnpVMldqQVZNUk13RVFZRApWUVFERXdwcmRXSmxjbTVsZEdWek1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCnFaWkVNZDhORHpoek5hTE1ab2tCenlabVUzQWh6R1Z4OW15MzJxaTB3UzkzS3Jib0xMUWhSSUhCYnYzdzZCbCsKZlBYaUdSeCt4OVpKY2lta1BqdmJtQ0ZpKzEvQmsxZkFONlNBSi9ISjJoenQxcExZbW41NXVVclpqdmFCTXNUdAp5U3B4QUlOUWNNU1NJYUFyeHF1VDJ0QjYzWkMvTHloQVFWWG4xL2lYS3hLTmpidjVlS21BdlRRTlQrNlMxaFIxCkJNQXNjSFc0R1lhNXVBTFZBTmZrVlpGaG5GMmt2cVMyRzRDMXV4emZtOFpyUnREOExRUjFmTnk1VnpnVkJLQjMKT2RKQkExNnl4YXk5N2JjaEtqenpqTmxKMFBlVDBOdFZZWVRlZ3F1ZG5iRURRSEdObC91Ym5UcGtWWHpFUGRKNAo3a3FQREdWUzRXYi9DZXpVcm9IQ1hRSURBUUFCbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0R3WURWUjBUCkFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVmQldoK3hIdlVSdFdKdUVuUUlYTHRDUzh2MEl3RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFHeHNvSXJOZkJYRkp1RjZwYmtvSkh3eVdCSkp0Qzlud1VabWNCMmYzT0xTZTN4LwpQQU5oYm9LcHZ6ME5ocm5IQ2VnUk1rWGlmSmhKZW9VU25sYUhJaWpjVlFDZ09sTDVnT0EyeFZkRk01MlZCTk5jCnN1Qmp2MHJoQnZXMWxaYWVEUGJuaGp3dGY5elp2WG4vMUkvZ1RKSVRiNmFVdk55b1U3WHY2c3RmN1NjaExwU3kKZmVwRjNKRFc5TXRvem9yMHhoV0U3M0FrZHE1bnE4OFlSdEcxK3UwRTJXbDJ5U20yR0dzRUkvcU1HOXlvd0NsRgozSnhOQm45MEk3V0xKd0pETGZZMXYwWXdRNDR3QUJteGN4R01qVzNmOHAvL3pxSHNaaXJGdTR3QUlUK003Mmw3CkhDbXAwbm84VXpKMUdMUFcxNzF5TWNsdlJNM3h2SENtZHd0VktBMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= server: https://1.2.3.4 name: development contexts: null current-context: "" kind: Config preferences: {} users: null [root@nccztsjb-node-11 config-exercise]# 已经设置好了集群(2)集群没有证书的kubectl config --kubeconfig=config-demo set-cluster development --server=https://1.2.3.4查看证书内容[root@nccztsjb-node-11 config-exercise]# kubectl config --kubeconfig=config-demo set-cluster development --server=https://1.2.3.4 Cluster "development" set. [root@nccztsjb-node-11 config-exercise]# cat config-demo apiVersion: v1 clusters: - cluster: server: https://1.2.3.4 name: development contexts: null current-context: "" kind: Config preferences: {} users: null [root@nccztsjb-node-11 config-exercise](3)集群有证书,但是证书验证的kubectl config --kubeconfig=config-demo set-cluster scratch --server=https://5.6.7.8 --insecure-skip-tls-verify查看证书的内容[root@nccztsjb-node-11 config-exercise]# cat config-demo apiVersion: v1 clusters: - cluster: certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2VENDQWRHZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQ0FYRFRJeE1USXdNakE0TURjMU5sb1lEekl4TVRFeE1URXhNRGd3TnpVMldqQVZNUk13RVFZRApWUVFERXdwcmRXSmxjbTVsZEdWek1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCnFaWkVNZDhORHpoek5hTE1ab2tCenlabVUzQWh6R1Z4OW15MzJxaTB3UzkzS3Jib0xMUWhSSUhCYnYzdzZCbCsKZlBYaUdSeCt4OVpKY2lta1BqdmJtQ0ZpKzEvQmsxZkFONlNBSi9ISjJoenQxcExZbW41NXVVclpqdmFCTXNUdAp5U3B4QUlOUWNNU1NJYUFyeHF1VDJ0QjYzWkMvTHloQVFWWG4xL2lYS3hLTmpidjVlS21BdlRRTlQrNlMxaFIxCkJNQXNjSFc0R1lhNXVBTFZBTmZrVlpGaG5GMmt2cVMyRzRDMXV4emZtOFpyUnREOExRUjFmTnk1VnpnVkJLQjMKT2RKQkExNnl4YXk5N2JjaEtqenpqTmxKMFBlVDBOdFZZWVRlZ3F1ZG5iRURRSEdObC91Ym5UcGtWWHpFUGRKNAo3a3FQREdWUzRXYi9DZXpVcm9IQ1hRSURBUUFCbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0R3WURWUjBUCkFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVmQldoK3hIdlVSdFdKdUVuUUlYTHRDUzh2MEl3RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFHeHNvSXJOZkJYRkp1RjZwYmtvSkh3eVdCSkp0Qzlud1VabWNCMmYzT0xTZTN4LwpQQU5oYm9LcHZ6ME5ocm5IQ2VnUk1rWGlmSmhKZW9VU25sYUhJaWpjVlFDZ09sTDVnT0EyeFZkRk01MlZCTk5jCnN1Qmp2MHJoQnZXMWxaYWVEUGJuaGp3dGY5elp2WG4vMUkvZ1RKSVRiNmFVdk55b1U3WHY2c3RmN1NjaExwU3kKZmVwRjNKRFc5TXRvem9yMHhoV0U3M0FrZHE1bnE4OFlSdEcxK3UwRTJXbDJ5U20yR0dzRUkvcU1HOXlvd0NsRgozSnhOQm45MEk3V0xKd0pETGZZMXYwWXdRNDR3QUJteGN4R01qVzNmOHAvL3pxSHNaaXJGdTR3QUlUK003Mmw3CkhDbXAwbm84VXpKMUdMUFcxNzF5TWNsdlJNM3h2SENtZHd0VktBMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= server: https://1.2.3.4 name: development - cluster: insecure-skip-tls-verify: true server: https://5.6.7.8 name: scratch contexts: null current-context: "" kind: Config preferences: {} users: null [root@nccztsjb-node-11 config-exercise]# (4)删除集群kubectl config --kubeconfig=config-demo unset clusters.development设置用户(1)设置用户,使用客户端的证书和客户端密钥,并且将证书数据嵌入到配置文件中kubectl config --kubeconfig=config-demo set-credentials developer --client-certificate=fake-cert-file --client-key=fake-key-seefile --embed-certs=true注意:客户端证书和key必须要存在。(2)设置用户,使用用户名和密码kubectl config --kubeconfig=config-demo set-credentials experimenter --username=exp --password=some-password(3)删除用户kubectl config --kubeconfig=config-demo unset users.experimenterexperimenter这个就是使用set-credentials参数后面的名字。增加上下文信息contextkubectl config --kubeconfig=config-demo set-context dev-frontend --cluster=development --namespace=frontend --user=developer设置context将集群、命名空间、用户进行分组。即在dev-fronted这个context的以developer用户的信息访问developement集群的frontend命名空间。kubectl config --kubeconfig=config-demo set-context dev-storage --cluster=development --namespace=storage --user=developerdev-stroage这个context访问stroage命名空间。查看通过以上的所有配置形成的kubeconfig文件[root@nccztsjb-node-11 config-exercise]# cat config-demo apiVersion: v1 clusters: - cluster: certificate-authority: fake-ca-file server: https://1.2.3.4 name: development - cluster: insecure-skip-tls-verify: true server: https://5.6.7.8 name: scratch contexts: - context: cluster: development namespace: frontend user: developer name: dev-frontend - context: cluster: development namespace: storage user: developer name: dev-storage - context: cluster: scratch namespace: default user: experimenter name: exp-scratch current-context: "" kind: Config preferences: {} users: - name: developer user: client-certificate: fake-cert-file client-key: fake-key-seefile - name: experimenter user: password: some-password. username: exp设置当前的contextkubectl config --kubeconfig=config-demo use-context dev-frontend查看当前的contextkubectl config --kubeconfig=config-demo current-context只查看和当前context有关的配置信息[root@nccztsjb-node-11 config-exercise]# kubectl config --kubeconfig=config-demo view --minify apiVersion: v1 clusters: - cluster: certificate-authority: fake-ca-file server: https://1.2.3.4 name: development contexts: - context: cluster: development namespace: frontend user: developer name: dev-frontend current-context: dev-frontend kind: Config preferences: {} users: - name: developer user: client-certificate: fake-cert-file client-key: fake-key-seefile那么kubeconfig中的其他的信息就不会显示查看配置中所有的context[root@nccztsjb-node-11 config-exercise]# kubectl config --kubeconfig=config-demo get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE * dev-frontend development developer frontend dev-storage development developer storage exp-scratch scratch experimenter default
概述参考:Helm教程简介Helm 是 k8s 的包管理工具,类似 Linux 系统常用的 apt、yum 等包管理工具。使用 helm 可以简化 k8s 应用部署。每个包称为一个 Chart,一个 Chart 是一个目录(一般情况下会将目录进行打包压缩,形成 name-version.tgz 格式的单一文件,方便传输和存储)。对于应用发布者而言,可以通过 Helm 打包应用,管理应用依赖关系,管理应用版本并发布应用到软件仓库。对于使用者而言,使用 Helm 后不用需要了解 Kubernetes 的 Yaml 语法并编写应用部署文件,可以通过 Helm 下载并在kubernetes上安装需要的应用。除此以外,Helm 还提供了kubernetes上的软件部署,删除,升级,回滚应用的强大功能。<br/>组件及相关术语HelmHelm 是一个命令行下的客户端工具。主要用于 Kubernetes 应用程序 Chart 的创建、打包、发布以及创建和管理本地和远程的 Chart 仓库。TillerTiller 是 Helm 的服务端,部署在 Kubernetes 集群中。Tiller 用于接收 Helm 的请求,并根据 Chart 生成 Kubernetes 的部署文件( Helm 称为 Release ),然后提交给 Kubernetes 创建应用。Tiller 还提供了 Release 的升级、删除、回滚等一系列功能。ChartHelm 的软件包,采用 TAR 格式。其中包含了运行一个应用所需要的镜像、依赖和资源定义(YAML )等,还可能包含 Kubernetes 集群中的服务定义。类似 APT 的 DEB 包或者 YUM 的 RPM 包。RepoistoryHelm 的软件仓库,Repository 本质上是一个 Web 服务器,该服务器保存了一系列的 Chart 软件包以供用户下载,并且提供了一个该 Repository 的 Chart 包的清单文件以供查询。Helm 可以同时管理多个不同的 Repository。首次安装 Helm 时,它已预配置为使用官方 Kubernetes chart 存储库 repo。该 repo 包含许多精心设计和维护的 charts。此 charts repo 默认以 stable 命名。Release使用 helm install 命令在 Kubernetes 集群中部署的 Chart 的一个实例称为 Release。在同一个集群上,一个 Chart 可以安装很多次。每次安装都会创建一个新的 release。例如一个 MySQL Chart,如果想在服务器上运行两个数据库,就可以把这个 Chart 安装两次。每次安装都会生成自己的 Release,会有自己的 Release 名称。注:需要注意的是:Helm 中提到的 Release 和通常概念中的版本有所不同,这里的 Release 可以理解为 Helm 使用 Chart 包部署的一个应用实例。<br/>工作原理Chart Install 过程:Helm 从指定的目录或者 tgz 文件中解析出 Chart 结构信息Helm 将指定的 Chart 结构和 Values 信息通过 gRPC 传递给 TillerTiller 根据 Chart 和 Values 生成一个 ReleaseTiller 将 Release 发送给 Kubernetes 用于生成 ReleaseChart Update 过程:Helm 从指定的目录或者 tgz 文件中解析出 Chart 结构信息Helm 将要更新的 Release 的名称和 Chart 结构,Values 信息传递给 TillerTiller 生成 Release 并更新指定名称的 Release 的 HistoryTiller 将 Release 发送给 Kubernetes 用于更新 ReleaseChart Rollback 过程:Helm 将要回滚的 Release 的名称传递给 TillerTiller 根据 Release 的名称查找 HistoryTiller 从 History 中获取上一个 ReleaseTiller 将上一个 Release 发送给 Kubernetes 用于替换当前 Release<br/>模版语法表达式模版表达式: {{ 模版表达式 }}模版表达式: {{- 模版表达式 -}} , 表示去掉表达式输出结果前面和后面的空格。去掉前面空格可以这么写{{- 模版表达式 }},去掉后面空格 {{ 模版表达式 -}}<br/>变量默认情况,点( . ) 代表全局作用域,用于引用全局对象。例子:# 这里引用了全局作用域下的Values对象中的key属性。 {{ .Values.key }}<br/>helm 全局作用域中有两个重要的全局对象:Values 和 ReleaseValues 代表的就是values.yaml定义的参数,通过 .Values 可以引用任意参数。例子:{{ .Values.replicaCount }} # 引用嵌套对象例子,跟引用json嵌套对象类似 {{ .Values.image.repository }}Release 代表一次应用发布,下面是Release对象包含的属性字段:Release.Name :release的名字,一般通过Chart.yaml定义,或者通过helm命令在安装应用的时候指定。Release.Time :release安装时间Release.Namespace :k8s名字空间Release.Revision :release版本号,是一个递增值,每次更新都会加一Release.IsUpgrade :true代表,当前release是一次更新Release.IsInstall :true代表,当前release是一次安装例子:{{ .Release.Name }}<br/>除了系统自带的变量,也可以自定义模版变量。\# 变量名以 $ 开始命名, 赋值运算符是 := (冒号+等号){{- $relname := .Release.Name -}}引用自定义变量:# 不需要 . 引用 {{ $relname }}<br/>函数&管道运算符(1)调用函数的语法:{{ functionName arg1 arg2... }}例子:# 调用quote函数:将结果用“”引号包括起来。 {{ quote .Values.favorite.food }}<br/>(2)管道(pipelines)运算符 |类似linux shell命令,通过管道 | 将多个命令串起来,处理模版输出的内容。例子:# 将.Values.favorite.food传递给quote函数处理,然后在输出结果。 {{ .Values.favorite.food | quote }} # 先将.Values.favorite.food的值传递给upper函数将字符转换成大写,然后专递给quote加上引号包括起来。 {{ .Values.favorite.food | upper | quote }} # 如果.Values.favorite.food为空,则使用default定义的默认值 {{ .Values.favorite.food | default "默认值" }} # 将.Values.favorite.food输出5次 {{ .Values.favorite.food | repeat 5 }} # 对输出结果缩进2个空格 {{ .Values.favorite.food | nindent 2 }}(3)常用的关系运算符>、 >=、 <、!=、与或非在helm模版中都以函数的形式实现。关系运算函数定义:eq 相当于 = ne 相当于 != lt 相当于 <= gt 相当于 >= and 相当于 && or 相当于 || not 相当于 !例子:# 相当于 if (.Values.fooString && (.Values.fooString == "foo")) {{ if and .Values.fooString (eq .Values.fooString "foo") }} {{ ... }} {{ end }}流程控制语句:IF/ELSE、with、range(1)IF/ELSE语法:{{ if 条件表达式 }} Do something {{ else if 条件表达式 }} Do something else {{ else }} Default case {{ end }}例子:apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap data: myvalue: "Hello World" drink: {{ .Values.favorite.drink | default "tea" | quote }} food: {{ .Values.favorite.food | upper | quote }} {{if eq .Values.favorite.drink "coffee"}} mug: true {{end}}<br/>(2)withwith 主要就是用来修改 . 作用域的,默认 . 代表全局作用域,with 语句可以修改.的含义.语法:{{ with 引用的对象 }} 这里可以使用 . (点), 直接引用with指定的对象 {{ end }}例子:# .Values.favorite是一个object类型 {{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} #相当于.Values.favorite.drink food: {{ .food | upper | quote }} {{- end }}ps:不能在with作用域内使用 . 引用全局对象, 如果非要在with里面引用全局对象,可以先在with外面将全局对象复制给一个变量,然后在with内部使用这个变量引用全局对象。例子:{{- $release:= .Release.Name -}} #先将值保存起来 {{- with .Values.favorite }} drink: {{ .drink | default "tea" | quote }} #相当于.Values.favorite.drink food: {{ .food | upper | quote }} release: {{ $release }} #间接引用全局对象的值 {{- end }}<br/>(3)rangerange 主要用于循环遍历数组类型。语法1:# 遍历map类型,用于遍历键值对象 # 变量key代表对象的属性名,val代表属性值 {{- range key,val := 键值对象 }} {{ $key }}: {{ $val | quote }} {{- end}}语法2:{{- range 数组 }} {{ . | title | quote }} # . (点),引用数组元素值。 {{- end }}例子:# values.yaml定义 # map类型 favorite: drink: coffee food: pizza # 数组类型 pizzaToppings: - mushrooms - cheese - peppers - onions # map类型遍历例子: {{- range $key, $val := .Values.favorite }} {{ $key }}: {{ $val | quote }} {{- end}} # 数组类型遍历例子: {{- range .Values.pizzaToppings}} {{ . | quote }} {{- end}}子模版定义可以在 _(下划线) 开头的文件中定义子模版,方便后续复用。helm create 默认创建了 _helpers.tpl 公共库定义文件,可以直接在里面定义子模版,也可以新建一个,只要以下划线开头命名即可。子模版语法:# 定义模版 {{ define "模版名字" }} {{ end }} # 引用模版 {{ include "模版名字" 作用域}}例子:# 模版定义 {{- define "mychart.app" -}} app_name: {{ .Chart.Name }} app_version: "{{ .Chart.Version }}+{{ .Release.Time.Seconds }}" {{- end -}} apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-configmap labels: {{ include "mychart.app" . | nindent 4 }} #引用mychart.app模版内容,并对输出结果缩进4个空格 data: myvalue: "Hello World"调试编写好 chart 包的模版之后,我们可以给helm命令加上--debug --dry-run 两个参数,让 helm 输出模版结果,但是不把模版输出结果交给k8s处理。例子:# helm install命令类似,加上--debug --dry-run两个参数即可 $ helm upgrade --debug --dry-run -i set replicas=2 --set host=www.xxxx.com myapp ./myapp<br/>常用命令参考:https://www.hellodemos.com/hello-helm-command/helm-command-demos.html多用可选参数-h或--help,查看命令详解chart 实例管理chart 实例的查看、创建、删除# 查看chart实例列表 $ helm list # 查看全部的chart实例(包括删除的) helm list -a # 列出已经删除的Release helm ls --deleted # 查看指定的chart实例信息 $ helm status RELEASE_NAME [flags] # 创建chart实例部署到k8s $ helm install chart包 [flags] # 常用可选参数: --name str # 指定helm实例名称 --namespace str # 指定安装的namespace。不指定则默认命名空间 --version str # 指定charts版本 -f values.yaml # 从文件读取自定义属性集合 --set "key=value" # 通过命令方式注入自定义参数,用于覆盖values.yaml定义的值 # 对象类型数据可以用 . (点)分割属性名。示例: --set apiApp.requests.cpu=1 # 可以使用多个 --set 指定多个参数 helm install --name test --namespace test ./helm-chart # 删除chart实例(注意:若不加purge参数,则并没有彻底删除) $ helm delete RELEASE_NAME [flags] # 选项: --purge # 彻底删除实例,并释放端口 # 示例: helm delete --purge RELEASE_NAME <br/>chart 实例的更新、回滚# 更新chart实例。默认情况下,如果chart实例名不存在,则upgrade会失败 $ helm upgrade [选项] 实例名称 [chart包目录] [flags] # 选项: -i # 实例名存在则更新,不存在时则安装(合并了install和uprade命令功能) --set "key=value" # 通过命令方式注入自定义参数 # 示例:根据chart包更新chart实例 helm upgrade myapp ./myapp helm upgrade -i myapp ./myapp # 示例:使用–set参数更新chart实例 helm upgrade --set replicas=2 --set host=www.xxxx.com myapp # 查看chart实例的版本信息 $ helm history HELM_NAME [flags] # 回滚chart实例的指定版本 $ helm rollback HELM_NAME [REVISION] [flags]<br/>charts 包管理charts 包的搜索、展示、下载# 在Artifact Hub或自己的hub实例中搜索Helm charts $ helm search hub [KEYWORD] [flags] # 搜索系统上配置的所有仓库,并查找匹配 $ helm search repo [keyword] [flags] # 说明: [KEYWORD] # 参数接受关键字字符串或者带引号的查询字符串 # 显示指定chart包(目录、文件或URL)中的Charts.yaml文件内容 $ helm show chart [CHART] [flags] # 显示指定chart(目录、文件或URL)中的README文内容 $ helm show readme [CHART] [flags] # 显示指定chart(目录、文件或URL)包中的values.yaml文件内容 $ helm show values [CHART] [flags] # 显示指定chart(目录、文件或URL)中的所有的内容(values.yaml, Charts.yaml, README) $ helm show all [CHART] [flags] # 从包仓库中检索包并下载到本地 $ helm pull [chart URL | repo/chartname] [...] [flags] # 下载charts到本地 $ helm fetch redis<br/>自定义 charts 包# 创建charts包 $ helm create NAME [flags] # 选项: -p, --starter string # 名称或绝对路径。 # 如果给定目录路径不存在,Helm会自动创建。 # 如果给定目录存在且非空,冲突文件会被覆盖,其他文件会被保留。 # 检查chart语法正确性 $ helm lint myapp # 打包成一个chart版本包文件。 $ helm package [CHART_PATH] [...] [flags] # 查看生成的yaml文件 $ helm template myapp-1.tgz<br/>helm 仓库管理:helm repo仓库的列表、添加、删除、更新和索引语法:$ helm repo COMMAND [flags] 从父命令继承的可选参数: --debug 启用详细输出 --kube-apiserver string Kubernetes-API服务器的地址和端口 --kube-as-group stringArray Kubernetes操作的组,可以重复此标志以指定多个组。 --kube-as-user string Kubernetes操作的用户名 --kube-ca-file string Kubernetes-API服务器连接的认证文件 --kube-context string kubeconfig context的名称 --kube-token string 用于身份验证的字符串令牌 --kubeconfig string kubeconfig文件的路径 -n, --namespace string 该请求的作用域的命名空间名称 --registry-config string 注册表配置文件的路径 (default "~/.config/helm/registry.json") --repository-cache string 包含缓存库索引的文件的路径 (default "~/.cache/helm/repository") --repository-config string 包含仓库名称和url的文件的路径 (default "~/.config/helm/repositories.yaml")常用命令# 查看chart仓库列表 $ helm repo list [flags] # 选项: -o, --output format :打印指定格式的输出。支持的格式:table, json, yaml (default table) # 从chart仓库中更新本地可用chart的信息 $ helm repo update [flags] # 读取当前目录,并根据找到的chart为chart仓库生成索引文件(index.yaml)。 $ helm repo index --merge string # 合并生成的索引到已经存在的索引文件 --url string # chart仓库的绝对URL # 添加chart仓库 $ helm repo add [NAME] [URL] [flags] $ helm repo add myharbor https://harbor.qing.cn/chartrepo/charts --username admin --password password # 删除一个或多个仓库 $ helm repo remove [REPO1 [REPO2 ...]] [flags]<br/>其他命令helm dependency:管理chart依赖# 列举所有的chart中声明的依赖 helm dependency list # 列举指定chart的依赖 helm dependency list CHART [flags]# 查看helm版本 $ helm version # 打印所有Helm使用的客户端环境信息 $ helm env [flags]<br/>基本使用这里以制作一个简单的网站应用chart包为例子介绍helm的基本用法。ps: 这里跳过docker镜像制作过程,镜像制作可以参考:Docker基础教程<br/>1.创建chart包通过 helm create 命令创建一个新的 chart 包例子:# 在当前目录创建一个myapp chart包 $ helm create myapp创建完成后,得到的目录结构如下:myapp - chart 包目录名 ├── charts - 依赖的子包目录,里面可以包含多个依赖的chart包 ├── Chart.yaml - chart定义,可以定义chart的名字,版本号信息 ├── templates - k8s配置模版目录,编写的k8s配置都在这个目录 │ │ 除了NOTES.txt和下划线开头命名的文件,其他文件可以随意命名 │ ├── deployment.yaml │ ├── _helpers.tpl - 下划线开头的文件,helm视为公共库定义文件,主要用于定义通用的子模版、函数等 │ │ helm不会将这些公共库文件的渲染结果提交给k8s处理 │ ├── ingress.yaml │ ├── NOTES.txt - chart包的帮助信息文件,执行helm install命令安装成功后会输出这个文件的内容 │ └── service.yaml └── values.yaml - chart包的参数配置文件,模版可以引用这里参数要在k8s中部署一个网站应用,需要编写 deployment、service、ingress 三个配置文件,刚才通过 helm create 命令已经创建好了。<br/>2.编写k8s应用部署配置文件为了演示chart包模版的用法,先把deployment、service、ingress 三个配置文件的内容清空,重新编写k8s部署文件。deployment.yaml 配置文件定义如下:apiVersion: apps/v1beta2 kind: Deployment metadata: name: myapp #deployment应用名 labels: app: myapp #deployment应用标签定义 spec: replicas: 1 #pod副本数 selector: matchLabels: app: myapp #pod选择器标签 template: metadata: labels: app: myapp #pod标签定义 spec: containers: - name: myapp #容器名 image: xxxxxx:1.7.9 #镜像地址 ports: - name: http containerPort: 80 protocol: TCPservice.yaml 定义如下:apiVersion: v1 kind: Service metadata: name: myapp-svc #服务名 spec: selector: #pod选择器定义 app: myapp ports: - protocol: TCP port: 80 targetPort: 80ingress.yaml 定义如下:apiVersion: extensions/v1beta1 kind: Ingress metadata: name: myapp-ingress #ingress应用名 spec: rules: - host: www.xxxxx.com #域名 http: paths: - path: / backend: serviceName: myapp-svc #服务名 servicePort: 803.提取 k8s 应用部署配置文件中的参数,作为 chart 包参数。定义的 k8s 配置文件还不能称之为模版,都是固定的配置。(这里所说的模版就类似大家平时做前端开发的时候用的模版技术是一个概念)通过提取配置中的参数,注入模版变量,模版表达式将配置文件转化为模版文件,helm在运行的时候根据参数动态的将模版文件渲染成最终的配置文件。下面将 deployment、service、ingress 三个配置文件转换成模版文件。deployment.yaml 配置模版如下:apiVersion: apps/v1beta2 kind: Deployment metadata: name: {{ .Release.Name }} #deployment应用名 labels: app: {{ .Release.Name }} #deployment应用标签定义 spec: replicas: {{ .Values.replicas}} #pod副本数 selector: matchLabels: app: {{ .Release.Name }} #pod选择器标签 template: metadata: labels: app: {{ .Release.Name }} #pod标签定义 spec: containers: - name: {{ .Release.Name }} #容器名 image: {{ .Values.image }}:{{ .Values.imageTag }} #镜像地址 ports: - name: http containerPort: 80 protocol: TCPservice.yaml 定义如下:apiVersion: v1 kind: Service metadata: name: {{ .Release.Name }}-svc #服务名 spec: selector: #pod选择器定义 app: {{ .Release.Name }} ports: - protocol: TCP port: 80 targetPort: 80ingress.yaml 定义如下:apiVersion: extensions/v1beta1 kind: Ingress metadata: name: {{ .Release.Name }}-ingress #ingress应用名 spec: rules: - host: {{ .Values.host }} #域名 http: paths: - path: / backend: serviceName: {{ .Release.Name }}-svc #服务名 servicePort: 80values.yaml chart 包参数定义:#域名 host: www.XXX.com #镜像参数 image: XXXXXXXXXXXXXXXXXX imageTag: 1.7.9 #pod 副本数 replicas:14.通过helm命令安装/更新应用安装应用:$ helm install ./myapp通过命令注入参数$ helm install --set replicas=2 --set host=www.xxxx.com ./myapp更新应用:# 命令格式: helm upgrade release名字 chart包目录 $ helm upgrade myapp ./myapp # 也可以指定–set参数 $ helm upgrade --set replicas=2 --set host=www.xxxx.com myapp ./myapp # 默认情况下,如果release名字不存在,upgrade会失败 # 可以加上-i 参数当release不存在的时候则安装,存在则更新,将install和uprade命令合并 $ helm upgrade -i --set replicas=2 --set host=www.xxxx.com myapp ./myapp
参考:Kubernetes K8S之kubectl命令详解及常用示例Kubectl 常用命令大全kubectl 语法kubectl [command] [TYPE] [NAME] [flags]说明:command:指定在一个或多个资源上要执行的操作。例如:create、get、describe、delete、apply等TYPE:指定资源类型(如:pod、node、services、deployments等)。资源类型大小写敏感,可以指定单数、复数或缩写形式。例如,以下命令生成相同的输出:$ kubectl get pod -n kubernetes-dashboard $ kubectl get pods -n kubernetes-dashboard $ kubectl get po -n kubernetes-dashboardNAME:指定资源的名称。名称大小写敏感。如果省略名称空间,则显示默认名称空间的资源的详细信息或者提示:No resources found in default namespace.。flags:指定可选的命令参数。例如,可以使用 -s 或 --server标识来指定Kubernetes API服务器的地址和端口;-n指定名称空间等。注意:你从命令行指定的flags将覆盖默认值和任何相应的环境变量。优先级最高。在多个资源上执行操作时,可以通过类型 [TYPE] 和名称 [NAME] 指定每个资源,也可以指定一个或多个文件。按类型和名称指定资源:# 查看一个资源类型中的多个资源 [root@k8s-master ~]# kubectl get pod -n kube-system coredns-6955765f44-c9zfh kube-proxy-28dwj NAME READY STATUS RESTARTS AGE coredns-6955765f44-c9zfh 1/1 Running 8 6d7h kube-proxy-28dwj 1/1 Running 9 6d6h # 查看多个资源类型 [root@k8s-master ~]# kubectl get svc,node NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 45h NAME STATUS ROLES AGE VERSION node/k8s-master Ready master 45h v1.17.4 node/k8s-node01 Ready <none> 45h v1.17.4 node/k8s-node02 Ready <none> 45h v1.17.4使用一个或多个文件指定资源:-f file1 -f file2 -f file<#># 使用YAML而不是JSON,因为YAML更容易使用,特别是对于配置文件。 $ kubectl get pod -f pod.yamlTYPE 资源及其缩写别名下表包含常用的资源类型及其缩写别名的列表。也可以在命令行通过kubectl api-resources得到。资源名简称Namespaced资源类deploymentsdeployTRUEDeploymentingressesingTRUEIngressnamespacesnsFALSENamespacenodesnoFALSENodepodspoTRUEPodreplicasetsrsTRUEReplicaSetreplicationcontrollersrcTRUEReplicationControllerservicessvcTRUEServiceapiservices FALSEAPIServicebindings TRUEBindingcertificatesigningrequestscsrFALSECertificateSigningRequestclusterrolebindings FALSEClusterRoleBindingclusterroles FALSEClusterRolecomponentstatusescsFALSEComponentStatusconfigmapscmTRUEConfigMapcontrollerrevisions TRUEControllerRevisioncronjobscjTRUECronJobcsidrivers FALSECSIDrivercsinodes FALSECSINodecustomresourcedefinitionscrd, crdsFALSECustomResourceDefinitiondaemonsetsdsTRUEDaemonSetendpointsepTRUEEndpointsendpointslices TRUEEndpointSliceeventsevTRUEEventhorizontalpodautoscalershpaTRUEHorizontalPodAutoscalerjobs TRUEJobleases TRUELeaselimitrangeslimitsTRUELimitRangelocalsubjectaccessreviews TRUELocalSubjectAccessReviewmutatingwebhookconfigurations FALSEMutatingWebhookConfigurationnetworkpoliciesnetpolTRUENetworkPolicypersistentvolumeclaimspvcTRUEPersistentVolumeClaimpersistentvolumespvFALSEPersistentVolumepoddisruptionbudgetspdbTRUEPodDisruptionBudgetpodsecuritypoliciespspFALSEPodSecurityPolicypodtemplates TRUEPodTemplatepriorityclassespcFALSEPriorityClassresourcequotasquotaTRUEResourceQuotarolebindings TRUERoleBindingroles TRUERoleruntimeclasses FALSERuntimeClasssecrets TRUESecretselfsubjectaccessreviews FALSESelfSubjectAccessReviewselfsubjectrulesreviews FALSESelfSubjectRulesReviewserviceaccountssaTRUEServiceAccountstatefulsetsstsTRUEStatefulSetstorageclassesscFALSEStorageClasssubjectaccessreviews FALSESubjectAccessReviewtokenreviews FALSETokenReviewvalidatingwebhookconfigurations FALSEValidatingWebhookConfigurationvolumeattachments FALSEVolumeAttachmentcommand 操作命令常用操作参数分类pod 相关常用命令# 查看pod列表 $ kubectl get pod [-o wide] [-n NAMESPACE | -A] # -o wide :查看pod运行在哪个节点上以及ip地址信息 # -n NAMESPACE :指定命名空间 # --all-namespaces : 所有命名空间 # --include-uninitialized : 包括未初始化的 # 查看指定pod的信息 $ kubectl get pod POD # 显示pod节点的标签信息 $ kubectl get pod --show-labels # 根据指定标签匹配到具体的pod $ kubectl get pod -l app=example(标签键值对) # 查看某pod的描述 $ kubectl describe pod POD [-n NAMESPACE] # 输出一个单容器pod POD的日志到标准输出 $ kubectl logs podName # 持续输出一个单容器pod POD的日志到标准输出 $ kubectl logs -f podName # 持续输出一个单容器pod POD的日志到标准输出,从最新100行开始 $ kubectl logs -f --tail 100 podName # 指定时间段输出日志 $ kubectl logs podName --since=1h # 指定时间戳输出日志 $ kubectl logs podName --since-time=2018-11-01T15:00:00Z # 查看运行pod的环境变量 $ kubectl exec podName env $ kubectl scale deployment DEPLOYMENT --replicas=8基础命令create/apply:创建资源根据文件或者输入来创建资源# 格式: $ kubectl create -f xxx.yaml # 不建议使用,无法更新,必须先delete # 创建Deployment和Service资源 $ kubectl create -f demo-deployment.yaml $ kubectl create -f demo-service.yaml# 创建+更新,可以重复使用 $ kubectl apply -f xxx.yaml # 应用资源,该目录下的所有 .yaml, .yml, 或 .json 文件都会被使用 $ kubectl apply -f <directory># 创建命名空间 $ kubectl create namespace test-demoget:获得资源信息# 查看默认命名空间中所有的资源信息(pod、service、deployment、replicaset) $ kubectl get all [-A | -n {命名空间名称}] # 查看指定命令空间下面的pod/svc/deployment $ kubectl get pod/svc/deployment [-o wide] -n NAMESPACE # -o wide:选项可以查看存在哪个对应的节点 # 查看所有namespace下面的pod/svc/deployment $ kubectl get pod/svc/deployment --all-namespaces $ kubectl get pod/svc/deployment -A # 查看node节点列表 $ kubectl get node # 显示node节点的标签信息 $ kubectl get node --show-labels # 查看服务的详细信息,显示了服务名称,类型,集群ip,端口,时间等信息 $ kubectl get service # 查看默认命名空间的服务 $ kubectl get svc $ kubectl get svc [-A | -n {命名空间名称}] # 查看命名空间 $ kubectl get namespaces $ kubectl get ns # 查看目前所有的replica set,显示了所有的pod的副本数,以及他们的可用数量以及状态等信息 $ kubectl get rs # 查看已经部署了的所有应用,可以看到容器,以及容器所用的镜像,标签等信息 $ kubectl get deployments $ kubectl get deploy [-o wide]delete:删除资源# 根据yaml文件删除对应的资源,但是yaml文件并不会被删除,这样更加高效 $ kubectl delete -f demo-deployment.yaml $ kubectl delete -f demo-service.yaml # 根据deployment资源来进行删除,会同时删除deployment、pod和service资源 $ kubectl delete deployment DEPLOYMENT [-n NAMESPACE] # 根据label删除 $ kubectl delete pod -l app=APP [-n NAMESPACE] # -l app=APP :使用标签(label),且标签内容为”app=APP“ # 强制立即删除某pod。注:无法删除对应的应用,因为存在deployment/rc之类的副本控制器,删除pod也会重新拉起来 $ kubectl delete pod POD --grace-period=0 --force [-n NAMESPACE] # --force :强制 # --grace-period=0 :延迟0秒。缺省未设定的情况下会等待30s $ kubectl delete pod podName --now [-n NAMESPACE] # 删除所有pod $ kubectl delete pods --allrun:创建并运行容器在集群中创建并运行一个或多个容器语法:run NAME --image=image [--env="key=value"] [--port=port] [--replicas=replicas] [--dry-run=bool] [--overrides=inline-json] [--command] -- [COMMAND] [args...]# 示例:运行一个名称为nginx,副本数为3,标签为app=example,镜像为nginx:1.10,端口为80的容器实例 $ kubectl run nginx --replicas=3 --labels="app=example" --image=nginx:1.10 --port=80 # 示例:运行一个名称为nginx,副本数为3,标签为app=example,镜像为nginx:1.10,端口为80的容器实例,并绑定到k8s-node1上 $ kubectl run nginx --image=nginx:1.10 --replicas=3 --labels="app=example" --port=80 --overrides='{"apiVersion":"apps/v1","spec":{"template":{"spec":{"nodeSelector":{"kubernetes.io/hostname":"k8s-node1"}}}}}'expose:创建service服务创建一个 service 服务,并且暴露端口让外部可以访问。指定 deployment、service、replica set、replication controller 或 pod,并使用该资源的选择器作为指定端口上新服务的选择器。deployment 或 replica set 只有当其选择器可转换为service支持的选择器时,即当选择器仅包含 matchLabels 组件时才会作为暴露新的Service。资源包括(不区分大小写):pod(po),service(svc),replication controller(rc),deployment(deploy),replica set(rs)语法:kubectl expose (-f FILENAME | TYPE NAME) [--port=port] [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--external-ip=external-ip-of-service] [--type=type]# 示例:创建一个nginx服务并且暴露端口让外界可以访问 $ kubectl expose deployment nginx --port=88 --type=NodePort --target-port=80 --name=nginx-service # --port :本地端口 # --target-port :容器端口 # 为RC的nginx创建service,并通过Service的80端口转发至容器的8000端口上 kubectl expose rc nginx --port=80 --target-port=8000 # 由“nginx-controller.yaml”中指定的type和name标识的RC创建Service,并通过Service的80端口转发至容器的8000端口上 kubectl expose -f nginx-controller.yaml --port=80 --target-port=8000set:修改应用的资源配置应用的一些特定资源,也可以修改应用已有的资源使用 kubectl set --help 查看,它的子命令,env,image,resources,selector,serviceaccount,subject语法:resources (-f FILENAME | TYPE NAME) ([--limits=LIMITS & --requests=REQUESTS]set resourceskubectl set resources 命令:这个命令用于设置资源的一些范围限制。资源对象中的Pod可以指定计算资源需求(CPU-单位m、内存-单位Mi),即使用的最小资源请求(Requests),限制(Limits)的最大资源需求,Pod将保证使用在设置的资源数量范围。对于每个Pod资源,如果指定了 Limits(限制)值,并省略了 Requests(请求),则 Requests 默认为 Limits 的值。可用资源对象包括(支持大小写):replicationcontroller、deployment、daemonset、job、replicaset。示例:# 将deployment的nginx容器cpu限制为“200m”,将内存设置为“512Mi” $ kubectl set resources deployment nginx -c=nginx --limits=cpu=200m,memory=512Mi # 设置所有nginx容器中 Requests和Limits $ kubectl set resources deployment nginx --limits=cpu=200m,memory=512Mi --requests=cpu=100m,memory=256Mi # 删除nginx中容器的计算资源值 $ kubectl set resources deployment nginx --limits=cpu=0,memory=0 --requests=cpu=0,memory=0set selectorkubectl set selector 命令设置资源的 selector(选择器)。如果在调用"set selector"命令之前已经存在选择器,则新创建的选择器将覆盖原来的选择器。selector 必须以字母或数字开头,最多包含63个字符,可使用:字母、数字、连字符" - " 、点"."和下划线" _ "。如果指定了--resource-version,则更新将使用此资源版本,否则将使用现有的资源版本。注意:目前 selector 命令只能用于 Service 对象。# 语法: selector (-f FILENAME | TYPE NAME) EXPRESSIONS [--resource-version=version]set imagekubectl set image 命令:用于更新现有资源的容器镜像。可用资源对象包括:pod (po)、replicationcontroller (rc)、deployment (deploy)、daemonset (ds)、job、replicaset (rs)。语法:image (-f FILENAME | TYPE NAME) CONTAINER_NAME_1=CONTAINER_IMAGE_1 ... CONTAINER_NAME_N=CONTAINER_IMAGE_N# 将deployment中的nginx容器镜像设置为“nginx:1.9.1” $ kubectl set image deployment/nginx busybox=busybox nginx=nginx:1.9.1 # 所有deployment和rc的nginx容器镜像更新为“nginx:1.9.1” $ kubectl set image deployments,rc nginx=nginx:1.9.1 --all # 将daemonset abc的所有容器镜像更新为“nginx:1.9.1” $ kubectl set image daemonset abc *=nginx:1.9.1 # 从本地文件中更新nginx容器镜像 $ kubectl set image -f path/to/file.yaml nginx=nginx:1.9.1 --local -o yamlexplain:显示资源文档信息$ kubectl explain rsedit:编辑资源信息# 编辑Deployment nginx的一些信息 $ kubectl edit deployment nginx # 编辑service类型的nginx的一些信息 $ kubectl edit service/nginx设置命令label:更新资源上的标签用于更新(增加、修改或删除)资源上的 label(标签)label 必须以字母或数字开头,可以使用字母、数字、连字符、点和下划线,最长63个字符。如果 --overwrite 为 true,则可以覆盖已有的label,否则尝试覆盖label将会报错。如果指定了--resource-version,则更新将使用此资源版本,否则将使用现有的资源版本。# 语法: label [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]示例:# 给名为foo的Pod添加label unhealthy=true $ kubectl label pods POD名称 unhealthy=true # 给名为foo的Pod修改label 为 'status' / value 'unhealthy',且覆盖现有的value $ kubectl label pods POD名称 status=unhealthy --overwrite # 给 namespace 中的所有 pod 添加 label $ kubectl label pods --all status=unhealthy # 仅当resource-version=1时才更新 名为foo的Pod上的label $ kubectl label pods POD名称 status=unhealthy --resource-version=1 # 删除名为“bar”的label 。(使用“ - ”减号相连) $ kubectl label node NODE名称 bar-annotate:更新注解信息更新一个或多个资源的Annotations信息。也就是注解信息,可以方便的查看做了哪些操作。Annotations由key/value组成。Annotations的目的是存储辅助数据,特别是通过工具和系统扩展操作的数据。如果--overwrite为true,现有的annotations可以被覆盖,否则试图覆盖annotations将会报错。如果设置了--resource-version,则更新将使用此resource version,否则将使用原有的resource version# 语法: annotate [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]示例:# 更新Pod“foo”,设置annotation “description”的value “my frontend”,如果同一个annotation多次设置,则只使用最后设置的value值 $ kubectl annotate pods foo description='my frontend' # 更新Pod"foo",设置annotation“description”的value“my frontend running nginx”,覆盖现有的值 $ kubectl annotate --overwrite pods foo description='my frontend running nginx' # 根据“pod.json”中的type和name更新pod的annotation $ kubectl annotate -f pod.json description='my frontend' # 更新 namespace中的所有pod $ kubectl annotate pods --all description='my frontend running nginx' # 只有当resource-version为1时,才更新pod 'foo' $ kubectl annotate pods foo description='my frontend running nginx' --resource-version=1 # 通过删除名为“description”的annotations来更新pod 'foo'。 # 不需要 -overwrite flag。 $ kubectl annotate pods foo description-completion:设置命令自动补全用于设置 kubectl 命令自动补全# 在 bash 中设置当前 shell 的自动补全,要先安装 bash-completion 包 $ source <(kubectl completion bash) # 在bash shell 中永久的添加自动补全 $ echo "source <(kubectl completion bash)" >> ~/.bashrc # 在 zsh 中设置当前 shell 的自动补全 $ source <(kubectl completion zsh) # 在您的 zsh shell 中永久的添加自动补全 $ echo "if [ $commands[kubectl] ]; then source <(kubectl completion zsh); fi" >> ~/.zshrc 部署命令rollout:管理资源用于对资源进行管理可用资源包括:deployments,daemonsets。子命令:history(查看历史版本)pause(暂停资源)resume(恢复暂停资源)status(查看资源状态)undo(回滚版本)# 语法 kubectl rollout SUBCOMMAND # 回滚到之前的deployment $ kubectl rollout undo deployment DEPLOYMENT # 查看daemonet的状态 $ kubectl rollout status daemonset NAME # 查看历史版本 $ kubectl rollout history deployment DEPLOYMENTrolling-update:滚动更新资源执行指定ReplicationController的滚动更新。该命令创建了一个新的RC, 然后一次更新一个pod方式逐步使用新的PodTemplate,最终实现Pod滚动更新,new-controller.json需要与之前RC在相同的namespace下。语法:rolling-update OLD_CONTROLLER_NAME ([NEW_CONTROLLER_NAME] --image=NEW_CONTAINER_IMAGE | -f NEW_CONTROLLER_SPEC)# 使用frontend-v2.json中的新RC数据更新frontend-v1的pod $ kubectl rolling-update frontend-v1 -f frontend-v2.json # 使用JSON数据更新frontend-v1的pod $ cat frontend-v2.json | kubectl rolling-update frontend-v1 -f - # 基于 stdin 输入的 JSON 替换 pod $ cat pod.json | kubectl replace -f - # 更新frontend资源的镜像 $ kubectl rolling-update frontend --image=image:v2 # 更新frontend-v1资源名称为frontend-v2并更新镜像 $ kubectl rolling-update frontend-v1 frontend-v2 --image=image:v2 # 退出已存在的进行中的滚动更新 $ kubectl rolling-update frontend-v1 frontend-v2 --rollbackscale:扩容或缩容(pod数)扩容或缩容 Deployment、ReplicaSet、Replication Controller或 Job 中Pod数量scale也可以指定多个前提条件,如:当前副本数量或 --resource-version ,进行伸缩比例设置前,系统会先验证前提条件是否成立。这个就是弹性伸缩策略。语法:kubectl scale [--resource-version=version] [--current-replicas=count] --replicas=COUNT (-f FILENAME | TYPE NAME)# 将名为foo中的pod副本数设置为3。 $ kubectl scale --replicas=3 rs/foo kubectl scale deploy/nginx --replicas=30 # 将由“foo.yaml”配置文件中指定的资源对象和名称标识的Pod资源副本设为3 $ kubectl scale --replicas=3 -f foo.yaml # 如果当前副本数为2,则将其扩展至3。 $ kubectl scale --current-replicas=2 --replicas=3 deployment/mysql # 设置多个RC中Pod副本数量 $ kubectl scale --replicas=5 rc/foo rc/bar rc/bazautoscale:自动扩容或缩容这个比scale更加强大,也是弹性伸缩策略 ,它是根据流量的多少来自动进行扩展或者缩容。指定Deployment、ReplicaSet或ReplicationController,并创建已经定义好资源的自动伸缩器。使用自动伸缩器可以根据需要自动增加或减少系统中部署的pod数量。语法:kubectl autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS] --max=MAXPODS [--cpu-percent=CPU] [flags]# 使用 Deployment “foo”设定,使用默认的自动伸缩策略,指定目标CPU使用率,使其Pod数量在2到10之间 $ kubectl autoscale deployment foo --min=2 --max=10 # 使用RC“foo”设定,使其Pod的数量介于1和5之间,CPU使用率维持在80% $ kubectl autoscale rc foo --max=5 --cpu-percent=80集群管理命令:certificate,cluster-info,top,cordon,uncordon,drain,taint### certificate命令:用于证书资源管理,授权等 # 当有node节点要向master请求,那么是需要master节点授权的 $ kubectl certificate approve node-csr-81F5uBehyEyLWco5qavBsxc1GzFcZk3aFM3XW5rT3mw node-csr-Ed0kbFhc_q7qx14H3QpqLIUs0uKo036O2SnFpIheM18 # 查看集群信息 $ kubectl cluster-info # 将当前集群状态输出到 stdout $ kubectl cluster-info dump # 将当前集群状态输出到 /path/to/cluster-state $ kubectl cluster-info dump --output-directory=/path/to/cluster-state # 检测核心组件状态【如果你的集群运行不正常,这是一个很好的、进行第一次诊断检查的机会】 $ kubectl get componentstatuses $ kubectl get cs # 查看node/pod的资源使用情况。需要heapster 或metrics-server支持 $ kubectl top node/pod -A # 查看日志: $ journalctl -u kubelet -f # 设为不可调度状态: $ kubectl cordon node1 # 解除不可调度状态 $ kubectl uncordon node1 # 将pod从NODE-1节点赶到其他节点: $ kubectl drain NODE-1 ### taint命令:用于给某个Node节点设置污点 # master运行pod $ kubectl taint nodes master.k8s node-role.kubernetes.io/master- # master不运行pod $ kubectl taint nodes master.k8s node-role.kubernetes.io/master=:NoSchedule # 查看kubelet进程启动参数 $ ps -ef | grep kubelet集群故障排查和调试命令describe:显示特定资源的详细信息# 查看my-nginx pod的详细状态 $ kubectl describe po my-nginxlogs:打印日志logs命令:用于在一个pod中打印一个容器的日志,如果pod中只有一个容器,可以省略容器名语法:$ kubectl logs [-f] [-p] POD [-c CONTAINER]参数选项:-c, --container="":容器名。-f, --follow[=false]:指定是否持续输出日志(实时日志)。--interactive[=true]:如果为true,当需要时提示用户进行输入。默认为true。--limit-bytes=0:输出日志的最大字节数。默认无限制。-p, --previous[=false]:如果为true,输出pod中曾经运行过,但目前已终止的容器的日志。--since=0:仅返回相对时间范围,如5s、2m或3h,之内的日志。默认返回所有日志。只能同时使用since和since-time中的一种。--since-time="":仅返回指定时间(RFC3339格式)之后的日志。默认返回所有日志。只能同时使用since和since-time中的一种。--tail=-1:要显示的最新的日志条数。默认为-1,显示所有的日志。--timestamps[=false]:在日志中包含时间戳。# 返回仅包含一个容器的pod nginx的日志快照 $ kubectl logs nginx # 持续输出pod MyPod中的日志 $ kubectl logs -f MyPod # 持续输出pod MyPod中的容器web-1的日志 $ kubectl logs -f MyPod -c web-1 # 仅输出pod nginx中最近的20条日志 $ kubectl logs --tail=20 nginx # 输出pod nginx中最近一小时内产生的所有日志 $ kubectl logs --since=1h nginx # 指定时间戳输出日志 $ kubectl logs MyPod --since-time=2018-11-01T15:00:00Z # 返回pod MyPod中已经停止的容器web-1的日志快照 $ kubectl logs -p MyPod -c web-1exec:进入容器进行交互exec命令:进入容器进行交互,在容器中执行命令语法:kubectl exec POD [-c CONTAINER] -- COMMAND [args...]命令选项:-c, --container="" 容器名。如果省略,则默认选择第一个 pod。 -p, --pod="" Pod名。 -i, --stdin[=false] 将控制台输入发送到容器。 -t, --tty[=false] 将标准输入控制台作为容器的控制台输入。参考实例:# 在pod 123456-7890的容器ruby-container中运行“date”并获取输出 kubectl exec 123456-7890 -c ruby-container date # 默认在pod 123456-7890的第一个容器中运行“date”并获取输出 kubectl exec 123456-7890 -- date # 切换到终端模式,将控制台输入发送到pod 123456-7890的ruby-container的“bash”命令,并将其输出到控制台/ # 错误控制台的信息发送回客户端。 kubectl exec 123456-7890 -c ruby-container -it -- bash -ilattach:连接到一个正在运行的容器attach命令:连接到一个正在运行的容器。语法:kubectl attach POD [-c CONTAINER]参数选项:-c, --container="": 容器名。如果省略,则默认选择第一个 pod。-i, --stdin[=false]: 将控制台输入发送到容器。-t, --tty[=false]: 将标准输入控制台作为容器的控制台输入。# 获取正在运行中的pod 123456-7890的输出,默认连接到第一个容器 $ kubectl attach 123456-7890 # 获取pod 123456-7890中ruby-container的输出 $ kubectl attach 123456-7890 -c ruby-container # 切换到终端模式,将控制台输入发送到pod 123456-7890的ruby-container的“bash”命令,并将其输出到控制台 $ kubectl attach 123456-7890 -c ruby-container -i -t其他命令:api-servions,config,help,plugin,versionapi-servions命令:打印受支持的api版本信息# 打印当前集群支持的api版本 $ kubectl api-versionshelp命令:用于查看命令帮助# 显示全部的命令帮助提示 $ kubectl --help # 具体的子命令帮助,例如 $ kubectl create --helpconfig 命令:用于修改kubeconfig配置文件(用于访问api,例如配置认证信息)设置 kubectl 与哪个 Kubernetes 集群进行通信并修改配置信息。查看 使用 kubeconfig 跨集群授权访问 文档获取详情配置文件信息。# 显示合并的 kubeconfig 配置 $ kubectl config view # 查看配置的摘要(上下文、命名空间) $ kubectl config get-contexts # 同时使用多个 kubeconfig 文件并查看合并的配置 $ KUBECONFIG=~/.kube/config:~/.kube/kubconfig2 kubectl config view # 获取 e2e 用户的密码 $ kubectl config view -o jsonpath='{.users[?(@.name == "e2e")].user.password}' # 查看当前使用的上下文 $ kubectl config current-context # 设置默认的上下文 $ kubectl config use-context 上下文名称 # 使用特定的用户名和命名空间新增一个上下文,并设置为默认的上下文。 $ kubectl config set-context 上下文名称 \ --user=cluster-admin \ --namespace=NAMESPACE \ && kubectl config use-context 上下文名称 # 更改当前上下文的命名空间 $ kubectl config set-context --current --namespace=NAMESPACE $ kubectl config set-context $(kubectl config current-context) --namespace=NAMESPACE # 恢复到默认命名空间 $ kubectl config set-context $(kubectl config current-context) --namespace= # 删除指定的上下文 $ kubectl config delete-context CLUSTER # 添加新的集群配置到 kubeconf 中,使用 basic auth 进行鉴权 $ kubectl config set-credentials kubeuser/foo.kubernetes.com --username=kubeuser --password=kubepasswordversion 命令:打印客户端和服务端版本信息# 打印客户端和服务端版本信息 $ kubectl versionplugin 命令:运行一个命令行插件高级命令:apply,patch,replace,convertapply命令:通过文件名或者标准输入对资源应用配置通过文件名或控制台输入,对资源进行配置。 如果资源不存在,将会新建一个。可以使用 JSON 或者 YAML 格式。语法:kubectl apply -f FILENAME参数选项:-f, --filename=[]:包含配置信息的文件名,目录名或者URL。--include-extended-apis[=true]:如果为true,则通过调用API服务器来包含新API的定义。默认为true-o, --output="":输出模式。"-o name"为快捷输出(资源/name).--record[=false]:在资源注释中记录当前 kubectl 命令。-R, --recursive[=false]:递归处理 -f, --filename 中使用的目录。当您希望管理组织在同一目录中的相关清单时很有用。--schema-cache-dir="~/.kube/schema":非空则将API schema缓存为指定文件,默认缓存到'$HOME/.kube/schema'--validate[=true]:如果为true,在发送到服务端前先使用schema来验证输入。# 将pod.yaml中的配置应用到pod $ kubectl apply -f ./pod.yaml # 将控制台输入的Yaml配置应用到Pod $ cat pod.yaml | kubectl apply -f -patch命令:使用补丁修改,更新资源的字段,也就是修改资源的部分内容语法:kubectl patch (-f FILENAME | TYPE NAME) -p PATCH# 使用策略合并补丁部分更新节点 $ kubectl patch node k8s-node-1 -p '{"spec":{"unschedulable":true}}' # 更新一个容器的镜像;Spec.containers.name是必需的,因为它是一个合并键 $ kubectl patch pod valid-pod -p '{"spec":{"containers":[{"name":"kubernetes-serve-hostname","image":"new image"}]}}'replace命令: 通过文件或者标准输入替换原有资源语法:kubectl replace -f FILENAME# 使用pod.yaml中的数据替换一个pod $ kubectl replace -f ./pod.yaml # 根据传递到标准输入的YAML替换一个pod。 $ cat pod.yaml | kubectl replace -f - # 将单容器pod的镜像的版本(标记)更新为v4 $ kubectl get pod mypod -o yaml | sed 's/\(image: myimage\):.*$/\1:v4/' | kubectl replace -f - # 强制替换、删除然后重新创建资源 $ kubectl replace --force -f ./pod.yamlconvert命令:转换配置文件为不同的API版本,支持YAML和JSON格式。该命令将配置文件名,目录或URL作为输入,并将其转换为指定的版本格式,如果目标版本未指定或不支持,则转换为最新版本。默认输出将以YAML格式打印出来,可以使用- o选项改变输出格式。语法:kubectl convert -f FILENAME# 将文件'pod.yaml'转换为最近版本并打印到标准输出 $ kubectl convert -f pod.yaml # 将“pod.yaml”指定的资源的实时状态转换为最新版本,并以json格式打印到标准输出 $ kubectl convert -f pod.yaml --local -o yaml # 将当前目录下的所有文件转换为最新版本,并将其全部创建 $ kubectl convert -f . | kubectl create -f -输出选项格式化输出所有kubectl命令的默认输出格式是人类可读的纯文本格式。要将详细信息以特定的格式输出到终端窗口,可以将 -o 或 --output标识添加到受支持的kubectl命令中。语法kubectl [command] [TYPE] [NAME] -o <output_format>根据kubectl操作,支持以下输出格式:输出格式描述-o custom-columns=使用逗号分隔的自定义列列表打印表-o custom-columns-file=使用文件中的自定义列模板打印表-o json输出一个JSON格式的API对象-o jsonpath=打印jsonpath表达式中定义的字段-o jsonpath-file=通过文件打印jsonpath表达式定义的字段-o name只打印资源名,不打印其他任何内容-o wide以纯文本格式输出,包含附加信息。对于pods,包含节点名-o yaml输出一个YAML格式的API对象导出配置文件# 导出proxy的配置为yaml文件 kubectl get ds -n kube-system -l k8s-app=kube-proxy -o yaml > kube-proxy-ds.yaml # 导出kube-dns的deployment和services的配置为yaml文件 kubectl get deployment -n kube-system -l k8s-app=kube-dns -o yaml > kube-dns-dp.yaml kubectl get services -n kube-system -l k8s-app=kube-dns -o yaml > kube-dns-services.yaml # 导出所有 configmap kubectl get configmap -n kube-system -o wide -o yaml > configmap.yaml
K8S中的 yaml 文件yaml语法学习Kubernetes 支持 YAML 和 JSON格式 管理资源对象JSON 格式:主要用于 api 接口之间消息的传递YAML 格式:用于配置和管理,YAML是一种简洁的非标记性语言,内容格式人性化,较易读。YAML语法格式:大小写敏感;使用缩进表示层级关系;不支持Tab键制表符缩进,只使用空格缩进;缩进的空格数目不重要,只要相同层级的元素左侧对齐即可,通常开头缩进两个空格;字符后缩进一个空格,如冒号,逗号,短横杆(-) 等"---" 表示YAML格式,一个文件的开始,用于分隔文件; 可以将创建多个资源写在同一个 yaml 文件中,用 --- 隔开,就不用写多个 yaml 文件了。"#” 表示注释;yaml 文件的学习方法:多看别人(官方)写的,能读懂能照着现场的文件改着用遇到不懂的,善用 kubectl explain ...命令查.deployment资源简述Deployment 为 Pod 和 ReplicaSet 提供了一个声明式定义 (declarative) 方法,用来替代以前的 ReplicationController 更方便的管理应用。作为最常用的 Kubernetes 对象,Deployment 经常会用来创建 ReplicaSet 和 Pod,我们往往不会直接在集群中使用 ReplicaSet 部署一个新的微服务,一方面是因为 ReplicaSet 的功能其实不够强大,一些常见的更新、扩容和缩容运维操作都不支持,Deployment 的引入就是为了支持这些复杂的操作。典型用例如下:使用 Deployment 来创建 ReplicaSet。ReplicaSet 在后台创建 pod。检查启动状态,看它是成功还是失败。然后,通过更新 Deployment 的 PodTemplateSpec 字段来声明 Pod 的新状态。这会创建一个新的 ReplicaSet,Deployment 会按照控制的速率将 pod 从旧的 ReplicaSet 移动到新的 ReplicaSet 中。如果当前状态不稳定,回滚到之前的 Deployment revision。每次回滚都会更新 Deployment 的 revision。扩容 Deployment 以满足更高的负载。暂停 Deployment 来应用 PodTemplateSpec 的多个修复,然后恢复上线。根据 Deployment 的状态判断上线是否 hang 住了。清除旧的不必要的 ReplicaSet。deployment 原理控制器模型在Kubernetes架构中,有一个叫做kube-controller-manager的组件。这个组件,是一系列控制器的集合。其中每一个控制器,都以独有的方式负责某种编排功能。而Deployment正是这些控制器中的一种。它们都遵循Kubernetes中一个通用的编排模式,即:控制循环用一段go语言伪代码,描述这个控制循环for { 实际状态 := 获取集群中对象X的实际状态 期望状态 := 获取集群中对象X的期望状态 if 实际状态 == 期望状态 { 什么都不做 }else{ 执行编排动作,将实际状态调整为期望状态 }在具体实现中,实际状态往往来自于Kubernetes集群本身。比如Kubelet通过心跳汇报的容器状态和节点状态,或者监控系统中保存的应用监控数据,或者控制器主动收集的它感兴趣的信息,这些都是常见的实际状态的来源;期望状态一般来自用户提交的YAML文件,这些信息都保存在Etcd中对于Deployment,它的控制器简单实现如下:Deployment Controller从Etcd中获取到所有携带 “app:nginx”标签的Pod,然后统计它们的数量,这就是实际状态Deployment对象的replicas的值就是期望状态Deployment Controller将两个状态做比较,然后根据比较结果,确定是创建Pod,还是删除已有Pod滚动更新Deployment滚动更新的实现,依赖的是Kubernetes中的ReplicaSetDeployment控制器实际操纵的,就是Replicas对象,而不是Pod对象。对于Deployment、ReplicaSet、Pod它们的关系如下图:ReplicaSet负责通过“控制器模式”,保证系统中Pod的个数永远等于指定的个数。这也正是Deployment只允许容器的restartPolicy=Always的主要原因:只有容器能保证自己始终是running状态的前提下,ReplicaSet调整Pod的个数才有意义。Deployment同样通过控制器模式,操作ReplicaSet的个数和属性,进而实现“水平扩展/收缩”和“滚动更新”两个编排动作对于“水平扩展/收缩”的实现,Deployment Controller只需要修改replicas的值即可。用户执行这个操作的指令如下:kubectl scale deployment nginx-deployment --replicas=4Deployment.yaml 文件解析Deployment yaml文件包含四个部分:apiVersion: 表示版本。版本查看命令:kubectl api-versionskind: 表示资源metadata: 表示元信息spec: 资源规范字段Deployment yaml 详解:apiVersion: apps/v1 # 指定api版本,此值必须在kubectl api-versions中。业务场景一般首选”apps/v1“ kind: Deployment # 指定创建资源的角色/类型 metadata: # 资源的元数据/属性 name: demo # 资源的名字,在同一个namespace中必须唯一 namespace: default # 部署在哪个namespace中。不指定时默认为default命名空间 labels: # 自定义资源的标签 app: demo version: stable annotations: # 自定义注释列表 name: string spec: # 资源规范字段,定义deployment资源需要的参数属性,诸如是否在容器失败时重新启动容器的属性 replicas: 1 # 声明副本数目 revisionHistoryLimit: 3 # 保留历史版本 selector: # 标签选择器 matchLabels: # 匹配标签,需与上面的标签定义的app保持一致 app: demo version: stable strategy: # 策略 type: RollingUpdate # 滚动更新策略 # ecreate:删除所有已存在的pod,重新创建新的 # RollingUpdate:滚动升级,逐步替换的策略,同时滚动升级时,支持更多的附加参数, # 例如设置最大不可用pod数量,最小升级间隔时间等等 rollingUpdate: # 滚动更新 maxSurge: 1 # 滚动升级时最大额外可以存在的副本数,可以为百分比,也可以为整数 maxUnavailable: 0 # 在更新过程中进入不可用状态的 Pod 的最大值,可以为百分比,也可以为整数 template: # 定义业务模板,如果有多个副本,所有副本的属性会按照模板的相关配置进行匹配 metadata: # 资源的元数据/属性 annotations: # 自定义注解列表 sidecar.istio.io/inject: "false" # 自定义注解名字 labels: # 自定义资源的标签 app: demo # 模板名称必填 version: stable spec: # 资源规范字段 restartPolicy: Always # Pod的重启策略。[Always | OnFailure | Nerver] # Always :在任何情况下,只要容器不在运行状态,就自动重启容器。默认 # OnFailure :只在容器异常时才自动容器容器。 # 对于包含多个容器的pod,只有它里面所有的容器都进入异常状态后,pod才会进入Failed状态 # Nerver :从来不重启容器 nodeSelector: # 将该Pod调度到包含这个label的node上,仅能基于简单的等值关系定义标签选择器 caas_cluster: work-node containers: # Pod中容器列表 - name: demo # 容器的名字 image: demo:v1 # 容器使用的镜像地址 imagePullPolicy: IfNotPresent # 每次Pod启动拉取镜像策略 # IfNotPresent :如果本地有就不检查,如果没有就拉取。默认 # Always : 每次都检查 # Never : 每次都不检查(不管本地是否有) command: [string] # 容器的启动命令列表,如不指定,使用打包时使用的启动命令 args: [string] # 容器的启动命令参数列表 # 如果command和args均没有写,那么用Docker默认的配置 # 如果command写了,但args没有写,那么Docker默认的配置会被忽略而且仅仅执行.yaml文件的command(不带任何参数的) # 如果command没写,但args写了,那么Docker默认配置的ENTRYPOINT的命令行会被执行,但是调用的参数是.yaml中的args # 如果如果command和args都写了,那么Docker默认的配置被忽略,使用.yaml的配置 workingDir: string # 容器的工作目录 volumeMounts: # 挂载到容器内部的存储卷配置 - name: string # 引用pod定义的共享存储卷的名称,需用volumes[]部分定义的的卷名 mountPath: string # 存储卷在容器内mount的绝对路径,应少于512字符 readOnly: boolean # 是否为只读模式 - name: string configMap: # 类型为configMap的存储卷,挂载预定义的configMap对象到容器内部 name: string items: - key: string path: string ports: # 需要暴露的端口库号列表 - name: http # 端口号名称 containerPort: 8080 # 容器开放对外的端口 # hostPort: 8080 # 容器所在主机需要监听的端口号,默认与Container相同 protocol: TCP # 端口协议,支持TCP和UDP,默认TCP env: # 容器运行前需设置的环境变量列表 - name: string # 环境变量名称 value: string # 环境变量的值 resources: # 资源管理。资源限制和请求的设置 limits: # 资源限制的设置,最大使用 cpu: "1" # CPU,"1"(1核心) = 1000m。将用于docker run --cpu-shares参数 memory: 500Mi # 内存,1G = 1024Mi。将用于docker run --memory参数 requests: # 资源请求的设置。容器运行时,最低资源需求,也就是说最少需要多少资源容器才能正常运行 cpu: 100m memory: 100Mi livenessProbe: # pod内部的容器的健康检查的设置。当探测无响应几次后将自动重启该容器 # 检查方法有exec、httpGet和tcpSocket,对一个容器只需设置其中一种方法即可 httpGet: # 通过httpget检查健康,返回200-399之间,则认为容器正常 path: /healthCheck # URI地址。如果没有心跳检测接口就为/ port: 8089 # 端口 scheme: HTTP # 协议 # host: 127.0.0.1 # 主机地址 # 也可以用这两种方法进行pod内容器的健康检查 # exec: # 在容器内执行任意命令,并检查命令退出状态码,如果状态码为0,则探测成功,否则探测失败容器重启 # command: # - cat # - /tmp/health # 也可以用这种方法 # tcpSocket: # 对Pod内容器健康检查方式设置为tcpSocket方式 # port: number initialDelaySeconds: 30 # 容器启动完成后首次探测的时间,单位为秒 timeoutSeconds: 5 # 对容器健康检查等待响应的超时时间,单位秒,默认1秒 periodSeconds: 30 # 对容器监控检查的定期探测间隔时间设置,单位秒,默认10秒一次 successThreshold: 1 # 成功门槛 failureThreshold: 5 # 失败门槛,连接失败5次,pod杀掉,重启一个新的pod readinessProbe: # Pod准备服务健康检查设置 httpGet: path: /healthCheck # 如果没有心跳检测接口就为/ port: 8089 scheme: HTTP initialDelaySeconds: 30 timeoutSeconds: 5 periodSeconds: 10 successThreshold: 1 failureThreshold: 5 lifecycle: # 生命周期管理 postStart: # 容器运行之前运行的任务 exec: command: - 'sh' - 'yum upgrade -y' preStop: # 容器关闭之前运行的任务 exec: command: ['service httpd stop'] initContainers: # 初始化容器 - command: - sleep 10; mkdir /wls/logs/nacos-0 image: {{ .Values.busyboxImage }} imagePullPolicy: IfNotPresent name: init volumeMounts: - mountPath: /wls/logs/ name: logs volumes: - name: logs hostPath: path: {{ .Values.nfsPath }}/logs volumes: # 在该pod上定义共享存储卷列表 - name: string # 共享存储卷名称 (volumes类型有很多种) emptyDir: {} # 类型为emtyDir的存储卷,与Pod同生命周期的一个临时目录。为空值 - name: string hostPath: # 类型为hostPath的存储卷,表示挂载Pod所在宿主机的目录 path: string # Pod所在宿主机的目录,将被用于同期中mount的目录 - name: string secret: # 类型为secret的存储卷,挂载集群与定义的secre对象到容器内部 scretname: string items: - key: string path: string imagePullSecrets: # 镜像仓库拉取镜像时使用的密钥,以key:secretkey格式指定 - name: harbor-certification hostNetwork: false # 是否使用主机网络模式,默认为false,如果设置为true,表示使用宿主机网络 terminationGracePeriodSeconds: 30 # 优雅关闭时间,这个时间内优雅关闭未结束,k8s 强制 kill dnsPolicy: ClusterFirst # 设置Pod的DNS的策略。默认ClusterFirst # 支持的策略:[Default | ClusterFirst | ClusterFirstWithHostNet | None] # Default : Pod继承所在宿主机的设置,也就是直接将宿主机的/etc/resolv.conf内容挂载到容器中 # ClusterFirst : 默认的配置,所有请求会优先在集群所在域查询,如果没有才会转发到上游DNS # ClusterFirstWithHostNet : 和ClusterFirst一样,不过是Pod运行在hostNetwork:true的情况下强制指定的 # None : 1.9版本引入的一个新值,这个配置忽略所有配置,以Pod的dnsConfig字段为准 affinity: # 亲和性调试 nodeAffinity: # 节点亲和性。满足特定条件的pod对象运行在同一个node上。效果同nodeSelector,但功能更强大 requiredDuringSchedulingIgnoredDuringExecution: # 硬亲和性 nodeSelectorTerms: # 节点满足任何一个条件就可以 - matchExpressions: # 有多个选项时,则只有同时满足这些逻辑选项的节点才能运行 pod - key: beta.kubernetes.io/arch operator: In values: - amd64 podAffinity: # pod亲和性。满足特定条件的pod对象运行在同一个node上 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - nginx topologyKey: kubernetes.io/hostname podAntiAffinity: # pod反亲和性。满足特定条件(相同pod标签)的pod对象不能运行在同一个node上 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - nginx topologyKey: kubernetes.io/hostname # 反亲和性的节点标签 key tolerations: # 污点容忍度。允许pod可以运行在匹配的污点上 - operator: "Equal" # 匹配类型。支持[Exists | Equal(默认值)]。Exists为容忍所有污点 key: "key1" value: "value1" effect: "NoSchedule" # 污点类型:[NoSchedule | PreferNoSchedule | NoExecute] # NoSchedule :不会被调度 # PreferNoSchedule:尽量不调度 # NoExecute:驱逐节点Deployment.yaml 配置项说明livenessProbe:存活指针用于判断 Pod(中的应用容器)是否健康,可以理解为健康检查。使用 livenessProbe 来定期的去探测,如果探测成功,则 Pod 状态可以判定为 Running;如果探测失败,可kubectl会根据Pod的重启策略来重启容器。如果未给Pod设置 livenessProbe,则默认探针永远返回 Success。执行 kubectl get pods 命令,输出信息中 STATUS 一列可以看到Pod是否处于Running状态。livenessProbe使用场景:有些后端应用在出现某些异常的时候会有假死的情况,这种情况容器依然是running状态,但是应用是无法访问的,所以需要加入存活探测livenessProbe来避免这种情况的发生。参考:https://blog.csdn.net/qq_41980563/article/details/122139923readinessProbe:就绪指针就绪的意思是已经准备好了,Pod 的就绪可以理解为这个 Pod 可以接受请求和访问。使用 readinessProbe 来定期的去探测,如果探测成功,则 Pod 的 Ready 状态判定为 True;如果探测失败,Pod 的 Ready 状态判定为 False。与 livenessProbe 不同的是,kubelet 不会对 readinessProbe 的探测情况有重启操作。当执行 kubectl get pods 命令,输出信息中 READY 一列可以看到 Pod 的 READY 状态是否为 True。readinessProbe 使用场景:k8s 应用更新虽然是滚动升级方式,但是很多后端程序启动都比较久,容器起来了,但是服务未起来,而 k8s 只要容器起来了就会移除掉旧的容器,这种情况就会导致在更新发版的时候应用访问失败。这时候就需要配置 readinessProbe 就绪检测,保证新的 pod 已经能正常使用了才会移除掉旧的 pod。initContainers:初始化容器用于主容器启动时先启动可一个或多个初始化容器,如果有多个,那么这几个 Init Container 按照定义的顺序依次执行,只有所有的 Init Container 执行完后,主容器才会启动。由于一个 Pod 里的存储卷是共享的,所以 Init Container 里产生的数据可以被主容器使用到。Init Container 可以在多种K8S资源里被使用到,如 Deployment、Daemon Set、Pet Set、Job 等,但归根结底都是在 Pod 启动时,在主容器启动前执行,做初始化工作。tolerations:污点容忍度节点污点:类似节点上的标签或注解信息,用来描述对应节点的元数据信息;污点定义的格式和标签、注解的定义方式很类似,都是用一个 key-value 数据来表示,不同于节点标签,污点的键值数据中包含对应污点的 effect,污点的 effect 是用于描述对应节点上的污点有什么作用;在 k8s 上污点有三种 effect(效用)策略,第一种策略是 NoSchedule,表示拒绝 pod 调度到对应节点上运行;第二种策略是 PreferSchedule,表示尽量不把 pod 调度到此节点上运行;第三种策略是 NoExecute,表示拒绝将 pod 调度到此节点上运行;该效用相比 NoSchedule 要严苛一点;从上面的描述来看,对应污点就是来描述拒绝pod运行在对应节点的节点属性。pod 对节点污点的容忍度:pod 要想运行在对应有污点的节点上,对应 pod 就要容忍对应节点上的污点;pod 对节点污点的容忍度就是在对应 pod 中定义怎么去匹配节点污点;通常匹配节点污点的方式有两种,一种是等值(Equal)匹配,一种是存在性(Exists)匹配;等值匹配表示对应pod的污点容忍度,必须和节点上的污点属性相等,所谓污点属性是指污点的 key、value 以及 effect;即容忍度必须满足和对应污点的key、value 和 effect 相同,这样表示等值匹配关系,其操作符为 Equal;存在性匹配是指对应容忍度只需要匹配污点的 key 和 effect 即可,value 不纳入匹配标准,即容忍度只要满足和对应污点的 key 和 effect 相同就表示对应容忍度和节点污点是存在性匹配,其操作符为 Exists;service资源简述Service是Kubernetes的核心概念,通过创建Service,可以为一组具有相同功能的容器应用提供一个统一的入口地址,并将请求负载分发到后端各个容器应用上。Service.yaml 文件解析apiVersion: v1 # 指定api版本,此值必须在kubectl api-versions中 kind: Service # 指定创建资源的角色/类型 metadata: # 资源的元数据/属性 name: demo # 资源的名字,在同一个namespace中必须唯一 namespace: default # 部署在哪个namespace中。不指定时默认为default命名空间 labels: # 设定资源的标签 - app: demo annotations: # 自定义注解属性列表 - name: string spec: # 资源规范字段 type: ClusterIP # service的类型,指定service的访问方式,默认ClusterIP。 # ClusterIP类型:虚拟的服务ip地址,用于k8s集群内部的pod访问,在Node上kube-porxy通过设置的iptables规则进行转发 # NodePort类型:使用宿主机端口,能够访问各个Node的外部客户端通过Node的IP和端口就能访问服务器 # LoadBalancer类型:使用外部负载均衡器完成到服务器的负载分发,需要在spec.status.loadBalancer字段指定外部负载均衡服务器的IP,并同时定义nodePort和clusterIP用于公有云环境。 clusterIP: string #虚拟服务IP地址,当type=ClusterIP时,如不指定,则系统会自动进行分配,也可以手动指定。当type=loadBalancer,需要指定 sessionAffinity: string #是否支持session,可选值为ClietIP,默认值为空。ClientIP表示将同一个客户端(根据客户端IP地址决定)的访问请求都转发到同一个后端Pod ports: - port: 8080 # 服务监听的端口号 targetPort: 8080 # 容器暴露的端口 nodePort: int # 当type=NodePort时,指定映射到物理机的端口号 protocol: TCP # 端口协议,支持TCP或UDP,默认TCP name: http # 端口名称 selector: # 选择器。选择具有指定label标签的pod作为管理范围 app: demo status: # 当type=LoadBalancer时,设置外部负载均衡的地址,用于公有云环境 loadBalancer: # 外部负载均衡器 ingress: ip: string # 外部负载均衡器的IP地址 hostname: string # 外部负载均衡器的主机名注:port和nodePort都是service的端口,前者暴露给k8s集群内部服务访问,后者暴露给k8s集群外部流量访问。从上两个端口过来的数据都需要经过反向代理kube-proxy,流入后端pod的targetPort上,最后到达pod内的容器。NodePort类型的service可供外部集群访问是因为service监听了宿主机上的端口,即监听了(所有节点)nodePort,该端口的请求会发送给service,service再经由负载均衡转发给Endpoints的节点。ingress.yaml 文件详解apiVersion: extensions/v1beta1 # 创建该对象所使用的 Kubernetes API 的版本 kind: Ingress # 想要创建的对象的类别,这里为Ingress metadata: name: showdoc # 资源名字,同一个namespace中必须唯一 namespace: op # 定义资源所在命名空间 annotations: # 自定义注解 kubernetes.io/ingress.class: nginx # 声明使用的ingress控制器 spec: rules: - host: showdoc.example.cn # 服务的域名 http: paths: - path: / # 路由路径 backend: # 后端Service serviceName: showdoc # 对应Service的名字 servicePort: 80 # 对应Service的端口
Docker Stack 部署应用概述单机模式下,可以使用 Docker Compose 来编排多个服务。Docker Swarm 只能实现对单个服务的简单部署。而Docker Stack 只需对已有的 docker-compose.yml 配置文件稍加改造就可以完成 Docker 集群环境下的多服务编排。stack是一组共享依赖,可以被编排并具备扩展能力的关联service。Docker Stack和Docker Compose区别Docker stack会忽略了“构建”指令,无法使用stack命令构建新镜像,它是需要镜像是预先已经构建好的。 所以docker-compose更适合于开发场景;Docker Compose是一个Python项目,在内部,它使用Docker API规范来操作容器。所以需要安装Docker -compose,以便与Docker一起在您的计算机上使用;Docker Stack功能包含在Docker引擎中。你不需要安装额外的包来使用它,docker stacks 只是swarm mode的一部分。Docker stack不支持基于第2版写的docker-compose.yml ,也就是version版本至少为3。然而Docker Compose对版本为2和3的 文件仍然可以处理;docker stack把docker compose的所有工作都做完了,因此docker stack将占主导地位。同时,对于大多数用户来说,切换到使用单机模式(Docker Compose)是一台主机上运行多个容器,每个容器单独提供服务;集群模式(swarm + stack)是多台机器组成一个集群,多个容器一起提供同一个服务;compose.yml deploy 配置说明docker-compose.yaml文件中deplo参数下的各种配置主要对应了swarm中的运维需求。docker stack deploy不支持的参数:(这些参数,就算yaml中包含,在stack的时候也会被忽略,当然也可以为了docker-compose up留着这些配置)build cgroup_parent container_name devices tmpfs external_links links network_mode restart security_opt userns_modedeploy:指定与服务的部署和运行有关的配置。注:只在 swarm 模式和 stack 部署下才会有用。且仅支持 V3.4 及更高版本。可以选参数:endpoint_mode:访问集群服务的方式。3.2版本开始引入的配置。用于指定服务发现,以方便外部的客户端连接到swarmvip:默认的方案。即通过 Docker 集群服务一个对外的虚拟 ip对外暴露服务,所有的请求都会通过这个虚拟 ip 到达集群服务内部的机器,客户端无法察觉有多少个节点提供服务,也不知道实际提供服务的IP和端口。dnsrr:DNS的轮询调度。所有的请求会自动轮询获取到集群 ip 列表中的一个 ip 地址。客户端访问的时候,Docker集群会通过DNS列表返回对应的服务一系列IP地址,客户连接其中的一个。这种方式通常用于使用自己的负载均衡器,或者window和linux的混合应用。labels:在服务上设置标签,并非附加在service中的容器上。如果在容器上设置标签,则在deploy之外定义labels。可以用容器上的 labels(跟 deploy 同级的配置) 覆盖 deploy 下的 labels。mode:用于指定是以副本模式(默认)启动还是全局模式replicated:副本模式,复制指定服务到集群的机器上。默认。global:全局模式,服务将部署至集群的每个节点。类似于k8s中的DaemonSet,会在每个节点上启动且只启动一个服务。replicas:用于指定副本数,只有mode为副本模式的时候生效。placement:主要用于指定约束和偏好。这个参数在运维的时候尤为关键constraints(约束):表示服务可以部署在符合约束条件的节点上,包含了:node attributematchesexamplenode.id节点idnode.id == 2ivku8v2gvtg4node.hostname节点主机名node.hostname != node-2node.role节点角色 (manager/worker)node.role == managernode.platform.os节点操作系统node.platform.os == windowsnode.platform.arch节点架构node.platform.arch == x86_64node.labels用户定义的labelsnode.labels.security == highengine.labelsDocker 引擎的 labelsengine.labels.operatingsystem == ubuntu-14.04preferences(偏好):表示服务可以均匀分布在指定的标签下。preferences 只有一个参数,就是spread,其参数值为节点的属性,即约束表中的内容例如:node.labels.zone这个标签在集群中有三个值,分别为west、east、north,那么服务中的副本将会等分为三份,分布到带有三个标签的节点上。max_replicas_per_node:3.8版本中开始引入的配置。控制每个节点上最多的副本数。注意:当 最大副本数*集群中可部署服务的节点数<副本数,会报错resources:用于限制服务的资源,这个参数在运维的时候尤为关键。示例:配置 redis 集群运行需要的 cpu 的百分比 和 内存的占用。避免占用资源过高出现异常。limit:用于限制最大的资源使用数量cpus:cpu占比,值的格式为百分比的小数格式memory:内存的大小。示例:512Mreservation:为最低的资源占用量。cpusmemoryrestart_policy:容器的重启策略condition:重启的条件。可选 none,on-failure 或者 any。默认值:anydelay:尝试重启的时间间隔(默认值:5s)。max_attempts:最大尝试重启容器的次数,超出次数,则不再尝试(默认值:一直重试)。window:判断重启是否成功之前的等待时间(一个总的时间,如果超过这个时间还没有成功,则不再重启)。rollback_config:更新失败时的回滚服务的策略。3.7版本加入。和升级策略相关参数基本一致。update_config:配置应如何更新服务,对于配置滚动更新很有用。parallelism:同时升级[回滚]的容器数delay:升级[回滚]一组容器的时间间隔failure_action:若更新[回滚]失败之后的策略:continue、 pause、rollback(仅在update_config中有) 。默认 pausemonitor:容器升级[回滚]之后,检测失败的时间检测 (支持的单位:ns|us|ms|s|m|h)。默认为 5smax_failure_ratio:最大失败率order:升级[回滚]期间的操作顺序。可选:stop-first(串行回滚,先停止旧的)、start-first(并行回滚,先启动新的)。默认 stop-first 。注意:只支持v3.4及更高版本compose.yml 文件示例version: "3" # 版本号,deploy功能是3版本及以上才有的 services: # 服务,每个服务对应配置相同的一个或者多个docker容器 redis: # 服务名,自取 image: redis:alpine # 创建该服务所基于的镜像。使用stack部署,只能基于镜像 ports: # 容器内外的端口映射情况 - "1883:1883" - "9001:9001" networks: # 替代了命令行模式的--link选项 - fiware volumes: # 容器内外数据传输的对应地址 - "/srv/mqtt/config:/mqtt/config:ro" - "/srv/mqtt/log:/mqtt/log" - "/srv/mqtt/data/:/mqtt/data/" command: -dbhost stack_mongo # 命令行模式中跟在最后的参数,此条没有固定的格式,建议参照所部署的docker镜像的说明文档来确定是否需要该项、需要写什么 deploy: mode: replicated replicas: 6 # replicas模式, 副本数目为1 endpoint_mode: vip labels: description: "This redis service label" resources: limits: cpus: '0.50' memory: 50M reservations: cpus: '0.25' memory: 20M restart_policy: condition: on-failure delay: 5s max_attempts: 3 window: 120s placement: constraints: - "node.role==worker" # 部署位置,只在工作节点部署 - "engine.labels.operatingsystem==ubuntu 18.04" preferences: - spread: node.labels.zone update_config: parallelism: 2 delay: 10s order: stop-first networks: # 定义部署该项目所需要的网络 fiware:stack 常用命令docker stack:编排部署应用# 部署一个新的stack(堆栈)或更新现有的stack。别名:deploy, up docker stack deploy [OPTIONS] 自定义STACK名称 # 选项: -c, --compose-file strings # Compose文件的路径,或从标准输入中读取的“-” --prune # 表示削减不再引用的服务。可以把一些down掉的service进行自动清理。 --orchestrator string # 指定编排模式 (swarm|kubernetes|all) --resolve-image string # 请求仓库来重新解析镜像的摘要和支持的平台。("always"|"changed"|"never") (默认 "always") --with-registry-auth # 发送仓库的授权详情到Swarm代理 --orchestrator # 使用的容器编排服务 # 通过compose.yml文件指令部署 docker stack deploy -c 文件名.yml 自定义STACK名称 # 列出现有堆栈。别名:ls, list docker stack ls [OPTIONS] # 列出堆栈中的任务 docker stack ps [OPTIONS] STACK # 选项: --no-trunc # 输出完整信息 # 删除一个或多个堆栈。别名:rm, remove, down docker stack rm [OPTIONS] STACK [STACK...] --orchestrator string # 指定适配器 (swarm|kubernetes|all) # 列出堆栈中的服务 docker stack services [OPTIONS] STACK搭建Docker私有仓库--自签名方式参考:https://developer.aliyun.com/article/303216#slide-3为了能集中管理创建好的镜像,方便部署服务,可以部署私有的Docker仓库。Docker为了确保安全使用TLS,需要CA认证,可以使用自签名方式。准备环境环境:两台Centos 7 虚拟机服务器IP:10.57.220.244 ,作为Docker仓库使用客户端IP:10.57.220.220 ,作为客户端来上传或拉取镜像域名:harbor.paic.com.cn(修改所有主机的 /etc/hosts 文件,添加域名和ip的对应, 格式:ip 域名)两台机器上均已安装好Docker 版本为 17.03.0-ce生成自签名证书 在服务器主机上生成自签名证书,创建一个文件夹用于存放证书mkdir -p certs # 生成自签名证书 openssl req -newkey rsa:4096 -nodes -sha256 -x509 -days 1825 -keyout `pwd`/certs/harbor.key -out `pwd`/certs/harbor.crt -config openssl_req.cnfopenssl_req.cnf# 定义输入用户信息选项的"特征名称"字段名,该扩展字段定义了多项用户信息。 distinguished_name = req_distinguished_name # 如果设为no,那么 req 指令将直接从配置文件中读取证书字段的信息,而不提示用户输入。 prompt = no [req_distinguished_name] #国家代码,一般都是CN(大写) C = CN ST = [] L = City #企业/单位名称 O = YZT #企业部门 OU = [] #证书的主域名。注意要和仓库的域名一致 CN = harbor.paic.com.cn分发自签名证书、指定私有仓库位置把生成的的 harbor.crt 复制到服务器端和客户端上的 /etc/pki/ca-trust/source/anchors 目录下\cp ./certs/harbor.crt /etc/pki/ca-trust/source/anchors指定私有仓库地址。vi /etc/docker/daemon.json ------------------------------------------------------------------------ "insecure-registries": ["harbor.paic.com.cn"] ------------------------------------------------------------------------ # 注意:若仓库的容器的5000端口不是绑定到宿主机的443端口上,则需要指定绑定的宿主机端口 "insecure-registries": ["harbor.paic.com.cn:5000"] }更新证书,然后重新启动dockerupdate-ca-trust;systemctl restart docker启动容器运行仓库镜像,如果本地没有相应的镜像会从Docker服务器上下载,然后才启动,可以用docker ps命令查看是否已经有窗口在运行。docker run -d -p 443:5000 \ --restart=always \ --name registry_https \ -v `pwd`/dockerregister:/var/lib/registry \ -v `pwd`/certs:/home/certs \ -e REGISTRY_HTTP_TLS_CERTIFICATE=/home/certs/harbor.crt \ -e REGISTRY_HTTP_TLS_KEY=/home/certs/harbor.key \ registry # 说明: # -p 443:5000 :将仓库的容器的5000端口绑定到宿主机443端口上。 # 因docker访问镜像仓库地址的默认端口号为443, # 若仓库的容器的5000端口不绑定到宿主机的443端口上,则push/pull时需指定绑定的宿主机端口 # 若仓库的容器的5000端口不绑定到宿主机的443端口上,又不想push/pull时指定绑定的宿主机端口,可以使用ngnix进行端口映射 # -v `pwd`/dockerregister:/var/lib/registry :指定宿主机的目录保存上传好的docker镜像 # -v `pwd`/certs:/home/certs :挂载自签名证书目录查看容器是否启动成功# docker ps | grep 容器名 docker ps | grep registry_https验证:# 使用curl查看一下仓库的镜像列表。 curl -XGET https://harbor.paic.com.cn:443/v2/_catalog # {"repositories":[]} # 使用curl查看一下仓库中某镜像的版本。 curl https://harbor.paic.com.cn:443/v2/redis/tags/listpush/pull 镜像注意:若仓库的容器的5000端口不是绑定到宿主机的443端口上,则需要指定绑定的宿主机端口# 打tag docker tag redis harbor.paic.com.cn/redis # 指定端口号 示例: # docker tag redis harbor.paic.com.cn:5000/redis # push镜像 docker push harbor.paic.com.cn/redis # pull镜像 docker pull harbor.paic.com.cn/redis
Docker Swarm 集群管理概述Docker Swarm 是 Docker 的集群管理工具。它将 Docker 主机池转变为单个虚拟 Docker 主机,使得容器可以组成跨主机的子网网络。Docker Swarm 提供了标准的 Docker API,所有任何已经与 Docker 守护程序通信的工具都可以使用 Swarm 轻松地扩展到多个主机。集群的管理和编排是使用嵌入到 docker 引擎的 SwarmKit,可以在 docker 初始化时启动 swarm 模式或者加入已存在的 swarm。支持的工具包括但不限于以下各项:DokkuDocker ComposeDocker MachineJenkinsDocker Swarm 优点任何规模都有高性能表现对于企业级的 Docker Engine 集群和容器调度而言,可拓展性是关键。任何规模的公司——不论是拥有五个还是上千个服务器——都能在其环境下有效使用 Swarm。经过测试,Swarm 可拓展性的极限是在 1000 个节点上运行 50000 个部署容器,每个容器的启动时间为亚秒级,同时性能无减损。灵活的容器调度Swarm 帮助 IT 运维团队在有限条件下将性能表现和资源利用最优化。Swarm 的内置调度器(scheduler)支持多种过滤器,包括:节点标签,亲和性和多种容器部策略如 binpack、spread、random 等等。服务的持续可用性Docker Swarm 由 Swarm Manager 提供高可用性,通过创建多个 Swarm master 节点和制定主 master 节点宕机时的备选策略。如果一个 master 节点宕机,那么一个 slave 节点就会被升格为 master 节点,直到原来的 master 节点恢复正常。此外,如果某个节点无法加入集群,Swarm 会继续尝试加入,并提供错误警报和日志。在节点出错时,Swarm 现在可以尝试把容器重新调度到正常的节点上去。和 Docker API 及整合支持的兼容性Swarm 对 Docker API 完全支持,这意味着它能为使用不同 Docker 工具(如 Docker CLI,Compose,Trusted Registry,Hub 和 UCP)的用户提供无缝衔接的使用体验。Docker Swarm 为 Docker 化应用的核心功能(诸如多主机网络和存储卷管理)提供原生支持开发的 Compose 文件能(通过 docker-compose up )轻易地部署到测试服务器或 Swarm 集群上。Docker Swarm 还可以从 Docker Trusted Registry 或 Hub 里 pull 并 run 镜像。集群模式,当修改了服务的配置后无需手动重启服务。并且只有集群中的manager才能管理集群中的一切(包括服务、容器都归它管,在一个woker节点上无法操作容器)节点swarm 集群由管理节点(manager)和工作节点(work node)构成。swarm mananger:负责整个集群的管理工作包括集群配置、服务管理等所有跟集群有关的工作。一个 Swarm 集群可以有多个管理节点,但只有一个管理节点可以成为 leader,leader 通过 raft 协议实现。为了利用swarm模式的容错功能,Docker建议根据组织的高可用性要求实现奇数个节点。当您拥有多个管理器时,您可以从管理器节点的故障中恢复而无需停机。N个管理节点的集群容忍最多损失 (N-1)/2 个管理节点。Docker建议一个集群最多7个管理器节点。work node:即图中的 available node,主要负责运行相应的服务来执行任务(task)。工作节点是任务执行节点,管理节点将服务 (service) 下发至工作节点执行。管理节点默认也作为工作节点。也可以通过配置让服务只运行在管理节点。服务和任务任务 (Task)是 Swarm 中的最小的调度单位,目前来说就是一个单一的容器。服务 (Services) 是指一组任务的集合,服务定义了任务的属性。服务有两种模式:replicated services (复制服务)按照一定规则在各个工作节点上运行指定个数的任务。global services (全局服务)每个工作节点上运行一个此任务。两种模式通过 docker service create 的 --mode 参数指定。下图展示了容器、任务、服务的关系。路由网格service 通过 ingress load balancing 来发布服务,且 swarm 集群中所有 node 都参与到 ingress 路由网格(ingress routing mesh) 中,访问任意一个 node+PublishedPort 即可访问到服务。当访问任何节点上的端口8080时,Docker将请求路由到活动容器。在群节点本身,端口8080可能并不实际绑定,但路由网格知道如何路由流量,并防止任何端口冲突的发生。路由网格在发布的端口上监听分配给节点的任何IP地址。对于外部可路由的IP地址,该端口可从主机外部获得。对于所有其他IP地址,只能从主机内部访问。Swarm 集群的搭建准备工作二个或二个以上可以通过网络进行通信的Linux主机或虚拟机,并安装了Docker(加入开机自启),或者使用docker-machine 创建三台虚拟机。swarm 不需要单独安装,安装了 docker 就自带了该软件已安装Docker Engine 1.12或更高版本关闭所有主机上的防火墙或者开放以下端口:TCP协议端口 2377 :集群管理端口TCP协议端口 7946 :节点之间通讯端口(不开放则会负载均衡失效)UDP协议端口 4789 :overlay网络通讯端口防火墙相关命令:# 查看firewalld防火墙状态 systemctl status firewalld # 查看所有打开的端口 firewall-cmd --zone=public --list-ports # 防火墙开放端口(更新firewalld防火墙规则后生效) firewall-cmd --zone=public --add-port=要开放的端口/tcp --permanent # 选项: –zone # 作用域 –add-port=80/tcp # 添加端口,格式为:端口/通讯协议 –permanent #永久生效,没有此参数重启后失效 # 示例: firewall-cmd --zone=public --add-port=3306/tcp --permanent # firewalld防火墙关闭接口(更新firewalld防火墙规则后生效) firewall-cmd --zone=public --remove-port=要关闭的端口/tcp --permanent # 更新firewalld防火墙规则(并不中断用户连接,即不丢失状态信息) firewall-cmd --reload # 启动firewalld防火墙 systemctl start firewalld # 关闭firewalld防火墙: systemctl stop firewalld # 开机禁用firewalld防火墙 systemctl disable firewalld # 开机启用firewalld防火墙: systemctl enable firewalld分别修改机器的主机名,更改成 swarm01,swarm02 ...hostnamectl set-hostname swarm01创建docker swarm集群1.master主机上初始化swarm。执行 docker swarm init 命令的节点自动成为管理节点。docker swarm init # 注:如果主机有多个网卡,拥有多个IP,必须使用 --advertise-addr 指定 IP。 # 示例: docker swarm init --advertise-addr 192.168.99.100# 执行命令后会给出加入这个swarm的命令 Swarm initialized: current node (4a8mo8cekpe0vpk0ze963avw9) is now a manager. To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-4lzr2216s61ecbyayyqynjwybmxy5y5th5ru8aal2a0d1t2vn3-ekdgf4swlz8fiq4nnzgnbhr5u 192.168.99.100:2377 To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.2.在node主机上执行命令加入swarmdocker swarm join --token SWMTKN-1-4lzr2216s61ecbyayyqynjwybmxy5y5th5ru8aal2a0d1t2vn3-ekdgf4swlz8fiq4nnzgnbhr5u 192.168.99.100:23773.查看集群信息。集群的大部分命令需要在管理节点中才能运行# 查看 swarm 集群状态 docker info # 查看集群节点信息 docker node lsSwarm 集群管理常用命令docker swarm:管理集群# 初始化一个swarm docker swarm init [OPTIONS] # 选项: --advertise-addr string # 发布的地址(格式:<ip|interface>[:port]) --force-new-cluster # 强制从当前状态创建一个新的集群(去除本地之外的所有管理器身份) --cert-expiry duration # 节点证书有效期(ns|us|ms|s|m|h)(默认为2160h0m0s) --data-path-addr string # 用于数据路径通信的地址或接口(格式: <ip|interface>) --data-path-port uint32 # 用于数据路径流量的端口号(1024 - 49151)。如果没有值,则默认端口号4789 --dispatcher-heartbeat duration # 调度程序的心跳周期(ns|us|ms|s|m|h)(默认为5s) --listen-addr node-addr # 监听地址(格式: <ip|interface>[:port]) (默认 0.0.0.0:2377) # 查看加入节点到集群的命令及令牌(token) docker swarm join-token [OPTIONS] (worker|manager) # 选项: -q, --quiet # 只显示令牌 --rotate # 使旧令牌无效并生成新令牌 # 查看加入工作节点到集群的命令及令牌 docker swarm join-token worker # 查看加入管理节点到集群的命令及令牌 docker swarm join-token manager # 将节点加入swarm集群,作为一个manager或worker docker swarm join [OPTIONS] HOST:PORT # 选项: --advertise-addr string # 发布的地址 (格式: <ip|interface>[:port]) --availability string # 节点的可用性 ("active"|"pause"|"drain") (default "active") --data-path-addr string # 用于数据路径通信的地址或接口 (格式: <ip|interface>) --listen-addr node-addr # 监听地址 (格式: <ip|interface>[:port]) (default 0.0.0.0:2377) --token string # 进入的swarm集群的令牌 # 主动退出集群,让节点处于down状态(在需要退出Swarm集群的节点主机上执行命令) docker swarm leave [OPTIONS] # 选项: -f, --force # 强制。Manager若要退出 Swarm 集群,需要加上强制选项 ## 移除一个work-node节点主机的完整步骤: # 1.在管理节点上操作,清空work-node节点的容器。id 可以使用命令 docker node ls 查看 docker node update --availability drain [id] # 2.在work-node节点主机上操作,退出集群 docker swarm leave # 3,在管理节点上操作,删除work-node节点 docker node rm [id] # 若想解散整个集群,则需先移除所有work-node节点主机,然后所有管理节点也退出集群 # 更新 swarm 集群的配置 docker swarm update [OPTIONS] # 选项: --autolock # 更改管理器自动锁定设置(true|false) --cert-expiry duration # 节点证书有效期(ns|us|ms|s|m|h)(默认为2160h0m0s) --dispatcher-heartbeat duration # 调度程序心跳周期(ns|us|ms|s|m|h)(默认为5s)docker node:管理swarm集群节点# 查看集群中的节点 docker node ls -f, --filter filter # 根据所提供的条件过滤输出。(格式:key=value) # 目前支持的过滤器是:id, label, name, membership[=accepted|pending] # , role[manager|worker] -q, --quiet # 只显示id # 查看运行的一个或多个及节点任务数,默认当前节点 docker node ps [OPTIONS] [NODE...] -f, --filter filter # 根据所提供的条件过滤输出 -q, --quiet # 只显示id # 将worker角色升级为manager docker node promote NODE [NODE...] # 将manager角色降级为worker docker node demote NODE [NODE...] # 查看节点的详细信息,默认json格式 docker node inspect 主机名 # 查看节点信息平铺格式 docker node inspect --pretty 主机名 # 从swarm中删除一个节点 docker node rm 主机名 # 从swarm中强制删除一个节点 docker node rm -f 主机名 # 更新一个节点 docker node update [options] 主机名 --label-add list # 添加节点标签(key=value) --label-rm list # 删除节点标签 --role string # 更改节点角色 ("worker"|"manager") --availability active/pause/drain # 设置节点的状态 # active 正常 # pause 暂停。调度程序不向节点分配新任务,但是现有任务仍然保持运行 # drain 排除自身work任务。调度程序不向节点分配新任务,且会关闭任何现有任务并在可用节点上安排它们docker service:服务管理# 列出服务列表 docker service ls # 列出服务任务信息 docker service ps [OPTIONS] SERVICE [SERVICE...] # 选项: --no-trunc # 显示完整的信息 -f, --filter filter # 根据所提供的条件过滤输出。过滤只运行的任务信息:"desired-state=running" -q, --quiet # 只显示任务id # 查看服务内输出 docker service logs [OPTIONS] SERVICE|TASK # 选项: --details # 显示提供给日志的额外细节 -f, --follow # 跟踪日志输出 --since string # 显示自时间戳 (2013-01-02T13:23:37Z) 或相对时间戳 (42m for 42 minutes) 以来的日志 -n, --tail string # 从日志末尾显示的行数(默认为“all”) -t, --timestamps # 显示时间戳 # 更新服务的相关配置 docker service update [options] 服务名 --args "指令" # 容器加入指令 --image IMAGE # 更新服务容器镜像 --rollback # 回滚服务容器版本 --network-add 网络名 # 添加容器网络 --network-rm 网络名 # 删除容器网络 --reserve-cpu int # 更新分配的cpu --reserve-memory bytes # 更新分配的内存(示例:512m) --publish-add 暴露端口:容器端口 # 映射容器端口到主机 --publish-rm 暴露端口:容器端口 # 移除暴露端口 --endpoint-mode dnsrr # 修改负载均衡模式为dnsrr --force # 强制重启服务 --config-rm 配置文件名称 # 删除配置文件 --constraint-add list # 新增一个约束 --constraint-rm list # 移除一个约束 --placement-pref-add pref # 新增一个偏好 --placement-pref-rm pref # 移除一个偏好 --config-add 配置文件名,target=/../容器内配置文件名 # 添加新的配置文件到容器内 # 查看服务详细信息,默认json格式 docker service inspect [OPTIONS] 服务名 [SERVICE...] # 查看服务信息平铺形式 docker service inspect --pretty 服务名 # 删除服务 docker service rm [OPTIONS] 服务名 [SERVICE...] # 缩容扩容服务容器副本数量 docker service scale 服务名=副本数 [SERVICE=REPLICAS...] # 创建一个服务。一般搭建好 Swarm 集群后,使用 docker stack 部署应用,此处仅作了解 docker service create [OPTIONS] IMAGE [COMMAND] [ARG...] # 选项: --name string # 指定容器名称 --replicas int # 指定副本数 --network 网络名 # 添加网络组 --mode string # 服务模式(复制或全局)(replicated | global) --reserve-cpu int # 预留的cpu --reserve-memory bytes # 预留的内存(512m) --limit-cpu int # 限制CPU --limit-memory bytes # 限制内存(512m) -l, --label list # 服务的标签(key=value) --container-label list # 容器标签(key=value) -p, --publish 暴露端口:容器端口 # 映射容器端口到主机 -e, --env MYVAR=myvalue # 配置环境变量 -w, --workdir string # 指定工作目录(示例:/tmp) -restart-condition string # 满足条件时重新启动(no | always | on-failure | unless-stopped) --restart-delay duration # 重新启动尝试之间的延迟 (ns/us/ms/s/m/h) --restart-max-attempts int # 放弃前的最大重启次数 --restart-window duration # 用于评估重启策略的窗口 (ns/us/ms/s/m/h) --stop-grace-period duration # 强制杀死容器前的等待时间 (ns/us/ms/s/m/h) --update-delay duration # 更新之间的延迟(ns/us/ms/s/m/h)(默认 0s) --update-failure-action string # 更新失败的操作("pause"停止|"continue"继续)(默认pause) --update-max-failure-ratio float # 更新期间容忍的失败率 --update-monitor duration # 每次任务更新后监控失败的持续时间(ns/us/ms/s/m/h)(默认 0s) --update-parallelism int # 同时更新的最大任务数(0表示一次更新全部任务)(默认为1) --endpoint-mode string # 负载均衡模式(vip or dnsrr) (默认 "vip") --rollback-monitor 20s # 每次容器与容器之间的回滚时间间隔 --rollback-max-failure-ratio .数值 # 回滚故障率如果小于百分比允许运行(“.2”为%20) --mount type=volume,src=volume名称,dst=容器目录 # 创建volume类型数据卷 --mount type=bind,src=宿主目录,dst=容器目录 # 创建bind读写目录挂载 --mount type=bind,src=宿主目录,dst=容器目录,readonly # 创建bind只读目录挂载 --config source=docker配置文件,target=配置文件路径 # 创建docker配置文件到容器本地目录docker config:管理配置文件# 查看已创建配置文件 docker config ls [OPTIONS] # 选项: -f, --filter filter # 根据所提供的条件过滤输出 -q, --quiet # 只显示id # 查看配置详细信息 docker config inspect 配置文件名 # 删除配置 docker config rm CONFIG [CONFIG...] # 创建配置文件 docker config create 配置文件名 本地配置文件 # 示例:新建配置文件并添加新配置文件到服务 # 1.创建配置文件 docker config create nginx2_config nginx2.conf # 2.删除旧配置文件 docker service update --config-rm ce_nginx_config 服务名 # 3.添加新配置文件到服务 ocker service update --config-add src=nginx2_config,target=/etc/nginx/nginx.conf ce_nginxdocker network:管理网络# 查看集群网络列表 docker network ls # 将容器连接到集群网络中 $ docker network connect [OPTIONS] NETWORK CONTAINER --alias strings # 为容器添加网络范围的别名 --driver-opt string · # 指定网络驱动程序 --ip string # 指定IPv4地址(如172.30.100.104) --ip6 string # 指定IPv6地址(例如,2001:db8::33) --link list # 添加到另一个容器的链接 --link-local-ip string # 为容器添加一个链接本地地址 docker network connect mynet nginx # 断开一个容器与集群网络的连接 $ docker network disconnect [OPTIONS] NETWORK CONTAINER -f, --force # 强制容器从网络断开连接 # 显示一个或多个集群网络的详细信息 $ docker network inspect [OPTIONS] NETWORK [NETWORK...] -f, --format string # 使用给定的Go模板格式化输出 -v, --verbose # 输出详细的诊断信息 # 创建一个集群网络 $ docker network create [OPTIONS] NETWORK --attachable # 准许手动容器连接 --aux-address map # 网络驱动使用的辅助IPv4或IPv6地址(默认映射[]) --config-from string # 要从其中复制配置的网络 --config-only # 创建仅配置网络 -d, --driver string # 管理网络的驱动程序(默认为“"bridge”)。选项:bridge、overlay、macvlan --gateway strings # 指定IPv4或IPv6主子网网关。示例:172.20.0.1 --ingress # 创建群路由-网格网络 --internal # 限制外部访问网络 --ip-range strings # 从子范围分配容器ip --ipam-driver string # IP管理驱动(默认为“default”) --ipam-opt map # 设置IPAM驱动程序的特定选项(默认map[]) --ipv6 # 启用IPv6网络 --label list # 在网络中设置元数据 -o, --opt map # 设置驱动程序特定选项(默认map[]) --scope string # 控制网络的范围 --subnet strings # 指定一个CIDR格式的网段。示例:172.20.0.0/24 # 示例: docker network create -d overlay --attachable apps_net # 移除所有未使用的集群网络 $ docker network prune [OPTIONS] --filter filter # 提供过滤值(e.g. 'until=<timestamp>') -f, --force # 强制,没有提示确认 # 删除一个或多个集群网络 $ docker network rm NETWORK [NETWORK...] # 别名:rm, removedocker secret:管理敏感数据存储# 查看敏感数据卷列表 $ docker secret ls # 显示一个或多个敏感数据卷的详细信息 $ docker secret inspect [OPTIONS] SECRET [SECRET...] --pretty # 易读的格式打印信息 # 从文件或标准输入创建一个敏感数据卷作为内容 $ docker secret create [OPTIONS] SECRET [file|-] -d, --driver string # 指定驱动 -l, --label list # 指定标签 --template-driver string # 指定模板驱动程序 # 移除一个或多个敏感数据卷 $ docker secret rm SECRET [SECRET...] # 别名:rm, removedocker网络管理参考:参考:docker的3种自定义网络(bridge、overlay、macvlan)Docker Swarm - 网络管理Docker的网络模式bridge、host、container 、overlay概述Docker 提供三种 user-defined 网络驱动:bridge,overlay 和 macvlanoverlay 和 macvlan 用于创建跨主机的网络Swarm 集群产生两种不同类型的流量:控制和管理层面:包括 Swarm 消息管理等,例如请求加入或离开Swarm,这种类型的流量总是被加密的。(涉及到集群内部的hostname、ip-address、subnet、gateway等)应用数据层面:包括容器与客户端的通信等。(涉及到防火墙、端口映射、网口映射、VIP等)在 Swarm Service 中有三个重要的网络概念:Overlay networks :管理 Swarm 中 Docker 守护进程间的通信。可以将服务附加到一个或多个已存在的 overlay 网络上,使得服务与服务之间能够通信。ingress network :是一个特殊的 overlay 网络,用于服务节点间的负载均衡,处理与群集服务相关的控制和数据流量。当任何 Swarm 节点在发布的端口上接收到请求时,它将该请求交给一个名为 IPVS 的模块。IPVS 跟踪参与该服务的所有IP地址,选择其中的一个,并通过 ingress 网络将请求路由到它。初始化或加入 Swarm 集群时会自动创建 ingress 网络,大多数情况下,用户不需要自定义配置,但是 docker 17.05 和更高版本允许你自定义。docker_gwbridge :是一种桥接网络,将 overlay 网络(包括 ingress 网络)连接到一个单独的 Docker 守护进程的物理网络。默认情况下,服务正在运行的每个容器都连接到本地 Docker 守护进程主机的 docker_gwbridge 网络。docker_gwbridge 网络在初始化或加入 Swarm 时自动创建。大多数情况下,用户不需要自定义配置,但是 Docker 允许自定义。在管理节点上查看网络$ docker network ls NETWORK ID NAME DRIVER SCOPE cb0ccb89a988 bridge bridge local 0174fb113496 docker_gwbridge bridge local 541b62778c0e host host local 8n7xppn5z4j2 ingress overlay swarm 369d459f340d none null localoverlay网络驱动程序会创建多个Docker守护主机之间的分布式网络。该网络位于(覆盖)特定于主机的网络之上,允许连接到它的容器(包括群集服务容器)安全地进行通信。Docker透明地处理每个数据包与正确的Docker守护程序主机和正确的目标容器的路由。自定义overlay 网络创建用于swarm服务的自定义的overlay网络 命令:docker network create -d overlay --attachable my-overlay # 注:overlay 网络创建可以在 Swarm 集群下的任意节点执行,并同步更新到所有节点。集群中部署了两个服务 nginx、alpine,现在我们进入alpine,去访问nginx。$ docker exec -it test1.1.oonwl8c5g4u3p17x8anifeubi bash $ ping nginx ping: bad address 'nginx' $ wget 192.168.99.100:8080 Connecting to 192.168.99.100:8080 (192.168.99.100:8080) index.html 100% |**********************************************************************************************************| 612 0:00:00 ETA发现集群中的各个服务不能用名称访问的,只能用集群服务发现的路由网络访问,若需要集群中的服务能通过名称进行访问,这就需要用到上面自定义的 overlay 网络。删除启动的服务,重新创建指定使用自定义网络的服务。docker service rm nginx alpine docker service create --name nginx -p 8080:80 --network my-overlay --replicas 3 nginx docker service create --name alpine --network my-overlay alpine ping www.baidu.com进入alpine容器中,重新测试下:$ ping nginx PING nginx (10.0.0.2): 56 data bytes 64 bytes from 10.0.0.2: seq=0 ttl=64 time=0.120 ms 64 bytes from 10.0.0.2: seq=1 ttl=64 time=0.094 ms 64 bytes from 10.0.0.2: seq=2 ttl=64 time=0.108 ms $ wget nginx Connecting to nginx (10.0.0.2:80) index.html 100% |**********************************************************************************************************| 612 0:00:00 ETA发现可以通过名称进行集群中的容器间的访问了。
Docker-Compose 多容器部署工具概述Docker-Compose项目是Docker官方的开源项目,是用于定义和运行多容器 Docker 应用程序的工具。负责实现对Docker容器集群的快速编排。 通过 Compose,可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。Docker-Compose将所管理的容器分为三层,分别是工程(project),服务(service)以及容器(container)。Docker-Compose运行目录下的所有文件(docker-compose.yml,extends文件或环境变量文件等)组成一个工程,若无特殊指定工程名即为当前目录名。一个工程当中可包含多个服务,每个服务中定义了容器运行的镜像,参数,依赖。一个服务当中可包括多个容器实例,Docker-Compose并没有解决负载均衡的问题,因此需要借助其它工具实现服务发现及负载均衡。Docker-Compose的工程配置文件默认为docker-compose.yml,可通过环境变量COMPOSE_FILE或-f参数自定义配置文件,其定义了多个有依赖关系的服务及每个服务运行的容器。 使用一个Dockerfile模板文件,可以让用户很方便的定义一个单独的应用容器。在工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个Web项目,除了Web服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。 Compose允许用户通过一个单独的docker-compose.yml模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。 Docker-Compose项目由Python编写,调用Docker服务提供的API来对容器进行管理。因此,只要所操作的平台支持Docker API,就可以在其上利用Compose来进行编排管理。Docker-Compose 安装及卸载Docker-compose安装安装 Docker Compose 可以通过下面命令自动下载适应版本的 Compose,并为安装脚本添加执行权限# 要安装其他版本的 Compose,请替换 v2.2.2。 sudo curl -L "https://get.daocloud.io/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose查看安装是否成功docker-compose -v创建软链:ln -s /usr/local/bin/docker-compose /usr/bin/docker-composeDocker-compose卸载apt-get remove docker-composedocker-compose 常用命令ps:列出所有运行容器docker-compose pslogs:查看服务日志输出docker-compose logs [options] [SERVICE...] # 选项包括 –no-color 关闭颜色。默认情况下,docker-compose将对不同的服务输出使用不同的颜色来区分 -f 跟踪日志输出 docker-compose logsup:使用当前目录下的docker-compose.yaml文件构建、启动、更新容器当服务的配置发生更改时,可使用 docker-compose up 命令更新配置此时,Compose 会删除旧容器并创建新容器,新容器会以不同的 IP 地址加入网络,名称保持不变,任何指向旧容起的连接都会被关闭,重新找到新容器并连接上去docker-compose up [options] [--scale SERVICE=NUM...] [SERVICE...] # 选项包括: -f # 指定yml部署模板文件 -d # 在后台运行服务容器 --force-recreate # 强制重新创建容器,不能与-no-recreate同时使用 -no-recreate # 如果容器已经存在,则不重新创建,不能与–force-recreate同时使用 -no-color # 不用颜色来区分不同的服务的控制输出 -no-deps # 不启动服务所链接的容器 -no-build # 不自动构建缺失的服务镜像 -build # 在启动容器前构建服务镜像 -V, --renew-anon-volumes # 重新创建匿名卷,而不是从以前的容器中检索数据 -abort-on-container-exit # 如果任何一个容器被停止,则停止所有容器。不能与-d同时使用 -t, -timeout TIMEOUT # 停止容器时候的超时(默认为10秒) -remove-orphans # 删除服务中没有在compose.yaml文件中定义的容器 --scale SERVICE=NUM # 将Compose.yaml中的SERVICE服务扩展到NUM个实例,快速实现一个负载均衡(单节点) # 该参数会覆盖Compose.yaml文件中的“scale”设置(如果存在)。 # 注意:如果Compose.yaml中设置了ports配置(端口绑定) # ,当使用scale参数拓展到多个实例时,会端口冲突,需删除Compose.yaml中的ports配置 docker-compose up --force-recreate -dport:显示某个容器端口所映射的公共端口docker-compose port [options] SERVICE PRIVATE_PORT # 选项包括: –protocol=proto 指定端口协议,TCP(默认值)或者UDP –index=index 如果同意服务存在多个容器,指定命令对象容器的序号(默认为1) # 示例:下面命令可以输出 eureka 服务 8761 端口所绑定的公共端口 docker-compose port eureka 8761build:构建或者重新构建服务容器。服务容器一旦构建后,将会带上一个标记名。可以随时在项目目录下运行docker-compose build来重新构建服务docker-compose build [options] [--build-arg key=val...] [SERVICE...] # 选项包括: –compress 通过gzip压缩构建上下环境 –force-rm 删除构建过程中的临时容器 –no-cache 构建镜像过程中不使用缓存 –pull 始终尝试通过拉取操作来获取更新版本的镜像 -m, –memory MEM 为构建的容器设置内存大小 –build-arg key=val 为服务设置build-time变量 # 示例: docker-compose buildstop:停止已运行的服务的容器docker-compose stop [options] [SERVICE...] # 选项包括 -t, –timeout TIMEOUT 停止容器时候的超时(默认为10秒) docker-compose stop eurekastart:启动指定服务已存在的容器docker-compose start [SERVICE...] docker-compose start eurekarestart:重启项目中的服务docker-compose restart [options] [SERVICE...] # 选项包括: -t, –timeout TIMEOUT 指定重启前停止容器的超时(默认为10秒) # 示例: docker-compose restartrm:删除所有或指定(停止状态的)服务的容器docker-compose rm [options] [SERVICE...] # 选项包括: –f, –force 强制直接删除,包括非停止状态的容器 -v 删除容器所挂载的数据卷 docker-compose rm eurekakill:通过发送 SIGKILL 信号来强制停止指定服务的容器docker-compose kill [options] [SERVICE...] # 选项包括: -s 来指定发送的信号 docker-compose kill eureka docker-compose kill -s SIGINTpush:推送服务依的镜像docker-compose push [options] [SERVICE...] # 选项包括: –ignore-push-failures 忽略推送镜像过程中的错误pull:下载服务镜像docker-compose pull [options] [SERVICE...] # 选项包括: –ignore-pull-failures 忽略拉取镜像过程中的错误 –parallel 多个镜像同时拉取 –quiet 拉取镜像过程中不打印进度信息 docker-compose pullscale:设置指定服务运行容器的个数,以 service=num 形式指定docker-compose scale user=3 movie=3run:在一个服务上执行一个命令docker-compose run web bashdown:停止并删除所有容器、网络、卷、镜像。docker-compose down [options] # 选项包括: –rmi type 删除镜像,类型必须是:all,删除compose文件中定义的所有镜像;local,删除镜像名为空的镜像 -v, –volumes 删除已经在compose文件中定义的和匿名的附在容器上的数据卷 –remove-orphans 删除服务中没有在compose中定义的容器 docker-compose downpause:暂停一个服务容器docker-compose pause [SERVICE...]uppause:恢复处于暂停状态中的服务docker-compose unpause [SERVICE...]config:验证并查看compose文件配置docker-compose config [options] # 选项包括: –resolve-image-digests 将镜像标签标记为摘要 -q, –quiet 只验证配置,不输出。 当配置正确时,不输出任何内容,当文件配置错误,输出错误信息 –services 打印服务名,一行一个 –volumes 打印数据卷名,一行一个create:为服务创建容器docker-compose create [options] [SERVICE...] # 选项包括: –force-recreate 重新创建容器,即使配置和镜像没有改变,不兼容–no-recreate参数 –no-recreate 如果容器已经存在,不需要重新创建,不兼容–force-recreate参数 –no-build 不创建镜像,即使缺失 –build 创建容器前,生成镜像Docker-compose 模板文件简介及示例模板Compose允许用户通过一个docker-compose.yml模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。 Compose模板文件是一个定义服务、网络和卷的YAML文件。Compose模板文件默认路径是当前目录下的docker-compose.yml,可以使用.yml或.yaml作为文件扩展名。Docker-Compose标准模板文件应该包含version、services、networks 三大部分,最关键的是services和networks两个部分。示例模板version: '3' networks: front-tier: driver: bridge back-tier: driver: bridge services: image: dockercloud/hello-world ports: - 8080 networks: - front-tier - back-tier redis: image: redis links: - web networks: - back-tier image: dockercloud/haproxy ports: - 80:80 links: - web networks: - front-tier - back-tier volumes: - /var/run/docker.sock:/var/run/docker.sock Compose目前有三个版本分别为Version 1,Version 2,Version 3,Compose区分Version 1和Version 2(Compose 1.6.0+,Docker Engine 1.10.0+)。Version 2支持更多的指令。Version 1将来会被弃用。启动应用创建一个webapp目录,将docker-compose.yaml文件拷贝到webapp目录下,使用docker-compose启动应用。docker-compose up -dDocker-compose.yml 配置说明version:指定 docker-compose.yml 文件的写法格式services:多个容器集合build:指定为构建镜像上下文路径。相对路径、绝对路径均可服务除了可以基于指定的镜像,还可以基于一份Dockerfile,在使用up启动时执行构建任务,构建标签是build,可以指定Dockerfile所在文件夹的路径。Compose将会利用Dockerfile自动构建镜像,然后使用镜像启动服务容器。context:上下文路径。dockerfile:指定构建镜像的 Dockerfile 文件名。使用dockerfile文件来构建时,必须指定构建路径args:添加构建参数,这是只能在构建过程中访问的环境变量。可选labels:设置构建镜像的标签。target:多层构建,可以指定构建哪一层。build: ./dir --------------- build: context: /path/to/build/dir dockerfile: Dockerfile args: buildno: 1 labels: - "com.example.description=Accounting webapp" - "com.example.department=Finance" - "com.example.label-with-empty-value" target: prod # build都是一个目录,如果要指定Dockerfile文件需要在build标签的子级标签中使用dockerfile标签指定。 # 如果同时指定image和build两个标签,那么Compose会构建镜像并且把镜像命名为image值指定的名字。container_name:指定自定义容器名称,而不是生成的默认名称(<项目名称><服务名称><序号>)。container_name: my-web-containercommand:覆盖容器启动后默认执行的命令在用命令行运行容器时,有的容器需要在命令中加入附加的命令行参数,这就需要加在compose文件的服务中的command中。这一个参数没有什么固定的模式,建议按照对应容器的镜像的使用说明来决定是否需要加,加什么。command: bundle exec thin -p 3000 ---------------------------------- command: ["bundle","exec","thin","-p","3000"]cap_add,cap_drop:添加或删除容器拥有的宿主机的内核功能。depends_on:设置依赖关系。docker-compose up :以依赖性顺序启动服务。在以下示例中,先启动 db 和 redis ,才会启动 web。docker-compose up SERVICE :自动包含 SERVICE 的依赖项。在以下示例中,docker-compose up web 还将创建并启动 db 和 redis。docker-compose stop :按依赖关系顺序停止服务。在以下示例中,web 在 db 和 redis 之前停止。注意:web 服务不会等待 redis db 完全启动 之后才启动。cap_add: - ALL # 开启全部权限 cap_drop: - SYS_PTRACE # 关闭 ptrace权限version: "3.7" services: build: . depends_on: - redis redis: image: redis image: postgresdns:配置 dns 服务器,可以是一个值或列表dns: 8.8.8.8 ------------ - 8.8.8.8 - 9.9.9.9dns_search:配置 DNS 搜索域,可以是一个值或列表dns_search: example.com ------------------------ dns_search: - dc1.example.com - dc2.example.comenv_file:从文件中获取环境变量,可以指定一个文件路径或路径列表,其优先级低于 environment 指定的环境变量env_file: .env --------------- env_file: - ./common.envenvironment添加环境变量。可以使用数组或字典、任何布尔值,布尔值需要用引号引起来,以确保 YML 解析器不会将其转换为 True 或 Falseenvironment: RACK_ENV: development SHOW: 'ture' ------------------------- environment: - RACK_ENV=development - SHOW=tureexpose:暴露端口,但不映射到宿主机,只被连接的服务访问。expose: - "3000" - "8000"ports:对外暴露的端口定义,和 expose 对应注意:ports属性是物理机和集群网络中服务(而不是ingress)之间的映射,容器访问ingress网络中的某个地址时,是无法根据端口做转发的。若使用了ports,则该服务会生成两个虚拟ip,一个是服务间通信的overlay网络虚拟ip,一个是用于映射服务端口到物理机端口的ingress网络虚拟ip,这时服务注册到注册中心(比如nacos)的ip很可能会是ingress网络虚拟ip,最终会导致基于注册中心服务发现的gateway路由和feign调用不通(超时)。解决方案为:若不用暴露端口给物理机,则只使用expose暴露端口到集群网络,这种可以注册正确的虚拟ip到注册中心若必须暴露端口给物理机,则配置文件中设定容器虚拟网段spring: application: name: @artifactId@ cloud: inetutils: ignored-interfaces: eth.* preferred-networks: 192.168.0详见:https://my.oschina.net/woniuyi/blog/4984748# 暴露端口信息。格式:- "宿主机端口:容器暴露端口",或者只是指定容器的端口,宿主机会随机映射端口。 ports: - "8763:8763" - "8763:8763" # 当使用HOST:CONTAINER格式来映射端口时,如果使用的容器端口小于60可能会得到错误得结果,因为YAML将会解析xx:yy这种数字格式为60进制。所以建议采用字符串格式。extra_hosts:添加容器内主机名映射。类似 docker client --add-host。extra_hosts: - "somehost:162.242.195.82" - "otherhost:50.31.209.229" # 以上会在此服务的内部容器中 /etc/hosts 创建一个具有 ip 地址和主机名的映射关系: 162.242.195.82 somehost 50.31.209.229 otherhosthealthcheck:用于检测 docker 服务是否健康运行。healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] # 设置检测程序 interval: 1m30s # 设置检测间隔 timeout: 10s # 设置检测超时时间 retries: 3 # 设置重试次数 start_period: 40s # 启动后,多少秒开始启动检测程序image:指定服务所使用的镜像名称或镜像ID。如果镜像在本地不存在,Compose将会尝试拉取镜像。# 以下格式都可以: image: redis image: ubuntu:14.04 image: tutum/influxdb image: example-registry.com:4000/postgresql image: a4bc65fd # 镜像idlinks:将指定容器连接到当前连接,可以设置别名,避免ip方式导致的容器重启动态改变的无法连接情况。在用命令行部署容器的时候,经常会需要在一个容器里面访问另外一个容器,这个时候就需要用到 --link参数,一般的使用形式为 --link [引用的其他容器名]:[容器内代表引用容器的字段]。被 --link A:B形式引用的A容器会以[A 的 ip:port] : 'B'的形式出现在容器内的/etc/hosts文件里。也就是说,经过 --link 引入的其他容器A将自己的地址和端口赋给了字段B。在容器内需要获得A的时候,可以直接使用字段B。注意:在v3版本的compose file中,links属性是会自动被 docker stack deploy命令忽略的,因为在v3中,links的功能已经移除。作为替代的是networks属性。links: # 指定服务名称:别名 - docker-compose-eureka-server:compose-eureka # 仅指定服务名称(同时作为别名) - dblogs:日志输出信息--no-color 单色输出,不显示其他颜. -f, --follow 跟踪日志输出,就是可以实时查看日志 -t, --timestamps 显示时间戳 --tail 从日志的结尾显示,--tail=200logging:日志输出控制linux上,容器日志一般存放在 /var/lib/docker/containers/CONTAINER_ID/ 下面,以 json.log 结尾的文件logging: driver: "json-file" options: max-size: "5g" # 日志文件的大小限制在5GB network_mode:设置网络模式network_mode: "bridge" network_mode: "host" network_mode: "none" network_mode: "service:[service name]" network_mode: "container:[container name/id]"networks:配置容器连接的网络,引用顶级 networks 下的条目 。建立一个和services参数并级的networks参数,在networks参数下,可以设立一个或者多个的网络,形成一个网络的networks列表。external: true :可以使用该属性引用已创建好了外部网络,实现跨堆栈的网络共享在每个服务里面可以添加networks属性,该属性下可以包括一个或者多个在先前建立的网络列表中的网络名称,表示该服务在这些网络中可以用这些服务的名称来在其他服务中访问。有时因为各种原因,服务的名称和其他容器内访问的字段会不一致,这个时候还可以在网络名称后面再加一级,aliases别名,别名可以发挥和服务本身的名字一样的作用,在其所属的network中被其他服务访问。aliases :同一网络上的其他容器可以使用服务名称或此别名来连接到对应容器的服务。networks: some-network: # 使用自定义驱动程序 driver: custom-driver-1 other-network: # 引用外部网络 external: true services: some-service: networks: some-network: aliases: - alias1 other-network: aliases: - alias2restartno:是默认的重启策略,在任何情况下都不会重启容器。always:容器总是重新启动。on-failure:在容器非正常退出时(退出状态非0),才会重启容器。unless-stopped:在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器注:swarm 集群模式,请改用 restart_policy。restart: "no" ------------------------------ restart: always ------------------------------ restart: on-failure ------------------------------ restart: unless-stoppedsecurity_opt:修改容器默认的 schema 标签。security-opt: - label:user:USER # 设置容器的用户标签 - label:role:ROLE # 设置容器的角色标签 - label:type:TYPE # 设置容器的安全策略标签 - label:level:LEVEL # 设置容器的安全等级标签tmpfs:在容器内安装一个临时文件系统。可以是单个值或列表的多个值。tmpfs: /run ------------------------------ tmpfs: - /run - /tmpvolumes:将主机的数据卷或着文件挂载到容器里可以直接使用 [HOST:CONTAINER]格式,或者使用[HOST:CONTAINER:ro]格式,后者对于容器来说,数据卷是只读的,可以有效保护宿主机的文件系统。 Compose的数据卷指定路径可以是相对路径,使用 . 或者 .. 来指定相对目录。volumes对应命令行中的 -v 选项,其本身的意义并没有发生什么变化。但是,由于volume在yml文件中是以字符串的形式存在的,所以像 $PWD 这样的写法是不能被接受的,要改成相对路径或绝对路径。# 数据卷的格式可以是下面多种形式 volumes: # 只是指定一个路径,Docker 会自动在创建一个数据卷(这个路径是容器内部的)。 - /var/lib/mysql # 使用绝对路径挂载数据卷 - /opt/data:/var/lib/mysql # 以 Compose 配置文件为中心的相对路径作为数据卷挂载到容器。 - ./cache:/tmp/cache # 使用用户的相对路径(~/ 表示的目录是 /home/<用户目录>/ 或者 /root/)。 - ~/configs:/etc/configs/:ro # 已经存在的命名的数据卷。 - datavolume:/var/lib/mysql # 如果不使用宿主机的路径,可以指定一个volume_driver。volumes_from:从另一个服务或容器挂载其数据卷volumes_from: - service_name - container_nameulimits:覆盖容器默认的 ulimit。ulimits: nproc: 65535 nofile: soft: 20000 hard: 40000
概述脚本:本质是一个文件,文件里面存放的是特定格式的指令,系统可以使用脚本解析器翻译或解析指令并执行(它不需要编译)shell 既是一个用 C 语言编写的应用程序,又是一种脚本语言(应用程序 解析 脚本语言)Shell 提供了一个界面,用户通过这个界面访问操作系统内核的服务。Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 Shell。Shell 编程跟 JavaScript、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。Linux 的 Shell 命令解析器种类众多,常见的有:Bourne Shell(/usr/bin/sh或/bin/sh)Bourne Again Shell(/bin/bash)(大多数Linux 系统默认的 Shell)C Shell(/usr/bin/csh)K Shell(/usr/bin/ksh)Shell for Root(/sbin/sh)本文档关注的是 Bash,也就是 Bourne Again Shell,由于易用和免费,Bash 在日常工作中被广泛使用。同时,Bash 也是大多数Linux 系统默认的 Shell。查看自己linux系统的默认解析器命令:echo $SHELL在一般情况下,人们并不区分 Bourne Shell 和 Bourne Again Shell,所以,像 #!/bin/sh,它同样也可以改为 #!/bin/bash。语法详解编写 shell 脚本文件的时候,最前面要加上一行:#!/bin/bash,因为linux里面不仅仅只有bash一个解析器,还有其它的,它们之间的语法会有一些不同,所以最好加上这一句话,告诉系统要用这个解析器。#! 是一个约定的标记,告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序。注意:shell脚本中将多条命令换行时,命令是从上向下执行,写在上面的命令即使执行错误,下面的命令也会继续执行。shell脚本中将多条命令写在同一行时,使用分号( ; )分隔,写在前面的命令即使执行失败,写在后面的命令也会继续执行。数学运算表达式(( ))是 Shell 数学计算命令,和 C++、C#、Java 等编程语言不同,在 Shell 中进行数据计算不那么方便,必须使用专门的数学计算命令,(( ))就是其中之一。+ # 加 - # 减 * # 乘 / # 除 % # 取余 ** # 幂Shell变量变量分类根据用途可以分为四种变量(变量的划分,每本书都不相同):环境变量:一组为系统内核、系统命令和应用程序提供运行环境而设定的变量的统称内部变量:特定为shell设定的一组变量的统称参数变量:传参的数据。位置参数是传给函数,语句块等等的数据,可以通过$1 $2… $N 以及配合shell内部变量(如$? $@等)进行引用用户自定义变量:用户自己设置的变量。又可分为:局部变量和全局变量局部变量:只在代码块或函数有效,出了代码块或函数,就消失的变量;在代码块或函数中声明的局部变量,必须通过 local 声明,否则它也是对当前shell进程都可见的。全局变量:在脚本中定义,仅在当前Shell脚本中有效,其他Shell脚本进程不能访问,其作用域从定义的位置开始,到脚本结束或被显示删除的地方为止。全局变量可以升级成为临时环境变量;通过export进行声明,使当前进程的子进程也能使用这一变量。临时环境变量只对该运行环境有效,如果执行另外一个shell脚本,这个临时环境变量无能为力环境变量和内部变量的区别和使用(详细:http://blog.sina.com.cn/s/blog_655047c00100hiao.html):相同:均为shell一启动就加载都是配合 $ 引用,并且在脚本中都是一开始就有的,不需要用户再设定不同:环境变量可以添加、修改,用户可以重新定义(详细:https://blog.csdn.net/LLZK_/article/details/53813266)shell内部变量是固定不变的。环境变量环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或多个应用程序将使用到的信息。Linux是一个多用户的操作系统,每个用户登录系统时都会有一个专用的运行环境,通常情况下每个用户的默认的环境都是相同的。这个默认环境就是一组环境变量的定义。每个用户都可以通过修改环境变量的方式对自己的运行环境进行配置。环境变量是和shell紧密相关的,用户登录系统后就启动了一个shell,对于Linux来说一般是bash(Bourne Again shell,Bourne shell(sh)的扩展),也可以切换到其他版本的shell。bash有两个基本的系统级配置文件:/etc/bashrc和/etc/profile。这些配置文件包含了两组不同的变量:shell变量和环境变量。shell变量是局部的,而环境变量是全局的。环境变量是通过shell命令来设置。设置好的环境变量又可以被所以当前用户的程序使用。环境变量的分类根据环境变量的生命周期可以将其分为 永久性环境变量 和 临时性变量根据用户等级的不同又可以将其分为 系统级变量 和 用户级变量对所有用户生效的永久性变量(系统级):这类变量对系统内的所有用户都生效,所有用户都可以使用这类变量。作用范围是整个系统。# 设置方式: # 使用 vi 命令打开 /etc/profile 文件,用export指令添加环境变量 # 步骤示例: # 1.打开配置文件,并按 i ,进入编辑模式 vi /etc/profile # 2.在配置文件末尾添加环境变量 export 环境变量名(一般大写)="值" # 3.使配置文件立即生效 source /etc/profile # 注意: # 1. /etc/profile 只有root(超级用户)才能修改。可以在etc目录下使用 ls -l 查看这个文件的用户及权限 # 2. 添加新的环境变量并退出配置文件后,需要执行命令 source /etc/profile 后才会立即生效。否则在下次重进此用户时才能生效对单一用户生效的永久性变量(用户级)该类环境变量只对当前的用户永久生效。也就是说假如用户A设置了此类环境变量,这个环境变量只有A可以使用。而对于其他的B,C,D,E….用户等等,这个变量是不存在的。# 设置方法:在用户主目录”~”下的隐藏文件 “.bashrc”中添加自己想要的环境变量 # 步骤示例: # 1.打开配置文件,并按 i ,进入编辑模式 vi ~/.bashrc # 2.在配置文件末尾添加环境变量 export 环境变量名(一般大写)="值" # 3.使配置文件立即生效 source ~/.bashrc # 注意: # 系统中可能存在两个文件,.bashrc和.bash_profile(有些系统中只有其中一个) # 原则上来说设置此类环境变量时在这两个文件任意一个里面添加都是可以的。二者设置大致相同 # ~/.bash_profile 是交互式login方式进入bash shell运行。即 .bash_profile 只会在用户登录的时候读取一次 # ~/.bashrc 是交互式non-login方式进入bash shell运行。即 .bashrc 在每次打开终端进行一次新的会话时都会读取 # 查看隐藏文件(.XXX): # 方式1:命令 ls -al # 方式2:命令 echo .*临时有效的环境变量(只对当前shell有效)临时环境变量作用域是当前shell脚本以及当前进程的子进程shell。退出当前shell脚本就消失了# 设置方法:直接使用export指令添加。 环境变量的常用指令# 查看显示环境变量:echo,变量使用时要加上符号“$” echo $PATH # 设置新的临时环境变量 export export 新临时环境变量名=内容 export MYNAME=”LLZZ” # 修改环境变量没有指令,可以直接使用环境变量名进行修改。 MYNAME=”ZZLL” # 查看所有环境变量 # 查看本地定义的所有shell变量 # 删除一个环境变量 unset 变量名 unset MYNAME常用的环境变量(都为大写)PATH:查看命令的搜索路径。通过设置环境变量PATH可以让我们运行程序或指令更加方便。# 查看环境变量PATH echo $PATH # 说明: # 每一个冒号都是一个路径,这些搜索路径都是一些可以找到可执行程序的目录列表。 # 当输入一个指令时,shell会先检查命令是否是内部命令,不是的话会再检查这个命令是否是一个应用程序。 # 然后shell会试着从搜索路径,即PATH中寻找这些应用程序。 # 如果shell在这些路径目录里没有找到可执行文件。则会报错。 # 若找到,shell内部命令或应用程序将被分解为系统调用并传给Linux内核。 # 示例: # 现在有一个c程序test.c通过gcc编译生成的可执行文件a.out(功能:输出helloworld)。平常执行这个a.out的时候是使用 # 方式1:相对路径调用:./a.out (”.”代表当前目录,”/”分隔符) # 方式2:绝对路径调用:/home/lzk/test/a.out # 方式3:通过设置PATH环境变量,直接用文件名调用: a.out (只要可以通过PATH中路径找得到这个可执行文件) # 使用export指令添加PATH中的路径 # 示例:将a.out的路径添加到搜索路径当中 export PATH=$PATH:路径 # PATH中路径是通过冒号“:”进行分隔的,把新的路径加在最后就OKHOME:指定用户的主工作目录,即为用户登录到Linux系统中时的默认目录,即“~”HISTSIZE:保存历史命令记录的条数。用户输入的指令都会被系统保存下来,这个环境变量记录的就是保持指令的条数。一般为1000这些历史指令都被保存在用户工作主目录 “~” 下的隐藏文件 .bash_profile 中 可以通过指令 history 来查看LOGNAME:指当前用户的登录名HOSTNAME:指主机的名称SHELL:指当前用户用的是哪种shellLANG/LANGUGE:和语言相关的环境变量,使用多种语言的用户可以修改此环境变量MAIL:指当前用户的邮件存放目录PS1:第一级Shell命令提示符,root用户是#,普通用户是$PS2:第二级Shell命令提示符,默认是“>”PS3:第三级Shell命令提示符。主要用于select循环控制结构的菜单选择提示符TMOUT:用户和系统交互过程的超时值系统与用户进行交互时,系统提示让用户进行输入,但用户迟迟没有输入,时间超过TMOUT设定的值后,shell将会因超时而终止执行。Shell内部变量位置变量(参数变量)当执行一个Shell脚本的时候,如果希望命令行的到传递的参数信息,就要使用位置变量进行如:./myshell.sh 100 200 可以理解为shell脚本的传参方法# 预定义变量 # 功能描述 $n # n为数字 # $0表示命令本身(执行脚本的命令) # $1-9代表第一个参数到第九个参数 # 10以上的参数需要使用大括号进行包裹如:${10} $* # 传递给函数或脚本的所有参数 $@ # 传递给函数或脚本的所有参数 $# # 代表参数的个数 # $* 和 $@ 都表示传递给函数或脚本的所有参数,不被双引号(" ")包含时,都以"$1" "$2" … "$n" 的形式输出所有参数。 # 但是当它们被双引号(" ")包含时, # "$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数; # "$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数预定义变量预定义变量:shell 设计者事先已经定义好的变量,可以直接在 shell 脚本中使用# 预定义变量 # 功能描述 $? # 命令执行后返回的状态 $$ # 当前进程的进程号(PID) $! # 最近运行的一个后台进程的进程号(PID) $? # 最后一次执行的命令的返回状态 # 若返回 0 :则上一个命令正确执行;若返回 非0(具体数值由命令自己决定),则上一个命令未正常执行 $LINENO # 调测用。用于显示脚本中当前执行的命令的行号 $OLDPWD # 配合cd命令改换到新目录之前所在的工作目录 # 用法:cd $OLDPWD (切换到之前的工作目录,和cd - 功能一样) $PPID # 当前进程的父进程的PID $PWD # 当前工作目录。等同于命令pwd的输出 $RANDOM # 随机数变量。每次引用这个变量会得到一个0~32767的随机数 $REPLY # 通过read命令读入的数据,如果没有被赋值指定变量,则默认赋值到 REPLY 变量中 $SECONDS # 脚本已经运行的时间(以秒为单位)自定义变量:定义、赋值变量是任何一种编程语言都必不可少的组成部分,变量用来存放各种数据。脚本语言在定义变量时通常不需要指明类型,直接赋值就可以,Shell 变量也遵循这个规则。在 Bash shell 中,每一个变量的值都是字符串,无论变量赋值时有没有使用引号,值都会以字符串的形式存储。shell变量和一些编程语言不同,一般shell的变量赋值的时候不用带“$”,而使用或者输出的时候要带“$”。加减乘除的时候要加两层小括号。括号外面要有一个“$”,括号里面的变量可以不用“$”。定义变量Shell 支持以下三种定义变量的方式:variable=value variable='value' variable="value"说明:variable 是变量名,value 是赋给变量的值如果 value 不包含任何空白符(例如空格、Tab 缩进等),那么可以不使用引号如果 value 包含了空白符,那么就必须使用单、双引号包围起来分析:读完命令之后,会对字符串或关键字按照空格切割,切割之后,分为了两个部分:c=he和llo,c=he被理解为一个变量赋值,而llo却找不到匹配的项,并且检索不到相关的命令,所以就会输出这个llo的报错。shell脚本中的变量类型只有整型和字符串注意:赋值号=的周围不能有空格,否则会被解析成命令,报错无此命令。这可能和常见的大部分编程语言都不一样。Shell 变量的命名规范:变量名由数字、字母、下划线组成;必须以字母或者下划线开头;不能使用 Shell 里的关键字(通过 help 命令可以查看保留关键字)。单引号和双引号赋值的区别:定义变量时,变量的值时使用单引号' '包围和双引号" "包围的区别:以单引号' '包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出。这种方式比较适合定义显示纯字符串的情况,即不希望解析变量、命令等的场景。以双引号" "包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。这种方式比较适合字符串中附带有变量和命令并且想将其解析后再输出的变量定义。建议:如果变量的内容是数字,可以不加引号如果需要原样输出就加单引号其他没有特别要求的字符串等最好都加上双引号。定义变量时加双引号是最常见的使用场景示例:#!/bin/bash c="this is a test" d=$((a+b)) f=test # 变量赋值的时候如果只有一个单词可以不用加引号 time=`date` # date 命令用来获得当前的系统时间 date=`date +%s` # data 命令的 %s 格式控制符可以得到当前的 UNIX 时间戳,可以用于计算脚本的运行时间 # UNIX 时间戳是指从 1970 年 1 月 1 日 00:00:00 到目前为止的秒数 echo $c echo "a = "$a # 输出a的 echo "a+b = "$((a+b)) # 输出a+b的值 echo $((a+b*a-b/a+a%b+a**2)) #表达式可以很长变量的赋值# 方式1:“=”并初始化 var=value # 注意:如果value是带有空格的字符串,需要加单或双引号 # 方式2:“=”不初始化 var= # 未赋值变量,值为null # 方式3:read命令 # read命令是读取标准输入的数据,然后存储到指定的变量中。注意:read接收的是标准输入而不是参数,read命令是不接收参数的。自定义变量:引用、修改、删除变量的引用# 方式1 $variable # 方式2(变量名外面的花括号 { }是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界)。推荐 ${variable} # 方式3 “$variable”或“${variable}” # 注:引用变量后,双引号会对于引用结果中空格和一些特殊符号进行的解析或者不解析。而引用结果用的空格在命令中的分隔作用会完全改变一个命令的输出,而特殊符号的是否解析也会影响最终的输出结果。skill="Java" echo "I am good at ${skill}Script" # 如果不给 skill 变量加花括号,写成`echo "I am good at $skillScript"`,解释器就会把 $skillScript 当成一个变量(其值为空),代码执行结果就不是我们期望的样子了。修改变量的值已定义的变量,可以被重新赋值,如:url="http://c.biancheng.net" echo ${url} # 第二次对变量赋值时不能在变量名前加 $,只有在使用变量时才能加 $ url="http://c.biancheng.net/shell/" echo ${url}删除变量使用 unset 命令可以删除变量。语法:unset variable_name注意:变量被删除后不能再次使用unset 命令不能删除只读变量示例:#!/bin/sh myUrl="http://c.biancheng.net/shell/" unset myUrl echo $myUrl # 会没有任何输出自定义变量:设置只读使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。下面的例子尝试更改只读变量,结果报错:#!/bin/bash myUrl="http://c.biancheng.net/shell/" readonly myUrl myUrl="http://c.biancheng.net/" # 会报错自定义变量:数组数组类型变量可以通过循环语句来遍历。定义方式:# 方式1: arr=(str1 str2 str3) # 方式2: arr=("str1" "str2" "str3") # 方式3: arr=('str1' 'str2' 'str3')打印数组长度echo ${#arr[@]}字符串转化为数组# 方式1: str="str1 str2 str3" arr=($str) echo "my arr: ${arr[@]}" # 方式2: str="str1 str2 str3" read -a arr <<< $str echo "my arr: ${arr[@]}" echo "my arr: ${arr[0]}" echo "my arr: ${arr[1]}" # 方式3: str="str1,str2,str3" IFS="," read -a arr <<< $str echo "my arr: ${arr[@]}"Shell命令替换:将命令的结果赋值变量Shell 也支持将命令的执行结果赋值给变量,常见的有以下两种方式:# 两种方式可以完成命令替换,一种是$(),一种是反引号` ` variable=$(commands) variable=`commands` # 说明: # 1.variable 是变量名,commands 是要执行的命令 # 2.commands 可以只有一个命令,也可以有多个命令,多个命令之间以分号;分隔注意:如果被替换的命令的输出内容包括多行(也即有换行符),或者含有多个连续的空白符,那么在输出变量时应该将变量用双引号包围,否则系统会使用默认的空白符来填充,这会导致换行无效,以及连续的空白符被压缩成一个,出现格式混乱的情况。两种变量替换的形式是等价的,可以随意使用反引号和单引号非常相似,容易产生混淆,所以不推荐使用这种方式;使用 $() 相对清晰,有些情况也必须使用 $(),比如$() 支持嵌套,反引号不行$() 仅在 Bash Shell 中有效,而反引号可在多种 Shell 中使用。Shell变量表达式# 表达式 # 说明 ${#string} # 计算$string的长度 ${string:position} # 从pos位置开始提取字符串 ${string:position:len} # 从pos位置开始提取长度为len的字符串 ${string#substr} # 从开头删除最短匹配子串 ${string##substr} # 从开头删除最长匹配子串 ${string%substr} # 从结尾删除最短匹配子串 ${string%%substr} # 从结尾删除最长匹配子串 # 注意:字符串的长度包括空格,但是没有像C语言中那种'\0'字符示例#!/bin/bash str="a b c d e f g h i j" echo "the source string is "${str} #源字符串 echo "the string length is "${#str} #字符串长度 echo "the 6th to last string is "${str:5} #截取从第五个后面开始到最后的字符 echo "the 6th to 8th string is "${str:5:2} #截取从第五个后面开始的2个字符 echo "after delete shortest string of start is "${str#a*f} #从开头删除a到f的字符 echo "after delete widest string of start is "${str##a*} #从开头删除a以后的字符 echo "after delete shortest string of end is "${str%f*j} #从结尾删除f到j的字符 echo "after delete widest string of end is "${str%%*j} #从结尾删除j前面的所有字符包括jshell测试判断:test 、 [ ] 、[[ ]]Shell中的 test 命令和 [ ] 用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。[[ ]] 是 bash 程序语言的关键字。并不是一个命令,[[ ]] 结构比 [ ] 结构更加通用。在 [[ 和 ]] 之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换。支持字符串的模式匹配,使用 =~ 操作符时甚至支持shell的正则表达式。字符串比较时可以把右边的作为一个模式,而不仅仅是一个字符串,比如 [[ hello == hell? ]] ,结果为真。[[ ]] 中匹配字符串或通配符,不需要引号。使用 [[ ]] 条件判断结构,而不是 [ ],能够防止脚本中的许多逻辑错误。比如:&&、||、<、> 操作符能够正常存在于 [[ ]] 条件判断结构中,但是如果出现在 [ ] 结构中的话,会报错。比如可以直接使用 if [[ $a !=1 && $a != 2 ]],如果不使用双括号, 则为 if [ $a -ne 1] && [ $a != 2 ] 或者 if [ $a -ne 1 -a $a != 2 ]。bash 把双中括号中的表达式看作一个单独的元素,并返回一个退出状态码。注意:使用 [ ] 的时候必须要每个变量之间都要有空格,和左右中括号也要有空格,否则报错。数值测试参数说明-eq等于则为真-ne不等于则为真-gt大于则为真-ge大于等于则为真-lt小于则为真-le小于等于则为真字符串测试参数说明=等于则为真!=不相等则为真-z 字符串字符串的长度为零则为真-n 字符串字符串的长度不为零则为真文件测试参数说明-e 文件名如果文件存在则为真-f 文件名如果文件存在且为普通文件则为真-d 文件名如果文件存在且为目录则为真-r 文件名如果文件存在且可读则为真-w 文件名如果文件存在且可写则为真-x 文件名如果文件存在且可执行则为真-s 文件名如果文件存在且至少有一个字符则为真-c 文件名如果文件存在且为字符型特殊文件则为真-b 文件名如果文件存在且为块特殊文件则为真示例#!/bin/bash # 文件测试 echo "Please input two numbers:" read num1 read num2 echo "num1 = "${num1} echo "num2 = "${num2} echo -e "by test\n" test $num1 -eq $num2 && echo "num1 == num2" || echo "num1 != num2" echo -e "by []\n" [ $num1 -eq $num2 ] && echo "num1 == num2" || echo "num1 != num2" # 数值测试 echo "Please input a filename: " # 从标准输入获取一个文件名,并存入filename变量中 read filename echo -e "by test\n" test -f $filename && echo "这是一个普通文件" || echo "这不是一个普通文件" echo -e "by []\n" [ -f $filename ] && echo "这是一个普通文件" || echo "这不是一个普通文件"逻辑操作符:与、或、非Shell 还提供了 与( -a )、或( -o )、非( ! ) 三个逻辑操作符用于将测试条件连接起来,其优先级为: ! 最高, -a 次之, -o 最低。示例:#!/bin/bash # 逻辑操作符和字符串测试 echo "Please input a city name: " # 从标准输入获取一个值,并存入city变量中 read city if "成都" = $city -o "南京" = $city echo '城市是成都或南京' echo '城市既不是成都也不是南京' fishell条件分支结构语句单分支判断语句格式:if 条件 ; then 结果; fi # 最后面一定要有fi,在shell脚本里面,控制分支结构结束都要和开头的单词相反,例如,if <–> fi,case <–> esac示例#!/bin/bash echo "Please input a filename" # read filename:表示从标准输入获取一个文件名,并存入felename变量中 read filename if [ -f $filename ] echo "this file is a ordinary file." fi双分支判断语句格式:if 条件 ; then 结果; else 结果; fi示例#!/bin/bash echo "Please input a filename" read filename if [ -f $filename ] echo "this file is a ordinary file." echo "this file is not a ordinary file." fi多分支判断语句多分支判断有两种(和C语言的一样 ):if-else if 和 case语法:if 条件 ; then 结果; elif 条件; then 结果; else 结果; fiif-else if 示例#!/bin/bash echo "Please input your math grades" read grades if [ $grades -gt 100 ] || [ $grades -lt 0 ];then echo "Please input the number range in 0 - 100" if [ $grades -ge 90 ] && [ $grades -le 100 ] echo "Your grade is excellent." elif [ $grades -ge 80 ] && [ $grades -le 89 ];then echo "Your grade is good." elif [ $grades -ge 70 ] && [ $grades -le 79 ];then echo "Your grade is middle." elif [ $grades -ge 60 ] && [ $grades -le 69 ];then echo "Your grade is passing." echo "Your grade is badly." ficase 示例#!/bin/bash echo "Please input a command" read cmd case $cmd in cpu) echo "The cpu information is" cat /proc/cpuinfo;; mem) echo "The mem information is" cat /proc/meminfo;; device) echo "The device information is" cat /proc/scsi/device_info;; CD-ROM) echo "The CD-ROM information is" cat /proc/sys/dev/cdrom/info;; *) echo "Your input command is invalid" esacshell循环语句for语句格式:for 变量 in 列表 done示例#!/bin/bash arr=("0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "a" "b" "c" "e" "e" "f") # 遍历(不带数组下标。* 和 @ 均可) for value in ${arr[*]} echo $value # 遍历(带数组下标) for (( i = 0 ; i < ${#arr[@]} ; i++ )) echo ${arr[$i]} donewhile语句while语句是只要条件为真就执行下面语句。格式:while 条件 done需要注意的是,这里的条件除了 while true 可以这样写,其它的条件都要用 test或者 []来判断示例# -----------文件名为test.sh----------- #!/bin/bash # $1 为调用脚本时的第1个传参 while [ $i -gt 0 ] echo $i ((i--)) # -----------调用----------- sh test.sh 10 # 文件名test.sh 后面跟的是参数,$0代表文件名,$1代表第一个参数...until语句until语句是只要条件为假就执行下列语句格式:until 条件 done示例#!/bin/bash until [ $i -le 0 ] echo $i ((i--)) doneShell函数格式:[function] funcName() {undefined [return 返回值] }说明:Shell 函数 return的返回值只能是整数,一般用来表示函数执行成功与否,0表示成功,其他值表示失败。如果 return 其他数据,比如一个字符串,报错提示:“numeric argument required”。return 返回值是可选的;如果没有return,则默认返回最后一条语句执行成功与否的状态值传参到函数中使用位置参数;位置参数在函数中的使用:从$1到$n,$0 是文件名函数调用方式:# 方式1:调用函数,然后再使用预定义变量 $? 获取函数返回值 function_name [arg1 arg2 ......] # 方式2:调用函数并将函数的标准输出赋值到一个变量中。注意:不是返回值 value=`function_name [arg1 arg2 ......]` value=$(function_name [arg1 arg2 ......])示例:#!/bin/bash #打印数字 printNum() echo $1 # 位置参数的使用 for i in `seq 2 8` # seq是一个命令,顺序生成一串数字或者字符 printNum $i # 位置参数的传参 done函数输出字符串的方式:使用反引号 ` ` 调用的方式将函数的标准输出赋值到一个变量中,脚本在需要的时候访问这个变量来获得函数的输出值。注意:若函数内进行了多次标准输出,则会将所有的输出值一起赋值到变量中。推荐函数内只进行一次标准输出。#!/bin/bash # 函数输出字符串 getString() echo "abc" return 0 # 函数输出赋值 value=`getString` echo ${value}-xxxxShell脚本的执行方式(以下方式,指定脚本可以使用绝对路径,也可以使用相对路径)路径/xxx.sh:先按照文件中 #! 指定的解析器解析,如果 #! 指定指定的解析器不存在 才会使用系统默认的解析器。注意:这种方式需要被执行文件有可执行权限(x)(chmod +x 文件名:给文件的所有身份都加上执行权限),否则报错该方式,即使脚本就在当前路径,也必须指定:./xxx.shbash xxx.sh:指明先用bash解析器解析,如果bash不存在才会使用默认解析器sh xxx.sh:直接使用默认解析器解析。可以使用相对路径,也可以使用绝对路径. xxx.sh:直接使用默认解析器解析注意:.(点)和文件名直接有一个空格拓展获取本机ip地址脚本命令方法一:ip addr多网卡情况也是返回第一个IPip addr | awk '/^[0-9]+: / {}; /inet.*global/ {print gensub(/(.*)\/(.*)/, "\\1", "g", $2)}' | sed -n '1p'shell方法二:ifconfig -aifconfig -a | grep inet | grep -v 127.0.0.1 | grep -v inet6 | awk '{print $2}' | tr -d "addr:" # 命令解析 - ifconfig -a 和window下执行此命令一样道理,返回本机所有ip信息 - grep inet 截取包含ip的行 - grep -v 127.0.0.1 去掉本地指向的那行 - grep -v inet6 去掉包含inet6的行 - awk { print $2} $2 表示默认以空格分割的第二组 同理 $1表示第一组 - tr -d "addr: 删除"addr:"这个字符串多网卡情况倘若有多个网卡,可能会出现多个不同网段的IP,这个时候如果还是执行上述命令就会返回多个IP,如下:假设某个机器有192...8和10...*网段的IP,现在要实现不同网段的IP地址打印不同的输出,shell脚本如下#!/bin/sh ip=`ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v inet6|awk '{print $2}'|tr -d "addr:"` echo $ip if[[ $ip =="10."*]] echo "该网段是10.*.*.*网段" echo "该网段是192.*.*.*网段" fijq命令:操作 JSON参考:https://www.cnblogs.com/kumufengchun/p/10799888.htmljq命令允许直接在命令行下对JSON进行操作,包括分片、过滤、转换等 jq是用C编写。jq的预编译的二进制文件可以直接在Linux、OS X和windows系统上运行,在linux系统中可以直接用yum安装。准备json串:kumufengchun.json{ "name":"kumufengchun", "age":"18", "city":"beijing", "email":"kumufengchun@gmail.com", "date":"Thursday", "country":"China", "company":["baidu","google","alibaba"] }准备json串:[ "name":"JSON", "good":true "name":"XML", "good":false ]jq基本使用# 用jq .直接查看json文件内容 # 方式1 jq . kumufengchun.json # 方式2 cat kumufengchun.json | jq . # 输出某个字段或者某个索引的值 # 语法: jq '.<key>' # 这里key是字段名称 jq .name kumufengchun.json # 输出:"kumufengchun" # 使用 -r 参数,不含双引号输出结果 jq -r .name kumufengchun.json # 输出: kumufengchun # 输出数组的值 # 语法: jq '.<key>[<value>]' # 这里value是数组的索引整数值 jq '.company[0]' kumufengchun.json # 输出列表、数组的一部分,对其进行切片 # 语法: jq '.<list-key>[s:e]' # 返回的是数组或者列表的index从s开始(包括s)到e结束(不包括e) jq '.company[0:2]' kumufengchun.json # 也可以省略开始的index,只有结束的index,如下,仍然是不包括结束index的值 jq '.company[:3]' kumufengchun.json # 也可以省略结束的index,只有开始的index,如下,输出到最后 jq '.company[1:]' kumufengchun.json # 开始的索引也可以是负数,表示从后边倒着数,从-1开始数 jq '.company[-2:]' kumufengchun.json # 循环输出所有的值,如数组嵌套 # 语法: jq '.[]' jq '.[]' test.json # 输出多个索引的值,可以用逗号分割 # 语法: jq '.key1,.key2' jq '.name,.age' kumufengchun.json # 如果是数组,用中括号括起来要输出的键值,键值先写谁,先输出谁 jq '.company[2,0]' kumufengchun.json # 用管道符号|可以对其进行再次处理 # 语法: jq .[] | .<key1> jq '.[]|.name' test.json # 括号的作用 echo 1 | jq '(.+2)*5' # 输出: echo {1,2,3} | jq '(.+2)*5' # 输出: # length求长度,如果是字符串是求的字符串的长度,如果是数组则求得是数组的长度 cat kumufengchun.json | jq '.[] | length' # 输出所有的keys # 语法: jq keys cat kumufengchun.json | jq 'keys' # 输出数组的keys(索引) cat kumufengchun.json | jq '.company | keys' # 判断存不存在某个键 cat kumufengchun.json | jq 'has("email")' # 输出: true获取 tee 前一个命令返回值管道中的命令,使用 $? 只能获取管道中最后一条命令的返回值PIPESTATUS[n],获取管道中第n个命令的返回值示例:cp abc def 2>&1 | tee a.log # ${PIPESTATUS[0]} 获取的是 cp 命令的返回值 test ${PIPESTATUS[0]} -ne 0 && exiteval:对变量进行两次扫描用法1:执行含有字符串的命令#!/bin/bash cmd="echo aaa" echo $cmd # 输出字符串:echo aaa eval $cmd # 输出字符串命令的执行结果:aaa用法2:创建指向变量的指针a="1111" b="a" eval echo \$$b # 输出:1111 # 注:Shell 在第1轮扫描该命令行时,反斜杠使紧跟其后的 $ 被转义,即用 $ 符号本身。第一轮扫描后,它的命令变为 echo $a给值存入变量中# 一个两列键值的文件 cat aaa.txt # 输出: # name zhangsan # age 20文件中的第一列成为变量名,第二列成为该变量的值while read NAME VALUE do eval $NAME=$VALUE done < aaa.txt echo $name # 输出:zhangsan字符串截取var=http://www.aaa.com/123.htm # 1. # 号截取,删除左边字符,保留右边字符 echo ${var#*//} # 其中 var 是变量名,# 号是运算符,*// 表示从左边开始删除第一个 // 号及左边的所有字符 # 即删除 http:// # 结果是 :www.aaa.com/123.htm # 2. ## 号截取,删除左边字符,保留右边字符。 echo ${var##*/} # ##*/ 表示从左边开始删除最后(最右边)一个 / 号及左边的所有字符 # 即删除 http://www.aaa.com/ # 结果是 123.htm # 3. %号截取,删除右边字符,保留左边字符 echo ${var%/*} # %/* 表示从右边开始,删除第一个 / 号及右边的字符 # 结果是:http://www.aaa.com # 4. %% 号截取,删除右边字符,保留左边字符 echo ${var%%/*} # %%/* 表示从右边开始,删除最后(最左边)一个 / 号及右边的字符 # 结果是:http: # 5. 从左边第几个字符开始,及字符的个数 echo ${var:0:5} # 其中的 0 表示左边第一个字符开始,5 表示字符的总个数。 # 结果是:http: # 6. 从左边第几个字符开始,一直到结束。 echo ${var:7} # 其中的 7 表示左边第8个字符开始,一直到结束。 # 结果是 :www.aaa.com/123.htm # 7. 从右边第几个字符开始,及字符的个数 echo ${var:0-7:3} # 其中的 0-7 表示右边算起第七个字符开始,3 表示字符的个数。 # 结果是:123 # 8. 从右边第几个字符开始,一直到结束 echo ${var:0-7} # 表示从右边第七个字符开始,一直到结束。 # 结果是:123.htm # 注:(左边的第一个字符是用 0 表示,右边的第一个字符用 0-1 表示)父脚本捕捉子脚本(子进程)的中止exit 0:正常运行程序并退出程序;exit 1:非正常运行导致退出程序;exit 后面数值大于0,均为非正常退出子脚本手动进行中止操作#!/bin/bash # 子脚本 echo aaa && exit 1父脚本通过 $?(有 tee 命令时,使用${PIPESTATUS[0]}`)命令获取子脚本的返回值Shell脚本加载另一个脚本#!/bin/bash # 方式1:source # 注意:被加载的脚本,不能缺省路径 source ./first.sh # 方式2:点号(.) # 注意:1.被加载的脚本,不能缺省路径。2.点号与脚本文件之间记得要有空格 . ./first.sh使用source命令和点号(.)是等价的,类似于C/C++中的#include预处理指令,都是将指定的脚本内容加载至当前的脚本中,由一个Shell进程来执行。使用sh命令来调用另外的脚本,会开启新的Shell进程来执行指定的脚本,父进程中的变量在子进程中无法被访问到。启用子shell线程子shell在linux脚本中使用()实现,即在()中的代码会在子shell中执行
概述参考:https://blog.csdn.net/weixin_34249678/article/details/92973397NFS是Network File System的缩写及网络文件系统。NFS主要功能是通过局域网络让不同的主机系统之间可以共享文件或目录。NFS系统和Windows网络共享、网络驱动器类似,只不过windows用于局域网, NFS用于企业集群架构中, 如果是大型网站, 会用到更复杂的分布式文件系统FastDFS,glusterfs,HDFSNFS 本身的服务并没有提供资料传递的协议,在资料传送或者其它相关讯息传递的时候,NFS使用的则是一个称为远程过程调用(Remote Procedure Call, RPC)的协议来协助 NFS 本身的运作。 NFS 可以视作是一个 RPC 服务端。除了启动NFS的主机需要激活RPC的服务,挂载NFS目录的Client机器,也需要同步激活RPC,Server端与Client端藉由RPC的协议来进行program port的对应。NFS主要是管理分享出来的目录,资料的传递是RPC的协议运作的。NFS的目的:实现多台服务器之间文件共享实现多台服务器之间数据一致要让NFS运行起来,对外提供服务,一共需要两个套件的支持。分别是:nfs-utils:NFS的主要组件,提供rpc.nfsd及rpc.mountd这两个NFS daemons与其它相关documents与说明文件、执行档等的组件。这portmap:提供端口映射的功能。客户端要访问nfs server的文件,NFS Server端必须NFS的服务,NFS的服务是由以下两个daemon(后台进程)来完成的:rpc.nfsd:管理 Client 是否能够登入主机的权限,其中还包含这个登入者的 ID 的判别。rpc.mountd:管理NFS的文件系统,当Client端顺利的通过rpc.nfsd而登入主机之后,在它可以使用NFS服务器提供的文件之前,还会经过文件使用权限 (就是那个-rwxrwxrwx与owner, group那几个权限)的认证程序!它会去读NFS的配置文件 /etc/exports 来比对Client的权限,当通过这一关之后Client就可以取得使用NFS文件的权限。故,可以通过 /etc/exports 文件管理NFS共享之目录的使用权限与安全设定linux系统搭建nfs1.所有节点安装NFS# 检查是否安装过了nfs,没有安装nfs则通过联网yum安装或离线本地安装 rpm -qa | grep nfs # 联网yum安装命令 yum -y install nfs-utils # 离线本地安装 yum -y localinstall ./nfs_install_rpm/*.rpm # 说明:nfs_install_rpm为nfs的rpm安装包目录。rpm安装包列表如下 # gssproxy-0.7.0-29.el7.x86_64.rpm # keyutils-1.5.8-3.el7.x86_64.rpm # keyutils-libs-1.5.8-3.el7.x86_64.rpm # keyutils-libs-devel-1.5.8-3.el7.x86_64.rpm # libbasicobjects-0.1.1-32.el7.x86_64.rpm # libcollection-0.7.0-32.el7.x86_64.rpm # libevent-2.0.21-4.el7.x86_64.rpm # libini_config-1.3.1-32.el7.x86_64.rpm # libnfsidmap-0.25-19.el7.x86_64.rpm # libpath_utils-0.2.1-32.el7.x86_64.rpm # libref_array-0.1.5-32.el7.x86_64.rpm # libtirpc-0.2.4-0.16.el7.x86_64.rpm # libverto-libevent-0.2.5-4.el7.x86_64.rpm # nfs-utils-1.3.0-0.68.el7.x86_64.rpm # rpcbind-0.2.0-49.el7.x86_64.rpm2.所有节点创建目录test ! -d /nfs && mkdir -p /nfs3.部署nfs服务端# 添加防火墙规则 firewall-cmd --permanent --add-service=rpc-bind firewall-cmd --permanent --add-service=mountd firewall-cmd --permanent --add-port=2049/tcp firewall-cmd --permanent --add-port=2049/udp firewall-cmd --reload # 配置NFS共享目录 vi /etc/exports # 插入以下内容 /nfs 192.168.52.0/24(rw,no_root_squash,no_all_squash,sync) # 重新加载NFS配置,启动服务,添加开机自启 exportfs -r && service rpcbind start && service nfs start && systemctl enable nfs-server4.部署nfs客户端# 查看共享目录列表。可跳过 showmount -e 192.168.52.141 # 挂载目录 mount -t nfs 192.168.52.141:/nfs /nfs # 将挂载命令写入 /etc/fstab文件:开机时系统就自动挂载 NSF vi /etc/fstab # 追加插入以下内容 192.168.52.141:/nfs /nfs nfs defaults,vers=4.0 # 测试挂载目录:在/mynfs目录中创建文件,然后在NFS服务端的共享目录中查看文件是否存在,存在则代表共享成功。# 卸载NFS目录 umount /nfsNFS的套件结构介绍/etc/exports:NFS的主要配置文件。系统可能没有预设,所以这个文件不一定会存在,可能要使用 vi 主动的建立这个文件/usr/sbin/exportfs:维护NFS共享资源的命令,可以利用这个命令重新共享 /etc/exports 变更的目录资源、将NFS Server分享的目录卸载或重新分享等等,这个指令是NFS系统里面相当重要的。/usr/sbin/showmount:一个重要的NFS命令。exportfs是用在NFS Server端,而showmount则主要用在Client端。这个showmount可以用来察看NFS共享出来的目录资源。/var/lib/nfs/*tab:在NFS服务器的登录档都放置到/var/lib/nfs/目录里面,在该目录下有两个比较重要的登录档,一个是etab ,主要记录了 NFS 所分享出来的目录的完整权限设定值;另一个xtab则记录曾经连结到此NFS主机的相关客户端资料。/var/lib/nfs/rmtab:状态文件,列出了挂接导出文件的远程客户机清单。/etc/exports配置文件的语法与参数# 格式示例 # vi /etc/exports /tmp 192.168.1.0/24(ro) localhost(rw) *.ev.ncku.edu.tw(ro,sync) [分享目录] [第一个主机(权限)] [可用主机名] [可用域名]/etc/exports文件的内容非常简单,每一行由共享路径,客户端列表以及每个客户端后紧跟的访问选项构成:[共享的目录] [主机名或IP(参数,参数)]参数是可选的,当不指定参数时,nfs将使用默认选项。默认的共享选项是 sync,ro,root_squash,no_delay当主机名或IP地址为空时,则代表共享给任意客户机提供服务。当将同一目录共享给多个客户机,但对每个客户机提供的权限不同时,可以这样:[共享的目录] [主机名1或IP1(参数1,参数2)] [主机名2或IP2(参数3,参数4)]# NFS共享的常用参数(注意:参数之间只用逗号(,)分隔,中间不能有空格!): ro # read-only,只读访问权限 rw # read-write,可读写的权限 sync # 资料同步写入到内存与硬盘中 async # 资料会先暂存于内存中,而非直接写入硬盘 secure # NFS通过1024以下的安全TCP/IP端口发送 insecure # NFS通过1024以上的端口发送 wdelay # 如果多个客户要写入NFS目录,则归组写入(默认) no_wdelay # 如果多个客户要写入NFS目录,则立即写入,当使用async时,无需此设置。 hide # 在NFS共享目录中不共享其子目录 no_hide # 共享NFS目录的子目录 subtree_check # 如果共享/usr/bin之类的子目录时,强制NFS检查父目录的权限(默认) no_subtree_check # 和上面相对,不检查父目录权限 no_all_squash # 保留共享文件的UID和GID(默认) all_squash # 不论登入 NFS 的使用者身份为何, 他的UID和GID映射匿名客户anonymous # (通常也就是 nobody(nfsnobody)),适合公用目录。 root_squash # 在登入NFS主机使用共享之目录的使用者如果是root时,那么这个使用者的权限将被映射成为匿名使用者 # 通常它的 UID 与 GID 都会变成nobody(nfsnobody) 那个系统帐号的身份的权限;(默认) no_root_squas # 登入NFS主机使用共享目录的使用者,如果是root的话,那么对于这个共享的目录来说,它就具有root的权限! # 这个项目『极不安全』,不建议使用! anonuid=xxx # 指定NFS服务器/etc/passwd文件中匿名客户的UID anongid=xxx # 指定NFS服务器/etc/passwd文件中匿名客户的GID/etc/exports 中client的书写规则单个主机:可以用短名及完全限定名,或者用IP地址;例如student01、student01.flying.com.cn或者192.168.10.1都是合法的主机名。Net-Group:可以列出/etc/netgroup文件中或NFS网组映射中定义的整组主机。网组名以@开头。通配符主机:.discuz.net *.*.comsenz.com掩码:192.168.1.0/255.255.255.0实例# vi /etc/exports # 1.将/tmp共享出去给任意主机使用,所有人都可以读写、存取,同时让root写入的文件还是具有root的权限 # 任何人都可以用 /tmp ,用通配字符(*)来处理主机名称,重点在no_root_squash /tmp *(rw,no_root_squash) # 2.同一目录针对不同范围开放不同权限 # 将一个公共的目录/home/public公开出去,但是只有限定局域网内192.168.0.0/24这个网段可以读写,其他人则只能读取 # 通配字符仅能用在主机名称的分辨上面,IP或网段就只能用192.168.0.0/24的状况,不可以使用192.168.0.* /home/public 192.168.0.0/24(rw) *(ro) # 3.仅给某个单一主机使用的目录设置 # 将一个私人的目录 /home/test 开放给192.168.0.100这个Client端的机器来使用 /home/test 192.168.0.100(rw) # 只要设定 IP 正确即可! # 开放匿名登入的情况 # 让*.linux.org网域的主机,登入我的NFS主机时,可以存取 /home/linux,但是其存资料的时候,使的UID与GID都变成40这个身份的使用者, 假设NFS服务器上的UID 40已经有设定妥当: # # 如果要开放匿名,那么重点是all_squash,并且要配合anonuid /home/linux *.linux.org(rw,all_squash,anonuid=40,anongid=40)showmount:NFS的连接观察showmount [-ae] [hostname|IP] # 参数: -a # 这个参数是一般在NFS SERVER上使用,是用来显示已经mount上本机nfs目录的cline机器 -e # 显示主机的 /etc/exports 所共享的目录。exportfs:维护NFS共享资源每次修改了配置文件后,可以用exportfs命令重新扫描/etc/exports文件,来使改动立刻生效。exportfs [-aruv] # 参数: -a # 全部挂载(或卸载)/etc/exports文件内的设置 -r # 重新挂载/etc/exports里面的设置,此外,亦同步更新/etc/exports及/var/lib/nfs/xtab 的内容! -u # 卸载某一目录 -v # 在export的时候,将分享的目录显示到屏幕上!实例:# 重新挂载一次 /etc/exports的设置 exportfs -arv # 输出: # exporting 192.168.0.100:/home/test # exporting 192.168.0.0/24:/home/public # exporting *.linux.org:/home/linux # exporting *:/home/public # exporting *:/tmp # 全部卸载 exportfs -auv/etc/fstab:开机自动挂载etc/fstab文件的作用磁盘被手动挂载之后都必须把挂载信息写入/etc/fstab这个文件中,否则下次开机启动时仍然需要重新挂载。系统开机时会主动读取/etc/fstab这个文件中的内容,根据文件里面的配置挂载磁盘。这样只需要将磁盘的挂载信息写入这个文件中就不需要每次开机启动之后手动进行挂载了。
入门知识软件结构C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程:就是在一定的协议下,实现两台计算机的通信的程序。网络通信的大致流程为:一个数据包经由应用程序产生,进入到协议栈中进行各种报文头的包装,然后操作系统调用网卡驱动程序指挥硬件,把数据发送到对端主机。整个过程的大体的图示如下:协议栈其实是位于操作系统中的一些协议的堆叠,这些协议包括 TCP、UDP、ARP、ICMP、IP等。通常某个协议的设计都是为了解决特定问题的,比如:TCP 的设计就负责安全可靠的传输数据UDP 设计就是报文小,传输效率高ARP 的设计是能够通过 IP 地址查询物理(Mac)地址ICMP 的设计目的是返回错误报文给主机IP 设计的目的是为了实现大规模主机的互联互通网络通信常见协议:UDP|TCPUDP:面向无连接的协议,通信的双方不用建立连接,可以直接发送数据好处:效率高,耗资小弊端:不安全,容易丢失数据TCP:面向连接协议,客户端和服务器端必须经过3次握手建立逻辑连接,才能通信好处:安全弊端:效率低三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。第一次握手,客户端向服务器端发出连接请求,等待服务器确认。// 服务器你死了吗?第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。// 我活着 啊!!第三次握手,客户端再次向服务器端发送确认信息,确认连接。// 我知道了!!TCP/IP 协议TCP/IP协议参考模型TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等传输层:TCP,UDP网络层:IP,ICMP,OSPF,EIGRP,IGMP数据链路层:SLIP,CSLIP,PPP,MTU每一抽象层建立在低一层提供的服务上,并且为高一层提供服务。IP地址IP地址:就相当于计算机的身份号(唯一)ip地址的作用:具有唯一性,在网络中可以通过ip地址找到另外一台计算机ip地址分类:ipv4:ip地址由4个字节组成,一个字节8位(比特位 1,0)二进制:11001101.11001100.11000001.11001111十进制:192.168.0.106每个字节的范围:0-255(2^8),ip地址第一位不能为0 ip地址的数量:42亿(2^32=4294967296个)问题:随着计算机的增多,ip地址面临枯竭(全球IPv4地址在2011年2月分配完毕)不够用,就出了ipv6地址ipv6:ip地址由16个字节组成,一个字节8位(比特位 1,0)ip地址的数量:2^128=3.4028236692093846346337460743177e+38号称可以为地球上每一粒沙子编写一个ip地址为了表示方便使用十六进制:fe80::a8a6:b83c:8b8b:2685%17一些常用dos命令:dos窗口 win+r ==> cmd ==> dos窗口1.查看电脑的IP信息 命令:ipconfig -------------------------------------------------- Windows IP 配置 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::a8a6:b83c:8b8b:2685%17 IPv4 地址 . . . . . . . . . . . . : 192.168.0.106 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.0.1 -------------------------------------------------- 2.测试你的电脑和指定ip地址的电脑是否可以连通 命令:ping ip地址 -------------------------------------------------- C:\Users\Administrator>ping 192.168.0.222 没有ping通 正在 Ping 192.168.0.222 具有 32 字节的数据: 来自 192.168.0.106 的回复: 无法访问目标主机。 来自 192.168.0.106 的回复: 无法访问目标主机。 来自 192.168.0.106 的回复: 无法访问目标主机。 来自 192.168.0.106 的回复: 无法访问目标主机。 -------------------------------------------------- C:\Users\Administrator>ping www.baidu.com 正在 Ping www.a.shifen.com [61.135.169.121] 具有 32 字节的数据: 来自 61.135.169.121 的回复: 字节=32 时间=6ms TTL=56 来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56 来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56 来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56 -------------------------------------------------- 3.ping本机的ip地址(你自己电脑的ip地址) 命令:ping 127.0.0.1 或 ping localhost 端口号端口号是一个逻辑端口,无法直接看到,使用一些软件可以看到(电脑管家,360.…)当打开网络软件(联网使用)时,操作系统就会为这个网络软件分配一个随机的端口号或者网络软件在打开的时候和操作系统要指定的端口号端口号是由两个字节组成,表示的范围:2^16=0-65535 之间1024之前的端口号,不能使用,已经被操作系统分配给一些已知的网络软件。注意:各个网络软件的端口号是不能重复常用的端口号:80端口:网络端口数据库:mysql:3306, oracle:1521Tomcat服务:8080保证数据能准确无误发送到对方计算机的某一个软件上,使用 ip地址:端口号测试端口号是否连通:telnet ip地址:端口号InetAddress类:获取IP地址java.net.InetAddress:描述计算机的ip地址此类表示互联网协议 (IP) 地址。可以使用InetAddress类中的方法获取到计算机的ip地址创建对象的方式:静态方法static InetAddress getLocalHost() // 返回本地主机(你自己电脑的ip地址对象)。 static InetAddress getByName(String host) // 在给定主机名的情况下确定主机的 IP 地址。 /* 参数: String host:可以传递主机名称、ip地址、域名 */ 非静态的方法:String getHostAddress() // 返回 IP 地址字符串(以文本表现形式)。 String getHostName() // 获取此 IP 地址的主机名。Socket:套接字应用程序比如浏览器、电子邮件、文件传输服务器等产生的数据,会通过传输层协议进行传输。而应用程序是不会和传输层直接建立联系的,而是有一个能够连接应用层和传输层之间的套件,这个套件就是 Socket。阻塞/非阻塞、同步/异步阻塞:等待结果,什么事都不能做非阻塞:可以做别的事情同步:主动获取结果异步:等待通知结果BIO:Block(阻塞的) IO 【同步、阻塞】NIO:Non-Block(非阻塞的)(同步)IO 【同步、非阻塞】——JDK1.4开始AIO:Asynchronous(异步-非阻塞)IO 【异步、非阻塞】 ——JDK1.7开始TCP/IP 通信TCP 通信的客户端:Socket作用:主动和服务器经过3次握手建立连接通路,给服务器发送数据,读取服务器回写的数据表示客户端的类:java.net.Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字:封装了IP地址和端口号的网络单位构造方法:public Socket(InetAddress address, int port) // 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。 public Socket(String host, int port) // 创建一个流套接字并将其连接到指定主机上的指定端口号。 /* 参数: InetAddress address | String host:传递服务器的ip地址 int port:服务器的端口号 */ 成员方法:OutputStream getOutputStream() // 返回此套接字的输出流。 InputStream getInputStream() // 返回此套接字的输入流。 void shutdownOutput() // 禁用此套接字的输出流。 // 对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列注意:创建客户端Socket对象的时候,客户端会根据服务器的ip地址和端口号和服务器经过三次握手连接连接通路服务器已经启动了,服务器的ip地址和端口号填写正确:握手成功,创建好Socket对象服务器没有启动,服务器的ip地址和端口号填写错误:握手失败,会抛出连接异常ConnectException: Connection refused: connect客户端和服务器之间进行数据传输,不能使用自己创建的流对象(只能和本地硬盘之间进行读写)。使用Socket对象中提供的网络流对象TCP 通信的服务端:ServerSocket作用:接收客户端的请求和客户端经过3次握手建立连接通路;读取客户端发送的数据,给客户端回写(发送)数据表示服务器的类:java.net.ServerSocket;此类实现服务器套接字。构造方法:public ServerSocket(int port) // 创建绑定到特定端口的服务器套接字。成员方法:Socket accept() // 侦听并接受到此套接字的连接。 /* 使用accpet方法,会一直监听客户端的请求 有客户端请求服务器,accept方法就会获取到请求的客户端Socket对象 没有客户端请求服务器,accept方法会进入到阻塞状态,一直等待 */注意: 服务器启动的时候,抛出了以下的异常:说明服务器使用的端口号已经被占用了,需要更换端口号 java.net.BindException: Address already in use: JVM_Bind文件上传案例文件上传的客户端读取本地文件,上传到服务器中,读取服务器回写的"上传成功!"文件上传就是文件的复制: 数据源:c:\1.jpg 目的地:服务器中import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class Demo01TCPClient { public static void main(String[] args) throws IOException { //1.创建本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源 FileInputStream fis = new FileInputStream("c:\\1.jpg"); //2.创建客户端Socket对象,构造方法绑定服务器的ip地址和端口号 Socket socket = new Socket("127.0.0.1", 9999); //3.使用客户端Socket对象中的方法getOutputStream,获取网络字节输出流OutputStream对象 OutputStream os = socket.getOutputStream(); //4.使用本地字节输入流FileInputStream对象中的方法read,读取要上传的而文件 byte[] bytes = new byte[1024]; int len = 0; while ((len = fis.read(bytes)) != -1){ //5.使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器中 os.write(bytes, 0, len); // 上传结束 socket.shutdownOutput(); //6.使用客户端Socket对象中的方法getInputStream,获取网络字节输入流InputStream对象 InputStream is = socket.getInputStream(); //7.使用网络字节输入流InputStream对象中的方法read,读取服务器回写的"上传成功!" while ((len = is.read(bytes)) != -1){ System.out.println(new String(bytes, 0, len)); //8.释放资源(FileInputStream对象, Socket) fis.close(); socket.close(); }文件上传的服务器端(多线程)读取客户端上传的文件,把文件保存到服务器的硬盘上,给客户端回写"上传成功!"文件上传就是文件的复制: 数据源: 客户端上传的文件 1.jpg 目的地: 服务器的硬盘中 d:\upload\1.jpgimport java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class Demo02TCPServer { public static void main(String[] args) throws IOException { //1.判断d盘有没有upload文件夹,没有则创建 File file = new File("d:\\upload"); if(!file.exists()){ file.mkdir(); //2.创建服务器ServerSocket对象,和系统要指定的端口号9999 ServerSocket server = new ServerSocket(9999); // 一直循环监听客户端的请求(轮询) while(true){ //3.使用服务器ServerSocket对象中的方法accpet,监听并获取请求的客户端Socket对象 Socket socket = server.accept(); // 开启一个新的线程完成这个客户端的文件上传 new Thread(()->{ try { //4.使用客户端Socket对象中的方法getInputStream,获取网络字节输入流InputStream对象 InputStream is = socket.getInputStream(); 自定义一个文件的名称,防止名称的重复,覆盖之前的文件 规则:不重复 ==> 自己写 ==> 域名 + 毫秒值 + 随机数 String fileName = "cormorant" + System.currentTimeMillis() + new Random().nextInt(9999999) + ".jpg"; //5.创建本地字节输出流FileOutputStream对象,绑定要输出的目的地 //FileOutputStream fos = new FileOutputStream(file + "\\1.jpg"); //d:\\upload\\1.jpg FileOutputStream fos = new FileOutputStream(file + File.separator + fileName); //6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件 byte[] bytes = new byte[1024]; int len = 0; while ((len = is.read(bytes)) != -1){ //7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件,写到服务器的硬盘中保存 fos.write(bytes, 0, len); //8.使用客户端Socket对象中的方法getOutputStream,获取网络字节输出流OutputStream对象 //9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功!" socket.getOutputStream().write("上传成功".getBytes()); //10.释放资源(fos, Socket, ServerScoket) fos.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); }).start(); //让服务器一直启动,不在关闭了 //server.close(); }文件上传的阻塞问题/* 解决:上传完图片之后,给服务器写一个结束标记,告之服务器文件已经上传完毕,无需在等待了 Socket对象中的方法 void shutdownOutput() 禁用此套接字的输出流。 对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列 socket.shutdownOutput();Socket 通信概述Linux 内核协议簇中有几十种通讯协议,AF-INET就是常见 TCP/IP 的通讯方式,AF-UNIX 是用于本机线程间通讯一种IPC机制,从用户角度看,所采用通讯模式相差不大,但就原理上看,相差较大。AF_INET 域 socket 通信过程典型的TCP/IP四层模型的通信过程:发送方、接收方依赖 IP:Port 来标识,即将本地的 socket 绑定到对应的 IP 端口上,发送数据时,指定对方的 IP 端口,经过Internet,可以根据此 IP 端口最终找到接收方;接收数据时,可以从数据包中获取到发送方的IP端口。发送方通过系统调用 send() 将原始数据发送到操作系统内核缓冲区中。内核缓冲区从上到下依次经过TCP层、IP层、链路层的编码,分别添加对应的头部信息,经过网卡将一个数据包发送到网络中。经过网络路由到接收方的网卡。网卡通过系统中断将数据包通知到接收方的操作系统,再沿着发送方编码的反方向进行解码,即依次经过链路层、IP层、TCP层去除头部、检查校验等,最终将原始数据上报到接收方进程。AF_UNIX 域 socket 通信过程典型的本地 IPC,类似于管道,依赖路径名标识发送方和接收方。即发送数据时,指定接收方绑定的路径名,操作系统根据该路径名可以直接找到对应的接收方,并将原始数据直接拷贝到接收方的内核缓冲区中,并上报给接收方进程进行处理。同样的接收方可以从收到的数据包中获取到发送方的路径名,并通过此路径名向其发送数据。异同及应用场景相同点操作系统提供的接口 socket(),bind(),connect(),accept(),send(),recv(),以及用来对其进行多路复用事件检测的 select(),poll(),epoll() 都是完全相同的。收发数据的过程中,上层应用感知不到底层的差别。不同点建立 socket 传递的地址域,及bind()的地址结构稍有区别:socket() 分别传递不同的域 AF_INET 和 AF_UNIXbind() 的地址结构分别为sockaddr_in(制定IP端口)和 sockaddr_un(指定路径名)AF_INET 需经过多个协议层的编解码,消耗系统cpu,并且数据传输需要经过网卡,受到网卡带宽的限制。AF_UNIX 数据到达内核缓冲区后,由内核根据指定路径名找到接收方socket对应的内核缓冲区,直接将数据拷贝过去,不经过协议层编解码,节省系统 cpu,并且不经过网卡,因此不受网卡带宽的限制。AF_UNIX 的传输速率远远大于 AF_INETAF_INET 不仅可以用作本机的跨进程通信,同样的可以用于不同机器之间的通信,其就是为了在不同机器之间进行网络互联传递数据而生。而 AF_UNIX 则只能用于本机内进程之间的通信。应用场景AF_UNIX 由于其对系统 cpu 的较少消耗,不受限于网卡带宽,及高效的传递速率,本机通信则首选 AF_UNIX 域AF_INET 多用于跨机器之间的通信AFUNIX Server Socket 通信Java AFUNIXServerSocket类参考:https://vimsky.com/examples/detail/java-class-org.newsclub.net.unix.AFUNIXServerSocket.html依赖: <dependency> <groupId>com.kohlschutter.junixsocket</groupId> <artifactId>junixsocket-core</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.kohlschutter.junixsocket</groupId> <artifactId>junixsocket-common</artifactId> <version>2.3.2</version> </dependency>使用示例1:runimport org.newsclub.net.unix.AFUNIXServerSocket; @AllArgsConstructor public class SocketJob implements Runnable{ private String path; public void run() throws IOException { File socketFile = new File(path); socketFile.deleteOnExit(); final ExecutorService executorService = Executors.newCachedThreadPool(); try (AFUNIXServerSocket server = AFUNIXServerSocket.newInstance()) { // 绑定路径 server.bind(new AFUNIXSocketAddress(socketFile)); System.out.println("server: " + server); while (!Thread.interrupted()) { System.out.println("Waiting for connection..."); executorService.execute(new ClientConnection(this, server.accept())); } finally { executorService.shutdown(); }使用示例2:mainimport org.newsclub.net.unix.AFUNIXServerSocket; public static void main(String[] args) throws IOException { final File socketFile = new File(new File(System.getProperty("java.io.tmpdir")), "junixsocket-test.sock"); try (AFUNIXServerSocket server = AFUNIXServerSocket.newInstance()) { server.bind(new AFUNIXSocketAddress(socketFile)); System.out.println("server: " + server); while (!Thread.interrupted()) { System.out.println("Waiting for connection..."); try (Socket sock = server.accept()) { System.out.println("Connected: " + sock); try (InputStream is = sock.getInputStream(); OutputStream os = sock.getOutputStream()) { byte[] buf = new byte[128]; int read = is.read(buf); System.out.println("Client's response: " + new String(buf, 0, read)); System.out.println("Saying hello to client " + os); os.write("Hello, dear Client".getBytes()); os.flush(); }socket 通讯消息接收import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; * AF_UNIX 域 socket 通讯消息接收 @AllArgsConstructor @Slf4j public class SocketMsgReceiveBO implements Runnable { * AF_UNIX 域 docker socket 客户端 private Socket socketClient; @Override public void run() { log.info("start socket client receive msg"); if (socketClient == null || socketClient.isClosed()){ log.error("socket client is unavailable"); return; StringBuffer acceptMsgBuffer = new StringBuffer(); try (InputStream is = socketClient.getInputStream()) { byte[] buf = new byte[2048]; int readLenth; // 退出当前循环依赖于socketClient端主动关闭链接,否则当前线程一致等待通讯 while ((readLenth = is.read(buf)) != -1){ acceptMsgBuffer.append(new String(buf, 0, readLenth, StandardCharsets.UTF_8)); log.info("server accept msg:{}", acceptMsgBuffer); ArrayList<String> validJsonMsgList = new ArrayList<>(); // 获取socket回调消息中的符合json格式的字符串 String validJsonMsg = getValidJsonStrFromMsg(acceptMsgBuffer.toString()); while (StringUtils.isNotBlank(validJsonMsg)){ // 添加到待解析消息集合中 validJsonMsgList.add(validJsonMsg); * 比较当前合法json格式的消息和原始消息的长度 * 若一致,则原始消息即为一个完整的符合json格式的消息,清空消息缓存Buffer * 若不一致,则从原始消息的符合json格式片段的最后一个字符之后开始截取,等待拼接到下一次的消息 String originMsg = acceptMsgBuffer.toString(); if (validJsonMsg.length() == originMsg.length()){ acceptMsgBuffer = new StringBuffer(); break; } else { acceptMsgBuffer = new StringBuffer(originMsg.substring(validJsonMsg.length())); log.info("all wait parse valid json msgs:{} and nextBuffer:{}", JSON.toJSONString(validJsonMsgList), acceptMsgBuffer); // 解析合法的消息 validJsonMsgList.forEach(msg -> parseSocketMsg(msg)); } catch (Exception e){ log.error("failed to receive socket client msg", e); } finally { String msg = acceptMsgBuffer.toString(); if (StringUtils.isNotBlank(msg) && isValidSocketMsg(msg)){ log.info("finally parse last msg:{}", msg); parseSocketMsg(msg); if (!socketClient.isClosed()){ // 若socket客户端未自动关闭,则主动关闭 try { socketClient.close(); log.info("success to close socket client"); } catch (IOException e){ log.error("failed to close socket client"); * 获取socket回调消息中的合法json格式的字符串 private String getValidJsonStrFromMsg(String msg) { if (StringUtils.isBlank(msg)){ return ""; * 为防止出现粘包现象,对该消息进行循环判断,一旦发现内部存在合法的json格式,即任务是有效的消息 * 依赖每次对消息进行截取长度少1的方式,以保证当不能找到匹配的JSON也能退出循环 while (!isValidSocketMsg(msg) && StringUtils.isNoneBlank(msg)){ // 从0开始截取到当前字符串的最后一个字符。substring(startIndex, endIndex)方法入参前闭后开区间 msg = msg.substring(0, msg.length()-1); int lastLeftBraceIndex = msg.lastIndexOf("}"); if (lastLeftBraceIndex < 0){ // 当字符串中没有"}"字符时,退出循环 break; // 从0截取到字符串的最后一个"}"字符 msg = msg.substring(0, lastLeftBraceIndex + 1); if (isValidSocketMsg(msg)){ return msg; return ""; * 如果接收到的消息非空,且符合json格式,则认为是合法的消息,可以进行解析 private boolean isValidSocketMsg(String msg) { if(StringUtils.isBlank(msg)) return false; try { JSON.parse(msg); return true; } catch (Exception e){ return false; * 解析符合json格式的消息 private void parseSocketMsg(String msgStr) { JSONObject msgJsonObj = JSON.parseObject(msgStr); // TODO 根据约定的通讯字段进行相关业务逻辑
IO流概述IO流的概述和分类输入流:把硬盘上的数据读取到内存中字符输入流:读取字符字节输入流:读取字节输出流:把内存中的数据写入到硬盘中字符输出流:写入字符字节输出流:写入字节字符流和字节流的区别字节流读取的和写入都是字节;字符流读取的和写入的都是字符使用字节流可以读写任意的文件,所有的数据(文本,音乐,视频,图片...),都是以字节的方式存储的使用字节流读取的文件中若包含中文,因一次只读写一个字节(1/2 GBK,1/3 UTF-8 个中文),使用起来不方便,可能会出现乱码使用字符流读取含有中文文本文件,一次读取一个字符(中文,英文,数字,符号...),使用起来很方便什么时候使用字符流:读写文本文件(使用记事本打开能看懂)什么时候使用字节流:读写非文本文件(图片,视频,音频...)关闭和刷新的区别flush:把内存缓冲区中的数据刷新到文件中,刷新完之后,流对象可以继续使用close:释放资源(释放流相关的所有的系统资源和流对象),在释放资源之前,会把内存缓冲区中的数据刷新到文件中刷新完之后,流对象就已经关闭了,就不能在使用了import java.io.FileWriter; import java.io.IOException; public class Demo02Writer { public static void main(String[] args) throws IOException { FileWriter fw = new FileWriter("day21\\e.txt"); fw.write(66); // B fw.flush(); // 把内存缓冲区中的数据刷新到文件中,刷新完之后,流对象可以继续使用 fw.write(67); // C fw.close(); // 在释放资源之前,把内存缓冲区中的数据刷新到文件中,刷新完之后,流对象就已经关闭了,就不能在使用了 fw.write(68); // IOException: Stream closed }IO流的异常处理(JDK7后)自动释放资源,不用再手动释放资源格式:try( // 定义流对象 AAA aaa = new AAA(); BBB bbb = new BBB(); 可能产生异常的代码 aaa.read(); bbb.write(); }catch(定义一个异常相关的变量,接收异常对象){ 异常的处理逻辑 }注意:在 try 的后边增加一个(),在()中定义流对象那么这些流对象的作用域,就只在 try 中有效,执行完 try 中的代码,会自动释放流对象省略 finally注意 try 后边的小括号中,不是任意对象都可以放,只有实现了 Closeable 接口的对象才能放import java.io.*; import java.util.Date; public class Demo02JDK7After { public static void main(String[] args) { //定义流对象 FileWriter fw = new FileWriter(new File("day21\\1.txt"), true); for (int i = 1; i <= 10; i++) { fw.write("你猜猜,我能写入到文件中吗?\r\n"); } catch(IOException e) { e.printStackTrace(); FileInputStream fis = new FileInputStream("c:\\2.jpg"); FileOutputStream fos = new FileOutputStream("d:\\2.jpg"); byte[] bytes = new byte[1024]; int len = 0; while ((len = fis.read(bytes)) != -1){ fos.write(bytes, 0, len); }catch (IOException e) { e.printStackTrace(); }字节流InputStream:字节输入流超类java.io.InputStream:字节输入流(此抽象类是表示字节输入流的所有类的超类)InputStream 里边定义了所有字节输入流中共性的成员方法,所有的字节输入流都可以使用这些方法共性的成员方法:int read() // 从输入流中读取数据的下一个字节。 int read(byte[] b) // 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。 /* 1.read方法的参数byte[]字节数组的作用: 起到缓冲作用,把读取到的字节依次存储到数组中 把数组一次性的由操作系统返回JVM,由JVM返回给java程序,效率高 数组的长度一般都定义为1024或者1024的整数倍 2.read方法的返回值int是什么? 每次读取的有效字节个数。int=-1 表示读取结束。 void close() // 关闭此输入流并释放与该流关联的所有系统资源。OutputStream:字节输出流超类java.io.OutputStream:字节输出流(此抽象类是表示输出字节流的所有类的超类)OutputStream 里边定义了所有字节输出流中共性的成员方法,所有的字节输出流都可以使用共性的成员方法:public void close() // 关闭此输出流并释放与此流相关联的任何系统资源。 public void flush() // 刷新此输出流并强制任何缓冲的输出字节被写出。 public void write(int b) // 将指定的字节输出流。 public void write(byte[] b) // 将 b.length字节从指定的字节数组写入此输出流。 public void write(byte[] b, int off, int len) // 从指定的字节数组写入 len个字节,从偏移量 off开始输出到此输出流。FileInputStream:文件字节输入流java.io.FileInputStream extends InputStream作用:把文件中的数据,以字节的方式读取到内存中构造方法:public FileInputStream(String name) public FileInputStream(File file) /* 参数:传递要读取的数据源 String name:要读取的数据源是一个文件的路径 File file:要读取的数据源是一个文件 */构造方法的作用: 1.会创建FileInputStream对象 2.会把创建好的FileInputStream对象,指向要读取文件的第一个字节注意:使用构造方法创建对象,如果要读取的文件不存在,不会创建文件,会抛出文件找不到异常使用字节输入流读取文件到内存中底层原理: java程序 ==> JVM ==> 操作系统 ==> 调用系统中读取文件的方法 ==> 读取文件使用示例字节输入流的使用步骤: 1.创建FileInputStream对象,构造方法中绑定要读取的数据源 2.使用FileInputStream对象中的方法read,以字节的方式读取文件 3.释放资源import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Arrays; public class Demo01InputStream { public static void main(String[] args) throws IOException { //1.创建FileInputStream对象,构造方法中绑定要读取的数据源 FileInputStream fis = new FileInputStream("day21\\a.txt"); //2.使用FileInputStream对象中的方法read,以字节的方式读取文件 //int read() 从文件中一次读取一个字节并返回 读取文件是一个重复的过程,所以可以使用循环优化 不知道文件中有多少字节,不知道循环多少次,使用while循环 循环结束的条件,read方法读取到-1的时候结束 循环中的布尔表达式:((len=fis.read()) != -1) 1.fis.read():读取文件中的一个字节 2.len = fis.read():把读取到的字节赋值给变量len 3.(len = fis.read()) != -1:判断变量len的值是否为-1 len不是-1就执行循环体,打印len(读取到的字节) len是-1结束循环 int len = 0; while ((len=fis.read()) != -1){ System.out.print((char)len); //3.释放资源 fis.close(); 使用字节输入流一次读取多个字节 FileInputStream fis = new FileInputStream("day21\\b.txt"); byte[] bytes = new byte[1024]; int len = 0; while ((len = fis.read(bytes)) != -1){ //System.out.println(Arrays.toString(bytes)); //System.out.println(len);//5 读取的有效字节个数 System.out.println(new String(bytes, 0, len)); fis.close(); }FileOutputStream:文件字节输出流java.io.FileOutputStream extends OutputStream作用:把内存中的字节写入到硬盘的文件中保存构造方法:public FileOutputStream(File file) public FileOutputStream(String name) /* 参数:输出的目的地 String name:输出的目的地就是一个文件路径 File file:输出的目的地就是一个文件 // 字节输出流的续写和换行 public FileOutputStream(File file, boolean append) public FileOutputStream(String name, boolean append) /* 参数:File file|String name:写入数据的目的地 boolean append:续写的开关 true:可以续写,使用构造方法创建对象,文件名相同,不会创建新的文件覆盖之前同名的文件,会继续往文件的末尾写数据 false:不可以续写,使用构造方法创建对象,文件名相同,会创建一个新的空白文件覆盖之前同名的文件,在新的文件中写数据 换行:使用换行符号 Windows系统里,每行结尾是 回车+换行 ,即 \r\n linux,Unix系统里,每行结尾只有 换行 ,即 \n Mac系统里,每行结尾是 回车 ,即 \r 。从 Mac OS X开始与Linux统一。 */ 构造方法的作用: 1.会创建FileOutputStream对象 2.会根据构造方法中传递的文件路径|文件,创建一个新的空白文件 3.会把FileOutputStream对象,指向创建好的文件,就可以往文件中写数据了使用字节输出流往文件中写输出的底层过程: java程序 ==> JVM ==> 操作系统(OS) ==> 调用系统中写数据的方法 ==> 把数据写入到文件中使用示例字节输出流的基本使用: 1.创建FileOutputStream对象,构造方法绑定要输出的目的地 2.使用FileOutputStream对象中的方法write,把数据写入到文件中 3.释放资源(关闭此文件输出流并释放与此流有关的所有系统资源)import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; public class Demo01OutputStream { public static void main(String[] args) throws IOException { //1.创建FileOutputStream对象,构造方法绑定要输出的目的地 FileOutputStream fos = new FileOutputStream("day20\\1.txt"); //2.使用FileOutputStream对象中的方法write,把数据写入到文件中 fos.write(97); //3.释放资源(关闭此文件输出流并释放与此流有关的所有系统资源) fos.close(); 字节输出流中写多个字节的方法 FileOutputStream fos = new FileOutputStream(new File("day20\\2.txt")); // public void write(byte[] b) 一次把字节数组中所有的字节写入到文件中 byte[] bytes = {65, 66, 67, 68, 69, 70}; fos.write(bytes); //ABCDEF public void write(byte[] b, int off, int len) 一次把字节数组的一部分写入到文件中 int off:数组的开始索引,从哪个索引开始写 int len:写的字节个数 fos.write(bytes, 2, 3); //CDE // 把字符串转换为字节数组,写入到文件中 byte[] bytes2 = "中国".getBytes(); //IDEA默认使用UTF-8编码,1个中文占用3个字节 [-28, -72, -83, -27, -101, -67] System.out.println(Arrays.toString(bytes2)); fos.write(bytes2);//中国 fos.close(); 字节输出流的续写和换行 FileOutputStream fos = new FileOutputStream("day20\\3.txt", true); for (int i = 1; i <=10 ; i++) { fos.write(("你好" + i + "\r\n").getBytes()); fos.close(); }字符流编码表编码:把能看懂的文字,转换为看不懂的文字(字符 ==> 字节)解码:把看不懂的文字,转换为能看懂的文字(字节 ==> 字符)编码表:就是生活中的文字和计算机中文字的对应关系表a ==> 97 ==> 01100001 ==> 存储到计算机中中 ==> 20013 ==> 0010000000001011 ==> 存储到计算机中常用的编码表:ASCII字符集 :英文,数字,标点符号和计算机中文字的对应关系0 ==> 48 A ==> 65 a ==> 97ISO-8859-1字符集:拉丁码表拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。ISO-8859-1 使用单字节编码,兼容ASCII编码。不支持中文GBxxx字符集:国标GB 就是国标的意思,是为了显示中文而设计的一套字符集。兼容ASCII表GB2312:简体中文码表。 7000多个简体汉字GBK:目前操作系统默认中文码表(简体,繁体),存储一个中文使用2个字节,21003个汉字GB18030:最新的中文码表。包含的文字最全(简体,繁体,少数民族,日韩文字)Unicode字符集 :万国码UTF-8:最常用的万国表,兼容所有国家的文字编码规则:128个US-ASCII字符,只需一个字节编码。拉丁文等字符,需要二个字节编码。大部分常用字(含中文),使用三个字节编码。其他极少使用的Unicode辅助字符,使用四字节编码。Reader:字符输入流超类java.io.Reader:字符输入流(用于读取字符流的抽象类)是所有字符输入流最顶层的父类,里边定义了所有字符输入流共性的成员方法,所有的字符输入流都可以使用共性的成员方法:int read() // 读取单个字符。 int read(char[] cbuf) // 将字符读入数组。 void close() // 关闭该流并释放与之关联的所有资源。Writer:字符输出流超类java.io.Writer:字符输出流( 写入字符流的抽象类)是所有字符输出流最顶层的父类,里边定义了所有字符输出流中共性的成员方法,所有字符输出流都可以使用共性的成员方法:abstract void close() // 关闭此输出流并释放与此流相关联的任何系统资源 abstract void flush() // 刷新此输出流并强制任何缓冲的输出字符被写出 void write(int c) // 写出一个字符 void write(char[] cbuf) // 将 b.length字符从指定的字符数组写出此输出流 void write(char[] b, int off, int len) // 从指定的字符数组写出len字符,从偏移量off开始输出到此输出流 void write(String str) // 写出一个字符串 void write(String str, int off, int len) // 写入字符串的某一部分InputStreamReader:字符转换输入流java.io.InputStreamReader extends Reader作用:InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。解码:字节==>字符构造方法:public InputStreamReader(InputStream in) // 创建一个使用默认字符集的 InputStreamReader public InputStreamReader(InputStream in, String charsetName) // 创建使用指定字符集的 InputStreamReader /* 参数: InputStream in:传递字节输入流,可以传递InputStream的任意的子类对象(读取文件中的字节) String charsetName:传递编码表名称,不区分大小写的可以传递GBK(gbk),UTF-8(utf-8)..., 不写默认使用IDEA设置的编码(UTF-8) */继续自父类的共性成员方法:int read() // 读取单个字符。 int read(char[] cbuf) // 将字符读入数组。 void close() // 关闭该流并释放与之关联的所有资源。OutputStreamWriter:字符转换输出流java.io.OutputStreamWriter extends Writer作用:OutputStreamWriter 是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。编码:字符 ==> 字节构造方法:public OutputStreamWriter(OutputStream out) // 创建使用默认字符编码的 OutputStreamWriter。 public OutputStreamWriter(OutputStream out, String charsetName) // 创建使用指定字符集的 OutputStreamWriter。 /* 参数: OutputStream out:传递字节输出流,可以传递OutputStream的任意子类对象(把字符转换之后的字节写入到文件中) String charsetName:传递编码表名称,不区分大小写的可以传递GBK(gbk),UTF-8(utf-8)..., 不写默认使用IDEA设置的编码(UTF-8) */继承自父类共性的成员方法:public abstract void close() // 关闭此输出流并释放与此流相关联的任何系统资源。 public abstract void flush() // 刷新此输出流并强制任何缓冲的输出字符被写出。 public void write(int c) // 写出一个字符。 public void write(char[] cbuf) // 将 b.length字符从指定的字符数组写出此输出流。 public abstract void write(char[] b, int off, int len) // 从指定的字符数组写出len字符,从偏移量off开始输出到此输出流 public void write(String str) // 写出一个字符串 public void write(String str, int off, int len) // 写入字符串的某一部分。FileReader:文件字符输入流便捷类java.io.FileReader extends InputStreamReader(转换流) extends Reader作用:把文件中的数据以字符的方式读取到内存中构造方法:public FileReader(String fileName) public FileReader(File file) /* 参数:传递要读取的数据源 String fileName:数据源就是一个文件路径 File file:数据源就是一个文件 */使用示例使用字符输入流读取文件的步骤 1.创建FileReader对象,构造方法中绑定要读取的数据源 2.使用FileReader对象中的方法read,以字符的方式读取文件 3.释放资源Stirng类的构造方法:public String(char[] value) // 把字符数组转换为字符串 public String(char[] value, int offset, int count) // 把字符数组的一部分转换为字符串import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class Demo02Reader { public static void main(String[] args) throws IOException { //1.创建FileReader对象,构造方法中绑定要读取的数据源 FileReader fr = new FileReader("day21\\c.txt"); //2.使用FileReader对象中的方法read,以字符的方式读取文件 //int read() 一次读取一个字符并返回 /*int len = 0; while ((len = fr.read()) != -1){ System.out.print((char)len); //int read(char[] cbuf) 使用数组缓冲一次读取多个字符 char[] chars = new char[1024]; int len =0; while ((len = fr.read(chars)) != -1){ System.out.println(new String(chars, 0, len)); //3.释放资源 fr.close(); }FileWriter:文件字符输出流便捷类java.io.FileWriter extends OutputStreamWriter(转换流) extends Writer作用:把内存中的数据,以字符的方式写入到文件中构造方法:public FileWriter(File file) public FileWriter(String fileName) /* 参数:传递写入的目的地 String fileName:目的地是一个文件的路径 File file:目的地是一个文件 public FileWriter(String fileName, boolean append) public FileWriter(File file, boolean append) /* 参数:File file|String fileName:写入数据的目的地 boolean append:续写的开关 true:可以续写,使用构造方法创建对象,文件名相同,不会创建新的文件覆盖之前同名的文件,会继续往文件的末尾写数据 false:不可以续写,使用构造方法创建对象,文件名相同,会创建一个新的空白文件覆盖之前同名的文件,在新的文件中写数据 换行:使用换行符号 Windows系统里,每行结尾是 回车 + 换行 ,即 \r\n linux,Unix系统里,每行结尾只有 换行 ,即 \n Mac系统里,每行结尾是 回车 ,即 \r 。从 Mac OS X开始与Linux统一 */一个参数构造方法的作用: 1.会创建FileWriter对象 2.会根据构造方法中传递的文件|文件的路径,创建一个新的空白的文件 3.会把FileWriter对象,执向空白的文件使用示例使用字符输出流基本步骤: 1.创建FileWriter对象,构造方法中绑定要写入的目的地 2.使用FileWriter对象中的方法write,把数据写入到内存缓冲区中 3.使用FileWriter对象中的方法flush,把内存缓冲区中的数据刷新到文件中 4.释放资源(close方法会自动调用flush,把内存缓冲区中的数据刷新到文件中,再释放资源)import java.io.FileWriter; import java.io.IOException; public class Demo01Writer { public static void main(String[] args) throws IOException { // 1.创建FileWriter对象,构造方法中绑定要写入的目的地 FileWriter fw = new FileWriter("day21\\d.txt"); // 2.使用FileWriter对象中的方法write,把数据写入到内存缓冲区中 // void write(int c):写出一个字符 fw.write(100); fw.write('中'); // 中 // void write(char[] cbuf) : 写字符数组中的多个字符 char[] chars = {'a','b','c','1','2','3','中','国','@','#'}; fw.write(chars); // abc123中国@# // abstract void write(char[] b, int off, int len):写字符数组的一部分字符;off数组的开始索引,len写的字符个数 fw.write(chars, 6, 2); //中国 // void write(String str) :写出一个字符串 fw.write("到中午了,我们该吃饭了"); // void write(String str, int off, int len):写入字符串的某一部分。off字符串的开始索引,len写的字符个数 fw.write("到中午了,我们该吃饭了", 5, 6);//我们该吃饭了 // 3.使用FileWriter对象中的方法flush,把内存缓冲区中的数据刷新到文件中 //fw.flush(); // 4.释放资源(close方法会自动调用flush,把内存缓冲区中的数据刷新到文件中,再释放资源) fw.close(); 字符输出流的续写和换行 FileWriter fw = new FileWriter(new File("day21\\g.txt"), true); for (int i = 1; i <= 10; i++) { fw.write("hello" + i + "\r\n"); fw.close(); }字符转换流和读写便捷类的区别字符转换流 InputStreamReader 和 OutputStreamWriter 可以指定编码,缺省 ufs-8 编码字符读写便捷类 FileReader 和 FileWriter 不能指定编码,默认 utf-8 编码使用 FileReader 读取 GBK 编码的文件:会出现乱码,编码和解码不一致导致。FileReader 只能读取默认编码(UTF-8 编码)的文件GBK:采用的双字节编码,一个中文占用2个字节UTF-8:采用多字节编码,一个中文占用3个字节缓冲流概述文件字节流(FileInputStream、FileOutputStream)和文件字符流(FileReader、FileWriter)这些都是计算机与硬盘之间发生的I/O 操作,基于硬盘的读写相对比较慢,读写操作收到了硬盘读取速度的限制。为了能够提高读写速度,一定程度上绕过硬盘的限制,Java提供了一种缓冲流来实现。缓冲字节流(BufferedInputStream、BufferedOutputStream)和缓冲字符流(BufferedReader、BufferedWriter)就是先把数据缓冲在内存里,在内存中去做 IO 操作。基于内存的 IO 操作比基于硬盘的 IO 操作快很多。缓冲流的基本原理:在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统 IO 次数,从而提高读写的效率。BufferedInputStream:字节缓冲输入流java.io.BufferedInputStream extends InputStream构造方法:public BufferedInputStream(InputStream in) // 创建一个具有默认缓冲区大小的BufferedInputStream对象 public BufferedInputStream(InputStream in, int size) // 创建具有指定缓冲区大小的 BufferedInputStream对象 /*参数: InputStream in:传递字节输入流,可以传递InputStream的任意子类对象 可以传递FileInputStream对象,缓冲流就会给FileInputStream对象增加一个缓冲区(字节数组) 提高FileInputStream读取文件的效率 int size:指定缓冲区的大小(数组的长度),不写使用默认值(KB) private static int DEFAULT_BUFFER_SIZE = 8192;继承自父类共性的成员方法:int read() // 一次读取一个字节并返回 int read(byte[] b) // 使用数组缓冲,一次读取多个字节 void close() // 关闭此输入流并释放与该流关联的所有系统资源。使用示例import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; public class Demo01BufferedInputStream { public static void main(String[] args) throws IOException { //1.创建BufferedInputStream对象,构造方法中传递FileInputStream对象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("day22\\1.txt")); //2.使用BufferedInputStream对象中的方法read,以字节的方式读取文件 //int read(byte[] b) 使用数组缓冲,一次读取多个字节 byte[] bytes = new byte[1024]; int len = 0; while ((len = bis.read(bytes)) ! =-1){ System.out.println(new String(bytes, 0, len)); } catch (IOException e) { e.printStackTrace(); }BufferedOutputStream:字节缓冲输出流java.io.BufferedOutputStream extends OutputStream构造方法:public BufferedOutputStream(OutputStream out) // 创建一个新的缓冲输出流,具有默认缓冲区大小 public BufferedOutputStream(OutputStream out, int size) // 创建一个新的缓冲输出流,具有指定缓冲区大小 /* 参数: OutputStream out:传递字节输出流,可以传递OutputStream的任意子类对象 我们可以传递FileOutputStream对象,缓冲流就会给FileOutputStream对象增加一个缓冲区(字节数组) 提高FileOutputStream读取文件的效率 int size:指定缓冲区的大小(数组的长度),不写使用默认值 */继承自父类的共性成员方法:public void close() // 关闭此输出流并释放与此流相关联的任何系统资源 public void flush() // 刷新此输出流并强制任何缓冲的输出字节被写出 public void write(byte[] b) // 将 b.length字节从指定的字节数组写入此输出流 public void write(byte[] b, int off, int len) // 从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流 public abstract void write(int b) // 将指定的字节输出流使用示例import java.io.BufferedOutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class Demo02BufferedOutputStream { public static void main(String[] args) throws IOException { //1.创建BufferedOutputStream对象,构造方法中传递FileOutputStream对象 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("day22\\2.txt")); //2.使用BufferedOutputStream对象中的方法write,把数据写入到内存缓冲区中 bos.write("你好".getBytes()); //3.使用BufferedOutputStream对象中的方法flush,把内存缓冲区中数据刷新到文件 bos.flush(); //4.释放资源(会先调用flush方法刷新数据到文件) } catch (IOException e) { e.printStackTrace(); }BufferedReader:字符缓冲输入流java.io.BufferedReader extends Reader构造方法:public BufferedReader(Reader in) // 创建一个使用默认大小输入缓冲区的缓冲字符输入流。 public BufferedReader(Reader in, int sz) // 创建一个使用指定大小输入缓冲区的缓冲字符输入流。 /* 参数: Reader in:传递字符输入流,可以传递Reader的任意的子类对象 我们可以FileReader,缓冲流就会给FileReader增加一个缓冲区 提高FileReader读取文件的效率 int sz:指定缓冲区的大小(数组长度),不指定使用默认值 */继承自父类的共性成员方法:int read() // 读取单个字符。 int read(char[] cbuf) // 将字符读入数组。 void close() // 关闭该流并释放与之关联的所有资源。特有的成员方法:String readLine() // 读取一个文本行。一次可以读取一行数据 /* 通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行(\r\n)。 返回:包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null使用示例import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class Demo04BufferedReader { public static void main(String[] args) throws IOException { //1.创建BufferedReader对象,构造方法中传递FileReader对象 BufferedReader br = new BufferedReader(new FileReader("day22\\3.txt")); //2.使用BufferedReader对象中的方法read|readLine,以字符的方式读取文件 //String readLine() 读取一个文本行。一次可以读取一行数据 //注意:读取的文件中有null,仅仅是一个字符串"null",不是默认值null String line; while ((line = br.readLine()) != null){ System.out.println(line); //你好1你好2你好3 不会读取每行结尾的回车换行符号(行的终止符号) } catch (IOException e) { e.printStackTrace(); }BufferedWriter:字符缓冲输出流java.io.BufferedWriter extends Writer构造方法:public BufferedWriter(Writer out) // 创建一个使用默认大小输出缓冲区的缓冲字符输出流。 public BufferedWriter(Writer out, int sz) // 创建一个使用给定大小输出缓冲区的新缓冲字符输出流 /* 参数: Writer out:传递字符输出流,可以传递Writer的任意的子类对象 我们可以传FileWriter,缓冲流就会给FileWriter增加一个缓冲区 提高FileWriter写入文件的效率 int sz:指定缓冲区的大小(数组长度),不指定使用默认值 */继承自父类共性的成员方法: abstract void close() // 关闭此输出流并释放与此流相关联的任何系统资源 abstract void flush() // 刷新此输出流并强制任何缓冲的输出字符被写出 void write(int c) // 写出一个字符 void write(char[] cbuf) // 将 b.length字符从指定的字符数组写出此输出流 abstract void write(char[] b, int off, int len) // 从指定的字符数组写出 len字符,从偏移量 off开始输出到此输出流 void write(String str) // 写出一个字符串 void write(String str, int off, int len) // 写入字符串的某一部分特有的成员方法:void newLine() // 写入一个行分隔符。写一个换行符号,根据系统不同,而写不同的换行符号 /* Windows系统里,每行结尾是 回车+换行 ,即 \r\n linux,Unix系统里,每行结尾只有 换行 ,即 \n Mac系统里,每行结尾是 回车 ,即 \r 。从 Mac OS X开始与Linux统一。 */使用示例import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; public class Demo05BufferedWriter { public static void main(String[] args) throws IOException { //1.创建BufferedWriter对象,构造方法中传递FileWriter对象 BufferedWriter bw = new BufferedWriter(new FileWriter("day22\\4.txt",true)); //2.使用BufferedWriter对象中的方法write,把数据写入到内存缓冲区中 for (int i = 1; i <= 10; i++) { bw.write("你好"+i); bw.newLine();//写换行 //3.使用BufferedWriter对象中的方法flush,把内存缓冲区中的数据刷新到文件中 bw.flush(); } catch (IOException e) { e.printStackTrace();
Stream流1.Sream流概述我们可以把集合|数组,转换为Stream流,使用Stream流中的方法,对集合|数组进行操作2.流式思想概述3.获取Stream流的方式使用Collection接口中,(jdk1.8之后)定义了一个方法stream,可以把集合转换为Stream流default Stream<E> stream() // 此转换方法只能是Collection接口下的集合使用使用Stream接口中方法of,可以把可变参数(数组)转换为Stream流java.util.stream.Stream 接口(jdk1.8之后)static <T> Stream<T> of(T... values) // 方法的参数是一个可变参数,也可以传递数组import java.util.*; import java.util.stream.Stream; public class Demo03Stream { public static void main(String[] args) { show02(); 把数组转换为Stream流 看到的方法参数是一个可变参数,那么就可以传递数组,因为可变参数底层就是一个数组 static <T> Stream<T> of (T... values) :方法的参数是一个可变参数,也可以传递数组 private static void show02() { Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9); Stream<String> stream2 = Stream.of("a", "b", "c"); String[] arr1 = {"a", "b", "c"}; Stream<String> stream3 = Stream.of(arr1); //注意:数组的数组类型必须是包装类,不要使用基本数据类型 int[] arr2 = {1,2,3}; Stream<int[]> stream4 = Stream.of(arr2); System.out.println(stream4.count());//1 把数组作为一个元素,转换为Stream流 Integer[] arr3 = {1,2,3}; Stream<Integer> stream5 = Stream.of(arr3); System.out.println(stream5.count());//3 把集合转换为Stream流 private static void show01() { ArrayList<Integer> list = new ArrayList<>(); Stream<Integer> stream1 = list.stream(); LinkedList<String> linked = new LinkedList<>(); Stream<String> stream2 = linked.stream(); HashSet<Integer> set = new HashSet<>(); Stream<Integer> stream3 = set.stream(); HashMap<String,String> map = new HashMap<>(); //map.stream();//Cannot resolve method 'stream()' Map集合不能直接转换为Stream流 //获取Map集合中所有的key Set<String> keySet = map.keySet(); Stream<String> stream4 = keySet.stream(); //获取Map集合中所有的value Collection<String> values = map.values(); Stream<String> stream5 = values.stream(); //获取Map集合中所有的Entry对象 Set<Map.Entry<String, String>> entrySet = map.entrySet(); Stream<Map.Entry<String, String>> stream6 = entrySet.stream(); }4.Stream的常用方法流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的链式调用。本小节中,终结方法包括count和forEach方法。非终结方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)函数拼接与终结方法在上述介绍的各种方法中,凡是返回值仍然为Stream接口的为函数拼接方法,它们支持链式调用;而返回值不再为Stream接口的为终结方法,不再支持链式调用。如下表所示:方法名方法作用方法种类是否支持链式调用filter过滤函数拼接是limit取用前几个函数拼接是skip跳过前几个函数拼接是map映射函数拼接是concat组合函数拼接是count统计个数终结否forEach逐一处理终结否备注:本小节之外的更多方法,请自行参考API文档。forEach 方法:用于遍历void forEach(Consumer<? super T> action) // 对此流的每个元素执行操作。 // 参数: // Consumer<? super T> action:是一个消费型的接口,参数可以传递Lambda表达式 // 唯一的抽象方法:void accept(T t) 消费一个指定泛型类型的数据注意:forEach方法是一个终结方法,没有返回值;也不能使用链式编程调用Stream流中的其他方法了import java.util.stream.Stream; public class Demo04forEach { public static void main(String[] args) { //获取Stream流 Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); //使用forEach方法遍历Stream流中的元素 /*stream.forEach((Integer i)->{ System.out.println(i); });*/ IllegalStateException: stream has already been operated upon or closed Stream流对象只能使用一次,使用完毕就会流向下一个对象 之前的Stream流对象就已经关闭了,被销毁了,所以就不能在使用了,会抛出非法状态异常 简化lambda表达式 stream.forEach(i-> System.out.println(i)); }count 方法:统计个数long count() // 返回此流中的元素数。注意:count方法是一个终结方法,返回值类型是long类型;也不能使用链式编程调用Stream流中的其他方法了import java.util.ArrayList; import java.util.Collections; import java.util.stream.Stream; public class Demo05count { public static void main(String[] args) { //获取Stream流 Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); long c1 = stream.count(); System.out.println(c1);//10 ArrayList<String> list = new ArrayList<>(); Collections.addAll(list,"aa","bb","cc","dd","ee"); //把集合转换为Stream流 Stream<String> stream2 = list.stream(); System.out.println(stream2.count());//5 }filter 方法:过滤元素Stream<T> filter(Predicate<? super T> predicate) // 参数: // Predicate<? super T> predicate:函数式接口,参数传递lambda表达式 // 唯一的抽象方法:boolean test(T t) 用于对接口指定泛型类型的数据进行判断注意:filter方法的返回值类型还是Stream类型,是一个非终结方法,可以使用返回的Stream对象继续调用Stream流中的方法(链式编程)import java.util.stream.Stream; public class Demo06filter { public static void main(String[] args) { //创建一个Stream流 Stream<String> stream = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "沸羊羊", "暖羊羊", "慢羊羊", "灰太狼", "红太狼", "小灰灰"); //使用filter方法过滤Stream流中的元素,只要包含羊羊的 //Stream<String> stream2 = stream.filter((String s) -> { // return s.contains("羊羊"); //}); //遍历stream2流对象 //stream2.forEach(s-> System.out.println(s)); //链式编程 stream.filter(s->s.contains("羊羊")).forEach(s-> System.out.println(s)); }limit 方法:获取前n个元素Stream<T> limit(long maxSize) // 返回由此流的元素组成的流,截短长度不能超过 maxSize 。 // 例如:limit(4);获取流中的前4个元素,把4个元素存储到一个新的Steam流中注意:1.传递数字大于流的长度,返回流中所有的元素2.limit方法的返回值类型还是Stream类型,是一个非终结方法,可以使用返回的Stream对象继续调用Stream流中的方法(链式编程)import java.util.stream.Stream; public class Demo07limit { public static void main(String[] args) { //创建一个Stream流 Stream<String> stream = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "沸羊羊", "暖羊羊", "慢羊羊", "灰太狼", "红太狼", "小灰灰"); //获取Stream流中的前4个元素,存储到一个新的Stream流中返回 //Stream<String> stream2 = stream.limit(4); //stream2.forEach(s-> System.out.println(s)); //stream.limit(4).forEach(s-> System.out.println(s)); stream.limit(10).forEach(s-> System.out.println(s));//9个 skip 方法:跳过前n个元素 Stream<T> skip(long n)注意:1.skip跳过的元素数量大于流中的个数,返回一个没有元素的空流2.skip方法的返回值类型还是Stream类型,是一个非终结方法,可以使用返回的Stream对象继续调用Stream流中的方法(链式编程)import java.util.stream.Stream; public class Demo08skip { public static void main(String[] args) { //获取Stream流 Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); //跳过前6个元素,把剩余的元素,存储到一个新的Stream流中返回 //Stream<Integer> stream2 = stream.skip(6); //stream2.forEach(s-> System.out.println(s)); //stream.skip(6).forEach(s-> System.out.println(s)); //stream.skip(11).forEach(s-> System.out.println(s)); System.out.println(stream.skip(11).count());//0 skip跳过的元素数量大于流中的个数,返回一个没有元素的空流 }map 方法:映射,类型转换<R> Stream<R> map(Function<T,R> mapper) // 参数: // Function<T,R> mapper:函数式接口,可以传递Lambda表达式 // 接口中唯一的抽象方法:R apply(T t) 根据参数类型T获取类型R类型的返回值,用于类型转换 T转换R注意:map方法的返回值类型还是Stream类型,是一个非终结方法,可以使用返回的Stream对象继续调用Stream流中的方法(链式编程)import java.util.stream.Stream; public class Demo09map { public static void main(String[] args) { Stream<String> stream = Stream.of("11", "22", "33", "44"); //使用map方法,把字符串类型的Stream流,转换为Integer类型的Stream流返回 //Stream<Integer> stream2 = stream.map((String s) -> { // return Integer.parseInt(s); //}); //遍历stream2流对象 //stream2.forEach(s-> System.out.println(s+10)); stream.map(s->Integer.parseInt(s)).forEach(s-> System.out.println(s)); Stream<String> stream3 = Stream.of("迪丽热巴", "杨幂", "柳岩"); //使用map方法,把字符串类型的Stream流,转换为Person类型的流返回 //Stream<Person> stream4 = stream3.map(s -> new Person(s)); //stream4.forEach(s-> System.out.println(s)); stream3.map(s->new Person(s)).forEach(p-> System.out.println(p)); }concat 方法:组合// 把两个Stream流,组合为一个新的Stream流 static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)注意:concat方法的返回值类型还是Stream类型,是一个非终结方法,可以使用返回的Stream对象继续调用Stream流中的方法(链式编程)import java.io.Serializable; import java.util.stream.Stream; public class Demo10concat { public static void main(String[] args) { Stream<String> stream1 = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "沸羊羊", "暖羊羊", "慢羊羊", "灰太狼", "红太狼", "小灰灰"); Stream<String> stream2 = Stream.of("迪丽热巴", "杨幂", "柳岩"); //使用concat方法,把stream1和stream2组合为一个新的流 //Stream<String> concat = Stream.concat(stream1, stream2); //遍历Stream流 //concat.forEach(s-> System.out.println(s)); Stream.concat(stream1, stream2).forEach(s-> System.out.println(s)); Stream<Integer> stream3 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Stream<String> stream4 = Stream.of("11", "22", "33", "44"); Stream<Object> stream5 = Stream.concat(stream3, stream4); stream5.forEach(s-> System.out.println(s)); }sorted 方法:排序Stream<T> sorted(Comparator<? super T> comparator)flatMap 方法:维度升降参考:https://blog.csdn.net/dengjili/article/details/90557392<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);可以将 Stream<Stream> 升维成 Stream,从而实现将 List<List> 里的对象集合快速合并为 List @Test public void test(){ Map<String, ArrayList<TestVo>> map = Maps.newHashMap("1", Lists.newArrayList(TestVo.builder().data("3").build(), TestVo.builder().data("1").build())); map.put("0", Lists.newArrayList(TestVo.builder().data("8").build())); map.put("3", Lists.newArrayList(TestVo.builder().data("2").build())); List<TestBaseVo> list = map.entrySet().stream() .map(s -> s.getValue().stream() .sorted(Comparator.comparing(TestVo::getData)) .map(t -> TestBaseVo.builder().clazz(t.getData()).build())) .flatMap(a -> a) .collect(Collectors.toList()); System.out.println(list); }5.收集Stream结果(重点)把Stream流转换为集合或者把Stream流转换为数组1).把Stream流转换为集合使用Stream流中的方法:R collect(Collector<?> collector) //把Stream流转换为集合 // 参数:Collector是一个接口,需要传递接口的实现类对象java.util.stream.Collectors:是一个工具类,里边提供的静态的方法,可以获取Collector接口的实现类对象static <T> Collector<T, ?, List<T>> toList() // 返回一个 Collector ,将输入元素累加到一个新的 List static <T> Collector<T, ?, Set<T>> toSet() // 返回一个 Collector ,将输入元素累加到一个新的 Set public static <T, K, U> Collector<T, ?, Map<K,U>> toMap( Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; public class Demo01StreamToCollection { public static void main(String[] args) { //创建一个Stream流 Stream<String> stream = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "沸羊羊", "暖羊羊", "慢羊羊", "灰太狼", "红太狼", "小灰灰", "灰太狼"); //把Stream流转换为List集合(有序,有索引,允许重复) //List<String> list = stream.collect(Collectors.toList()); //System.out.println(list);//[美羊羊, 喜羊羊, 懒羊羊, 沸羊羊, 暖羊羊, 慢羊羊, 灰太狼, 红太狼, 小灰灰, 灰太狼] //把Stream流转换为Set集合(不包含带索引的方法,不允许存储重复的元素) Set<String> set = stream.collect(Collectors.toSet()); System.out.println(set);//[美羊羊, 沸羊羊, 红太狼, 灰太狼, 暖羊羊, 小灰灰, 喜羊羊, 懒羊羊, 慢羊羊] }2).把Stream流转换为数组Stream流中的方法:Object[] toArray() // 返回一个包含此流的元素的数组。import java.util.stream.Stream; public class Demo02StreamToArray { public static void main(String[] args) { //创建一个Stream流 Stream<String> stream = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "沸羊羊", "暖羊羊", "慢羊羊", "灰太狼", "红太狼", "小灰灰", "灰太狼"); Object[] arr = stream.toArray(); for (Object o : arr) { System.out.println(o);
概念及语法规则正则表达式就是一个包含了某些规则的字符串。用来对其他的字符串进行校验,校验其他的字符串是否满足正则表达式的规则。正则表达式可以用来搜索、编辑或处理文本。正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。语法规则特殊字符^ :为匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与"\n"或"\r"之后的位置匹配。 $ :为匹配输入字符串的结束位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。 \ :转义符 [ ] :表示一个区间,区间的范围可以自己定义正则表达式的字符类[abc] :代表a或者b,或者c字符中的一个。 [^abc] :代表除a,b,c以外的任何字符。 [a-z] :代表a-z的所有小写字符中的一个。 [A-Z] :代表A-Z的所有大写字符中的一个。 [0-9] :代表0-9之间的某一个数字字符。 [a-zA-Z0-9] :代表a-z或者A-Z或者0-9之间的任意一个字符。 [a-dm-p] :a 到 d 或 m 到 p之间的任意一个字符。正则表达式的逻辑运算符&& :并且。多个条件同时满足 || :或者。满足其中一个条件即可正则表达式的预定义字符类在正则中已经定义好的一些有特殊含义的字符,可以直接使用"." :匹配任何字符(常用) "\\d" :任何数字[0-9]的简写(常用) "\\D" :任何非数字[^0-9]的简写 "\\w" :单词字符:[a-zA-Z_0-9]的简写 (常用) "\\W" :非单词字符:[^\w] 的简写 "\\s" :空白字符(空格):[ \t\n\x0B\f\r] 的简写 "\\S" :非空白字符:[^\s] 的简写注意:在正则表达式中,写 \ 的时候,需要写两个 \,因为 \ 本身是一个转义字符正则表达式的限定符(数量词)X* : 0次到多次 任意次 X? : 0次或1次 0,1 X+ : 1次或多次 X>=1 X{n} : 恰好n次 X==n次 X{n,} : 至少n次 X>=n次 X{n,m} : n到m次(n和m都是包含的) n=<X<=m正则表达式的分组括号 ( )// 示例:校验字符串"abc"可以出现任意次 String regex = "(abc)*";常用正则表达式校验数字// 纯数字 ^[0-9]*$ // 正整数 ^[1-9]\d*$ // 负整数 ^-[1-9]\d*$ // 非零整数(正整数 + 负整数) ^-?[1-9]\d*$ // 非零正整数 ^[1-9]\d*$ // 非负整数(正整数 + 0) ^[1-9]\d*|0$ 或 ^\d+$ // 非正整数(负整数 + 0) ^-[1-9]\d*|0$ // 正浮点数 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ // 负浮点数 ^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ // 浮点数 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$ // 非负浮点数(正浮点数 + 0) ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$ // 非正浮点数(负浮点数 + 0) ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$ 校验字符1. 汉字:^[\u4e00-\u9fa5]{0,}$ 2. 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$ 3. 长度为3-20的所有字符:^.{3,20}$ 4. 由26个英文字母组成的字符串:^[A-Za-z]+$ 5. 由26个大写英文字母组成的字符串:^[A-Z]+$ 6. 由26个小写英文字母组成的字符串:^[a-z]+$ 7. 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$ 8. 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$ 9. 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$ 10. 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$ 11. 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+ 12. 禁止输入含有~的字符:[^~\x22]+特殊需求表达式1. Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$ 2. 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.? 3. InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$ 4. 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$ 5. 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7} 6. 身份证号(15位、18位数字):^\d{15}|\d{18}$ 7. 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$ 8. 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$ 9. 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$ 10. 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$ 11. 日期格式:^\d{4}-\d{1,2}-\d{1,2} 12. 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$ 13. 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$ 14. xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$ 15. 中文字符的正则表达式:[\u4e00-\u9fa5] 16. 双字节字符(包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1)):[^\x00-\xff] 17. 空白行的正则表达式(可以用来删除空白行):\n\s*\r 18. 首尾空白字符的正则表达式(可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式): ^\s*|\s*$ 或 (^\s*)|(\s*$) 19. 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始) 20. 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字) 21. IP地址:\d+\.\d+\.\d+\.\d+ (提取IP地址时有用) 22. 钱的输入格式: 钱的表示形式主要有4种:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000" 通用(可为负数、逗号分隔可选、可精确到分)正则表达式: ^(0|-?[1-9][0-9]*|(-?[1-9][0-9]{0,2})(,[0-9]{3})*)(.[0-9]{1,2})?$String类中和正则表达式相关的方法// 判断字符串是否满足参数传递的正则表达式的规则。满足:返回true,不满足:返回false boolean matches(String regex) // 根据给定正则表达式的匹配拆分此字符串。 String[] split(String regex) // 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 String replaceAll(String regex, String replacement)示例:public class Demo07StringRegex { String replaceAll(String regex, String replacement) 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 过滤文章中的关键字 private static void show04() { String s = "13131sadfdsafjdsafjdlj13j213jlalf;"; System.out.println("原字符串:"+s); //需求:使用replaceAll方法把字符串中每一个数字替换为@_@ String s1 = s.replaceAll("\\d", "@_@"); System.out.println("替换后的字符串:"+s1); //需求:使用replaceAll方法把字符串中1个数字或者连续的数字替换为一个@_@ String s2 = s.replaceAll("\\d+","@_@"); System.out.println("替换后的字符串:"+s2); String[] split(String regex) 根据给定正则表达式的匹配拆分此字符串。 String regex:传递正则表达式 正则表达式中.代表任意字符 \\.使用转义字符,把有特殊含义的.转换为普通的. private static void show03() { //根据空格切割字符串 s = "11 22 33 44"; String[] arr = s.split("\\s+");//根据一个空格或者多个空格切割字符串 //根据.切割字符串 String s = "192.168.1.44"; String[] arr = s.split("\\."); }代码示例字符类 示例public class Demo02Regex { public static void main(String[] args) { //1.验证str是否以h开头,以d结尾,中间是a,e,i,o,u中某个字符 String regex = "h[aeiou]d"; String str = "ead"; //false str = "hid"; //true str = "hwd"; //false //使用字符串调用matches方法根据正则表达式进行校验 boolean b1 = str.matches(regex); System.out.println("b1:" + b1); //2.验证str是否以h开头,以d结尾,中间不是a,e,i,o,u中的某个字符 regex = "h[^aeiou]d"; str = "wad"; //false str = "h2d"; //true str = "had"; //false boolean b2 = str.matches(regex); System.out.println("b2:"+b2); //3.验证str是否a-z的任何一个小写字符开头,后跟ad regex = "[a-z]ad"; str = "dad"; //true str = "3ad"; //false boolean b3 = str.matches(regex); System.out.println("b3:"+b3); //4.验证str是否以a-d或者m-p之间某个字符开头,后跟ad regex = "[a-dm-p]ad"; str = "cad"; //true str = "ead"; //false boolean b4 = str.matches(regex); System.out.println("b4:"+b4); }逻辑运算符 示例public class Demo03Regex { public static void main(String[] args) { String str = "had";//true str = "Had";//false str = "ead"; 1.要求字符串是否是除a、e、i、o、u外的其它小写字符开头,后跟ad 除a、e、i、o、u外:[^aeiou] 小写字符:[a-z] 这两个条件同时满足:并且 && String regex = "[[^aeiou]&&[a-z]]ad"; boolean b1 = str.matches(regex); System.out.println("b1:"+b1); 2.要求字符串是aeiou中的某个字符开头,后跟ad "[a||e||i||o||u]ad"就相当于[aeiou]ad 或运算符是可以省略不写的 //regex = "[a||e||i||o||u]ad"; regex = "[aeiou]ad"; str = "aad";//true str = "add";//false str = "wad";//false boolean b2 = str.matches(regex); System.out.println("b2:"+b2); }预定义字符 示例public class Demo04Regex { public static void main(String[] args) { String str = "258";//true str = "25x";//false //1.验证str是否3位数字 String regex = "[0-9][0-9][0-9]"; regex = "\\d\\d\\d"; boolean b1 = str.matches(regex); System.out.println("b1:"+b1); //2.验证手机号:1开头,第二位:3/5/8,剩下9位都是0-9的数字 regex = "1[358][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]"; regex = "1[358]\\d\\d\\d\\d\\d\\d\\d\\d\\d"; str = "13888888888";//true str = "23888888888";//false str = "138888888881";//false str = "138888888a8";//false boolean b2 = str.matches(regex); System.out.println("b2:"+b2); //3.验证字符串是否以h开头,以d结尾,中间是任何字符 regex = "h.d"; str = "h#d";//true str = "h中d";//true str = "h1d";//true str = "h12d";//false str = "hd";//false boolean b3 = str.matches(regex); System.out.println("b3:"+b3); 4.验证str是否是:had. had.这个字符串中.不是任意的字符,就是一个普通的. 需要使用转义字符把有特殊含义的.转换为普通的. //regex = "had."; regex = "had\\."; str = "hadA";//false str = "had.";//true boolean b4 = str.matches(regex); System.out.println("b4:"+b4); }数量词 示例public class Demo05Regex { public static void main(String[] args) { String str = ""; //false str = "123";//true str = "1234";//false str = "12a";//false //1.验证str是否是三位数字 String regex = "\\d\\d\\d"; regex = "\\d{3}"; boolean b1 = str.matches(regex); System.out.println("b1:"+b1); //2.验证str是否是多位数字:数字出现的次数1次以上 regex = "\\d+"; str = "1231231232132131231";//true str = "1";//true str = "";//false str = "1231a12312";//false boolean b2 = str.matches(regex); System.out.println("b2:"+b2); //3.验证str是否是手机号:1开头,第二位:3/5/8,剩下9位都是0-9的数字 regex = "1[358]\\d{9}"; str = "13888888888";//true str = "138888888881";//false boolean b3 = str.matches(regex); System.out.println("b3:"+b3); //4.验证小数:必须出现小数点,但是只能出现1次 double d = 1.1; d = 0.1; d = .1; d= 1.; //d=.; regex = "\\d*\\.\\d*"; str = "1.1";//true str = "0.1";//true str = ".1";//true str = "1.";//true str = "1.aa";//false str = "1..1";//false str = "11";//false boolean b4 = str.matches(regex); System.out.println("b4:"+b4); //5.验证小数:小数点可以不出现,也可以出现1次 regex = "\\d*\\.?\\d*"; str = "11";//true str = "1113123.13123123123";//true str = "";//true str = "a";//false str = "11..11";//false boolean b5 = str.matches(regex); System.out.println("b5:"+b5); //6.验证小数:要求匹配:3、3.、3.14、+3.14、-3. regex = "[+-]?\\d+\\.?\\d*"; str = "3";//true str = "3.";//true str = "3.14";//true str = "+3.14";//true str = "-3.";//true boolean b6 = str.matches(regex); System.out.println("b6:"+b6); //7.验证qq号码:1).5--15位;2).全部是数字;3).第一位不是0 regex = "[1-9]\\d{4,14}"; str = "11111";//true str = "1111";//false str = "0111111";//false str = "1231aaa2";//false boolean b7 = str.matches(regex); System.out.println("b7:"+b7); }分组括号( ) 示例public class Demo06Regex { public static void main(String[] args) { //校验字符串"abc"作为一组可以出现任意次 String regex = "(abc)*"; String str = "";//true str = "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc";//true str = "aabbcc";//false boolean b1 = str.matches(regex); System.out.println("b1:"+b1); str = "DG8FV-B9TKY-FRT9J-99899-XPQ4G";//true str = "DG8FV-B9TKY-FRT9J-9989-XPQ4G";//false str = "DG8FV-B9TKYFRT9J-99189-XPQ4G";//false //验证这个序列号:分为5组,每组之间使用-隔开,每组由5位A-Z或者0-9的字符组成 regex = "([A-Z0-9]{5}-){4}[A-Z0-9]{5}"; boolean b2 = str.matches(regex); System.out.println("b2:"+b2);
概述介绍及使用描述:Javax.validation是 spring 集成自带的一个参数校验接口。可通过添加注解来设置校验条件。springboot框架创建web项目后,不需要再添加其他的依赖。使用:在Controller上使用 @Valid 或 @Validated 注解 开启校验public String test(@RequestBody @Valid MyRequest req){};@Validated 和 @Valid 的异同相同点:在检验参数符合规范的功能上基本一致;不同点:提供者不同:validated 是Spring Validation验证框架对参数的验证机制;valid是 javax 提供的参数验证机制作用域不同:validated :类,方法,参数valid:方法,字段,构造方法,参数,TYPE_US注:TYPE_USE:在 Java 8 之前的版本中,只能允许在声明式前使用 Annotation。而在 Java 8 版本中,Annotation 可以被用在任何使用 Type 的地方,例如:初始化对象时 (new),对象类型转化时,使用 implements 表达式时,或者使用 throws 表达式时。//初始化对象时 String myString = new @Valid String(); //对象类型转化时 myString = (@Valid String) str; //使用 implements 表达式时 class MyList<T> implements List<@Valid T> {...} //使用 throws 表达式时 public void validateValues() throws @Valid ValidationFailedException{...}参数校验常用注解除了前四个 @Null,@ NotNull,@ NotBlank,@NotEmpty外,其他所有的注解,传 null 时都会被当作有效处理注解常用参数值:message(校验不通过反馈信息)JSR303定义的基础校验类型:注解验证的数据类型备注Null任意类型参数值必须是 NullNotNull任意类型参数值必须不是 NullNotBlank只能作用于字符串字符串不能为 null,而且字符串长度必须大于0,至少包含一个非空字符串NotEmptyCharSequenceCollectionMapArray参数值不能为null,且不能为空(字符串长度必须大于0,空字符串(“ ”)可以通过校验)Size(min,max )CharSequenceCollectionMapArray字符串:字符串长度必须在指定的范围内Collection:集合大小必须在指定的范围内Map:map的大小必须在指定的范围内Array:数组长度必须在指定的范围内Pattern(regexp)字符串类型验证字符串是否符合正则表达式Min(value)整型类型参数值必须大于等于 最小值Max(value)整型类型参数值必须小于等于 最大值DecimalMin(value)整型类型参数值必须大于等于 最小值DecimalMax(value)整型类型参数值必须小于等于 最大值Positive数字类型参数值为正数PositiveOrZero数字类型参数值为正数或0Negative数字类型参数值为负数NegativeOrZero数字类型参数值为负数或0Digits(integer,fraction)数字类型参数值为数字,且最大长度不超过integer位,整数部分最高位不超过fraction位AssertTrue布尔类型参数值必须为 trueAssertFalse布尔类型参数值必须为 falsePast时间类型(Date)参数值为时间,且必须小于 当前时间PastOrPresent时间类型(Date)参数值为时间,且必须小于或等于 当前时间Future时间类型(Date)参数值为时间,且必须大于 当前时间FutureOrPresent时间类型(Date)参数值为时间,且必须大于或等于 当前日期Email字符串类型被注释的元素必须是电子邮箱地址Hibernate Validator 中附加的 constraint :注解验证的数据类型备注Length字符串类型字符串的长度在min 和 max 之间Range数字类型字符串类型数值或者字符串的值必须在 min 和 max 指定的范围内Pattern注解校验 常用正则表达式@Pattern(regexp = "^[1-9]]\\d*$", message = "XX参数值必须是正整数")高阶使用自定义分组校验有时多个场景接口公用一个请求对象,不同业务场景对请求对象的参数校验需求不同,可以使用分组校验来解决注意:没有指定显示分组的被校验字段和校验注解,默认都是 Default 组(即 Default.class)若自定义的分组接口未继承 Default 分组,且 @Validated(或 @Valid)注解未传参 Default.class,则只会校验请求对象中进行了显示分组的字段,不会校验默认分组(没有进行显示分组)的字段自定义的分组接口不继承 Default 分组 + @Validated(或 @Valid)注解传参 {自定义分组接口.class, Default.class}= 自定义的分组接口继承 Default 分组 + @Validated(或 @Valid)注解只传参自定义分组接口示例:新建自定义分组校验接口public interface Student { }import javax.validation.groups.Default; public interface Teacher extends Default { }新建请求对象@Data public class UserDTO { @NotBlank(message = "不能没有名称") private String name; @NotBlank(message = "老师不能没有手机号", groups = Teacher.class) private String phone; @NotEmpty(message = "学生不能没有书", groups = Student.clas) @Size(min = 2, message = "学生必须有两本书", groups = Student.class) private List<String> bookNames; }Controller@RestController public class ValidatedController { * 测试 校验student分组+默认分组 @PostMapping("student") public UserDTO validatedStudent(@Validated(value = {Student.class, Default.class}) @RequestBody UserDTO userDTO) { return userDTO; * 测试 校验student分组+默认分组 @PostMapping("teacher") public UserDTO validatedTeacher(@Validated(value = {Teacher.class}) @RequestBody UserDTO userDTO) { return userDTO; * 测试 分组校验 default @PostMapping("default") public UserDTO validatedDefault(@Validated(value = {Default.class}) @RequestBody UserDTO userDTO) { return userDTO; * 测试 分组校验 onlyStudent @PostMapping("onlyStudent") public UserDTO validatedOnlyStudent(@Validated(value = {Student.class}) @RequestBody UserDTO userDTO) { return userDTO; }自定义校验注解定义注解@Documented //指定注解的处理类 @Constraint(validatedBy = {VersionValidatorHandler.class }) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) public @interface ConstantVersion { String message() default "{constraint.default.const.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String value(); }注解处理类public class VersionValidatorHandler implements ConstraintValidator<Constant, String> { private String constant; @Override public void initialize(Constant constraintAnnotation) { //获取设置的字段值 this.constant = constraintAnnotation.value(); @Override public boolean isValid(String value, ConstraintValidatorContext context) { //判断参数是否等于设置的字段值,返回结果 return constant.equals(value); }自定义注解使用@ConstantVersion (message = "verson只能为1.0.0",value="1.0.0") String version;Controller@RestController public class TestController { @RequestMapping("/test") public String createUser(@Valid User user, BindingResult bindingResult){ if (bindingResult.hasErrors()){ return bindingResult.getFieldError().getDefaultMessage(); return "success"; }嵌套检验描述:当对象 Man 的字段 houses 包含 House 对象类型时,在检验 houses 字段时可以检验 House 对象的属性字段时,就称为嵌套检验方案:在被检验的字段上添加 @Valid 注解就可以实现嵌套检验示例如下:在检验 Man 对象的 houses 字段时,在houses 字段上添加 @Valid 注解后,就可以检验 list 中的 House 的属性是否符合要求;否则只会检验 houses 的集合大小是否大于1,不会校验集合中的 House 对象,比如 House 对象的 name 长度是否符合要求。class Man{ @Valid @Size(min = 1) private List<House> houses; class House{ @Length(min = 1,max = 10) private String name; }拓展异常处理参数校验异常:MethodArgumentNotValidException方式一:基于异常监听@ControllerAdvice(参考:优雅的java参数校验)/** * 全局异常处理器 @Slf4j @ControllerAdvice public class GlobalExceptionHandler { * 异常处理 @ResponseBody @ExceptionHandler(value = Exception.class) public DataResult exceptionHandler(Exception e) { log.error("GlobalExceptionHandler.exceptionHandler , 异常信息",e); return DataResult.fail(e.getMessage()); * 业务异常 @ResponseBody @ExceptionHandler(value = BplCommonException.class) public DataResult bplCommonExceptionHandler(BplCommonException e) { log.warn("",e); return DataResult.fail(e.getMessage()); * 处理所有RequestBody注解参数验证异常 @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public DataResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e){ log.warn(e.getMessage()); return DataResult.fail(e.getBindingResult().getAllErrors().get(0).getDefaultMessage()); * 处理所有RequestParam注解数据验证异常 @ExceptionHandler(BindException.class) public DataResult handleBindException(BindException ex) { FieldError fieldError = ex.getBindingResult().getFieldError(); log.warn("必填校验异常:{}({})", fieldError.getDefaultMessage(),fieldError.getField()); return DataResult.fail(ex.getBindingResult().getAllErrors().get(0).getDefaultMessage()); }方式二:基于Handle或Filter(参考:https://blog.51cto.com/u_12012821/2511625)if (e instanceof MethodArgumentNotValidException) { String errorMsg = ((MethodArgumentNotValidException) e) .getBindingResult() .getAllErrors() .stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.joining(",")); resp = R.builder() .code(ResultCodeEnum.BUSINESS_ERROR.getCode()) .message(errorMsg).success(false) .build(); } 参考:Java 注解方式校验请求参数Java请求参数校验 Validation API使用