text: /*
  SQLyog v10.2 
  MySQL - 5.6.45-log : Database - una-boot-db
  *********************************************************************
  */
  
  
  /*!40101 SET NAMES utf8 */;
  
  /*!40101 SET SQL_MODE=''*/;
  
  /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
  /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
  /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
  /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
  CREATE DATABASE /*!32312 IF NOT EXISTS*/`una-boot-db` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin */;
  
  USE `una-boot-db`;
  
  /*Table structure for table `categories` */
  
  DROP TABLE IF EXISTS `categories`;
  
  CREATE TABLE `categories` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `allow_nav` int(11) DEFAULT '0',
    `description` varchar(128) COLLATE utf8_bin DEFAULT '',
    `keywords` varchar(64) COLLATE utf8_bin DEFAULT '',
    `name` varchar(64) COLLATE utf8_bin NOT NULL,
    `parent_id` int(11) DEFAULT '0',
    `slug` varchar(64) COLLATE utf8_bin NOT NULL,
    `sort_num` int(11) DEFAULT '0',
    `template` varchar(64) COLLATE utf8_bin NOT NULL,
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `categories` */
  
  insert  into `categories`(`id`,`create_time`,`deleted`,`update_time`,`allow_nav`,`description`,`keywords`,`name`,`parent_id`,`slug`,`sort_num`,`template`) values (1,'2019-11-19 07:33:46',0,'2019-11-19 07:41:30',0,'谭朝红的博客','谭朝红,博客,Spring,Spring Boot,Spring Cloud','首页',0,'index',1,'profile.html'),(2,'2019-11-19 07:37:38',0,'2019-11-19 07:37:38',1,'项目','项目','项目',0,'projects',2,'projects.html'),(3,'2019-11-19 07:38:23',0,'2019-11-19 07:38:23',1,'指南','指南','指南',0,'guides',3,'guides.html'),(4,'2019-11-19 07:39:02',0,'2019-11-19 07:39:02',1,'博客','博客','博客',0,'blogs',4,'blogs.html'),(5,'2019-11-19 07:40:43',0,'2019-11-19 07:40:43',1,'关于我','关于我','关于我',0,'about',5,'about.html');
  
  /*Table structure for table `links` */
  
  DROP TABLE IF EXISTS `links`;
  
  CREATE TABLE `links` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `name` varchar(50) COLLATE utf8_bin NOT NULL,
    `sort_num` int(11) DEFAULT '0',
    `url` varchar(256) COLLATE utf8_bin NOT NULL,
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `links` */
  
  insert  into `links`(`id`,`create_time`,`deleted`,`update_time`,`name`,`sort_num`,`url`) values (1,'2019-11-20 21:51:39',0,'2019-11-20 21:51:39','玄霖志·许进',1,'https://xujin.org');
  
  /*Table structure for table `post_category` */
  
  DROP TABLE IF EXISTS `post_category`;
  
  CREATE TABLE `post_category` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `category_id` int(11) DEFAULT NULL,
    `post_id` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `post_category` */
  
  insert  into `post_category`(`id`,`create_time`,`deleted`,`update_time`,`category_id`,`post_id`) values (5,'2019-11-21 22:57:11',0,'2019-11-21 22:57:11',4,5),(4,'2019-11-21 22:47:36',0,'2019-11-21 22:47:36',4,4),(6,'2019-11-21 23:00:53',0,'2019-11-21 23:00:53',4,6),(7,'2019-11-21 23:03:17',0,'2019-11-21 23:03:17',4,7),(8,'2019-11-21 23:05:35',0,'2019-11-21 23:05:35',4,8),(9,'2019-11-21 23:07:47',0,'2019-11-21 23:07:47',4,9),(10,'2019-11-21 23:09:54',0,'2019-11-21 23:09:54',4,10),(11,'2019-11-21 23:11:53',0,'2019-11-21 23:11:53',4,11),(12,'2019-11-21 23:13:41',0,'2019-11-21 23:13:41',4,12),(13,'2019-11-21 23:15:42',0,'2019-11-21 23:15:42',4,13),(14,'2019-11-21 23:17:48',0,'2019-11-21 23:17:48',4,14),(15,'2019-11-21 23:20:22',0,'2019-11-21 23:20:22',4,15),(16,'2019-11-21 23:22:53',0,'2019-11-21 23:22:53',4,16),(17,'2019-11-21 23:24:59',0,'2019-11-21 23:24:59',4,17),(18,'2019-11-21 23:28:26',0,'2019-11-21 23:28:26',4,18),(19,'2019-11-21 23:30:10',0,'2019-11-21 23:30:10',4,19);
  
  /*Table structure for table `post_tag` */
  
  DROP TABLE IF EXISTS `post_tag`;
  
  CREATE TABLE `post_tag` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `post_id` int(11) DEFAULT NULL,
    `tag_id` int(11) DEFAULT NULL,
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `post_tag` */
  
  insert  into `post_tag`(`id`,`create_time`,`deleted`,`update_time`,`post_id`,`tag_id`) values (10,'2019-11-21 22:57:11',0,'2019-11-21 22:57:11',5,3),(9,'2019-11-21 22:47:36',0,'2019-11-21 22:47:36',4,4),(11,'2019-11-21 22:57:11',0,'2019-11-21 22:57:11',5,6),(8,'2019-11-21 22:47:36',0,'2019-11-21 22:47:36',4,3),(12,'2019-11-21 23:00:53',0,'2019-11-21 23:00:53',6,3),(13,'2019-11-21 23:00:53',0,'2019-11-21 23:00:53',6,7),(14,'2019-11-21 23:03:17',0,'2019-11-21 23:03:17',7,3),(15,'2019-11-21 23:05:35',0,'2019-11-21 23:05:35',8,3),(16,'2019-11-21 23:05:35',0,'2019-11-21 23:05:35',8,8),(17,'2019-11-21 23:07:47',0,'2019-11-21 23:07:47',9,3),(18,'2019-11-21 23:07:47',0,'2019-11-21 23:07:47',9,9),(19,'2019-11-21 23:09:54',0,'2019-11-21 23:09:54',10,3),(20,'2019-11-21 23:09:54',0,'2019-11-21 23:09:54',10,10),(21,'2019-11-21 23:11:53',0,'2019-11-21 23:11:53',11,3),(22,'2019-11-21 23:13:41',0,'2019-11-21 23:13:41',12,3),(23,'2019-11-21 23:15:42',0,'2019-11-21 23:15:42',13,3),(24,'2019-11-21 23:15:42',0,'2019-11-21 23:15:42',13,11),(25,'2019-11-21 23:17:48',0,'2019-11-21 23:17:48',14,3),(26,'2019-11-21 23:20:22',0,'2019-11-21 23:20:22',15,1),(27,'2019-11-21 23:20:22',0,'2019-11-21 23:20:22',15,2),(28,'2019-11-21 23:20:22',0,'2019-11-21 23:20:22',15,3),(29,'2019-11-21 23:22:53',0,'2019-11-21 23:22:53',16,1),(30,'2019-11-21 23:22:53',0,'2019-11-21 23:22:53',16,2),(31,'2019-11-21 23:22:53',0,'2019-11-21 23:22:53',16,3),(32,'2019-11-21 23:24:59',0,'2019-11-21 23:24:59',17,12),(33,'2019-11-21 23:24:59',0,'2019-11-21 23:24:59',17,13),(34,'2019-11-21 23:28:26',0,'2019-11-21 23:28:26',18,1),(35,'2019-11-21 23:28:26',0,'2019-11-21 23:28:26',18,14),(36,'2019-11-21 23:30:10',0,'2019-11-21 23:30:10',19,1),(37,'2019-11-21 23:30:10',0,'2019-11-21 23:30:10',19,15);
  
  /*Table structure for table `posts` */
  
  DROP TABLE IF EXISTS `posts`;
  
  CREATE TABLE `posts` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `allow_chat` int(11) DEFAULT '0',
    `allow_favor` int(11) DEFAULT '0',
    `allow_top` int(11) DEFAULT '0',
    `author` varchar(64) COLLATE utf8_bin DEFAULT '',
    `description` varchar(128) COLLATE utf8_bin DEFAULT '',
    `html` longtext COLLATE utf8_bin,
    `keywords` varchar(64) COLLATE utf8_bin DEFAULT '',
    `likes` int(11) DEFAULT '0',
    `markdown` longtext COLLATE utf8_bin,
    `original` int(11) DEFAULT '1',
    `project_url` varchar(512) COLLATE utf8_bin DEFAULT '',
    `slug` varchar(128) COLLATE utf8_bin NOT NULL,
    `status` int(11) DEFAULT '0',
    `summary` varchar(512) COLLATE utf8_bin DEFAULT '',
    `template` varchar(64) COLLATE utf8_bin NOT NULL,
    `thumb` varchar(1024) COLLATE utf8_bin DEFAULT '',
    `title` varchar(128) COLLATE utf8_bin NOT NULL,
    `url` varchar(255) COLLATE utf8_bin DEFAULT '',
    `visits` int(11) DEFAULT '0',
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `posts` */
  
  insert  into `posts`(`id`,`create_time`,`deleted`,`update_time`,`allow_chat`,`allow_favor`,`allow_top`,`author`,`description`,`html`,`keywords`,`likes`,`markdown`,`original`,`project_url`,`slug`,`status`,`summary`,`template`,`thumb`,`title`,`url`,`visits`) values (5,'2019-11-21 22:57:11',0,'2019-11-21 22:57:11',1,1,1,'谭朝红','过滤器被用于过滤应用程序的HTTP请求和响应，我们可以在请求发送到控制器之前对请求进行过滤，或者在响应发送到客户端之前进行过滤','<h1 id=\"h1-spring-boot-\"><a name=\"Spring Boot （九）— 过滤器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot （九）— 过滤器</h1><p>过滤器被用于过滤应用程序的HTTP请求和响应，我们可以在请求发送到控制器之前对请求进行过滤，或者在响应发送到客户端之前进行过滤。在本章节中，通过一个简单的例子来展示在Spring Boot中如何使用过滤器，首先通过一张图来说明过滤器的使用步骤：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-07-18-24-47-57ef75e3576d4388a8e6a91e74de1e2c.png\" alt=\"![](https://cdn.ramostear.com/2019-03-07-18-25-23-d1af3dce816741c39efe69da9618f3a6.png)![](https://cdn.ramostear.com/2019-03-07-18-25-23-d1af3dce816741c39efe69da9618f3a6.png)\">\r\n<h1 id=\"h1-1-\"><a name=\"1. 创建过滤器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. 创建过滤器</h1><p>在Spring Boot中，自定义的过滤器需要实现Filter接口中的方法，并使用<strong><a href=\"https://github.com/Component\" title=\"@Component\" class=\"at-link\">@Component</a></strong>注解进行标注，具体代码如下：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.filter;\r\n\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.stereotype.Component;\r\n\r\nimport javax.servlet.*;\r\nimport javax.servlet.http.HttpServletRequest;\r\nimport java.io.IOException;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/7 0007-16:12\r\n */\r\n@Component\r\npublic class UserServiceFilter implements Filter{\r\n\r\n    private static final Logger logger = LoggerFactory.getLogger ( UserServiceFilter.class);\r\n\r\n    @Override\r\n    public void init(FilterConfig filterConfig) throws ServletException {}\r\n\r\n    @Override\r\n    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,\r\n                         FilterChain filterChain) throws IOException, ServletException {\r\n        HttpServletRequest request = (HttpServletRequest)servletRequest;\r\n        logger.info ( \"Request url : \" + request.getRequestURL ());\r\n        logger.info ( \"Request method : \"+request.getMethod ());\r\n        filterChain.doFilter ( servletRequest,servletResponse );\r\n    }\r\n\r\n    @Override\r\n    public void destroy() {}\r\n}\r\n</code></pre>\r\n<p>在上述的代码中，当请求发送到控制器之前，过滤器生效，将从ServletRequest对象中读取请求的地址以及请求的方式。</p>\r\n<h1 id=\"h1-2-\"><a name=\"2. 控制器类\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 控制器类</h1><p>接下来，定义一个简单的控制器，用于处理客户端发送的请求，本章节中的控制器我们沿用之前章节中的控制器：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/7 0007-16:28\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepo = new HashMap&lt;&gt; ();\r\n\r\n    @PostConstruct\r\n    public void initUserRepo(){\r\n        User admin = new User ().setId ( 1 ).setName ( \"admin\" );\r\n        userRepo.put ( admin.getId (),admin );\r\n\r\n        User editor = new User ().setId ( 2 ).setName ( \"editor\" );\r\n        userRepo.put ( editor.getId (),editor );\r\n    }\r\n\r\n\r\n    @GetMapping(\"/users\")\r\n    public ResponseEntity&lt;Object&gt; getUser(){\r\n        return new ResponseEntity&lt;&gt; (userRepo.values (), HttpStatus.OK);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>getUser方法用于返回用户存储库中的所有用户信息，映射地址为：<strong>“/users”</strong>。</p>\r\n<h1 id=\"h1-3-\"><a name=\"3. 运行并测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. 运行并测试</h1><p>接下来，你可以使用Maven命令：<strong>mvn clean install</strong>名来创建一个jar文件，并使用<strong>java -jar jarfile</strong>命令来运行程序。下面是程序运行成功后的控制台窗口截图：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-07-18-25-00-72fdfaa40e2a4bd1b704553b4f411ae8.png\" alt=\"\">\r\n<p>可以看到，我们的应用程序在Tomcat服务器上以8080端口启动成功。现在，打开Postman测试应用程序，以GET的方式打开请求地址：<a href=\"http://localhost:8080/users\">http://localhost:8080/users</a> ，并观察返回的信息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-07-18-25-12-bd83bfa6b9b74b29be4e9cf3cb7f2e1c.png\" alt=\"\">\r\n<p>然后，你可以在控制台窗口中看到如下的输出信息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-07-18-25-23-d1af3dce816741c39efe69da9618f3a6.png\" alt=\"\">\r\n','Spring Boot,过滤器',0,'# Spring Boot （九）— 过滤器\r\n\r\n过滤器被用于过滤应用程序的HTTP请求和响应，我们可以在请求发送到控制器之前对请求进行过滤，或者在响应发送到客户端之前进行过滤。在本章节中，通过一个简单的例子来展示在Spring Boot中如何使用过滤器，首先通过一张图来说明过滤器的使用步骤：\r\n\r\n![![](https://cdn.ramostear.com/2019-03-07-18-25-23-d1af3dce816741c39efe69da9618f3a6.png)![](https://cdn.ramostear.com/2019-03-07-18-25-23-d1af3dce816741c39efe69da9618f3a6.png)](https://cdn.ramostear.com/2019-03-07-18-24-47-57ef75e3576d4388a8e6a91e74de1e2c.png)\r\n\r\n# 1. 创建过滤器\r\n\r\n在Spring Boot中，自定义的过滤器需要实现Filter接口中的方法，并使用**@Component**注解进行标注，具体代码如下：\r\n\r\n```java\r\npackage com.ramostear.application.filter;\r\n\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.stereotype.Component;\r\n\r\nimport javax.servlet.*;\r\nimport javax.servlet.http.HttpServletRequest;\r\nimport java.io.IOException;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/7 0007-16:12\r\n */\r\n@Component\r\npublic class UserServiceFilter implements Filter{\r\n\r\n    private static final Logger logger = LoggerFactory.getLogger ( UserServiceFilter.class);\r\n\r\n    @Override\r\n    public void init(FilterConfig filterConfig) throws ServletException {}\r\n\r\n    @Override\r\n    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,\r\n                         FilterChain filterChain) throws IOException, ServletException {\r\n        HttpServletRequest request = (HttpServletRequest)servletRequest;\r\n        logger.info ( \"Request url : \" + request.getRequestURL ());\r\n        logger.info ( \"Request method : \"+request.getMethod ());\r\n        filterChain.doFilter ( servletRequest,servletResponse );\r\n    }\r\n\r\n    @Override\r\n    public void destroy() {}\r\n}\r\n\r\n```\r\n\r\n在上述的代码中，当请求发送到控制器之前，过滤器生效，将从ServletRequest对象中读取请求的地址以及请求的方式。\r\n\r\n# 2. 控制器类\r\n\r\n接下来，定义一个简单的控制器，用于处理客户端发送的请求，本章节中的控制器我们沿用之前章节中的控制器：\r\n\r\n```java\r\npackage com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/7 0007-16:28\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepo = new HashMap<> ();\r\n\r\n    @PostConstruct\r\n    public void initUserRepo(){\r\n        User admin = new User ().setId ( 1 ).setName ( \"admin\" );\r\n        userRepo.put ( admin.getId (),admin );\r\n\r\n        User editor = new User ().setId ( 2 ).setName ( \"editor\" );\r\n        userRepo.put ( editor.getId (),editor );\r\n    }\r\n\r\n\r\n    @GetMapping(\"/users\")\r\n    public ResponseEntity<Object> getUser(){\r\n        return new ResponseEntity<> (userRepo.values (), HttpStatus.OK);\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\ngetUser方法用于返回用户存储库中的所有用户信息，映射地址为：**“/users”**。\r\n\r\n\r\n\r\n# 3. 运行并测试\r\n\r\n接下来，你可以使用Maven命令：**mvn clean install**名来创建一个jar文件，并使用**java -jar jarfile**命令来运行程序。下面是程序运行成功后的控制台窗口截图：\r\n\r\n![](https://cdn.ramostear.com/2019-03-07-18-25-00-72fdfaa40e2a4bd1b704553b4f411ae8.png)\r\n\r\n可以看到，我们的应用程序在Tomcat服务器上以8080端口启动成功。现在，打开Postman测试应用程序，以GET的方式打开请求地址：http://localhost:8080/users ，并观察返回的信息：\r\n\r\n![](https://cdn.ramostear.com/2019-03-07-18-25-12-bd83bfa6b9b74b29be4e9cf3cb7f2e1c.png)\r\n\r\n然后，你可以在控制台窗口中看到如下的输出信息：\r\n\r\n![](https://cdn.ramostear.com/2019-03-07-18-25-23-d1af3dce816741c39efe69da9618f3a6.png)',1,'','2019/54/21/g8o86n0o.html',0,'过滤器被用于过滤应用程序的HTTP请求和响应，我们可以在请求发送到控制器之前对请求进行过滤，或者在响应发送到客户端之前进行过滤','blog.html','https://cdn.ramostear.com/2019-03-28-01-49-57-4da430be0f64413db24148b2f7213bee.png;base64','在Spring Boot中使用过滤器','',0),(6,'2019-11-21 23:00:53',0,'2019-11-21 23:00:53',1,1,1,'谭朝红','Spring Boot提供的RestTemplate类可以在应用中调用Restful Web服务，这极大的简化了与Http服务的通信方式，统一了Restful服务调用的标准','<h1 id=\"h1-spring-boot-resttemplate\"><a name=\"Spring Boot（十）— RestTemplate\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot（十）— RestTemplate</h1><p>Spring Boot提供的RestTemplate类可以在应用中调用Restful Web服务，这极大的简化了与Http服务的通信方式，统一了Restful服务调用的标准，其内部对Http进行了封装，我们只需要传递相应的URL、请求方法、主体信息以及返回值类型即可，相比于HttpClient,RestTemplate以一种更优雅的方式对Restful Web服务进行调用。下面是的代码展示了如何使用<strong><a href=\"https://github.com/Bean\" title=\"@Bean\" class=\"at-link\">@Bean</a></strong>注解创建RestTemplate并进行连接。</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.context.annotation.Bean;\r\nimport org.springframework.web.client.RestTemplate;\r\n\r\n@SpringBootApplication\r\npublic class RestTemplateApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(RestTemplateApplication.class, args);\r\n    }\r\n\r\n    @Bean\r\n    public RestTemplate restTemplate(){\r\n        return new RestTemplate();\r\n    }\r\n}\r\n</code></pre>\r\n<p>接下来，我将演示不同请求方法下如何使用RestTemplate。在开始正文之前，我们需要创建一个提供Restful Web Service的控制器，下面给出的代码显示了控制器的详细内容：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepository = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n    @GetMapping(\"/users\")\r\n    public ResponseEntity&lt;Object&gt; getUser(){\r\n        return new ResponseEntity&lt;&gt;(userRepository.values(), HttpStatus.OK);\r\n    }\r\n\r\n    @PostMapping(\"/users\")\r\n    public ResponseEntity&lt;Object&gt; createUser(@RequestBody User user){\r\n        userRepository.put(user.getId(),user);\r\n        return new ResponseEntity&lt;&gt;(\"User is created successfully\",HttpStatus.CREATED);\r\n    }\r\n\r\n    @PutMapping(\"/users/{id}\")\r\n    public ResponseEntity&lt;Object&gt; updateUser(@PathVariable(name = \"id\") long id,@RequestBody User user){\r\n        userRepository.remove(id);\r\n        user.setId(id);\r\n        userRepository.put(id,user);\r\n        return new ResponseEntity&lt;&gt;(\"User is updated successfully\",HttpStatus.OK);\r\n    }\r\n\r\n    @DeleteMapping(\"/users/{id}\")\r\n    public ResponseEntity&lt;Object&gt; delete(@PathVariable(name = \"id\") long id){\r\n        userRepository.remove(id);\r\n        return new ResponseEntity&lt;&gt;(\"User is deleted successfully\",HttpStatus.OK);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>除此之外，还需要创建一个数据模型作为用户信息的载体，下面的代码给出了用户数据模型的详细信息：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.model;\r\n\r\nimport lombok.Getter;\r\nimport lombok.NoArgsConstructor;\r\nimport lombok.Setter;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:12\r\n * @modify by :\r\n * @since:\r\n */\r\n@Getter\r\n@Setter\r\n@NoArgsConstructor\r\npublic class User {\r\n    private long id;\r\n    private String name;\r\n\r\n    public User setId(long id){\r\n        this.id = id;\r\n        return this;\r\n    }\r\n\r\n    public User setName(String name){\r\n        this.name = name;\r\n        return this;\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-1-http-get-\"><a name=\"1. HTTP GET请求\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. HTTP GET请求</h1><p>现在，我们可以使用RestTemplate的exchange()方法来请求URL为<strong><a href=\"http://localhost:8080/users\">http://localhost:8080/users</a></strong>的<strong>GET API</strong>。下面将给出RestTemplate调用Restful Web服务的基本流程：</p>\r\n<ul>\r\n<li>创建RestTemplate对象，这里使用<a href=\"https://github.com/AutoWired\" title=\"@AutoWired\" class=\"at-link\">@AutoWired</a>注解进行自动装配</li><li>使用HttpHeaders对象设置请求头参数</li><li>使用HttpEntity包装类对请求对象进行包装</li><li>调用exchange()方法，并传入URL、HttpMethod以及返回值类型</li></ul>\r\n<p>下面的代码显示了使用RestTemplate的exchange()方法调用Restful Web服务的详细过程：</p>\r\n<pre><code class=\"lang-java\">@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n    @GetMapping(\"/rest/template/users\")\r\n    public String getUsers(){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity&lt;String&gt; entity = new HttpEntity&lt;&gt;(headers);\r\n        return restTemplate.exchange(ROOT_URI, HttpMethod.GET,entity,String.class).getBody();\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-2-http-post-\"><a name=\"2. HTTP POST请求\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. HTTP POST请求</h1><p>RestTemplate调用<strong>POST API</strong>的流程与调用<strong>GET API</strong>的流程基本相同，现在，我们使用RestTemplate调用<strong>POST API</strong>创建一个新的用户。相比调用<strong>GET API</strong>，唯一不同的地方在于需要把用户信息包装到请求主体中，同时需要将请求方法修改为POST。下面的代码展示了如何调用POST API:</p>\r\n<pre><code class=\"lang-java\">@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n    @PostMapping(\"/rest/template/users\")\r\n    public String createdUser(@RequestBody User user){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity&lt;User&gt; entity = new HttpEntity&lt;&gt;(user,headers);\r\n        return restTemplate.exchange(ROOT_URI,HttpMethod.POST,entity,String.class).getBody();\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-3-http-put-\"><a name=\"3. HTTP PUT 请求\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. HTTP PUT 请求</h1><p>接下来，使用RestTemplate调用<strong>PUT API</strong>对用户信息进行修改，与调用<strong>POST API</strong>的方法相同，我们需要将待修改的用户信息包装到请求主体中，同时需要修改请求方法为PUT。下面代码展示了如何调用PUT API:</p>\r\n<pre><code class=\"lang-java\">@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n   @PutMapping(\"/rest/template/users/{id}\")\r\n    public String updateUser(@PathVariable(name = \"id\")long id,@RequestBody User user){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity&lt;User&gt; entity = new HttpEntity&lt;&gt;(user,headers);\r\n        return restTemplate.exchange(ROOT_URI+\"/\"+id,HttpMethod.PUT,entity,String.class).getBody();\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-4-http-delete-\"><a name=\"4. HTTP DELETE请求\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. HTTP DELETE请求</h1><p>RestTemplate调用<strong>DELETE API</strong>的方法与上述三种请求方法基本一致，需要注意的是HttpMethod的设置，需要设置为<strong>DELETE</strong>。下面的代码展示了如何调用<strong>DELETE API</strong>删除用户信息：</p>\r\n<pre><code class=\"lang-java\">@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n   @DeleteMapping(\"/rest/template/users/{id}\")\r\n    public String deleteUser(@PathVariable(name = \"id\")long id){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity&lt;User&gt; entity = new HttpEntity&lt;&gt;(headers);\r\n        return restTemplate.exchange(ROOT_URI+\"/\"+id,HttpMethod.DELETE,entity,String.class).getBody();\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-5-\"><a name=\"5. 代码清单\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5. 代码清单</h1><p>下面给出的是完整的RestTemplate调用Restful Web服务的代码：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.webservice;\r\n\r\nimport com.ramostear.application.model.User;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.http.HttpEntity;\r\nimport org.springframework.http.HttpHeaders;\r\nimport org.springframework.http.HttpMethod;\r\nimport org.springframework.http.MediaType;\r\nimport org.springframework.web.bind.annotation.*;\r\nimport org.springframework.web.client.RestTemplate;\r\n\r\nimport java.util.Arrays;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/8 0008-0:36\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n    @GetMapping(\"/rest/template/users\")\r\n    public String getUsers(){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity&lt;String&gt; entity = new HttpEntity&lt;&gt;(headers);\r\n        return restTemplate.exchange(ROOT_URI, HttpMethod.GET,entity,String.class).getBody();\r\n    }\r\n\r\n    @PostMapping(\"/rest/template/users\")\r\n    public String createdUser(@RequestBody User user){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity&lt;User&gt; entity = new HttpEntity&lt;&gt;(user,headers);\r\n        return restTemplate.exchange(ROOT_URI,HttpMethod.POST,entity,String.class).getBody();\r\n    }\r\n\r\n    @PutMapping(\"/rest/template/users/{id}\")\r\n    public String updateUser(@PathVariable(name = \"id\")long id,@RequestBody User user){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity&lt;User&gt; entity = new HttpEntity&lt;&gt;(user,headers);\r\n        return restTemplate.exchange(ROOT_URI+\"/\"+id,HttpMethod.PUT,entity,String.class).getBody();\r\n    }\r\n\r\n    @DeleteMapping(\"/rest/template/users/{id}\")\r\n    public String deleteUser(@PathVariable(name = \"id\")long id){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity&lt;User&gt; entity = new HttpEntity&lt;&gt;(headers);\r\n        return restTemplate.exchange(ROOT_URI+\"/\"+id,HttpMethod.DELETE,entity,String.class).getBody();\r\n    }\r\n}\r\n</code></pre>\r\n<p>Maven build 文件 — pom.xml:</p>\r\n<pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n    xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"&gt;\r\n    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;\r\n    &lt;parent&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\r\n        &lt;version&gt;2.1.3.RELEASE&lt;/version&gt;\r\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\r\n    &lt;/parent&gt;\r\n    &lt;groupId&gt;com.ramostear&lt;/groupId&gt;\r\n    &lt;artifactId&gt;rest-template&lt;/artifactId&gt;\r\n    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;\r\n    &lt;name&gt;rest-template&lt;/name&gt;\r\n    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;\r\n\r\n    &lt;properties&gt;\r\n        &lt;java.version&gt;1.8&lt;/java.version&gt;\r\n    &lt;/properties&gt;\r\n\r\n    &lt;dependencies&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;\r\n            &lt;artifactId&gt;lombok&lt;/artifactId&gt;\r\n            &lt;optional&gt;true&lt;/optional&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\r\n            &lt;scope&gt;test&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n    &lt;/dependencies&gt;\r\n\r\n    &lt;build&gt;\r\n        &lt;plugins&gt;\r\n            &lt;plugin&gt;\r\n                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;\r\n            &lt;/plugin&gt;\r\n        &lt;/plugins&gt;\r\n    &lt;/build&gt;\r\n\r\n&lt;/project&gt;\r\n</code></pre>\r\n<p>应用程序主文件 — RestTemplateApplication.java:</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.context.annotation.Bean;\r\nimport org.springframework.web.client.RestTemplate;\r\n\r\n@SpringBootApplication\r\npublic class RestTemplateApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(RestTemplateApplication.class, args);\r\n    }\r\n\r\n    @Bean\r\n    public RestTemplate restTemplate(){\r\n        return new RestTemplate();\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-6-\"><a name=\"6. 打包运行\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6. 打包运行</h1><p>现在，可以使用下面的命令创建一个可执行的jar文件，并运行该jar文件。</p>\r\n<p>使用maven命令创建可执行的jar文件：</p>\r\n<pre><code class=\"lang-tex\">mvn clean install\r\n</code></pre>\r\n<p>执行此命令，当控制台窗口显示<strong>“BUILD SUCCESS”</strong>信息后，你可以在当前工程目下的target子目录中找到创建的JAR文件。</p>\r\n<p>接下来，使用以下命令运行创建的JAR文件：</p>\r\n<pre><code class=\"lang-tex\">java -jar JARFILE\r\n</code></pre>\r\n<p>执行命令，并观察控制台窗口输出的日志信息。</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-02-37-37-0b08fda7ae6843dd983441596c848646.png\" alt=\"\">\r\n<h1 id=\"h1-7-\"><a name=\"7. 测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7. 测试</h1><p>启动Postman应用程序，在地址栏中输入以下的URL，并观察测试结果。</p>\r\n<h2 id=\"h2-7-1-resttemplate-\"><a name=\"7.1 通过RestTemplate获取用户信息\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.1 通过RestTemplate获取用户信息</h2><p>请求URL:<a href=\"http://localhost:8080/rest/template/users\">http://localhost:8080/rest/template/users</a> </p>\r\n<p>请求方法:GET</p>\r\n<p>参数:无</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-02-37-52-47f36f2435f547209ddff3f8fb116fed.png\" alt=\"\">\r\n<h2 id=\"h2-7-2-resttemplate-\"><a name=\"7.2 通过RestTemplate新增用户\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.2 通过RestTemplate新增用户</h2><p>请求URL: <a href=\"http://localhost:8080/rest/template/users\">http://localhost:8080/rest/template/users</a> </p>\r\n<p>请求方法：POST</p>\r\n<p>参数：{“id”:3,”name”:ramostear}</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-02-38-06-c9c6e1bd83634feea70f1899d25279b3.png\" alt=\"\">\r\n<h2 id=\"h2-7-3-resttemplate-\"><a name=\"7.3 通过RestTemplate更新用户\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.3 通过RestTemplate更新用户</h2><p>请求URL: <a href=\"http://localhost:8080/rest/template/users/3\">http://localhost:8080/rest/template/users/3</a></p>\r\n<p>请求方法：PUT</p>\r\n<p>参数：{“id”:3,”name”:”reader”}</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-02-38-19-7eb2aacf3f4540378e6cd3d7224db0e6.png\" alt=\"\">\r\n<h2 id=\"h2-7-4-resttemplate-\"><a name=\"7.4 通过RestTemplate删除用户\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.4 通过RestTemplate删除用户</h2><p>请求URL: <a href=\"http://localhost:8080/rest/template/users/3\">http://localhost:8080/rest/template/users/3</a></p>\r\n<p>请求方法：DELETE</p>\r\n<p>参数：无</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-02-38-32-6ca0fcd77837408ba3a449dc94afdf34.png\" alt=\"\">\r\n','Spring Boot,RestTemplate',0,'# Spring Boot（十）— RestTemplate\r\n\r\nSpring Boot提供的RestTemplate类可以在应用中调用Restful Web服务，这极大的简化了与Http服务的通信方式，统一了Restful服务调用的标准，其内部对Http进行了封装，我们只需要传递相应的URL、请求方法、主体信息以及返回值类型即可，相比于HttpClient,RestTemplate以一种更优雅的方式对Restful Web服务进行调用。下面是的代码展示了如何使用**@Bean**注解创建RestTemplate并进行连接。\r\n\r\n```java\r\npackage com.ramostear.application;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.context.annotation.Bean;\r\nimport org.springframework.web.client.RestTemplate;\r\n\r\n@SpringBootApplication\r\npublic class RestTemplateApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(RestTemplateApplication.class, args);\r\n	}\r\n\r\n	@Bean\r\n	public RestTemplate restTemplate(){\r\n		return new RestTemplate();\r\n	}\r\n}\r\n\r\n```\r\n\r\n接下来，我将演示不同请求方法下如何使用RestTemplate。在开始正文之前，我们需要创建一个提供Restful Web Service的控制器，下面给出的代码显示了控制器的详细内容：\r\n\r\n```java\r\npackage com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepository = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n    @GetMapping(\"/users\")\r\n    public ResponseEntity<Object> getUser(){\r\n        return new ResponseEntity<>(userRepository.values(), HttpStatus.OK);\r\n    }\r\n\r\n    @PostMapping(\"/users\")\r\n    public ResponseEntity<Object> createUser(@RequestBody User user){\r\n        userRepository.put(user.getId(),user);\r\n        return new ResponseEntity<>(\"User is created successfully\",HttpStatus.CREATED);\r\n    }\r\n\r\n    @PutMapping(\"/users/{id}\")\r\n    public ResponseEntity<Object> updateUser(@PathVariable(name = \"id\") long id,@RequestBody User user){\r\n        userRepository.remove(id);\r\n        user.setId(id);\r\n        userRepository.put(id,user);\r\n        return new ResponseEntity<>(\"User is updated successfully\",HttpStatus.OK);\r\n    }\r\n\r\n    @DeleteMapping(\"/users/{id}\")\r\n    public ResponseEntity<Object> delete(@PathVariable(name = \"id\") long id){\r\n        userRepository.remove(id);\r\n        return new ResponseEntity<>(\"User is deleted successfully\",HttpStatus.OK);\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\n除此之外，还需要创建一个数据模型作为用户信息的载体，下面的代码给出了用户数据模型的详细信息：\r\n\r\n```java\r\npackage com.ramostear.application.model;\r\n\r\nimport lombok.Getter;\r\nimport lombok.NoArgsConstructor;\r\nimport lombok.Setter;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:12\r\n * @modify by :\r\n * @since:\r\n */\r\n@Getter\r\n@Setter\r\n@NoArgsConstructor\r\npublic class User {\r\n    private long id;\r\n    private String name;\r\n\r\n    public User setId(long id){\r\n        this.id = id;\r\n        return this;\r\n    }\r\n\r\n    public User setName(String name){\r\n        this.name = name;\r\n        return this;\r\n    }\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 1. HTTP GET请求\r\n\r\n现在，我们可以使用RestTemplate的exchange()方法来请求URL为**http://localhost:8080/users**的**GET API**。下面将给出RestTemplate调用Restful Web服务的基本流程：\r\n\r\n- 创建RestTemplate对象，这里使用@AutoWired注解进行自动装配\r\n- 使用HttpHeaders对象设置请求头参数\r\n- 使用HttpEntity包装类对请求对象进行包装\r\n- 调用exchange()方法，并传入URL、HttpMethod以及返回值类型\r\n\r\n下面的代码显示了使用RestTemplate的exchange()方法调用Restful Web服务的详细过程：\r\n\r\n```java\r\n@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n    @GetMapping(\"/rest/template/users\")\r\n    public String getUsers(){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity<String> entity = new HttpEntity<>(headers);\r\n        return restTemplate.exchange(ROOT_URI, HttpMethod.GET,entity,String.class).getBody();\r\n    }\r\n}\r\n```\r\n\r\n\r\n\r\n# 2. HTTP POST请求\r\n\r\nRestTemplate调用**POST API**的流程与调用**GET API**的流程基本相同，现在，我们使用RestTemplate调用**POST API**创建一个新的用户。相比调用**GET API**，唯一不同的地方在于需要把用户信息包装到请求主体中，同时需要将请求方法修改为POST。下面的代码展示了如何调用POST API:\r\n\r\n```java\r\n@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n    @PostMapping(\"/rest/template/users\")\r\n    public String createdUser(@RequestBody User user){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity<User> entity = new HttpEntity<>(user,headers);\r\n        return restTemplate.exchange(ROOT_URI,HttpMethod.POST,entity,String.class).getBody();\r\n    }\r\n}\r\n```\r\n\r\n# 3. HTTP PUT 请求\r\n\r\n接下来，使用RestTemplate调用**PUT API**对用户信息进行修改，与调用**POST API**的方法相同，我们需要将待修改的用户信息包装到请求主体中，同时需要修改请求方法为PUT。下面代码展示了如何调用PUT API:\r\n\r\n```java\r\n@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n   @PutMapping(\"/rest/template/users/{id}\")\r\n    public String updateUser(@PathVariable(name = \"id\")long id,@RequestBody User user){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity<User> entity = new HttpEntity<>(user,headers);\r\n        return restTemplate.exchange(ROOT_URI+\"/\"+id,HttpMethod.PUT,entity,String.class).getBody();\r\n    }\r\n}\r\n```\r\n\r\n\r\n\r\n# 4. HTTP DELETE请求\r\n\r\nRestTemplate调用**DELETE API**的方法与上述三种请求方法基本一致，需要注意的是HttpMethod的设置，需要设置为**DELETE**。下面的代码展示了如何调用**DELETE API**删除用户信息：\r\n\r\n```java\r\n@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n   @DeleteMapping(\"/rest/template/users/{id}\")\r\n    public String deleteUser(@PathVariable(name = \"id\")long id){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity<User> entity = new HttpEntity<>(headers);\r\n        return restTemplate.exchange(ROOT_URI+\"/\"+id,HttpMethod.DELETE,entity,String.class).getBody();\r\n    }\r\n}\r\n```\r\n\r\n\r\n\r\n# 5. 代码清单\r\n\r\n\r\n\r\n下面给出的是完整的RestTemplate调用Restful Web服务的代码：\r\n\r\n```java\r\npackage com.ramostear.application.webservice;\r\n\r\nimport com.ramostear.application.model.User;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.http.HttpEntity;\r\nimport org.springframework.http.HttpHeaders;\r\nimport org.springframework.http.HttpMethod;\r\nimport org.springframework.http.MediaType;\r\nimport org.springframework.web.bind.annotation.*;\r\nimport org.springframework.web.client.RestTemplate;\r\n\r\nimport java.util.Arrays;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/8 0008-0:36\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserWebService {\r\n\r\n    @Autowired\r\n    RestTemplate restTemplate;\r\n\r\n    private static final String ROOT_URI = \"http://localhost:8080/users\";\r\n\r\n    @GetMapping(\"/rest/template/users\")\r\n    public String getUsers(){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity<String> entity = new HttpEntity<>(headers);\r\n        return restTemplate.exchange(ROOT_URI, HttpMethod.GET,entity,String.class).getBody();\r\n    }\r\n\r\n    @PostMapping(\"/rest/template/users\")\r\n    public String createdUser(@RequestBody User user){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity<User> entity = new HttpEntity<>(user,headers);\r\n        return restTemplate.exchange(ROOT_URI,HttpMethod.POST,entity,String.class).getBody();\r\n    }\r\n\r\n    @PutMapping(\"/rest/template/users/{id}\")\r\n    public String updateUser(@PathVariable(name = \"id\")long id,@RequestBody User user){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity<User> entity = new HttpEntity<>(user,headers);\r\n        return restTemplate.exchange(ROOT_URI+\"/\"+id,HttpMethod.PUT,entity,String.class).getBody();\r\n    }\r\n\r\n    @DeleteMapping(\"/rest/template/users/{id}\")\r\n    public String deleteUser(@PathVariable(name = \"id\")long id){\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));\r\n        HttpEntity<User> entity = new HttpEntity<>(headers);\r\n        return restTemplate.exchange(ROOT_URI+\"/\"+id,HttpMethod.DELETE,entity,String.class).getBody();\r\n    }\r\n}\r\n\r\n```\r\n\r\nMaven build 文件 — pom.xml:\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n	xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n	<modelVersion>4.0.0</modelVersion>\r\n	<parent>\r\n		<groupId>org.springframework.boot</groupId>\r\n		<artifactId>spring-boot-starter-parent</artifactId>\r\n		<version>2.1.3.RELEASE</version>\r\n		<relativePath/> <!-- lookup parent from repository -->\r\n	</parent>\r\n	<groupId>com.ramostear</groupId>\r\n	<artifactId>rest-template</artifactId>\r\n	<version>0.0.1-SNAPSHOT</version>\r\n	<name>rest-template</name>\r\n	<description>Demo project for Spring Boot</description>\r\n\r\n	<properties>\r\n		<java.version>1.8</java.version>\r\n	</properties>\r\n\r\n	<dependencies>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-web</artifactId>\r\n		</dependency>\r\n\r\n		<dependency>\r\n			<groupId>org.projectlombok</groupId>\r\n			<artifactId>lombok</artifactId>\r\n			<optional>true</optional>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-test</artifactId>\r\n			<scope>test</scope>\r\n		</dependency>\r\n	</dependencies>\r\n\r\n	<build>\r\n		<plugins>\r\n			<plugin>\r\n				<groupId>org.springframework.boot</groupId>\r\n				<artifactId>spring-boot-maven-plugin</artifactId>\r\n			</plugin>\r\n		</plugins>\r\n	</build>\r\n\r\n</project>\r\n\r\n```\r\n\r\n应用程序主文件 — RestTemplateApplication.java:\r\n\r\n```java\r\npackage com.ramostear.application;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.context.annotation.Bean;\r\nimport org.springframework.web.client.RestTemplate;\r\n\r\n@SpringBootApplication\r\npublic class RestTemplateApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(RestTemplateApplication.class, args);\r\n	}\r\n\r\n	@Bean\r\n	public RestTemplate restTemplate(){\r\n		return new RestTemplate();\r\n	}\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 6. 打包运行\r\n\r\n现在，可以使用下面的命令创建一个可执行的jar文件，并运行该jar文件。\r\n\r\n使用maven命令创建可执行的jar文件：\r\n\r\n```tex\r\nmvn clean install\r\n```\r\n\r\n执行此命令，当控制台窗口显示**“BUILD SUCCESS”**信息后，你可以在当前工程目下的target子目录中找到创建的JAR文件。\r\n\r\n接下来，使用以下命令运行创建的JAR文件：\r\n\r\n```tex\r\njava -jar JARFILE\r\n```\r\n\r\n执行命令，并观察控制台窗口输出的日志信息。\r\n\r\n![](https://cdn.ramostear.com/2019-03-08-02-37-37-0b08fda7ae6843dd983441596c848646.png)\r\n\r\n\r\n\r\n# 7. 测试\r\n\r\n启动Postman应用程序，在地址栏中输入以下的URL，并观察测试结果。\r\n\r\n## 7.1 通过RestTemplate获取用户信息\r\n\r\n请求URL:http://localhost:8080/rest/template/users \r\n\r\n请求方法:GET\r\n\r\n参数:无\r\n\r\n![](https://cdn.ramostear.com/2019-03-08-02-37-52-47f36f2435f547209ddff3f8fb116fed.png)\r\n\r\n\r\n\r\n## 7.2 通过RestTemplate新增用户\r\n\r\n请求URL: http://localhost:8080/rest/template/users \r\n\r\n请求方法：POST\r\n\r\n参数：{“id”:3,\"name\":ramostear}\r\n\r\n![](https://cdn.ramostear.com/2019-03-08-02-38-06-c9c6e1bd83634feea70f1899d25279b3.png)\r\n\r\n\r\n\r\n## 7.3 通过RestTemplate更新用户\r\n\r\n请求URL: http://localhost:8080/rest/template/users/3\r\n\r\n请求方法：PUT\r\n\r\n参数：{“id”:3,\"name\":\"reader\"}\r\n\r\n![](https://cdn.ramostear.com/2019-03-08-02-38-19-7eb2aacf3f4540378e6cd3d7224db0e6.png)\r\n\r\n\r\n\r\n## 7.4 通过RestTemplate删除用户\r\n\r\n请求URL: http://localhost:8080/rest/template/users/3\r\n\r\n请求方法：DELETE\r\n\r\n参数：无\r\n\r\n![](https://cdn.ramostear.com/2019-03-08-02-38-32-6ca0fcd77837408ba3a449dc94afdf34.png)',1,'','2019/58/21/g80x26x2.html',0,'Spring Boot提供的RestTemplate类可以在应用中调用Restful Web服务，这极大的简化了与Http服务的通信方式，统一了Restful服务调用的标准','blog.html','https://cdn.ramostear.com/2019-03-28-01-51-58-67dfcdc0fc5643ebb14ec7e5a0f90212.png;base64','使用RestTemplate对Restful服务进行调用','',0),(7,'2019-11-21 23:03:17',0,'2019-11-21 23:03:17',1,1,1,'谭朝红','文件上传和下载是Web应用程序比较常用的功能之一，在本章节中，我将以一个简单的案例来讲解在Spring Boot中如何进行文件的上传与下载','<h1 id=\"h1-spring-boot-\"><a name=\"Spring Boot（十一）— 文件上传和下载\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot（十一）— 文件上传和下载</h1><p>文件上传和下载是Web应用程序比较常用的功能之一，在本章节中，我将以一个简单的案例来讲解在Spring Boot中如何进行文件的上传与下载。在开始正文之前，我们通过一张思维导图来了解一下文件上传与下载的简单流程：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-18-22-31-780bd13186fc406db8212e9e87e65d66.png\" alt=\"文件上传与下载思维导图\" title=\"文件上传与下载思维导图\">\r\n<h1 id=\"h1-1-\"><a name=\"1. 文件上传\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. 文件上传</h1><p>对于文件上传，控制器中对应的上传方法的参数必须是<strong>MultipartFile</strong>对象，<strong>MultipartFile</strong>对象可以是一个数组对象，也可以是单个对象，如果是一个数组对象，则可以进行多文件上传；这里我们仅演示单个文件上传，下面的代码展示了文件上传方法的基本结构：</p>\r\n<pre><code class=\"lang-java\">@PostMapping(value = \"/upload\",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)\r\n@ResponseBody\r\npublic String fileUpload(@RequestParam(\"file\") MultipartFile file) throws IOException {\r\n    return null;\r\n}\r\n</code></pre>\r\n<p>接下来，我们使用<strong>FileOutputStream</strong>对象将客户端上传的文件写入到磁盘中，并返回<strong>“File is upload successfully”</strong>的提示信息，下面是文件上传完整的代码：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.FileInfo;\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.stereotype.Controller;\r\nimport org.springframework.web.bind.annotation.*;\r\nimport org.springframework.web.multipart.MultipartFile;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.io.*;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/8 0008-15:35\r\n */\r\n@Controller\r\npublic class FileController {\r\n\r\n\r\n    @Value ( \"${file.upload.root.dir}\" )\r\n    String fileUploadRootDir;\r\n\r\n    private static Map&lt;String,FileInfo&gt; fileRepository = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initFileRepository(){\r\n        FileInfo file1 = new FileInfo ().setFileName ( \"bg1.jpg\" );\r\n        FileInfo file2 = new FileInfo ().setFileName ( \"bg2.jpg\" );\r\n        FileInfo file3 = new FileInfo ().setFileName ( \"bg3.jpg\" );\r\n        fileRepository.put ( file1.getName (),file1 );\r\n        fileRepository.put ( file2.getName (),file2 );\r\n        fileRepository.put ( file3.getName (),file3 );\r\n    }\r\n\r\n@PostMapping(value = \"/upload\",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)\r\n@ResponseBody\r\npublic String fileUpload(@RequestParam(\"file\") MultipartFile file) throws IOException {\r\n\r\n        File convertFile = new File ( fileUploadRootDir+file.getOriginalFilename ());\r\n        FileOutputStream fileOutputStream = new FileOutputStream ( convertFile );\r\n        fileOutputStream.write ( file.getBytes () );\r\n        fileOutputStream.close ();\r\n\r\n        FileInfo fileInfo = new FileInfo()\r\n                .setFileName ( file.getOriginalFilename());\r\n\r\n        fileRepository.put ( fileInfo.getName (),fileInfo);\r\n\r\n        return \"File is upload successfully\";\r\n    }\r\n}\r\n</code></pre>\r\n<p><strong>fileRepository</strong>用于存放已上传文件的索引信息。</p>\r\n<h1 id=\"h1-2-\"><a name=\"2. 文件下载\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 文件下载</h1><p>在Spring Boot应用程序中，我们可以使用<strong>InputStreamResource</strong>对象来下载文件，在下载文件的方法中，我们需要通过Response来设置HttpHeander对象的相关属性，如<strong>Content-Disposition</strong>、<strong>Cache-Control</strong>、<strong>Pragma</strong>和<strong>Expires</strong>等属性。除此之外，还需要指定Response的响应类型。下面的代码给出了文件下载的详细信息：</p>\r\n<pre><code class=\"lang-java\">@GetMapping(\"/download/{fileName}\")\r\n@ResponseBody\r\npublic ResponseEntity&lt;Object&gt; downloadFile(@PathVariable(name = \"fileName\") String fileName) throws FileNotFoundException {\r\n\r\n        File file = new File ( fileUploadRootDir+fileName);\r\n        InputStreamResource resource = new InputStreamResource ( new FileInputStream ( file ) );\r\n\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.add ( \"Content-Disposition\",String.format(\"attachment;filename=\\\"%s\",fileName));\r\n        headers.add ( \"Cache-Control\",\"no-cache,no-store,must-revalidate\" );\r\n        headers.add ( \"Pragma\",\"no-cache\" );\r\n        headers.add ( \"Expires\",\"0\" );\r\n\r\n        ResponseEntity&lt;Object&gt; responseEntity = ResponseEntity.ok()\r\n                .headers ( headers )\r\n                .contentLength ( file.length ())\r\n                .contentType(MediaType.parseMediaType ( \"application/txt\" ))\r\n                .body(resource);\r\n\r\n        return responseEntity;\r\n    }\r\n</code></pre>\r\n<h1 id=\"h1-3-\"><a name=\"3. 代码清单\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. 代码清单</h1><h2 id=\"h2-3-1-\"><a name=\"3.1 文件上传和下载控制器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.1 文件上传和下载控制器</h2><p>下面给出的是完整的文件上传和下载的代码：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.FileInfo;\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.core.io.InputStreamResource;\r\nimport org.springframework.http.HttpHeaders;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.MediaType;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.stereotype.Controller;\r\nimport org.springframework.ui.Model;\r\nimport org.springframework.web.bind.annotation.*;\r\nimport org.springframework.web.multipart.MultipartFile;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.io.*;\r\nimport java.util.Collection;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/8 0008-15:35\r\n */\r\n@Controller\r\npublic class FileController {\r\n\r\n\r\n    @Value ( \"${file.upload.root.dir}\" )\r\n    String fileUploadRootDir;\r\n\r\n    private static Map&lt;String,FileInfo&gt; fileRepository = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initFileRepository(){\r\n        FileInfo file1 = new FileInfo ().setFileName ( \"bg1.jpg\" );\r\n        FileInfo file2 = new FileInfo ().setFileName ( \"bg2.jpg\" );\r\n        FileInfo file3 = new FileInfo ().setFileName ( \"bg3.jpg\" );\r\n        fileRepository.put ( file1.getName (),file1 );\r\n        fileRepository.put ( file2.getName (),file2 );\r\n        fileRepository.put ( file3.getName (),file3 );\r\n    }\r\n\r\n    @GetMapping(\"/files\")\r\n    public String files(Model model){\r\n        Collection&lt;FileInfo&gt; files = fileRepository.values ();\r\n        model.addAttribute ( \"data\",files );\r\n        return \"files\";\r\n    }\r\n\r\n\r\n    @PostMapping(value = \"/upload\",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)\r\n    @ResponseBody\r\n    public String fileUpload(@RequestParam(\"file\") MultipartFile file) throws IOException {\r\n\r\n        File convertFile = new File ( fileUploadRootDir+file.getOriginalFilename ());\r\n        FileOutputStream fileOutputStream = new FileOutputStream ( convertFile );\r\n        fileOutputStream.write ( file.getBytes () );\r\n        fileOutputStream.close ();\r\n\r\n        FileInfo fileInfo = new FileInfo()\r\n                .setFileName ( file.getOriginalFilename());\r\n\r\n        fileRepository.put ( fileInfo.getName (),fileInfo);\r\n\r\n        return \"File is upload successfully\";\r\n    }\r\n\r\n    @GetMapping(\"/download/{fileName}\")\r\n    @ResponseBody\r\n    public ResponseEntity&lt;Object&gt; downloadFile(@PathVariable(name = \"fileName\") String fileName) throws FileNotFoundException {\r\n\r\n        File file = new File ( fileUploadRootDir+fileName);\r\n        InputStreamResource resource = new InputStreamResource ( new FileInputStream ( file ) );\r\n\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.add ( \"Content-Disposition\",String.format(\"attachment;filename=\\\"%s\",fileName));\r\n        headers.add ( \"Cache-Control\",\"no-cache,no-store,must-revalidate\" );\r\n        headers.add ( \"Pragma\",\"no-cache\" );\r\n        headers.add ( \"Expires\",\"0\" );\r\n\r\n        ResponseEntity&lt;Object&gt; responseEntity = ResponseEntity.ok()\r\n                .headers ( headers )\r\n                .contentLength ( file.length ())\r\n                .contentType(MediaType.parseMediaType ( \"application/txt\" ))\r\n                .body(resource);\r\n\r\n        return responseEntity;\r\n    }\r\n\r\n\r\n\r\n}\r\n</code></pre>\r\n<h2 id=\"h2-3-2-\"><a name=\"3.2 数据模型\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.2 数据模型</h2><p>创建一个文件信息数据模型作为上传文件信息的载体，下面是FileInfo.java的代码：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.model;\r\n\r\nimport lombok.Data;\r\n\r\nimport java.util.Date;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date  : 2019/3/8 0008-15:25\r\n */\r\n@Data\r\npublic class FileInfo {\r\n\r\n    private String name;\r\n    private Date uploadTime = new Date();\r\n\r\n    public FileInfo setFileName(String name){\r\n        this.setName ( name );\r\n        return this;\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<h2 id=\"h2-3-3-maven-build-\"><a name=\"3.3 Maven build 文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.3 Maven build 文件</h2><p>下面是本次demo应用程序的pom.xml文件配置清单：</p>\r\n<pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n    xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"&gt;\r\n    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;\r\n    &lt;parent&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\r\n        &lt;version&gt;2.1.3.RELEASE&lt;/version&gt;\r\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\r\n    &lt;/parent&gt;\r\n    &lt;groupId&gt;com.ramostear&lt;/groupId&gt;\r\n    &lt;artifactId&gt;file-handling&lt;/artifactId&gt;\r\n    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;\r\n    &lt;name&gt;file-handling&lt;/name&gt;\r\n    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;\r\n\r\n    &lt;properties&gt;\r\n        &lt;java.version&gt;1.8&lt;/java.version&gt;\r\n    &lt;/properties&gt;\r\n\r\n    &lt;dependencies&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-thymeleaf&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-freemarker&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;\r\n            &lt;artifactId&gt;lombok&lt;/artifactId&gt;\r\n            &lt;optional&gt;true&lt;/optional&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\r\n            &lt;scope&gt;test&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n    &lt;/dependencies&gt;\r\n\r\n    &lt;build&gt;\r\n        &lt;plugins&gt;\r\n            &lt;plugin&gt;\r\n                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;\r\n            &lt;/plugin&gt;\r\n        &lt;/plugins&gt;\r\n    &lt;/build&gt;\r\n\r\n&lt;/project&gt;\r\n</code></pre>\r\n<blockquote>\r\n<p>注：本次案例使用freemarker模板引擎作为视图模板</p>\r\n</blockquote>\r\n<h2 id=\"h2-3-4-\"><a name=\"3.4 配置文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.4 配置文件</h2><p>application.properties文件主要设置了<strong>freemarker</strong>的相关属性以及自定义的<strong>file.upload.root.dir </strong>属性:</p>\r\n<pre><code class=\"lang-properties\">spring.freemarker.cache=false\r\nspring.freemarker.prefix=\r\nspring.freemarker.suffix=.html\r\nspring.freemarker.enabled=true\r\nspring.freemarker.charset=UTF-8\r\nspring.freemarker.template-loader-path=classpath:/templates/\r\nfile.upload.root.dir = C:/work/upload/\r\n</code></pre>\r\n<p><strong>file.upload.root.dir</strong>自定义属性设置了文件上传的更目录为：<strong>C:/work/upload/</strong></p>\r\n<h2 id=\"h2-3-5-\"><a name=\"3.5 视图\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.5 视图</h2><p>在视图文件中，创建了一个form表单用于上传文件，另外还创建了一个已上传文件列表，提供文件下载操作。</p>\r\n<p>文件上传表单：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-18-23-54-30c336bab2dc4af3a4a53a949dfecc6e.png\" alt=\"上传文件form表单\" title=\"上传文件form表单\">\r\n<p>文件下载列表：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-18-24-16-2acd5e38bc4247e098805173d8fa46c5.png\" alt=\"文件下载列表\" title=\"文件下载列表\">\r\n<blockquote>\r\n<p>说明：文件上使用的是异步上传方式进行上传，没有使用同步提交form表单的方式进行</p>\r\n</blockquote>\r\n<p>文件上传异步操作代码如下：</p>\r\n<pre><code class=\"lang-javascript\">$(\"#upload\").on(\"click\",function () {\r\n           var fileObj = document.getElementById(\"file\").files[0];\r\n           var form = new FormData();\r\n           form.append(\"file\",fileObj);\r\n           var xhr = new XMLHttpRequest();\r\n           xhr.open(\"post\",\"http://localhost:8080/upload\",true);\r\n           xhr.onload = function(event){\r\n               alert(event.currentTarget.responseText);\r\n               window.location.href = window.location.href;\r\n           };\r\n           xhr.send(form);\r\n        });\r\n</code></pre>\r\n<h1 id=\"h1-4-\"><a name=\"4. 打包运行\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. 打包运行</h1><p>使用Maven命令对应用程序进行打包，下面是maven打包的命令：</p>\r\n<pre><code class=\"lang-tex\">mvn clean install\r\n</code></pre>\r\n<p>在控制台窗口中运行上述命令，等待maven打包。若控制台中显示<strong>“BUILD SUCCESS”</strong>信息，你可以在当前工程目录下的target文件夹中找到相应的JAR文件。</p>\r\n<p>现在，你可以使用下面的命令来运行JAR文件：</p>\r\n<pre><code class=\"lang-tex\">java -jar YOUR_JARFILE_NAME\r\n</code></pre>\r\n<p>JAR文件成功启动后，你可以在控制台窗口中看到如下的信息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-18-24-45-b299776efd5a4c6ba1ba4c3757703622.png\" alt=\"控制台窗口信息\" title=\"控制台窗口信息\">\r\n<h1 id=\"h1-5-\"><a name=\"5. 测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5. 测试</h1><p>打开浏览器并在地址栏输入：<a href=\"http://localhost:8080/files\">http://localhost:8080/files</a> 。下面是成功请求后的浏览器截图：<br><img src=\"https://cdn.ramostear.com/2019-03-08-18-25-13-891de6f45dcc48059eb10f4cdb06a8eb.png\" alt=\"文件列表\" title=\"文件列表\">\r\n<p>接下来，点击其中任意一个<strong>download</strong>按钮，测试文件下载功能是否正常：<br><img src=\"https://cdn.ramostear.com/2019-03-08-18-26-00-2ded0b19d7834fbea458cb93a579d470.png\" alt=\"下载文件\" title=\"下载文件\">\r\n<p>最后，我们测试一下文件上传功能是否正常。在进行测试之前，我们先看一下文件上传目录中存储的文件信息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-18-26-15-7cef263127954cc090d0ea07627b1c7e.png\" alt=\"文件上传目录\" title=\"文件上传目录\">\r\n<p>接下来，我们选择一份需要上传的文件，然后点击<strong>upload</strong>按钮上传文件：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-18-26-41-3ef0de1e16d14cda9b181511ae329c6f.png\" alt=\"上传文件\" title=\"上传文件\">\r\n<p>此时，文件以及上传成功，我们再次观察文件上传目录中的文件信息，以验证文件是否成功写入磁盘：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-08-18-26-55-7ae1ef770ad543a6b38c1ce8687e8638.png\" alt=\"文件上传目录对比\" title=\"文件上传目录对比\">\r\n<h1 id=\"h1-6-\"><a name=\"6. 附件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6. 附件</h1><p>本章节的全部源代码已经上传至Github代码仓库中，你可以访问下面的地址获得全部的源码：<a href=\"https://github.com/ramostear/Spring-Boot-File-Handling-Tutorial\" title=\"https://github.com/ramostear/Spring-Boot-File-Handling-Tutorial\">https://github.com/ramostear/Spring-Boot-File-Handling-Tutorial</a></p>\r\n','SpringBoot,文件上传,文件下载',0,'# Spring Boot（十一）— 文件上传和下载\r\n\r\n文件上传和下载是Web应用程序比较常用的功能之一，在本章节中，我将以一个简单的案例来讲解在Spring Boot中如何进行文件的上传与下载。在开始正文之前，我们通过一张思维导图来了解一下文件上传与下载的简单流程：\r\n\r\n![文件上传与下载思维导图](https://cdn.ramostear.com/2019-03-08-18-22-31-780bd13186fc406db8212e9e87e65d66.png \"文件上传与下载思维导图\")\r\n\r\n# 1. 文件上传\r\n\r\n对于文件上传，控制器中对应的上传方法的参数必须是**MultipartFile**对象，**MultipartFile**对象可以是一个数组对象，也可以是单个对象，如果是一个数组对象，则可以进行多文件上传；这里我们仅演示单个文件上传，下面的代码展示了文件上传方法的基本结构：\r\n\r\n```java\r\n@PostMapping(value = \"/upload\",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)\r\n@ResponseBody\r\npublic String fileUpload(@RequestParam(\"file\") MultipartFile file) throws IOException {\r\n	return null;\r\n}\r\n```\r\n\r\n接下来，我们使用**FileOutputStream**对象将客户端上传的文件写入到磁盘中，并返回**“File is upload successfully”**的提示信息，下面是文件上传完整的代码：\r\n\r\n```java\r\npackage com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.FileInfo;\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.stereotype.Controller;\r\nimport org.springframework.web.bind.annotation.*;\r\nimport org.springframework.web.multipart.MultipartFile;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.io.*;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/8 0008-15:35\r\n */\r\n@Controller\r\npublic class FileController {\r\n\r\n\r\n    @Value ( \"${file.upload.root.dir}\" )\r\n    String fileUploadRootDir;\r\n\r\n    private static Map<String,FileInfo> fileRepository = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initFileRepository(){\r\n        FileInfo file1 = new FileInfo ().setFileName ( \"bg1.jpg\" );\r\n        FileInfo file2 = new FileInfo ().setFileName ( \"bg2.jpg\" );\r\n        FileInfo file3 = new FileInfo ().setFileName ( \"bg3.jpg\" );\r\n        fileRepository.put ( file1.getName (),file1 );\r\n        fileRepository.put ( file2.getName (),file2 );\r\n        fileRepository.put ( file3.getName (),file3 );\r\n    }\r\n\r\n@PostMapping(value = \"/upload\",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)\r\n@ResponseBody\r\npublic String fileUpload(@RequestParam(\"file\") MultipartFile file) throws IOException {\r\n\r\n        File convertFile = new File ( fileUploadRootDir+file.getOriginalFilename ());\r\n        FileOutputStream fileOutputStream = new FileOutputStream ( convertFile );\r\n        fileOutputStream.write ( file.getBytes () );\r\n        fileOutputStream.close ();\r\n\r\n        FileInfo fileInfo = new FileInfo()\r\n                .setFileName ( file.getOriginalFilename());\r\n\r\n        fileRepository.put ( fileInfo.getName (),fileInfo);\r\n\r\n        return \"File is upload successfully\";\r\n    }\r\n}\r\n\r\n```\r\n\r\n**fileRepository**用于存放已上传文件的索引信息。\r\n\r\n\r\n\r\n# 2. 文件下载\r\n\r\n在Spring Boot应用程序中，我们可以使用**InputStreamResource**对象来下载文件，在下载文件的方法中，我们需要通过Response来设置HttpHeander对象的相关属性，如**Content-Disposition**、**Cache-Control**、**Pragma**和**Expires**等属性。除此之外，还需要指定Response的响应类型。下面的代码给出了文件下载的详细信息：\r\n\r\n```java\r\n@GetMapping(\"/download/{fileName}\")\r\n@ResponseBody\r\npublic ResponseEntity<Object> downloadFile(@PathVariable(name = \"fileName\") String fileName) throws FileNotFoundException {\r\n\r\n        File file = new File ( fileUploadRootDir+fileName);\r\n        InputStreamResource resource = new InputStreamResource ( new FileInputStream ( file ) );\r\n\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.add ( \"Content-Disposition\",String.format(\"attachment;filename=\\\"%s\",fileName));\r\n        headers.add ( \"Cache-Control\",\"no-cache,no-store,must-revalidate\" );\r\n        headers.add ( \"Pragma\",\"no-cache\" );\r\n        headers.add ( \"Expires\",\"0\" );\r\n\r\n        ResponseEntity<Object> responseEntity = ResponseEntity.ok()\r\n                .headers ( headers )\r\n                .contentLength ( file.length ())\r\n                .contentType(MediaType.parseMediaType ( \"application/txt\" ))\r\n                .body(resource);\r\n\r\n        return responseEntity;\r\n    }\r\n```\r\n\r\n\r\n\r\n# 3. 代码清单\r\n\r\n## 3.1 文件上传和下载控制器\r\n\r\n下面给出的是完整的文件上传和下载的代码：\r\n\r\n```java\r\npackage com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.FileInfo;\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.core.io.InputStreamResource;\r\nimport org.springframework.http.HttpHeaders;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.MediaType;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.stereotype.Controller;\r\nimport org.springframework.ui.Model;\r\nimport org.springframework.web.bind.annotation.*;\r\nimport org.springframework.web.multipart.MultipartFile;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.io.*;\r\nimport java.util.Collection;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/8 0008-15:35\r\n */\r\n@Controller\r\npublic class FileController {\r\n\r\n\r\n    @Value ( \"${file.upload.root.dir}\" )\r\n    String fileUploadRootDir;\r\n\r\n    private static Map<String,FileInfo> fileRepository = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initFileRepository(){\r\n        FileInfo file1 = new FileInfo ().setFileName ( \"bg1.jpg\" );\r\n        FileInfo file2 = new FileInfo ().setFileName ( \"bg2.jpg\" );\r\n        FileInfo file3 = new FileInfo ().setFileName ( \"bg3.jpg\" );\r\n        fileRepository.put ( file1.getName (),file1 );\r\n        fileRepository.put ( file2.getName (),file2 );\r\n        fileRepository.put ( file3.getName (),file3 );\r\n    }\r\n\r\n    @GetMapping(\"/files\")\r\n    public String files(Model model){\r\n        Collection<FileInfo> files = fileRepository.values ();\r\n        model.addAttribute ( \"data\",files );\r\n        return \"files\";\r\n    }\r\n\r\n\r\n    @PostMapping(value = \"/upload\",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)\r\n    @ResponseBody\r\n    public String fileUpload(@RequestParam(\"file\") MultipartFile file) throws IOException {\r\n\r\n        File convertFile = new File ( fileUploadRootDir+file.getOriginalFilename ());\r\n        FileOutputStream fileOutputStream = new FileOutputStream ( convertFile );\r\n        fileOutputStream.write ( file.getBytes () );\r\n        fileOutputStream.close ();\r\n\r\n        FileInfo fileInfo = new FileInfo()\r\n                .setFileName ( file.getOriginalFilename());\r\n\r\n        fileRepository.put ( fileInfo.getName (),fileInfo);\r\n\r\n        return \"File is upload successfully\";\r\n    }\r\n\r\n    @GetMapping(\"/download/{fileName}\")\r\n    @ResponseBody\r\n    public ResponseEntity<Object> downloadFile(@PathVariable(name = \"fileName\") String fileName) throws FileNotFoundException {\r\n\r\n        File file = new File ( fileUploadRootDir+fileName);\r\n        InputStreamResource resource = new InputStreamResource ( new FileInputStream ( file ) );\r\n\r\n        HttpHeaders headers = new HttpHeaders();\r\n        headers.add ( \"Content-Disposition\",String.format(\"attachment;filename=\\\"%s\",fileName));\r\n        headers.add ( \"Cache-Control\",\"no-cache,no-store,must-revalidate\" );\r\n        headers.add ( \"Pragma\",\"no-cache\" );\r\n        headers.add ( \"Expires\",\"0\" );\r\n\r\n        ResponseEntity<Object> responseEntity = ResponseEntity.ok()\r\n                .headers ( headers )\r\n                .contentLength ( file.length ())\r\n                .contentType(MediaType.parseMediaType ( \"application/txt\" ))\r\n                .body(resource);\r\n\r\n        return responseEntity;\r\n    }\r\n\r\n\r\n\r\n}\r\n\r\n```\r\n\r\n## 3.2 数据模型\r\n\r\n创建一个文件信息数据模型作为上传文件信息的载体，下面是FileInfo.java的代码：\r\n\r\n```java\r\npackage com.ramostear.application.model;\r\n\r\nimport lombok.Data;\r\n\r\nimport java.util.Date;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date  : 2019/3/8 0008-15:25\r\n */\r\n@Data\r\npublic class FileInfo {\r\n\r\n    private String name;\r\n    private Date uploadTime = new Date();\r\n\r\n    public FileInfo setFileName(String name){\r\n        this.setName ( name );\r\n        return this;\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n## 3.3 Maven build 文件\r\n\r\n下面是本次demo应用程序的pom.xml文件配置清单：\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n	xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n	<modelVersion>4.0.0</modelVersion>\r\n	<parent>\r\n		<groupId>org.springframework.boot</groupId>\r\n		<artifactId>spring-boot-starter-parent</artifactId>\r\n		<version>2.1.3.RELEASE</version>\r\n		<relativePath/> <!-- lookup parent from repository -->\r\n	</parent>\r\n	<groupId>com.ramostear</groupId>\r\n	<artifactId>file-handling</artifactId>\r\n	<version>0.0.1-SNAPSHOT</version>\r\n	<name>file-handling</name>\r\n	<description>Demo project for Spring Boot</description>\r\n\r\n	<properties>\r\n		<java.version>1.8</java.version>\r\n	</properties>\r\n\r\n	<dependencies>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-thymeleaf</artifactId>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-freemarker</artifactId>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-web</artifactId>\r\n		</dependency>\r\n\r\n		<dependency>\r\n			<groupId>org.projectlombok</groupId>\r\n			<artifactId>lombok</artifactId>\r\n			<optional>true</optional>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-test</artifactId>\r\n			<scope>test</scope>\r\n		</dependency>\r\n	</dependencies>\r\n\r\n	<build>\r\n		<plugins>\r\n			<plugin>\r\n				<groupId>org.springframework.boot</groupId>\r\n				<artifactId>spring-boot-maven-plugin</artifactId>\r\n			</plugin>\r\n		</plugins>\r\n	</build>\r\n\r\n</project>\r\n\r\n```\r\n\r\n> 注：本次案例使用freemarker模板引擎作为视图模板\r\n\r\n\r\n\r\n## 3.4 配置文件\r\n\r\napplication.properties文件主要设置了**freemarker**的相关属性以及自定义的**file.upload.root.dir **属性:\r\n\r\n```properties\r\nspring.freemarker.cache=false\r\nspring.freemarker.prefix=\r\nspring.freemarker.suffix=.html\r\nspring.freemarker.enabled=true\r\nspring.freemarker.charset=UTF-8\r\nspring.freemarker.template-loader-path=classpath:/templates/\r\nfile.upload.root.dir = C:/work/upload/\r\n```\r\n\r\n**file.upload.root.dir**自定义属性设置了文件上传的更目录为：**C:/work/upload/**\r\n\r\n\r\n\r\n\r\n\r\n## 3.5 视图\r\n\r\n在视图文件中，创建了一个form表单用于上传文件，另外还创建了一个已上传文件列表，提供文件下载操作。\r\n\r\n文件上传表单：\r\n\r\n![上传文件form表单](https://cdn.ramostear.com/2019-03-08-18-23-54-30c336bab2dc4af3a4a53a949dfecc6e.png \"上传文件form表单\")\r\n\r\n文件下载列表：\r\n\r\n![文件下载列表](https://cdn.ramostear.com/2019-03-08-18-24-16-2acd5e38bc4247e098805173d8fa46c5.png \"文件下载列表\")\r\n\r\n> 说明：文件上使用的是异步上传方式进行上传，没有使用同步提交form表单的方式进行\r\n\r\n文件上传异步操作代码如下：\r\n\r\n```javascript\r\n$(\"#upload\").on(\"click\",function () {\r\n           var fileObj = document.getElementById(\"file\").files[0];\r\n           var form = new FormData();\r\n           form.append(\"file\",fileObj);\r\n           var xhr = new XMLHttpRequest();\r\n           xhr.open(\"post\",\"http://localhost:8080/upload\",true);\r\n           xhr.onload = function(event){\r\n               alert(event.currentTarget.responseText);\r\n               window.location.href = window.location.href;\r\n           };\r\n           xhr.send(form);\r\n        });\r\n```\r\n\r\n\r\n\r\n# 4. 打包运行\r\n\r\n使用Maven命令对应用程序进行打包，下面是maven打包的命令：\r\n\r\n```tex\r\nmvn clean install\r\n```\r\n\r\n在控制台窗口中运行上述命令，等待maven打包。若控制台中显示**“BUILD SUCCESS”**信息，你可以在当前工程目录下的target文件夹中找到相应的JAR文件。\r\n\r\n现在，你可以使用下面的命令来运行JAR文件：\r\n\r\n```tex\r\njava -jar YOUR_JARFILE_NAME\r\n```\r\n\r\nJAR文件成功启动后，你可以在控制台窗口中看到如下的信息：\r\n\r\n![控制台窗口信息](https://cdn.ramostear.com/2019-03-08-18-24-45-b299776efd5a4c6ba1ba4c3757703622.png \"控制台窗口信息\")\r\n\r\n\r\n\r\n# 5. 测试\r\n\r\n打开浏览器并在地址栏输入：http://localhost:8080/files 。下面是成功请求后的浏览器截图：\r\n![文件列表](https://cdn.ramostear.com/2019-03-08-18-25-13-891de6f45dcc48059eb10f4cdb06a8eb.png \"文件列表\")\r\n\r\n\r\n接下来，点击其中任意一个**download**按钮，测试文件下载功能是否正常：\r\n![下载文件](https://cdn.ramostear.com/2019-03-08-18-26-00-2ded0b19d7834fbea458cb93a579d470.png \"下载文件\")\r\n\r\n\r\n最后，我们测试一下文件上传功能是否正常。在进行测试之前，我们先看一下文件上传目录中存储的文件信息：\r\n\r\n![文件上传目录](https://cdn.ramostear.com/2019-03-08-18-26-15-7cef263127954cc090d0ea07627b1c7e.png \"文件上传目录\")\r\n\r\n接下来，我们选择一份需要上传的文件，然后点击**upload**按钮上传文件：\r\n\r\n![上传文件](https://cdn.ramostear.com/2019-03-08-18-26-41-3ef0de1e16d14cda9b181511ae329c6f.png \"上传文件\")\r\n\r\n此时，文件以及上传成功，我们再次观察文件上传目录中的文件信息，以验证文件是否成功写入磁盘：\r\n\r\n![文件上传目录对比](https://cdn.ramostear.com/2019-03-08-18-26-55-7ae1ef770ad543a6b38c1ce8687e8638.png \"文件上传目录对比\")\r\n\r\n\r\n\r\n# 6. 附件\r\n\r\n本章节的全部源代码已经上传至Github代码仓库中，你可以访问下面的地址获得全部的源码：[https://github.com/ramostear/Spring-Boot-File-Handling-Tutorial](https://github.com/ramostear/Spring-Boot-File-Handling-Tutorial \"https://github.com/ramostear/Spring-Boot-File-Handling-Tutorial\")',1,'','2019/01/21/90w51537.html',0,'文件上传和下载是Web应用程序比较常用的功能之一，在本章节中，我将以一个简单的案例来讲解在Spring Boot中如何进行文件的上传与下载','blog.html','https://cdn.ramostear.com/2019-03-28-01-53-40-78761d42e32d44eeacf723c47760b936.png;base64','在Spring Boot程序中上传和下载文件','',0),(4,'2019-11-21 22:47:36',0,'2019-11-21 22:47:36',1,1,1,'谭朝红','Spring Boot中使用拦截器','<h1 id=\"h1-spring-boot-\"><a name=\"Spring Boot （八）— 拦截器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot （八）— 拦截器</h1><p>在Spring Boot应用程序中，可以使用拦截器对Request请求到达控制器前和Response响应发送到客户端前进行拦截，并做相应的操作，如添加请求头信息或者进行鉴权等操作。在本章节中，我将演示如何在Spring Boot应用程序中使用拦截器。</p>\r\n<p>在开始正文之前，我们先通过一张图来了解Spring Boot应用程序中拦截器的使用步骤：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-07-04-05-42-d7711ea4e2644732803e06a9aaa9e749.png\" alt=\"\">\r\n<p>首先，我们需要定义一个拦截器类，并使用<a href=\"https://github.com/Component\" title=\"@Component\" class=\"at-link\">@Component</a>注解进行标注，此外，该拦截器类还需要实现HandlerInterceptor接口的三个方法：</p>\r\n<ul>\r\n<li><strong>preHandle()</strong> : 用于在请求发送到控制器前执行操作</li><li><strong>postHandle</strong> : 用于在将响应发送到客户端之前进行操作</li><li><strong>afterHandle</strong> : 用于在完成请求和响应后执行操作</li></ul>\r\n<h1 id=\"h1-1-userserviceinterceptor-java\"><a name=\"1. 创建拦截器 — UserServiceInterceptor.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. 创建拦截器 — UserServiceInterceptor.java</h1><pre><code class=\"lang-java\">package com.ramostear.application.interceptor;\r\n\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.lang.Nullable;\r\nimport org.springframework.stereotype.Component;\r\nimport org.springframework.web.servlet.HandlerInterceptor;\r\nimport org.springframework.web.servlet.ModelAndView;\r\n\r\nimport javax.servlet.http.HttpServletRequest;\r\nimport javax.servlet.http.HttpServletResponse;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-23:55\r\n * @modify by :\r\n * @since:\r\n */\r\n@Component\r\npublic class UserServiceInterceptor implements HandlerInterceptor {\r\n\r\n    private static final Logger logger = LoggerFactory.getLogger(UserServiceInterceptor.class);\r\n\r\n    @Override\r\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\r\n        logger.info(\"Pre handle method is calling.\");\r\n        logger.info(\"Request URL is : \"+ request.getRequestURL());\r\n        return true;\r\n    }\r\n\r\n    @Override\r\n    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {\r\n        logger.info(\"Post handle method is calling.\");\r\n        logger.info(\"Response Status is : \"+response.getStatus());\r\n    }\r\n\r\n    @Override\r\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {\r\n        logger.info(\"Request and Response is completed.\");\r\n    }\r\n}\r\n</code></pre>\r\n<p>在上述的拦截器中，我们在请求发送到控制器之前，记录了当前请求的URL地址，在控制器执行完操作响应客户端之前，记录了当前响应的状态码；最后，在完成请求和响应后，记录请求和响应已经完成的消息。</p>\r\n<h1 id=\"h1-2-\"><a name=\"2. 注册拦截器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 注册拦截器</h1><p>接下来，我们需要将定义好的拦截器注册到拦截器注册表中。在Spring Boot 2.0中，我们可以通过扩展WebMvcConfigurationSupport类来注册我们的拦截器：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.config;\r\n\r\nimport com.ramostear.application.interceptor.UserServiceInterceptor;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.stereotype.Component;\r\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\r\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/7 0007-2:42\r\n * @modify by :\r\n * @since:\r\n */\r\n@Component\r\npublic class UserServiceInterceptorConfig extends WebMvcConfigurationSupport{\r\n\r\n    @Autowired\r\n    UserServiceInterceptor userServiceInterceptor;\r\n\r\n    @Override\r\n    protected void addInterceptors(InterceptorRegistry registry) {\r\n        registry.addInterceptor(userServiceInterceptor);\r\n    }\r\n}\r\n</code></pre>\r\n<blockquote>\r\n<p>说明：在Spring Boot 2.0及以后的版本中，<strong>WebMvcConfigurerAdapter</strong>类已经被废弃掉了，所以我们可以选择直接实现<strong>WebMvcConfigurer</strong> 接口中的方法来注册自定义拦截器，也可以选择扩展<strong>WebMvcConfigurationSupport</strong>类来注册自定义的拦截器。这里采用扩展<strong>WebMvcConfigurationSupport</strong>类的方式注册自定义的拦截器。</p>\r\n</blockquote>\r\n<p>以下是Spring 官方的文档信息：</p>\r\n<blockquote>\r\n<pre><code class=\"lang-java\">/**\r\n* @author Rossen Stoyanchev\r\n* @since 3.1\r\n* @deprecated as of 5.0 {@link WebMvcConfigurer} has default methods (made\r\n* possible by a Java 8 baseline) and can be implemented directly without the\r\n* need for this adapter\r\n*/\r\n@Deprecated\r\npublic abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {\r\n    ...\r\n}\r\n</code></pre>\r\n</blockquote>\r\n<h1 id=\"h1-3-userservicecontroller-java\"><a name=\"3. 控制器 — UserServiceController.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. 控制器 — UserServiceController.java</h1><p>我们将定义一个控制器来处理客户端的请求，客户端通过HTTP GET方法请求用户资源信息：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/7 0007-2:51\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepo = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initUserRepo(){\r\n        User admin = new User().setId(1).setName(\"admin\");\r\n        User editor = new User().setId(2).setName(\"editor\");\r\n        userRepo.put(admin.getId(),admin);\r\n        userRepo.put(editor.getId(),editor);\r\n    }\r\n\r\n    @GetMapping(\"/users\")\r\n    public ResponseEntity&lt;Object&gt; getUsers(){\r\n        return new ResponseEntity&lt;&gt;(userRepo.values(), HttpStatus.OK);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-4-pojo-user-java\"><a name=\"4. POJO类 — User.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. POJO类 — User.java</h1><p>用户信息模型的POJO类代码如下所示：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application.model;\r\n\r\nimport lombok.Data;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-23:52\r\n * @modify by :\r\n * @since:\r\n */\r\n@Data\r\npublic class User {\r\n\r\n    private long id;\r\n\r\n    private String name;\r\n\r\n    public User setId(long id){\r\n        this.id = id;\r\n        return this;\r\n    }\r\n\r\n    public User setName(String name){\r\n        this.name = name;\r\n        return this;\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-5-interceptorapplication-java\"><a name=\"5.应用主类 — InterceptorApplication.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5.应用主类 — InterceptorApplication.java</h1><p>应用程序的主类代码如下所示：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class InterceptorApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(InterceptorApplication.class, args);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-6-maven-pom-xml\"><a name=\"6. Maven构建文件 — pom.xml\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6. Maven构建文件 — pom.xml</h1><p>maven 构建文件代码清单如下所示：</p>\r\n<pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n    xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"&gt;\r\n    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;\r\n    &lt;parent&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\r\n        &lt;version&gt;2.1.3.RELEASE&lt;/version&gt;\r\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\r\n    &lt;/parent&gt;\r\n    &lt;groupId&gt;com.ramostear&lt;/groupId&gt;\r\n    &lt;artifactId&gt;interceptor&lt;/artifactId&gt;\r\n    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;\r\n    &lt;name&gt;interceptor&lt;/name&gt;\r\n    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;\r\n\r\n    &lt;properties&gt;\r\n        &lt;java.version&gt;1.8&lt;/java.version&gt;\r\n    &lt;/properties&gt;\r\n\r\n    &lt;dependencies&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\r\n            &lt;scope&gt;test&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;\r\n            &lt;artifactId&gt;lombok&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n    &lt;/dependencies&gt;\r\n\r\n    &lt;build&gt;\r\n        &lt;plugins&gt;\r\n            &lt;plugin&gt;\r\n                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;\r\n            &lt;/plugin&gt;\r\n        &lt;/plugins&gt;\r\n    &lt;/build&gt;\r\n\r\n&lt;/project&gt;\r\n</code></pre>\r\n<h1 id=\"h1-7-\"><a name=\"7. 运行程序并测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7. 运行程序并测试</h1><p>你可以使用maven的<strong>“mvn clean install”</strong>命令来进行打包，然后使用<strong>java -jar <jarfile></jarfile>命令来运行项目，也可以使用集成开发环境IDEA或者Eclipse来打包运行项目。</p>\r\n<p>现在，我们的应用程序在Tomcat服务器上使用8080端口启动成功，控制台输出信息如下所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-07-04-06-14-e59b31612f77471893ab7d9b068e6645.png\" alt=\"\">\r\n<p>接下来，启动Postman应用程序，在地址栏输入：<a href=\"http://localhost:8080/users\">http://localhost:8080/users</a> ,并以GET的方式发送请求，如下图所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-07-04-06-33-f1add52e2e19463fbcc5daaed945195d.png\" alt=\"\">\r\n<p>测试显示，控制器成功返回了用户信息，最后，让我们观察控制台窗口输出的信息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-07-04-06-49-25e12a0c969b4094874582c10ad40ea9.png\" alt=\"\">\r\n<p>通过控制台显示的信息可知，我们的拦截器已经成功的对客户端的请求以及服务端的响应进行了拦截。</p>\r\n','spring boot,拦截器',0,'# Spring Boot （八）— 拦截器\r\n\r\n在Spring Boot应用程序中，可以使用拦截器对Request请求到达控制器前和Response响应发送到客户端前进行拦截，并做相应的操作，如添加请求头信息或者进行鉴权等操作。在本章节中，我将演示如何在Spring Boot应用程序中使用拦截器。\r\n\r\n在开始正文之前，我们先通过一张图来了解Spring Boot应用程序中拦截器的使用步骤：\r\n\r\n![](https://cdn.ramostear.com/2019-03-07-04-05-42-d7711ea4e2644732803e06a9aaa9e749.png)\r\n\r\n首先，我们需要定义一个拦截器类，并使用@Component注解进行标注，此外，该拦截器类还需要实现HandlerInterceptor接口的三个方法：\r\n\r\n- **preHandle()** : 用于在请求发送到控制器前执行操作\r\n- **postHandle** : 用于在将响应发送到客户端之前进行操作\r\n- **afterHandle** : 用于在完成请求和响应后执行操作\r\n\r\n\r\n\r\n# 1. 创建拦截器 — UserServiceInterceptor.java\r\n\r\n```java\r\npackage com.ramostear.application.interceptor;\r\n\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.lang.Nullable;\r\nimport org.springframework.stereotype.Component;\r\nimport org.springframework.web.servlet.HandlerInterceptor;\r\nimport org.springframework.web.servlet.ModelAndView;\r\n\r\nimport javax.servlet.http.HttpServletRequest;\r\nimport javax.servlet.http.HttpServletResponse;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-23:55\r\n * @modify by :\r\n * @since:\r\n */\r\n@Component\r\npublic class UserServiceInterceptor implements HandlerInterceptor {\r\n\r\n    private static final Logger logger = LoggerFactory.getLogger(UserServiceInterceptor.class);\r\n\r\n    @Override\r\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\r\n        logger.info(\"Pre handle method is calling.\");\r\n        logger.info(\"Request URL is : \"+ request.getRequestURL());\r\n        return true;\r\n    }\r\n\r\n    @Override\r\n    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {\r\n        logger.info(\"Post handle method is calling.\");\r\n        logger.info(\"Response Status is : \"+response.getStatus());\r\n    }\r\n\r\n    @Override\r\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {\r\n        logger.info(\"Request and Response is completed.\");\r\n    }\r\n}\r\n\r\n```\r\n\r\n在上述的拦截器中，我们在请求发送到控制器之前，记录了当前请求的URL地址，在控制器执行完操作响应客户端之前，记录了当前响应的状态码；最后，在完成请求和响应后，记录请求和响应已经完成的消息。\r\n\r\n\r\n\r\n# 2. 注册拦截器\r\n\r\n接下来，我们需要将定义好的拦截器注册到拦截器注册表中。在Spring Boot 2.0中，我们可以通过扩展WebMvcConfigurationSupport类来注册我们的拦截器：\r\n\r\n```java\r\npackage com.ramostear.application.config;\r\n\r\nimport com.ramostear.application.interceptor.UserServiceInterceptor;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.stereotype.Component;\r\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\r\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/7 0007-2:42\r\n * @modify by :\r\n * @since:\r\n */\r\n@Component\r\npublic class UserServiceInterceptorConfig extends WebMvcConfigurationSupport{\r\n\r\n    @Autowired\r\n    UserServiceInterceptor userServiceInterceptor;\r\n\r\n    @Override\r\n    protected void addInterceptors(InterceptorRegistry registry) {\r\n        registry.addInterceptor(userServiceInterceptor);\r\n    }\r\n}\r\n\r\n```\r\n\r\n> 说明：在Spring Boot 2.0及以后的版本中，**WebMvcConfigurerAdapter**类已经被废弃掉了，所以我们可以选择直接实现**WebMvcConfigurer** 接口中的方法来注册自定义拦截器，也可以选择扩展**WebMvcConfigurationSupport**类来注册自定义的拦截器。这里采用扩展**WebMvcConfigurationSupport**类的方式注册自定义的拦截器。\r\n\r\n以下是Spring 官方的文档信息：\r\n\r\n> ```java\r\n> /**\r\n> * @author Rossen Stoyanchev\r\n> * @since 3.1\r\n> * @deprecated as of 5.0 {@link WebMvcConfigurer} has default methods (made\r\n> * possible by a Java 8 baseline) and can be implemented directly without the\r\n> * need for this adapter\r\n> */\r\n> @Deprecated\r\n> public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {\r\n>     ...\r\n> }\r\n> ```\r\n\r\n\r\n\r\n# 3. 控制器 — UserServiceController.java\r\n\r\n我们将定义一个控制器来处理客户端的请求，客户端通过HTTP GET方法请求用户资源信息：\r\n\r\n```java\r\npackage com.ramostear.application.controller;\r\n\r\nimport com.ramostear.application.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/7 0007-2:51\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepo = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initUserRepo(){\r\n        User admin = new User().setId(1).setName(\"admin\");\r\n        User editor = new User().setId(2).setName(\"editor\");\r\n        userRepo.put(admin.getId(),admin);\r\n        userRepo.put(editor.getId(),editor);\r\n    }\r\n\r\n    @GetMapping(\"/users\")\r\n    public ResponseEntity<Object> getUsers(){\r\n        return new ResponseEntity<>(userRepo.values(), HttpStatus.OK);\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 4. POJO类 — User.java\r\n\r\n用户信息模型的POJO类代码如下所示：\r\n\r\n```java\r\npackage com.ramostear.application.model;\r\n\r\nimport lombok.Data;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-23:52\r\n * @modify by :\r\n * @since:\r\n */\r\n@Data\r\npublic class User {\r\n\r\n    private long id;\r\n\r\n    private String name;\r\n\r\n    public User setId(long id){\r\n        this.id = id;\r\n        return this;\r\n    }\r\n\r\n    public User setName(String name){\r\n        this.name = name;\r\n        return this;\r\n    }\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 5.应用主类 — InterceptorApplication.java\r\n\r\n应用程序的主类代码如下所示：\r\n\r\n```java\r\npackage com.ramostear.application;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class InterceptorApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(InterceptorApplication.class, args);\r\n	}\r\n\r\n}\r\n\r\n```\r\n\r\n# 6. Maven构建文件 — pom.xml\r\n\r\nmaven 构建文件代码清单如下所示：\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n	xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n	<modelVersion>4.0.0</modelVersion>\r\n	<parent>\r\n		<groupId>org.springframework.boot</groupId>\r\n		<artifactId>spring-boot-starter-parent</artifactId>\r\n		<version>2.1.3.RELEASE</version>\r\n		<relativePath/> <!-- lookup parent from repository -->\r\n	</parent>\r\n	<groupId>com.ramostear</groupId>\r\n	<artifactId>interceptor</artifactId>\r\n	<version>0.0.1-SNAPSHOT</version>\r\n	<name>interceptor</name>\r\n	<description>Demo project for Spring Boot</description>\r\n\r\n	<properties>\r\n		<java.version>1.8</java.version>\r\n	</properties>\r\n\r\n	<dependencies>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-web</artifactId>\r\n		</dependency>\r\n\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-test</artifactId>\r\n			<scope>test</scope>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.projectlombok</groupId>\r\n			<artifactId>lombok</artifactId>\r\n		</dependency>\r\n	</dependencies>\r\n\r\n	<build>\r\n		<plugins>\r\n			<plugin>\r\n				<groupId>org.springframework.boot</groupId>\r\n				<artifactId>spring-boot-maven-plugin</artifactId>\r\n			</plugin>\r\n		</plugins>\r\n	</build>\r\n\r\n</project>\r\n\r\n```\r\n\r\n\r\n\r\n# 7. 运行程序并测试\r\n\r\n你可以使用maven的**“mvn clean install”**命令来进行打包，然后使用**java -jar <jarfile>**命令来运行项目，也可以使用集成开发环境IDEA或者Eclipse来打包运行项目。\r\n\r\n现在，我们的应用程序在Tomcat服务器上使用8080端口启动成功，控制台输出信息如下所示：\r\n\r\n![](https://cdn.ramostear.com/2019-03-07-04-06-14-e59b31612f77471893ab7d9b068e6645.png)\r\n\r\n接下来，启动Postman应用程序，在地址栏输入：http://localhost:8080/users ,并以GET的方式发送请求，如下图所示：\r\n\r\n![](https://cdn.ramostear.com/2019-03-07-04-06-33-f1add52e2e19463fbcc5daaed945195d.png)\r\n\r\n测试显示，控制器成功返回了用户信息，最后，让我们观察控制台窗口输出的信息：\r\n\r\n![](https://cdn.ramostear.com/2019-03-07-04-06-49-25e12a0c969b4094874582c10ad40ea9.png)\r\n\r\n通过控制台显示的信息可知，我们的拦截器已经成功的对客户端的请求以及服务端的响应进行了拦截。',1,'','2019/43/21/3ob2y4p4.html',0,'在Spring Boot应用程序中，可以使用拦截器对Request请求到达控制器前和Response响应发送到客户端前进行拦截，并做相应的操作，如添加请求头信息或者进行鉴权等操作','blog.html','https://cdn.ramostear.com/2019-03-28-01-48-33-2defea7ae03b4173964f73fb07cad33e.png;base64','Spring Boot中使用拦截器','',0),(8,'2019-11-21 23:05:35',0,'2019-11-21 23:05:35',1,1,1,'谭朝红','Spring Boot 应用程序默认使用Apache Commons Logging来记录所有内部的日志。除此之外，Spring Boot还支持使用Java Util Longging、Log4j2和Logback来记录系统日志','<h1 id=\"h1-spring-boot-\"><a name=\"Spring Boot (五)—日志记录\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot (五)—日志记录</h1><p>Spring Boot 应用程序默认使用Apache Commons Logging来记录所有内部的日志。除此之外，Spring Boot还支持使用Java Util Longging、Log4j2和Logback来记录系统日志。我们可以通过配置相关参数，让其在控制台输出日志或者将日志写入到日志文件中。</p>\r\n<p>通常情况下，如果使用Spring Boot Starter的方式引入依赖，系统将会使用Logback来记录日志，以此同时，Logback还可以和Common Logging、Util Loging、Log4j以及SLF4进行整合。</p>\r\n<h1 id=\"h1-1-\"><a name=\"1. 日志的格式\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. 日志的格式</h1><p>在Spring Boot 应用程序中，默认的日志格式如下所示：</p>\r\n<pre><code class=\"lang-tex\">2019-03-05 02:02:00.013  INFO 7700 ---[main] .r.s.b.p.SpringBootPropertiesApplication : The following profiles are active: dev\r\n2019-03-05 02:02:01.697  INFO 7700 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8081 (http)\r\n2019-03-05 02:02:01.732  INFO 7700 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\r\n</code></pre>\r\n<p>通常情况下，日志文件为我们提供了如下的几个信息：</p>\r\n<ul>\r\n<li>日期和时间，该日志记录的日期和时间</li><li>日志级别，日志级别包括INFO、ERROR、DEBUG和WARN等</li><li>进程编号，日志信息通常会包含当前的进程ID</li><li>—- ，信息分隔符</li><li>线程名称，线程名称将使用方括号“[]”包含起来</li><li>记录器名称，显示源文件类的名称</li><li>日志消息</li></ul>\r\n<h1 id=\"h1-2-\"><a name=\"2. 控制台日志\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 控制台日志</h1><p>Spring Boot应用程序在默认的情况下，“INFO”、“ERROR”和“WARN”级别的日志信息将打印到控制台窗口中，如果需要启用“DEBUG”级别的日志记录，需要在启动应用程序的时候添加日志记录级别标识命令：</p>\r\n<pre><code class=\"lang-te\">java -jar jarfile.jar --debug\r\n</code></pre>\r\n<p>除此之外，也可以在applicatioin.properties或者application.yml文件中进行配置，以application.properties文件为例：</p>\r\n<pre><code class=\"lang-tex\">debug = true\r\n</code></pre>\r\n<h1 id=\"h1-3-\"><a name=\"3. 文件日志\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. 文件日志</h1><p>默认情况下，所有的日志信息都将在控制台打印输出，而不会写入到日志文件中。如果需要将日志信息记录到日志文件中，则需要在application.properties或者application.yml配置文件中设置属性logging.file 或者logging.path的值。</p>\r\n<p>我们可以显示的指定日志文件的存储路径，以application.properties为例,指定日志文件存储路径：</p>\r\n<pre><code class=\"lang-tex\">logging.path = D:\\work\\logging\\\r\n</code></pre>\r\n<p>此时，日志文件默认的文件名称是<strong>spring.log</strong></p>\r\n<p>除了上面的配置之外，我们还可以同时指定日志文件存储路径和文件名称：</p>\r\n<pre><code class=\"lang-tex\">logging.file = D:\\work\\logging\\mine-logging.log\r\n</code></pre>\r\n<blockquote>\r\n<p>注：一个日志文件最多能够存储10,000KB的日志信息。</p>\r\n</blockquote>\r\n<h1 id=\"h1-4-\"><a name=\"4. 日志级别\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. 日志级别</h1><p>Spring Boot应用程序支持所有的日志记录级别，如“TRACE”、“DEBUG”、“INFO”、”WARN”、”ERROR”、</p>\r\n<p>“FATAL”以及”OFF”。你可以在配置文件中指定一个root logger:</p>\r\n<pre><code class=\"lang-tex\">logging.level.root = WARN\r\n</code></pre>\r\n<blockquote>\r\n<p>提示：Logback不支持“FATAL”级别的日志，“FATAL”级别的日志将被映射到“ERROR”级别的日志中。</p>\r\n</blockquote>\r\n<h1 id=\"h1-5-logback\"><a name=\"5. 配置Logback\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5. 配置Logback</h1><p>Logback支持一XML文件格式来配置Spring Boot应用程序日志。一般地、日志配置文件也应该放置在类路径下，下面给出Logback.xml文件的一个配置示例：</p>\r\n<pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;configuration&gt;\r\n &lt;appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\"&gt;&lt;/appender&gt;\r\n &lt;root level = \"INFO\"&gt;\r\n    &lt;appender-ref ref = \"STDOUT\"/&gt;\r\n &lt;/root&gt;\r\n&lt;/configuration&gt;\r\n</code></pre>\r\n<p>接下来，我们将在Spring Boot Application主类中使用slf4j logger来记录日志。首先是Logback.xml文件配置：</p>\r\n<pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;configuration&gt;\r\n    &lt;appender name=\"STDOUT\" class=\"cn.qos.logback.core.ConsoleAppender\"&gt;\r\n        &lt;encoder&gt;\r\n            &lt;pattern&gt;[%d{yyyy-MM-dd\'T\'HH:mm:ss.sss\'z\'}][%C][%t][%L][%-5p] %m%n&lt;/pattern&gt;\r\n        &lt;/encoder&gt;\r\n    &lt;/appender&gt;\r\n    &lt;appender name=\"FILE\" class=\"cn.qos.logback.core.FileAppender\"&gt;\r\n        &lt;File&gt;D:\\work\\logging\\mine-log.log&lt;/File&gt;\r\n        &lt;encoder&gt;\r\n            &lt;pattern&gt;[%d{yyyy-MM-dd\'T\'HH:mm:ss.sss\'z\'}][%C][%t][%L][%-5p] %m%n&lt;/pattern&gt;\r\n        &lt;/encoder&gt;\r\n    &lt;/appender&gt;\r\n    &lt;root level=\"INFO\"&gt;\r\n        &lt;appender-ref ref=\"STDOUT\"/&gt;\r\n        &lt;appender-ref ref=\"FILE\"/&gt;\r\n    &lt;/root&gt;\r\n&lt;/configuration&gt;\r\n</code></pre>\r\n<p>主类源代码：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.spring.boot.logger;\r\n\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class SpringBootLoggerApplication {\r\n\r\n    private static final Logger logger = LoggerFactory.getLogger(SpringBootLoggerApplication.class);\r\n    public static void main(String[] args) {\r\n        logger.info(\"This is a info level logger message.\");\r\n        logger.warn(\"This is a warn level logger message.\");\r\n        logger.error(\"This is a error level logger message.\");\r\n        SpringApplication.run(SpringBootLoggerApplication.class, args);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>现在，我们启动应用，观察控制台和日志文件中的内容。控制台窗口输出内容：</p>\r\n<pre><code class=\"lang-tex\">\"C:\\Program Files\\Java\\jdk1.8.0_144\\bin\\java\"...\r\n[2019-03-05T19:25:59.059Z] [com.ramostear.spring.boot.logger.SpringBootLoggerApplication] [main] [13] [INFO ] This is a info level logger message.\r\n[2019-03-05T19:25:59.059Z] [com.ramostear.spring.boot.logger.SpringBootLoggerApplication] [main] [14] [WARN ] This is a warn level logger message.\r\n[2019-03-05T19:25:59.059Z] [com.ramostear.spring.boot.logger.SpringBootLoggerApplication] [main] [15] [ERROR] This is a error level logger message.\r\n</code></pre>\r\n<p>接下来，我们再看看日志文件中记录的内容：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-05-19-34-14-a00b5d24ff4b4565b20fa5e8a9ce0885.png\" alt=\"\">\r\n','SpringBoot,Logging',0,'# Spring Boot (五)—日志记录\r\n\r\nSpring Boot 应用程序默认使用Apache Commons Logging来记录所有内部的日志。除此之外，Spring Boot还支持使用Java Util Longging、Log4j2和Logback来记录系统日志。我们可以通过配置相关参数，让其在控制台输出日志或者将日志写入到日志文件中。\r\n\r\n通常情况下，如果使用Spring Boot Starter的方式引入依赖，系统将会使用Logback来记录日志，以此同时，Logback还可以和Common Logging、Util Loging、Log4j以及SLF4进行整合。\r\n\r\n# 1. 日志的格式\r\n\r\n在Spring Boot 应用程序中，默认的日志格式如下所示：\r\n\r\n```tex\r\n2019-03-05 02:02:00.013  INFO 7700 ---[main] .r.s.b.p.SpringBootPropertiesApplication : The following profiles are active: dev\r\n2019-03-05 02:02:01.697  INFO 7700 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8081 (http)\r\n2019-03-05 02:02:01.732  INFO 7700 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\r\n```\r\n\r\n通常情况下，日志文件为我们提供了如下的几个信息：\r\n\r\n- 日期和时间，该日志记录的日期和时间\r\n- 日志级别，日志级别包括INFO、ERROR、DEBUG和WARN等\r\n- 进程编号，日志信息通常会包含当前的进程ID\r\n- --- ，信息分隔符\r\n- 线程名称，线程名称将使用方括号“[]”包含起来\r\n- 记录器名称，显示源文件类的名称\r\n- 日志消息\r\n\r\n\r\n\r\n# 2. 控制台日志\r\n\r\nSpring Boot应用程序在默认的情况下，“INFO”、“ERROR”和“WARN”级别的日志信息将打印到控制台窗口中，如果需要启用“DEBUG”级别的日志记录，需要在启动应用程序的时候添加日志记录级别标识命令：\r\n\r\n```te\r\njava -jar jarfile.jar --debug\r\n```\r\n\r\n除此之外，也可以在applicatioin.properties或者application.yml文件中进行配置，以application.properties文件为例：\r\n\r\n```tex\r\ndebug = true\r\n```\r\n\r\n\r\n\r\n# 3. 文件日志\r\n\r\n默认情况下，所有的日志信息都将在控制台打印输出，而不会写入到日志文件中。如果需要将日志信息记录到日志文件中，则需要在application.properties或者application.yml配置文件中设置属性logging.file 或者logging.path的值。\r\n\r\n我们可以显示的指定日志文件的存储路径，以application.properties为例,指定日志文件存储路径：\r\n\r\n```tex\r\nlogging.path = D:\\work\\logging\\\r\n```\r\n\r\n此时，日志文件默认的文件名称是**spring.log**\r\n\r\n除了上面的配置之外，我们还可以同时指定日志文件存储路径和文件名称：\r\n\r\n```tex\r\nlogging.file = D:\\work\\logging\\mine-logging.log\r\n```\r\n\r\n> 注：一个日志文件最多能够存储10,000KB的日志信息。\r\n\r\n\r\n\r\n# 4. 日志级别\r\n\r\nSpring Boot应用程序支持所有的日志记录级别，如“TRACE”、“DEBUG”、“INFO”、\"WARN\"、\"ERROR\"、\r\n\r\n\"FATAL\"以及\"OFF\"。你可以在配置文件中指定一个root logger:\r\n\r\n```tex\r\nlogging.level.root = WARN\r\n```\r\n\r\n> 提示：Logback不支持“FATAL”级别的日志，“FATAL”级别的日志将被映射到“ERROR”级别的日志中。\r\n\r\n\r\n\r\n# 5. 配置Logback\r\n\r\nLogback支持一XML文件格式来配置Spring Boot应用程序日志。一般地、日志配置文件也应该放置在类路径下，下面给出Logback.xml文件的一个配置示例：\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<configuration>\r\n <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\"></appender>\r\n <root level = \"INFO\">\r\n	<appender-ref ref = \"STDOUT\"/>\r\n </root>\r\n</configuration>\r\n```\r\n\r\n接下来，我们将在Spring Boot Application主类中使用slf4j logger来记录日志。首先是Logback.xml文件配置：\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<configuration>\r\n    <appender name=\"STDOUT\" class=\"cn.qos.logback.core.ConsoleAppender\">\r\n        <encoder>\r\n            <pattern>[%d{yyyy-MM-dd\'T\'HH:mm:ss.sss\'z\'}][%C][%t][%L][%-5p] %m%n</pattern>\r\n        </encoder>\r\n    </appender>\r\n    <appender name=\"FILE\" class=\"cn.qos.logback.core.FileAppender\">\r\n        <File>D:\\work\\logging\\mine-log.log</File>\r\n        <encoder>\r\n            <pattern>[%d{yyyy-MM-dd\'T\'HH:mm:ss.sss\'z\'}][%C][%t][%L][%-5p] %m%n</pattern>\r\n        </encoder>\r\n    </appender>\r\n    <root level=\"INFO\">\r\n        <appender-ref ref=\"STDOUT\"/>\r\n        <appender-ref ref=\"FILE\"/>\r\n    </root>\r\n</configuration>\r\n\r\n```\r\n\r\n主类源代码：\r\n\r\n```java\r\npackage com.ramostear.spring.boot.logger;\r\n\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class SpringBootLoggerApplication {\r\n\r\n	private static final Logger logger = LoggerFactory.getLogger(SpringBootLoggerApplication.class);\r\n	public static void main(String[] args) {\r\n		logger.info(\"This is a info level logger message.\");\r\n		logger.warn(\"This is a warn level logger message.\");\r\n		logger.error(\"This is a error level logger message.\");\r\n		SpringApplication.run(SpringBootLoggerApplication.class, args);\r\n	}\r\n\r\n}\r\n\r\n```\r\n\r\n现在，我们启动应用，观察控制台和日志文件中的内容。控制台窗口输出内容：\r\n\r\n```tex\r\n\"C:\\Program Files\\Java\\jdk1.8.0_144\\bin\\java\"...\r\n[2019-03-05T19:25:59.059Z] [com.ramostear.spring.boot.logger.SpringBootLoggerApplication] [main] [13] [INFO ] This is a info level logger message.\r\n[2019-03-05T19:25:59.059Z] [com.ramostear.spring.boot.logger.SpringBootLoggerApplication] [main] [14] [WARN ] This is a warn level logger message.\r\n[2019-03-05T19:25:59.059Z] [com.ramostear.spring.boot.logger.SpringBootLoggerApplication] [main] [15] [ERROR] This is a error level logger message.\r\n```\r\n\r\n接下来，我们再看看日志文件中记录的内容：\r\n\r\n![](https://cdn.ramostear.com/2019-03-05-19-34-14-a00b5d24ff4b4565b20fa5e8a9ce0885.png)',1,'','2019/03/21/2bvkdw98.html',0,'Spring Boot 应用程序默认使用Apache Commons Logging来记录所有内部的日志。除此之外，Spring Boot还支持使用Java Util Longging、Log4j2和Logback来记录系统日志','blog.html','https://cdn.ramostear.com/2019-03-28-01-39-59-0f72bda1b1614cf3ad3cc5b6464384b8.png;base64','配置Spring Boot的日志记录信息','',0),(9,'2019-11-21 23:07:47',0,'2019-11-21 23:07:47',1,1,1,'谭朝红','Spring Boot能够轻松的构建起企业级的RESTful Web服务程序。在本章节中，我将通过一个简单的例子演示Spring Boot的这一能力','<h1 id=\"h1-spring-boot-restful-web-\"><a name=\"Spring Boot（六） — 构建RESTful Web服务\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot（六） — 构建RESTful Web服务</h1><p>Spring Boot能够轻松的构建起企业级的RESTful Web服务程序。在本章节中，我将通过一个简单的例子演示Spring Boot的这一能力。<br>为了构建RESTful Web服务，我们需要将Spring Boot Starter Web依赖添加到pom.xml配置文件中。\r\n<pre><code class=\"lang-xml\">&lt;dependency&gt;\r\n    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n&lt;/dependency&gt;\r\n</code></pre>\r\n<p>下面是完整的Maven pom.xml配置文件代码：</p>\r\n<pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n    xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"&gt;\r\n    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;\r\n    &lt;parent&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\r\n        &lt;version&gt;2.1.3.RELEASE&lt;/version&gt;\r\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\r\n    &lt;/parent&gt;\r\n    &lt;groupId&gt;com.ramostear&lt;/groupId&gt;\r\n    &lt;artifactId&gt;restful-webservice&lt;/artifactId&gt;\r\n    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;\r\n    &lt;name&gt;restful-webservice&lt;/name&gt;\r\n    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;\r\n\r\n    &lt;properties&gt;\r\n        &lt;java.version&gt;1.8&lt;/java.version&gt;\r\n    &lt;/properties&gt;\r\n\r\n    &lt;dependencies&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\r\n            &lt;scope&gt;test&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;\r\n            &lt;artifactId&gt;lombok&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n    &lt;/dependencies&gt;\r\n\r\n    &lt;build&gt;\r\n        &lt;plugins&gt;\r\n            &lt;plugin&gt;\r\n                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;\r\n            &lt;/plugin&gt;\r\n        &lt;/plugins&gt;\r\n    &lt;/build&gt;\r\n\r\n&lt;/project&gt;\r\n</code></pre>\r\n<blockquote>\r\n<p>说明：Lombok是一个java库，可以自动插入编辑器并构建工具，使用lombok注解可以自动生成getter、setter以及构造函数等方法，你可以到<a href=\"https://projectlombok.org\">Lombok官网</a>了解更多详细信息。</p>\r\n</blockquote>\r\n<h1 id=\"h1-1-restcontroller\"><a name=\"1. RestController\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. RestController</h1><p><a href=\"https://github.com/RestController\" title=\"@RestController\" class=\"at-link\">@RestController</a>注解用于定义RESTful Web服务，它提供JSON、XML以及自定义的相应信息，其使用语法如下：</p>\r\n<pre><code class=\"lang-java\">@RestController\r\npublic class UserController{\r\n    //TODO ...\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-2-requestmapping\"><a name=\"2. RequestMapping\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. RequestMapping</h1><p><a href=\"https://github.com/RequestMapping\" title=\"@RequestMapping\" class=\"at-link\">@RequestMapping</a>注解用于定义访问REST Endpoint的请求路径。<a href=\"https://github.com/RequestMapping\" title=\"@RequestMapping\" class=\"at-link\">@RequestMapping</a>的使用语法如下：</p>\r\n<pre><code class=\"lang-java\">@RequestMapping(\"/users\")\r\npublic ResponseEntity&lt;Object&gt; getUsers(){\r\n    //TODO ...\r\n}\r\n</code></pre>\r\n<blockquote>\r\n<p>注：<a href=\"https://github.com/RequestMapping\" title=\"@RequestMapping\" class=\"at-link\">@RequestMapping</a>注解默认的请求方法GET.</p>\r\n</blockquote>\r\n<p>在接下来的内容当中，我们将使用<a href=\"https://github.com/RequestMapping\" title=\"@RequestMapping\" class=\"at-link\">@RequestMapping</a>的组合注解来定义REST Endpoint方法的映射路径：</p>\r\n<ul>\r\n<li><a href=\"https://github.com/GetMapping\" title=\"@GetMapping\" class=\"at-link\">@GetMapping</a>，<a href=\"https://github.com/GetMapping\" title=\"@GetMapping\" class=\"at-link\">@GetMapping</a>(“/users”)等同于<a href=\"https://github.com/RequestMapping\" title=\"@RequestMapping\" class=\"at-link\">@RequestMapping</a>(“/users”,method=RequestMethod.GET)</li><li><a href=\"https://github.com/PostMapping\" title=\"@PostMapping\" class=\"at-link\">@PostMapping</a>,<a href=\"https://github.com/PostMapping\" title=\"@PostMapping\" class=\"at-link\">@PostMapping</a>(“/users”)等同于<a href=\"https://github.com/RequestMapping\" title=\"@RequestMapping\" class=\"at-link\">@RequestMapping</a>(“/users”,method=RequestMethod.POST)</li><li><a href=\"https://github.com/PutMapping\" title=\"@PutMapping\" class=\"at-link\">@PutMapping</a>,<a href=\"https://github.com/PutMapping\" title=\"@PutMapping\" class=\"at-link\">@PutMapping</a>(“/users/{id}”)等同于<a href=\"https://github.com/RequestMapping\" title=\"@RequestMapping\" class=\"at-link\">@RequestMapping</a>(“/users/{id}”,method=RequestMethod.PUT)</li><li><a href=\"https://github.com/DeleteMapping\" title=\"@DeleteMapping\" class=\"at-link\">@DeleteMapping</a>,<a href=\"https://github.com/DeleteMapping\" title=\"@DeleteMapping\" class=\"at-link\">@DeleteMapping</a>(“/users/{id}”)等同于<a href=\"https://github.com/RequestMapping\" title=\"@RequestMapping\" class=\"at-link\">@RequestMapping</a>(“/users/{id}”,method=RequestMethod.DELETE)</li></ul>\r\n<h1 id=\"h1-3-requestbody\"><a name=\"3. RequestBody\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. RequestBody</h1><p><a href=\"https://github.com/RequestBody\" title=\"@RequestBody\" class=\"at-link\">@RequestBody</a>注解用于定义请求正文内容类型。其语法如下：</p>\r\n<pre><code class=\"lang-java\">public ResponseEntity&lt;Object&gt; createUser(@RequestBody User user){\r\n    //TODO ...\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-4-pathvariable\"><a name=\"4. PathVariable\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. PathVariable</h1><p><a href=\"https://github.com/PathVariable\" title=\"@PathVariable\" class=\"at-link\">@PathVariable</a>注解用于定义请求路径中的动态变量，请求路径中的动态变量使用花括号“{}”包裹起来。语法如下：</p>\r\n<pre><code class=\"lang-java\">@PutMapping(\"/users/{id}\")\r\npublic ResponseEntity&lt;Obejct&gt; updateUser(@PathVariable(\"id\") long id,@RequestBody User user){\r\n    //TODO ...\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-5-get-api\"><a name=\"5. GET API\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5. GET API</h1><p>默认的HTTP请求方法是GET,GET方法不需要任何的Request Body。你可以发送任何的请求参数和路径变量来定义一个动态的URL。接下来，我将演示如何定义一个HTTP GET请求方法，用于获取所有的用户信息。</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepository = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n   @GetMapping(\"/users\")\r\n    public ResponseEntity&lt;Object&gt; getUser(){\r\n        return new ResponseEntity&lt;&gt;(userRepository.values(), HttpStatus.OK);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>在这里，我使用了一个Hash Map来扮演用户存储库的角色，定义的请求路径是<strong>/users</strong></p>\r\n<h1 id=\"h1-6-post-api\"><a name=\"6. POST API\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6. POST API</h1><p>HTTP POST请求用于创建新的资源，此方法包含了Request Body。下面我将演示如何定义HTTP POST方法来创建用户信息，并存储到用户存储库中。</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepository = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n\r\n    @PostMapping(\"/users\")\r\n    public ResponseEntity&lt;Object&gt; createUser(@RequestBody User user){\r\n        userRepository.put(user.getId(),user);\r\n        return new ResponseEntity&lt;&gt;(\"User is created successfully\",HttpStatus.CREATED);\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-7-put-api\"><a name=\"7. PUT API\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7. PUT API</h1><p>HTTP PUT请求用于更新存储库中现有的资源，此方法包含请求正文。接下来我将给出相关示例来演示如何定义一个HTTP PUT请求方法来更新用户信息。</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepository = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n    @PutMapping(\"/users/{id}\")\r\n    public ResponseEntity&lt;Object&gt; updateUser(@PathVariable(name = \"id\") long id,@RequestBody User user){\r\n        userRepository.remove(id);\r\n        user.setId(id);\r\n        userRepository.put(id,user);\r\n        return new ResponseEntity&lt;&gt;(\"User is updated successfully\",HttpStatus.OK);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>在此方法中，定义的请求路径是<strong>/users/{id}</strong> ,其中<strong>{id}</strong>是动态的路径变量，用来定义需要更新的用户ID。</p>\r\n<h1 id=\"h1-8-delete-api\"><a name=\"8. DELETE API\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>8. DELETE API</h1><p>HTTP DELETE请求用于删除存储库中现有的资源，此方法不包含任何请求正文。接下来我将给出相关示例来演示如何定义一个HTTP PUT请求方法来更新用户信息。</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepository = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n\r\n    @DeleteMapping(\"/users/{id}\")\r\n    public ResponseEntity&lt;Object&gt; delete(@PathVariable(name = \"id\") long id){\r\n        userRepository.remove(id);\r\n        return new ResponseEntity&lt;&gt;(\"User is deleted successfully\",HttpStatus.OK);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>在此方法中，定义的请求路径是<strong>/users/{id}</strong> ,其中<strong>{id}</strong>是动态的路径变量，用来定义需要删除的用户ID。</p>\r\n<h1 id=\"h1-9-\"><a name=\"9. 代码清单\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>9. 代码清单</h1><h2 id=\"h2-9-1-spring-boot-restfulwebserviceapplication-java\"><a name=\"9.1 Spring Boot应用程序主类 — RestfulWebserviceApplication.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>9.1 Spring Boot应用程序主类 — RestfulWebserviceApplication.java</h2><pre><code class=\"lang-java\">package com.ramostear.restful.webservice;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class RestfulWebserviceApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(RestfulWebserviceApplication.class, args);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<h2 id=\"h2-9-2-pojo-user-java\"><a name=\"9.2 POJO类 — User.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>9.2 POJO类 — User.java</h2><pre><code class=\"lang-java\">package com.ramostear.restful.webservice.model;\r\n\r\nimport lombok.Getter;\r\nimport lombok.NoArgsConstructor;\r\nimport lombok.Setter;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:12\r\n * @modify by :\r\n * @since:\r\n */\r\n@Getter\r\n@Setter\r\n@NoArgsConstructor\r\npublic class User {\r\n    private long id;\r\n    private String name;\r\n\r\n    public User setId(long id){\r\n        this.id = id;\r\n        return this;\r\n    }\r\n\r\n    public User setName(String name){\r\n        this.name = name;\r\n        return this;\r\n    }\r\n}\r\n</code></pre>\r\n<h2 id=\"h2-9-3-rest-controller-userservicecontroller-java\"><a name=\"9.3 Rest Controller类 — UserServiceController.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>9.3 Rest Controller类 — UserServiceController.java</h2><pre><code class=\"lang-java\">package com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepository = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n    @GetMapping(\"/users\")\r\n    public ResponseEntity&lt;Object&gt; getUser(){\r\n        return new ResponseEntity&lt;&gt;(userRepository.values(), HttpStatus.OK);\r\n    }\r\n\r\n    @PostMapping(\"/users\")\r\n    public ResponseEntity&lt;Object&gt; createUser(@RequestBody User user){\r\n        userRepository.put(user.getId(),user);\r\n        return new ResponseEntity&lt;&gt;(\"User is created successfully\",HttpStatus.CREATED);\r\n    }\r\n\r\n    @PutMapping(\"/users/{id}\")\r\n    public ResponseEntity&lt;Object&gt; updateUser(@PathVariable(name = \"id\") long id,@RequestBody User user){\r\n        userRepository.remove(id);\r\n        user.setId(id);\r\n        userRepository.put(id,user);\r\n        return new ResponseEntity&lt;&gt;(\"User is updated successfully\",HttpStatus.OK);\r\n    }\r\n\r\n    @DeleteMapping(\"/users/{id}\")\r\n    public ResponseEntity&lt;Object&gt; delete(@PathVariable(name = \"id\") long id){\r\n        userRepository.remove(id);\r\n        return new ResponseEntity&lt;&gt;(\"User is deleted successfully\",HttpStatus.OK);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-10-\"><a name=\"10. 测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>10. 测试</h1><p>你可以选择命令行工具或者IDE来编译、打包并运行项目，这里我使用Intellij IDEA来运行项目，运行结果如下图所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-06-05-21-57-75cdc4aeeb30455f98358e0753a2e873.png\" alt=\"\">\r\n<p>接下来，我们将使用<strong>Postman</strong>应用程序来测试准备好的RESTful Web 服务接口：</p>\r\n<ol>\r\n<li>GET API : <a href=\"http://localhost:8080/users\">http://localhost:8080/users</a></li></ol>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-06-05-21-15-f03740bec24b481c932475f1c5a16859.png\" alt=\"\">\r\n<ol>\r\n<li>POST API : <a href=\"http://locahost:8080/users\">http://locahost:8080/users</a></li></ol>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-06-05-22-45-da2e12f1310f435fb4598f1df0fe505c.png\" alt=\"\">\r\n<ol>\r\n<li>PUT API : <a href=\"http://locahost:8080/users/3\">http://locahost:8080/users/3</a></li></ol>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-06-05-23-27-ecef35ad941b46fdba13c64ae9cfeeae.png\" alt=\"\">\r\n<ol>\r\n<li>DELETE API : <a href=\"http://localhost:8080/users/3\">http://localhost:8080/users/3</a></li></ol>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-06-05-24-04-2f527421220b4afcb57fe7da49880cbb.png\" alt=\"\">\r\n','SpringBoot,Restful',0,'# Spring Boot（六） — 构建RESTful Web服务\r\n\r\nSpring Boot能够轻松的构建起企业级的RESTful Web服务程序。在本章节中，我将通过一个简单的例子演示Spring Boot的这一能力。\r\n为了构建RESTful Web服务，我们需要将Spring Boot Starter Web依赖添加到pom.xml配置文件中。\r\n\r\n```xml\r\n<dependency>\r\n    <groupId>org.springframework.boot</groupId>\r\n    <artifactId>spring-boot-starter-web</artifactId>\r\n</dependency>\r\n```\r\n\r\n下面是完整的Maven pom.xml配置文件代码：\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n	xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n	<modelVersion>4.0.0</modelVersion>\r\n	<parent>\r\n		<groupId>org.springframework.boot</groupId>\r\n		<artifactId>spring-boot-starter-parent</artifactId>\r\n		<version>2.1.3.RELEASE</version>\r\n		<relativePath/> <!-- lookup parent from repository -->\r\n	</parent>\r\n	<groupId>com.ramostear</groupId>\r\n	<artifactId>restful-webservice</artifactId>\r\n	<version>0.0.1-SNAPSHOT</version>\r\n	<name>restful-webservice</name>\r\n	<description>Demo project for Spring Boot</description>\r\n\r\n	<properties>\r\n		<java.version>1.8</java.version>\r\n	</properties>\r\n\r\n	<dependencies>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-web</artifactId>\r\n		</dependency>\r\n\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-test</artifactId>\r\n			<scope>test</scope>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.projectlombok</groupId>\r\n			<artifactId>lombok</artifactId>\r\n		</dependency>\r\n	</dependencies>\r\n\r\n	<build>\r\n		<plugins>\r\n			<plugin>\r\n				<groupId>org.springframework.boot</groupId>\r\n				<artifactId>spring-boot-maven-plugin</artifactId>\r\n			</plugin>\r\n		</plugins>\r\n	</build>\r\n\r\n</project>\r\n\r\n```\r\n\r\n> 说明：Lombok是一个java库，可以自动插入编辑器并构建工具，使用lombok注解可以自动生成getter、setter以及构造函数等方法，你可以到[Lombok官网](https://projectlombok.org)了解更多详细信息。\r\n\r\n# 1. RestController\r\n\r\n@RestController注解用于定义RESTful Web服务，它提供JSON、XML以及自定义的相应信息，其使用语法如下：\r\n\r\n```java\r\n@RestController\r\npublic class UserController{\r\n    //TODO ...\r\n}\r\n```\r\n\r\n\r\n\r\n# 2. RequestMapping\r\n\r\n@RequestMapping注解用于定义访问REST Endpoint的请求路径。@RequestMapping的使用语法如下：\r\n\r\n```java\r\n@RequestMapping(\"/users\")\r\npublic ResponseEntity<Object> getUsers(){\r\n    //TODO ...\r\n}\r\n```\r\n\r\n> 注：@RequestMapping注解默认的请求方法GET.\r\n\r\n在接下来的内容当中，我们将使用@RequestMapping的组合注解来定义REST Endpoint方法的映射路径：\r\n\r\n- @GetMapping，@GetMapping(\"/users\")等同于@RequestMapping(\"/users\",method=RequestMethod.GET)\r\n- @PostMapping,@PostMapping(\"/users\")等同于@RequestMapping(\"/users\",method=RequestMethod.POST)\r\n- @PutMapping,@PutMapping(\"/users/{id}\")等同于@RequestMapping(\"/users/{id}\",method=RequestMethod.PUT)\r\n- @DeleteMapping,@DeleteMapping(\"/users/{id}\")等同于@RequestMapping(\"/users/{id}\",method=RequestMethod.DELETE)\r\n\r\n\r\n\r\n# 3. RequestBody\r\n\r\n@RequestBody注解用于定义请求正文内容类型。其语法如下：\r\n\r\n```java\r\npublic ResponseEntity<Object> createUser(@RequestBody User user){\r\n    //TODO ...\r\n}\r\n```\r\n\r\n\r\n\r\n# 4. PathVariable\r\n\r\n@PathVariable注解用于定义请求路径中的动态变量，请求路径中的动态变量使用花括号“{}”包裹起来。语法如下：\r\n\r\n```java\r\n@PutMapping(\"/users/{id}\")\r\npublic ResponseEntity<Obejct> updateUser(@PathVariable(\"id\") long id,@RequestBody User user){\r\n    //TODO ...\r\n}\r\n```\r\n\r\n\r\n\r\n# 5. GET API\r\n\r\n默认的HTTP请求方法是GET,GET方法不需要任何的Request Body。你可以发送任何的请求参数和路径变量来定义一个动态的URL。接下来，我将演示如何定义一个HTTP GET请求方法，用于获取所有的用户信息。\r\n\r\n```java\r\npackage com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepository = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n   @GetMapping(\"/users\")\r\n    public ResponseEntity<Object> getUser(){\r\n        return new ResponseEntity<>(userRepository.values(), HttpStatus.OK);\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\n在这里，我使用了一个Hash Map来扮演用户存储库的角色，定义的请求路径是**/users**\r\n\r\n\r\n\r\n# 6. POST API\r\n\r\nHTTP POST请求用于创建新的资源，此方法包含了Request Body。下面我将演示如何定义HTTP POST方法来创建用户信息，并存储到用户存储库中。\r\n\r\n```java\r\npackage com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepository = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n\r\n    @PostMapping(\"/users\")\r\n    public ResponseEntity<Object> createUser(@RequestBody User user){\r\n        userRepository.put(user.getId(),user);\r\n        return new ResponseEntity<>(\"User is created successfully\",HttpStatus.CREATED);\r\n    }\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 7. PUT API\r\n\r\nHTTP PUT请求用于更新存储库中现有的资源，此方法包含请求正文。接下来我将给出相关示例来演示如何定义一个HTTP PUT请求方法来更新用户信息。\r\n\r\n```java\r\npackage com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepository = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n    @PutMapping(\"/users/{id}\")\r\n    public ResponseEntity<Object> updateUser(@PathVariable(name = \"id\") long id,@RequestBody User user){\r\n        userRepository.remove(id);\r\n        user.setId(id);\r\n        userRepository.put(id,user);\r\n        return new ResponseEntity<>(\"User is updated successfully\",HttpStatus.OK);\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\n在此方法中，定义的请求路径是**/users/{id}** ,其中**{id}**是动态的路径变量，用来定义需要更新的用户ID。\r\n\r\n\r\n\r\n# 8. DELETE API\r\n\r\nHTTP DELETE请求用于删除存储库中现有的资源，此方法不包含任何请求正文。接下来我将给出相关示例来演示如何定义一个HTTP PUT请求方法来更新用户信息。\r\n\r\n```java\r\npackage com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepository = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n\r\n    @DeleteMapping(\"/users/{id}\")\r\n    public ResponseEntity<Object> delete(@PathVariable(name = \"id\") long id){\r\n        userRepository.remove(id);\r\n        return new ResponseEntity<>(\"User is deleted successfully\",HttpStatus.OK);\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\n在此方法中，定义的请求路径是**/users/{id}** ,其中**{id}**是动态的路径变量，用来定义需要删除的用户ID。\r\n\r\n\r\n\r\n# 9. 代码清单\r\n\r\n## 9.1 Spring Boot应用程序主类 — RestfulWebserviceApplication.java\r\n\r\n```java\r\npackage com.ramostear.restful.webservice;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class RestfulWebserviceApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(RestfulWebserviceApplication.class, args);\r\n	}\r\n\r\n}\r\n\r\n```\r\n\r\n## 9.2 POJO类 — User.java\r\n\r\n```java\r\npackage com.ramostear.restful.webservice.model;\r\n\r\nimport lombok.Getter;\r\nimport lombok.NoArgsConstructor;\r\nimport lombok.Setter;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:12\r\n * @modify by :\r\n * @since:\r\n */\r\n@Getter\r\n@Setter\r\n@NoArgsConstructor\r\npublic class User {\r\n    private long id;\r\n    private String name;\r\n\r\n    public User setId(long id){\r\n        this.id = id;\r\n        return this;\r\n    }\r\n\r\n    public User setName(String name){\r\n        this.name = name;\r\n        return this;\r\n    }\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n## 9.3 Rest Controller类 — UserServiceController.java\r\n\r\n```java\r\npackage com.ramostear.restful.webservice.controller;\r\n\r\nimport com.ramostear.restful.webservice.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.*;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author ramostear\r\n * @create-time 2019/3/6 0006-3:14\r\n * @modify by :\r\n * @since:\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepository = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initUserRepository(){\r\n        User admin = new User();\r\n        admin.setId(1).setName(\"admin\");\r\n        userRepository.put(admin.getId(),admin);\r\n\r\n        User editor = new User();\r\n        editor.setId(2).setName(\"editor\");\r\n        userRepository.put(editor.getId(),editor);\r\n    }\r\n\r\n    @GetMapping(\"/users\")\r\n    public ResponseEntity<Object> getUser(){\r\n        return new ResponseEntity<>(userRepository.values(), HttpStatus.OK);\r\n    }\r\n\r\n    @PostMapping(\"/users\")\r\n    public ResponseEntity<Object> createUser(@RequestBody User user){\r\n        userRepository.put(user.getId(),user);\r\n        return new ResponseEntity<>(\"User is created successfully\",HttpStatus.CREATED);\r\n    }\r\n\r\n    @PutMapping(\"/users/{id}\")\r\n    public ResponseEntity<Object> updateUser(@PathVariable(name = \"id\") long id,@RequestBody User user){\r\n        userRepository.remove(id);\r\n        user.setId(id);\r\n        userRepository.put(id,user);\r\n        return new ResponseEntity<>(\"User is updated successfully\",HttpStatus.OK);\r\n    }\r\n\r\n    @DeleteMapping(\"/users/{id}\")\r\n    public ResponseEntity<Object> delete(@PathVariable(name = \"id\") long id){\r\n        userRepository.remove(id);\r\n        return new ResponseEntity<>(\"User is deleted successfully\",HttpStatus.OK);\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 10. 测试\r\n\r\n你可以选择命令行工具或者IDE来编译、打包并运行项目，这里我使用Intellij IDEA来运行项目，运行结果如下图所示：\r\n\r\n![](https://cdn.ramostear.com/2019-03-06-05-21-57-75cdc4aeeb30455f98358e0753a2e873.png)\r\n\r\n接下来，我们将使用**Postman**应用程序来测试准备好的RESTful Web 服务接口：\r\n\r\n1. GET API : http://localhost:8080/users\r\n\r\n![](https://cdn.ramostear.com/2019-03-06-05-21-15-f03740bec24b481c932475f1c5a16859.png)\r\n\r\n2. POST API : http://locahost:8080/users\r\n\r\n![](https://cdn.ramostear.com/2019-03-06-05-22-45-da2e12f1310f435fb4598f1df0fe505c.png)\r\n\r\n3. PUT API : http://locahost:8080/users/3\r\n\r\n![](https://cdn.ramostear.com/2019-03-06-05-23-27-ecef35ad941b46fdba13c64ae9cfeeae.png)\r\n\r\n4. DELETE API : http://localhost:8080/users/3\r\n\r\n![](https://cdn.ramostear.com/2019-03-06-05-24-04-2f527421220b4afcb57fe7da49880cbb.png)\r\n\r\n',1,'','2019/06/21/kd08x05d.html',0,'Spring Boot能够轻松的构建起企业级的RESTful Web服务程序。在本章节中，我将通过一个简单的例子演示Spring Boot的这一能力','blog.html','https://cdn.ramostear.com/2019-03-28-01-44-50-b4e99f87d6cc40a4b6dc72a0ce5eb08e.png;base64','使用Spring Boot构建RESTful 服务','',0),(10,'2019-11-21 23:09:54',0,'2019-11-21 23:09:54',1,1,1,'谭朝红','异常处理是一种识别并响应错误的一致性机制，异常机制可以把程序中的异常处理代码和正常的业务逻辑代码分离，包装程序的可读性和健壮性','<h1 id=\"h1-spring-boot-\"><a name=\"Spring Boot （七）— 异常处理\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot （七）— 异常处理</h1><p>异常处理是一种识别并响应错误的一致性机制，异常机制可以把程序中的异常处理代码和正常的业务逻辑代码分离，包装程序的可读性和健壮性。在Spring Boot应用程序中，能够捕获并及时的响应客户端的错误操作是一件非常重要的事情。在本章节中,我将展示如何处理Spring Boot中的异常。</p>\r\n<h1 id=\"h1-1-\"><a name=\"1. 相关注解说明\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. 相关注解说明</h1><p>在进行演示之前，我们先了解一下在Spring Boot应用程序中与异常处理相关的几个注解</p>\r\n<table>\r\n<thead>\r\n<tr>\r\n<th>注解名称</th>\r\n<th>说明</th>\r\n</tr>\r\n</thead>\r\n<tbody>\r\n<tr>\r\n<td><a href=\"https://github.com/ControllerAdvice\" title=\"@ControllerAdvice\" class=\"at-link\">@ControllerAdvice</a></td>\r\n<td>该标签用于处理全局的异常信息</td>\r\n</tr>\r\n<tr>\r\n<td><a href=\"https://github.com/ExceptionHadler\" title=\"@ExceptionHadler\" class=\"at-link\">@ExceptionHadler</a></td>\r\n<td>用于处理特定异常信息，并返回相关的响应到客户端</td>\r\n</tr>\r\n</tbody>\r\n</table>\r\n<p>首先，我们需要使用<strong><a href=\"https://github.com/ControllerAdvice\" title=\"@ControllerAdvice\" class=\"at-link\">@ControllerAdvice</a></strong>注解来定义一个全局的异常信息处理类，其语法如下：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.exception.handler;\r\n\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.ControllerAdvice;\r\nimport org.springframework.web.bind.annotation.ExceptionHandler;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:33\r\n */\r\n@ControllerAdvice\r\npublic class UserExceptionHandler {\r\n    //TODO ...\r\n}\r\n</code></pre>\r\n<p>接下来，我们需要定义一个扩展了<strong>RuntimeException</strong>类的自定义异常处理类：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.exception.handler;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:31\r\n */\r\npublic class UserNotFoundException extends RuntimeException{\r\n    private static final long serialVersionUID = 5534028121297403043L;\r\n}\r\n</code></pre>\r\n<p>最后，我们使用<strong><a href=\"https://github.com/ExceptionHandler\" title=\"@ExceptionHandler\" class=\"at-link\">@ExceptionHandler</a></strong>注解来定义一个处理具体异常信息的方法，其语法如下：</p>\r\n<pre><code class=\"lang-java\">@ExceptionHandler(value = UserNotFoundException.class)\r\n    public ResponseEntity&lt;Object&gt; exception(UserNotFoundException ex){\r\n        return new ResponseEntity&lt;&gt;(\"user not found.\", HttpStatus.NOT_FOUND);\r\n    }\r\n</code></pre>\r\n<p>以上工作准备完成之后，我们可以使用如下的方式来处理API中的异常信息：</p>\r\n<pre><code class=\"lang-java\">@GetMapping(\"/users/{id}\")\r\n    public ResponseEntity&lt;Object&gt; getUser(@PathVariable(name = \"id\") long id){\r\n        if(!userRepo.containsKey ( id )){\r\n            throw new UserNotFoundException ();\r\n        }\r\n        return new ResponseEntity&lt;&gt; (userRepo.get (id), HttpStatus.OK);\r\n    }\r\n</code></pre>\r\n<p>在接下来的内容当中，我将给出完整的示例代码，使用HTTP GET方法请求一个用户信息，当用户存储库中没有相应的用户信息时，返回“user not found”提示信息。</p>\r\n<h1 id=\"h1-2-usernotfoundexception-java\"><a name=\"2. 自定义异常信息类 — UserNotFoundException.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 自定义异常信息类 — UserNotFoundException.java</h1><pre><code class=\"lang-java\">package com.ramostear.exception.handler;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:31\r\n */\r\npublic class UserNotFoundException extends RuntimeException{\r\n    private static final long serialVersionUID = 5534028121297403043L;\r\n}\r\n</code></pre>\r\n<blockquote>\r\n<p>说明：这里只是做了一个简单的扩展</p>\r\n</blockquote>\r\n<h1 id=\"h1-2-userexceptionhandler-java\"><a name=\"2. 全局异常处理类 —UserExceptionHandler.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 全局异常处理类 —UserExceptionHandler.java</h1><pre><code class=\"lang-java\">package com.ramostear.exception.handler;\r\n\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.ControllerAdvice;\r\nimport org.springframework.web.bind.annotation.ExceptionHandler;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:33\r\n */\r\n@ControllerAdvice\r\npublic class UserExceptionHandler {\r\n\r\n\r\n    @ExceptionHandler(value = UserNotFoundException.class)\r\n    public ResponseEntity&lt;Object&gt; exception(UserNotFoundException ex){\r\n        return new ResponseEntity&lt;&gt;(\"user not found.\", HttpStatus.NOT_FOUND);\r\n    }\r\n\r\n\r\n}\r\n</code></pre>\r\n<p>在UserExceptionHandler.java文件中，我们定义了一个处理用户不存在异常的方法，</p>\r\n<h1 id=\"h1-3-api-userservicecontroller-java\"><a name=\"3. API类 — UserServiceController.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. API类 — UserServiceController.java</h1><pre><code class=\"lang-java\">package com.ramostear.exception.handler.controller;\r\n\r\nimport com.ramostear.exception.handler.UserNotFoundException;\r\nimport com.ramostear.exception.handler.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.PathVariable;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:26\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map&lt;Long,User&gt; userRepo = new HashMap&lt;&gt;();\r\n\r\n    @PostConstruct\r\n    public void initUserRepo(){\r\n        User admin = new User ().setId ( 1 ).setName ( \"admin\" );\r\n        userRepo.put ( admin.getId (),admin );\r\n\r\n        User editor = new User ().setId ( 2 ).setName ( \"editor\" );\r\n        userRepo.put ( editor.getId (),editor );\r\n    }\r\n\r\n\r\n    @GetMapping(\"/users/{id}\")\r\n    public ResponseEntity&lt;Object&gt; getUser(@PathVariable(name = \"id\") long id){\r\n        if(!userRepo.containsKey ( id )){\r\n            throw new UserNotFoundException ();\r\n        }\r\n        return new ResponseEntity&lt;&gt; (userRepo.get (id), HttpStatus.OK);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>在getUser()方法中，如果用户没有找到，则抛出UserNotFoundException异常。</p>\r\n<h1 id=\"h1-4-exceptionhandlerapplication-java\"><a name=\"4. 应用主类 —ExceptionHandlerApplication.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. 应用主类 —ExceptionHandlerApplication.java</h1><pre><code class=\"lang-java\">package com.ramostear.exception.handler;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class ExceptionHandlerApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(ExceptionHandlerApplication.class, args);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-5-pojo-user-java\"><a name=\"5. 用户POJO类 — User.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5. 用户POJO类 — User.java</h1><pre><code class=\"lang-java\">package com.ramostear.exception.handler.model;\r\n\r\nimport lombok.Getter;\r\nimport lombok.NoArgsConstructor;\r\nimport lombok.Setter;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:23\r\n */\r\n@Getter\r\n@Setter\r\n@NoArgsConstructor\r\npublic class User {\r\n\r\n    private long id;\r\n\r\n    private String name;\r\n\r\n    public User setId(long id){\r\n        this.id = id;\r\n        return this;\r\n    }\r\n\r\n    public User setName(String name){\r\n        this.name = name;\r\n        return this;\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-6-maven-pom-xml\"><a name=\"6. Maven构建文件 — pom.xml\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6. Maven构建文件 — pom.xml</h1><pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n    xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"&gt;\r\n    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;\r\n    &lt;parent&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\r\n        &lt;version&gt;2.1.3.RELEASE&lt;/version&gt;\r\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\r\n    &lt;/parent&gt;\r\n    &lt;groupId&gt;com.ramostear&lt;/groupId&gt;\r\n    &lt;artifactId&gt;exception-handler&lt;/artifactId&gt;\r\n    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;\r\n    &lt;name&gt;exception-handler&lt;/name&gt;\r\n    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;\r\n\r\n    &lt;properties&gt;\r\n        &lt;java.version&gt;1.8&lt;/java.version&gt;\r\n    &lt;/properties&gt;\r\n\r\n    &lt;dependencies&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\r\n            &lt;scope&gt;test&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;\r\n            &lt;artifactId&gt;lombok&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n    &lt;/dependencies&gt;\r\n\r\n    &lt;build&gt;\r\n        &lt;plugins&gt;\r\n            &lt;plugin&gt;\r\n                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;\r\n            &lt;/plugin&gt;\r\n        &lt;/plugins&gt;\r\n    &lt;/build&gt;\r\n\r\n&lt;/project&gt;\r\n</code></pre>\r\n<h1 id=\"h1-8-\"><a name=\"8. 运行测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>8. 运行测试</h1><p>接下来，我们将打包运行我们的程序，本次教程演示将使用IDEA来运行程序，运行结果如下图所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-06-17-52-31-a0b953b6952a413daf635fa3f3c5ef2c.png\" alt=\"\">\r\n<p>然后，启动Postman应用程序，我们先在地址栏输入：<a href=\"http://localhost:8080/users/1\">http://localhost:8080/users/1</a> ，观察正常情况下的测试信息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-06-17-52-45-b48cdadb2b3f4782a331171226ee6429.png\" alt=\"\">\r\n<p>Postman的测试结果显示，请求状态为200，且返回了用户的详细信息。现在，我们更新URL为：<a href=\"http://localhost:8080/users/3\">http://localhost:8080/users/3</a> ,再次观察测试结果：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-06-17-52-59-f0eb6a6981c946d6baa2c6231db23550.png\" alt=\"\">\r\n<p>此时的HTTP Status为404，且返回了“user not found.”的提示信息。</p>\r\n','SpringBoot,异常处理',0,'# Spring Boot （七）— 异常处理\r\n\r\n异常处理是一种识别并响应错误的一致性机制，异常机制可以把程序中的异常处理代码和正常的业务逻辑代码分离，包装程序的可读性和健壮性。在Spring Boot应用程序中，能够捕获并及时的响应客户端的错误操作是一件非常重要的事情。在本章节中,我将展示如何处理Spring Boot中的异常。\r\n\r\n# 1. 相关注解说明\r\n\r\n在进行演示之前，我们先了解一下在Spring Boot应用程序中与异常处理相关的几个注解\r\n\r\n| 注解名称          | 说明                                           |\r\n| ----------------- | ---------------------------------------------- |\r\n| @ControllerAdvice | 该标签用于处理全局的异常信息                   |\r\n| @ExceptionHadler  | 用于处理特定异常信息，并返回相关的响应到客户端 |\r\n\r\n首先，我们需要使用**@ControllerAdvice**注解来定义一个全局的异常信息处理类，其语法如下：\r\n\r\n```java\r\npackage com.ramostear.exception.handler;\r\n\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.ControllerAdvice;\r\nimport org.springframework.web.bind.annotation.ExceptionHandler;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:33\r\n */\r\n@ControllerAdvice\r\npublic class UserExceptionHandler {\r\n	//TODO ...\r\n}\r\n\r\n```\r\n\r\n接下来，我们需要定义一个扩展了**RuntimeException**类的自定义异常处理类：\r\n\r\n```java\r\npackage com.ramostear.exception.handler;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:31\r\n */\r\npublic class UserNotFoundException extends RuntimeException{\r\n    private static final long serialVersionUID = 5534028121297403043L;\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n最后，我们使用**@ExceptionHandler**注解来定义一个处理具体异常信息的方法，其语法如下：\r\n\r\n```java\r\n@ExceptionHandler(value = UserNotFoundException.class)\r\n    public ResponseEntity<Object> exception(UserNotFoundException ex){\r\n        return new ResponseEntity<>(\"user not found.\", HttpStatus.NOT_FOUND);\r\n    }\r\n```\r\n\r\n\r\n\r\n以上工作准备完成之后，我们可以使用如下的方式来处理API中的异常信息：\r\n\r\n```java\r\n@GetMapping(\"/users/{id}\")\r\n    public ResponseEntity<Object> getUser(@PathVariable(name = \"id\") long id){\r\n        if(!userRepo.containsKey ( id )){\r\n            throw new UserNotFoundException ();\r\n        }\r\n        return new ResponseEntity<> (userRepo.get (id), HttpStatus.OK);\r\n    }\r\n```\r\n\r\n在接下来的内容当中，我将给出完整的示例代码，使用HTTP GET方法请求一个用户信息，当用户存储库中没有相应的用户信息时，返回“user not found”提示信息。\r\n\r\n\r\n\r\n# 2. 自定义异常信息类 — UserNotFoundException.java\r\n\r\n```java\r\npackage com.ramostear.exception.handler;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:31\r\n */\r\npublic class UserNotFoundException extends RuntimeException{\r\n    private static final long serialVersionUID = 5534028121297403043L;\r\n}\r\n\r\n```\r\n\r\n> 说明：这里只是做了一个简单的扩展\r\n\r\n\r\n\r\n# 2. 全局异常处理类 —UserExceptionHandler.java\r\n\r\n```java\r\npackage com.ramostear.exception.handler;\r\n\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.ControllerAdvice;\r\nimport org.springframework.web.bind.annotation.ExceptionHandler;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:33\r\n */\r\n@ControllerAdvice\r\npublic class UserExceptionHandler {\r\n\r\n\r\n    @ExceptionHandler(value = UserNotFoundException.class)\r\n    public ResponseEntity<Object> exception(UserNotFoundException ex){\r\n        return new ResponseEntity<>(\"user not found.\", HttpStatus.NOT_FOUND);\r\n    }\r\n\r\n\r\n}\r\n\r\n```\r\n\r\n在UserExceptionHandler.java文件中，我们定义了一个处理用户不存在异常的方法，\r\n\r\n\r\n\r\n# 3. API类 — UserServiceController.java\r\n\r\n```java\r\npackage com.ramostear.exception.handler.controller;\r\n\r\nimport com.ramostear.exception.handler.UserNotFoundException;\r\nimport com.ramostear.exception.handler.model.User;\r\nimport org.springframework.http.HttpStatus;\r\nimport org.springframework.http.ResponseEntity;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.PathVariable;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\nimport javax.annotation.PostConstruct;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:26\r\n */\r\n@RestController\r\npublic class UserServiceController {\r\n\r\n    private static Map<Long,User> userRepo = new HashMap<>();\r\n\r\n    @PostConstruct\r\n    public void initUserRepo(){\r\n        User admin = new User ().setId ( 1 ).setName ( \"admin\" );\r\n        userRepo.put ( admin.getId (),admin );\r\n\r\n        User editor = new User ().setId ( 2 ).setName ( \"editor\" );\r\n        userRepo.put ( editor.getId (),editor );\r\n    }\r\n\r\n\r\n    @GetMapping(\"/users/{id}\")\r\n    public ResponseEntity<Object> getUser(@PathVariable(name = \"id\") long id){\r\n        if(!userRepo.containsKey ( id )){\r\n            throw new UserNotFoundException ();\r\n        }\r\n        return new ResponseEntity<> (userRepo.get (id), HttpStatus.OK);\r\n    }\r\n\r\n}\r\n\r\n```\r\n\r\n在getUser()方法中，如果用户没有找到，则抛出UserNotFoundException异常。\r\n\r\n\r\n\r\n# 4. 应用主类 —ExceptionHandlerApplication.java\r\n\r\n```java\r\npackage com.ramostear.exception.handler;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class ExceptionHandlerApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(ExceptionHandlerApplication.class, args);\r\n	}\r\n\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 5. 用户POJO类 — User.java\r\n\r\n```java\r\npackage com.ramostear.exception.handler.model;\r\n\r\nimport lombok.Getter;\r\nimport lombok.NoArgsConstructor;\r\nimport lombok.Setter;\r\n\r\n/**\r\n * @author : ramostear\r\n * @date : 2019/3/6 0006-16:23\r\n */\r\n@Getter\r\n@Setter\r\n@NoArgsConstructor\r\npublic class User {\r\n\r\n    private long id;\r\n\r\n    private String name;\r\n\r\n    public User setId(long id){\r\n        this.id = id;\r\n        return this;\r\n    }\r\n\r\n    public User setName(String name){\r\n        this.name = name;\r\n        return this;\r\n    }\r\n}\r\n\r\n```\r\n\r\n# 6. Maven构建文件 — pom.xml\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n	xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n	<modelVersion>4.0.0</modelVersion>\r\n	<parent>\r\n		<groupId>org.springframework.boot</groupId>\r\n		<artifactId>spring-boot-starter-parent</artifactId>\r\n		<version>2.1.3.RELEASE</version>\r\n		<relativePath/> <!-- lookup parent from repository -->\r\n	</parent>\r\n	<groupId>com.ramostear</groupId>\r\n	<artifactId>exception-handler</artifactId>\r\n	<version>0.0.1-SNAPSHOT</version>\r\n	<name>exception-handler</name>\r\n	<description>Demo project for Spring Boot</description>\r\n\r\n	<properties>\r\n		<java.version>1.8</java.version>\r\n	</properties>\r\n\r\n	<dependencies>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-web</artifactId>\r\n		</dependency>\r\n\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-test</artifactId>\r\n			<scope>test</scope>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.projectlombok</groupId>\r\n			<artifactId>lombok</artifactId>\r\n		</dependency>\r\n	</dependencies>\r\n\r\n	<build>\r\n		<plugins>\r\n			<plugin>\r\n				<groupId>org.springframework.boot</groupId>\r\n				<artifactId>spring-boot-maven-plugin</artifactId>\r\n			</plugin>\r\n		</plugins>\r\n	</build>\r\n\r\n</project>\r\n\r\n```\r\n\r\n# 8. 运行测试\r\n\r\n接下来，我们将打包运行我们的程序，本次教程演示将使用IDEA来运行程序，运行结果如下图所示：\r\n\r\n![](https://cdn.ramostear.com/2019-03-06-17-52-31-a0b953b6952a413daf635fa3f3c5ef2c.png)\r\n\r\n然后，启动Postman应用程序，我们先在地址栏输入：http://localhost:8080/users/1 ，观察正常情况下的测试信息：\r\n\r\n![](https://cdn.ramostear.com/2019-03-06-17-52-45-b48cdadb2b3f4782a331171226ee6429.png)\r\n\r\nPostman的测试结果显示，请求状态为200，且返回了用户的详细信息。现在，我们更新URL为：http://localhost:8080/users/3 ,再次观察测试结果：\r\n\r\n![](https://cdn.ramostear.com/2019-03-06-17-52-59-f0eb6a6981c946d6baa2c6231db23550.png)\r\n\r\n此时的HTTP Status为404，且返回了“user not found.”的提示信息。',1,'','2019/08/21/1xx2k8x8.html',0,'异常处理是一种识别并响应错误的一致性机制，异常机制可以把程序中的异常处理代码和正常的业务逻辑代码分离，包装程序的可读性和健壮性','blog.html','https://cdn.ramostear.com/2019-03-28-01-46-41-443cf604fe84475b9104f39fc9650e61.png;base64','优雅的处理Spring Boot异常信息','',0),(11,'2019-11-21 23:11:53',0,'2019-11-21 23:11:53',1,1,1,'谭朝红','Spring Boot应用程序属性支持我们开发的应用工作在不同的环境中。在本章节中，我将展示如何配置和指定Spring Boot 应用程序的属性','<h1 id=\"h1-spring-boot-\"><a name=\"Spring Boot（四）—应用程序属性\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot（四）—应用程序属性</h1><p>Spring Boot应用程序属性支持我们开发的应用工作在不同的环境中。在本章节中，我将展示如何配置和指定Spring Boot 应用程序的属性。</p>\r\n<h1 id=\"h1-1-\"><a name=\"1. 命令行属性\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. 命令行属性</h1><p>Spring Boot应用程序可以将命令行属性转换为Spring Boot Environment属性。在Spring Boot中，命令行属性的优先级高于其他资源属性。在默认情况下，Spring Boot使用8080端口作为Tomcat的启动端口。下面让我们使用命令行属性来更改端口号：</p>\r\n<ul>\r\n<li><p>1.创建可执行的JAR文件，然后使用命令 java -jar <jarfile>运行jar文件。<p></p></jarfile>\r\n</li><li><p>2.使用命令行更改Spring Boot应用程序的默认端口号</p>\r\n<pre><code class=\"lang-tex\">D:\\work\\2019\\domain-driver-design\\deploy-tomcat\\target&gt; java -jar deploy-tomcat-0.0.1-SNAPSHOT.war --server.port = 8888\r\n</code></pre>\r\n<blockquote>\r\n<p><strong>注</strong> ：你可以使用分隔符“-”来分割多个属性</p>\r\n</blockquote>\r\n</li></ul>\r\n<h1 id=\"h1-2-\"><a name=\"2. 属性文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 属性文件</h1><p>属性文件的作用是可以在单个文件中设置多个属性的值。在Spring Boot 应用程序中，属性保存在类路径下的application.properties文件中。通常情况下，application.properties文件存放在<strong>src/main/resource</strong>目录中，其属性设置格式如下：</p>\r\n<pre><code class=\"lang-properties\">server.port = 8888\r\nserver.tomcate.max-http-post-size = -1\r\nspring.application.name = spring-boot-tutorial\r\nspring.datasource.username = admin\r\nspring.datasource.password = springboottutorial\r\n...\r\n</code></pre>\r\n<h1 id=\"h1-3-yaml-\"><a name=\"3 . YAML文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3 . YAML文件</h1><p>Spring Boot应用程序支持基于YAML格式的属性配置文件为Spring Boot提供属性设置。YAML文件时Spring Boot比较推荐的属性配置文件，相较于application.properties文件，YAML配置文件册层次更为清晰。同样的，YAML文件也存放于类路径下（“src/main/resource”）,其文件内容格式为：</p>\r\n<pre><code class=\"lang-yaml\">server:\r\n  port: 8888\r\n  tomcate:\r\n    max-http-post-size: -1\r\nspring: \r\n  application:\r\n    name: spring-boot-tutorial\r\n  datasource:\r\n    username: admin\r\n    password: springboottutorial\r\n  mvc:\r\n    favicon:\r\n      enabled: false\r\n</code></pre>\r\n<blockquote>\r\n<p>注：YAML文件中，每个属性的上下层级之间相差两个空格</p>\r\n</blockquote>\r\n<h1 id=\"h1-4-\"><a name=\"4. 外部属性文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. 外部属性文件</h1><p>Spring Boot支持我们将属性配置文件放在外部空间中，而不仅仅局限于将配置文件放置在类路径下。在运行JAR文件的时候，我们可以指定外部配置文件的存放路径。</p>\r\n<pre><code class=\"lang-tex\">D:\\work\\2019\\domain-driver-design\\deploy-tomcat\\target&gt; java -jar -Dspring.config.location = D:\\work\\configs\\application.properties deploy-tomcat-0.0.1-SNAPSHOT.war\r\n</code></pre>\r\n<h1 id=\"h1-5-value-\"><a name=\"5. @Value 注解\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5. <a href=\"https://github.com/Value\" title=\"@Value\" class=\"at-link\">@Value</a> 注解</h1><p>在Spring Boot应用程序中，可以使用<strong><a href=\"https://github.com/Value\" title=\"@Value\" class=\"at-link\">@Value</a></strong>注解读取应用中的属性值，其读取相关属性值得语法如下：</p>\r\n<pre><code class=\"lang-java\">@Value(\"${property_key_name}\")\r\nObject param;\r\n</code></pre>\r\n<p>接下来，我们将演示使用<strong><a href=\"https://github.com/Value\" title=\"@Value\" class=\"at-link\">@Value</a></strong>注解来读取application.properties配置文件中spring.application.name属性的值：</p>\r\n<pre><code class=\"lang-java\">@Value(\"${spring.application.name}\")\r\nprivate String appName;\r\n</code></pre>\r\n<p>完整的示例代码：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.spring.boot.properties;\r\n\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\n@SpringBootApplication\r\n@RestController\r\npublic class SpringBootPropertiesApplication {\r\n\r\n    @Value(\"${spring.application.name}\")\r\n    private String appName;\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(SpringBootPropertiesApplication.class, args);\r\n    }\r\n\r\n    @GetMapping(\"/\")\r\n    public String say(){\r\n        return appName;\r\n    }\r\n}\r\n</code></pre>\r\n<p>application.properties文件内容：</p>\r\n<pre><code class=\"lang-pro\">spring.application.name= Spring Boot Properties Demo\r\n</code></pre>\r\n<p>下面，我们启动程序，并在浏览器地址栏中输入：<a href=\"http://localhost:8080/\">http://localhost:8080/</a> ，观察浏览器页面结果：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-05-02-18-16-c1503fed399c45babde365d475e55ab2.png\" alt=\"\">\r\n<blockquote>\r\n<p>注：如果在程序运行时没有在配置文件中提供该属性，则Spring Boot将抛出<strong>IllegalArgumentException</strong>异常信息：</p>\r\n<pre><code class=\"lang-tex\">Could not resolve placeholder \'spring.application.name\' in value \"${spring.application.name}\"\r\n</code></pre>\r\n<p>因为Spring Boot无法解析表达式“${spring.application.name}”中的占位符“spring.application.name”。</p>\r\n</blockquote>\r\n<p>要解决上面演示的问题，只需要在表达式后面增加一个默认的属性值即可。</p>\r\n<ul>\r\n<li><p>语法：</p>\r\n<pre><code class=\"lang-java\"><a href=\"https://github.com/Value\" title=\"@Value\" class=\"at-link\">@Value</a>(&quot;${property_key_name:default_value}&quot;)\r\n</code></pre>\r\n</li><li><p>代码示例：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.spring.boot.properties;\r\n\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\n<a href=\"https://github.com/SpringBootApplication\" title=\"@SpringBootApplication\" class=\"at-link\">@SpringBootApplication</a>\r\n<a href=\"https://github.com/RestController\" title=\"@RestController\" class=\"at-link\">@RestController</a>\r\npublic class SpringBootPropertiesApplication {\r\n\r\n    <a href=\"https://github.com/Value\" title=\"@Value\" class=\"at-link\">@Value</a>(&quot;${spring.application.name:Spring Boot Application}&quot;)\r\n    private String appName;\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(SpringBootPropertiesApplication.class, args);\r\n    }\r\n\r\n    <a href=\"https://github.com/GetMapping\" title=\"@GetMapping\" class=\"at-link\">@GetMapping</a>(&quot;/&quot;)\r\n    public String say(){\r\n        return appName;\r\n    }\r\n}\r\n</code></pre>\r\n</li></ul>\r\n<p>再次运行应用程序，观察浏览器页面变化：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-05-02-18-59-74f043b7a2964a41aa1d2de4138865f1.png\" alt=\"\">\r\n<h1 id=\"h1-6-active-\"><a name=\"6. active配置模式\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6. active配置模式</h1><p>Spring Boot支持基于Spring active模式来设置不同环境下的应用属性。例如，我们可以创建两份配置文件application_dev.properties和application_prod.properties，application_dev.properties用于开发环境中设置属性，application_prod.properties则用于生产环境中配置应用属性。</p>\r\n<p>首先，我们来看一下默认的application.properties文件中的配置：</p>\r\n<pre><code class=\"lang-pro\">server.port = 8888\r\nspring.application.name = Default Spring Boot Application Name\r\n</code></pre>\r\n<p>启动应用程序，观察控制台输出信息：</p>\r\n<pre><code>2019-03-05 01:29:19.590  INFO 7828 --- [main] .r.s.b.p.SpringBootPropertiesApplication : \r\nNo active profile set, falling back to default profiles: default\r\n\r\n2019-03-05 01:29:20.869  INFO 7828 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : \r\nTomcat initialized with port(s): 8888 (http)\r\n</code></pre><p>现在，Tomcat服务器已经使用8888端口启动成功，且提示没有设置任何的active profile。</p>\r\n<p>接下来，我们创建两个active配置文件application_dev.properties和application_prod.properties，其配置内容如下。</p>\r\n<p>application_dev.properties配置文件：</p>\r\n<pre><code class=\"lang-pro\">server.port = 8081\r\nspring.application.name = Development Spring Boot Application Name\r\n</code></pre>\r\n<p>application_prod.properties配置文件：</p>\r\n<pre><code class=\"lang-pro\">server.port = 8082\r\nspring.application.name = Product Spring Boot Application Name\r\n</code></pre>\r\n<p>两个独立的配置文件准备好后，我们需要调整application.properties文件的配置内容，以模拟使用active配置文件来切换不同环境下使用不同的属性配置：</p>\r\n<ul>\r\n<li><p>1.将默认的属性配置切换到dev模式上，切换到开发环境上的配置命令如下：</p>\r\n<pre><code class=\"lang-tex\">D:\\work\\2019\\domain-driver-design\\spring-boot-properties\\target&gt; java -jar spring-boot-properties-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev\r\n</code></pre>\r\n<p>你可以在控制台中观察到如下的输出：</p>\r\n<pre><code class=\"lang-tex\">2019-03-05 02:02:00.013  INFO 7700 ---[main] .r.s.b.p.SpringBootPropertiesApplication : The following profiles are active: dev\r\n2019-03-05 02:02:01.697  INFO 7700 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8081 (http)\r\n2019-03-05 02:02:01.732  INFO 7700 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\r\n</code></pre>\r\n<p>现在，Tomcat服务器已经使用端口8081（http）</p>\r\n</li><li><p>2.接下来，我们再将应用从dev切换到prod环境上，执行如下命令：</p>\r\n<pre><code class=\"lang-tex\">D:\\work\\2019\\domain-driver-design\\spring-boot-properties\\target&gt;java -jar spring-boot-properties-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod\r\n</code></pre>\r\n<p>你可以在控制台中观察到如下的输出：</p>\r\n<pre><code class=\"lang-tex\">2019-03-05 02:09:33.983  INFO 5944 --- [main] .r.s.b.p.SpringBootPropertiesApplication : The following profiles are active: prod\r\n\r\n2019-03-05 02:09:35.643  INFO 5944 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8082 (http)\r\n\r\n2019-03-05 02:09:35.678  INFO 5944 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\r\n</code></pre>\r\n<p>现在，Tomcat服务器已经使用端口8082（http）</p>\r\n</li></ul>\r\n<p>如果应用使用的是YAML文件来配置应用属性，我们可以将不同环境的配置信息放在一个配置文件中，而不用像使用properties文件那样创建多个独立的配置文件。</p>\r\n<p>下面我们将演示使用YAML文件来配置并切换不同环境下的属性配置，在YAML文件中，使用分隔符“—-”来分割不同active下的属性配置：</p>\r\n<p>application.yml配置文件</p>\r\n<pre><code class=\"lang-yaml\">spring:\r\n   application:\r\n      name: Default Spring Application Name\r\nserver:\r\n   port: 8080\r\n\r\n---\r\nspring:\r\n   profiles: dev\r\n   application:\r\n      name: Development Spring Application Name\r\nserver:\r\n   port: 8081\r\n\r\n---\r\nspring:\r\n   profiles: prod\r\n   application:\r\n      name: Product Spring Application Name\r\nserver:\r\n   port: 8082\r\n</code></pre>\r\n<blockquote>\r\n<p>说明：使用YAML作为配置文件时，切换active的命令与使用Properties配置文件时一致。</p>\r\n</blockquote>\r\n<p>​      </p>\r\n','SpringBoot,Properties',0,'# Spring Boot（四）—应用程序属性\r\n\r\nSpring Boot应用程序属性支持我们开发的应用工作在不同的环境中。在本章节中，我将展示如何配置和指定Spring Boot 应用程序的属性。\r\n\r\n\r\n\r\n# 1. 命令行属性\r\n\r\nSpring Boot应用程序可以将命令行属性转换为Spring Boot Environment属性。在Spring Boot中，命令行属性的优先级高于其他资源属性。在默认情况下，Spring Boot使用8080端口作为Tomcat的启动端口。下面让我们使用命令行属性来更改端口号：\r\n\r\n- 1.创建可执行的JAR文件，然后使用命令 java -jar <JARFILE>运行jar文件。\r\n\r\n- 2.使用命令行更改Spring Boot应用程序的默认端口号\r\n\r\n  ```tex\r\n  D:\\work\\2019\\domain-driver-design\\deploy-tomcat\\target> java -jar deploy-tomcat-0.0.1-SNAPSHOT.war --server.port = 8888\r\n  ```\r\n\r\n  > **注** ：你可以使用分隔符“-”来分割多个属性\r\n\r\n\r\n\r\n# 2. 属性文件\r\n\r\n属性文件的作用是可以在单个文件中设置多个属性的值。在Spring Boot 应用程序中，属性保存在类路径下的application.properties文件中。通常情况下，application.properties文件存放在**src/main/resource**目录中，其属性设置格式如下：\r\n\r\n```properties\r\nserver.port = 8888\r\nserver.tomcate.max-http-post-size = -1\r\nspring.application.name = spring-boot-tutorial\r\nspring.datasource.username = admin\r\nspring.datasource.password = springboottutorial\r\n...\r\n\r\n```\r\n\r\n\r\n\r\n# 3 . YAML文件\r\n\r\nSpring Boot应用程序支持基于YAML格式的属性配置文件为Spring Boot提供属性设置。YAML文件时Spring Boot比较推荐的属性配置文件，相较于application.properties文件，YAML配置文件册层次更为清晰。同样的，YAML文件也存放于类路径下（“src/main/resource”）,其文件内容格式为：\r\n\r\n```yaml\r\nserver:\r\n  port: 8888\r\n  tomcate:\r\n    max-http-post-size: -1\r\nspring: \r\n  application:\r\n    name: spring-boot-tutorial\r\n  datasource:\r\n    username: admin\r\n    password: springboottutorial\r\n  mvc:\r\n    favicon:\r\n      enabled: false\r\n```\r\n\r\n> 注：YAML文件中，每个属性的上下层级之间相差两个空格\r\n\r\n\r\n\r\n# 4. 外部属性文件\r\n\r\nSpring Boot支持我们将属性配置文件放在外部空间中，而不仅仅局限于将配置文件放置在类路径下。在运行JAR文件的时候，我们可以指定外部配置文件的存放路径。\r\n\r\n```tex\r\nD:\\work\\2019\\domain-driver-design\\deploy-tomcat\\target> java -jar -Dspring.config.location = D:\\work\\configs\\application.properties deploy-tomcat-0.0.1-SNAPSHOT.war\r\n```\r\n\r\n\r\n\r\n# 5. @Value 注解\r\n\r\n在Spring Boot应用程序中，可以使用**@Value**注解读取应用中的属性值，其读取相关属性值得语法如下：\r\n\r\n```java\r\n@Value(\"${property_key_name}\")\r\nObject param;\r\n```\r\n\r\n接下来，我们将演示使用**@Value**注解来读取application.properties配置文件中spring.application.name属性的值：\r\n\r\n```java\r\n@Value(\"${spring.application.name}\")\r\nprivate String appName;\r\n```\r\n\r\n完整的示例代码：\r\n\r\n```java\r\npackage com.ramostear.spring.boot.properties;\r\n\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\n@SpringBootApplication\r\n@RestController\r\npublic class SpringBootPropertiesApplication {\r\n\r\n	@Value(\"${spring.application.name}\")\r\n	private String appName;\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(SpringBootPropertiesApplication.class, args);\r\n	}\r\n\r\n	@GetMapping(\"/\")\r\n	public String say(){\r\n		return appName;\r\n	}\r\n}\r\n\r\n```\r\n\r\napplication.properties文件内容：\r\n\r\n```pro\r\nspring.application.name= Spring Boot Properties Demo\r\n\r\n```\r\n\r\n下面，我们启动程序，并在浏览器地址栏中输入：http://localhost:8080/ ，观察浏览器页面结果：\r\n\r\n![](https://cdn.ramostear.com/2019-03-05-02-18-16-c1503fed399c45babde365d475e55ab2.png)\r\n\r\n> 注：如果在程序运行时没有在配置文件中提供该属性，则Spring Boot将抛出**IllegalArgumentException**异常信息：\r\n>\r\n> ```tex\r\n> Could not resolve placeholder \'spring.application.name\' in value \"${spring.application.name}\"\r\n> ```\r\n>\r\n> 因为Spring Boot无法解析表达式“${spring.application.name}”中的占位符“spring.application.name”。\r\n\r\n\r\n\r\n要解决上面演示的问题，只需要在表达式后面增加一个默认的属性值即可。\r\n\r\n- 语法：\r\n\r\n  ```java\r\n  @Value(\"${property_key_name:default_value}\")\r\n  ```\r\n\r\n- 代码示例：\r\n\r\n  ```java\r\n  package com.ramostear.spring.boot.properties;\r\n  \r\n  import org.springframework.beans.factory.annotation.Value;\r\n  import org.springframework.boot.SpringApplication;\r\n  import org.springframework.boot.autoconfigure.SpringBootApplication;\r\n  import org.springframework.web.bind.annotation.GetMapping;\r\n  import org.springframework.web.bind.annotation.RestController;\r\n  \r\n  @SpringBootApplication\r\n  @RestController\r\n  public class SpringBootPropertiesApplication {\r\n  \r\n  	@Value(\"${spring.application.name:Spring Boot Application}\")\r\n  	private String appName;\r\n  \r\n  	public static void main(String[] args) {\r\n  		SpringApplication.run(SpringBootPropertiesApplication.class, args);\r\n  	}\r\n  \r\n  	@GetMapping(\"/\")\r\n  	public String say(){\r\n  		return appName;\r\n  	}\r\n  }\r\n  \r\n  ```\r\n\r\n再次运行应用程序，观察浏览器页面变化：\r\n\r\n![](https://cdn.ramostear.com/2019-03-05-02-18-59-74f043b7a2964a41aa1d2de4138865f1.png)\r\n\r\n \r\n\r\n# 6. active配置模式\r\n\r\nSpring Boot支持基于Spring active模式来设置不同环境下的应用属性。例如，我们可以创建两份配置文件application_dev.properties和application_prod.properties，application_dev.properties用于开发环境中设置属性，application_prod.properties则用于生产环境中配置应用属性。\r\n\r\n首先，我们来看一下默认的application.properties文件中的配置：\r\n\r\n```pro\r\nserver.port = 8888\r\nspring.application.name = Default Spring Boot Application Name\r\n```\r\n\r\n启动应用程序，观察控制台输出信息：\r\n\r\n```\r\n2019-03-05 01:29:19.590  INFO 7828 --- [main] .r.s.b.p.SpringBootPropertiesApplication : \r\nNo active profile set, falling back to default profiles: default\r\n\r\n2019-03-05 01:29:20.869  INFO 7828 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : \r\nTomcat initialized with port(s): 8888 (http)\r\n```\r\n\r\n现在，Tomcat服务器已经使用8888端口启动成功，且提示没有设置任何的active profile。\r\n\r\n接下来，我们创建两个active配置文件application_dev.properties和application_prod.properties，其配置内容如下。\r\n\r\napplication_dev.properties配置文件：\r\n\r\n```pro\r\nserver.port = 8081\r\nspring.application.name = Development Spring Boot Application Name\r\n```\r\n\r\napplication_prod.properties配置文件：\r\n\r\n```pro\r\nserver.port = 8082\r\nspring.application.name = Product Spring Boot Application Name\r\n```\r\n\r\n两个独立的配置文件准备好后，我们需要调整application.properties文件的配置内容，以模拟使用active配置文件来切换不同环境下使用不同的属性配置：\r\n\r\n- 1.将默认的属性配置切换到dev模式上，切换到开发环境上的配置命令如下：\r\n\r\n  ```tex\r\n  D:\\work\\2019\\domain-driver-design\\spring-boot-properties\\target> java -jar spring-boot-properties-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev\r\n  \r\n  ```\r\n\r\n  你可以在控制台中观察到如下的输出：\r\n\r\n  ```tex\r\n  2019-03-05 02:02:00.013  INFO 7700 ---[main] .r.s.b.p.SpringBootPropertiesApplication : The following profiles are active: dev\r\n  2019-03-05 02:02:01.697  INFO 7700 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8081 (http)\r\n  2019-03-05 02:02:01.732  INFO 7700 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\r\n  \r\n  ```\r\n\r\n  现在，Tomcat服务器已经使用端口8081（http）\r\n\r\n- 2.接下来，我们再将应用从dev切换到prod环境上，执行如下命令：\r\n\r\n  ```tex\r\n  D:\\work\\2019\\domain-driver-design\\spring-boot-properties\\target>java -jar spring-boot-properties-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod\r\n  \r\n  ```\r\n\r\n  你可以在控制台中观察到如下的输出：\r\n\r\n  ```tex\r\n  2019-03-05 02:09:33.983  INFO 5944 --- [main] .r.s.b.p.SpringBootPropertiesApplication : The following profiles are active: prod\r\n  \r\n  2019-03-05 02:09:35.643  INFO 5944 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8082 (http)\r\n  \r\n  2019-03-05 02:09:35.678  INFO 5944 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\r\n  \r\n  ```\r\n\r\n  现在，Tomcat服务器已经使用端口8082（http）\r\n\r\n\r\n\r\n如果应用使用的是YAML文件来配置应用属性，我们可以将不同环境的配置信息放在一个配置文件中，而不用像使用properties文件那样创建多个独立的配置文件。\r\n\r\n下面我们将演示使用YAML文件来配置并切换不同环境下的属性配置，在YAML文件中，使用分隔符“---”来分割不同active下的属性配置：\r\n\r\napplication.yml配置文件\r\n\r\n```yaml\r\nspring:\r\n   application:\r\n      name: Default Spring Application Name\r\nserver:\r\n   port: 8080\r\n\r\n---\r\nspring:\r\n   profiles: dev\r\n   application:\r\n      name: Development Spring Application Name\r\nserver:\r\n   port: 8081\r\n\r\n---\r\nspring:\r\n   profiles: prod\r\n   application:\r\n      name: Product Spring Application Name\r\nserver:\r\n   port: 8082\r\n\r\n\r\n```\r\n\r\n\r\n\r\n> 说明：使用YAML作为配置文件时，切换active的命令与使用Properties配置文件时一致。\r\n\r\n​      ',1,'','2019/10/21/w8n5jhe8.html',0,'Spring Boot应用程序属性支持我们开发的应用工作在不同的环境中。在本章节中，我将展示如何配置和指定Spring Boot 应用程序的属性','blog.html','https://cdn.ramostear.com/2019-03-28-01-38-10-7b44be70727d453baf81c4d490782cb4.png;base64','配置Spring Boot 应用程序参数','',0),(12,'2019-11-21 23:13:41',0,'2019-11-21 23:13:41',1,1,1,'谭朝红','在本章节中，将想你展示如何利用Spring Initializer快速构建一个Spring Boot应用程序.快速构建起一个Spring Boot应用程序的方法之一是使用Spring Initializer','<h1 id=\"h1-spring-boot-\"><a name=\"Spring Boot系列教程（二）快速构建\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot系列教程（二）快速构建</h1><p>在本章节中，将想你展示如何利用Spring Initializer快速构建一个Spring Boot应用程序。</p>\r\n<h1 id=\"h1-1-spring-initializer\"><a name=\"1. Spring Initializer\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. Spring Initializer</h1><p>快速构建起一个Spring Boot应用程序的方法之一是使用Spring Initializer.首先，我们需要访问Spring Initializer的官网<a href=\"https://start.spring.io,&quot;Spring Initializer&quot;\">https://start.spring.io</a> ,接下来，我们需要选择相应的构建方式、语言以及Spring Boot的版本。默认的构建方式是Maven Project,使用Java语言，Spring Boot的版本默认最新版本。</p>\r\n<p>接下来，我们需要提供一个项目的GroupId和ArtifactId,最后是检索并选择我们需要引入的依赖包。以上选择设置完成后，点击<strong>“Generate Project”</strong>按钮生成并下载Spring Initializer生成的项目包。本次案例中，我们将添加spring-boot-starter-web依赖项，用于编写一个REST风格的端点示例：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-03-23-39-22-b90b12b354754041a63136a9b02888d1.png\" alt=\"Spring Initializer\" title=\"Spring Initializer\">\r\n<h1 id=\"h1-2-maven\"><a name=\"2.Maven\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2.Maven</h1><p>现在，解压下载后的项目压缩包，并导入到对应的IDE中（本次案例使用的是Spring Tools Suite）,打开根目录下的pom.xml文件：</p>\r\n<pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n    xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"&gt;\r\n    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;\r\n    &lt;parent&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\r\n        &lt;version&gt;2.1.3.RELEASE&lt;/version&gt;\r\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\r\n    &lt;/parent&gt;\r\n    &lt;groupId&gt;com.ramostear&lt;/groupId&gt;\r\n    &lt;artifactId&gt;quick-build&lt;/artifactId&gt;\r\n    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;\r\n    &lt;name&gt;quick-build&lt;/name&gt;\r\n    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;\r\n\r\n    &lt;properties&gt;\r\n        &lt;java.version&gt;1.8&lt;/java.version&gt;\r\n    &lt;/properties&gt;\r\n\r\n    &lt;dependencies&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\r\n            &lt;scope&gt;test&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n    &lt;/dependencies&gt;\r\n\r\n    &lt;build&gt;\r\n        &lt;plugins&gt;\r\n            &lt;plugin&gt;\r\n                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;\r\n            &lt;/plugin&gt;\r\n        &lt;/plugins&gt;\r\n    &lt;/build&gt;\r\n\r\n&lt;/project&gt;\r\n</code></pre>\r\n<h1 id=\"h1-3-\"><a name=\"3. 依赖包\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. 依赖包</h1><p>Spring Boot为开发者提供了很多starters用于快速添加依赖包。本次案例中我们添加了一个Spring Web依赖的Starter:</p>\r\n<pre><code class=\"lang-xml\">...\r\n&lt;dependencies&gt;\r\n    ...\r\n    &lt;dependency&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n    &lt;/dependency&gt;\r\n    ...\r\n&lt;/dependencies&gt;\r\n...\r\n</code></pre>\r\n<h1 id=\"h1-4-\"><a name=\"4. 主类和方法\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. 主类和方法</h1><p>一个Spring Boot应用程序需要提供应用主类和方法，并且该主类需要（通常）使用<strong><a href=\"https://github.com/SpringBootApplication\" title=\"@SpringBootApplication\" class=\"at-link\">@SpringBootApplication</a></strong>注解进行标注。你可以在<strong>src/java/main</strong>目录下找到主类文件。</p>\r\n<p>此示例中，主类文件位于<strong>src/java/main</strong>目录中，且位于<strong>com.ramostear.quickbuild</strong>包中：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.quickbuild;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n/**\r\n * @author：  ramostear\r\n * @date：   2019年3月3日下午9:49:28\r\n */\r\n@SpringBootApplication\r\npublic class QuickBuildApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(QuickBuildApplication.class, args);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-5-rest-endpoint\"><a name=\"5. REST EndPoint\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5. REST EndPoint</h1><p> 接下来，我们将在主类中编写一个返回“Hello Spring Boot”信息的Rest Endpoint。我们需要在主类上新增一个<strong><a href=\"https://github.com/ResController\" title=\"@ResController\" class=\"at-link\">@ResController</a></strong>注解，然后使用<strong><a href=\"https://github.com/GetMapping\" title=\"@GetMapping\" class=\"at-link\">@GetMapping</a></strong>注解注释我们的请求方法：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.quickbuild;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n/**\r\n * @author：  ramostear\r\n * @date：   2019年3月3日下午9:49:28\r\n */\r\n@SpringBootApplication\r\n@RestController\r\npublic class QuickBuildApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(QuickBuildApplication.class, args);\r\n    }\r\n\r\n\r\n    @GetMapping(\"/\")\r\n    public String sayHello() {\r\n        return \"Hello Spring Boot.\";\r\n    }\r\n\r\n\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-6-jar-\"><a name=\"6. 创建可执行的JAR文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6. 创建可执行的JAR文件</h1><p>现在，我们可用通过Maven命令行来创建一个可执行的JAR文件。在命令行工具中使用Maven命令：<strong>mvn clean install</strong> </p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-03-23-40-05-db622eed2c584a74b3265e4cee0d1c19.png\" alt=\"mvn clean install\" title=\"mvn clean install\">\r\n<p>执行上述命令后，你可以在控制台看到<strong>BUILD SUCCESS</strong>消息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-03-23-40-46-6780d46ae257475f85f729c7fcc17033.png\" alt=\"BUILD SUCCESS\" title=\"BUILD SUCCESS\">\r\n<h1 id=\"h1-7-jar-\"><a name=\"7. 运行JAR 文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7. 运行JAR 文件</h1><p>上述步骤完成后，可以在target目录中找到创建的JAR文件：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-03-23-41-11-b5e837bda7334436b74b7400f1cb3285.png\" alt=\"target dir\" title=\"target dir\">\r\n<p>现在，可以使用命令<strong>java -jar <jarfile></jarfile> 运行JAR文件。本案例中创建的JAR文件名为<strong>quick-build-0.0.1-SNAPSHOT.jar</strong></p>\r\n<pre><code class=\"lang-tex\">C:\\Users\\Administrator\\Desktop\\quick-build\\target&gt; java -jar quick-build-0.0.1-SNAPSHOT.jar\r\n</code></pre>\r\n<p>JAR文件成功运行后，你可以在控制台下看到如下的信息输出：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-03-23-41-35-254d70efdb9d46558e470d61fdd35e68.png\" alt=\"console start infomatioin\" title=\"console start infomatioin\">\r\n<p>控制台信息显示Tomcat容器的服务端口为8080，打开浏览器并在地址栏输入<strong><a href=\"http://localhost:8080/\">http://localhost:8080/</a></strong>,你可以看到如下的输出：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-03-23-42-05-f22234caea544460a73140517c6e2705.png\" alt=\"browser info\" title=\"browser info\">\r\n','SpringBoot2.0',0,'# Spring Boot系列教程（二）快速构建\r\n\r\n在本章节中，将想你展示如何利用Spring Initializer快速构建一个Spring Boot应用程序。\r\n\r\n\r\n\r\n# 1. Spring Initializer\r\n\r\n快速构建起一个Spring Boot应用程序的方法之一是使用Spring Initializer.首先，我们需要访问Spring Initializer的官网[https://start.spring.io](https://start.spring.io,\"Spring Initializer\") ,接下来，我们需要选择相应的构建方式、语言以及Spring Boot的版本。默认的构建方式是Maven Project,使用Java语言，Spring Boot的版本默认最新版本。\r\n\r\n接下来，我们需要提供一个项目的GroupId和ArtifactId,最后是检索并选择我们需要引入的依赖包。以上选择设置完成后，点击**“Generate Project”**按钮生成并下载Spring Initializer生成的项目包。本次案例中，我们将添加spring-boot-starter-web依赖项，用于编写一个REST风格的端点示例：\r\n\r\n![Spring Initializer](https://cdn.ramostear.com/2019-03-03-23-39-22-b90b12b354754041a63136a9b02888d1.png \"Spring Initializer\")\r\n\r\n\r\n\r\n# 2.Maven\r\n\r\n现在，解压下载后的项目压缩包，并导入到对应的IDE中（本次案例使用的是Spring Tools Suite）,打开根目录下的pom.xml文件：\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n	xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n	<modelVersion>4.0.0</modelVersion>\r\n	<parent>\r\n		<groupId>org.springframework.boot</groupId>\r\n		<artifactId>spring-boot-starter-parent</artifactId>\r\n		<version>2.1.3.RELEASE</version>\r\n		<relativePath/> <!-- lookup parent from repository -->\r\n	</parent>\r\n	<groupId>com.ramostear</groupId>\r\n	<artifactId>quick-build</artifactId>\r\n	<version>0.0.1-SNAPSHOT</version>\r\n	<name>quick-build</name>\r\n	<description>Demo project for Spring Boot</description>\r\n\r\n	<properties>\r\n		<java.version>1.8</java.version>\r\n	</properties>\r\n\r\n	<dependencies>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-web</artifactId>\r\n		</dependency>\r\n\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-test</artifactId>\r\n			<scope>test</scope>\r\n		</dependency>\r\n	</dependencies>\r\n\r\n	<build>\r\n		<plugins>\r\n			<plugin>\r\n				<groupId>org.springframework.boot</groupId>\r\n				<artifactId>spring-boot-maven-plugin</artifactId>\r\n			</plugin>\r\n		</plugins>\r\n	</build>\r\n\r\n</project>\r\n\r\n```\r\n\r\n\r\n\r\n# 3. 依赖包\r\n\r\nSpring Boot为开发者提供了很多starters用于快速添加依赖包。本次案例中我们添加了一个Spring Web依赖的Starter:\r\n\r\n```xml\r\n...\r\n<dependencies>\r\n    ...\r\n    <dependency>\r\n    	<groupId>org.springframework.boot</groupId>\r\n        <artifactId>spring-boot-starter-web</artifactId>\r\n    </dependency>\r\n    ...\r\n</dependencies>\r\n...\r\n```\r\n\r\n\r\n\r\n# 4. 主类和方法\r\n\r\n一个Spring Boot应用程序需要提供应用主类和方法，并且该主类需要（通常）使用**@SpringBootApplication**注解进行标注。你可以在**src/java/main**目录下找到主类文件。\r\n\r\n此示例中，主类文件位于**src/java/main**目录中，且位于**com.ramostear.quickbuild**包中：\r\n\r\n```java\r\npackage com.ramostear.quickbuild;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n/**\r\n * @author：  ramostear\r\n * @date：   2019年3月3日下午9:49:28\r\n */\r\n@SpringBootApplication\r\npublic class QuickBuildApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(QuickBuildApplication.class, args);\r\n	}\r\n\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 5. REST EndPoint\r\n\r\n 接下来，我们将在主类中编写一个返回“Hello Spring Boot”信息的Rest Endpoint。我们需要在主类上新增一个**@ResController**注解，然后使用**@GetMapping**注解注释我们的请求方法：\r\n\r\n```java\r\npackage com.ramostear.quickbuild;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n/**\r\n * @author：  ramostear\r\n * @date：   2019年3月3日下午9:49:28\r\n */\r\n@SpringBootApplication\r\n@RestController\r\npublic class QuickBuildApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(QuickBuildApplication.class, args);\r\n	}\r\n\r\n	\r\n	@GetMapping(\"/\")\r\n	public String sayHello() {\r\n		return \"Hello Spring Boot.\";\r\n	}\r\n	\r\n	\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 6. 创建可执行的JAR文件\r\n\r\n现在，我们可用通过Maven命令行来创建一个可执行的JAR文件。在命令行工具中使用Maven命令：**mvn clean install** \r\n\r\n![mvn clean install](https://cdn.ramostear.com/2019-03-03-23-40-05-db622eed2c584a74b3265e4cee0d1c19.png \"mvn clean install\")\r\n\r\n执行上述命令后，你可以在控制台看到**BUILD SUCCESS**消息：\r\n\r\n![BUILD SUCCESS](https://cdn.ramostear.com/2019-03-03-23-40-46-6780d46ae257475f85f729c7fcc17033.png \"BUILD SUCCESS\")\r\n\r\n\r\n\r\n# 7. 运行JAR 文件\r\n\r\n上述步骤完成后，可以在target目录中找到创建的JAR文件：\r\n\r\n![target dir](https://cdn.ramostear.com/2019-03-03-23-41-11-b5e837bda7334436b74b7400f1cb3285.png \"target dir\")\r\n\r\n现在，可以使用命令**java -jar <JARFILE>** 运行JAR文件。本案例中创建的JAR文件名为**quick-build-0.0.1-SNAPSHOT.jar**\r\n\r\n```tex\r\nC:\\Users\\Administrator\\Desktop\\quick-build\\target> java -jar quick-build-0.0.1-SNAPSHOT.jar\r\n```\r\n\r\nJAR文件成功运行后，你可以在控制台下看到如下的信息输出：\r\n\r\n![console start infomatioin](https://cdn.ramostear.com/2019-03-03-23-41-35-254d70efdb9d46558e470d61fdd35e68.png \"console start infomatioin\")\r\n\r\n控制台信息显示Tomcat容器的服务端口为8080，打开浏览器并在地址栏输入**http://localhost:8080/**,你可以看到如下的输出：\r\n\r\n![browser info](https://cdn.ramostear.com/2019-03-03-23-42-05-f22234caea544460a73140517c6e2705.png \"browser info\")',1,'','2019/12/21/0p683t59.html',0,'在本章节中，将想你展示如何利用Spring Initializer快速构建一个Spring Boot应用程序.快速构建起一个Spring Boot应用程序的方法之一是使用Spring Initializer','blog.html','https://cdn.ramostear.com/2019-03-28-01-34-06-7e2f14cc81fd42a8894fe74474ca2907.png;base64','Spring Boot 2.0 快速构建应用','',0),(13,'2019-11-21 23:15:42',0,'2019-11-21 23:15:42',1,1,1,'谭朝红','Spring Boot应用程序除了生成JAR文件直接运行外，也可以创建成一个WAR文件部署到Web服务器中。在本章节中，我将为你展示如何使用Spring Boot创建WAR文件并在Tomcat Web容器中进行部署','<h1 id=\"h1-spring-boot-tomcat-\"><a name=\"Spring Boot(三)—Tomcat中部署\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot(三)—Tomcat中部署</h1><p>Spring Boot应用程序除了生成JAR文件直接运行外，也可以创建成一个WAR文件部署到Web服务器中。在本章节中，我将为你展示如何使用Spring Boot创建WAR文件并在Tomcat Web容器中进行部署。</p>\r\n<h1 id=\"h1-1-spring-boot-servlet-\"><a name=\"1. Spring Boot Servlet初始化程序\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. Spring Boot Servlet初始化程序</h1><p>如果想要在Tomcat Web服务器中部署Spring Boot应用程序，需要让被<strong><a href=\"https://github.com/SpringBootApplication\" title=\"@SpringBootApplication\" class=\"at-link\">@SpringBootApplication</a></strong>注解注释的的主类继承<strong>SpringBootServletInitializer</strong>类，并覆盖configure方法。</p>\r\n<p>下面的代码片段是使用JAR文件运行Spring Boot应用程序的主文件代码：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.spring.boot.deploytomcat;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n/**\r\n * @author ramostear\r\n */\r\n@SpringBootApplication\r\npublic class DeployTomcatApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(DeployTomcatApplication.class, args);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>现在，我们需要扩展<strong>SpringBootServletInlitializer</strong>类以支持WAR文件部署。扩展后的主类代码如下：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.spring.boot.tutorial2;\r\n\r\n@SpringBootApplication\r\npublic class Application extends SpringBootServletInitializer{\r\n    @Override\r\n    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder){\r\n        return builder.sources(Application.class,args);\r\n    }\r\n    public static void main(String[]args){\r\n        SpringBootApplication.run(Application.class,args);\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-2-\"><a name=\"2. 设置主类\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 设置主类</h1><p>对于Spring Boot应用程序，你需要明确的指出在maven构建项目是主类的位置。你需要在pom.xml文件加入如下的代码：</p>\r\n<pre><code class=\"lang-xml\">&lt;start-class&gt;com.ramostear.spring.boot.tutorial2.Application&lt;/start-class&gt;\r\n</code></pre>\r\n<h1 id=\"h1-3-\"><a name=\"3. 更新打包方式\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. 更新打包方式</h1><p>想要将Spring Boot应用程序从jar包更换为war包，还需要修改pom.xml中的packaging配置：</p>\r\n<pre><code class=\"lang-xml\">&lt;packaging&gt;war&lt;/packaging&gt;\r\n</code></pre>\r\n<p>现在，我们可以编写一个简单的REST Endpoint来返回字符串“Hey Spring Boot,I am from tomcat server.”。为了演示此功能，我们还需要将Spring Boot Web Starter添加到pom.xml文件中：</p>\r\n<pre><code class=\"lang-xml\">&lt;dependencies&gt;\r\n    ...\r\n    &lt;dependency&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n    &lt;/dependency&gt;\r\n    ...\r\n&lt;/dependencies&gt;\r\n</code></pre>\r\n<p>接下来，我们将在主类中编写一个简单的REST Endpoint:</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.spring.boot.deploytomcat;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.boot.builder.SpringApplicationBuilder;\r\nimport org.springframework.boot.web.servlet.support.SpringBootServletInitializer;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\n/**\r\n * @author ramostear\r\n */\r\n@SpringBootApplication\r\n@RestController\r\npublic class DeployTomcatApplication extends SpringBootServletInitializer {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(DeployTomcatApplication.class, args);\r\n    }\r\n\r\n    @Override\r\n    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {\r\n        return builder.sources(DeployTomcatApplication.class);\r\n    }\r\n\r\n    @GetMapping(\"/\")\r\n    public String sayHello(){\r\n        return \"Hey Spring Boot,I am from tomcat server.\";\r\n    }\r\n}\r\n</code></pre>\r\n<h1 id=\"h1-4-spring-boot-\"><a name=\"4. 打包Spring Boot应用程序\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. 打包Spring Boot应用程序</h1><p>我们将使用Maven打包命令创建一个可以在Tomcat服务器中运行的WAR文件。使用Maven命令：mvn:package打包应用程序。你可以在当前工程下的target目录中找到war文件：</p>\r\n<pre><code class=\"lang-tex\">C:\\~projectdir&gt; mvn package\r\n</code></pre>\r\n<p>控制台输出：</p>\r\n<pre><code class=\"lang-tex\">[INFO] Scanning for projects...\r\n[INFO]                                                                         \r\n[INFO] ------------------------------------------------------------------------\r\n[INFO] Building deploy-tomcat 0.0.1-SNAPSHOT\r\n[INFO] ------------------------------------------------------------------------\r\n[INFO] \r\n[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ deploy-tomcat ---\r\n[INFO] Using \'UTF-8\' encoding to copy filtered resources.\r\n[INFO] Copying 1 resource\r\n[INFO] Copying 0 resource\r\n\r\n.....\r\n\r\n\r\n[INFO] \r\n[INFO] --- spring-boot-maven-plugin:2.1.3.RELEASE:repackage (repackage) @ deploy-tomcat ---\r\n[INFO] Replacing main artifact with repackaged archive\r\n[INFO] ------------------------------------------------------------------------\r\n[INFO] BUILD SUCCESS\r\n[INFO] ------------------------------------------------------------------------\r\n[INFO] Total time: 10.273 s\r\n[INFO] Finished at: 2019-03-04T23:34:12+08:00\r\n[INFO] Final Memory: 38M/288M\r\n[INFO] ------------------------------------------------------------------------\r\n\r\nProcess finished with exit code 0\r\n</code></pre>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-04-23-47-20-9e88af7c1f0248879aa1475dde8f341e.png\" alt=\"\">\r\n<h1 id=\"h1-5-tomcat\"><a name=\"5 . 部署到Tomcat\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5 . 部署到Tomcat</h1><p>现在，运行Tomcat服务器，并在webapps目录下部署准备好的war文件。你也可以登录Tomcat的管理界面上传war文件并部署：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-04-23-47-41-8b7e83b359734e04a6dd6e0587551a86.png\" alt=\"\">\r\n<p>成功部署war文件后，代开浏览器，并在浏览器地址栏输入：<a href=\"http://localhost:8080/deploy-tomcat-0.0.1-SNAPSHOT/\">http://localhost:8080/deploy-tomcat-0.0.1-SNAPSHOT/</a> ,观察页面输出结果：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-04-23-47-57-414f81c45d9e4a059bde86e830ea5c94.png\" alt=\"\">\r\n<h1 id=\"h1-6-\"><a name=\"6 . 主要代码\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6 . 主要代码</h1><p>pom.xml代码：</p>\r\n<pre><code class=\"lang-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n    xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"&gt;\r\n    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;\r\n    &lt;parent&gt;\r\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\r\n        &lt;version&gt;2.1.3.RELEASE&lt;/version&gt;\r\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\r\n    &lt;/parent&gt;\r\n    &lt;groupId&gt;com.ramostear&lt;/groupId&gt;\r\n    &lt;artifactId&gt;deploy-tomcat&lt;/artifactId&gt;\r\n    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;\r\n    &lt;name&gt;deploy-tomcat&lt;/name&gt;\r\n    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;\r\n\r\n    &lt;properties&gt;\r\n        &lt;java.version&gt;1.8&lt;/java.version&gt;\r\n    &lt;/properties&gt;\r\n\r\n    &lt;dependencies&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\r\n            &lt;scope&gt;test&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n    &lt;/dependencies&gt;\r\n\r\n    &lt;build&gt;\r\n        &lt;plugins&gt;\r\n            &lt;plugin&gt;\r\n                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;\r\n            &lt;/plugin&gt;\r\n        &lt;/plugins&gt;\r\n    &lt;/build&gt;\r\n    &lt;packaging&gt;war&lt;/packaging&gt;\r\n&lt;/project&gt;\r\n</code></pre>\r\n<p>DeployTomcatApplication.java文件：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.spring.boot.deploytomcat;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.boot.builder.SpringApplicationBuilder;\r\nimport org.springframework.boot.web.servlet.support.SpringBootServletInitializer;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\n/**\r\n * @author ramostear\r\n */\r\n@SpringBootApplication\r\n@RestController\r\npublic class DeployTomcatApplication extends SpringBootServletInitializer {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(DeployTomcatApplication.class, args);\r\n    }\r\n\r\n    @Override\r\n    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {\r\n        return builder.sources(DeployTomcatApplication.class);\r\n    }\r\n\r\n    @GetMapping(\"/\")\r\n    public String sayHello(){\r\n        return \"Hey Spring Boot,I am from tomcat server.\";\r\n    }\r\n}\r\n</code></pre>\r\n','SpringBoot,Tomcat',0,'# Spring Boot(三)—Tomcat中部署\r\n\r\nSpring Boot应用程序除了生成JAR文件直接运行外，也可以创建成一个WAR文件部署到Web服务器中。在本章节中，我将为你展示如何使用Spring Boot创建WAR文件并在Tomcat Web容器中进行部署。\r\n\r\n\r\n\r\n# 1. Spring Boot Servlet初始化程序\r\n\r\n如果想要在Tomcat Web服务器中部署Spring Boot应用程序，需要让被**@SpringBootApplication**注解注释的的主类继承**SpringBootServletInitializer**类，并覆盖configure方法。\r\n\r\n下面的代码片段是使用JAR文件运行Spring Boot应用程序的主文件代码：\r\n\r\n```java\r\npackage com.ramostear.spring.boot.deploytomcat;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n/**\r\n * @author ramostear\r\n */\r\n@SpringBootApplication\r\npublic class DeployTomcatApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(DeployTomcatApplication.class, args);\r\n	}\r\n\r\n}\r\n\r\n\r\n```\r\n\r\n现在，我们需要扩展**SpringBootServletInlitializer**类以支持WAR文件部署。扩展后的主类代码如下：\r\n\r\n```java\r\npackage com.ramostear.spring.boot.tutorial2;\r\n\r\n@SpringBootApplication\r\npublic class Application extends SpringBootServletInitializer{\r\n    @Override\r\n    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder){\r\n        return builder.sources(Application.class,args);\r\n    }\r\n    public static void main(String[]args){\r\n        SpringBootApplication.run(Application.class,args);\r\n    }\r\n}\r\n```\r\n\r\n\r\n\r\n# 2. 设置主类\r\n\r\n对于Spring Boot应用程序，你需要明确的指出在maven构建项目是主类的位置。你需要在pom.xml文件加入如下的代码：\r\n\r\n```xml\r\n<start-class>com.ramostear.spring.boot.tutorial2.Application</start-class>\r\n```\r\n\r\n\r\n\r\n# 3. 更新打包方式\r\n\r\n想要将Spring Boot应用程序从jar包更换为war包，还需要修改pom.xml中的packaging配置：\r\n\r\n```xml\r\n<packaging>war</packaging>\r\n```\r\n\r\n现在，我们可以编写一个简单的REST Endpoint来返回字符串“Hey Spring Boot,I am from tomcat server.”。为了演示此功能，我们还需要将Spring Boot Web Starter添加到pom.xml文件中：\r\n\r\n```xml\r\n<dependencies>\r\n	...\r\n    <dependency>\r\n		<groupId>org.springframework.boot</groupId>\r\n        <artifactId>spring-boot-starter-web</artifactId>\r\n	</dependency>\r\n    ...\r\n</dependencies>\r\n\r\n```\r\n\r\n接下来，我们将在主类中编写一个简单的REST Endpoint:\r\n\r\n```java\r\npackage com.ramostear.spring.boot.deploytomcat;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.boot.builder.SpringApplicationBuilder;\r\nimport org.springframework.boot.web.servlet.support.SpringBootServletInitializer;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\n/**\r\n * @author ramostear\r\n */\r\n@SpringBootApplication\r\n@RestController\r\npublic class DeployTomcatApplication extends SpringBootServletInitializer {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(DeployTomcatApplication.class, args);\r\n	}\r\n\r\n	@Override\r\n	protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {\r\n		return builder.sources(DeployTomcatApplication.class);\r\n	}\r\n\r\n	@GetMapping(\"/\")\r\n	public String sayHello(){\r\n		return \"Hey Spring Boot,I am from tomcat server.\";\r\n	}\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n# 4. 打包Spring Boot应用程序\r\n\r\n我们将使用Maven打包命令创建一个可以在Tomcat服务器中运行的WAR文件。使用Maven命令：mvn:package打包应用程序。你可以在当前工程下的target目录中找到war文件：\r\n\r\n```tex\r\nC:\\~projectdir> mvn package\r\n```\r\n\r\n控制台输出：\r\n\r\n```tex\r\n[INFO] Scanning for projects...\r\n[INFO]                                                                         \r\n[INFO] ------------------------------------------------------------------------\r\n[INFO] Building deploy-tomcat 0.0.1-SNAPSHOT\r\n[INFO] ------------------------------------------------------------------------\r\n[INFO] \r\n[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ deploy-tomcat ---\r\n[INFO] Using \'UTF-8\' encoding to copy filtered resources.\r\n[INFO] Copying 1 resource\r\n[INFO] Copying 0 resource\r\n\r\n.....\r\n\r\n\r\n[INFO] \r\n[INFO] --- spring-boot-maven-plugin:2.1.3.RELEASE:repackage (repackage) @ deploy-tomcat ---\r\n[INFO] Replacing main artifact with repackaged archive\r\n[INFO] ------------------------------------------------------------------------\r\n[INFO] BUILD SUCCESS\r\n[INFO] ------------------------------------------------------------------------\r\n[INFO] Total time: 10.273 s\r\n[INFO] Finished at: 2019-03-04T23:34:12+08:00\r\n[INFO] Final Memory: 38M/288M\r\n[INFO] ------------------------------------------------------------------------\r\n\r\nProcess finished with exit code 0\r\n```\r\n\r\n![](https://cdn.ramostear.com/2019-03-04-23-47-20-9e88af7c1f0248879aa1475dde8f341e.png)\r\n\r\n# 5 . 部署到Tomcat\r\n\r\n现在，运行Tomcat服务器，并在webapps目录下部署准备好的war文件。你也可以登录Tomcat的管理界面上传war文件并部署：\r\n\r\n![](https://cdn.ramostear.com/2019-03-04-23-47-41-8b7e83b359734e04a6dd6e0587551a86.png)\r\n\r\n\r\n\r\n成功部署war文件后，代开浏览器，并在浏览器地址栏输入：http://localhost:8080/deploy-tomcat-0.0.1-SNAPSHOT/ ,观察页面输出结果：\r\n\r\n![](https://cdn.ramostear.com/2019-03-04-23-47-57-414f81c45d9e4a059bde86e830ea5c94.png)\r\n\r\n\r\n\r\n# 6 . 主要代码\r\n\r\npom.xml代码：\r\n\r\n```xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n	xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n	<modelVersion>4.0.0</modelVersion>\r\n	<parent>\r\n		<groupId>org.springframework.boot</groupId>\r\n		<artifactId>spring-boot-starter-parent</artifactId>\r\n		<version>2.1.3.RELEASE</version>\r\n		<relativePath/> <!-- lookup parent from repository -->\r\n	</parent>\r\n	<groupId>com.ramostear</groupId>\r\n	<artifactId>deploy-tomcat</artifactId>\r\n	<version>0.0.1-SNAPSHOT</version>\r\n	<name>deploy-tomcat</name>\r\n	<description>Demo project for Spring Boot</description>\r\n\r\n	<properties>\r\n		<java.version>1.8</java.version>\r\n	</properties>\r\n\r\n	<dependencies>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-web</artifactId>\r\n		</dependency>\r\n\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-test</artifactId>\r\n			<scope>test</scope>\r\n		</dependency>\r\n	</dependencies>\r\n\r\n	<build>\r\n		<plugins>\r\n			<plugin>\r\n				<groupId>org.springframework.boot</groupId>\r\n				<artifactId>spring-boot-maven-plugin</artifactId>\r\n			</plugin>\r\n		</plugins>\r\n	</build>\r\n	<packaging>war</packaging>\r\n</project>\r\n\r\n```\r\n\r\nDeployTomcatApplication.java文件：\r\n\r\n```java\r\npackage com.ramostear.spring.boot.deploytomcat;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.boot.builder.SpringApplicationBuilder;\r\nimport org.springframework.boot.web.servlet.support.SpringBootServletInitializer;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\n/**\r\n * @author ramostear\r\n */\r\n@SpringBootApplication\r\n@RestController\r\npublic class DeployTomcatApplication extends SpringBootServletInitializer {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(DeployTomcatApplication.class, args);\r\n	}\r\n\r\n	@Override\r\n	protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {\r\n		return builder.sources(DeployTomcatApplication.class);\r\n	}\r\n\r\n	@GetMapping(\"/\")\r\n	public String sayHello(){\r\n		return \"Hey Spring Boot,I am from tomcat server.\";\r\n	}\r\n}\r\n\r\n```\r\n\r\n',1,'','2019/13/21/ozsdz3hy.html',0,'Spring Boot应用程序除了生成JAR文件直接运行外，也可以创建成一个WAR文件部署到Web服务器中。在本章节中，我将为你展示如何使用Spring Boot创建WAR文件并在Tomcat Web容器中进行部署','blog.html','https://cdn.ramostear.com/2019-03-28-01-36-06-8dba985bc1f1485e8e308a7d0aff6283.png;base64','在Tomcat中部署Spring Boot程序','',0),(14,'2019-11-21 23:17:47',0,'2019-11-21 23:32:21',1,0,0,'谭朝红','在运行时重新启动SpringBoot应用程序','<h1 id=\"h1--http-spring-boot-\"><a name=\"通过HTTP重启Spring Boot应用程序\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>通过HTTP重启Spring Boot应用程序</h1><h2 id=\"h2-u9700u6C42u80CCu666F\"><a name=\"需求背景\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>需求背景</h2><p>在一个很奇葩的需求下，要求在客户端动态修改Spring Boot配置文件中的属性，例如端口号、应用名称、数据库连接信息等，然后通过一个Http请求重启Spring Boot程序。这个需求类似于操作系统更新配置后需要进行重启系统才能生效的应用场景。</p>\r\n<p>动态配置系统并更新生效是应用的一种通用性需求，实现的方式也有很多种。例如监听配置文件变化、使用配置中心等等。网络上也有很多类似的教程存在，但大多数都是在开发阶段，借助Spring Boot DevTools插件实现应用程序的重启，或者是使用spring-boot-starter-actuator和spring-cloud-starter-config来提供端点（Endpoint）的刷新。</p>\r\n<blockquote>\r\n<p>第一种方式无法在生产环境中使用（不考虑），第二种方式需要引入Spring Cloud相关内容，这无疑是杀鸡用了宰牛刀。</p>\r\n</blockquote>\r\n<p>接下来，我将尝试采用另外一种方式实现HTTP请求重启Spring Boot应用程序这个怪异的需求。</p>\r\n<h2 id=\"h2-u5C1Du8BD5u601Du8DEF\"><a name=\"尝试思路\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>尝试思路</h2><p>重启Spring Boot应用程序的关键步骤是对主类中<strong>SpringApplication.run(Application.class,args);</strong>方法返回值的处理。<strong>SpringApplication#run()</strong>方法将会返回一个<strong>ConfigurableApplicationContext</strong>类型对象，通过查看官方文档可以看到，<strong>ConfigurableApplicationContext</strong>接口类中定义了一个<strong>close()</strong>方法，可以用来关闭当前应用的上下文：</p>\r\n<pre><code class=\"lang-java\">package org.springframework.context;\r\n\r\nimport java.io.Closeable;\r\nimport org.springframework.beans.BeansException;\r\nimport org.springframework.beans.factory.config.BeanFactoryPostProcessor;\r\nimport org.springframework.beans.factory.config.ConfigurableListableBeanFactory;\r\nimport org.springframework.core.env.ConfigurableEnvironment;\r\nimport org.springframework.core.io.ProtocolResolver;\r\nimport org.springframework.lang.Nullable;\r\n\r\npublic interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {\r\nvoid close();\r\n\r\n}\r\n</code></pre>\r\n<p>继续看官方源码，<strong>AbstractApplicationContext</strong>类中实现<strong>close()</strong>方法，下面是实现类中的方法摘要：</p>\r\n<pre><code class=\"lang-java\">public void close() {\r\n        Object var1 = this.startupShutdownMonitor;\r\n        synchronized(this.startupShutdownMonitor) {\r\n            this.doClose();\r\n            if (this.shutdownHook != null) {\r\n                try {\r\n                    Runtime.getRuntime().removeShutdownHook(this.shutdownHook);\r\n                } catch (IllegalStateException var4) {\r\n                    ;\r\n                }\r\n            }\r\n\r\n        }\r\n    }\r\n</code></pre>\r\n<p><strong>#close()</strong>方法将会调用#doClose()方法，我们再来看看#doClose()方法做了哪些操作，下面是<strong>doClose()</strong>方法的摘要：</p>\r\n<pre><code class=\"lang-java\">protected void doClose() {\r\n        if (this.active.get() &amp;&amp; this.closed.compareAndSet(false, true)) {\r\n\r\n            ...\r\n\r\n            LiveBeansView.unregisterApplicationContext(this);\r\n\r\n            ...\r\n\r\n            this.destroyBeans();\r\n            this.closeBeanFactory();\r\n            this.onClose();\r\n            if (this.earlyApplicationListeners != null) {\r\n                this.applicationListeners.clear();\r\n                this.applicationListeners.addAll(this.earlyApplicationListeners);\r\n            }\r\n\r\n            this.active.set(false);\r\n        }\r\n\r\n    }\r\n</code></pre>\r\n<p>在<strong>#doClose()</strong>方法中，首先将应用上下文从注册表中清除掉，然后是销毁Bean工厂中的Beans,紧接着关闭Bean工厂。</p>\r\n<p>官方文档看到这里，就产生了解决一个结局重启应用应用程序的大胆猜想。在应用程序的<strong>main()</strong>方法中，我们可以使用一个临时变量来存放SpringApplication.run()返回的ConfigurableApplicationContext对象，当我们完成对Spring Boot应用程序中属性的设置后，调用<strong>ConfigurableApplicationContext</strong>的<strong>#close()</strong>方法，最后再调用SpringApplication.run()方法重新给ConfigurableApplicationContext对象进行赋值已达到重启的效果。</p>\r\n<p>现在，我们再来看一下SpringApplication.run()方法中是如何重新创建ConfigurableApplicationContext对象的。在SpringApplication类中，run()方法会调用createApplicationContext()方法来创建一个ApplicationContext对象：</p>\r\n<pre><code class=\"lang-java\">protected ConfigurableApplicationContext createApplicationContext() {\r\n        Class&lt;?&gt; contextClass = this.applicationContextClass;\r\n        if (contextClass == null) {\r\n            try {\r\n                switch(this.webApplicationType) {\r\n                case SERVLET:\r\n                    contextClass = Class.forName(\"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext\");\r\n                    break;\r\n                case REACTIVE:\r\n                    contextClass = Class.forName(\"org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext\");\r\n                    break;\r\n                default:\r\n                    contextClass = Class.forName(\"org.springframework.context.annotation.AnnotationConfigApplicationContext\");\r\n                }\r\n            } catch (ClassNotFoundException var3) {\r\n                throw new IllegalStateException(\"Unable create a default ApplicationContext, please specify an ApplicationContextClass\", var3);\r\n            }\r\n        }\r\n\r\n        return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);\r\n    }\r\n</code></pre>\r\n<p><strong>createApplicationContext()</strong>方法会根据<strong>WebApplicationType</strong>类型来创建ApplicationContext对象。在<strong>WebApplicationType</strong>中定义了三种种类型：<strong>NONE</strong>、<strong>SERVLET</strong>和<strong>REACTIVE</strong>。通常情况下，将会创建servlet类型的ApplicationContext对象。</p>\r\n<p>接下来，我将以一个简单的Spring Boot工程来验证上述的猜想是否能够达到重启Spring Boot应用程序的需求。</p>\r\n<h2 id=\"h2-u7F16u7801u5B9Eu73B0\"><a name=\"编码实现\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>编码实现</h2><p>首先，在application.properties文件中加入如下的配置信息，为动态修改配置信息提供数据：</p>\r\n<pre><code class=\"lang-properties\">spring.application.name= SPRING-BOOT-APPLICATION\r\n</code></pre>\r\n<p>接下来，在Spring Boot主类中定义两个私有变量，用于存放main()方法的参数和SpringApplication.run()方法返回的值。下面的代码给出了主类的示例：</p>\r\n<pre><code class=\"lang-java\">public class ExampleRestartApplication {\r\n\r\n    @Value ( \"${spring.application.name}\" )\r\n    String appName;\r\n\r\n    private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class );\r\n\r\n    private static String[] args;\r\n    private static ConfigurableApplicationContext context;\r\n\r\n    public static void main(String[] args) {\r\n        ExampleRestartApplication.args = args;\r\n        ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args);\r\n    }\r\n}\r\n</code></pre>\r\n<p>最后，直接在主类中定义用于刷新并重启Spring Boot应用程序的端点(Endpoint)，并使用<strong><a href=\"https://github.com/RestController\" title=\"@RestController\" class=\"at-link\">@RestController</a></strong>注解对主类进行注释。</p>\r\n<pre><code class=\"lang-java\">@GetMapping(\"/refresh\")\r\npublic String restart(){\r\n    logger.info ( \"spring.application.name:\"+appName);\r\n    try {\r\n        PropUtil.init ().write ( \"spring.application.name\",\"SPRING-DYNAMIC-SERVER\" );\r\n    } catch (IOException e) {\r\n        e.printStackTrace ( );\r\n    }\r\n\r\n    ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue&lt;&gt; ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ());\r\n    threadPool.execute (()-&gt;{\r\n        context.close ();\r\n        context = SpringApplication.run ( ExampleRestartApplication.class,args );\r\n    } );\r\n    threadPool.shutdown ();\r\n    return \"spring.application.name:\"+appName;\r\n}\r\n</code></pre>\r\n<blockquote>\r\n<p>说明：为了能够重新启动Spring Boot应用程序，需要将close()和run()方法放在一个独立的线程中执行。</p>\r\n</blockquote>\r\n<p>为了验证Spring Boot应用程序在被修改重启有相关的属性有没有生效，再添加一个获取属性信息的端点，返回配置属性的信息。</p>\r\n<pre><code class=\"lang-java\">@GetMapping(\"/info\")\r\npublic String info(){\r\n    logger.info ( \"spring.application.name:\"+appName);\r\n    return appName;\r\n}\r\n</code></pre>\r\n<h2 id=\"h2-u5B8Cu6574u7684u4EE3u7801\"><a name=\"完整的代码\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>完整的代码</h2><p>下面给出了主类的全部代码：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.application;\r\n\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.context.ConfigurableApplicationContext;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\nimport java.io.IOException;\r\nimport java.util.concurrent.*;\r\n\r\n/**\r\n * @author ramostear\r\n */\r\n@SpringBootApplication\r\n@RestController\r\npublic class ExampleRestartApplication {\r\n\r\n    @Value ( \"${spring.application.name}\" )\r\n    String appName;\r\n\r\n    private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class );\r\n\r\n    private static String[] args;\r\n    private static ConfigurableApplicationContext context;\r\n\r\n    public static void main(String[] args) {\r\n        ExampleRestartApplication.args = args;\r\n        ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args);\r\n    }\r\n\r\n    @GetMapping(\"/refresh\")\r\n    public String restart(){\r\n        logger.info ( \"spring.application.name:\"+appName);\r\n        try {\r\n            PropUtil.init ().write ( \"spring.application.name\",\"SPRING-DYNAMIC-SERVER\" );\r\n        } catch (IOException e) {\r\n            e.printStackTrace ( );\r\n        }\r\n\r\n        ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue&lt;&gt; ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ());\r\n        threadPool.execute (()-&gt;{\r\n            context.close ();\r\n            context = SpringApplication.run ( ExampleRestartApplication.class,args );\r\n        } );\r\n        threadPool.shutdown ();\r\n        return \"spring.application.name:\"+appName;\r\n    }\r\n\r\n    @GetMapping(\"/info\")\r\n    public String info(){\r\n        logger.info ( \"spring.application.name:\"+appName);\r\n        return appName;\r\n    }\r\n}\r\n</code></pre>\r\n<p>接下来，运行Spring Boot程序，下面是应用程序启动成功后控制台输出的日志信息：</p>\r\n<pre><code class=\"lang-tex\">[2019-03-12T19:05:53.053z][org.springframework.scheduling.concurrent.ExecutorConfigurationSupport][main][171][INFO ] Initializing ExecutorService \'applicationTaskExecutor\'\r\n[2019-03-12T19:05:53.053z][org.apache.juli.logging.DirectJDKLog][main][173][INFO ] Starting ProtocolHandler [\"http-nio-8080\"]\r\n[2019-03-12T19:05:53.053z][org.springframework.boot.web.embedded.tomcat.TomcatWebServer][main][204][INFO ] Tomcat started on port(s): 8080 (http) with context path \'\'\r\n[2019-03-12T19:05:53.053z][org.springframework.boot.StartupInfoLogger][main][59][INFO ] Started ExampleRestartApplication in 1.587 seconds (JVM running for 2.058)\r\n</code></pre>\r\n<p>在测试修改系统配置并重启之前，使用Postman测试工具访问：<a href=\"http://localhost:8080/info\">http://localhost:8080/info</a> ，查看一下返回的信息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-12-19-50-27-c2194858b5cf4caf8718b19691a02049.png\" alt=\"\">\r\n<p>成功返回<strong>SPRING-BOOT-APPLICATION</strong>提示信息。</p>\r\n<p>然后，访问：<a href=\"http://localhost:8080/refresh\">http://localhost:8080/refresh</a> ，设置应用应用程序<strong>spring.application.name</strong>的值为<strong>SPRING-DYNAMIC-SERVER</strong>，观察控制台输出的日志信息：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-12-19-50-39-0e48b7f05cd8427985646b9d133b53ce.png\" alt=\"\">\r\n<p>可以看到，Spring Boot应用程序已经重新启动成功，最后，在此访问：<a href=\"http://localhost:8080/info\">http://localhost:8080/info</a> ,验证之前的修改是否生效：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-12-19-50-51-fbb61e0691a7438c868dbbfda90c99b5.png\" alt=\"\">\r\n<p>请求成功返回了<strong>SPRING-DYNAMIC-SERVER</strong>信息，最后在看一眼application.properties文件中的配置信息是否真的被修改了：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-03-12-19-51-02-ecbec9f0421f4f9ebe153da9358e1d5e.png\" alt=\"\">\r\n<p>配置文件的属性也被成功的修改，证明之前的猜想验证成功了。</p>\r\n<blockquote>\r\n<p>本次内容所描述的方法不适用于以JAR文件启动的Spring Boot应用程序，以WAR包的方式启动应用程序亲测可用。┏ (^ω^)=☞目前该药方副作用未知，如有大牛路过，还望留步指点迷津，不胜感激。</p>\r\n</blockquote>\r\n<h2 id=\"h2-u7ED3u675Fu8BED\"><a name=\"结束语\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>结束语</h2><p>本次内容记录了自己验证HTTP请求重启Spring Boot应用程序试验的一次经历，文章中所涉及到的内容仅代表个人的一些观点和不成熟的想法，并未将此方法应用到实际的项目中去，如因引用本次内容中的方法应用到实际生产开发工作中所带来的风险，需引用者自行承担因风险带来的后遗症(๑￫ܫ￩)——此药方还有待商榷(O_o)(o_O)。</p>\r\n','SpringBoot',0,'# 通过HTTP重启Spring Boot应用程序\r\n\r\n## 需求背景\r\n\r\n在一个很奇葩的需求下，要求在客户端动态修改Spring Boot配置文件中的属性，例如端口号、应用名称、数据库连接信息等，然后通过一个Http请求重启Spring Boot程序。这个需求类似于操作系统更新配置后需要进行重启系统才能生效的应用场景。\r\n\r\n动态配置系统并更新生效是应用的一种通用性需求，实现的方式也有很多种。例如监听配置文件变化、使用配置中心等等。网络上也有很多类似的教程存在，但大多数都是在开发阶段，借助Spring Boot DevTools插件实现应用程序的重启，或者是使用spring-boot-starter-actuator和spring-cloud-starter-config来提供端点（Endpoint）的刷新。\r\n\r\n> 第一种方式无法在生产环境中使用（不考虑），第二种方式需要引入Spring Cloud相关内容，这无疑是杀鸡用了宰牛刀。\r\n\r\n接下来，我将尝试采用另外一种方式实现HTTP请求重启Spring Boot应用程序这个怪异的需求。\r\n\r\n\r\n\r\n## 尝试思路\r\n\r\n重启Spring Boot应用程序的关键步骤是对主类中**SpringApplication.run(Application.class,args);**方法返回值的处理。**SpringApplication#run()**方法将会返回一个**ConfigurableApplicationContext**类型对象，通过查看官方文档可以看到，**ConfigurableApplicationContext**接口类中定义了一个**close()**方法，可以用来关闭当前应用的上下文：\r\n\r\n```java\r\npackage org.springframework.context;\r\n\r\nimport java.io.Closeable;\r\nimport org.springframework.beans.BeansException;\r\nimport org.springframework.beans.factory.config.BeanFactoryPostProcessor;\r\nimport org.springframework.beans.factory.config.ConfigurableListableBeanFactory;\r\nimport org.springframework.core.env.ConfigurableEnvironment;\r\nimport org.springframework.core.io.ProtocolResolver;\r\nimport org.springframework.lang.Nullable;\r\n\r\npublic interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {\r\nvoid close();\r\n    \r\n}    \r\n```\r\n\r\n继续看官方源码，**AbstractApplicationContext**类中实现**close()**方法，下面是实现类中的方法摘要：\r\n\r\n```java\r\npublic void close() {\r\n        Object var1 = this.startupShutdownMonitor;\r\n        synchronized(this.startupShutdownMonitor) {\r\n            this.doClose();\r\n            if (this.shutdownHook != null) {\r\n                try {\r\n                    Runtime.getRuntime().removeShutdownHook(this.shutdownHook);\r\n                } catch (IllegalStateException var4) {\r\n                    ;\r\n                }\r\n            }\r\n\r\n        }\r\n    }\r\n```\r\n\r\n**#close()**方法将会调用#doClose()方法，我们再来看看#doClose()方法做了哪些操作，下面是**doClose()**方法的摘要：\r\n\r\n```java\r\nprotected void doClose() {\r\n        if (this.active.get() && this.closed.compareAndSet(false, true)) {\r\n            \r\n            ...\r\n            \r\n            LiveBeansView.unregisterApplicationContext(this);\r\n			\r\n            ...\r\n\r\n            this.destroyBeans();\r\n            this.closeBeanFactory();\r\n            this.onClose();\r\n            if (this.earlyApplicationListeners != null) {\r\n                this.applicationListeners.clear();\r\n                this.applicationListeners.addAll(this.earlyApplicationListeners);\r\n            }\r\n\r\n            this.active.set(false);\r\n        }\r\n\r\n    }\r\n```\r\n\r\n在**#doClose()**方法中，首先将应用上下文从注册表中清除掉，然后是销毁Bean工厂中的Beans,紧接着关闭Bean工厂。\r\n\r\n官方文档看到这里，就产生了解决一个结局重启应用应用程序的大胆猜想。在应用程序的**main()**方法中，我们可以使用一个临时变量来存放SpringApplication.run()返回的ConfigurableApplicationContext对象，当我们完成对Spring Boot应用程序中属性的设置后，调用**ConfigurableApplicationContext**的**#close()**方法，最后再调用SpringApplication.run()方法重新给ConfigurableApplicationContext对象进行赋值已达到重启的效果。\r\n\r\n现在，我们再来看一下SpringApplication.run()方法中是如何重新创建ConfigurableApplicationContext对象的。在SpringApplication类中，run()方法会调用createApplicationContext()方法来创建一个ApplicationContext对象：\r\n\r\n```java\r\nprotected ConfigurableApplicationContext createApplicationContext() {\r\n        Class<?> contextClass = this.applicationContextClass;\r\n        if (contextClass == null) {\r\n            try {\r\n                switch(this.webApplicationType) {\r\n                case SERVLET:\r\n                    contextClass = Class.forName(\"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext\");\r\n                    break;\r\n                case REACTIVE:\r\n                    contextClass = Class.forName(\"org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext\");\r\n                    break;\r\n                default:\r\n                    contextClass = Class.forName(\"org.springframework.context.annotation.AnnotationConfigApplicationContext\");\r\n                }\r\n            } catch (ClassNotFoundException var3) {\r\n                throw new IllegalStateException(\"Unable create a default ApplicationContext, please specify an ApplicationContextClass\", var3);\r\n            }\r\n        }\r\n\r\n        return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);\r\n    }\r\n```\r\n\r\n**createApplicationContext()**方法会根据**WebApplicationType**类型来创建ApplicationContext对象。在**WebApplicationType**中定义了三种种类型：**NONE**、**SERVLET**和**REACTIVE**。通常情况下，将会创建servlet类型的ApplicationContext对象。\r\n\r\n接下来，我将以一个简单的Spring Boot工程来验证上述的猜想是否能够达到重启Spring Boot应用程序的需求。\r\n\r\n\r\n\r\n## 编码实现\r\n\r\n首先，在application.properties文件中加入如下的配置信息，为动态修改配置信息提供数据：\r\n\r\n```properties\r\nspring.application.name= SPRING-BOOT-APPLICATION\r\n```\r\n\r\n接下来，在Spring Boot主类中定义两个私有变量，用于存放main()方法的参数和SpringApplication.run()方法返回的值。下面的代码给出了主类的示例：\r\n\r\n```java\r\npublic class ExampleRestartApplication {\r\n\r\n	@Value ( \"${spring.application.name}\" )\r\n	String appName;\r\n\r\n	private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class );\r\n\r\n	private static String[] args;\r\n	private static ConfigurableApplicationContext context;\r\n\r\n	public static void main(String[] args) {\r\n		ExampleRestartApplication.args = args;\r\n		ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args);\r\n	}\r\n}\r\n```\r\n\r\n最后，直接在主类中定义用于刷新并重启Spring Boot应用程序的端点(Endpoint)，并使用**@RestController**注解对主类进行注释。\r\n\r\n```java\r\n@GetMapping(\"/refresh\")\r\npublic String restart(){\r\n    logger.info ( \"spring.application.name:\"+appName);\r\n    try {\r\n        PropUtil.init ().write ( \"spring.application.name\",\"SPRING-DYNAMIC-SERVER\" );\r\n    } catch (IOException e) {\r\n        e.printStackTrace ( );\r\n    }\r\n\r\n    ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<> ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ());\r\n    threadPool.execute (()->{\r\n        context.close ();\r\n        context = SpringApplication.run ( ExampleRestartApplication.class,args );\r\n    } );\r\n    threadPool.shutdown ();\r\n    return \"spring.application.name:\"+appName;\r\n}\r\n```\r\n\r\n> 说明：为了能够重新启动Spring Boot应用程序，需要将close()和run()方法放在一个独立的线程中执行。\r\n\r\n为了验证Spring Boot应用程序在被修改重启有相关的属性有没有生效，再添加一个获取属性信息的端点，返回配置属性的信息。\r\n\r\n```java\r\n@GetMapping(\"/info\")\r\npublic String info(){\r\n    logger.info ( \"spring.application.name:\"+appName);\r\n    return appName;\r\n}\r\n```\r\n\r\n\r\n\r\n## 完整的代码\r\n\r\n下面给出了主类的全部代码：\r\n\r\n```java\r\npackage com.ramostear.application;\r\n\r\nimport org.slf4j.Logger;\r\nimport org.slf4j.LoggerFactory;\r\nimport org.springframework.beans.factory.annotation.Value;\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\nimport org.springframework.context.ConfigurableApplicationContext;\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\nimport java.io.IOException;\r\nimport java.util.concurrent.*;\r\n\r\n/**\r\n * @author ramostear\r\n */\r\n@SpringBootApplication\r\n@RestController\r\npublic class ExampleRestartApplication {\r\n\r\n	@Value ( \"${spring.application.name}\" )\r\n	String appName;\r\n\r\n	private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class );\r\n\r\n	private static String[] args;\r\n	private static ConfigurableApplicationContext context;\r\n\r\n	public static void main(String[] args) {\r\n		ExampleRestartApplication.args = args;\r\n		ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args);\r\n	}\r\n\r\n	@GetMapping(\"/refresh\")\r\n	public String restart(){\r\n		logger.info ( \"spring.application.name:\"+appName);\r\n		try {\r\n			PropUtil.init ().write ( \"spring.application.name\",\"SPRING-DYNAMIC-SERVER\" );\r\n		} catch (IOException e) {\r\n			e.printStackTrace ( );\r\n		}\r\n\r\n		ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<> ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ());\r\n		threadPool.execute (()->{\r\n			context.close ();\r\n			context = SpringApplication.run ( ExampleRestartApplication.class,args );\r\n		} );\r\n		threadPool.shutdown ();\r\n		return \"spring.application.name:\"+appName;\r\n	}\r\n\r\n	@GetMapping(\"/info\")\r\n	public String info(){\r\n		logger.info ( \"spring.application.name:\"+appName);\r\n		return appName;\r\n	}\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n接下来，运行Spring Boot程序，下面是应用程序启动成功后控制台输出的日志信息：\r\n\r\n```tex\r\n[2019-03-12T19:05:53.053z][org.springframework.scheduling.concurrent.ExecutorConfigurationSupport][main][171][INFO ] Initializing ExecutorService \'applicationTaskExecutor\'\r\n[2019-03-12T19:05:53.053z][org.apache.juli.logging.DirectJDKLog][main][173][INFO ] Starting ProtocolHandler [\"http-nio-8080\"]\r\n[2019-03-12T19:05:53.053z][org.springframework.boot.web.embedded.tomcat.TomcatWebServer][main][204][INFO ] Tomcat started on port(s): 8080 (http) with context path \'\'\r\n[2019-03-12T19:05:53.053z][org.springframework.boot.StartupInfoLogger][main][59][INFO ] Started ExampleRestartApplication in 1.587 seconds (JVM running for 2.058)\r\n```\r\n\r\n在测试修改系统配置并重启之前，使用Postman测试工具访问：http://localhost:8080/info ，查看一下返回的信息：\r\n\r\n![](https://cdn.ramostear.com/2019-03-12-19-50-27-c2194858b5cf4caf8718b19691a02049.png)\r\n\r\n成功返回**SPRING-BOOT-APPLICATION**提示信息。\r\n\r\n然后，访问：http://localhost:8080/refresh ，设置应用应用程序**spring.application.name**的值为**SPRING-DYNAMIC-SERVER**，观察控制台输出的日志信息：\r\n\r\n![](https://cdn.ramostear.com/2019-03-12-19-50-39-0e48b7f05cd8427985646b9d133b53ce.png)\r\n\r\n可以看到，Spring Boot应用程序已经重新启动成功，最后，在此访问：http://localhost:8080/info ,验证之前的修改是否生效：\r\n\r\n![](https://cdn.ramostear.com/2019-03-12-19-50-51-fbb61e0691a7438c868dbbfda90c99b5.png)\r\n\r\n请求成功返回了**SPRING-DYNAMIC-SERVER**信息，最后在看一眼application.properties文件中的配置信息是否真的被修改了：\r\n\r\n![](https://cdn.ramostear.com/2019-03-12-19-51-02-ecbec9f0421f4f9ebe153da9358e1d5e.png)\r\n\r\n配置文件的属性也被成功的修改，证明之前的猜想验证成功了。\r\n\r\n> 本次内容所描述的方法不适用于以JAR文件启动的Spring Boot应用程序，以WAR包的方式启动应用程序亲测可用。┏ (^ω^)=☞目前该药方副作用未知，如有大牛路过，还望留步指点迷津，不胜感激。\r\n\r\n## 结束语\r\n\r\n本次内容记录了自己验证HTTP请求重启Spring Boot应用程序试验的一次经历，文章中所涉及到的内容仅代表个人的一些观点和不成熟的想法，并未将此方法应用到实际的项目中去，如因引用本次内容中的方法应用到实际生产开发工作中所带来的风险，需引用者自行承担因风险带来的后遗症(๑￫ܫ￩)——此药方还有待商榷(O_o)(o_O)。\r\n\r\n\r\n\r\n',1,'','2019/16/21/39k9vuha.html',1,'在运行时重新启动SpringBoot应用程序','blog.html','https://cdn.ramostear.com/2019-03-28-02-52-54-4125b91bb51c41209c2bb01c27c16b2a.png;base64','在生产环境中重启Spring Boot应用','',2875),(15,'2019-11-21 23:20:22',0,'2019-11-21 23:32:15',1,1,1,'谭朝红','SaaS是这样一种架构模式：它让多个不同环境的用户使用同一套应用程序，且保证用户之间的数据相互隔离','<blockquote>\r\n<p>本次教程所涉及到的源码已上传至<a href=\"https://github.com/ramostear/una-saas-toturial\">Github</a>,如果你不需要继续阅读下面的内容，你可以直接点击此链接获取源码内容。<a href=\"https://github.com/ramostear/una-saas-toturial\">https://github.com/ramostear/una-saas-toturial</a></p>\r\n</blockquote>\r\n<h2 id=\"h2-1-\"><a name=\"1. 概述\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1. 概述</h2><p>笔者从2014年开始接触SaaS（Software as a Service），即多租户（或多承租）软件应用平台；并一直从事相关领域的架构设计及研发工作。机缘巧合，在笔者本科毕业设计时完成了一个基于SaaS的高效财务管理平台的课题研究，从中收获颇多。最早接触SaaS时，国内相关资源匮乏，唯一有的参照资料是《互联网时代的软件革命：SaaS架构设计》（叶伟等著）一书。最后课题的实现是基于OSGI（Open Service Gateway Initiative）Java动态模块化系统规范来实现的。</p>\r\n<p>时至今日，五年的时间过去了，软件开发的技术发生了巨大的改变，笔者所实现SaaS平台的技术栈也更新了好几波，真是印证了那就话：“山重水尽疑无路，柳暗花明又一村”。基于之前走过的许多弯路和踩过的坑，以及近段时间有许多网友问我如何使用Spring Boot实现多租户系统，决定写一篇文章聊一聊关于SaaS的硬核技术。</p>\r\n<p>说起SaaS，它只是一种软件架构，并没有多少神秘的东西，也不是什么很难的系统，我个人的感觉，SaaS平台的难度在于商业上的运营，而非技术上的实现。就技术上来说，SaaS是这样一种架构模式：它让多个不同环境的用户使用同一套应用程序，且保证用户之间的数据相互隔离。现在想想看，这也有点共享经济的味道在里面。</p>\r\n<p>笔者在这里就不再深入聊SaaS软件成熟度模型和数据隔离方案对比的事情了。今天要聊的是使用Spring Boot快速构建独立数据库/共享数据库独立Schema的多租户系统。我将提供一个SaaS系统最核心的技术实现，而其他的部分有兴趣的朋友可以在此基础上自行扩展。</p>\r\n<h2 id=\"h2-2-\"><a name=\"2. 尝试了解多租户的应用场景\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2. 尝试了解多租户的应用场景</h2><p>假设我们需要开发一个应用程序，并且希望将同一个应用程序销售给N家客户使用。在常规情况下，我们需要为此创建N个Web服务器（Tomcat）,N个数据库（DB），并为N个客户部署相同的应用程序N次。现在，如果我们的应用程序进行了升级或者做了其他任何的改动，那么我们就需要更新N个应用程序同时还需要维护N台服务器。接下来，如果业务开始增长，客户由原来的N个变成了现在的N+M个，我们将面临N个应用程序和M个应用程序版本维护，设备维护以及成本控制的问题。运维几乎要哭死在机房了…</p>\r\n<p>为了解决上述的问题，我们可以开发多租户应用程序，我们可以根据当前用户是谁，从而选择对应的数据库。例如，当请求来自A公司的用户时，应用程序就连接A公司的数据库，当请求来自B公司的用户时，自动将数据库切换到B公司数据库，以此类推。从理论上将没有什么问题，但我们如果考虑将现有的应用程序改造成SaaS模式，我们将遇到第一个问题：如果识别请求来自哪一个租户？如何自动切换数据源？</p>\r\n<h2 id=\"h2-3-\"><a name=\"3. 维护、识别和路由租户数据源\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3. 维护、识别和路由租户数据源</h2><p>我们可以提供一个独立的库来存放租户信息，如数据库名称、链接地址、用户名、密码等，这可以统一的解决租户信息维护的问题。租户的识别和路由有很多种方法可以解决，下面列举几个常用的方式：</p>\r\n<ul>\r\n<li>1.可以通过域名的方式来识别租户：我们可以为每一个租户提供一个唯一的二级域名，通过二级域名就可以达到识别租户的能力，如tenantone.example.com,tenant.example.com；tenantone和tenant就是我们识别租户的关键信息。</li><li>2.可以将租户信息作为请求参数传递给服务端，为服务端识别租户提供支持，如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的参数tenantId就是应用程序识别租户的关键信息。</li><li>3.可以在请求头（Header）中设置租户信息，例如JWT等技术，服务端通过解析Header中相关参数以获得租户信息。</li><li>4.在用户成功登录系统后，将租户信息保存在Session中，在需要的时候从Session取出租户信息。</li></ul>\r\n<p>解决了上述问题后，我们再来看看如何获取客户端传入的租户信息，以及在我们的业务代码中如何使用租户信息（最关键的是DataSources的问题）。</p>\r\n<p>我们都知道，在启动Spring Boot应用程序之前，就需要为其提供有关数据源的配置信息（有使用到数据库的情况下）,按照一开始的需求，有N个客户需要使用我们的应用程序，我们就需要提前配置好N个数据源（多数据源）,如果N&lt;50,我认为我还能忍受，如果更多，这样显然是无法接受的。为了解决这一问题，我们需要借助Hibernate 5提供的动态数据源特性，让我们的应用程序具备动态配置客户端数据源的能力。简单来说，当用户请求系统资源时，我们将用户提供的租户信息（tenantId）存放在ThreadLoacal中，紧接着获取TheadLocal中的租户信息，并根据此信息查询单独的租户库，获取当前租户的数据配置信息，然后借助Hibernate动态配置数据源的能力，为当前请求设置数据源，最后之前用户的请求。这样我们就只需要在应用程序中维护一份数据源配置信息（租户数据库配置库），其余的数据源动态查询配置。接下来，我们将快速的演示这一功能。</p>\r\n<h2 id=\"h2-4-\"><a name=\"4. 项目构建\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4. 项目构建</h2><p>我们将使用Spring Boot 2.1.5版本来实现这一演示项目，首先你需要在Maven配置文件中加入如下的一些配置：</p>\r\n<pre><code class=\"lang-xml\">&lt;dependencies&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;\r\n            &lt;scope&gt;runtime&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;\r\n            &lt;artifactId&gt;lombok&lt;/artifactId&gt;\r\n            &lt;optional&gt;true&lt;/optional&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\r\n            &lt;scope&gt;test&lt;/scope&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;mysql&lt;/groupId&gt;\r\n            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;\r\n            &lt;version&gt;5.1.47&lt;/version&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\r\n            &lt;artifactId&gt;spring-boot-starter-freemarker&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n        &lt;dependency&gt;\r\n            &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;\r\n            &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;\r\n        &lt;/dependency&gt;\r\n    &lt;/dependencies&gt;\r\n</code></pre>\r\n<p>然后提供一个可用的配置文件，并加入如下的内容：</p>\r\n<pre><code class=\"lang-yaml\">spring:\r\n  freemarker:\r\n    cache: false\r\n    template-loader-path:\r\n    - classpath:/templates/\r\n    prefix:\r\n    suffix: .html\r\n  resources:\r\n    static-locations:\r\n    - classpath:/static/\r\n  devtools:\r\n    restart:\r\n      enabled: true\r\n  jpa:\r\n    database: mysql\r\n    show-sql: true\r\n    generate-ddl: false\r\n    hibernate:\r\n      ddl-auto: none\r\nuna:\r\n  master:\r\n    datasource:\r\n      url:  jdbc:mysql://localhost:3306/master_tenant?useSSL=false\r\n      username: root\r\n      password: root\r\n      driverClassName:  com.mysql.jdbc.Driver\r\n      maxPoolSize:  10\r\n      idleTimeout:  300000\r\n      minIdle:  10\r\n      poolName: master-database-connection-pool\r\nlogging:\r\n  level:\r\n    root: warn\r\n    org:\r\n      springframework:\r\n        web:  debug\r\n      hibernate: debug\r\n</code></pre>\r\n<blockquote>\r\n<p>由于采用Freemarker作为视图渲染引擎，所以需要提供Freemarker的相关技术</p>\r\n<p>una<img src=\"http://www.emoji-cheat-sheet.com/graphics/emojis/master.png\" class=\"emoji\" title=\":master:\" alt=\":master:\">datasource配置项就是上面说的统一存放租户信息的数据源配置信息，你可以理解为主库。\r\n</blockquote>\r\n<p>接下来，我们需要关闭Spring Boot自动配置数据源的功能，在项目主类上添加如下的设置：</p>\r\n<pre><code class=\"lang-java\">@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})\r\npublic class UnaSaasApplication {\r\n\r\n    public static void main(String[] args) {\r\n        SpringApplication.run(UnaSaasApplication.class, args);\r\n    }\r\n\r\n}\r\n</code></pre>\r\n<p>最后，让我们看看整个项目的结构：<br><img src=\"https://cdn.ramostear.com/2019-05-27-05-00-05-3252d32bc9eb44c1b0375f4565a915b0.png\" alt=\"\">\r\n<h2 id=\"h2-5-\"><a name=\"5. 实现租户数据源查询模块\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5. 实现租户数据源查询模块</h2><p>我们将定义一个实体类存放租户数据源信息，它包含了租户名，数据库连接地址，用户名和密码等信息，其代码如下：</p>\r\n<pre><code class=\"lang-java\">@Data\r\n@Entity\r\n@Table(name = \"MASTER_TENANT\")\r\n@NoArgsConstructor\r\n@AllArgsConstructor\r\n@Builder\r\npublic class MasterTenant implements Serializable{\r\n\r\n    @Id\r\n    @Column(name=\"ID\")\r\n    private String id;\r\n\r\n    @Column(name = \"TENANT\")\r\n    @NotEmpty(message = \"Tenant identifier must be provided\")\r\n    private String tenant;\r\n\r\n    @Column(name = \"URL\")\r\n    @Size(max = 256)\r\n    @NotEmpty(message = \"Tenant jdbc url must be provided\")\r\n    private String url;\r\n\r\n    @Column(name = \"USERNAME\")\r\n    @Size(min = 4,max = 30,message = \"db username length must between 4 and 30\")\r\n    @NotEmpty(message = \"Tenant db username must be provided\")\r\n    private String username;\r\n\r\n    @Column(name = \"PASSWORD\")\r\n    @Size(min = 4,max = 30)\r\n    @NotEmpty(message = \"Tenant db password must be provided\")\r\n    private String password;\r\n\r\n    @Version\r\n    private int version = 0;\r\n}\r\n</code></pre>\r\n<p>持久层我们将继承JpaRepository接口，快速实现对数据源的CURD操作，同时提供了一个通过租户名查找租户数据源的接口，其代码如下：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.una.saas.master.repository;\r\n\r\nimport com.ramostear.una.saas.master.model.MasterTenant;\r\nimport org.springframework.data.jpa.repository.JpaRepository;\r\nimport org.springframework.data.jpa.repository.Query;\r\nimport org.springframework.data.repository.query.Param;\r\nimport org.springframework.stereotype.Repository;\r\n\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/25 0025-8:22\r\n * @modify by :\r\n * @since:\r\n */\r\n@Repository\r\npublic interface MasterTenantRepository extends JpaRepository&lt;MasterTenant,String&gt;{\r\n\r\n    @Query(\"select p from MasterTenant p where p.tenant = :tenant\")\r\n    MasterTenant findByTenant(@Param(\"tenant\") String tenant);\r\n}\r\n</code></pre>\r\n<p>业务层提供通过租户名获取租户数据源信息的服务（其余的服务各位可自行添加）：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.una.saas.master.service;\r\n\r\nimport com.ramostear.una.saas.master.model.MasterTenant;\r\n\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/25 0025-8:26\r\n * @modify by :\r\n * @since:\r\n */\r\n\r\npublic interface MasterTenantService {\r\n    /**\r\n     * Using custom tenant name query\r\n     * @param tenant    tenant name\r\n     * @return          masterTenant\r\n     */\r\n    MasterTenant findByTenant(String tenant);\r\n}\r\n</code></pre>\r\n<p>最后，我们需要关注的重点是配置主数据源（Spring Boot需要为其提供一个默认的数据源）。在配置之前，我们需要获取配置项，可以通过<a href=\"https://github.com/ConfigurationProperties\" title=\"@ConfigurationProperties\" class=\"at-link\">@ConfigurationProperties</a>(“una.master.datasource”)获取配置文件中的相关配置信息：</p>\r\n<pre><code class=\"lang-java\">@Getter\r\n@Setter\r\n@Configuration\r\n@ConfigurationProperties(\"una.master.datasource\")\r\npublic class MasterDatabaseProperties {\r\n\r\n    private String url;\r\n\r\n    private String password;\r\n\r\n    private String username;\r\n\r\n    private String driverClassName;\r\n\r\n    private long connectionTimeout;\r\n\r\n    private int maxPoolSize;\r\n\r\n    private long idleTimeout;\r\n\r\n    private int minIdle;\r\n\r\n    private String poolName;\r\n\r\n    @Override\r\n    public String toString(){\r\n        StringBuilder builder = new StringBuilder();\r\n        builder.append(\"MasterDatabaseProperties [ url=\")\r\n                .append(url)\r\n                .append(\", username=\")\r\n                .append(username)\r\n                .append(\", password=\")\r\n                .append(password)\r\n                .append(\", driverClassName=\")\r\n                .append(driverClassName)\r\n                .append(\", connectionTimeout=\")\r\n                .append(connectionTimeout)\r\n                .append(\", maxPoolSize=\")\r\n                .append(maxPoolSize)\r\n                .append(\", idleTimeout=\")\r\n                .append(idleTimeout)\r\n                .append(\", minIdle=\")\r\n                .append(minIdle)\r\n                .append(\", poolName=\")\r\n                .append(poolName)\r\n                .append(\"]\");\r\n        return builder.toString();\r\n    }\r\n}\r\n</code></pre>\r\n<p>接下来是配置自定义的数据源，其源码如下：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.una.saas.master.config;\r\n\r\nimport com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties;\r\nimport com.ramostear.una.saas.master.model.MasterTenant;\r\nimport com.ramostear.una.saas.master.repository.MasterTenantRepository;\r\nimport com.zaxxer.hikari.HikariDataSource;\r\nimport lombok.extern.slf4j.Slf4j;\r\nimport org.hibernate.cfg.Environment;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.beans.factory.annotation.Qualifier;\r\nimport org.springframework.context.annotation.Bean;\r\nimport org.springframework.context.annotation.Configuration;\r\nimport org.springframework.context.annotation.Primary;\r\nimport org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;\r\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\r\nimport org.springframework.orm.jpa.JpaTransactionManager;\r\nimport org.springframework.orm.jpa.JpaVendorAdapter;\r\nimport org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;\r\nimport org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;\r\nimport org.springframework.transaction.annotation.EnableTransactionManagement;\r\n\r\nimport javax.persistence.EntityManagerFactory;\r\nimport javax.sql.DataSource;\r\nimport java.util.Properties;\r\n\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/25 0025-8:31\r\n * @modify by :\r\n * @since:\r\n */\r\n@Configuration\r\n@EnableTransactionManagement\r\n@EnableJpaRepositories(basePackages = {\"com.ramostear.una.saas.master.model\",\"com.ramostear.una.saas.master.repository\"},\r\n                       entityManagerFactoryRef = \"masterEntityManagerFactory\",\r\n                       transactionManagerRef = \"masterTransactionManager\")\r\n@Slf4j\r\npublic class MasterDatabaseConfig {\r\n\r\n    @Autowired\r\n    private MasterDatabaseProperties masterDatabaseProperties;\r\n\r\n    @Bean(name = \"masterDatasource\")\r\n    public DataSource masterDatasource(){\r\n        log.info(\"Setting up masterDatasource with :{}\",masterDatabaseProperties.toString());\r\n        HikariDataSource datasource = new HikariDataSource();\r\n        datasource.setUsername(masterDatabaseProperties.getUsername());\r\n        datasource.setPassword(masterDatabaseProperties.getPassword());\r\n        datasource.setJdbcUrl(masterDatabaseProperties.getUrl());\r\n        datasource.setDriverClassName(masterDatabaseProperties.getDriverClassName());\r\n        datasource.setPoolName(masterDatabaseProperties.getPoolName());\r\n        datasource.setMaximumPoolSize(masterDatabaseProperties.getMaxPoolSize());\r\n        datasource.setMinimumIdle(masterDatabaseProperties.getMinIdle());\r\n        datasource.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout());\r\n        datasource.setIdleTimeout(masterDatabaseProperties.getIdleTimeout());\r\n        log.info(\"Setup of masterDatasource successfully.\");\r\n        return datasource;\r\n    }\r\n\r\n    @Primary\r\n    @Bean(name = \"masterEntityManagerFactory\")\r\n    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){\r\n        LocalContainerEntityManagerFactoryBean lb = new LocalContainerEntityManagerFactoryBean();\r\n        lb.setDataSource(masterDatasource());\r\n        lb.setPackagesToScan(\r\n           new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()}\r\n        );\r\n\r\n        //Setting a name for the persistence unit as Spring sets it as \'default\' if not defined.\r\n        lb.setPersistenceUnitName(\"master-database-persistence-unit\");\r\n\r\n        //Setting Hibernate as the JPA provider.\r\n        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();\r\n        lb.setJpaVendorAdapter(vendorAdapter);\r\n\r\n        //Setting the hibernate properties\r\n        lb.setJpaProperties(hibernateProperties());\r\n\r\n        log.info(\"Setup of masterEntityManagerFactory successfully.\");\r\n        return lb;\r\n    }\r\n\r\n    @Bean(name = \"masterTransactionManager\")\r\n    public JpaTransactionManager masterTransactionManager(@Qualifier(\"masterEntityManagerFactory\")EntityManagerFactory emf){\r\n        JpaTransactionManager transactionManager = new JpaTransactionManager();\r\n        transactionManager.setEntityManagerFactory(emf);\r\n        log.info(\"Setup of masterTransactionManager successfully.\");\r\n        return transactionManager;\r\n    }\r\n\r\n    @Bean\r\n    public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){\r\n        return new PersistenceExceptionTranslationPostProcessor();\r\n    }\r\n\r\n    private Properties hibernateProperties(){\r\n        Properties properties = new Properties();\r\n        properties.put(Environment.DIALECT,\"org.hibernate.dialect.MySQL5Dialect\");\r\n        properties.put(Environment.SHOW_SQL,true);\r\n        properties.put(Environment.FORMAT_SQL,true);\r\n        properties.put(Environment.HBM2DDL_AUTO,\"update\");\r\n        return properties;\r\n    }\r\n}\r\n</code></pre>\r\n<p>在改配置类中，我们主要提供包扫描路径，实体管理工程，事务管理器和数据源配置参数的配置。</p>\r\n<h2 id=\"h2-6-\"><a name=\"6. 实现租户业务模块\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6. 实现租户业务模块</h2><p>在此小节中，租户业务模块我们仅提供一个用户登录的场景来演示SaaS的功能。其实体层、业务层和持久化层根普通的Spring Boot Web项目没有什么区别，你甚至感觉不到它是一个SaaS应用程序的代码。</p>\r\n<p>首先，创建一个用户实体User，其源码如下：</p>\r\n<pre><code class=\"lang-java\">@Entity\r\n@Table(name = \"USER\")\r\n@Data\r\n@NoArgsConstructor\r\n@AllArgsConstructor\r\n@Builder\r\npublic class User implements Serializable {\r\n    private static final long serialVersionUID = -156890917814957041L;\r\n\r\n    @Id\r\n    @Column(name = \"ID\")\r\n    private String id;\r\n\r\n    @Column(name = \"USERNAME\")\r\n    private String username;\r\n\r\n    @Column(name = \"PASSWORD\")\r\n    @Size(min = 6,max = 22,message = \"User password must be provided and length between 6 and 22.\")\r\n    private String password;\r\n\r\n    @Column(name = \"TENANT\")\r\n    private String tenant;\r\n}\r\n</code></pre>\r\n<p>业务层提供了一个根据用户名检索用户信息的服务，它将调用持久层的方法根据用户名对租户的用户表进行检索，如果找到满足条件的用户记录，则返回用户信息，如果没有找到，则返回null;持久层和业务层的源码分别如下：</p>\r\n<pre><code class=\"lang-java\">@Repository\r\npublic interface UserRepository extends JpaRepository&lt;User,String&gt;,JpaSpecificationExecutor&lt;User&gt;{\r\n\r\n    User findByUsername(String username);\r\n}\r\n</code></pre>\r\n<pre><code class=\"lang-java\">@Service(\"userService\")\r\npublic class UserServiceImpl implements UserService{\r\n\r\n    @Autowired\r\n    private UserRepository userRepository;\r\n\r\n    private static TwitterIdentifier identifier = new TwitterIdentifier();\r\n\r\n\r\n\r\n    @Override\r\n    public void save(User user) {\r\n        user.setId(identifier.generalIdentifier());\r\n        user.setTenant(TenantContextHolder.getTenant());\r\n        userRepository.save(user);\r\n    }\r\n\r\n    @Override\r\n    public User findById(String userId) {\r\n        Optional&lt;User&gt; optional = userRepository.findById(userId);\r\n        if(optional.isPresent()){\r\n            return optional.get();\r\n        }else{\r\n            return null;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public User findByUsername(String username) {\r\n        System.out.println(TenantContextHolder.getTenant());\r\n        return userRepository.findByUsername(username);\r\n    }\r\n</code></pre>\r\n<blockquote>\r\n<p>在这里，我们采用了Twitter的雪花算法来实现了一个ID生成器。</p>\r\n</blockquote>\r\n<h2 id=\"h2-7-\"><a name=\"7. 配置拦截器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7. 配置拦截器</h2><p>我们需要提供一个租户信息的拦截器，用以获取租户标识符，其源代码和配置拦截器的源代码如下：</p>\r\n<pre><code class=\"lang-java\">/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/26 0026-23:17\r\n * @modify by :\r\n * @since:\r\n */\r\n@Slf4j\r\npublic class TenantInterceptor implements HandlerInterceptor{\r\n\r\n    @Override\r\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\r\n        String tenant = request.getParameter(\"tenant\");\r\n        if(StringUtils.isBlank(tenant)){\r\n            response.sendRedirect(\"/login.html\");\r\n            return false;\r\n        }else{\r\n            TenantContextHolder.setTenant(tenant);\r\n            return true;\r\n        }\r\n    }\r\n}\r\n</code></pre>\r\n<pre><code class=\"lang-java\">@Configuration\r\npublic class InterceptorConfig extends WebMvcConfigurationSupport {\r\n\r\n    @Override\r\n    protected void addInterceptors(InterceptorRegistry registry) {\r\n        registry.addInterceptor(new TenantInterceptor()).addPathPatterns(\"/**\").excludePathPatterns(\"/login.html\");\r\n        super.addInterceptors(registry);\r\n    }\r\n}\r\n</code></pre>\r\n<blockquote>\r\n<p>/login.html是系统的登录路径，我们需要将其排除在拦截器拦截的范围之外，否则我们永远无法进行登录</p>\r\n</blockquote>\r\n<h2 id=\"h2-8-\"><a name=\"8. 维护租户标识信息\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>8. 维护租户标识信息</h2><p>在这里，我们使用ThreadLocal来存放租户标识信息，为动态设置数据源提供数据支持，该类提供了设置租户标识、获取租户标识以及清除租户标识三个静态方法。其源码如下：</p>\r\n<pre><code class=\"lang-java\">public class TenantContextHolder {\r\n\r\n    private static final ThreadLocal&lt;String&gt; CONTEXT = new ThreadLocal&lt;&gt;();\r\n\r\n    public static void setTenant(String tenant){\r\n        CONTEXT.set(tenant);\r\n    }\r\n\r\n    public static String getTenant(){\r\n        return CONTEXT.get();\r\n    }\r\n\r\n    public static void clear(){\r\n        CONTEXT.remove();\r\n    }\r\n}\r\n</code></pre>\r\n<blockquote>\r\n<p>此类时实现动态数据源设置的关键</p>\r\n</blockquote>\r\n<h2 id=\"h2-9-\"><a name=\"9. 动态数据源切换\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>9. 动态数据源切换</h2><p>要实现动态数据源切换，我们需要借助两个类来完成，CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。从它们的命名上就可以看出，一个负责解析租户标识，一个负责提供租户标识对应的租户数据源信息。</p>\r\n<p>首先，我们需要实现CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法，完成租户标识的解析功能。实现类的源码如下：</p>\r\n<pre><code class=\"lang-java\">package com.ramostear.una.saas.tenant.config;\r\n\r\nimport com.ramostear.una.saas.context.TenantContextHolder;\r\nimport org.apache.commons.lang3.StringUtils;\r\nimport org.hibernate.context.spi.CurrentTenantIdentifierResolver;\r\n\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/26 0026-22:38\r\n * @modify by :\r\n * @since:\r\n */\r\npublic class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {\r\n\r\n    /**\r\n     * 默认的租户ID\r\n     */\r\n    private static final String DEFAULT_TENANT = \"tenant_1\";\r\n\r\n    /**\r\n     * 解析当前租户的ID\r\n     * @return\r\n     */\r\n    @Override\r\n    public String resolveCurrentTenantIdentifier() {\r\n        //通过租户上下文获取租户ID，此ID是用户登录时在header中进行设置的\r\n        String tenant = TenantContextHolder.getTenant();\r\n        //如果上下文中没有找到该租户ID，则使用默认的租户ID，或者直接报异常信息\r\n        return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;\r\n    }\r\n\r\n    @Override\r\n    public boolean validateExistingCurrentSessions() {\r\n        return true;\r\n    }\r\n}\r\n</code></pre>\r\n<blockquote>\r\n<p>此类的逻辑非常简单，就是从ThreadLocal中获取当前设置的租户标识符</p>\r\n</blockquote>\r\n<p>有了租户标识符解析类之后，我们需要扩展租户数据源提供类，实现从数据库动态查询租户数据源信息，其源码如下：</p>\r\n<pre><code>@Slf4j\r\n@Configuration\r\npublic class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{\r\n\r\n    private static final long serialVersionUID = -7522287771874314380L;\r\n\r\n    @Autowired\r\n    private MasterTenantRepository masterTenantRepository;\r\n\r\n    private Map&lt;String,DataSource&gt; dataSources = new TreeMap&lt;&gt;();\r\n\r\n    @Override\r\n    protected DataSource selectAnyDataSource() {\r\n        if(dataSources.isEmpty()){\r\n            List&lt;MasterTenant&gt; tenants = masterTenantRepository.findAll();\r\n            tenants.forEach(masterTenant-&gt;{\r\n                dataSources.put(masterTenant.getTenant(), DataSourceUtils.wrapperDataSource(masterTenant));\r\n            });\r\n        }\r\n        return dataSources.values().iterator().next();\r\n    }\r\n\r\n    @Override\r\n    protected DataSource selectDataSource(String tenant) {\r\n        if(!dataSources.containsKey(tenant)){\r\n            List&lt;MasterTenant&gt; tenants = masterTenantRepository.findAll();\r\n            tenants.forEach(masterTenant-&gt;{\r\n                dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant));\r\n            });\r\n        }\r\n        return dataSources.get(tenant);\r\n    }\r\n}\r\n</code></pre><blockquote>\r\n<p>在该类中，通过查询租户数据源库，动态获得租户数据源信息，为租户业务模块的数据源配置提供数据数据支持。</p>\r\n</blockquote>\r\n<p>最后，我们还需要提供租户业务模块数据源配置，这是整个项目核心的地方，其代码如下：</p>\r\n<pre><code class=\"lang-java\">@Slf4j\r\n@Configuration\r\n@EnableTransactionManagement\r\n@ComponentScan(basePackages = {\r\n        \"com.ramostear.una.saas.tenant.model\",\r\n        \"com.ramostear.una.saas.tenant.repository\"\r\n})\r\n@EnableJpaRepositories(basePackages = {\r\n        \"com.ramostear.una.saas.tenant.repository\",\r\n        \"com.ramostear.una.saas.tenant.service\"\r\n},entityManagerFactoryRef = \"tenantEntityManagerFactory\"\r\n,transactionManagerRef = \"tenantTransactionManager\")\r\npublic class TenantDataSourceConfig {\r\n\r\n    @Bean(\"jpaVendorAdapter\")\r\n    public JpaVendorAdapter jpaVendorAdapter(){\r\n        return new HibernateJpaVendorAdapter();\r\n    }\r\n\r\n    @Bean(name = \"tenantTransactionManager\")\r\n    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){\r\n        JpaTransactionManager transactionManager = new JpaTransactionManager();\r\n        transactionManager.setEntityManagerFactory(entityManagerFactory);\r\n        return transactionManager;\r\n    }\r\n\r\n    @Bean(name = \"datasourceBasedMultiTenantConnectionProvider\")\r\n    @ConditionalOnBean(name = \"masterEntityManagerFactory\")\r\n    public MultiTenantConnectionProvider multiTenantConnectionProvider(){\r\n        return new DataSourceBasedMultiTenantConnectionProviderImpl();\r\n    }\r\n\r\n    @Bean(name = \"currentTenantIdentifierResolver\")\r\n    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){\r\n        return new CurrentTenantIdentifierResolverImpl();\r\n    }\r\n\r\n    @Bean(name = \"tenantEntityManagerFactory\")\r\n    @ConditionalOnBean(name = \"datasourceBasedMultiTenantConnectionProvider\")\r\n    public LocalContainerEntityManagerFactoryBean entityManagerFactory(\r\n            @Qualifier(\"datasourceBasedMultiTenantConnectionProvider\")MultiTenantConnectionProvider connectionProvider,\r\n            @Qualifier(\"currentTenantIdentifierResolver\")CurrentTenantIdentifierResolver tenantIdentifierResolver\r\n    ){\r\n        LocalContainerEntityManagerFactoryBean localBean = new LocalContainerEntityManagerFactoryBean();\r\n        localBean.setPackagesToScan(\r\n                new String[]{\r\n                        User.class.getPackage().getName(),\r\n                        UserRepository.class.getPackage().getName(),\r\n                        UserService.class.getPackage().getName()\r\n\r\n                }\r\n        );\r\n        localBean.setJpaVendorAdapter(jpaVendorAdapter());\r\n        localBean.setPersistenceUnitName(\"tenant-database-persistence-unit\");\r\n        Map&lt;String,Object&gt; properties = new HashMap&lt;&gt;();\r\n        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);\r\n        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider);\r\n        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver);\r\n        properties.put(Environment.DIALECT,\"org.hibernate.dialect.MySQL5Dialect\");\r\n        properties.put(Environment.SHOW_SQL,true);\r\n        properties.put(Environment.FORMAT_SQL,true);\r\n        properties.put(Environment.HBM2DDL_AUTO,\"update\");\r\n        localBean.setJpaPropertyMap(properties);\r\n        return localBean;\r\n    }\r\n}\r\n</code></pre>\r\n<blockquote>\r\n<p>在改配置文件中，大部分内容与主数据源的配置相同，唯一的区别是租户标识解析器与租户数据源补给源的设置，它将告诉Hibernate在执行数据库操作命令前，应该设置什么样的数据库连接信息，以及用户名和密码等信息。</p>\r\n</blockquote>\r\n<h2 id=\"h2-10-\"><a name=\"10. 应用测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>10. 应用测试</h2><p>最后，我们通过一个简单的登录案例来测试本次课程中的SaaS应用程序，为此，需要提供一个Controller用于处理用户登录逻辑。在本案例中，没有严格的对用户密码进行加密，而是使用明文进行比对，也没有提供任何的权限认证框架，知识单纯的验证SaaS的基本特性是否具备。登录控制器代码如下：</p>\r\n<pre><code class=\"lang-java\">/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/27 0027-0:18\r\n * @modify by :\r\n * @since:\r\n */\r\n@Controller\r\npublic class LoginController {\r\n\r\n    @Autowired\r\n    private UserService userService;\r\n\r\n    @GetMapping(\"/login.html\")\r\n    public String login(){\r\n        return \"/login\";\r\n    }\r\n\r\n    @PostMapping(\"/login\")\r\n    public String login(@RequestParam(name = \"username\") String username, @RequestParam(name = \"password\")String password, ModelMap model){\r\n        System.out.println(\"tenant:\"+TenantContextHolder.getTenant());\r\n        User user = userService.findByUsername(username);\r\n        if(user != null){\r\n            if(user.getPassword().equals(password)){\r\n                model.put(\"user\",user);\r\n                return \"/index\";\r\n            }else{\r\n                return \"/login\";\r\n            }\r\n        }else{\r\n            return \"/login\";\r\n        }\r\n    }\r\n}\r\n</code></pre>\r\n<p>在启动项目之前，我们需要为主数据源创建对应的数据库和数据表，用于存放租户数据源信息，同时还需要提供一个租户业务模块数据库和数据表，用来存放租户业务数据。一切准备就绪后，启动项目，在浏览器中输入：<a href=\"http://localhost:8080/login.html\">http://localhost:8080/login.html</a></p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-05-27-05-00-35-b3c8f49c2d054047bf00ca5623c9b919.png\" alt=\"\">\r\n<p>在登录窗口中输入对应的租户名，用户名和密码，测试是否能够正常到达主页。可以多增加几个租户和用户，测试用户是否正常切换到对应的租户下。</p>\r\n<h2 id=\"h2-u603Bu7ED3\"><a name=\"总结\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>总结</h2><p>在这里，我分享了使用Spring Boot+JPA快速实现多租户应用程序的方法，此方法只涉及了实现SaaS应用平台的最核心技术手段，并不是一个完整可用的项目代码，如用户的认证、授权等并未出现在本文中。额外的业务模块感兴趣的朋友可以在此设计基础上自行扩展，如对其中的代码有任何的疑问，欢迎大家在下方给我留言。</p>\r\n','Java,SpringBoot,SaaS,多租户',0,'> 本次教程所涉及到的源码已上传至[Github](<https://github.com/ramostear/una-saas-toturial>),如果你不需要继续阅读下面的内容，你可以直接点击此链接获取源码内容。[https://github.com/ramostear/una-saas-toturial](https://github.com/ramostear/una-saas-toturial)\r\n\r\n## 1. 概述\r\n\r\n笔者从2014年开始接触SaaS（Software as a Service），即多租户（或多承租）软件应用平台；并一直从事相关领域的架构设计及研发工作。机缘巧合，在笔者本科毕业设计时完成了一个基于SaaS的高效财务管理平台的课题研究，从中收获颇多。最早接触SaaS时，国内相关资源匮乏，唯一有的参照资料是《互联网时代的软件革命：SaaS架构设计》（叶伟等著）一书。最后课题的实现是基于OSGI（Open Service Gateway Initiative）Java动态模块化系统规范来实现的。\r\n\r\n时至今日，五年的时间过去了，软件开发的技术发生了巨大的改变，笔者所实现SaaS平台的技术栈也更新了好几波，真是印证了那就话：“山重水尽疑无路，柳暗花明又一村”。基于之前走过的许多弯路和踩过的坑，以及近段时间有许多网友问我如何使用Spring Boot实现多租户系统，决定写一篇文章聊一聊关于SaaS的硬核技术。\r\n\r\n说起SaaS，它只是一种软件架构，并没有多少神秘的东西，也不是什么很难的系统，我个人的感觉，SaaS平台的难度在于商业上的运营，而非技术上的实现。就技术上来说，SaaS是这样一种架构模式：它让多个不同环境的用户使用同一套应用程序，且保证用户之间的数据相互隔离。现在想想看，这也有点共享经济的味道在里面。\r\n\r\n笔者在这里就不再深入聊SaaS软件成熟度模型和数据隔离方案对比的事情了。今天要聊的是使用Spring Boot快速构建独立数据库/共享数据库独立Schema的多租户系统。我将提供一个SaaS系统最核心的技术实现，而其他的部分有兴趣的朋友可以在此基础上自行扩展。\r\n\r\n\r\n\r\n## 2. 尝试了解多租户的应用场景\r\n\r\n假设我们需要开发一个应用程序，并且希望将同一个应用程序销售给N家客户使用。在常规情况下，我们需要为此创建N个Web服务器（Tomcat）,N个数据库（DB），并为N个客户部署相同的应用程序N次。现在，如果我们的应用程序进行了升级或者做了其他任何的改动，那么我们就需要更新N个应用程序同时还需要维护N台服务器。接下来，如果业务开始增长，客户由原来的N个变成了现在的N+M个，我们将面临N个应用程序和M个应用程序版本维护，设备维护以及成本控制的问题。运维几乎要哭死在机房了...\r\n\r\n为了解决上述的问题，我们可以开发多租户应用程序，我们可以根据当前用户是谁，从而选择对应的数据库。例如，当请求来自A公司的用户时，应用程序就连接A公司的数据库，当请求来自B公司的用户时，自动将数据库切换到B公司数据库，以此类推。从理论上将没有什么问题，但我们如果考虑将现有的应用程序改造成SaaS模式，我们将遇到第一个问题：如果识别请求来自哪一个租户？如何自动切换数据源？\r\n\r\n\r\n\r\n## 3. 维护、识别和路由租户数据源\r\n\r\n我们可以提供一个独立的库来存放租户信息，如数据库名称、链接地址、用户名、密码等，这可以统一的解决租户信息维护的问题。租户的识别和路由有很多种方法可以解决，下面列举几个常用的方式：\r\n\r\n- 1.可以通过域名的方式来识别租户：我们可以为每一个租户提供一个唯一的二级域名，通过二级域名就可以达到识别租户的能力，如tenantone.example.com,tenant.example.com；tenantone和tenant就是我们识别租户的关键信息。\r\n- 2.可以将租户信息作为请求参数传递给服务端，为服务端识别租户提供支持，如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的参数tenantId就是应用程序识别租户的关键信息。\r\n- 3.可以在请求头（Header）中设置租户信息，例如JWT等技术，服务端通过解析Header中相关参数以获得租户信息。\r\n- 4.在用户成功登录系统后，将租户信息保存在Session中，在需要的时候从Session取出租户信息。\r\n\r\n解决了上述问题后，我们再来看看如何获取客户端传入的租户信息，以及在我们的业务代码中如何使用租户信息（最关键的是DataSources的问题）。\r\n\r\n我们都知道，在启动Spring Boot应用程序之前，就需要为其提供有关数据源的配置信息（有使用到数据库的情况下）,按照一开始的需求，有N个客户需要使用我们的应用程序，我们就需要提前配置好N个数据源（多数据源）,如果N<50,我认为我还能忍受，如果更多，这样显然是无法接受的。为了解决这一问题，我们需要借助Hibernate 5提供的动态数据源特性，让我们的应用程序具备动态配置客户端数据源的能力。简单来说，当用户请求系统资源时，我们将用户提供的租户信息（tenantId）存放在ThreadLoacal中，紧接着获取TheadLocal中的租户信息，并根据此信息查询单独的租户库，获取当前租户的数据配置信息，然后借助Hibernate动态配置数据源的能力，为当前请求设置数据源，最后之前用户的请求。这样我们就只需要在应用程序中维护一份数据源配置信息（租户数据库配置库），其余的数据源动态查询配置。接下来，我们将快速的演示这一功能。\r\n\r\n\r\n\r\n## 4. 项目构建\r\n\r\n我们将使用Spring Boot 2.1.5版本来实现这一演示项目，首先你需要在Maven配置文件中加入如下的一些配置：\r\n\r\n```xml\r\n<dependencies>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter</artifactId>\r\n		</dependency>\r\n\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-devtools</artifactId>\r\n			<scope>runtime</scope>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.projectlombok</groupId>\r\n			<artifactId>lombok</artifactId>\r\n			<optional>true</optional>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-test</artifactId>\r\n			<scope>test</scope>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-data-jpa</artifactId>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-web</artifactId>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-configuration-processor</artifactId>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>mysql</groupId>\r\n			<artifactId>mysql-connector-java</artifactId>\r\n			<version>5.1.47</version>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.springframework.boot</groupId>\r\n			<artifactId>spring-boot-starter-freemarker</artifactId>\r\n		</dependency>\r\n		<dependency>\r\n			<groupId>org.apache.commons</groupId>\r\n			<artifactId>commons-lang3</artifactId>\r\n		</dependency>\r\n	</dependencies>\r\n```\r\n\r\n然后提供一个可用的配置文件，并加入如下的内容：\r\n\r\n```yaml\r\nspring:\r\n  freemarker:\r\n    cache: false\r\n    template-loader-path:\r\n    - classpath:/templates/\r\n    prefix:\r\n    suffix: .html\r\n  resources:\r\n    static-locations:\r\n    - classpath:/static/\r\n  devtools:\r\n    restart:\r\n      enabled: true\r\n  jpa:\r\n    database: mysql\r\n    show-sql: true\r\n    generate-ddl: false\r\n    hibernate:\r\n      ddl-auto: none\r\nuna:\r\n  master:\r\n    datasource:\r\n      url:  jdbc:mysql://localhost:3306/master_tenant?useSSL=false\r\n      username: root\r\n      password: root\r\n      driverClassName:  com.mysql.jdbc.Driver\r\n      maxPoolSize:  10\r\n      idleTimeout:  300000\r\n      minIdle:  10\r\n      poolName: master-database-connection-pool\r\nlogging:\r\n  level:\r\n    root: warn\r\n    org:\r\n      springframework:\r\n        web:  debug\r\n      hibernate: debug\r\n\r\n```\r\n\r\n> 由于采用Freemarker作为视图渲染引擎，所以需要提供Freemarker的相关技术\r\n>\r\n> una:master:datasource配置项就是上面说的统一存放租户信息的数据源配置信息，你可以理解为主库。\r\n\r\n接下来，我们需要关闭Spring Boot自动配置数据源的功能，在项目主类上添加如下的设置：\r\n\r\n```java\r\n@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})\r\npublic class UnaSaasApplication {\r\n\r\n	public static void main(String[] args) {\r\n		SpringApplication.run(UnaSaasApplication.class, args);\r\n	}\r\n\r\n}\r\n```\r\n\r\n最后，让我们看看整个项目的结构：\r\n![](https://cdn.ramostear.com/2019-05-27-05-00-05-3252d32bc9eb44c1b0375f4565a915b0.png)\r\n\r\n\r\n\r\n## 5. 实现租户数据源查询模块\r\n\r\n我们将定义一个实体类存放租户数据源信息，它包含了租户名，数据库连接地址，用户名和密码等信息，其代码如下：\r\n\r\n```java\r\n@Data\r\n@Entity\r\n@Table(name = \"MASTER_TENANT\")\r\n@NoArgsConstructor\r\n@AllArgsConstructor\r\n@Builder\r\npublic class MasterTenant implements Serializable{\r\n\r\n    @Id\r\n    @Column(name=\"ID\")\r\n    private String id;\r\n\r\n    @Column(name = \"TENANT\")\r\n    @NotEmpty(message = \"Tenant identifier must be provided\")\r\n    private String tenant;\r\n\r\n    @Column(name = \"URL\")\r\n    @Size(max = 256)\r\n    @NotEmpty(message = \"Tenant jdbc url must be provided\")\r\n    private String url;\r\n\r\n    @Column(name = \"USERNAME\")\r\n    @Size(min = 4,max = 30,message = \"db username length must between 4 and 30\")\r\n    @NotEmpty(message = \"Tenant db username must be provided\")\r\n    private String username;\r\n\r\n    @Column(name = \"PASSWORD\")\r\n    @Size(min = 4,max = 30)\r\n    @NotEmpty(message = \"Tenant db password must be provided\")\r\n    private String password;\r\n\r\n    @Version\r\n    private int version = 0;\r\n}\r\n```\r\n\r\n持久层我们将继承JpaRepository接口，快速实现对数据源的CURD操作，同时提供了一个通过租户名查找租户数据源的接口，其代码如下：\r\n\r\n```java\r\npackage com.ramostear.una.saas.master.repository;\r\n\r\nimport com.ramostear.una.saas.master.model.MasterTenant;\r\nimport org.springframework.data.jpa.repository.JpaRepository;\r\nimport org.springframework.data.jpa.repository.Query;\r\nimport org.springframework.data.repository.query.Param;\r\nimport org.springframework.stereotype.Repository;\r\n\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/25 0025-8:22\r\n * @modify by :\r\n * @since:\r\n */\r\n@Repository\r\npublic interface MasterTenantRepository extends JpaRepository<MasterTenant,String>{\r\n\r\n    @Query(\"select p from MasterTenant p where p.tenant = :tenant\")\r\n    MasterTenant findByTenant(@Param(\"tenant\") String tenant);\r\n}\r\n```\r\n\r\n业务层提供通过租户名获取租户数据源信息的服务（其余的服务各位可自行添加）：\r\n\r\n```java\r\npackage com.ramostear.una.saas.master.service;\r\n\r\nimport com.ramostear.una.saas.master.model.MasterTenant;\r\n\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/25 0025-8:26\r\n * @modify by :\r\n * @since:\r\n */\r\n\r\npublic interface MasterTenantService {\r\n    /**\r\n     * Using custom tenant name query\r\n     * @param tenant    tenant name\r\n     * @return          masterTenant\r\n     */\r\n    MasterTenant findByTenant(String tenant);\r\n}\r\n```\r\n\r\n最后，我们需要关注的重点是配置主数据源（Spring Boot需要为其提供一个默认的数据源）。在配置之前，我们需要获取配置项，可以通过@ConfigurationProperties(\"una.master.datasource\")获取配置文件中的相关配置信息：\r\n\r\n```java\r\n@Getter\r\n@Setter\r\n@Configuration\r\n@ConfigurationProperties(\"una.master.datasource\")\r\npublic class MasterDatabaseProperties {\r\n\r\n    private String url;\r\n\r\n    private String password;\r\n\r\n    private String username;\r\n\r\n    private String driverClassName;\r\n\r\n    private long connectionTimeout;\r\n\r\n    private int maxPoolSize;\r\n\r\n    private long idleTimeout;\r\n\r\n    private int minIdle;\r\n\r\n    private String poolName;\r\n\r\n    @Override\r\n    public String toString(){\r\n        StringBuilder builder = new StringBuilder();\r\n        builder.append(\"MasterDatabaseProperties [ url=\")\r\n                .append(url)\r\n                .append(\", username=\")\r\n                .append(username)\r\n                .append(\", password=\")\r\n                .append(password)\r\n                .append(\", driverClassName=\")\r\n                .append(driverClassName)\r\n                .append(\", connectionTimeout=\")\r\n                .append(connectionTimeout)\r\n                .append(\", maxPoolSize=\")\r\n                .append(maxPoolSize)\r\n                .append(\", idleTimeout=\")\r\n                .append(idleTimeout)\r\n                .append(\", minIdle=\")\r\n                .append(minIdle)\r\n                .append(\", poolName=\")\r\n                .append(poolName)\r\n                .append(\"]\");\r\n        return builder.toString();\r\n    }\r\n}\r\n```\r\n\r\n接下来是配置自定义的数据源，其源码如下：\r\n\r\n```java\r\npackage com.ramostear.una.saas.master.config;\r\n\r\nimport com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties;\r\nimport com.ramostear.una.saas.master.model.MasterTenant;\r\nimport com.ramostear.una.saas.master.repository.MasterTenantRepository;\r\nimport com.zaxxer.hikari.HikariDataSource;\r\nimport lombok.extern.slf4j.Slf4j;\r\nimport org.hibernate.cfg.Environment;\r\nimport org.springframework.beans.factory.annotation.Autowired;\r\nimport org.springframework.beans.factory.annotation.Qualifier;\r\nimport org.springframework.context.annotation.Bean;\r\nimport org.springframework.context.annotation.Configuration;\r\nimport org.springframework.context.annotation.Primary;\r\nimport org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;\r\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\r\nimport org.springframework.orm.jpa.JpaTransactionManager;\r\nimport org.springframework.orm.jpa.JpaVendorAdapter;\r\nimport org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;\r\nimport org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;\r\nimport org.springframework.transaction.annotation.EnableTransactionManagement;\r\n\r\nimport javax.persistence.EntityManagerFactory;\r\nimport javax.sql.DataSource;\r\nimport java.util.Properties;\r\n\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/25 0025-8:31\r\n * @modify by :\r\n * @since:\r\n */\r\n@Configuration\r\n@EnableTransactionManagement\r\n@EnableJpaRepositories(basePackages = {\"com.ramostear.una.saas.master.model\",\"com.ramostear.una.saas.master.repository\"},\r\n                       entityManagerFactoryRef = \"masterEntityManagerFactory\",\r\n                       transactionManagerRef = \"masterTransactionManager\")\r\n@Slf4j\r\npublic class MasterDatabaseConfig {\r\n\r\n    @Autowired\r\n    private MasterDatabaseProperties masterDatabaseProperties;\r\n\r\n    @Bean(name = \"masterDatasource\")\r\n    public DataSource masterDatasource(){\r\n        log.info(\"Setting up masterDatasource with :{}\",masterDatabaseProperties.toString());\r\n        HikariDataSource datasource = new HikariDataSource();\r\n        datasource.setUsername(masterDatabaseProperties.getUsername());\r\n        datasource.setPassword(masterDatabaseProperties.getPassword());\r\n        datasource.setJdbcUrl(masterDatabaseProperties.getUrl());\r\n        datasource.setDriverClassName(masterDatabaseProperties.getDriverClassName());\r\n        datasource.setPoolName(masterDatabaseProperties.getPoolName());\r\n        datasource.setMaximumPoolSize(masterDatabaseProperties.getMaxPoolSize());\r\n        datasource.setMinimumIdle(masterDatabaseProperties.getMinIdle());\r\n        datasource.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout());\r\n        datasource.setIdleTimeout(masterDatabaseProperties.getIdleTimeout());\r\n        log.info(\"Setup of masterDatasource successfully.\");\r\n        return datasource;\r\n    }\r\n\r\n    @Primary\r\n    @Bean(name = \"masterEntityManagerFactory\")\r\n    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){\r\n        LocalContainerEntityManagerFactoryBean lb = new LocalContainerEntityManagerFactoryBean();\r\n        lb.setDataSource(masterDatasource());\r\n        lb.setPackagesToScan(\r\n           new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()}\r\n        );\r\n\r\n        //Setting a name for the persistence unit as Spring sets it as \'default\' if not defined.\r\n        lb.setPersistenceUnitName(\"master-database-persistence-unit\");\r\n\r\n        //Setting Hibernate as the JPA provider.\r\n        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();\r\n        lb.setJpaVendorAdapter(vendorAdapter);\r\n\r\n        //Setting the hibernate properties\r\n        lb.setJpaProperties(hibernateProperties());\r\n\r\n        log.info(\"Setup of masterEntityManagerFactory successfully.\");\r\n        return lb;\r\n    }\r\n\r\n    @Bean(name = \"masterTransactionManager\")\r\n    public JpaTransactionManager masterTransactionManager(@Qualifier(\"masterEntityManagerFactory\")EntityManagerFactory emf){\r\n        JpaTransactionManager transactionManager = new JpaTransactionManager();\r\n        transactionManager.setEntityManagerFactory(emf);\r\n        log.info(\"Setup of masterTransactionManager successfully.\");\r\n        return transactionManager;\r\n    }\r\n\r\n    @Bean\r\n    public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){\r\n        return new PersistenceExceptionTranslationPostProcessor();\r\n    }\r\n\r\n    private Properties hibernateProperties(){\r\n        Properties properties = new Properties();\r\n        properties.put(Environment.DIALECT,\"org.hibernate.dialect.MySQL5Dialect\");\r\n        properties.put(Environment.SHOW_SQL,true);\r\n        properties.put(Environment.FORMAT_SQL,true);\r\n        properties.put(Environment.HBM2DDL_AUTO,\"update\");\r\n        return properties;\r\n    }\r\n}\r\n\r\n```\r\n\r\n在改配置类中，我们主要提供包扫描路径，实体管理工程，事务管理器和数据源配置参数的配置。\r\n\r\n## 6. 实现租户业务模块\r\n\r\n在此小节中，租户业务模块我们仅提供一个用户登录的场景来演示SaaS的功能。其实体层、业务层和持久化层根普通的Spring Boot Web项目没有什么区别，你甚至感觉不到它是一个SaaS应用程序的代码。\r\n\r\n首先，创建一个用户实体User，其源码如下：\r\n\r\n```java\r\n@Entity\r\n@Table(name = \"USER\")\r\n@Data\r\n@NoArgsConstructor\r\n@AllArgsConstructor\r\n@Builder\r\npublic class User implements Serializable {\r\n    private static final long serialVersionUID = -156890917814957041L;\r\n\r\n    @Id\r\n    @Column(name = \"ID\")\r\n    private String id;\r\n\r\n    @Column(name = \"USERNAME\")\r\n    private String username;\r\n\r\n    @Column(name = \"PASSWORD\")\r\n    @Size(min = 6,max = 22,message = \"User password must be provided and length between 6 and 22.\")\r\n    private String password;\r\n\r\n    @Column(name = \"TENANT\")\r\n    private String tenant;\r\n}\r\n\r\n```\r\n\r\n业务层提供了一个根据用户名检索用户信息的服务，它将调用持久层的方法根据用户名对租户的用户表进行检索，如果找到满足条件的用户记录，则返回用户信息，如果没有找到，则返回null;持久层和业务层的源码分别如下：\r\n\r\n```java\r\n@Repository\r\npublic interface UserRepository extends JpaRepository<User,String>,JpaSpecificationExecutor<User>{\r\n\r\n    User findByUsername(String username);\r\n}\r\n```\r\n\r\n```java\r\n@Service(\"userService\")\r\npublic class UserServiceImpl implements UserService{\r\n\r\n    @Autowired\r\n    private UserRepository userRepository;\r\n\r\n    private static TwitterIdentifier identifier = new TwitterIdentifier();\r\n\r\n\r\n\r\n    @Override\r\n    public void save(User user) {\r\n        user.setId(identifier.generalIdentifier());\r\n        user.setTenant(TenantContextHolder.getTenant());\r\n        userRepository.save(user);\r\n    }\r\n\r\n    @Override\r\n    public User findById(String userId) {\r\n        Optional<User> optional = userRepository.findById(userId);\r\n        if(optional.isPresent()){\r\n            return optional.get();\r\n        }else{\r\n            return null;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public User findByUsername(String username) {\r\n        System.out.println(TenantContextHolder.getTenant());\r\n        return userRepository.findByUsername(username);\r\n    }\r\n```\r\n\r\n> 在这里，我们采用了Twitter的雪花算法来实现了一个ID生成器。\r\n\r\n\r\n\r\n## 7. 配置拦截器\r\n\r\n我们需要提供一个租户信息的拦截器，用以获取租户标识符，其源代码和配置拦截器的源代码如下：\r\n\r\n```java\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/26 0026-23:17\r\n * @modify by :\r\n * @since:\r\n */\r\n@Slf4j\r\npublic class TenantInterceptor implements HandlerInterceptor{\r\n\r\n    @Override\r\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\r\n        String tenant = request.getParameter(\"tenant\");\r\n        if(StringUtils.isBlank(tenant)){\r\n            response.sendRedirect(\"/login.html\");\r\n            return false;\r\n        }else{\r\n            TenantContextHolder.setTenant(tenant);\r\n            return true;\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n```java\r\n@Configuration\r\npublic class InterceptorConfig extends WebMvcConfigurationSupport {\r\n\r\n    @Override\r\n    protected void addInterceptors(InterceptorRegistry registry) {\r\n        registry.addInterceptor(new TenantInterceptor()).addPathPatterns(\"/**\").excludePathPatterns(\"/login.html\");\r\n        super.addInterceptors(registry);\r\n    }\r\n}\r\n```\r\n\r\n> /login.html是系统的登录路径，我们需要将其排除在拦截器拦截的范围之外，否则我们永远无法进行登录\r\n\r\n\r\n\r\n## 8. 维护租户标识信息\r\n\r\n在这里，我们使用ThreadLocal来存放租户标识信息，为动态设置数据源提供数据支持，该类提供了设置租户标识、获取租户标识以及清除租户标识三个静态方法。其源码如下：\r\n\r\n```java\r\npublic class TenantContextHolder {\r\n\r\n    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();\r\n\r\n    public static void setTenant(String tenant){\r\n        CONTEXT.set(tenant);\r\n    }\r\n\r\n    public static String getTenant(){\r\n        return CONTEXT.get();\r\n    }\r\n\r\n    public static void clear(){\r\n        CONTEXT.remove();\r\n    }\r\n}\r\n```\r\n\r\n> 此类时实现动态数据源设置的关键\r\n\r\n\r\n\r\n## 9. 动态数据源切换\r\n\r\n要实现动态数据源切换，我们需要借助两个类来完成，CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。从它们的命名上就可以看出，一个负责解析租户标识，一个负责提供租户标识对应的租户数据源信息。\r\n\r\n首先，我们需要实现CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法，完成租户标识的解析功能。实现类的源码如下：\r\n\r\n```java\r\npackage com.ramostear.una.saas.tenant.config;\r\n\r\nimport com.ramostear.una.saas.context.TenantContextHolder;\r\nimport org.apache.commons.lang3.StringUtils;\r\nimport org.hibernate.context.spi.CurrentTenantIdentifierResolver;\r\n\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/26 0026-22:38\r\n * @modify by :\r\n * @since:\r\n */\r\npublic class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {\r\n\r\n    /**\r\n     * 默认的租户ID\r\n     */\r\n    private static final String DEFAULT_TENANT = \"tenant_1\";\r\n\r\n    /**\r\n     * 解析当前租户的ID\r\n     * @return\r\n     */\r\n    @Override\r\n    public String resolveCurrentTenantIdentifier() {\r\n        //通过租户上下文获取租户ID，此ID是用户登录时在header中进行设置的\r\n        String tenant = TenantContextHolder.getTenant();\r\n        //如果上下文中没有找到该租户ID，则使用默认的租户ID，或者直接报异常信息\r\n        return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;\r\n    }\r\n\r\n    @Override\r\n    public boolean validateExistingCurrentSessions() {\r\n        return true;\r\n    }\r\n}\r\n\r\n```\r\n\r\n> 此类的逻辑非常简单，就是从ThreadLocal中获取当前设置的租户标识符\r\n\r\n有了租户标识符解析类之后，我们需要扩展租户数据源提供类，实现从数据库动态查询租户数据源信息，其源码如下：\r\n\r\n```\r\n@Slf4j\r\n@Configuration\r\npublic class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{\r\n\r\n    private static final long serialVersionUID = -7522287771874314380L;\r\n\r\n    @Autowired\r\n    private MasterTenantRepository masterTenantRepository;\r\n\r\n    private Map<String,DataSource> dataSources = new TreeMap<>();\r\n\r\n    @Override\r\n    protected DataSource selectAnyDataSource() {\r\n        if(dataSources.isEmpty()){\r\n            List<MasterTenant> tenants = masterTenantRepository.findAll();\r\n            tenants.forEach(masterTenant->{\r\n                dataSources.put(masterTenant.getTenant(), DataSourceUtils.wrapperDataSource(masterTenant));\r\n            });\r\n        }\r\n        return dataSources.values().iterator().next();\r\n    }\r\n\r\n    @Override\r\n    protected DataSource selectDataSource(String tenant) {\r\n        if(!dataSources.containsKey(tenant)){\r\n            List<MasterTenant> tenants = masterTenantRepository.findAll();\r\n            tenants.forEach(masterTenant->{\r\n                dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant));\r\n            });\r\n        }\r\n        return dataSources.get(tenant);\r\n    }\r\n}\r\n```\r\n\r\n> 在该类中，通过查询租户数据源库，动态获得租户数据源信息，为租户业务模块的数据源配置提供数据数据支持。\r\n\r\n最后，我们还需要提供租户业务模块数据源配置，这是整个项目核心的地方，其代码如下：\r\n\r\n```java\r\n@Slf4j\r\n@Configuration\r\n@EnableTransactionManagement\r\n@ComponentScan(basePackages = {\r\n        \"com.ramostear.una.saas.tenant.model\",\r\n        \"com.ramostear.una.saas.tenant.repository\"\r\n})\r\n@EnableJpaRepositories(basePackages = {\r\n        \"com.ramostear.una.saas.tenant.repository\",\r\n        \"com.ramostear.una.saas.tenant.service\"\r\n},entityManagerFactoryRef = \"tenantEntityManagerFactory\"\r\n,transactionManagerRef = \"tenantTransactionManager\")\r\npublic class TenantDataSourceConfig {\r\n\r\n    @Bean(\"jpaVendorAdapter\")\r\n    public JpaVendorAdapter jpaVendorAdapter(){\r\n        return new HibernateJpaVendorAdapter();\r\n    }\r\n\r\n    @Bean(name = \"tenantTransactionManager\")\r\n    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){\r\n        JpaTransactionManager transactionManager = new JpaTransactionManager();\r\n        transactionManager.setEntityManagerFactory(entityManagerFactory);\r\n        return transactionManager;\r\n    }\r\n\r\n    @Bean(name = \"datasourceBasedMultiTenantConnectionProvider\")\r\n    @ConditionalOnBean(name = \"masterEntityManagerFactory\")\r\n    public MultiTenantConnectionProvider multiTenantConnectionProvider(){\r\n        return new DataSourceBasedMultiTenantConnectionProviderImpl();\r\n    }\r\n\r\n    @Bean(name = \"currentTenantIdentifierResolver\")\r\n    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){\r\n        return new CurrentTenantIdentifierResolverImpl();\r\n    }\r\n\r\n    @Bean(name = \"tenantEntityManagerFactory\")\r\n    @ConditionalOnBean(name = \"datasourceBasedMultiTenantConnectionProvider\")\r\n    public LocalContainerEntityManagerFactoryBean entityManagerFactory(\r\n            @Qualifier(\"datasourceBasedMultiTenantConnectionProvider\")MultiTenantConnectionProvider connectionProvider,\r\n            @Qualifier(\"currentTenantIdentifierResolver\")CurrentTenantIdentifierResolver tenantIdentifierResolver\r\n    ){\r\n        LocalContainerEntityManagerFactoryBean localBean = new LocalContainerEntityManagerFactoryBean();\r\n        localBean.setPackagesToScan(\r\n                new String[]{\r\n                        User.class.getPackage().getName(),\r\n                        UserRepository.class.getPackage().getName(),\r\n                        UserService.class.getPackage().getName()\r\n\r\n                }\r\n        );\r\n        localBean.setJpaVendorAdapter(jpaVendorAdapter());\r\n        localBean.setPersistenceUnitName(\"tenant-database-persistence-unit\");\r\n        Map<String,Object> properties = new HashMap<>();\r\n        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);\r\n        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider);\r\n        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver);\r\n        properties.put(Environment.DIALECT,\"org.hibernate.dialect.MySQL5Dialect\");\r\n        properties.put(Environment.SHOW_SQL,true);\r\n        properties.put(Environment.FORMAT_SQL,true);\r\n        properties.put(Environment.HBM2DDL_AUTO,\"update\");\r\n        localBean.setJpaPropertyMap(properties);\r\n        return localBean;\r\n    }\r\n}\r\n```\r\n\r\n> 在改配置文件中，大部分内容与主数据源的配置相同，唯一的区别是租户标识解析器与租户数据源补给源的设置，它将告诉Hibernate在执行数据库操作命令前，应该设置什么样的数据库连接信息，以及用户名和密码等信息。\r\n\r\n\r\n\r\n## 10. 应用测试\r\n\r\n最后，我们通过一个简单的登录案例来测试本次课程中的SaaS应用程序，为此，需要提供一个Controller用于处理用户登录逻辑。在本案例中，没有严格的对用户密码进行加密，而是使用明文进行比对，也没有提供任何的权限认证框架，知识单纯的验证SaaS的基本特性是否具备。登录控制器代码如下：\r\n\r\n```java\r\n/**\r\n * @author : Created by Tan Chaohong (alias:ramostear)\r\n * @create-time 2019/5/27 0027-0:18\r\n * @modify by :\r\n * @since:\r\n */\r\n@Controller\r\npublic class LoginController {\r\n\r\n    @Autowired\r\n    private UserService userService;\r\n\r\n    @GetMapping(\"/login.html\")\r\n    public String login(){\r\n        return \"/login\";\r\n    }\r\n\r\n    @PostMapping(\"/login\")\r\n    public String login(@RequestParam(name = \"username\") String username, @RequestParam(name = \"password\")String password, ModelMap model){\r\n        System.out.println(\"tenant:\"+TenantContextHolder.getTenant());\r\n        User user = userService.findByUsername(username);\r\n        if(user != null){\r\n            if(user.getPassword().equals(password)){\r\n                model.put(\"user\",user);\r\n                return \"/index\";\r\n            }else{\r\n                return \"/login\";\r\n            }\r\n        }else{\r\n            return \"/login\";\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n在启动项目之前，我们需要为主数据源创建对应的数据库和数据表，用于存放租户数据源信息，同时还需要提供一个租户业务模块数据库和数据表，用来存放租户业务数据。一切准备就绪后，启动项目，在浏览器中输入：http://localhost:8080/login.html\r\n\r\n![](https://cdn.ramostear.com/2019-05-27-05-00-35-b3c8f49c2d054047bf00ca5623c9b919.png)\r\n\r\n在登录窗口中输入对应的租户名，用户名和密码，测试是否能够正常到达主页。可以多增加几个租户和用户，测试用户是否正常切换到对应的租户下。\r\n\r\n\r\n\r\n## 总结\r\n\r\n在这里，我分享了使用Spring Boot+JPA快速实现多租户应用程序的方法，此方法只涉及了实现SaaS应用平台的最核心技术手段，并不是一个完整可用的项目代码，如用户的认证、授权等并未出现在本文中。额外的业务模块感兴趣的朋友可以在此设计基础上自行扩展，如对其中的代码有任何的疑问，欢迎大家在下方给我留言。',1,'','2019/18/21/d02c555r.html',1,'SaaS是这样一种架构模式：它让多个不同环境的用户使用同一套应用程序，且保证用户之间的数据相互隔离','blog.html','https://cdn.ramostear.com/2019-05-27-05-00-57-101b5798fb894091ab24c0d3fe504d69.png;base64','Spring Boot 构建多租户SaaS平台核心技术指南','',1107),(16,'2019-11-21 23:22:53',0,'2019-11-21 23:32:07',0,1,1,'谭朝红','从宏观的角度来看，SaaS是一种软件应用程序交付方式，软件提供商集中化托管一个或多个软件应用程序，并通过互联网向租户体用这些软件应用程序。从分类上看，SaaS（软件即服务）也是云计算重要的一部分','<h1 id=\"h1--saas-\"><a name=\"架构师必备技能指南：SaaS(软件即服务)架构设计\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>架构师必备技能指南：SaaS(软件即服务)架构设计</h1><p style=\"text-align:right\">SaaS(软件即服务)平台架构设计指南</p>\r\n\r\n<h2 id=\"h2-1-\"><a name=\"1、介绍\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1、介绍</h2><p>从计算机诞生开始，就伴随着计算机应用程序的演变。简短的回顾历史，我们可以清楚的看到应用程序发生的巨大变化。上世纪70年代中期，随着个人PC机的爆炸式增长以及程序员的崛起，让计算机的计算能力得到了大跨越的提升，个人PC机上可以运行非常复杂的应用程序。</p>\r\n<p>进入上世纪80年代，随着Bulletin Board System（简称：BBS）电子公告板系统的兴起，它可以为广大PC机用户提供基本的在线服务，如在线聊天、电子邮件、消息发送和文件下载。由于受到那个时代计算机网络传输速度的限制，在线服务的响应速度慢，交互体验差是最大的通病。</p>\r\n<p>进入90年代中后期，随着万维网的出现，计算机的计算能开始进入快速提升阶段，加之网络基础设施的持续完善，计算机网络技术也随之发展起来，这让Web网站可以提供功能多元化和更为复杂的在线服务，直到今天，我们所看到的互联网（或云）开发的在线服务应用程序。</p>\r\n<p>在这段计算机技术快速成长的时间里，计算机软件到底发生了哪些变化？从历史的发展中，我们可以看到，应用程序本身没有发生本质的变化（程序=数据结构+算法），变化的是软件的供需方式发生了改变。现在，应用程序消费者不需要再在他们的PC机上下载和安装特定的应用程序，即可获得软件所提供的计算服务。在云计算技术的支持下，消费者（企业或个人）只需要使用Web工具（浏览器）访问并登录软件提供商的Web系统，通过简单的配置，就可以获得自己所需应用程序服务。这种通过网络即可使用软件的服务，即使SaaS（软件即服务）。</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-05-28-07-06-12-88d5bcd927674ddcb485628266a53dfb.jpg\" alt=\"\">\r\n<center>图 1-1 2015中国SaaS生态</center>\r\n\r\n<p>在本篇文章中，我们将着重介绍SaaS架构设计,并围绕WHAT（是什么？）、WHY（为什么？）、WHERE（在哪里？）和HOW（怎么样？）这四个问题，对以下的几点进行阐述：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-05-28-07-07-01-de27b9f3bf92433e86f85f9cbf71108f.png\" alt=\"\">\r\n<center>图 1-2 文章结构</center>\r\n\r\n<ul>\r\n<li>1、什么是SaaS平台？</li><li>2、为什么需要使用SaaS平台架构？</li><li>3、SaaS平台主要的特性和优势有哪些？</li><li>4、SaaS平台适合在什么领域进行实施？</li><li>5、SaaS平台有哪些先天性的缺陷？</li><li>6、SaaS平台有哪些核心的组件？</li><li>7、实施SaaS架构设计时的注意事项有哪些？</li></ul>\r\n<h2 id=\"h2-2-saas-\"><a name=\"2、什么是SaaS平台？\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2、什么是SaaS平台？</h2><p><img src=\"https://cdn.ramostear.com/2019-05-28-07-06-26-4dbd6523bf2d4d52b5c9a78ef64e2ad6.png\" alt=\"\">\r\n<center>图 2-1 SaaS组成结构</center>\r\n\r\n<p>在你决定实施SaaS品台架构设计前，你有必要先了解SaaS平台是什么。从宏观的角度来看，SaaS是一种软件应用程序交付方式，软件提供商集中化托管一个或多个软件应用程序，并通过互联网向租户体用这些软件应用程序。从分类上看，SaaS（软件即服务）也是云计算重要的一部分。目前国内主流的云服务提供商如阿里云、百度云、腾讯云等，为广大用户提供了不同业务需求的云服务，它们大致可以分为以下几类：</p>\r\n<ul>\r\n<li>1、基础设施即服务：如CPU、Network、Disk和Memory等</li><li>2、平台即服务：如阿里云服务器和云数据库等</li><li>3、软件即服务：阿里短信、阿里邮箱等</li><li>4、数据即服务：如阿里云对象存储，七牛云存储等</li><li>5、其他软件服务：机器学习、人工智能等</li></ul>\r\n<p>SaaS应用程序的任何更新或者修复漏洞操作都是由软件提供商负责实施和处理的，由于租户是通过互联网获取软件服务，所以租户端无需下载任何的升级包或者修复补丁，是一种开箱即获取最新软件产品的服务方式。</p>\r\n<p>通过对什么是SaaS的介绍，接下来，我们了解一下选择SaaS作为软件架构来设计产品的一些理由。</p>\r\n<h2 id=\"h2-3-saas-\"><a name=\"3、为什么选择SaaS?\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3、为什么选择SaaS?</h2><p>我们将从不同的角度来阐述几个为什么选择SaaS的理由。透过对这些因素的分析，为你是否需要将自己的软件SaaS化提供一定的参考依据。</p>\r\n<h3 id=\"h3-3-1-\"><a name=\"3.1、消费者角度\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.1、消费者角度</h3><p>获取软件服务的方式足够简单，SaaS也许是迄今为止使用软件最简单的方式之一，租户只需要动动鼠标和键盘，即可在几小时甚至几分钟内获得一个大型的软件服务。相比于传统使用软件的方式，租户省去了研发、部署、运维等一系列繁复的过程，且获得软件的时间和费用成本都大幅度降低。</p>\r\n<h2 id=\"h2-3-2-\"><a name=\"3.2、商业角度\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.2、商业角度</h2><p>SaaS可以体用跨地域、跨平台的软件服务。与此同时，软件服务商可以统一对软件进行版本管理，这将带来以下几点好处（包括但不限于）：</p>\r\n<ul>\r\n<li>1、缩短产品上线时间：多端适配，统一版本，统一更新</li><li>2、降低维护成本：不需要同时维护多个版本的软件实例，运维压力减小</li><li>3、容易升级：由于版本得到有效控制，一次升级，即可覆盖所有租户端</li></ul>\r\n<h2 id=\"h2-4-saas-\"><a name=\"4、SaaS的特性和优势\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4、SaaS的特性和优势</h2><p>我们将SaaS应用程序与传统的桌面应用程序做一个水平的对比，部署一个SaaS产品将可以获得以下的几点优势。</p>\r\n<h3 id=\"h3-4-1-\"><a name=\"4.1、简单\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.1、简单</h3><p>SaaS化的产品通过互联网向租户提供软件服务，随着Web技术（如jQuery、Node.js）的进步，Web页面的交互体验度大幅度提升，交互更流畅、更人性化。与传统的桌面应用程序的人机交互效果相差无几。</p>\r\n<h3 id=\"h3-4-2-\"><a name=\"4.2、经济实惠\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2、经济实惠</h3><p>SaaS化产品可以为租户提供弹性的付费方案，如按日、按月、按年、按使用人数或者按使用量进行计费，它将给租户提供更经济的使用软件的财务预算表。</p>\r\n<h3 id=\"h3-4-3-\"><a name=\"4.3、安全\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3、安全</h3><p>使用SaaS产品无需担心数据安全问题，这好比将钱存入银行一样安全。相较于企业内部部署的软件系统而言，SaaS产品具备更高的安全保障能力，因为软件提供商具有更多软件安全防护的技术资源、人力资源和财政资源。</p>\r\n<h3 id=\"h3-4-4-\"><a name=\"4.4、兼容性\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.4、兼容性</h3><p>与传统软件相比、SaaS软件的兼容性更好，它没有传统软件的多本版维护问题和操作系统兼容问题。在SaaS软件中，租户用户在使用软件的过程中，几乎上感觉不到软件发生了改变。当租户用户登录到系统上时，就已经获得了最新版本的软件。</p>\r\n<h2 id=\"h2-5-saas-\"><a name=\"5、SaaS软件的适用范围\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5、SaaS软件的适用范围</h2><p>SaaS产品具有广泛的适应范围，特别是与其他云产品（如IaaS(基础设施即服务)和PaaS(平台即服务)）配合使用时这种能力表现尤为突出，例如阿里云之类的云计算技术允许你配置可托管的Web站点、数据库服务器等。你只需要打开浏览器并登录到阿里云控制台，通过操作对应的控制面板，即可获得相关的软件服务。</p>\r\n<p>从理论上讲，SaaS可以将任何的软件SaaS，下面列举一些通用的分类供大家参考：</p>\r\n<ul>\r\n<li>1、Office在线办公类SaaS产品</li><li>2、电子邮件和即时消息类SaaS产品</li><li>3、社交媒体类SaaS产品</li><li>4、第三方API类SaaS产品</li><li>5、安全和访问控制类SaaS产品</li><li>6、机器学习类SaaS产品</li><li>7、人工智能类SaaS产品</li><li>8、地理位置服务类SaaS产品</li><li>9、数据流和数据检索类SaaS产品</li></ul>\r\n<h2 id=\"h2-6-saas-\"><a name=\"6、SaaS产品的天生缺陷\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6、SaaS产品的天生缺陷</h2><p><img src=\"https://cdn.ramostear.com/2019-05-28-07-06-47-1f5f51ee9eca4d00a1b7f426bfbf4aa7.png\" alt=\"\">\r\n<center>图 6-1 SaaS产品的缺点</center>\r\n\r\n<p>从上图我们可以直观的看到，SaaS产品与生俱来的几个缺陷，接下来我们将逐一进行描述。</p>\r\n<h3 id=\"h3-6-1-\"><a name=\"6.1、软件控制权\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6.1、软件控制权</h3><p>与企业内部部署的软件不同，由于SaaS软件被击中托管在服务提供商的Web服务器中，所以租户无法控制所有的软件应用程序，SaaS化的软件比企业自行部署的软件获得的控制权更少，租户可操作的自定义控制权极度有限。</p>\r\n<h3 id=\"h3-6-2-\"><a name=\"6.2、消费者基数小\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6.2、消费者基数小</h3><p>由于SaaS软件是将一套应用程序共享给一个或者多个租户共同使用，这种共享的消费方式还未被大多数的消费者所接受。同时，受制于市场环境的影响，目前还有大多数的软件还未SaaS化。</p>\r\n<h3 id=\"h3-6-3-\"><a name=\"6.3、性能瓶颈\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6.3、性能瓶颈</h3><p>共享应用程序必然会带来服务器性能的下降、如计算速度、网络资源、I/O读写等都将面临严峻的考验。在性能方面，企业内部部署的“独享模式”的应用程序比SaaS软件的“共享模式”略胜一筹。</p>\r\n<h3 id=\"h3-6-4-\"><a name=\"6.4、安全问题\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6.4、安全问题</h3><p>当租户在选择一款SaaS产品时，产品的安全性将会被放置在第一位进行考虑。如数据的隔离、敏感数据的加密、数据访问权限控制、个人隐私等问题。在2018年5月25日，GDPR(General Data Protection Regulation)《通用数据保护条例》出现之后，越来越多的人开始重视数据安全问题。如何最大程度的打消租户的这一顾虑，需要服务提供商加强对自身信誉度的提升，以赢得租户的信赖。</p>\r\n<h2 id=\"h2-7-saas-\"><a name=\"7、SaaS产品的核心组件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7、SaaS产品的核心组件</h2><p>不同类型的SaaS产品，由于要面对不同的用户愿景，可能在功能和业务上会有所不同，但任何一个SaaS产品，都具备以下几个共同的核心组件。</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-05-28-07-07-45-19118cb442464b0fb46467478730b5d7.png\" alt=\"\">\r\n<center>图 7-1 SaaS 核心组件</center>\r\n\r\n<h3 id=\"h3-7-1-\"><a name=\"7.1、安全组件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.1、安全组件</h3><p>在SaaS产品中，系统安全永远是第一位需要考虑的事情，如何保障租户数据的安全，是你首要的事情。这如同银行首选需要保障储户资金安全一样。安全组件就是统一的对SaaS产品进行安全防护，保障系统数据安全。</p>\r\n<h3 id=\"h3-7-2-\"><a name=\"7.2、数据隔离组件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.2、数据隔离组件</h3><p>安全组件解决了用户数据安全可靠的问题，但数据往往还需要解决隐私问题，各企业之间的数据必须相互不可见，即相互隔离。在SaaS产品中，如何识别、区分、隔离个租户的数据时你在实施SaaS平台架构设计时需要考虑的第二个问题。</p>\r\n<h3 id=\"h3-7-3-\"><a name=\"7.3、可配置组件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.3、可配置组件</h3><p>尽管SaaS产品在设计之初就考虑了大多数通用的功能，让租户开箱即用，但任然有为数不少的租户需要定制服务自身业务需求的配置项，如UI布局、主题、标识（Logo）等信息。正因为无法抽象出一个完全通用的应用程序，所以在SaaS产品中，你需要提供一个可用于自定义配置的组件。</p>\r\n<h3 id=\"h3-7-4-\"><a name=\"7.4、可扩展组件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.4、可扩展组件</h3><p>随着SaaS产品业务和租户数量的增长，原有的服务器配置将无法继续满足新的需求，系统性能将会与业务量和用户量成反比。此时，SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术，在多个服务器上部署多个软件运行示例并提供相同的软件服务，以此实现水平扩展SaaS产品的整体服务性能。为了实现可扩展能力，就需要SaaS展示层的代码与业务逻辑部分的代码进行分离，两者独立部署。例如使用VUE+微服务构建前后端分离且可水平进行扩展的分布式SaaS应用产品。对于可扩展，还有另外一种方式，即垂直扩展，其做法比较简单，也比较粗暴：通过增加单台服务器的配置，如购买性能更好的CUP、存储更大的内存条、增大带宽等措施，让服务器能够处理更多的用户请求。但此做法对于提升产品性能没有质的改变，且成本很高。</p>\r\n<h3 id=\"h3-7-5-0-\"><a name=\"7.5、0停机时间升级产品\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.5、0停机时间升级产品</h3><p>以往的软件在升级或者修复Bug是，都需要将运行的程序脱机一段时间，等待升级或修复工作完成后，再重新启动应用程序。而SaaS产品则需要全天候保障服务的可用性。这就需要你考虑如何实现在不重启原有应用程序的情况下，完成应用程序的升级修复工作。</p>\r\n<h3 id=\"h3-7-6-\"><a name=\"7.6、多租户组件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>7.6、多租户组件</h3><p>要将原有产品SaaS化，就必须提供多租户组件，多租户组件是衡量一个应用程序是否具备SaaS服务能力的重要指标之一。SaaS产品需要同时容纳多个租户的数据，同时还需要保证各租户之间的数据不会相互干扰，保证租户中的用户能够按期望索引到正确的数据，多租户组件是你必须要解决的一个问题。其余的组件都将围绕此组件展开各自的业务。</p>\r\n<h2 id=\"h2-u603Bu7ED3\"><a name=\"总结\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>总结</h2><p>本文将软件应用程序的发展历程作为切入点，并围绕WHAT(是什么？)、WHY(为什么？)、WHERE(在哪些领域实施？)和HOW(怎么样？)这四个问题对SaaS展开了介绍。文中详细的阐述了基于SaaS架构的软件设计需要注意的问题，并分析了SaaS产品的特性、有点、缺点。最后还介绍了基于SaaS架构的软件产品应该具备的几个核心组件以及他们各自的作用。希望本次能够让你对SaaS平台架构有一个全面的了解，并且在你准备实施SaaS平台架构设计前能够提供一些价值的参考信息。</p>\r\n','架构师,SaaS,软件即服务',0,'# 架构师必备技能指南：SaaS(软件即服务)架构设计\r\n\r\n<p style=\"text-align:right\">SaaS(软件即服务)平台架构设计指南</p>\r\n\r\n## 1、介绍\r\n\r\n从计算机诞生开始，就伴随着计算机应用程序的演变。简短的回顾历史，我们可以清楚的看到应用程序发生的巨大变化。上世纪70年代中期，随着个人PC机的爆炸式增长以及程序员的崛起，让计算机的计算能力得到了大跨越的提升，个人PC机上可以运行非常复杂的应用程序。\r\n\r\n进入上世纪80年代，随着Bulletin Board System（简称：BBS）电子公告板系统的兴起，它可以为广大PC机用户提供基本的在线服务，如在线聊天、电子邮件、消息发送和文件下载。由于受到那个时代计算机网络传输速度的限制，在线服务的响应速度慢，交互体验差是最大的通病。\r\n\r\n进入90年代中后期，随着万维网的出现，计算机的计算能开始进入快速提升阶段，加之网络基础设施的持续完善，计算机网络技术也随之发展起来，这让Web网站可以提供功能多元化和更为复杂的在线服务，直到今天，我们所看到的互联网（或云）开发的在线服务应用程序。\r\n\r\n在这段计算机技术快速成长的时间里，计算机软件到底发生了哪些变化？从历史的发展中，我们可以看到，应用程序本身没有发生本质的变化（程序=数据结构+算法），变化的是软件的供需方式发生了改变。现在，应用程序消费者不需要再在他们的PC机上下载和安装特定的应用程序，即可获得软件所提供的计算服务。在云计算技术的支持下，消费者（企业或个人）只需要使用Web工具（浏览器）访问并登录软件提供商的Web系统，通过简单的配置，就可以获得自己所需应用程序服务。这种通过网络即可使用软件的服务，即使SaaS（软件即服务）。\r\n\r\n![](https://cdn.ramostear.com/2019-05-28-07-06-12-88d5bcd927674ddcb485628266a53dfb.jpg)\r\n\r\n<center>图 1-1 2015中国SaaS生态</center>\r\n\r\n在本篇文章中，我们将着重介绍SaaS架构设计,并围绕WHAT（是什么？）、WHY（为什么？）、WHERE（在哪里？）和HOW（怎么样？）这四个问题，对以下的几点进行阐述：\r\n\r\n![](https://cdn.ramostear.com/2019-05-28-07-07-01-de27b9f3bf92433e86f85f9cbf71108f.png)\r\n\r\n<center>图 1-2 文章结构</center>\r\n\r\n- 1、什么是SaaS平台？\r\n- 2、为什么需要使用SaaS平台架构？\r\n- 3、SaaS平台主要的特性和优势有哪些？\r\n- 4、SaaS平台适合在什么领域进行实施？\r\n- 5、SaaS平台有哪些先天性的缺陷？\r\n- 6、SaaS平台有哪些核心的组件？\r\n- 7、实施SaaS架构设计时的注意事项有哪些？\r\n\r\n\r\n\r\n## 2、什么是SaaS平台？\r\n\r\n![](https://cdn.ramostear.com/2019-05-28-07-06-26-4dbd6523bf2d4d52b5c9a78ef64e2ad6.png)\r\n\r\n<center>图 2-1 SaaS组成结构</center>\r\n\r\n在你决定实施SaaS品台架构设计前，你有必要先了解SaaS平台是什么。从宏观的角度来看，SaaS是一种软件应用程序交付方式，软件提供商集中化托管一个或多个软件应用程序，并通过互联网向租户体用这些软件应用程序。从分类上看，SaaS（软件即服务）也是云计算重要的一部分。目前国内主流的云服务提供商如阿里云、百度云、腾讯云等，为广大用户提供了不同业务需求的云服务，它们大致可以分为以下几类：\r\n\r\n- 1、基础设施即服务：如CPU、Network、Disk和Memory等\r\n- 2、平台即服务：如阿里云服务器和云数据库等\r\n- 3、软件即服务：阿里短信、阿里邮箱等\r\n- 4、数据即服务：如阿里云对象存储，七牛云存储等\r\n- 5、其他软件服务：机器学习、人工智能等\r\n\r\nSaaS应用程序的任何更新或者修复漏洞操作都是由软件提供商负责实施和处理的，由于租户是通过互联网获取软件服务，所以租户端无需下载任何的升级包或者修复补丁，是一种开箱即获取最新软件产品的服务方式。\r\n\r\n通过对什么是SaaS的介绍，接下来，我们了解一下选择SaaS作为软件架构来设计产品的一些理由。\r\n\r\n\r\n\r\n## 3、为什么选择SaaS?\r\n\r\n我们将从不同的角度来阐述几个为什么选择SaaS的理由。透过对这些因素的分析，为你是否需要将自己的软件SaaS化提供一定的参考依据。\r\n\r\n### 3.1、消费者角度\r\n\r\n获取软件服务的方式足够简单，SaaS也许是迄今为止使用软件最简单的方式之一，租户只需要动动鼠标和键盘，即可在几小时甚至几分钟内获得一个大型的软件服务。相比于传统使用软件的方式，租户省去了研发、部署、运维等一系列繁复的过程，且获得软件的时间和费用成本都大幅度降低。\r\n\r\n## 3.2、商业角度\r\n\r\nSaaS可以体用跨地域、跨平台的软件服务。与此同时，软件服务商可以统一对软件进行版本管理，这将带来以下几点好处（包括但不限于）：\r\n\r\n- 1、缩短产品上线时间：多端适配，统一版本，统一更新\r\n- 2、降低维护成本：不需要同时维护多个版本的软件实例，运维压力减小\r\n- 3、容易升级：由于版本得到有效控制，一次升级，即可覆盖所有租户端\r\n\r\n## 4、SaaS的特性和优势\r\n\r\n我们将SaaS应用程序与传统的桌面应用程序做一个水平的对比，部署一个SaaS产品将可以获得以下的几点优势。\r\n\r\n### 4.1、简单\r\n\r\nSaaS化的产品通过互联网向租户提供软件服务，随着Web技术（如jQuery、Node.js）的进步，Web页面的交互体验度大幅度提升，交互更流畅、更人性化。与传统的桌面应用程序的人机交互效果相差无几。\r\n\r\n### 4.2、经济实惠\r\n\r\nSaaS化产品可以为租户提供弹性的付费方案，如按日、按月、按年、按使用人数或者按使用量进行计费，它将给租户提供更经济的使用软件的财务预算表。\r\n\r\n### 4.3、安全\r\n\r\n使用SaaS产品无需担心数据安全问题，这好比将钱存入银行一样安全。相较于企业内部部署的软件系统而言，SaaS产品具备更高的安全保障能力，因为软件提供商具有更多软件安全防护的技术资源、人力资源和财政资源。\r\n\r\n### 4.4、兼容性\r\n\r\n与传统软件相比、SaaS软件的兼容性更好，它没有传统软件的多本版维护问题和操作系统兼容问题。在SaaS软件中，租户用户在使用软件的过程中，几乎上感觉不到软件发生了改变。当租户用户登录到系统上时，就已经获得了最新版本的软件。\r\n\r\n\r\n\r\n## 5、SaaS软件的适用范围\r\n\r\nSaaS产品具有广泛的适应范围，特别是与其他云产品（如IaaS(基础设施即服务)和PaaS(平台即服务)）配合使用时这种能力表现尤为突出，例如阿里云之类的云计算技术允许你配置可托管的Web站点、数据库服务器等。你只需要打开浏览器并登录到阿里云控制台，通过操作对应的控制面板，即可获得相关的软件服务。\r\n\r\n从理论上讲，SaaS可以将任何的软件SaaS，下面列举一些通用的分类供大家参考：\r\n\r\n- 1、Office在线办公类SaaS产品\r\n- 2、电子邮件和即时消息类SaaS产品\r\n- 3、社交媒体类SaaS产品\r\n- 4、第三方API类SaaS产品\r\n- 5、安全和访问控制类SaaS产品\r\n- 6、机器学习类SaaS产品\r\n- 7、人工智能类SaaS产品\r\n- 8、地理位置服务类SaaS产品\r\n- 9、数据流和数据检索类SaaS产品\r\n\r\n## 6、SaaS产品的天生缺陷\r\n\r\n![](https://cdn.ramostear.com/2019-05-28-07-06-47-1f5f51ee9eca4d00a1b7f426bfbf4aa7.png)\r\n\r\n<center>图 6-1 SaaS产品的缺点</center>\r\n\r\n从上图我们可以直观的看到，SaaS产品与生俱来的几个缺陷，接下来我们将逐一进行描述。\r\n\r\n\r\n\r\n### 6.1、软件控制权\r\n\r\n与企业内部部署的软件不同，由于SaaS软件被击中托管在服务提供商的Web服务器中，所以租户无法控制所有的软件应用程序，SaaS化的软件比企业自行部署的软件获得的控制权更少，租户可操作的自定义控制权极度有限。\r\n\r\n### 6.2、消费者基数小\r\n\r\n由于SaaS软件是将一套应用程序共享给一个或者多个租户共同使用，这种共享的消费方式还未被大多数的消费者所接受。同时，受制于市场环境的影响，目前还有大多数的软件还未SaaS化。\r\n\r\n### 6.3、性能瓶颈\r\n\r\n共享应用程序必然会带来服务器性能的下降、如计算速度、网络资源、I/O读写等都将面临严峻的考验。在性能方面，企业内部部署的“独享模式”的应用程序比SaaS软件的“共享模式”略胜一筹。\r\n\r\n### 6.4、安全问题\r\n\r\n当租户在选择一款SaaS产品时，产品的安全性将会被放置在第一位进行考虑。如数据的隔离、敏感数据的加密、数据访问权限控制、个人隐私等问题。在2018年5月25日，GDPR(General Data Protection Regulation)《通用数据保护条例》出现之后，越来越多的人开始重视数据安全问题。如何最大程度的打消租户的这一顾虑，需要服务提供商加强对自身信誉度的提升，以赢得租户的信赖。\r\n\r\n\r\n\r\n## 7、SaaS产品的核心组件\r\n\r\n不同类型的SaaS产品，由于要面对不同的用户愿景，可能在功能和业务上会有所不同，但任何一个SaaS产品，都具备以下几个共同的核心组件。\r\n\r\n![](https://cdn.ramostear.com/2019-05-28-07-07-45-19118cb442464b0fb46467478730b5d7.png)\r\n\r\n<center>图 7-1 SaaS 核心组件</center>\r\n\r\n### 7.1、安全组件\r\n\r\n在SaaS产品中，系统安全永远是第一位需要考虑的事情，如何保障租户数据的安全，是你首要的事情。这如同银行首选需要保障储户资金安全一样。安全组件就是统一的对SaaS产品进行安全防护，保障系统数据安全。\r\n\r\n### 7.2、数据隔离组件\r\n\r\n安全组件解决了用户数据安全可靠的问题，但数据往往还需要解决隐私问题，各企业之间的数据必须相互不可见，即相互隔离。在SaaS产品中，如何识别、区分、隔离个租户的数据时你在实施SaaS平台架构设计时需要考虑的第二个问题。\r\n\r\n### 7.3、可配置组件\r\n\r\n尽管SaaS产品在设计之初就考虑了大多数通用的功能，让租户开箱即用，但任然有为数不少的租户需要定制服务自身业务需求的配置项，如UI布局、主题、标识（Logo）等信息。正因为无法抽象出一个完全通用的应用程序，所以在SaaS产品中，你需要提供一个可用于自定义配置的组件。\r\n\r\n### 7.4、可扩展组件\r\n\r\n随着SaaS产品业务和租户数量的增长，原有的服务器配置将无法继续满足新的需求，系统性能将会与业务量和用户量成反比。此时，SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术，在多个服务器上部署多个软件运行示例并提供相同的软件服务，以此实现水平扩展SaaS产品的整体服务性能。为了实现可扩展能力，就需要SaaS展示层的代码与业务逻辑部分的代码进行分离，两者独立部署。例如使用VUE+微服务构建前后端分离且可水平进行扩展的分布式SaaS应用产品。对于可扩展，还有另外一种方式，即垂直扩展，其做法比较简单，也比较粗暴：通过增加单台服务器的配置，如购买性能更好的CUP、存储更大的内存条、增大带宽等措施，让服务器能够处理更多的用户请求。但此做法对于提升产品性能没有质的改变，且成本很高。\r\n\r\n### 7.5、0停机时间升级产品\r\n\r\n以往的软件在升级或者修复Bug是，都需要将运行的程序脱机一段时间，等待升级或修复工作完成后，再重新启动应用程序。而SaaS产品则需要全天候保障服务的可用性。这就需要你考虑如何实现在不重启原有应用程序的情况下，完成应用程序的升级修复工作。\r\n\r\n### 7.6、多租户组件\r\n\r\n要将原有产品SaaS化，就必须提供多租户组件，多租户组件是衡量一个应用程序是否具备SaaS服务能力的重要指标之一。SaaS产品需要同时容纳多个租户的数据，同时还需要保证各租户之间的数据不会相互干扰，保证租户中的用户能够按期望索引到正确的数据，多租户组件是你必须要解决的一个问题。其余的组件都将围绕此组件展开各自的业务。\r\n\r\n\r\n\r\n## 总结\r\n\r\n本文将软件应用程序的发展历程作为切入点，并围绕WHAT(是什么？)、WHY(为什么？)、WHERE(在哪些领域实施？)和HOW(怎么样？)这四个问题对SaaS展开了介绍。文中详细的阐述了基于SaaS架构的软件设计需要注意的问题，并分析了SaaS产品的特性、有点、缺点。最后还介绍了基于SaaS架构的软件产品应该具备的几个核心组件以及他们各自的作用。希望本次能够让你对SaaS平台架构有一个全面的了解，并且在你准备实施SaaS平台架构设计前能够提供一些价值的参考信息。',1,'','2019/20/21/im2l2t9c.html',1,'从宏观的角度来看，SaaS是一种软件应用程序交付方式，软件提供商集中化托管一个或多个软件应用程序，并通过互联网向租户体用这些软件应用程序。从分类上看，SaaS（软件即服务）也是云计算重要的一部分','blog.html','https://cdn.ramostear.com/2019-05-28-07-07-59-9febef7ff09b4c3c983475e8c649ed01.png;base64','架构师必备技能指南：SaaS(软件即服务)架构设计','',3551),(17,'2019-11-21 23:24:59',0,'2019-11-21 23:32:00',1,1,1,'谭朝红','今天，我分享的主题是：在一个项目中同时使用Hibernate和Mybatis两个ORM框架。','<h1 id=\"h1--hibernate-mybatis-\"><a name=\"鱼与熊掌得兼：Hibernate与Mybatis共存\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>鱼与熊掌得兼：Hibernate与Mybatis共存</h1><blockquote>\r\n<p>很长一段时间，网上有很多关于Hibernate与Mybatis孰优孰劣的争论，两个阵营的人谁也不能说服谁，每个人的理由都很有道理。今天，我分享的主题是：在一个项目中同时使用Hibernate和Mybatis两个ORM框架。</p>\r\n</blockquote>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-36-06-74f2465d86054cc5a0a794a173d93ada.png\" alt=\"\">\r\n<p>​    作为一个开发者，没有必要花费过多的时间去证明技术无用论，当你开始指责某个框架垃圾，另外一个框架最好时，隐性的暴露出你对某个框架没有深入的研究，无知的指责对于技术的提升没有任何的帮助。框架本身没有对错一说，只有适合和更适合项目的选择。任何框架都有自身的能力范围，就拿Hibernate和Mybatis这两个ORM框架来说，Hibernate封装了很多有用的API给开发者，降低了操作数据库的难度和复杂度，同时也减少了模板代码的数量，但Hibernate留给开发者可操作的空间相对Mybatis少了很多；Mybatis框架使用起来很灵活，开发者可以自定义查询语句，但增加了模板代码的数量，看起来没有Hibernate那么便捷。两种框架在便捷与灵活两个指标上做出了取舍与妥协，这不能说是框架的错。对于一个框架而言，需要有自身专注的领域和设计愿景，不可能面面俱到。</p>\r\n<p>​    使用任何一种技术框架，都需要贴合现实的业务需求以及自身的技术能力。当你还没有深入的去了解一门技术或者当前业务需求无法与框架契合时，不要盲目的批判框架的好坏。今天，我不再去对比Hibernate与Mybatis两者之间的优劣，而是给出一个比较中庸的放方案，将两个ORM框架同时整合在一个项目中。</p>\r\n<h2 id=\"h2--\"><a name=\"一、准备开发环境\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>一、准备开发环境</h2><p>​    如果你想成功运行本文中的源代码，需要满足一下的几个条件：</p>\r\n<ul>\r\n<li>1、JDK : JDK 1.8.x及以上版本</li><li>2、Maven : Maven 3.x或更高版本</li><li>3、Git：版本控制工具，选择一个你喜欢的</li><li>4、IDE : 选择你比较喜欢的一个代码编辑器，如STS、IntelliJ IDEA。笔者使用的是IntelliJ IDEA</li><li>5、Databases : 选择一个你熟练使用的数据库系统。笔者在本文中使用的是MySQL 5.1.x版本的数据库系统</li></ul>\r\n<blockquote>\r\n<p>如需获取本次分享内容的源代码进调试，可以到文章末尾找到源代码仓库连接</p>\r\n</blockquote>\r\n<p>​    </p>\r\n<h2 id=\"h2--\"><a name=\"二、搭建项目\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>二、搭建项目</h2><h3 id=\"h3-2-1-\"><a name=\"2-1、引入依赖\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2-1、引入依赖</h3><p>​    为了快速构建项目，笔者采用Spring Boot来构建项目，同时使用加入Spring Data JPA和Mybatis两个ORM框架的依赖包。在此需要特别说明，Hibernate是一个JPA标准的实现，尔Spring Data JPA是一个JPA数据访问抽象，通过Spring Data JPA，可以轻松使用Hibernate框架。</p>\r\n<p>​    你可以通过Spring Initializer来初始化项目，也可以通过IDEA自带的Spring Initializer功能构建项目，项目构建完成之后，pom.xml文件中的配置如下（包含但不限于文中给出的依赖项）：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-36-24-47e29048ab7442b5a1fba58092299033.png\" alt=\"\">\r\n<h3 id=\"h3-2-2-user-java\"><a name=\"2-2、定义实体类-User.java\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2-2、定义实体类-User.java</h3><p>​    为了演示同时使用Hibernate和Mybatis操作数据库，需要提供一个实体类User.java，代码如下所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-36-41-ab8372f5675b4247b0c8227f37221aa3.png\" alt=\"\">\r\n<blockquote>\r\n<p>说明：</p>\r\n<p>在本次演示的项目中，使用到了Lombok插件，它可以让开发者减少模板代码的书写，提高开发速度。<a href=\"https://github.com/Data\" title=\"@Data\" class=\"at-link\">@Data</a>注解可以自动生成类属性的getter、setter和toString方法。<a href=\"https://github.com/NoArgsConstructor\" title=\"@NoArgsConstructor\" class=\"at-link\">@NoArgsConstructor</a>会自动为类生成无参构造函数，<a href=\"https://github.com/AllArgsConstructor\" title=\"@AllArgsConstructor\" class=\"at-link\">@AllArgsConstructor</a>则会生成带全部属性的构造函数。</p>\r\n</blockquote>\r\n<h3 id=\"h3-2-3-\"><a name=\"2-3、定义数据持久化接口\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2-3、定义数据持久化接口</h3><p>​    在本次课程中，将使用Spring Data JPA来完成写操作，如新增、修改、删除；使用Mybatis来完成读操作，如根据用户ID查询、查询所有的用户等。Spring Data JPA和MyBatis的持久化接口都位于com.ramostear.hm.orm包下，Spring Data JPA的持久化接口相对比较简单，之间继承JpaRepository类即可，代码如下：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-36-56-9ed2f5f38e57450b97de885bc57ad784.png\" alt=\"\">\r\n<blockquote>\r\n<p>说明：因为JPA只负责写操作，所以直接继承并使用JpaRepository提供的API即可，不需要额外的定义其他的接口方法。</p>\r\n</blockquote>\r\n<p>​    下面是Mybatis的映射接口，定义了两个方法：根据ID查询用户信息和查询所有的用户信息。代码如下所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-37-13-9f96787ce52d49488fdaea9b6c753f37.png\" alt=\"\">\r\n<blockquote>\r\n<p>说明：</p>\r\n<p>此接口需要注意的地方是<a href=\"https://github.com/Component\" title=\"@Component\" class=\"at-link\">@Component</a>和<a href=\"https://github.com/Mapper\" title=\"@Mapper\" class=\"at-link\">@Mapper</a>注解，<a href=\"https://github.com/Component\" title=\"@Component\" class=\"at-link\">@Component</a>注解标注此接口后，Spring会自动扫描并配置此类；<a href=\"https://github.com/Mapper\" title=\"@Mapper\" class=\"at-link\">@Mapper</a>注解是把这个mapper的DAO交由Spring进行管理。</p>\r\n</blockquote>\r\n<p>定义完Mybatis 映射接口后，需要提供一个进行数据库查询的xml配置文件。该文件位于resources/mapper文件夹中，UserMapper.xml完整代码如下：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-37-27-06ae26c23cd84877a0ac30bdef01732f.png\" alt=\"\">\r\n<h3 id=\"h3-2-4-userservice\"><a name=\"2-4、定义UserService\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2-4、定义UserService</h3><p>​    在UserService接口中，提供三个方法：保存用户信息、根据ID查询用户、查询所有的用户。UserService接口代码如下：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-37-50-14cec1361bf648c8b6574953c4a5e96d.png\" alt=\"\">\r\n<p>在UserService接口的实现类中，需要同时注入UserRepository和UserMapper两个依赖。我们使用构造函数的方式来注入这两个依赖。代码如下：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-38-05-2e0e26c545e84373b2b30c8c56b36be7.png\" alt=\"\">\r\n<blockquote>\r\n<p>说明：</p>\r\n<p><a href=\"https://github.com/Transactional\" title=\"@Transactional\" class=\"at-link\">@Transactional</a>注解用于设置每个方法的事务控制方式。<a href=\"https://github.com/Service\" title=\"@Service\" class=\"at-link\">@Service</a>注解声明该类是一个服务提供类，且设置了该类被Spring初始化时Bean对象的名称为“userService”。</p>\r\n</blockquote>\r\n<h3 id=\"h3-2-5-\"><a name=\"2-5、定义控制器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2-5、定义控制器</h3><p>​    最后，提供一个控制器，用于处理客户端的相关请求。在控制器中，提供了三个请求处理方法，分别处理客户端新增用户、根据ID查询用户和查询所有用户的请求。控制器代码如下：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-38-25-7c72d9e4d43b4b6abb55b914879f349f.png\" alt=\"\">\r\n<blockquote>\r\n<p>说明：</p>\r\n<p>在本次教程中，为了编码IDEA报警告，所有的依赖注入都采用构造函数的方式注入相关的依赖。</p>\r\n</blockquote>\r\n<h2 id=\"h2--hibernate-mybatis\"><a name=\"三、配置Hibernate和Mybatis\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>三、配置Hibernate和Mybatis</h2><p>​    网络上有很多关于在Spring Boot项目中配置Hibernate和Mybatis的教程，但同时配置Hibernate和Mybatis的文章很少，有一些是通过Java代码的方式对这两个ORM框架进行配置，采用的是多数据源的方法来整合两个框架。其实整合这两个框架没有想象中的那么难，只需要在application.yml或者application.properties配置文件中加入几行代码，就可以完成两个框架的整合。以application.yml配置文件为例，配置代码如下：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-38-42-fa0c2695a5674e52b920707cd3e47edf.png\" alt=\"\">\r\n<p>是不是很简单，并为没有太多复杂的配置，这是一种较为简单的整合方式。Hibernate和Mybatis共用一个数据源，如果是JPA的忠实粉丝，现在想要使用Mybatis，只需要额外加入mybatis的配置即可。</p>\r\n<h2 id=\"h2--\"><a name=\"四、测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>四、测试</h2><p>​    通过以上的几个步骤，整个项目已经搭建完毕，接下来将使用Postman测试工具对Controller的三个方法进行测试，验证两个ORM框架在同一个项目中是否能共存。</p>\r\n<p>​    首先测试 POST <a href=\"http://localhost/users\">http://localhost/users</a> ，验证Hibernate是否能够成功将用户信息持久化。打开Postman工具，在地址栏输入<a href=\"http://localhost/users请求地址，请求方式选择POST,在Body栏输入如下的信息：\">http://localhost/users请求地址，请求方式选择POST,在Body栏输入如下的信息：</a></p>\r\n<pre><code class=\"lang-json\">{\r\n    \"username\":\"谭朝红\",\r\n    \"alias\":\"ramostear\",\r\n    \"age\":28\r\n}\r\n</code></pre>\r\n<p>点击“Send”按钮发送请求，观察服务端响应信息，测试结果如下图所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-38-59-bf51be9dee0b4e6ab0c04b58cd366dfa.png\" alt=\"\">\r\n<p>可以看到，服务端成功返回用户信息，且用户ID=3。接下来，我们请求 GET <a href=\"http://localhost/users/3\">http://localhost/users/3</a> ,验证Mybatis是否能够成功查询出用户信息，测试结果如下：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-06-06-14-39-14-70c8877335314f5aa7230622c8ccc451.png\" alt=\"\">\r\n<p>通过测试，服务端成功返回了用户ID=3的用户信息：</p>\r\n<pre><code class=\"lang-json\">{\r\n  \"id\": 3,\r\n  \"username\": \"谭朝红\",\r\n  \"alias\": \"ramostear\",\r\n  \"age\": 28\r\n}\r\n</code></pre>\r\n<p>由此证明，在同一个项目中，Hibernate和Mybatis均能正常工作，整合方案有效，解决了在同一项目中Hibernate与Mybatis共存的问题。</p>\r\n<h2 id=\"h2--\"><a name=\"五、总结\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>五、总结</h2><p>​    本次课程验证了同时使用Hibernate和Mybatis两个ORM框架的方案可行，且采用了一种比较简单的方式来整合两个框架，摒弃了多数据源的复杂配置，快速实现两个框架并用的需求。</p>\r\n<p>​    在一个项目中同时使用两个ORM框架有没有实际的意义呢？我的答案是肯定的。同时使用两个ORM框架，两者之间可以相互弥补自身的不足，以达到灵活性和便捷性同时兼顾，另外一方面，在单独使用Mybatis时，开发者需要手动或者借助其他的工具生成数据库表信息，而采用本文的整合方案，Mybatis可以借助JPA自动生成数据库表的能力，从而简化使用Mybatis的步骤。最后，对于一些读多于写的系统，完全可以将这两个框架同时使用，写操作少的模块，可以使用Spring Data JPA快速完成相关功能的实现，对于读操作部分，则可以利用Mybatis来优化查询语句。两者之间的优势互补，能进一步的提升开发效率和系统性能。</p>\r\n<blockquote>\r\n<p>本次分享内容的源代码仓库地址：</p>\r\n<p><a href=\"https://github.com/ramostear/orm-hm.git\">https://github.com/ramostear/orm-hm.git</a></p>\r\n</blockquote>\r\n','Hibernate,Mybatis',0,'# 鱼与熊掌得兼：Hibernate与Mybatis共存\r\n\r\n> 很长一段时间，网上有很多关于Hibernate与Mybatis孰优孰劣的争论，两个阵营的人谁也不能说服谁，每个人的理由都很有道理。今天，我分享的主题是：在一个项目中同时使用Hibernate和Mybatis两个ORM框架。\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-36-06-74f2465d86054cc5a0a794a173d93ada.png)\r\n\r\n​	作为一个开发者，没有必要花费过多的时间去证明技术无用论，当你开始指责某个框架垃圾，另外一个框架最好时，隐性的暴露出你对某个框架没有深入的研究，无知的指责对于技术的提升没有任何的帮助。框架本身没有对错一说，只有适合和更适合项目的选择。任何框架都有自身的能力范围，就拿Hibernate和Mybatis这两个ORM框架来说，Hibernate封装了很多有用的API给开发者，降低了操作数据库的难度和复杂度，同时也减少了模板代码的数量，但Hibernate留给开发者可操作的空间相对Mybatis少了很多；Mybatis框架使用起来很灵活，开发者可以自定义查询语句，但增加了模板代码的数量，看起来没有Hibernate那么便捷。两种框架在便捷与灵活两个指标上做出了取舍与妥协，这不能说是框架的错。对于一个框架而言，需要有自身专注的领域和设计愿景，不可能面面俱到。\r\n\r\n​	使用任何一种技术框架，都需要贴合现实的业务需求以及自身的技术能力。当你还没有深入的去了解一门技术或者当前业务需求无法与框架契合时，不要盲目的批判框架的好坏。今天，我不再去对比Hibernate与Mybatis两者之间的优劣，而是给出一个比较中庸的放方案，将两个ORM框架同时整合在一个项目中。\r\n\r\n\r\n\r\n## 一、准备开发环境\r\n\r\n​	如果你想成功运行本文中的源代码，需要满足一下的几个条件：\r\n\r\n- 1、JDK : JDK 1.8.x及以上版本\r\n- 2、Maven : Maven 3.x或更高版本\r\n- 3、Git：版本控制工具，选择一个你喜欢的\r\n- 4、IDE : 选择你比较喜欢的一个代码编辑器，如STS、IntelliJ IDEA。笔者使用的是IntelliJ IDEA\r\n- 5、Databases : 选择一个你熟练使用的数据库系统。笔者在本文中使用的是MySQL 5.1.x版本的数据库系统\r\n\r\n> 如需获取本次分享内容的源代码进调试，可以到文章末尾找到源代码仓库连接\r\n\r\n​	\r\n\r\n## 二、搭建项目\r\n\r\n### 2-1、引入依赖\r\n\r\n​	为了快速构建项目，笔者采用Spring Boot来构建项目，同时使用加入Spring Data JPA和Mybatis两个ORM框架的依赖包。在此需要特别说明，Hibernate是一个JPA标准的实现，尔Spring Data JPA是一个JPA数据访问抽象，通过Spring Data JPA，可以轻松使用Hibernate框架。\r\n\r\n​	你可以通过Spring Initializer来初始化项目，也可以通过IDEA自带的Spring Initializer功能构建项目，项目构建完成之后，pom.xml文件中的配置如下（包含但不限于文中给出的依赖项）：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-36-24-47e29048ab7442b5a1fba58092299033.png)\r\n\r\n\r\n\r\n### 2-2、定义实体类-User.java\r\n\r\n​	为了演示同时使用Hibernate和Mybatis操作数据库，需要提供一个实体类User.java，代码如下所示：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-36-41-ab8372f5675b4247b0c8227f37221aa3.png)\r\n\r\n> 说明：\r\n>\r\n> 在本次演示的项目中，使用到了Lombok插件，它可以让开发者减少模板代码的书写，提高开发速度。@Data注解可以自动生成类属性的getter、setter和toString方法。@NoArgsConstructor会自动为类生成无参构造函数，@AllArgsConstructor则会生成带全部属性的构造函数。\r\n\r\n\r\n\r\n### 2-3、定义数据持久化接口\r\n\r\n​	在本次课程中，将使用Spring Data JPA来完成写操作，如新增、修改、删除；使用Mybatis来完成读操作，如根据用户ID查询、查询所有的用户等。Spring Data JPA和MyBatis的持久化接口都位于com.ramostear.hm.orm包下，Spring Data JPA的持久化接口相对比较简单，之间继承JpaRepository类即可，代码如下：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-36-56-9ed2f5f38e57450b97de885bc57ad784.png)\r\n\r\n> 说明：因为JPA只负责写操作，所以直接继承并使用JpaRepository提供的API即可，不需要额外的定义其他的接口方法。\r\n\r\n​	下面是Mybatis的映射接口，定义了两个方法：根据ID查询用户信息和查询所有的用户信息。代码如下所示：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-37-13-9f96787ce52d49488fdaea9b6c753f37.png)\r\n\r\n> 说明：\r\n>\r\n> 此接口需要注意的地方是@Component和@Mapper注解，@Component注解标注此接口后，Spring会自动扫描并配置此类；@Mapper注解是把这个mapper的DAO交由Spring进行管理。\r\n\r\n定义完Mybatis 映射接口后，需要提供一个进行数据库查询的xml配置文件。该文件位于resources/mapper文件夹中，UserMapper.xml完整代码如下：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-37-27-06ae26c23cd84877a0ac30bdef01732f.png)\r\n\r\n\r\n\r\n### 2-4、定义UserService\r\n\r\n​	在UserService接口中，提供三个方法：保存用户信息、根据ID查询用户、查询所有的用户。UserService接口代码如下：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-37-50-14cec1361bf648c8b6574953c4a5e96d.png)\r\n\r\n在UserService接口的实现类中，需要同时注入UserRepository和UserMapper两个依赖。我们使用构造函数的方式来注入这两个依赖。代码如下：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-38-05-2e0e26c545e84373b2b30c8c56b36be7.png)\r\n\r\n> 说明：\r\n>\r\n> @Transactional注解用于设置每个方法的事务控制方式。@Service注解声明该类是一个服务提供类，且设置了该类被Spring初始化时Bean对象的名称为“userService”。\r\n\r\n\r\n\r\n### 2-5、定义控制器\r\n\r\n​	最后，提供一个控制器，用于处理客户端的相关请求。在控制器中，提供了三个请求处理方法，分别处理客户端新增用户、根据ID查询用户和查询所有用户的请求。控制器代码如下：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-38-25-7c72d9e4d43b4b6abb55b914879f349f.png)\r\n\r\n> 说明：\r\n>\r\n> 在本次教程中，为了编码IDEA报警告，所有的依赖注入都采用构造函数的方式注入相关的依赖。\r\n\r\n\r\n\r\n## 三、配置Hibernate和Mybatis\r\n\r\n​	网络上有很多关于在Spring Boot项目中配置Hibernate和Mybatis的教程，但同时配置Hibernate和Mybatis的文章很少，有一些是通过Java代码的方式对这两个ORM框架进行配置，采用的是多数据源的方法来整合两个框架。其实整合这两个框架没有想象中的那么难，只需要在application.yml或者application.properties配置文件中加入几行代码，就可以完成两个框架的整合。以application.yml配置文件为例，配置代码如下：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-38-42-fa0c2695a5674e52b920707cd3e47edf.png)\r\n\r\n是不是很简单，并为没有太多复杂的配置，这是一种较为简单的整合方式。Hibernate和Mybatis共用一个数据源，如果是JPA的忠实粉丝，现在想要使用Mybatis，只需要额外加入mybatis的配置即可。\r\n\r\n\r\n\r\n## 四、测试\r\n\r\n​	通过以上的几个步骤，整个项目已经搭建完毕，接下来将使用Postman测试工具对Controller的三个方法进行测试，验证两个ORM框架在同一个项目中是否能共存。\r\n\r\n​	首先测试 POST http://localhost/users ，验证Hibernate是否能够成功将用户信息持久化。打开Postman工具，在地址栏输入http://localhost/users请求地址，请求方式选择POST,在Body栏输入如下的信息：\r\n\r\n```json\r\n{\r\n	\"username\":\"谭朝红\",\r\n	\"alias\":\"ramostear\",\r\n	\"age\":28\r\n}\r\n```\r\n\r\n点击“Send”按钮发送请求，观察服务端响应信息，测试结果如下图所示：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-38-59-bf51be9dee0b4e6ab0c04b58cd366dfa.png)\r\n\r\n可以看到，服务端成功返回用户信息，且用户ID=3。接下来，我们请求 GET http://localhost/users/3 ,验证Mybatis是否能够成功查询出用户信息，测试结果如下：\r\n\r\n![](https://cdn.ramostear.com/2019-06-06-14-39-14-70c8877335314f5aa7230622c8ccc451.png)\r\n\r\n通过测试，服务端成功返回了用户ID=3的用户信息：\r\n\r\n```json\r\n{\r\n  \"id\": 3,\r\n  \"username\": \"谭朝红\",\r\n  \"alias\": \"ramostear\",\r\n  \"age\": 28\r\n}\r\n```\r\n\r\n由此证明，在同一个项目中，Hibernate和Mybatis均能正常工作，整合方案有效，解决了在同一项目中Hibernate与Mybatis共存的问题。\r\n\r\n\r\n\r\n## 五、总结\r\n\r\n​	本次课程验证了同时使用Hibernate和Mybatis两个ORM框架的方案可行，且采用了一种比较简单的方式来整合两个框架，摒弃了多数据源的复杂配置，快速实现两个框架并用的需求。\r\n\r\n​	在一个项目中同时使用两个ORM框架有没有实际的意义呢？我的答案是肯定的。同时使用两个ORM框架，两者之间可以相互弥补自身的不足，以达到灵活性和便捷性同时兼顾，另外一方面，在单独使用Mybatis时，开发者需要手动或者借助其他的工具生成数据库表信息，而采用本文的整合方案，Mybatis可以借助JPA自动生成数据库表的能力，从而简化使用Mybatis的步骤。最后，对于一些读多于写的系统，完全可以将这两个框架同时使用，写操作少的模块，可以使用Spring Data JPA快速完成相关功能的实现，对于读操作部分，则可以利用Mybatis来优化查询语句。两者之间的优势互补，能进一步的提升开发效率和系统性能。\r\n\r\n\r\n\r\n> 本次分享内容的源代码仓库地址：\r\n>\r\n> https://github.com/ramostear/orm-hm.git\r\n>\r\n> ',1,'','2019/23/21/880936a3.html',1,'今天，我分享的主题是：在一个项目中同时使用Hibernate和Mybatis两个ORM框架。','blog.html','https://cdn.ramostear.com/2019-06-06-14-39-40-55703746b79242f095eedebff6a4c005.png;base64','鱼与熊掌得兼：Hibernate与Mybatis共存','',2415),(18,'2019-11-21 23:28:26',0,'2019-11-21 23:31:41',1,1,1,'谭朝红','Stream是Java中数据源的包装器，通过Stream我们可以快速的对数据源进行操作，且Stream不对任何数据进行存储','<h1 id=\"h1--java-8-stream-api\"><a name=\"技术精讲：Java 8 Stream API\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>技术精讲：Java 8 Stream API</h1><blockquote>\r\n<p>Stream(流)是在Java 8中新增的新特性，首先需要为Java 8 Stream正名：Java 8中的Stream跟Java I/O Stream(例如：InputStream,OutputStream等)没有任何的关系。Stream是Java中数据源的包装器，通过Stream我们可以快速的对数据源进行操作（例如：过滤，排序、求和等等），且Stream不对任何数据进行存储，所以Stream也不是数据结构。</p>\r\n</blockquote>\r\n<p>​    在Java 8中，Stream增强了Array,List等对象操作数据的能力，Stream提供了一些列的方法使的这些原有的集合类可以轻松创建Stream对象实例。通过Stream API提供的静态方法，可以快速的生成有限/无限的数据流。特别指出，Stream只是增强了原有集合类的能力，并未对底层的源码做任何的修改。</p>\r\n<h2 id=\"h2-1-stream-\"><a name=\"1.Stream的工作流程\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1.Stream的工作流程</h2><p>​    接下来，将介绍使用Java Stream的基本步骤。在Java 8中，Stream的生命周期一共有三个阶段：</p>\r\n<ul>\r\n<li>1.获取数据源并创建Stream实例。数据源可以是数组、列表、对象或者I/O流</li><li>2.执行中间操作（Intermediate Operations）。中间操作可以是过滤、排序、类型转换等操作</li><li>3.执行终端操作（Terminal Operation）。终端操作主要是对最终结果进行计数、求和、创建新集合等操作</li></ul>\r\n<p>图1-1展示了Java 8 Stream的工作工作流程。</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-56-21-bce20665e181434cb032488659080aff.png\" alt=\"Stream工作流程\" title=\"Stream工作流程\">\r\n<center>图 1-1 Stream工作流程</center>\r\n\r\n<h2 id=\"h2-2-stream-\"><a name=\"2.Stream 操作管道是什么？\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2.Stream 操作管道是什么？</h2><p>​    所谓的Stream操作管道是指“中间操作”和“终端操作”的组合。在Java 8 Stream API中，许多的操作方法都将当前的Stream作为最终结果返回，这就允许开发人员以连式编程的方式，组合出更大的Stream操作管道。Java 8 Stream API位于java.util.stream包下，其组织结构如图2-1所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-57-02-035073f6008f42f3abdd5ad36486902b.png\" alt=\"java.util.stream包\" title=\"java.util.stream包\">\r\n<center>图2-1 java.util.stream包</center>\r\n\r\n<p>接下来，将详细介绍“中间操作”和“终端操作”各自的方法和特性。</p>\r\n<h2 id=\"h2-3-stream-intermediate-operations-\"><a name=\"3.Stream中间操作（Intermediate Operations）是什么？\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.Stream中间操作（Intermediate Operations）是什么？</h2><p>​    Stream的中间操作将返回一个Stream,它允许开发者以查询的方式调用多个其他的操作。特别注意的是，在“终端操作”方法未被调用之前，“中间操作”的方法不会被执行。Stream的“中间操作”一共包括七个：Stream.filter,Stream.map,Stream.flatmap,Stream.peek,Stream.sorted和Stream.limit。</p>\r\n<h3 id=\"h3-3-1-stream-filter\"><a name=\"3.1.Stream.filter\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.1.Stream.filter</h3><p>​    Stream.filter将返回一个包含与之谓词相匹配的元素的新的Stream。下面将通过一个示例，演示Stream.filter的使用方法。</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-57-19-43507693f53b4349962b271e03ca8e1c.png\" alt=\"Stream.filter\" title=\"Stream.filter\">\r\n<p>执行结果：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-57-37-ab1fcd1eb26649ca9c6f2c50876f1d72.png\" alt=\"\">\r\n<p>此示例使用“中间操作”Stream.filter过滤以“j”开头的字符串,并使用“终端操作”Stream.count统计以“j”开头的字符串数量。</p>\r\n<h3 id=\"h3-3-2-stream-map\"><a name=\"3.2.Stream.map\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.2.Stream.map</h3><p>​    Stream.map将使用java.util.function.Function提供的方法转换Stream中的元素。Stream.map操作通常用于转换集合对象。下面是使用Stream.map的示例代码：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-57-50-9ddf17329523491a92f6e3d35b257041.png\" alt=\"Stream.map\" title=\"Stream.map\">\r\n<p>执行结果：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-58-04-192b4e270d85412cac6cf900dd583211.png\" alt=\"\">\r\n<h3 id=\"h3-3-3-stream-flatmap\"><a name=\"3.3.Stream.flatmap\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.3.Stream.flatmap</h3><p>​    Stream.flatmap通常将Stream中的每一个元素转换成0个或多个元素。下面将通过flatmap来统计一个文本中单词的出现次数（不重复）。示例代码如下：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-58-17-2ddd2fbf06b348f5b4e634e2e7db27bf.png\" alt=\"Stream.flatmap\" title=\"Stream.flatmap\">\r\n<p>执行结果：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-58-36-f2218436ee4e406c9aa18c40588f2917.png\" alt=\"\">\r\n<p>source.txt内容：</p>\r\n<pre><code class=\"lang-tex\">java 8 stream example by ramostear\r\n</code></pre>\r\n<h3 id=\"h3-3-4-stream-peek\"><a name=\"3.4.Stream.peek\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.4.Stream.peek</h3><p>​    Stream.peek在调试期间非常有用,它允许您在操作Stream之前查看Stream内部的数据。下面是示例代码：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-58-52-abe2a1c712af48b79d4b5ab00188cc15.png\" alt=\"\">\r\n<p>执行结果：</p>\r\n<pre><code class=\"lang-tex\">Java\r\nC#\r\nC++\r\nGO\r\nresult size = 4\r\n</code></pre>\r\n<h3 id=\"h3-3-5-stream-distinct\"><a name=\"3.5.Stream.distinct\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.5.Stream.distinct</h3><p>​    Stream.distinct是根据其内部的equals方法在Stream中对元素进行去重操作。下面是使用示例：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-59-06-50151efebfe44083a46478e50e5664d9.png\" alt=\"\">\r\n<p>执行结果：</p>\r\n<pre><code class=\"lang-tex\">6\r\n1\r\n8\r\n11\r\n</code></pre>\r\n<h3 id=\"h3-3-6-stream-sorted\"><a name=\"3.6.Stream.sorted\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.6.Stream.sorted</h3><p>​    Stream.sorted方法用于将Stream中的数据元素进行排序。下面是使用示例：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-59-20-082a59fab5a94d80b8b27da697764ca1.png\" alt=\"\">\r\n<p>执行结果：</p>\r\n<pre><code class=\"lang-tex\">1\r\n6\r\n8\r\n11\r\n</code></pre>\r\n<h3 id=\"h3-3-7-stream-limit\"><a name=\"3.7.Stream.limit\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.7.Stream.limit</h3><p>​    Stream.limit方法用于限定Stream中元素的个数。下面是使用示例：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-59-39-04bb3e85f831433585fc34fb9903bdd2.png\" alt=\"\">\r\n<p>执行结果：</p>\r\n<pre><code class=\"lang-tex\">0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n11\r\n618\r\n</code></pre>\r\n<h2 id=\"h2-4-stream-terminal-operations-\"><a name=\"4.Stream 终端操作（Terminal  Operations）是什么？\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.Stream 终端操作（Terminal  Operations）是什么？</h2><p>​    终端操作用于生产最终的数据结果，如对象，数组或者列表。在终端操作方法被执行前，中间操作方法将不会被执行。终端操作方法一共有十二个，它们是：forEach,toArray,reduce,collect,min,max,count,anymatch,allMatch,noneMatch,findFirst和findAny。表4-1列举了这十二个终端操作的使用方法：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-22-59-56-957b09752ab44e6cbe245c709c7063c1.png\" alt=\"Stream终端操作方法\" title=\"Stream终端操作方法\">\r\n<center>图 4-1 Stream终端操作方法</center>\r\n\r\n\r\n\r\n<h2 id=\"h2-5-stream-\"><a name=\"5.如何创建Stream实例？\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5.如何创建Stream实例？</h2><p>​    Stream支持多种数据源创建Stream实例，如Array,List,Object和I/O流。接下来，通过简单的示例来演示如何创建Stream。</p>\r\n<h3 id=\"h3-5-1-stream\"><a name=\"5.1 使用数组创建Stream\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5.1 使用数组创建Stream</h3><p>​    首先，我们定义一个静态的数组users,并初始几条用户数据，代码如下：</p>\r\n<pre><code class=\"lang-java\">private static User[] users = {new User(\"user1\"),new User(\"user2\"),new User(\"user3\")};\r\n</code></pre>\r\n<p>接下来，将利用users来创建一个Stream对象实例，示例代码如下：</p>\r\n<pre><code class=\"lang-java\">public static void fromArray() {\r\n    Stream&lt;User&gt; uStream = Stream.of(users);\r\n    System.out.println(\"create stream from array.\");\r\n    uStream.forEach(u-&gt;System.out.println(u.getUsername()));\r\n}\r\n</code></pre>\r\n<h3 id=\"h3-5-2-stream\"><a name=\"5.2 使用对象创建Stream\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5.2 使用对象创建Stream</h3><p>​    沿用上面定义的users数组，通过数组下标获得用户对象，并利用这些对象创建一个Stream实例，代码如下：</p>\r\n<pre><code class=\"lang-java\">public static void fromObjects() {\r\n    Stream&lt;User&gt; uStream = Stream.of(users[0],users[1],users[2]);\r\n    System.out.println(\"create stream from objects.\");\r\n    uStream.forEach(u-&gt;System.out.println(u.getUsername()));\r\n}\r\n</code></pre>\r\n<h3 id=\"h3-5-3-list-stream\"><a name=\"5.3 使用List创建Stream\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5.3 使用List创建Stream</h3><p>​    接下来，我们将users数组转换成List，并使用此List来创建Stream实例。代码如下：</p>\r\n<pre><code class=\"lang-java\">public static void fromList() {\r\n    List&lt;User&gt; list = Arrays.asList(users);\r\n    Stream&lt;User&gt; uStream = list.stream();\r\n    System.out.println(\"create stream from list.\");\r\n    uStream.forEach(u-&gt;System.out.println(u.getUsername()));\r\n}\r\n</code></pre>\r\n<h3 id=\"h3-5-4-builder-stream\"><a name=\"5.4 使用builder方法创建Stream\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>5.4 使用builder方法创建Stream</h3><p>​    最后，我们将演示使用Stream内置的builder()静态方法创建Stream实例。代码如下：</p>\r\n<pre><code class=\"lang-java\">public static void byBuilder() {\r\n    Stream.Builder&lt;User&gt; userStream = Stream.builder();\r\n    userStream.accept(users[0]);\r\n    userStream.accept(users[1]);\r\n    userStream.accept(users[2]);\r\n\r\n    System.out.println(\"create stream by builder.\");\r\n    userStream.build().forEach(u-&gt;System.out.println(u.getUsername()));\r\n\r\n}\r\n</code></pre>\r\n<p>​    以上就是通过数据源创建Stream实例的几种不同方式。</p>\r\n<h2 id=\"h2-6-stream-\"><a name=\"6.为什么要使用Stream?\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>6.为什么要使用Stream?</h2><p>​    在命令式编程中，我们必须逐行编写代码，才能完成相关的计算。例如，计算1~10000的数的总和，我们需要使用for(int i=1;i&lt;=10000;i++){….}循环语句来迭代求值。在这个语句中，我们需要花费额外的精力来维护loop变量的值。而在声明式编程中，我们只需要关注想要实现的目标，而不是其内部重复的循环逻辑。Stream API通过使用内部迭代器和lambda表达式，帮助我们完成了那些重复性的逻辑。Stream API的作用不仅仅用于数据迭代，它还能通过中间操作和终端操作实现数据的排序、类型转换、求和、计数、求最大值、最小值以及模式匹配等操作。</p>\r\n<p>​    接下来，表6-1展示了命令式编程和声明式编程的优劣：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-02-23-00-23-e437bb48f8b8484393b4c8d1ced232dd.png\" alt=\"命令式编程与声明式编程\" title=\"命令式编程与声明式编程\">\r\n<h2 id=\"h2-u603Bu7ED3\"><a name=\"总结\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>总结</h2><p>​    在本文中，介绍了Java Stream的基本工作流程，并详细列举了中间操作和终端操作的细节。通过使用Stream，我们可以将传统的命令式编程代码进行简化，以提高编程效率。</p>\r\n','Java8,Stream',0,'# 技术精讲：Java 8 Stream API\r\n\r\n> Stream(流)是在Java 8中新增的新特性，首先需要为Java 8 Stream正名：Java 8中的Stream跟Java I/O Stream(例如：InputStream,OutputStream等)没有任何的关系。Stream是Java中数据源的包装器，通过Stream我们可以快速的对数据源进行操作（例如：过滤，排序、求和等等），且Stream不对任何数据进行存储，所以Stream也不是数据结构。\r\n\r\n\r\n\r\n​	在Java 8中，Stream增强了Array,List等对象操作数据的能力，Stream提供了一些列的方法使的这些原有的集合类可以轻松创建Stream对象实例。通过Stream API提供的静态方法，可以快速的生成有限/无限的数据流。特别指出，Stream只是增强了原有集合类的能力，并未对底层的源码做任何的修改。\r\n\r\n\r\n\r\n## 1.Stream的工作流程\r\n\r\n​	接下来，将介绍使用Java Stream的基本步骤。在Java 8中，Stream的生命周期一共有三个阶段：\r\n\r\n- 1.获取数据源并创建Stream实例。数据源可以是数组、列表、对象或者I/O流\r\n- 2.执行中间操作（Intermediate Operations）。中间操作可以是过滤、排序、类型转换等操作\r\n- 3.执行终端操作（Terminal Operation）。终端操作主要是对最终结果进行计数、求和、创建新集合等操作\r\n\r\n图1-1展示了Java 8 Stream的工作工作流程。\r\n\r\n![Stream工作流程](https://cdn.ramostear.com/2019-07-02-22-56-21-bce20665e181434cb032488659080aff.png \"Stream工作流程\")\r\n\r\n<center>图 1-1 Stream工作流程</center>\r\n\r\n## 2.Stream 操作管道是什么？\r\n\r\n​	所谓的Stream操作管道是指“中间操作”和“终端操作”的组合。在Java 8 Stream API中，许多的操作方法都将当前的Stream作为最终结果返回，这就允许开发人员以连式编程的方式，组合出更大的Stream操作管道。Java 8 Stream API位于java.util.stream包下，其组织结构如图2-1所示：\r\n\r\n![java.util.stream包](https://cdn.ramostear.com/2019-07-02-22-57-02-035073f6008f42f3abdd5ad36486902b.png \"java.util.stream包\")\r\n\r\n<center>图2-1 java.util.stream包</center>\r\n\r\n接下来，将详细介绍“中间操作”和“终端操作”各自的方法和特性。\r\n\r\n\r\n\r\n## 3.Stream中间操作（Intermediate Operations）是什么？\r\n\r\n​	Stream的中间操作将返回一个Stream,它允许开发者以查询的方式调用多个其他的操作。特别注意的是，在“终端操作”方法未被调用之前，“中间操作”的方法不会被执行。Stream的“中间操作”一共包括七个：Stream.filter,Stream.map,Stream.flatmap,Stream.peek,Stream.sorted和Stream.limit。\r\n\r\n### 3.1.Stream.filter\r\n\r\n​	Stream.filter将返回一个包含与之谓词相匹配的元素的新的Stream。下面将通过一个示例，演示Stream.filter的使用方法。\r\n\r\n![Stream.filter](https://cdn.ramostear.com/2019-07-02-22-57-19-43507693f53b4349962b271e03ca8e1c.png \"Stream.filter\")\r\n\r\n执行结果：\r\n\r\n![](https://cdn.ramostear.com/2019-07-02-22-57-37-ab1fcd1eb26649ca9c6f2c50876f1d72.png)\r\n\r\n此示例使用“中间操作”Stream.filter过滤以“j”开头的字符串,并使用“终端操作”Stream.count统计以“j”开头的字符串数量。\r\n\r\n### 3.2.Stream.map\r\n\r\n​	Stream.map将使用java.util.function.Function提供的方法转换Stream中的元素。Stream.map操作通常用于转换集合对象。下面是使用Stream.map的示例代码：\r\n\r\n![Stream.map](https://cdn.ramostear.com/2019-07-02-22-57-50-9ddf17329523491a92f6e3d35b257041.png \"Stream.map\")\r\n\r\n执行结果：\r\n\r\n![](https://cdn.ramostear.com/2019-07-02-22-58-04-192b4e270d85412cac6cf900dd583211.png)\r\n\r\n\r\n\r\n### 3.3.Stream.flatmap\r\n\r\n​	Stream.flatmap通常将Stream中的每一个元素转换成0个或多个元素。下面将通过flatmap来统计一个文本中单词的出现次数（不重复）。示例代码如下：\r\n\r\n![Stream.flatmap](https://cdn.ramostear.com/2019-07-02-22-58-17-2ddd2fbf06b348f5b4e634e2e7db27bf.png \"Stream.flatmap\")\r\n\r\n执行结果：\r\n\r\n![](https://cdn.ramostear.com/2019-07-02-22-58-36-f2218436ee4e406c9aa18c40588f2917.png)\r\n\r\nsource.txt内容：\r\n\r\n```tex\r\njava 8 stream example by ramostear\r\n```\r\n\r\n\r\n\r\n### 3.4.Stream.peek\r\n\r\n​	Stream.peek在调试期间非常有用,它允许您在操作Stream之前查看Stream内部的数据。下面是示例代码：\r\n\r\n![](https://cdn.ramostear.com/2019-07-02-22-58-52-abe2a1c712af48b79d4b5ab00188cc15.png)\r\n\r\n执行结果：\r\n\r\n```tex\r\nJava\r\nC#\r\nC++\r\nGO\r\nresult size = 4\r\n```\r\n\r\n\r\n\r\n### 3.5.Stream.distinct\r\n\r\n​	Stream.distinct是根据其内部的equals方法在Stream中对元素进行去重操作。下面是使用示例：\r\n\r\n![](https://cdn.ramostear.com/2019-07-02-22-59-06-50151efebfe44083a46478e50e5664d9.png)\r\n\r\n执行结果：\r\n\r\n```tex\r\n6\r\n1\r\n8\r\n11\r\n```\r\n\r\n### 3.6.Stream.sorted\r\n\r\n​	Stream.sorted方法用于将Stream中的数据元素进行排序。下面是使用示例：\r\n\r\n![](https://cdn.ramostear.com/2019-07-02-22-59-20-082a59fab5a94d80b8b27da697764ca1.png)\r\n\r\n执行结果：\r\n\r\n```tex\r\n1\r\n6\r\n8\r\n11\r\n```\r\n\r\n\r\n\r\n### 3.7.Stream.limit\r\n\r\n​	Stream.limit方法用于限定Stream中元素的个数。下面是使用示例：\r\n\r\n![](https://cdn.ramostear.com/2019-07-02-22-59-39-04bb3e85f831433585fc34fb9903bdd2.png)\r\n\r\n执行结果：\r\n\r\n```tex\r\n0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n11\r\n618\r\n```\r\n\r\n\r\n\r\n## 4.Stream 终端操作（Terminal  Operations）是什么？\r\n\r\n​	终端操作用于生产最终的数据结果，如对象，数组或者列表。在终端操作方法被执行前，中间操作方法将不会被执行。终端操作方法一共有十二个，它们是：forEach,toArray,reduce,collect,min,max,count,anymatch,allMatch,noneMatch,findFirst和findAny。表4-1列举了这十二个终端操作的使用方法：\r\n\r\n![Stream终端操作方法](https://cdn.ramostear.com/2019-07-02-22-59-56-957b09752ab44e6cbe245c709c7063c1.png \"Stream终端操作方法\")\r\n\r\n<center>图 4-1 Stream终端操作方法</center>\r\n\r\n\r\n\r\n## 5.如何创建Stream实例？\r\n\r\n​	Stream支持多种数据源创建Stream实例，如Array,List,Object和I/O流。接下来，通过简单的示例来演示如何创建Stream。\r\n\r\n### 5.1 使用数组创建Stream\r\n\r\n​	首先，我们定义一个静态的数组users,并初始几条用户数据，代码如下：\r\n\r\n```java\r\nprivate static User[] users = {new User(\"user1\"),new User(\"user2\"),new User(\"user3\")};\r\n```\r\n\r\n接下来，将利用users来创建一个Stream对象实例，示例代码如下：\r\n\r\n```java\r\npublic static void fromArray() {\r\n    Stream<User> uStream = Stream.of(users);\r\n    System.out.println(\"create stream from array.\");\r\n    uStream.forEach(u->System.out.println(u.getUsername()));\r\n}\r\n```\r\n\r\n\r\n\r\n### 5.2 使用对象创建Stream\r\n\r\n​	沿用上面定义的users数组，通过数组下标获得用户对象，并利用这些对象创建一个Stream实例，代码如下：\r\n\r\n```java\r\npublic static void fromObjects() {\r\n    Stream<User> uStream = Stream.of(users[0],users[1],users[2]);\r\n    System.out.println(\"create stream from objects.\");\r\n    uStream.forEach(u->System.out.println(u.getUsername()));\r\n}\r\n```\r\n\r\n\r\n\r\n### 5.3 使用List创建Stream\r\n\r\n​	接下来，我们将users数组转换成List，并使用此List来创建Stream实例。代码如下：\r\n\r\n```java\r\npublic static void fromList() {\r\n    List<User> list = Arrays.asList(users);\r\n    Stream<User> uStream = list.stream();\r\n    System.out.println(\"create stream from list.\");\r\n    uStream.forEach(u->System.out.println(u.getUsername()));\r\n}\r\n```\r\n\r\n\r\n\r\n### 5.4 使用builder方法创建Stream\r\n\r\n​	最后，我们将演示使用Stream内置的builder()静态方法创建Stream实例。代码如下：\r\n\r\n```java\r\npublic static void byBuilder() {\r\n    Stream.Builder<User> userStream = Stream.builder();\r\n    userStream.accept(users[0]);\r\n    userStream.accept(users[1]);\r\n    userStream.accept(users[2]);\r\n\r\n    System.out.println(\"create stream by builder.\");\r\n    userStream.build().forEach(u->System.out.println(u.getUsername()));\r\n\r\n}\r\n```\r\n\r\n\r\n\r\n​	以上就是通过数据源创建Stream实例的几种不同方式。\r\n\r\n\r\n\r\n## 6.为什么要使用Stream?\r\n\r\n​	在命令式编程中，我们必须逐行编写代码，才能完成相关的计算。例如，计算1~10000的数的总和，我们需要使用for(int i=1;i<=10000;i++){....}循环语句来迭代求值。在这个语句中，我们需要花费额外的精力来维护loop变量的值。而在声明式编程中，我们只需要关注想要实现的目标，而不是其内部重复的循环逻辑。Stream API通过使用内部迭代器和lambda表达式，帮助我们完成了那些重复性的逻辑。Stream API的作用不仅仅用于数据迭代，它还能通过中间操作和终端操作实现数据的排序、类型转换、求和、计数、求最大值、最小值以及模式匹配等操作。\r\n\r\n​	接下来，表6-1展示了命令式编程和声明式编程的优劣：\r\n\r\n![命令式编程与声明式编程](https://cdn.ramostear.com/2019-07-02-23-00-23-e437bb48f8b8484393b4c8d1ced232dd.png \"命令式编程与声明式编程\")\r\n\r\n\r\n\r\n## 总结\r\n\r\n​	在本文中，介绍了Java Stream的基本工作流程，并详细列举了中间操作和终端操作的细节。通过使用Stream，我们可以将传统的命令式编程代码进行简化，以提高编程效率。',1,'','2019/25/21/k6s41632.html',1,'Stream是Java中数据源的包装器，通过Stream我们可以快速的对数据源进行操作，且Stream不对任何数据进行存储','blog.html','https://cdn.ramostear.com/2019-07-02-23-00-49-af34715fba8a47f498dcb45f53ebb812.png;base64','技术精讲:Java 8 Stream API','',1543),(19,'2019-11-21 23:30:10',0,'2019-11-21 23:31:23',1,1,1,'谭朝红','本篇文章时Java设计模式系列技术文章的开篇，作为导读文章，将快速地对设计模式的基本概念、模式分类和适用范围进行解读','<h1 id=\"h1--java-\"><a name=\"开端-Java设计模式导读\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>开端-Java设计模式导读</h1><blockquote>\r\n<p>和往常一样，本篇文章依旧采用<strong>“3W”</strong>顺序（即What,Why和Where）来回答软件工程中的设计模式是什么，为什么需要设计模式以及在什么地方使用设计模式这三个问题。</p>\r\n</blockquote>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-39-54-79d9184d6f344f4bb667f569717f13e2.png\" alt=\"\">\r\n<p>​    本篇文章时Java设计模式系列技术文章的开篇，作为导读文章，将快速地对设计模式的基本概念、模式分类和适用范围进行解读。在后续的章节中，将对每一种类别的设计模式进行详细的讲解，讲解的内容包括每种设计模式的基本原理、适用范围和实战案例剖析三个部分。</p>\r\n<h1 id=\"h1-1-\"><a name=\"1 模式的基本概念\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>1 模式的基本概念</h1><p>​    模式是指解决某个特定领域问题，实现既定目标的方法或思想。具体来说，模式是那些身处于某个行业的从业人员根据实际的工作经验总结出的，具有通用性的且被行业公认的解决问题的方法或流程。模式并非只在软件工程中被应用，其在日常的生产活动中被广泛地使用，如制造业，餐饮业，建筑设计、医疗卫生、教育培训以及软件工程等都有模式的身影。</p>\r\n<h1 id=\"h1-2-\"><a name=\"2 什么是设计模式？\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>2 什么是设计模式？</h1><p>​    首先，设计模式是一种模式。在软件工程中，设计模式是一种通用的、可重复使用的用于解决既定范围内普遍发生的重复性问题的软件设计方法。使用成熟可靠的设计模式，可以提高代码复用性，节省开发时间，从而实现功能更强大、高度可维护的代码。这有助于降低软件产品的总体拥有成本，即TCO(Total Cost of Ownership)。另一方面，由于采用了统一的标准设计方法(思想或理论知识)，可以显著提升开发团队的生产效率和协作能力。</p>\r\n<h1 id=\"h1-3-java-\"><a name=\"3 Java设计模式的分类\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3 Java设计模式的分类</h1><p>​    在Java编程语言中，常用的设计模式可分为三种类型：</p>\r\n<ul>\r\n<li>建造类设计模式：主要用于定义和约束如何创建一个新的对象</li><li>结构类设计模式：主要用于定义如何使用多个对象组合出一个或多个复合对象</li><li>行为类设计模式：主要用于定义和描述对象之间的交互规则和限定对象的职责边界线</li></ul>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-40-12-c1ff43b80a914db9878840d10102caec.png\" alt=\"\">\r\n<center>图3-1 设计模式分类</center>\r\n\r\n<h2 id=\"h2-3-1-\"><a name=\"3.1 建造类设计模式\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.1 建造类设计模式</h2><p>​    建造类共包括五（5）种基本设计模式：单例模式，工厂模式，抽象工厂模式，建造器模式和原型模式，如图3-2所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-40-30-2978687bff314415b2d817f4b945fe7c.png\" alt=\"\">\r\n<center>图3-2 建造类设计模式</center>\r\n\r\n<h2 id=\"h2-3-2-\"><a name=\"3.2 结构类设计模式\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.2 结构类设计模式</h2><p>​    结构类共包括八（8）种基本设计模式：适配器模式，组合模式，代理模式，享元模式，过滤器模式，桥接模式，修饰模式和外观模式，如图3-3所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-40-47-49877df9e0104fce8c4f9153286e3307.png\" alt=\"\">\r\n<center>图3-3 结构类设计模式</center>\r\n\r\n<h2 id=\"h2-3-3-\"><a name=\"3.3 行为类设计模式\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>3.3 行为类设计模式</h2><p>​    行为类共包括十一（11）种基本设计模式：模板方法模式，解释器模式，责任链模式，观察者模式，战略模式，命令模式，状态模式，访客模式，转义模式，迭代器模式和备忘录模式，如图3-4所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-41-02-0988cc02d5694461841340a2898e4f71.png\" alt=\"\">\r\n<center>图3-4 行为类设计模式</center>\r\n\r\n<p>​    设计模式不仅仅只有上述描述的这三大类，除此之外还有许多的设计模式。现已知的设计模式还有100多种，如DAO模式，依赖注入模式和MVC模式等。</p>\r\n<h1 id=\"h1-4-\"><a name=\"4 快速理解设计模式\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4 快速理解设计模式</h1><p>​    在接下来的内容中，将快速对Java中常见的24中设计模式的基本概念进行梳理，以求对各种设计模式的原理和适用范围有一个大致的认识。</p>\r\n<h2 id=\"h2-4-1-\"><a name=\"4.1 建造类\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.1 建造类</h2><p>​    建造类设计模式提供了对创建对象的基本定义和约束条件，以寻求最佳的实例化Java对象解决方案。</p>\r\n<h3 id=\"h3-4-1-1-singleton\"><a name=\"4.1.1 单例模式-Singleton\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.1.1 单例模式-Singleton</h3><p>​    单例模式限制类的实例化过程，以确保在Java虚拟机（JVM）中有且只有一个类的实例化对象。单例模式是Java中最常用，也是最简单的设计模式之一。单例模式通常需具备如下的几个特征：</p>\r\n<ul>\r\n<li>单例模式限制类的实例化，且Java虚拟机中只能存在一个该类的示例化对象</li><li>单例模式必须提供一个全局可用的访问入口来获取该类的实例化对象</li><li>单例模式常被用于日志记录，驱动程序对象设计，缓存以及线程池</li><li>单例模式也会被用于其他的设计模式当中，如抽象工厂模式，建造者模式，原型模式等</li></ul>\r\n<p>单例模式的Java类的内部结构如图4-1所示：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-41-18-6933741f405b46a5a6667407026fc03e.jpg\" alt=\"\">\r\n<center>图4-1 单例模式类图</center>\r\n\r\n<p>下面是单例模式的一份示例代码清单：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-41-37-3cf9dc0bd8d2443e9cde40a4a3fae83b.png\" alt=\"\">\r\n<h3 id=\"h3-4-1-2-factory\"><a name=\"4.1.2 工厂模式-Factory\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.1.2 工厂模式-Factory</h3><p>​    在Java程序设计过程中，当一个超类(super class)具有多个子类(sub class)，且需要频繁的创建子类对象时，我们可以采用工厂模式。工厂模式的作用是将子类的实例化工作统一交由工厂类来完成，通过对输入参数的判断，工厂类自动实例化具体的子类。实现工厂模式需要满足三个条件：</p>\r\n<ul>\r\n<li>超类（super class）：超类是一个抽象类</li><li>子类（sub class）: 子类需继承超类</li><li>工厂类（factory class）:工厂类根据输入参数实例化子类</li></ul>\r\n<p>图4-2为Java工厂模式的类图：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-41-59-7ad6191096f342ed803416d2e9383c3f.jpg\" alt=\"\">\r\n<center>图4-2 工厂模式UML类图</center>\r\n\r\n<p>下面是工厂模式的一份示例代码清单：<br><img src=\"https://cdn.ramostear.com/2019-07-15-11-42-38-ec55a0981b8c416aac28c5d2520f9ae3.png\" alt=\"\">\r\n<h3 id=\"h3-4-1-3-abstract-factory\"><a name=\"4.1.3 抽象工厂模式-Abstract Factory\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.1.3 抽象工厂模式-Abstract Factory</h3><p>​    抽象工厂模式与工厂模式很类似，抽象工厂模式可以简单的理解为“工厂的工厂”。在工厂模式中，根据提供的输入参数返回产品类的实例化对象，这个过程需要通过if-else或者switch这样的逻辑判断语句来完成具体子类的判定。而在抽象工厂模式中，每种产品都有具体的工厂类与之对应，从而避免在编码过程中使用大量的逻辑判断代码。抽象工厂模式会根据输入的工厂类型以返回具体的工厂子类。抽象工厂类只负责实例化工厂子类，不参与商品子类的实例化工作。图4-3是抽象工厂模式的UML类图：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-43-12-f679593a2c6a4f5e8b3e7fd2157c1ba0.jpg\" alt=\"\">\r\n<center>图4-3 抽象工厂模式</center>\r\n\r\n<h3 id=\"h3-4-1-4-builder\"><a name=\"4.1.4 建造器模式-Builder\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.1.4 建造器模式-Builder</h3><p>​    建造者模式通常被用于需要多个步骤创建对象的场景中。建造者模式的主要意图是将类的构建逻辑转移到类的实例化之外，当一个类有许多的属性，当在实例化该类的对象时，并不一定拥有该实例化对象的全部属性信息，便可使用建造者模式通过逐步获取实例化对象的属性信息，来完成该类的实例化过程。而工厂模式和抽象工厂模式需要在实例化时获取该类实例化对象的全部属性信息。图4-4展示了建造器模式的基本逻辑关系：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-43-30-f7c045fd6df14c03b9fdf89ec43abee3.jpg\" alt=\"\">\r\n<center>图 4-4 建造器模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-1-5-prototype\"><a name=\"4.1.5 原型模式-Prototype\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.1.5 原型模式-Prototype</h3><p>​    原型模式的主要作用是可以利用现有的类通过复制（克隆）的方式创建一个新的对象。当示例化一个类的对象需要耗费大量的时间和系统资源时，可是采用原型模式，将原始已存在的对象通过复制（克隆）机制创建新的对象，然后根据需要，对新对象进行修改。原型模式要求被复制的对象自身具备拷贝功能，此功能不能由外界完成。图4-5展示了原型模式的基本逻辑：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-43-45-332839c6da9640a285a568927667a4a1.jpg\" alt=\"\">\r\n<center>图4-5 原型模式UML类图</center>\r\n\r\n<h2 id=\"h2-4-2-\"><a name=\"4.2 结构类\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2 结构类</h2><p>​    结构类设计模式主要解决如何通过多个小对象组合出一个大对象的问题，如使用继承和接口实现将多个类组合在一起。</p>\r\n<h3 id=\"h3-4-2-1-adapter\"><a name=\"4.2.1 适配器模式-Adapter\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2.1 适配器模式-Adapter</h3><p>​    适配器模式的主要作用是使现有的多个可用接口能够在一起为客服端提供新的接口服务。在适配器模式中，负责连接不同接口的对象成为适配器。在现实生活中，我们也能够找到很多实际的案例来理解适配器的工作原理，例如常用的手机充电头，在手机和电源插座之间，手机充电头就扮演一个适配器的角色，它能够同时适配220V，200V，120V等不同的电压，最终将电转换成手机可用的5V电压为手机进行充电。图4-6展示了适配器的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-44-01-65ebf9c29ab942a98936cf2c52b33084.jpg\" alt=\"\">\r\n<center>图 4-6 适配器模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-2-2-composite\"><a name=\"4.2.2 组合模式-Composite\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2.2 组合模式-Composite</h3><p>​    组合模式的主要作用是让整体与局部之前具有相同的行为。例如我们需要绘制一个图形（正方形，三角形，圆形或其他多边形），首先需要准备一张空白的纸，然后是选择一种绘制图案的颜色，再次是确定绘制图案的大小，最后是绘制图案。不管是绘制正方形还是三角形，都需要按照这个步骤进行。在软件设计过程中，组合模式的最大意义在于保证了客户端在调用单个对象与组合对象时，在其操作流程上是保持一致的。图4-7展示了组合模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-44-22-a61a8ddfc34e47eb81fa76a9f11e6fc8.jpg\" alt=\"\">\r\n<center>图 4-7 组合模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-2-3-proxy\"><a name=\"4.2.3 代理模式-Proxy\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2.3 代理模式-Proxy</h3><p>​    代理模式的主要作用是通过提供一个代理对象或者一个占位符来控制对实际对象的访问行为。代理模式通常用于需要频繁操作一些复杂对象的地方，通过使用代理模式，可以借由代理类来操作目标对象，简化操作流程。图4-8展示了代理模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-44-40-7ab8cebb6b09455db8e1005cf6e530cb.jpg\" alt=\"\">\r\n<center>图 4-8 代理模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-2-4-flywight\"><a name=\"4.2.4 享元模式-Flywight\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2.4 享元模式-Flywight</h3><p>​    享元模式的主要作用是通过共享来有效地支持大量细粒度的对象。例如当需要创建一个类的很多对象时，可以使用享元模式，通过共享对象信息来减轻内存负载。如果在软件设计过程中采用享元模式，需要考虑以下三个问题：</p>\r\n<ul>\r\n<li>应用程序需要创建的对象数量是否很大？</li><li>对象的创建对内存消耗和时间消耗是否有严格的要求？</li><li>对象的属性是否可以分为内在属性和外在属性？对象的外在属性是否支持有客户端定义？</li></ul>\r\n<p>图4-9展示了享元模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-44-57-45a838b4fbcb42cf8825e89b783e55c7.jpg\" alt=\"\">\r\n<center>图 4-9 享元模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-2-5-facade\"><a name=\"4.2.5 外观模式-Facade\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2.5 外观模式-Facade</h3><p>​    外观模式的主要作用是为子系统中的一组接口提供一个统一的接口，以便客户端更容易去使用子系统中的接口。简单的理解是外观模式为众多复杂接口定义了一个更高级别的接口。外观模式的目的是让接口更容易被使用，图4-10展示了外观模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-45-12-5dbacc92b89f4f1e8f76f4d55c533a37.jpg\" alt=\"\">\r\n<center>图 4-10 外观模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-2-6-bridge\"><a name=\"4.2.6 桥接模式-Bridge\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2.6 桥接模式-Bridge</h3><p>​    桥接模式的主要用途是将抽象类与抽象类的具体实现相分离，以实现结构上的解耦，使抽象和实现可以独立的进行变化。桥接模式的实现优先遵循组合而不是继承，当使用桥接模式时，在一定程度上可以在客户端中因此接口的内部实现。图4-11展示了桥接模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-45-31-fbdfb4d5171e465fb31e2e5e1a15c83f.jpg\" alt=\"\">\r\n<center>图 4-11 桥接模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-2-7-decorator\"><a name=\"4.2.7 修饰模式-Decorator\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2.7 修饰模式-Decorator</h3><p>​    修饰模式的主要作用是在运行时动态的组合类的行为。通常，你会添加一些新的类或者新的方法来扩展已有的代码库，然而，在某些情况下你需要在程序运行时为某个对象组合新的行为，此时你可以采用修饰模式。图4-12展示了修饰模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-45-50-8e86c531fdd8415ca80fb072fbff8697.jpg\" alt=\"\">\r\n<center>图 4-12 修饰模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-2-8-filter\"><a name=\"4.2.8 过滤器模式-Filter\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.2.8 过滤器模式-Filter</h3><p>​    过滤器模式是使用不同的标准来过滤一组对象，通过逻辑运算以解耦的方式将对象组合起来。图 4-13展示了过滤器模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-46-09-e4d9245ffde3474586b82465a0b90aa3.jpg\" alt=\"\">\r\n<center>图 4-13 过滤器模式</center>\r\n\r\n<h2 id=\"h2-4-3-\"><a name=\"4.3 行为类\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3 行为类</h2><p>​    行为类设计模式主要用于定义和描述对象之间的交互规则和职责边界，为对象之间更好的交互提供解决方案。</p>\r\n<h3 id=\"h3-4-3-1-template-method\"><a name=\"4.3.1 模板方法模式-Template Method\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.1 模板方法模式-Template Method</h3><p>​    模板方法模式的主要作用是在一个方法里实现一个算法，可以将算法中的的一些步骤抽象为方法，并将这些方法的实现推迟到子类中去实现。例如建造一栋房子，我们需要设计图纸，打地基，构筑墙体，安装门窗和内部装修。我们可以设计不同的房屋样式（别墅，高楼，板房等）,不同的门窗和不同的装修材料和风格，但是其顺序不能颠倒。在这种情况下，我们可以定义一个模板方法，规定方法的执行顺序，而将方法的实现推迟到子类中完成。图4-14展示了模板方法模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-46-26-94ae5c815d294699b4b36cc380263821.jpg\" alt=\"\">\r\n<center>图 4-14 模板方法模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-2-mediator\"><a name=\"4.3.2 解释器模式-Mediator\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.2 解释器模式-Mediator</h3><p>​    解释器（中介）模式的主要设计意图是定义一个中间对象，封装一组对象的交互，从而降低对象的耦合度，避免了对象间的显示引用，并可以独立地改变对象的行为。解释器（中介）模式可以在系统中的不同对象之间提供集中式的交互介质，降低系统中各组件的耦合度。图 4-15展示了解释器（中介）模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-46-44-6d911de4a3df4a3481704fffc4473a44.jpg\" alt=\"\">\r\n<center>图 4-15 解释器（中介）模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-3-chain-of-responsibility\"><a name=\"4.3.3 责任链模式-Chain of Responsibility\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.3 责任链模式-Chain of Responsibility</h3><p>​    责任链模式主要作用是让多个对象具有对同一任务（请求）的处理机会，以解除请求发送者与接收者之间的耦合度。try-catch就是一个典型的责任链模式的应用案例。在try-catch语句中，可以同时存在多个catch语句块，每个catch语句块都是处理该特定异常的处理器。当try语句块中发生异常是，异常将被发送到第一个catch语句块进行处理，如果第一个语句块无法处理它，它将会被请求转发到链中的下一个catch语句块。如果最后一个catch语句块仍然不能处理该异常，则该异常将会被向上抛出。图4-16展示了责任链模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-47-01-d83dbf2d0cc34b7484f22157d338b193.jpg\" alt=\"\">\r\n<center>图 4-16 责任链模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-4-observer\"><a name=\"4.3.4 观察者模式-Observer\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.4 观察者模式-Observer</h3><p>​    观察者模式的目的是在多个对象之间定义一对多的依赖关系，当一个对象的状态发生改变时，观察者会通知依赖它的对象，并根据新状态做出相应的反应。简单来说，如果你需要在对象状态发生改变时及时收到通知，你可以定义一个监听器，对该对象的状态进行监听，此时的监听器即为观察者（Observer），被监听对象称为主题（Subject）。Java消息服务（JMS）即采用了观察者设计模式（同时还使用了中介模式），允许应用程序订阅数据并将数据发布到其他应用程序中。图4-17展示了观察者模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-47-21-aecf9c1ae07441a08cbdc061921efd21.jpg\" alt=\"\">\r\n<center>图 4-17 观察者模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-5-strategy\"><a name=\"4.3.5 策略模式-Strategy\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.5 策略模式-Strategy</h3><p>​    策略模式的主要目的是将可互换的方法封装在各自独立的类中，并且让每个方法都实现一个公共的操作。策略模式定义了策略的输入与输出，实现则由各个独立的类完成。策略模式可以让一组策略共存，代码互不干扰，它不仅将选择策略的逻辑从策略本身中分离出来，还帮助我们组织和简化了代码。一个典型的例子是Collections.sort()方法，采用Comparator作为方法参数，根据Comparator接口实现类的不同，对象将以不同的方式进行排序。图 4-18 展示了策略模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-47-41-ca6ff43fc612441384792f81d03a1b58.jpg\" alt=\"\">\r\n<center>图 4-18 策略模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-6-command\"><a name=\"4.3.6 命令模式-Command\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.6 命令模式-Command</h3><p>​    命令模式的设计意图是将请求封装在对象的内部。直接调用是执行方法的通常做法，然而，在有些时候我们无法控制方法被执行的时机和上下文信息。在这种情况下，可以将方法封装到对象的内部，通过在对象内部存储调用方所需要的信息，就可以让客户端或者服务自由决定何时调用方法。图 4-19 展示了命令模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-48-03-4220f5faa2f64adf82f2cca1aafe09a2.jpg\" alt=\"\">\r\n<center>图 4-19 命令模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-37-state\"><a name=\"4.37 状态模式-State\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.37 状态模式-State</h3><p>​    状态模式的设计意图是更具对象的状态改变其行为。如果我们必须根据对象的状态改变对象的行为，可以在对象中定义一个状态变量，并使用逻辑判断语句块（如if-else）根据状态执行不同的操作。图4-20展示了状态模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-48-26-9fb143ae0fa64b61a824fef030383f8d.jpg\" alt=\"\">\r\n<center>图 4-20 状态模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-8-visitor\"><a name=\"4.3.8 访客模式-Visitor\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.8 访客模式-Visitor</h3><p>​    访客模式的设计意图是在不改变现有类层次结构的前提下，对该层次结构进行扩展。例如在购物网站中，我们将不同的商品添加进购物车，然后支付按钮时，它会计算出需要支付的总金额数。我们可以在购物车类中完成金额的计算，也可以使用访客模式，将购物应付金额逻辑转移到新的类中。图 4-21展示了访客模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-48-44-c84507d53436456ab28367c606d17c1c.jpg\" alt=\"\">\r\n<center>图 4-21 访客模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-9-interpreter\"><a name=\"4.3.9 转义（翻译）模式-Interpreter\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.9 转义（翻译）模式-Interpreter</h3><p>​    转义（翻译）模式的设计意图是让你根据事先定义好的一系列组合规则，组合可执行的对象。实现转义（翻译）模式的一个基本步骤如下：</p>\r\n<ul>\r\n<li>创建执行解释工作的上下文引擎</li><li>根据不同的表达式实现类，实现上下文中的解释工作</li><li>创建一个客户端，客户端从用户那里获取输入，并决定使用哪一种表达式来输出转义后的内容</li></ul>\r\n<p>图4-22展示了转义（翻译）模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-49-08-a39c717a61534172bffe116c5888ecde.jpg\" alt=\"\">\r\n<center>图 4-22 转义（翻译）模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-10-iterator\"><a name=\"4.3.10 迭代器模式-Iterator\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.10 迭代器模式-Iterator</h3><p>​    迭代器模式为迭代一组对象提供了一个标准的方法。迭代器模式被广泛的应用于Java Collection框架中，Iterator接口提供了遍历集合元素的方法。迭代器模式不仅仅是遍历集合，我们还可以根据不同的要求提供不同类型的迭代器。迭代器模式通过集合隐藏内部的遍历细节，客户端只需要使用对应的迭代方法即可完成元素的遍历操作。图4-23 展示了迭代器的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-49-28-cc56d5d821e44297b1bd07f7810688c2.jpg\" alt=\"\">\r\n<center>图 4-23 迭代器模式UML类图</center>\r\n\r\n<h3 id=\"h3-4-3-11-memento\"><a name=\"4.3.11 备忘录模式-Memento\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>4.3.11 备忘录模式-Memento</h3><p>​    备忘录模式的设计意图是为对象的状态提供存储和恢复功能。备忘录模式由两个对象来实现-Originator和Caretaker。Originator需要具有保存和恢复对象状态的能力，它使用内部类来保存对象的状态。内部内则称为备忘录，因为它是对象私有的，因此外部类不能直接访问它。图4-24展示了备忘录模式的基本原理：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-49-53-6bd50fb204244419aa8b7332e28b7a52.jpg\" alt=\"\">\r\n<center>图 4-24 备忘录模式UML类图</center>\r\n\r\n<h1 id=\"h1-u5C0Fu8282\"><a name=\"小节\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>小节</h1><p>​    在本篇文章中，说明了模式是指解决某个特定领域问题，实现既定目标的方法或思想；设计模式是一种通用的、可重复使用的用于解决既定范围内普遍发生的重复性问题的软件设计方法。同时，对Java中常见的设计模式进行了分类，设计模式分为建造、结构和行为三种类型，并对每种类型的设计模式的基本概念和原理进行了介绍，在后续的章节中，将详细的介绍每种设计模式的原理、使用方式和适用范围，并给出相应的实战源码。</p>\r\n<h1 id=\"h1-u9644u4EF6\"><a name=\"附件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>附件</h1><p>​    在文章最后，为了便于大家阅读，提供了本文的电子版文件<br><img src=\"https://cdn.ramostear.com/2019-07-15-11-52-41-681967dedfba4e76919329c76c8b761a.png\" alt=\"\">\r\n<p>​    百度网盘下载地址：<a href=\"https://pan.baidu.com/s/1OREFj5QmLsHcKNbaJGLTmw\">https://pan.baidu.com/s/1OREFj5QmLsHcKNbaJGLTmw</a></p>\r\n<p>​    文件提取码：jqgf</p>\r\n<p>​    文件提取二维码：</p>\r\n<p><img src=\"https://cdn.ramostear.com/2019-07-15-11-50-26-9cf42e53a0d449109917cb78370aca6f.png\" alt=\"\">\r\n','Java,设计模式',0,'# 开端-Java设计模式导读\r\n\r\n> 和往常一样，本篇文章依旧采用**“3W”**顺序（即What,Why和Where）来回答软件工程中的设计模式是什么，为什么需要设计模式以及在什么地方使用设计模式这三个问题。\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-39-54-79d9184d6f344f4bb667f569717f13e2.png)\r\n\r\n​	本篇文章时Java设计模式系列技术文章的开篇，作为导读文章，将快速地对设计模式的基本概念、模式分类和适用范围进行解读。在后续的章节中，将对每一种类别的设计模式进行详细的讲解，讲解的内容包括每种设计模式的基本原理、适用范围和实战案例剖析三个部分。\r\n\r\n\r\n\r\n# 1 模式的基本概念\r\n\r\n​	模式是指解决某个特定领域问题，实现既定目标的方法或思想。具体来说，模式是那些身处于某个行业的从业人员根据实际的工作经验总结出的，具有通用性的且被行业公认的解决问题的方法或流程。模式并非只在软件工程中被应用，其在日常的生产活动中被广泛地使用，如制造业，餐饮业，建筑设计、医疗卫生、教育培训以及软件工程等都有模式的身影。\r\n\r\n\r\n\r\n# 2 什么是设计模式？\r\n\r\n​	首先，设计模式是一种模式。在软件工程中，设计模式是一种通用的、可重复使用的用于解决既定范围内普遍发生的重复性问题的软件设计方法。使用成熟可靠的设计模式，可以提高代码复用性，节省开发时间，从而实现功能更强大、高度可维护的代码。这有助于降低软件产品的总体拥有成本，即TCO(Total Cost of Ownership)。另一方面，由于采用了统一的标准设计方法(思想或理论知识)，可以显著提升开发团队的生产效率和协作能力。\r\n\r\n\r\n\r\n# 3 Java设计模式的分类\r\n\r\n​	在Java编程语言中，常用的设计模式可分为三种类型：\r\n\r\n- 建造类设计模式：主要用于定义和约束如何创建一个新的对象\r\n- 结构类设计模式：主要用于定义如何使用多个对象组合出一个或多个复合对象\r\n- 行为类设计模式：主要用于定义和描述对象之间的交互规则和限定对象的职责边界线\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-40-12-c1ff43b80a914db9878840d10102caec.png)\r\n\r\n<center>图3-1 设计模式分类</center>\r\n\r\n## 3.1 建造类设计模式\r\n\r\n​	建造类共包括五（5）种基本设计模式：单例模式，工厂模式，抽象工厂模式，建造器模式和原型模式，如图3-2所示：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-40-30-2978687bff314415b2d817f4b945fe7c.png)\r\n\r\n<center>图3-2 建造类设计模式</center>\r\n\r\n## 3.2 结构类设计模式\r\n\r\n​	结构类共包括八（8）种基本设计模式：适配器模式，组合模式，代理模式，享元模式，过滤器模式，桥接模式，修饰模式和外观模式，如图3-3所示：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-40-47-49877df9e0104fce8c4f9153286e3307.png)\r\n\r\n<center>图3-3 结构类设计模式</center>\r\n\r\n## 3.3 行为类设计模式\r\n\r\n​	行为类共包括十一（11）种基本设计模式：模板方法模式，解释器模式，责任链模式，观察者模式，战略模式，命令模式，状态模式，访客模式，转义模式，迭代器模式和备忘录模式，如图3-4所示：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-41-02-0988cc02d5694461841340a2898e4f71.png)\r\n\r\n<center>图3-4 行为类设计模式</center>\r\n\r\n​	设计模式不仅仅只有上述描述的这三大类，除此之外还有许多的设计模式。现已知的设计模式还有100多种，如DAO模式，依赖注入模式和MVC模式等。\r\n\r\n\r\n\r\n# 4 快速理解设计模式\r\n\r\n​	在接下来的内容中，将快速对Java中常见的24中设计模式的基本概念进行梳理，以求对各种设计模式的原理和适用范围有一个大致的认识。\r\n\r\n## 4.1 建造类\r\n\r\n​	建造类设计模式提供了对创建对象的基本定义和约束条件，以寻求最佳的实例化Java对象解决方案。\r\n\r\n### 4.1.1 单例模式-Singleton\r\n\r\n​	单例模式限制类的实例化过程，以确保在Java虚拟机（JVM）中有且只有一个类的实例化对象。单例模式是Java中最常用，也是最简单的设计模式之一。单例模式通常需具备如下的几个特征：\r\n\r\n- 单例模式限制类的实例化，且Java虚拟机中只能存在一个该类的示例化对象\r\n- 单例模式必须提供一个全局可用的访问入口来获取该类的实例化对象\r\n- 单例模式常被用于日志记录，驱动程序对象设计，缓存以及线程池\r\n- 单例模式也会被用于其他的设计模式当中，如抽象工厂模式，建造者模式，原型模式等\r\n\r\n单例模式的Java类的内部结构如图4-1所示：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-41-18-6933741f405b46a5a6667407026fc03e.jpg)\r\n\r\n<center>图4-1 单例模式类图</center>\r\n\r\n下面是单例模式的一份示例代码清单：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-41-37-3cf9dc0bd8d2443e9cde40a4a3fae83b.png)\r\n\r\n\r\n\r\n### 4.1.2 工厂模式-Factory\r\n\r\n​	在Java程序设计过程中，当一个超类(super class)具有多个子类(sub class)，且需要频繁的创建子类对象时，我们可以采用工厂模式。工厂模式的作用是将子类的实例化工作统一交由工厂类来完成，通过对输入参数的判断，工厂类自动实例化具体的子类。实现工厂模式需要满足三个条件：\r\n\r\n- 超类（super class）：超类是一个抽象类\r\n- 子类（sub class）: 子类需继承超类\r\n- 工厂类（factory class）:工厂类根据输入参数实例化子类\r\n\r\n图4-2为Java工厂模式的类图：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-41-59-7ad6191096f342ed803416d2e9383c3f.jpg)\r\n\r\n<center>图4-2 工厂模式UML类图</center>\r\n\r\n下面是工厂模式的一份示例代码清单：\r\n![](https://cdn.ramostear.com/2019-07-15-11-42-38-ec55a0981b8c416aac28c5d2520f9ae3.png)\r\n\r\n\r\n\r\n### 4.1.3 抽象工厂模式-Abstract Factory\r\n\r\n​	抽象工厂模式与工厂模式很类似，抽象工厂模式可以简单的理解为“工厂的工厂”。在工厂模式中，根据提供的输入参数返回产品类的实例化对象，这个过程需要通过if-else或者switch这样的逻辑判断语句来完成具体子类的判定。而在抽象工厂模式中，每种产品都有具体的工厂类与之对应，从而避免在编码过程中使用大量的逻辑判断代码。抽象工厂模式会根据输入的工厂类型以返回具体的工厂子类。抽象工厂类只负责实例化工厂子类，不参与商品子类的实例化工作。图4-3是抽象工厂模式的UML类图：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-43-12-f679593a2c6a4f5e8b3e7fd2157c1ba0.jpg)\r\n\r\n<center>图4-3 抽象工厂模式</center>\r\n\r\n### 4.1.4 建造器模式-Builder\r\n\r\n​	建造者模式通常被用于需要多个步骤创建对象的场景中。建造者模式的主要意图是将类的构建逻辑转移到类的实例化之外，当一个类有许多的属性，当在实例化该类的对象时，并不一定拥有该实例化对象的全部属性信息，便可使用建造者模式通过逐步获取实例化对象的属性信息，来完成该类的实例化过程。而工厂模式和抽象工厂模式需要在实例化时获取该类实例化对象的全部属性信息。图4-4展示了建造器模式的基本逻辑关系：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-43-30-f7c045fd6df14c03b9fdf89ec43abee3.jpg)\r\n\r\n<center>图 4-4 建造器模式UML类图</center>\r\n\r\n### 4.1.5 原型模式-Prototype\r\n\r\n​	原型模式的主要作用是可以利用现有的类通过复制（克隆）的方式创建一个新的对象。当示例化一个类的对象需要耗费大量的时间和系统资源时，可是采用原型模式，将原始已存在的对象通过复制（克隆）机制创建新的对象，然后根据需要，对新对象进行修改。原型模式要求被复制的对象自身具备拷贝功能，此功能不能由外界完成。图4-5展示了原型模式的基本逻辑：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-43-45-332839c6da9640a285a568927667a4a1.jpg)\r\n\r\n<center>图4-5 原型模式UML类图</center>\r\n\r\n## 4.2 结构类\r\n\r\n​	结构类设计模式主要解决如何通过多个小对象组合出一个大对象的问题，如使用继承和接口实现将多个类组合在一起。\r\n\r\n### 4.2.1 适配器模式-Adapter\r\n\r\n​	适配器模式的主要作用是使现有的多个可用接口能够在一起为客服端提供新的接口服务。在适配器模式中，负责连接不同接口的对象成为适配器。在现实生活中，我们也能够找到很多实际的案例来理解适配器的工作原理，例如常用的手机充电头，在手机和电源插座之间，手机充电头就扮演一个适配器的角色，它能够同时适配220V，200V，120V等不同的电压，最终将电转换成手机可用的5V电压为手机进行充电。图4-6展示了适配器的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-44-01-65ebf9c29ab942a98936cf2c52b33084.jpg)\r\n\r\n<center>图 4-6 适配器模式UML类图</center>\r\n\r\n### 4.2.2 组合模式-Composite\r\n\r\n​	组合模式的主要作用是让整体与局部之前具有相同的行为。例如我们需要绘制一个图形（正方形，三角形，圆形或其他多边形），首先需要准备一张空白的纸，然后是选择一种绘制图案的颜色，再次是确定绘制图案的大小，最后是绘制图案。不管是绘制正方形还是三角形，都需要按照这个步骤进行。在软件设计过程中，组合模式的最大意义在于保证了客户端在调用单个对象与组合对象时，在其操作流程上是保持一致的。图4-7展示了组合模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-44-22-a61a8ddfc34e47eb81fa76a9f11e6fc8.jpg)\r\n\r\n<center>图 4-7 组合模式UML类图</center>\r\n\r\n### 4.2.3 代理模式-Proxy\r\n\r\n​	代理模式的主要作用是通过提供一个代理对象或者一个占位符来控制对实际对象的访问行为。代理模式通常用于需要频繁操作一些复杂对象的地方，通过使用代理模式，可以借由代理类来操作目标对象，简化操作流程。图4-8展示了代理模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-44-40-7ab8cebb6b09455db8e1005cf6e530cb.jpg)\r\n\r\n<center>图 4-8 代理模式UML类图</center>\r\n\r\n### 4.2.4 享元模式-Flywight\r\n\r\n​	享元模式的主要作用是通过共享来有效地支持大量细粒度的对象。例如当需要创建一个类的很多对象时，可以使用享元模式，通过共享对象信息来减轻内存负载。如果在软件设计过程中采用享元模式，需要考虑以下三个问题：\r\n\r\n- 应用程序需要创建的对象数量是否很大？\r\n- 对象的创建对内存消耗和时间消耗是否有严格的要求？\r\n- 对象的属性是否可以分为内在属性和外在属性？对象的外在属性是否支持有客户端定义？\r\n\r\n图4-9展示了享元模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-44-57-45a838b4fbcb42cf8825e89b783e55c7.jpg)\r\n\r\n<center>图 4-9 享元模式UML类图</center>\r\n\r\n### 4.2.5 外观模式-Facade\r\n\r\n​	外观模式的主要作用是为子系统中的一组接口提供一个统一的接口，以便客户端更容易去使用子系统中的接口。简单的理解是外观模式为众多复杂接口定义了一个更高级别的接口。外观模式的目的是让接口更容易被使用，图4-10展示了外观模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-45-12-5dbacc92b89f4f1e8f76f4d55c533a37.jpg)\r\n\r\n<center>图 4-10 外观模式UML类图</center>\r\n\r\n### 4.2.6 桥接模式-Bridge\r\n\r\n​	桥接模式的主要用途是将抽象类与抽象类的具体实现相分离，以实现结构上的解耦，使抽象和实现可以独立的进行变化。桥接模式的实现优先遵循组合而不是继承，当使用桥接模式时，在一定程度上可以在客户端中因此接口的内部实现。图4-11展示了桥接模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-45-31-fbdfb4d5171e465fb31e2e5e1a15c83f.jpg)\r\n\r\n<center>图 4-11 桥接模式UML类图</center>\r\n\r\n### 4.2.7 修饰模式-Decorator\r\n\r\n​	修饰模式的主要作用是在运行时动态的组合类的行为。通常，你会添加一些新的类或者新的方法来扩展已有的代码库，然而，在某些情况下你需要在程序运行时为某个对象组合新的行为，此时你可以采用修饰模式。图4-12展示了修饰模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-45-50-8e86c531fdd8415ca80fb072fbff8697.jpg)\r\n\r\n<center>图 4-12 修饰模式UML类图</center>\r\n\r\n### 4.2.8 过滤器模式-Filter\r\n\r\n​	过滤器模式是使用不同的标准来过滤一组对象，通过逻辑运算以解耦的方式将对象组合起来。图 4-13展示了过滤器模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-46-09-e4d9245ffde3474586b82465a0b90aa3.jpg)\r\n\r\n<center>图 4-13 过滤器模式</center>\r\n\r\n## 4.3 行为类\r\n\r\n​	行为类设计模式主要用于定义和描述对象之间的交互规则和职责边界，为对象之间更好的交互提供解决方案。\r\n\r\n### 4.3.1 模板方法模式-Template Method\r\n\r\n​	模板方法模式的主要作用是在一个方法里实现一个算法，可以将算法中的的一些步骤抽象为方法，并将这些方法的实现推迟到子类中去实现。例如建造一栋房子，我们需要设计图纸，打地基，构筑墙体，安装门窗和内部装修。我们可以设计不同的房屋样式（别墅，高楼，板房等）,不同的门窗和不同的装修材料和风格，但是其顺序不能颠倒。在这种情况下，我们可以定义一个模板方法，规定方法的执行顺序，而将方法的实现推迟到子类中完成。图4-14展示了模板方法模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-46-26-94ae5c815d294699b4b36cc380263821.jpg)\r\n\r\n<center>图 4-14 模板方法模式UML类图</center>\r\n\r\n### 4.3.2 解释器模式-Mediator\r\n\r\n​	解释器（中介）模式的主要设计意图是定义一个中间对象，封装一组对象的交互，从而降低对象的耦合度，避免了对象间的显示引用，并可以独立地改变对象的行为。解释器（中介）模式可以在系统中的不同对象之间提供集中式的交互介质，降低系统中各组件的耦合度。图 4-15展示了解释器（中介）模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-46-44-6d911de4a3df4a3481704fffc4473a44.jpg)\r\n\r\n<center>图 4-15 解释器（中介）模式UML类图</center>\r\n\r\n### 4.3.3 责任链模式-Chain of Responsibility\r\n\r\n​	责任链模式主要作用是让多个对象具有对同一任务（请求）的处理机会，以解除请求发送者与接收者之间的耦合度。try-catch就是一个典型的责任链模式的应用案例。在try-catch语句中，可以同时存在多个catch语句块，每个catch语句块都是处理该特定异常的处理器。当try语句块中发生异常是，异常将被发送到第一个catch语句块进行处理，如果第一个语句块无法处理它，它将会被请求转发到链中的下一个catch语句块。如果最后一个catch语句块仍然不能处理该异常，则该异常将会被向上抛出。图4-16展示了责任链模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-47-01-d83dbf2d0cc34b7484f22157d338b193.jpg)\r\n\r\n<center>图 4-16 责任链模式UML类图</center>\r\n\r\n### 4.3.4 观察者模式-Observer\r\n\r\n​	观察者模式的目的是在多个对象之间定义一对多的依赖关系，当一个对象的状态发生改变时，观察者会通知依赖它的对象，并根据新状态做出相应的反应。简单来说，如果你需要在对象状态发生改变时及时收到通知，你可以定义一个监听器，对该对象的状态进行监听，此时的监听器即为观察者（Observer），被监听对象称为主题（Subject）。Java消息服务（JMS）即采用了观察者设计模式（同时还使用了中介模式），允许应用程序订阅数据并将数据发布到其他应用程序中。图4-17展示了观察者模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-47-21-aecf9c1ae07441a08cbdc061921efd21.jpg)\r\n\r\n<center>图 4-17 观察者模式UML类图</center>\r\n\r\n### 4.3.5 策略模式-Strategy\r\n\r\n​	策略模式的主要目的是将可互换的方法封装在各自独立的类中，并且让每个方法都实现一个公共的操作。策略模式定义了策略的输入与输出，实现则由各个独立的类完成。策略模式可以让一组策略共存，代码互不干扰，它不仅将选择策略的逻辑从策略本身中分离出来，还帮助我们组织和简化了代码。一个典型的例子是Collections.sort()方法，采用Comparator作为方法参数，根据Comparator接口实现类的不同，对象将以不同的方式进行排序。图 4-18 展示了策略模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-47-41-ca6ff43fc612441384792f81d03a1b58.jpg)\r\n\r\n<center>图 4-18 策略模式UML类图</center>\r\n\r\n### 4.3.6 命令模式-Command\r\n\r\n​	命令模式的设计意图是将请求封装在对象的内部。直接调用是执行方法的通常做法，然而，在有些时候我们无法控制方法被执行的时机和上下文信息。在这种情况下，可以将方法封装到对象的内部，通过在对象内部存储调用方所需要的信息，就可以让客户端或者服务自由决定何时调用方法。图 4-19 展示了命令模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-48-03-4220f5faa2f64adf82f2cca1aafe09a2.jpg)\r\n\r\n<center>图 4-19 命令模式UML类图</center>\r\n\r\n### 4.37 状态模式-State\r\n\r\n​	状态模式的设计意图是更具对象的状态改变其行为。如果我们必须根据对象的状态改变对象的行为，可以在对象中定义一个状态变量，并使用逻辑判断语句块（如if-else）根据状态执行不同的操作。图4-20展示了状态模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-48-26-9fb143ae0fa64b61a824fef030383f8d.jpg)\r\n\r\n<center>图 4-20 状态模式UML类图</center>\r\n\r\n### 4.3.8 访客模式-Visitor\r\n\r\n​	访客模式的设计意图是在不改变现有类层次结构的前提下，对该层次结构进行扩展。例如在购物网站中，我们将不同的商品添加进购物车，然后支付按钮时，它会计算出需要支付的总金额数。我们可以在购物车类中完成金额的计算，也可以使用访客模式，将购物应付金额逻辑转移到新的类中。图 4-21展示了访客模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-48-44-c84507d53436456ab28367c606d17c1c.jpg)\r\n\r\n<center>图 4-21 访客模式UML类图</center>\r\n\r\n### 4.3.9 转义（翻译）模式-Interpreter \r\n\r\n​	转义（翻译）模式的设计意图是让你根据事先定义好的一系列组合规则，组合可执行的对象。实现转义（翻译）模式的一个基本步骤如下：\r\n\r\n- 创建执行解释工作的上下文引擎\r\n- 根据不同的表达式实现类，实现上下文中的解释工作\r\n- 创建一个客户端，客户端从用户那里获取输入，并决定使用哪一种表达式来输出转义后的内容\r\n\r\n图4-22展示了转义（翻译）模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-49-08-a39c717a61534172bffe116c5888ecde.jpg)\r\n\r\n<center>图 4-22 转义（翻译）模式UML类图</center>\r\n\r\n### 4.3.10 迭代器模式-Iterator\r\n\r\n​	迭代器模式为迭代一组对象提供了一个标准的方法。迭代器模式被广泛的应用于Java Collection框架中，Iterator接口提供了遍历集合元素的方法。迭代器模式不仅仅是遍历集合，我们还可以根据不同的要求提供不同类型的迭代器。迭代器模式通过集合隐藏内部的遍历细节，客户端只需要使用对应的迭代方法即可完成元素的遍历操作。图4-23 展示了迭代器的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-49-28-cc56d5d821e44297b1bd07f7810688c2.jpg)\r\n\r\n<center>图 4-23 迭代器模式UML类图</center>\r\n\r\n### 4.3.11 备忘录模式-Memento\r\n\r\n​	备忘录模式的设计意图是为对象的状态提供存储和恢复功能。备忘录模式由两个对象来实现-Originator和Caretaker。Originator需要具有保存和恢复对象状态的能力，它使用内部类来保存对象的状态。内部内则称为备忘录，因为它是对象私有的，因此外部类不能直接访问它。图4-24展示了备忘录模式的基本原理：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-49-53-6bd50fb204244419aa8b7332e28b7a52.jpg)\r\n\r\n<center>图 4-24 备忘录模式UML类图</center>\r\n\r\n# 小节\r\n\r\n​	在本篇文章中，说明了模式是指解决某个特定领域问题，实现既定目标的方法或思想；设计模式是一种通用的、可重复使用的用于解决既定范围内普遍发生的重复性问题的软件设计方法。同时，对Java中常见的设计模式进行了分类，设计模式分为建造、结构和行为三种类型，并对每种类型的设计模式的基本概念和原理进行了介绍，在后续的章节中，将详细的介绍每种设计模式的原理、使用方式和适用范围，并给出相应的实战源码。\r\n\r\n\r\n\r\n# 附件\r\n\r\n​	在文章最后，为了便于大家阅读，提供了本文的电子版文件\r\n![](https://cdn.ramostear.com/2019-07-15-11-52-41-681967dedfba4e76919329c76c8b761a.png)\r\n\r\n​	百度网盘下载地址：https://pan.baidu.com/s/1OREFj5QmLsHcKNbaJGLTmw\r\n\r\n​	文件提取码：jqgf\r\n\r\n​	文件提取二维码：\r\n\r\n![](https://cdn.ramostear.com/2019-07-15-11-50-26-9cf42e53a0d449109917cb78370aca6f.png)',1,'','2019/28/21/l0n7h2x5.html',1,'本篇文章时Java设计模式系列技术文章的开篇，作为导读文章，将快速地对设计模式的基本概念、模式分类和适用范围进行解读','blog.html','https://cdn.ramostear.com/2019-07-15-11-53-18-a2652cde4de641cb9c74af54467d4203.png;base64','开端-Java设计模式导读','',544);
  
  /*Table structure for table `settings` */
  
  DROP TABLE IF EXISTS `settings`;
  
  CREATE TABLE `settings` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `_key` varchar(255) COLLATE utf8_bin DEFAULT NULL,
    `_value` longtext COLLATE utf8_bin,
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=18 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `settings` */
  
  insert  into `settings`(`id`,`create_time`,`deleted`,`update_time`,`_key`,`_value`) values (1,'2019-11-20 02:07:34',0,'2019-11-20 02:07:34','blog_name','树下魅狐'),(2,'2019-11-20 02:07:34',0,'2019-11-20 02:07:34','blog_url','https://www.ramostear.com'),(3,'2019-11-20 02:07:34',0,'2019-11-20 02:12:46','blog_logo','data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAABMCAYAAADX/oqbAAAgAElEQVR4Xu29CZhUxdU+fu7tng0GcFiHYQCBAUXcCKIiiKgIKiJuEBEQCGhiTL7syZeYxOz7YqLGrF++xGxmcUvcwX1XNAgKCsLgADPDMMz09Ha7771Vv+etrtNWX27PDIt+//jvfp55era+t27Vqbfe89Y5pyw6zK8nB4+ssyxxmiSa7hPViYg9QEoa4Nv2Eb609pBNW31pb5W2vdW35MsLdr659TA3oXS5Ug+UeuA92gPW4XiubNmU923p2zg9bUePF5Z1oiDLFpZl+xbZlmVbHpEtLdsWFtk+WRFf/4y/k2Vv6OtmXq1xncdP7Nj7oEXkHo42la5R6oFSD7z3euCQAMujKQsT5akT9lbEjklF3WE+WRZZlg2AEhqgpGVZADAJAANQ2TZAyxakwMuWNr5XACf6u5knjuvcu31g1nmRiF6wiFree11eeqJSD5R64GB74KAAS9LkIWk7taKzMnFsVyQ1UthkCxmxpU2Wb+UByJZEFgOXYlxk2ZZlWXj3bSviSzCvHBvjv5NlxepT8Zcmd+xpJaINRPSMRVRyGw92hEufK/XAe6gHDgiwJDVUEFVOc2znjNaqtlPh0oEhKeCxIxZYEwDII8sG0/KJ4BSqnxUo5VxBS4KBKabFTEzagmz19xzAWXY/z91wRutbG4lIENGTRPS4RRR/D/V96VFKPVDqgQPsgV4DlqSGyURls9J20t9X0T4bmhRrUR5giSwLACbsiC1IKmDKA5ICIuUWWjngYkYFOIPGJSPCiti+lDYZQBaR4uVzmxs3a9DaS0SPWUTrDvAZS/9e6oFSD7xHeqBXgCWpYQ6RdbZjJ+JdFbH3wdWTZNu+JW3fiijXL8esFNOyhZTQqqwccEXU7wFNinFZtta4LFuSZQn1e2Zklo2fhXYV8ftq3/3b6Xt2+gAtQSRtoleJ6JGSvvUescDSY5R64AB6oFvAktTQn0hcSyRrPNvv6Iq0nEhWRDGnvIgeUa6eJSiidgEZuLA/KCQAyM4DmBLblciec/1Y48L1lFifY2G2+hx2FPXPA7LOPdPaWxKCSNg5FzGlQevZA3jW0r+WeqDUA//hPVAUsCSNqCeyPw4swSsTaa/K2Nka6OYFgGVDRLdzQPM2kCnGBdfPhyYFpgXGpQFIMa6cS6n+Twn1+Jv6OcfA1N/hYuJdSntMMn73hPg+R7uHAC18bSKiRy0iCPSlV6kHSj3wHu+BUMCSNKqGyPs8kRUBBmXthCOsrpGKEdmRvHalmVEuzirn8tlgYCpk4W2RXQERA5fSuBQQIdxBi+ysceVdxDygaRFeQiNrPXlv80vVnpsMgFaaiP5p5cCr9Cr1QKkH3sM9sB9gSaJyoqEfJYqMAIWxSWZ9ahsibOhWuTCFXDxVzhV8W3zXgGVHdKCojrvKMa0ckwIwRcCkcmzL3FXMxWvldg9zQJb7nNpt1Aysn5t9/qT2ll0BwJL657+XQOs9bKmlRyv1AEEED7wkDV4syDrFJhAjkkSJjKDkQBssSQHK2wxLAZZmViocAZp43mW0U55Fe6Rt7/HJapKW1V9I6i9t6i+kPUZYVKuAKcLXywWYMpDp+C0NdLnfk6T0yR17Hq3yslkDtCRrW1mif1SUmFbJsEs98J7tgQLAklRzHJG9OgdUSruyiNqriAR26fC/FsdP5cRxsmREx1Mhaj3HuDZ7Nj02xHEe7a7XdgwYMDbtR6ZQxDpJEJ1ixm+pMAnLst6O38oDmTU44zw8sasdojvvGrKexe//sIgQClF6lXqg1APvsR4oACyf+n/CpsgYFbWgvvw4UWcNg1UetFSYgm35gmwZsWxPqrCFrWRZ9/Z13QOOk9oyYOAcn+j9PlFDzuXUMVshLqJtye2ntjW/DlA1dg2DoPVAKV7rPWappccp9YDpEkrqO5sosgDyVK5nIpIomSRy+hlAZcE1NH9GbqCwrKfLfP/WQ0lcbqL6qkS/xCJpR1ZKkpYKJNXxW2BaeY3Lsu0T97U8UCkEtgk5zKEAsHS81lqL6LnSKJd6oNQD750eUAxLEkWJ+nxJEA2yKaq1qwgRxSJEwusOsGyi+y2iOw9Xl7ze/4grpbRWcPyWb0dsaUHsfzt+a3Qi9vDQTLoow9KABRCDe/jG4Wpb6TqlHij1wP9tD2jAKptCFP1Azg2EdqXkqBai2KCgOxj4eZNFdNPhfgSAFkl7uYrfiiDV5+1IesRnVWczzzQkOk3h3WRYvGvIv/u1RYS0ntKr1AOlHvgP7wEGrFVE9hQNVBq0RLOglNodBGCEvDdaRD98p55/a3XNdT7JMyHGc/wXR7/3db2XGhIdiL8Kalf4Oci8OojoVxYRNg5Kr1IPlHrgP7gHQKkGE0WuJ4pGc94htCu8i1Yip38RhtVFRDe/kxHmO/v1m5AR1g1k2VEf5WuUS5iLqI/4/mtHxTtQuSFMuwoDsS0W0T/+g8ep1PRSD5R6AGAkiU4hslfiW4HNPuUO4ks0EmUGhwGWR/RgGdH973QPbu/bb6VF1iK9a5hL2YlYFgnZeFS8c58BWA/lQJamkQp8DWVez1lE3YZavNPPU7p+qQdKPXBoPQDAWkhE2CHUzApES03+zYIydcFdQZsI7OoGK/f+jr52VVWNFHbZLZy7CMBCCUBbyG1jErGEBibsTr6iG11HRIgjgxCXZ1qGCP83i2jbO9ro0sVLPVDqgXesByxp05dI0GhVsVi7hLmUHHqNyB1uMiytZT1kEYHRvCuv3VXVP/ZJqvgslKNBZH1U+FvqUwlXEO2KBHQ0SfRRUm5uKGBts4j++q40vHSTUg+UeuCw94Aly+zbyBcZIsoKQb6dZ1qRFqJsWYhLeJtF9PJhb0mRC+6p7LvSs+SCnPhuqxLMFcJ/fVg6iQ0DYOsXLCK0H7SwLxF9vkiuITMuHHTxrrX/3eqn0n1KPfD/hx6wZLl1h/Bl1sak9yn7NtOyHSJvDxH10R3BAaO3WERN71bntJeXT/KsyNdk7uAKC5H15Z63eXA2w1oV2og4MJSeOZ2Ijg4CViDANEZEf7KI8F56lXqg1AP/QT1gyUrrXhKUIWllmWmRQAgANC0fQZdHBFjWN61cAb135bWvsnKUFPQDUodX5Cqa9nOdrVWex4AVjLsK066CO4frLKI178oDlG5S6oFSDxy2HrBEX+th8qRjSSuzP9MSO5GjYwKWRfSlg737X4kiiw4wHipGNJDKym/Kl7EhyxmUTUPwL5b8zODUXa4h/gflaEoC/MEOZulzpR74P+gBS1RH1pAvcJSNQ0JmSNom00IlT+hD+fxBIvqORYSgzYN6STqqn0Wv9/r0G9TnSkbKfsNlbGwpd/fz3Moekp/DAkiDzAt14Q8m1zBXgjX8hXuUXv+3PVBsfA51bLobd/OJD/U+/7e99//xu1v+APuX0pfH2r4CrBCmRW8KojIj0v2XFtHug30uSUeeSGQ1W7S9V2WNu4gGl9mRHzDDKpf+znIhENDaYy5hD8nRvckz7M5I+W89GWhPf++pKw8HQPY02bprY0+fRfsP5hnDrns4rtOb6/Z0n96Me3DcerpmT3/vTT93Zys9XT/ss4cK7gfa5oNpY0G7LVljf0b6dDlJK0OecHJMq0DT2k2CEPPELAuu1Gs9zbJif88lWh+52qLGn/fmGpLoNNe2V6ldQin3VhANIAFvMDQ4VDGrbv5m/v23FlFzkTaYA8HfH8jgmANT7PueHj+sDcVW8mKGUOwaHKfG1+stYIWBdNjzHUhfFQP+now7f4/TTz/99GQyOUhKaXueZ23YsOGfgc7lawXfg2AbNtYH8izFQKynZ8HnDsd9DnY8D6TdPdnlgYBqb/olAFiDI/OFJ78DoBK+zNgArP01rS0aBABaD1tET/c027r7u6RRVxNF77do21s9XUcSLRZEZ6KMjUe0L0o00BLC16fnhAFXb5kXgl9RH77gtXr16us9z5vj+340m82WeZ4X8TxPvWez2ajv+3jH7+LZbLbL87ztrusmUqnUHTt27HjcYBvm5OB29sag6IwzzviR7/sL9f0wCbevX79+VsgkNK9bMPirV69+3HVdtLcsk8mod7Q7mUz+9yOPPMJxdGb/5SfvqFGjtnmeNyCbzVqu69r63fI877+I6C+6HcF75+9fV1d3TTweXxaPx3Fi9wfMdq9cufLOWCz2XDqd/tt9990XdqJ3d+DCl7LnzZv3mUwmc5bv+0e5rhvBs+IdgKXbu8lxnH/s2rXrf0IWMH5Wda9Vq1bNIKJv4hrZbLYc757n4Xv1rm1Ajb/neTb6xHVdy3EcBZC4n+d5iUwm0ymE+FEqlUKsX7H+EUuWLFnouu7RmUymUkoZSaVSZTw+2u7wPIRxE0LgPpFMJoN74L6k74vF9rNF7C0IxKrfZs2a9XHP8z6FfvJ9H+PqvvLKK+N7sCtauHDhEf379z82Ho+rfpZSoi1Rx3EsIQSIhI3v9XskFos9v317qAdVbGx7DVyWHErDhGc/rZiVLx2StsG08prWHhLUgqvaRDstot/1BDQ9ANYiIitp0Y57erqOJPpvIjoya9s7PcsaXybJjwjfCwGsRp9oW4SoioiqjWh3MK7RAZH+TYvoj2H3/tCHPnRVOp1eKYSAkZRrg2WgUu/aYGHQNgZMGyze30qn0z9tbW1FmAW6C6V5wkA11KB0e+xZs2Y95bruSEwU3AuG2tHRcWpTU1NbACzMe5jXtFevXv2o67plevIBcNUEdBznL2vWrLkhcJ18OydMmDDMcZwNuKd+LguTRbfjk0T0t8Az5T87efLkM1Kp1K/i8XhNPB6PxuNxMB1kHuRfK1asWBuPx6scx6mKx+NPPP744x83/txTX9kLFiyYqsFlJAAYz8iAYgCWAhQNXDsTicT7E4lES2BMuN2q31atWvW4BqRy/Q6A4n7DBOeFSoEiAzm+DwCXlU6nP0NEt4XZwMqVK3/uuu6xuHYmkykTQkQBTPoZFOjq5+KFwkb/w9bwTPjS4/FdIvpJN95E0MbsM84444++78/Q/aRsy3Gc5W+88QZOVjdBvGAcrrrqquOFEDeizQBUbre5gOP32kZ4fqCdb7mu+6/GxkYuktDd+PYKtFS1BjE48i8p5EQSpFxCxbSCmpakLUJQUqfq/OZQEp8ljcTBrMdY9BZPnFDckkQwzhUZ296WikRPigrplZHwy3zpRXJlm80OSFu5FafgJYkuJqIpAcC6yyL6d8hNbawmVVVVfxBCDAZgaYDiSaEMSq9QMCwFJmxI+h2G1uQ4zuWBScITpGCiBNswa9aso4UQD/AEwTsmRTKZ/M6bb755q2FYuI75VTDgq1ateozBSrMPBV6ZTOblNWvWfEL3HcC84Brjx4+/OJPJ3MyAFWBYn0ONsbDPTp8+/ZR0Ov27ZDLZByuxBiwsSB8KANb9BmBVJhKJ9S+99NLyYgBqMpVLL710BRF9XDNGHgvFgnhcNJgUTPBsNtu+b98+xOjhWWE3wX4DYH0pm82ei0mpQVABPDMevXhgLDD+CrA062FgVOwHIJnNZpullDN12/P3u/LKKy+ybftaZrwaqHA/XFcBlwGM6hl4sTDZbjab9V3XnU5EDMK8cIUtkNz9AKxNvu/30XahmKLjOPe88cYbPG/M65jXotWrV9+RyWSG6EVQzYeQxUIBOzNQDbLok6TneX9obW0FcJmLbLC93S3k6jly5WWGRT8vhbxW+jJjSdshXzhFNK2NJMglIiQSPxKKMr34paQRHySyR1nUdF13/y6JPiJsu25vWcWEqJR+VAqvTEg/In0vSuTbogC0pJVLywkC1jIdTMraFlgKgl/RWcGXKgY2Z86cgQMHDvyg7/uTsZroQVJsC8zLdd1qKWUtAxZPEhiy4ZK0d3R0XKKNCn1WDGBMoLHPPPPMj/i+/ylMFA2GbAAvb968GWyFQRrXRE2wUNAyAEu5OHgGzRzaH3zwwaWG4eDz+WuNHTv2Rs/zLsa9MVngOvDEdF33i0R0t/FZ3N+dNm3a5PLy8p/H4/H+iUQimkgkAFj4QoL8R4xOlldeeeU/k8lkVSqVqkomk5WpVKoiFos9uWXLlmsNIDSfSz3vFVdccaHv+1/iicJum+u6vuM4bZ7n7fQ8b5zneXXZbLbCHBPNtv4Rj8e/qvuMx8OsqUaXX375Fb7vz3Jd9wi4aI7jqDH3fX+IEOIIMB8NKGBVuzExfd8vcJv1ZN3r+/4SPTaqj/D98uXLP2VZ1mw9DophMWPR11VMSzOs/HUDYJXMZDK/8jzv1waDBygWswXlGM2aNes0IQQYlgJdvGcyGSy4ezdt2jRXjxHbkmlbqv+XLFlyhmVZn0VfcPuZYRkus7ouL+K4vu/7CsT1GLzc0dGxOGTRKADH7jZxFGC5w6KzbZJ/gUtoCSsj1W5hEU1LEkALYQlIJMaRWwf8klT7VaJIP6KKr1q0LTTiXBKd4BJdu6dP9TDbF34OsHyvjKQf9YUXIemXCVUNlScwAOl6nJphNkgSzRRE58GF1LuGKJ1crGqDAixSGwPqC8GpFfod3yMmDb9X/zdgwICBQ4cOPTMSiVzsum69sbpjosOoH0qn05/WxsSGG5yMvKooMXP27Nm3Z7PZybyy6xULA+5t3rwZOhaelw1UpVQZxsp9Ya9ateoR7dKU64mhNCxc98EHH1ygr8NtQpaAMtJx48bdm81mJzGLYNcKRuh53teJCKzJBDln7ty5f0in00fH4/GKZDIZ0YDVHI/HAYxm8URvwYIFC2zbXuU4zoBkMlkBwOrq6op2dHR8ta2tDWCIa+cnOb6fM2dO/yFDhvwxm80O0RNbuWqpVOqBHTt2/HrHjh3QInnMImPHjv0CEZ1jPAN5nufFYrGLKLfDjevzM/MkRffz2GOskZaG8JnIKaec8t/RaPQ83X9g0PbGjRvPM+zM1GZ4fMw+Us9z8cUXIwuDsAjgObq6ugCIAK7ydDpdBj0LLiJPcGhCmPB4DnwGOlkymeS0MvM+YbZVsCCfeeaZP/N9/zwGLM1IcX27ubn5wo6ODowT2xX6BrbFfaNA7/TTT6+prq4+yXEc9H85xg7ALqWMRqPR0VLKEUKI/r7vHymEGMWSgumBZLPZpxKJxAf1GPQEtPthS/4QCr82ciNJWmwJcnJMq1tN6wUSCqxu5zy+3qKWpKHTiKxFuRI20ZstanozhBWNTEejn9xd0efoMik9W/p+VFCOYUnhs2sY8X0vQuQb1RhuCp5NKIkgKq7SE/T1HvQ3E7BgtKbhwoDxMyYGgKtgt2T06NHXWpa1gDUGABYGLJlMXkA5YIcB5IEhYAx52j579uwN2Wy2rwYqdkGUuNvW1va5vXv3vqjBBddDxgFf15x4EJIf1SthBbs5WoSP7tq164ZXX30VugVACm1CXJ1q29ixY592XXcY03oYNARV3N/zPOgmYE08GdOzZs06q7Ky8rOJRALgU67BqrO5uXm2nvS8I8nupzj77LPnlpeXfzYej1em02kFcrFYrKW5uRmMlI0Yk1BNmsWLF19qWdaHMbmZLabT6fvXrl37U2NB4THB/axRo0ZdK4RYYDKtTCbzcCaTAavnCWmClpZo1fjyWAOwKk455ZRPRSKR87TrplypjRs3AvR5/pg70+wCBhcVBgAGN7Yj084YMBk8w6YVf57Hu6fFS13j7LPPfgKLqtaa8nYFO43H439samrC5gQzK9iDaVdmm3lO4F0Bup4Xqt95XgwbNqyhX79+X/Z9f4T2RBTwas1vaTab3WAsttw3wSrCxQFLDqMxwoo8aEmqkTn9KtODpvWEL6gxSvSv3oIV/k/S4E8Jskbm6m7J71i0pyAeSxJVN1VVfTVRVnECmFUFkWcL348qZgWGZfkAqmhAy9Ii/F/D2JMkOp9yetiPeyiLU4xhYWCKARYDl1VfX3+LEKIBQIWVUbtUP8tkMtCezEnCzKiACp911lnziOinmrJjpc1rArhmKpV6rKmp6TsGYIFZMGgxc1MTb9WqVQ8DqOAe6UnOu1+Rjo6OB9etW4eNExgojJMBKzNu3LjN7IIYmhwDFjQI7DCi3XiG9AUXXPDNbDZ7AsAKjAng09zcfGU8HkcWAbNR1i3YFRPnnHPO99Pp9GkALDAsfG7v3r2rs9ksjmgrAJRly5Z9z/f9E9mFcl03dc8991yuwQoMOGwRofr6+n94nlfNK3w2m21xHGeRAdTmIsKMJAhYZSeffPLnIpHIXAYssM2NGzdeZixa/HwmWPHk50WBWRADI+yGbWo/9q7nVPDcUH1ATN6rMFlcMbZN55133tHZbPZfWnvl3VTWYmFXrzU2Nn5KLxZsD0G74rlhLuImeJmLeH4xHzVq1M9934erzm4h3rFwoEgB9z8vTqb3wZ5HAbwUHvNVV3YtSfldFUCaZ1rdalpbSBDy8u7oCbQk9R9IFLmCyAbj0WWYy75g0e6CvMR/HzHotySscVFLKke7zJJeVLuE2B0ss0gBFwCsTLuGkbe1rEet3O7Mfi9JdJRFhOPBunvxoJiugekWmqsJryi8GtoVFRWDBw8e/FdjgsAVeTyTyXzZGBxz9SoAmXPOOedG3/fP1boJNAZfi6RqsF3X7dy2bRuEZwYaAJYJOHlB3wQsMBNs14Nxgb53dXVtfvbZZ78VAKz0mDFjZtu2/V0DsJRGg10wrdVgVyoPWHV1ddGpU6f+Op1OK/cAoNXV1bVh69atCH/AZDRjrMwJLE444YSx/fr1uxWABYEezKyrq+veRCLx/QDzyy5duvRWIcQgZleO42y9//77sXvM7jrGhV8ABDWxa2tr4RqexSs7FhDHcc4yAD8P1IYehDabExEMC4ClXEK4adBmNm7c+H7jnmFAFWQ+ebDWnwsyLFNu6IlhsetvulTmIliwEM6dO/caIcSnM5kMa1dpKeUReA6ETUDE37p166WBhTBoV9ymoFTCoGsyrHw8W//+/Qf269fvD3AhjXHoyGQyYKhsx+YCZc6J/XYO9zv52R9Rdiv54mLSLmGPmhZRxvfp0QjRCxYRaF4IWPSbTmSfQSRrdWoiACtm0V5MZPV6eGDtpAqilVES0wFA5XAFfd8vJ+lFhMwzLMW0RE50xzvCHMDA4BoS0WsWEdyEg31xRzMz4MHBoPD3+J/gapIHurq6OmyPKw1LT3wwFmwGwKAUKwlx5dTAnHPOOY/4vl/PgnJLS8tnjjjiiJ+we4ZVqqmpCSwEFSowyAAs/sLPTKntD3zgA2u1K8gaFm/XYzcq9uSTTyJEgQ0Gi0a6oaHhE1LKy40tdOX+sBbheR76Fpst+Fzm7LPPPrO6unoJQIcZVnt7+81vvfUWwjow6WFf7Ary8+fbePrppz+aSqWqmWF1dXV1JRIJMBd1fQb5pUuX/sMQpwFuz61duxY7zAAsHgvWdJjt0NChQ8+xbftLxgICwLoGxSn1tU2XmgEFQ8F6mJqMp5566nWRSGQeAJ/DTDZu3AimxvMHtseAjHab4Gxqcgw05uRn2+J74m/7zcuAQbMLaorkfB9Td1L3Offcc/8KXZSZe1dX1++i0eglrutCE1Tj29HR8fV9+/a9YPQL7AoB4wyEpstsstBi7DAPYHV1db/2fb/BXDgQQxdYOIrJGwWgFdoxfl30VZI0upBpdatp5R5KYJs18jxRdDORGAApgSjSQGSN1Dq1Lr+sxuQ+izruw3drho642ha0Kir9aJkCqLcBK8+wCPFXwisnoUR3tUuoXEThR2RegG+3iLCqHsoryLJ4xWWgyovu+iYmyEVGjBiB7V9oBQxYe7LZLHYq826UBhle3ZWBnXfeeRN837+bt+h93xdPP/30/OOOO+7XUsrxerDtZDJ5e0tLC1xMBj42LPzMYEAmYLmuW+44juP7/kAdXxZ5/PHHseNYwNTGjx8PhneSIVZ7vu9XGBrWzVhb2NDOPffcy8rLy+diN40Z1ptvvrkoFouhTcx6YHAMPiYLsKdNm/Y7uJMGw4Jgf47WGxncM0uWLPkVGBbHRTmO0/zAAw9gMwOAxf3P7lGeyVRWVg6urq4+lQNL8eyO47xERNgpRl8x28VkMRlQgftz2mmnfdm27QvBUDg2znAJGSAZqExg5jaZIGK6hMzkTf2tQBs1DNmcuAx8pvAevAf/vz137tz1ruv24SDY3bt3X9OvX78PSimnsauWSqWeam5uhsuP9psLIdsVu8ym98Gs0JwTeYmENzGGDx/+VSHEnABgzQnMCZPRFWVZRZHcHxFNSF9CaXJ6pWntV08rWCMeP8O6cucevtSv+qGmfuXHRIU4M+rLUVESXkTmgAqAFcW79P1yQUp0z/3e96FhsUuIdzCr/P+R8K1A3M9BIpcJWjxAwd+ZA5NflYcPH36f53kI+lQBfp7ngWEh7ikvVGuDUKyGQebcc89dLYT4JAOW4zhvvvDCCx+fOHHiJyKRyIV6Zcc1N+/cuRPCcZhhgWUpwzIAqwLaz759+x6sqqq6SE+4aEtLyy/eeOMNZCyw6J486qij7sSqq3cG97nYmvS8eoNh/cwErIsuuuhzQohxYFhwC+PxuLV+/foLA+4gsyVTEwEjsaZNm/bVbDZ7qcGwAFhgo2BA/Dnn/e9//5cty0KwJcdFlW3evPm/t2/fjmhvjIMJViykh4nb7KbyRGdXhNuWd6nN3eIZM2ZgwiHUQwGW3iVEfB8mKq7FbQ26+2bMl8mumGEVsykTtIImzM9lghaDpvk79bl58+Zhs+AGDoD2PC/19NNPL2xoaFhcWVn5IWNTIvbWW29hEcvbgwFcvBPNbTHnBIMVC+55d5D7sK6u7tuQOsyYvkwmc25gTvACzHOiIOSEb1wUsOQQqhbl0ZdIUF1vNS3hi1whwHw9LX1Ga06zwmldSrtKHD2lrnP48P472nfJ9ramMWVSeAConIsnwaQ0YGkAk75fJvB3oVy/PGDh/7VrCJaFz0WIrj5IkDI/ZnZ6mHGZA2fS+0h9fbpQnHoAACAASURBVP0bZtyM53lPZDIZ6EVs2BgQFsvzgDVv3rxbXdcFbeeI9DtffPHF39bX108/4ogjvsqAhe35HTt2QHDmXUJeDU3GBsB6SLuEEN3Ltm/ffsvQoUM5vgvC+9r169f/wbhO+uijj8bOIsf/bHBddyh2efTmAX5/i+ESpi+77LJvZ7PZQel0Grt90K/aXnnlFfQ/G7EJ0myQmNxq0p166qmf8zxvJWK3AFpdXV0ALKzyDxqrb2bevHmXVVdXX8qTDs+TyWQ61q9f/5W9e/ciLIbZK7MyXqF5bExNytTVmPGFTRIGk+jpp5/+TSnlJRx3pDUshEiwy8thEmEupqknmSyJr2/aF3/fkwmHgRY+EwRFANZPEM7ALDOdTm944YUXruvXr1/96NGjf88xU9Cy9u3b99lYLIbFAs/BdsULa8EuNIf2BN7N9vPzRerq6pBqNo+9hEwm42UymflGPB9Aku9Z4HkEY7K69ZVlHY2UVvQu6csJvdW0wBhsH5MpWCNen8ZTXSO6xoxbKvxsFAc6k2W7bvURb+71xK621u0DRCY+EMCDcAblGpLWslR4g/p9LnAUupaUb4c5SOFbioHRh60c3T/UVxg1DwJZwYozYsSIq4gIEdMqAl4zk19lMpnbA4MT1Ae8efPmrTcjq3ft2gUGgU2CPpMmTfo7REteDWOx2DeQrxVwC03DEiZggWHdeeedK+fOnXsbM7hUKvXvF1988UbW1UaPHn1aRUXFdXwPx3Hu9H0fLgO2pXmX0ASszMKFC2+EOwiGhfeurq69r7zyCmJs1Ekmmj0yiDDlz7utU6ZMucCyrBsAVhq0AFiQCaBP5V3JkSNHVk6bNu3H2Wx2IDYN9G4hdrn8tra2P7366qv3GvqgufOHNsDGzbg6MxSBdR9zh20/nWnmzJnfEkIgt1NFpGuGhXAVk2GZC1HoLnBg8oUtigdqsyZwMWAVLLwXXHABFi4wfrUQJhKJm//973+jeGXVpEmT/tf3fWxmIOTATqfTt7e1tf2vsWts2imz9+5AN7iQq36vr69/2HXd0bzoQoN1HAebR3hhDNg2eBOJ9ViT8ap/7kncI1lHowRFPkPSWtlbTYuZVmGN+NxpPMnJZ5zrOl1DyZLqBHopSapiolIIaVki1XfQo9t3vjbczqYrWcuCq6gYGIKTVMBobpcw6svcbqH0PVvFacGFFB+zSAW2HuorSMuLaQtqVRk9evRwy7IeRdKwjiBWuy/JZPJKIuo0JjCL5XmXcN68eVitAXTK5cFEfOKJJ/A7lKeumDhx4g1CiDG8GjqO8+SePXu+F2BZposJhvWAybBuv/32lbNnz4ZLNwwTD0bz7LPPQu9TWs748eNXI46MASuRSIAVwk2t40h3z/OKAhYYVkdHx1OvvfYawAZ9xW7Kfs+r/2afdNJJAKyfxOPxMoNhPUFE3wi4WtmZM2eePGTIEDAyFbXPEeE6Xi0Zi8X+uWnTpt8Ykw3GzofnBvVHtg0zLCBMN1Guz8yZM79jAhYm3oYNGwBY/JxgjTzZTJa1n4sWMMpubaqIAReI0AYIBn9P8+fPnyCEuItTf2BfGzduvKKzsxOAWnHUUUd92rKsmWA+yIlNp9ONLS0tyO0MAgiznvxi081zFLCs+vr644joAdZFdSYIEtMhk7A7z/IGg/7BA1Z+ZOsj59s+fZwEva83mtZ+TKuqfyJx/KnzvFjHaLLArEIAy7aELS1PlJfv2pN1X29v2TpBMS3sFuYZFjQr4ZUrFzDnQiKgFIzLkr7fR4hPBaPdDxK5wthUmLZgH3XUUZ8RQnwgm80O0MxKMSzHcR5Np9OY+LwSY4KEARb+B3lsCpNB25955hmkwajAxdGjRy+trKxcqKPNEd/V2dLSgvy7IHthwwLDygMWM6xZs2Z9OxKJTOBs/aeeegqahRKJjznmmK97nnccA1ZTU9NZNTU1t/m+P5wjr33fLwpYEN0BWJs2bULoA6/2pqgf3HGiqVOnYmv7p7FYjBkW3hmwcA3WhhSYzJo1aw7EYtd184G1nAqiwdx1HOeRtra2G9rb2xHRjv7m2CVeoE2XMJhXGFzRlVszc+bM76J6ht69VfFLGzZsQGwfjyuPQzGGUMBKVqxYMS4ej0/yPA/MFNHtAAsV6a7ZrErPMQMuOcFaV3GAfaFvfhkCWPl7nX/++VdZlvVJA7A6nnzySdiNWgjr6+tnVVdXf4LFcEiWu3fv5vCGYqxnP7fTmF8FADxy5Mhjo9HoH1zXrTUzJxzH+YLrutj8eOcAC43qmvX+jj5u+15r57q4leoa2lPuYZ5pHXm8m+h3xCKZdfpIaYFRCVmEYdlkeZKEL6Ul3Mo+65r37HTdZEd9uRRKdI/KXMQ7p+qwaxiVAgDm9/X9/z6UAoPXXHPN6nQ6PRMpEgio42x6bUxcxgTxK0g9qIPG47pulV45TFcwvW/fPhgHyjkHXQcYQ34Cz58//yG9Cwb2gEl764svvgg3ErtgZQMHDmwYMmTID3EPHQ+FbeiPpVIp1Nw3V6f8btfKlSvv13FLSsMCwzrttNOu7du375n69/b27dt/tHv37vUasG5CHp7eLOjYtWvX5TU1NX8UQgwP07CmTp064cgjj7zacRwluBcBrGIrp9p/Ofnkk+dLKW/CLiEYls4/RAQ+GNZ+gIVfHH300WNGjRr1NYRZcdUBTkjW6StwbwBca/fs2fODVCrFCcImcJnMp9juGs9DMKzvaoaVT07uAbAwDqaOlgeR5cuXfx07ZoiLCyRBK3ezWLWGgGANPRHu/DcNwOJdvPy95s+fjyT+yZzhEI/Hn33++ecReAy7UjusEydOvAMgqeOxkJnx487OTmiI5mJYsEEUkuuXB6qTTjppoud5DZ7nLc9ms9Ngr/A4DMB6LR6Pfyygc5obSCabO3CXUPlxn/njiV333bLOS+y1LexWW0SVdaPbyvxkU3Tvtrhob66xhPAK6mmV9Y37R06Oun36T8p0tY1RK52Fkjk9ARZ5kqQCLLKEJ8jOxCLR+7uaNk9FXFbUygWMArAiSqiHMC+UvqVSdnz3y1VE2w+SVSEr/VOe512i435UtQYWe41yGgAmFZPDycEcK6UNq8NxnM8lEokdgWhv0wgUYM2ePXtk3759lbak43yiO3bsuHb79u2YaByjYx111FF/QtS2NgAYwV/37dv3WyNkgIV8tT2/cuXKexmwNMNafvzxx58xZMiQj2kDtvft23f3pk2bUH3BPfbYY/+mV3cww427d+/+TE1Nze+ZYenUnPwu4dSpU482AEvFYWmGhVgtM/aKWQfeWQeBIdrTp0+fns1m/8IMSwMWaoqBcTJgcRQ0i/UqePeUU065orKy8mIkJQcAi3dnMandTCbz0z179kCXYeM3q3zw70xhnNkhT8ICwOKqDQZgBXd/+TlNF1M9zAc/+MHzs9nsdRgPnUOokp+57pahkXFdLzXRMeGNYF6VQeG6LpjQOuO5guI+SvG8zHaF+3R0dNzy8ssvI/CXswOsCRMmIBJ9OOxY32vd3r17we6Z3SbPPffcY2tra+GOY6FGQDPXCTOrS6gdVA770DvNJlBBIulMpVJXZTIZ2H5Q5yxmJwWubo8alnvNTZ+LP/irb5OXtaQliQFLKvyxUQiUotVHpKPRSFtUeO1utOII6Xn93VRygCRhqeMEcwbcI2CRZSEswTcBi6TlSyH9TpIPxVrePBHJzyrCXbmCEN11IKl2DSst+npFLr3jYF72smXLRtu2jax2LuCX10w4jkWLh0p81W6a2lnTA/5cR0fH9x3HgY5mRnub4mJegL7ooouuFkKsNIrGpR577DFktKOuVz5AtaGh4TtSyuM4GTabze5oa2tDEKQZNsDb6gAspGKg7cywlg8ePHj45MmTb9Yru93V1bV+w4YNN4wYMWJiTU3NF1lcT6fT/2pubr6lpqbmf5lh9RKwntYuIQNWd26Ffcopp1wohADDUhpWEcAyRXQ2Xo6urjjmmGNm9e3bd6mUEsnnmDCqOoCZQ+g4zgOdnZ0oocIgElpiJmSXjTWsPMM6FMBavXr1F3zfv0AvTBzQm99EMOpwcQkbVWIGz8PBvPp7jC1qbrFOuN/zzJ8//1Lbtr+oAUsliz/yyCNwY2FXebscO3bsB23bns9xg57ndba2tl4RjJFavnw5drGrtKehqpZwe/UCqNxYDXyq/WYAsud5uzs7Oz8amBfmzrnpdZhB0L0HrMzKb/0zseZ3EBfJslEJC3aYY1gmYEmJiAULiISz5KVlKyYFD1VKyyIbu4G9ACypNKxChkWSfIkwCcv3O9OpZ5Kde8aD1gC4EEiKwFElvutqDn1s64uVmcx+CdW9RC+lWaCUBg5kzWazgxm4uPwH1/sx6hblV5F4PH5Ta2sr0pSYGZl6iZm3l49JWrBgwS+klCfqapcAwVeeeOKJ640obtX00aNHL4lGo0uMgFSvra0NY8O7aWbgnb9y5cq7uSwOMywY61lnnfVLbAzopOY969at+9y4ceMWVFVVLeKJ3tXV9YO9e/c+XlNT85sAw8oHjh555JGDpk6d+kXtEjLDMgGLmVFY2AWHNaBkzM+6urrKdP0slKQJMiwGLLybwYuclqPc5oaGhll9+vRZIoTgZFssIirhVn9t7erqQhQ9xxQFKwWECeR5wOpGw+qJYeUZ2+rVq5djsdDF+9Tuqq4Iq6o26ORuZiqqKoQuz6JK/Agh0gjwzGazcOODtcw4ul4xrYsvvhg7m8h/VAnjKIfzxBNPoNRPQXZAbW3tidXV1d9igMd7IpG4NplMQm7Ix5ddfPHFl1VVVUFHzXscDFS8AcJlawLlZbxkMvmn1tZWsFwOL+F5weEobLum6xlkvWoeFGVY3lU3LYs9cOPvgVHAHguAhPcwwDIuBcAiA7DAwPJ1jrRLGC66SyEtEjYFActSjIuE79tCZltTiRfdrrbxKrpda1mma2jZ9JmBjtNj6eUiAJaPvTFSccykZ7UynXjiiRdXVFRcC/aiB1BNii1btiAYzgykUx51yPZ+vnzHJZdc8ox2L5Vr0NnZ+fN169Zh29kMcpTDhg07srq6+hesYcGw4vE4NJoHQnLj3BUrVtylr4scLmhY0NOqZs6c+XXbthE5D3YYee6555ZPnDjxk5FIZAoD1rZt2xD8adfU1PxaCFFraFg3EdFarqiwcOHCmw8HYIUwLGhYGIuwoFOlfxklfxRgcSpQbW3tSf3790cALiLj80wLEz+dTq/JZDJgJgxaDIJB941X9f0YFi9UB+ASmi6maV9cpYHdM3ORC9vcMU2WwdXc5SwArMsuuwwa5mAu4phMJtc89dRTcOk5lSZHIoi8hoaG+7Gvxf2VSqX+HI/HUb0BoJ5fMObOnXtSnz593oc0Gyllf5SZQZgJSyW+748xyiEpZphIJH7e3NyMVC32FjgDgtOZOJ4wGOkeusMaCljy6l8M7lh7c5t0M7lH6gmwFMOKKNYV2Q+w1NkVvWNYliVsizwp39aw2CVEFSHLly5Z1NXc2bZVOskxapcwn1sIV9H3rGjkv4akUsUOl+iJaIUBllkPK29Uxx9//NJoNLqCC93hPRaL/Xjv3r2PFUkXMQdExenMnz//grKyss+bFS43bdq0dPfu3fg7DzDTfXfcuHG36wJqXCjwxc7OTmS9m4algiBXrFhxpzYk5XrceeedSA/qe+qpp15TXl5+hhaoI01NTd8bNGjQSikldgNhZB2NjY0ox0M1NTW/9H0fhQo5Dgv6FAOWFwSsWCz2xquvvopzKxmkiwUE9sSwwgCLgzvxrBgnGJY56fM5bf379x9cU1Nzne/7x5quYTabRS0q1DRHehHHSpkxU/uJvDqs4VBcwjDACpYuOhzVGvLleObMmYPF7c9m7bDW1tbvv/LKKzjWjhfCfML22LFjfymEQG18tqvtHR0dqBRrJnBzPzGY5+uP8SJ9wgknLCsvL8duOde6txOJxJ+bm5v/bJaeMa4b6nUUKb1UnGElz/1Ak/PqE/Vk2coLLMqwCO4f/L63ASvoEoKZBQEryLCkRb4twbCscIYlpM+AZVnC84l2NrfvEpGsO0DFYQktwgvf65uJXt2f4gVF/HpCKePvvQYsAMrkyZMBIP14hzCTybQ0NTVxhU32z80oaNaY1Ip+6aWXXi+lRBSyEjEzmcy+hx9+GJHiAMn9EmvHjBnzPSnlDD0JFbh0dHQgCZcBIq+NrVixArs/KiJEMywAVtXEiROnDx069GOsNXR0dNzRp08fUH1oP7jmpsbGRnVYbk1Nzc+ZYWkNC4AF9qe0kyBgJRIJBI6icqjJKou6hDNmzLgSWQABhvUrXTfeZFgsZAfdQq6eYSbjchWEaF1dHeLXjjWTn33fb3FdF3W3zDpgxQI90Q2HKroHAcsMYu2pvAw+a8oKbKrdMqyLL774Q5ZlLWfm7rquWLNmzcKAzJBP2B4zZgxiohC2wVqsu2/fPuhdeJmJ6GYcVhCw8LM1efLk32IH3ai8m25sbEQhR45Z43mhwmmKFAMIdQdDXUJn8efXJp74+1k5nLELGFZuCoVoWPrJoGFBZLfLyvxobcObZbVjn48MGPQY+daeyOB64e/dUSuE6CNa3jwjs2f7STIZG/h24KgU0qZuNSzLk65lSQ/uYZcnnuxq3/m+nJaFWu+5elnlmdSVQw4+cJQBi10Os5QGTwoeKHvSpEkrIpEIJh2faGLFYrEbY7EYWBazHjNGh11BNfALFy68B4yJASuZTD73+OOP/8Cg7bzCKbo/YsSIRWVlZZ/m3TztFn7Ydd0CvUEzLOz68ak5zLCqBg0aNPjYY49F9UlobxBHU6gSyQaWSCQe2L17N9wBANbNAYbF5WVgdP78+fOR0zjWqNYQX79+PWK7egVYM2fORE2sxQENC+VlsJPVHWCZqzz+zwSugu9ra2vvQUlrLQJjnPA6W09E1hLNdJBgcKQCrMOhYRlpLGxXQaBl5tgbl9DUsNDmfPL2ZZddhrpsJ3KQbTab3bp27VowcdMdzMe4DRkypKFv377YhWa9D3Fhv8DJRtqO8b9msUMG0vzioJ8tMm7cuNnV1dWfZ3uC55FIJP7e1taG05ZMuwjGEJo7yPuFaDBSF7iE3kd+sTB25/f/CgE9x796CViaYdl9B6Qrj5nx54px475qffMDTd0xG/mVr1R6zVXTM1vWL3Vbtl1oSepRw4JLqABLCl/6IrOzq7Up4nrD4QqCaUGEH5lOLDrQKqgBhqVWVUMnCQKVOUjR44477l9gWQYNbt29e/eHQzLRwRJYD/AuuOCCE/r06XML13iCuN/W1vaLF198ES6XSdvzu2SDBw8+orq6eo151FQ2m/1zIpFAhHeB3rBixYrbGLAMhqX0npkzZ8IFGKADSDGJ88djxWKxm1tbWxELJQBYAQ0LUezMsPzzzz//w9C+GLAQ2vDvf/8bScHmLqG5Xc3akdKhZs6ceUs6nZ4XYFgAEzy/Wt0XL16Mulc49Ui103XdHdq9NQN7eaLzYsJjVD5s2DAExJ6j63pxHBt2V18tssIHk257isPqteiun0npYiHpQmbFAzZJE7jM3TJT39kvwXrRokVP6zFVgntXV9fdTz31FE6JMu2KXUjFWseMGfOk67r9jaKNLyYSCWRCBN1C023mNpu1vDAnfi+EwKlGqrSz4zgtu3fvhudh6lZhqUxm3xfsDoYCVmrBtZvT6x48Ku+MBABrP9HdcAmr3nfeXypHT/i89cPViD06oFfm/V9ekH714ZtRATlUdC9wCXMMS0ry4q73aldn8wSI7yrMQfje6HTyQrWJefAvk2WZhqUorwFm6m9HH3006Df89nxoQzwevyGRSGBiM9hwYif/LBYvXny153krWL8CYN13332Y7CwiKxZjTCrlRo4ZM+bRTCYzVLtocOEaE4kE3Eg2LOV2Ll++/C86sFLVwdKTXF17+vTp37Asa7wOyVDiO6+Ib7zxBra08ZI1NTU3+b6PVB7WsABYCCpUbZszZ87iioqKOQbDKm9qavravn37EMXMO0BBl5CNEjXCH3AcZ6LBsGQ8HseBCPsBFgrn6TilHXfffTfnoQVH2WQoqpz14MGDl0spEc1t1vUCi0Vt+qCGwjoZT0plC2bg6KGENYQkDOfZuvG3UM/HeNBg1VEObVAgdvnll18ghFDhDMywtm3bdv3rr7+OvFRT8C7YfR09evStvu9PMVJoUl1dXUgPC7pw5gaFOVcYiCMTJky4vKys7L9MLSsWi2FO5EsTFUllMsGwe8CS1/15ZOfff7RDpDp4O1AxrO40LLJsGSmrkn1nLr6+7Fef5Ojkg4IKufqGYZ3r/rnWziRrwuKwTA2LIMoLCPOetzve/rqdzYzLRbyLPWOTcWg1h/IKrtw8EEEgy4u+EydOfMRD7JnWABzHaW1vb8cBokH9Kp86s2TJkt+6rnsU13jKZDK7H3roIRTVC9J2M2nYGzVq1A+EEPN13BfcOi8ej0NvKDCs5cuX/1EDlorBuf3229Evakdq6tSp11RUVJwZBCzHcWJbt27FSqg2SWpqam4MaFg/0oClCshNmjRp7NixY7+oAUuFNrS3t9/f1NSEU73NLetgyooyxpkzZ25BpQeOdEelgGQyyVHQimFdeOGFC/v27XsW4n40sCbuuusuADu/eLeLWQf/XjHjIUOGfEUIcTnHBWmgN0vYcP9y5kEwyfdwAxbaF5zops311nZ5Qhfspl1++eVfE0KoqrU67EDee++9iOvjcAbTTvKucH19PXIIrzGKRcIthNwAoDMXTo6R4iKC5iLBc6X8mGOOWQvGZmhZrXv27IFcgGv1lOwcClYFSO5ccd2DySduQ/E0rbLnXEITsIIall1eJarPXfXDsp98eL/zAHvb6+b/yU//fmjikd8/4KfjgzjS3YzDgoZFpHIGPTAsKT0/7YkX9nW2TtHHgL0yPtmF2tSH+gqCVjEjw6SoGDNmzMcikchVDCJgJKlUCiEHOLBhP1992rRp/caMGXM3n6+H+BusPo8++igE56A7aCbTyvr6+gVSyu8b5WYQo4OKENAI2LCyy5YtQ+kQrh/FDEuJvBMnTpw2ePBgnALMB7Xy2Yevb9++nRceH4AV0LBMwFKr4bx58xCIOhipOclksiyVSjlbtmzBBIHRmfpdQZmSuXPn4sDazwKwONK9o6MDuglSkjgK2p0zZ85ZgwYNWoR8Oz4PsrGx8cPr169vNAI92S0zV2g1eWpra7EwnBFgWAg/wYvdInZPwpJ8e8ol7MklDE6+IDgdDFiZYG1+jxhCaHZDOGAUWQtr1qzBmHKxQ3Nc8juvtbW1x0SjUdRDU3GFOif2HsdxwKrZrsw4Kc48UO698aU0xPHjx2NRuMpIWUMYDk7F5jAcM1k8yGx7BqzY1HOzXsvWMrUlyFDWrYZFNOC8q/8n+rOPqS3ww/VyV/zgnNSLt/9M+KIMqTkqrEHHYamwBgAWCU8KqQALQaWtsfZXpO8eXyb8+yckYxBtD9erO+PKryYwhoaGBsRTQRfielItnZ2dqNTAuyH5na6FCxeeU1ZW9lnWr2Bczc3NP1q3bh1SLfLuUEBjYVHVrq+v3xowrHWO40DryesNy5Yt+x2XxDUYlgrLGDx4cM3EiRMRQJo/WVqLo081NjaCHe3HsHT8F1wpCOK8qsMt/LQQ4mSdT6hOzUHycWdnJ1xiM/m5oIrB+eef/1gikag1GJZsb2+/QqdtcL+7M2bMOGbkyJH/BcDiTINUKvXKQw89hPLa7DazNlZQNbSuru5YKeVdOIjDOP1nh+d5fLhrGAtEOzm/UC1UPeQSHihg8eLHNmrqVD2J7aZd7zepL7nkksmVlZU/0wGo6jzN9vb2Pz/zzDM4KMYMkzFjn3jn1R4xYsSznudhE4hzYhsdx0HJJFNuKGD8ISVz8htV48ePf8r3/f5cvSSTybTqOZFP+9F1sEzACgstyT+3Qif3Qzd+I/avn1yntHblIasI0f1Ed1PDqpw0fUeff/3yyMOFDOZ1shd/YmVy8zPX5wHL0LCCDAuAtdeJPZd1UtMinvztMcnO3x/mNoUZEa8orEFUjh079se6SJnSSjDB0+n0DxzHQX2nAgO5/PLLUUHzNKOcjLjnnnuw9RtG280tfbWaDR8+/FGc/abdGwBkOp1O81mDMAZ32bJlv9GAhWJ30bvuugvXz8eRTZ8+/TdaeM+nF7W1td3S2tr6lLYCiO7QsFS1Bn0vLAbQsPCCYfkNDQ3DjzzyyB86jlMdj8eRBI2aVqKzs/Nr6XT6Wb27xCuzMsxLLrnk+5lMZhZOgE6lUpVIy4nFYo+2trZ+22ACDATuwoULv2tZFtqBQyBUoGJHR8efn3322b8aVSvNHVhv5MiRw6SUt3qed4zJfF3XRXwSoq4xhsw2wnS2fFR9LzWsYC7cfrmEAbsMY1YHAli4nAlaYFcoe6zCGZBcjffXX3/9E1u2bMGZg6xfhcU+qevU1dWhPhaS4zmOCjXwuWR1sZgp0xXneaHizMaOHfspKSWqa+T1Xcdx4HkE50SQ2Qbd+0LAyiy5/qn4Y388jQErF55QfJfQKquU/S66dnH0+x8IPaHmcABG/LQLf+Z1tswhaQmwqXzgqCqDLMGwlIYFwEq62Zc7kp1Ty0h845jYPjCAd/IVdBcBWpVDhw6tr6qqukfXw1KAhRUllUrhBGBzsLNLliz5HaozcGJqKpV686GHHkI6jhndHnQleQLYuuTsMiPQDzsx2PnawvR96dKlKPyvDk4Fk7vjjjtMwEK1z29bloVSM3nRffv27R9OJpOo4KkMpqam5mdGLiFOAALDYtGd2Y03ffp0nMk4L5lM8rmEkWQy6cfjcQAD15+HUaaWLl367XQ6PcVxnCoNWDjmS7711luLHccBsLE7yGDiTpkyZWxDQwOqrqo6WCwoo98aGxtv2rlzJx8qobbetyD15gAADadJREFUx40bB5fvayj3Yyala2BHOhP3c3c6Wx6wZsyYgb7D9dTJzEZ5GQY9sx5W0eRnNsrLLrsMO6HTuBIIJ73jXZ8sjpg85OIpV50BF7X1ma0AeHUQb97Wly5d+j9CCIypYleZTCZ93333QTcy3cEw4MGzwq5w2vkXTcDyPA+nTGNh6GmDwmSMzLKqxowZ87zefeQUttZ4PB6cE8EjxYqyLMWwkuddvdfZ8PCgfMHXHhhWn5MX3FP512+pHMN36pVe9IUznY1rfkUIdxDkIZcwTMNSeYbCa2uM7xtR5vofPj7ZEXpyzzvQTpNlqUMlR4wYcamU8js6B0xVDXBdF8Llaxz3c8YZZ4wbMWLEN3QKg9KY2tvb//Xkk08C/NkdNDUDDofIA9bQoUNxSClcvnxEseu69/q+D41JiapLly79JSYWJ6neddddcE/zydRTpky5try8fDYDVjabTb322mvYKOBJCA0LqUDMsABYYFjQIPDKR0qD5Zx66qmIqToZDAtJzIlEAvE3qG2FINR8gOCVV155u3lUfSKRsHbv3v3FlpYWjJt5qASnmiiXZf78+VfjqC19MjKqHCh3Vkdz47CMPZ7nDYQLYgaKGq6zn0qlrtN1mHg7vqedTDynqioB4A3Uw8LJz+jPoCbULWCtWLHi59lsFuktivnqI9j4iPp89QPW3Lg0i5FIzHaFYE/YjGJH06ZNG9DQ0IDThfKHzcbj8efXrl2LMjTBcAZTs8vbVb9+/er69OnzQgCwXvI873OGjhUs7x0MA2GpRC3kI0eOvFpK+TlDKgFr+77neQfFshRgxSbP9r22RjsPWEXisNglHHDBh66J3PRRaB3v2Et+5a/VsdtvfFS62X49aViWFO72eHu/Pp5z4cT4QUe5H+izMNPKu4WYcLW1tYiuPp8DFV3XxSEUiMtSAu9FF110SVVV1SXGrld027ZtX9uwYQMStk3ajom6X+13jg+rra3dks1mKw1Rs9HzPKymivksXbr053xwJib1HXfcUQBYkyZNmtu/f38cQsDFBhs3bdqEzRMGLDCsXyFlx3EcpLQAfFHllBkWBwGy+ErHHXccSiZPCQAWmCOeXYVbLFu27DYAVjqdrgKo7dq164vbtm3bGKhOkWdX2qVUlUMvvPDCr1iWdbLBtBTbYeDWwbAq4dmc4JpZXZ9KpRBuEaygEUwfMmPFFGCB9c2YMeMP2Wz2NA2QqDgKFofxMrMMui3xu3LlSmxGfATtN+thGWVlGLj2Ky/Dz6SrNSRd1wVLw3Fvig0vWrRodkVFBUoj5QFrz549v3nmmWcQxGwuhMUSjdVzDhs27CWc/M27e77vp13XRW5pcBMlzI3jOWRG81cMHz78NiHE+wwgbHUcB+EzxdzMIAgWuoTt9Q06toNFrOIaVqTPEd6ARdcNt74yH37xO/qKzbj0J177zvOsApewcJcQDAuA9WayIzq1vRXM4918BVkWmFbVwIEDV/u+DzDgCGvUOVcsCy6R7/so/MflOMQ///lPgElYOIM5AVgIxj0REPmQ67rHGIAFBsTPD8BCUKoqIYZJbTAs1T91dXWjR48e/RPtYtmokrB161azvDHCGlCtoQ45eFrDwlH1YFi8pW0m3KrJPWbMmK/jsFIcQ68ZFgOW2nxYunTpbdCt4vF4665du37+2muvIYAzeCBqQRCs1qnUhDrzzDPfX1FRscj3/RreNTTCMwC+JmChJNajLS0tX9eTljU8tJUnILM/syJqgeiO/q6vr6+rra3FcWNI9QFg4ZgqTEwOEzB1utASv6tWrfoECgEiaVgzLBVygtAW2IPeNVZ2YTKSQHmZZDabxYYNgAj3Vu7T0qVLUcPqFFxXu65lzz///NXNzc2cl2qGM/BCaKYkMWD9AgBlAJbyEoyTjMyQhGKljAtYFubEkCFDoB2ewLm3vu9/z/M8xMKZ6TksvhcNILXkR/8ybt8dX9yqrLgH0V0V7jt6+vY+9/9m7LuBCrG5V8/z3lr/40LAymtY+V1CxbCSnVuntLdwHt+70TxeffFesKLoErRVlZWVCEGYkclkID7joAR/8eLFX8agoa4QwCKRSGxfs2YNUmE4MBVgYIr0ZtoIi6zYrr82k8msZE1FswukziDBVc6fP//9QgjkeWIClK1ZswbF/pgVqlX5fe973yc0EGE36c7W1lZUuWCG5dXU1CC5G1VI1SEFnudBN0EaEF5mpQAuiofPqtCJ6urqDycSCbT9T+b/Llq06DNbtmy59eWXX0YmhAoLMasthKRvYFLkn1sDe9Upp5yyxLbtEy3LGgydDecnIrcSNfuQD5nNZpN79uy51XGcDmZJBoNF+4Nb9eZk2S88gu87fvz4syKRyPzNmzeDbbJLyLFF5rZ/UHRXtdaEEGMR2c+lf/hMR7i6ulgkF8EDaKmFgiupOo7zguM4YFUMuPko9yuuuOLzUspheoEqQ92p++67DwsMxiPIBBl0zLpTyo4HDRp0jpTy2zqsQZ0U7bruXUSEsBn0i8nQ8pkbxs5x0PPgk9P79O/ff5U+qBc21eL7Pk7PxqLH/cbXM/uuYDfUkl+47RNeJm3bUvpkkbQt24cqJBBNYKGYXpmgCPnkCyGklGWVlVnrW5ej8e/4S37lkcp0y8uDZGVa9ImMcakzJahfXFBbTNCAIYKa2gQNahT0/JHiqeZbLpvR0YrJ8W6/9tsx1JNQ1WM3TkBGu8z8LwY8M9XHBAIlUhtnFxZEX3O9dwZHo4pkHnB6qGfOLInbwekV3H9qFzBQgwrXNpOyWQMyJ7h5pBa7uOw2Mchx7A4n/5pumsl8OE6KjZbzBFGEDv2LzwU/az4XPwu3O6/PGZOPFweTbbDgbo6R0mS068r35g0C7oew04u57WwniiEbYM2VG8xDSc3FMGjPvIPG7MpMzeHP8RignebJ2BxiEwTW4ILAz2n2M9tmMGHZdKGDz1ogl3AteT1mw/SBtszSeWHmcQh1C3usOPpuz/6Dvd8Tg0dMOH3vLl79D/YyB/M5c9eQd0dMsDLdEF4Z2ehM6mwCAW/RmzqBOYlMQ2CjYqbCz8DgaE5ak2EFWQS3hXk2T4j8blkgfYRdjKDmw31g1nfi5w0GG5pgzW1jUMvXDDNAU0WvG2BtAkfQls30rODRXmZ+HN/HTL4tWNVDGDTuyy48szUzrKI3k9gElUMtL8NjyQsB9xMDI9sda4nBZzZtRG0gFVl0zXis4PPyGAftLTgnzDHDmKPtvDAVhKYY5Z/z8/I9A1gHgzSH8TPm6skDZDINdveYZfGt0f/mZxkkODs+bLUJ6mYAKp5AQbZhGhEDAhtvkInkdxB145R4b7hjfF9z8vPENJkYgxBPljCQNCcW/m4yMdaPTMZjrtzBic4szWy/eU/ua36e4PWDfcwAHcYWzDpWvGiYYMDjxvcI9j/3oXmdIMPi/ylmntwukwGbmhs+xzZosubgQhAGqqa0wa5ckKWZu7dBDcsE+rA5Eawtx89oXpPbZT5T/rolwDp8qBU2QLzS8TZ68G4mOzNdOQwaD2JYAKLJzNiwwgAiyBTMyVtsReyJhQX/bkaXs0vC7TMnjOnKcDu4X4pd05z4fG2TwYW5UyYwB5+XJ7kZGc/9a7KU4MRjEDAntAmQJhgG9Zcwl9C8DjNR/l1v5iQvNtxmfi5+XtO9NJkru67mMwfdX3ZZgwtuMfsMA+cwlhX2zOa4mzbP1zS9AHXN3nTO4ZvS7/0rBUGLB8lkEeZg8iQMTiym3kFjYGYSnEAMjMHVuTvAMrP+2XBMewgysOD/mJMm6JIwYAXZI7ORoEHnt3uMtBhzMnY3AcwJb/YzTzBmtaaLyyAbnBhBEA/e11wogn1tttfsj7DxZrvgdwY/85rFgNe0FdzHHAdzwQjaXE+gai6eJrgwY+b+5OuEgV7Q3sKuGXxmtglT5ywKgiXAOrwgGmRMwYkbBKhigGVOrjBmEgTGsPsEwSH4pGEMK+x/zN8FJxG3szu2FvaZ4HOb4BK8ZtgkMIGDWVrYpDf7gCe3ORnCWFUYyIeNa9jiYI5VGPiZ48bfmyzTvE8xy+T2Be8VHKdi7TOBtadxMxfcMLZajJWG2Yxpo0FmzeMU1raC8SgB1uEFLHMihhl5cLUNu3sxoy82cYOToDdPZF7LZG0HA1jdsZIwgA7eg/spOBFNsDHdqiCLCIJ32D2DfVpsohVjpN2Na7Cd5nMUe9ZiANjd4hC2AAX73hzL3gBq2DXDQNVcdHprn8VAq5i99uq6JcDqzfQ+uP8JrpZhFD+MYYVN1GITKcyFCBrXwbU+96liFN9cbcP+rzfAHDToIIgWu675fKbxh4GK2c7eAGJPfRW8dzGA7w3wddfeYqAb1r7ubKOn9vX02SDoFRv37p43aAth8yI49kVBvwRYPZnoof89CFTFgCtoDN0BQbGVuKdrh93j0J+wd+B2IPc5kAnQ08TvDlx7c5+wdhfr5wO5Xm8WsGJ91tv7FFu8evP5YuAc/GxvrtVbew1btAr6oARYBzKNDv1/ewMoB2oAB8pmzKfojkGFteNwsrdD7823r9Cbfu3uuQ9nWw7lWgfyHD3ZSdi1evpMsbYHr3Ww1ykGXL0emxJgHYp5lT5b6oFSD7yrPVACrHe1u0s3K/VAqQcOpQdKgHUovVf6bKkHSj3wrvbA/wOmv6/+zsYxMAAAAABJRU5ErkJggg=='),(4,'2019-11-20 02:07:34',0,'2019-11-20 02:07:34','blog_favicon','data:image/png;base64,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'),(5,'2019-11-20 02:07:34',0,'2019-11-20 02:07:34','blog_icp','黔ICP备 15000250号'),(6,'2019-11-20 02:07:34',0,'2019-11-20 02:07:34','blog_npp','贵公网安备 52230102000133号'),(7,'2019-11-20 02:07:34',0,'2019-11-20 02:07:34','blog_email','ramostear@163.com'),(8,'2019-11-20 02:07:34',0,'2019-11-20 02:12:46','blog_copyright','©2019-ramostear'),(9,'2019-11-20 02:07:34',0,'2019-11-20 02:07:34','blog_notice','谭朝红的博客'),(10,'2019-11-20 03:20:31',0,'2019-11-20 03:20:31','blog_keywords','Java,Spring Boot,Spring Cloud,SaaS,Ramostear,微服务,谭朝红,树下魅狐'),(11,'2019-11-20 03:20:31',0,'2019-11-20 03:20:31','blog_description','本站是树下魅狐（谭朝红|Ramostear）的技术分享博客。内容涵盖Java后端技术，如Spring Boot、Spring Cloud、微服务架构、SaaS架构，DevOps、系统监控等；前端技术，如Flutter,Node,Vue等相关的研究与知识分享。'),(12,'2019-11-22 01:06:22',0,'2019-11-22 01:08:00','gitalk_enable','1'),(13,'2019-11-22 01:06:22',0,'2019-11-22 01:06:22','gitalk_client_id','e29f4c596f24537e0e90'),(14,'2019-11-22 01:06:22',0,'2019-11-22 01:06:22','gitalk_client_secret','093e887a620928fc40e0cf6817af01b04bef23d3'),(15,'2019-11-22 01:06:22',0,'2019-11-22 01:06:22','gitalk_repo','ramostear-comment'),(16,'2019-11-22 01:06:22',0,'2019-11-22 01:06:22','gitalk_owner','ramostear'),(17,'2019-11-22 01:06:22',0,'2019-11-22 01:06:22','gitalk_admin','ramostear');
  
  /*Table structure for table `tags` */
  
  DROP TABLE IF EXISTS `tags`;
  
  CREATE TABLE `tags` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `name` varchar(64) COLLATE utf8_bin NOT NULL,
    `slug` varchar(64) COLLATE utf8_bin NOT NULL,
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `tags` */
  
  insert  into `tags`(`id`,`create_time`,`deleted`,`update_time`,`name`,`slug`) values (1,'2019-11-21 15:08:57',0,'2019-11-21 15:08:57','Java','java'),(2,'2019-11-21 15:13:59',0,'2019-11-21 15:13:59','SaaS','saas'),(3,'2019-11-21 22:44:18',0,'2019-11-21 22:44:18','Spring Boot','springboot'),(4,'2019-11-21 22:44:32',0,'2019-11-21 22:44:32','Intercept','intercept'),(5,'2019-11-21 22:50:56',0,'2019-11-21 22:50:56','Spring Data JPA','jpa'),(6,'2019-11-21 22:56:15',0,'2019-11-21 22:56:15','Filter','filter'),(7,'2019-11-21 22:59:15',0,'2019-11-21 22:59:15','RestTemplate','rest'),(8,'2019-11-21 23:04:45',0,'2019-11-21 23:04:45','日志','logging'),(9,'2019-11-21 23:06:58',0,'2019-11-21 23:06:58','Restful','restful'),(10,'2019-11-21 23:09:07',0,'2019-11-21 23:09:07','Exception','exception'),(11,'2019-11-21 23:14:53',0,'2019-11-21 23:14:53','Tomcat','tomcat'),(12,'2019-11-21 23:24:31',0,'2019-11-21 23:24:31','Hibernate','hibernate'),(13,'2019-11-21 23:24:39',0,'2019-11-21 23:24:39','Mybatis','mybatis'),(14,'2019-11-21 23:26:33',0,'2019-11-21 23:26:33','Stream','stream'),(15,'2019-11-21 23:29:48',0,'2019-11-21 23:29:48','设计模式','design-pattern');
  
  /*Table structure for table `themes` */
  
  DROP TABLE IF EXISTS `themes`;
  
  CREATE TABLE `themes` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `name` varchar(64) COLLATE utf8_bin NOT NULL,
    `status` int(11) DEFAULT NULL,
    `thumb` varchar(1024) COLLATE utf8_bin NOT NULL,
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `themes` */
  
  insert  into `themes`(`id`,`create_time`,`deleted`,`update_time`,`name`,`status`,`thumb`) values (1,'2019-11-19 07:05:47',0,'2019-11-19 07:09:11','ramostear',1,'themes\\ramostear\\thumb.png');
  
  /*Table structure for table `user` */
  
  DROP TABLE IF EXISTS `user`;
  
  CREATE TABLE `user` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `deleted` tinyint(4) DEFAULT '0',
    `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `password` varchar(255) COLLATE utf8_bin NOT NULL,
    `role` varchar(64) COLLATE utf8_bin NOT NULL,
    `username` varchar(64) COLLATE utf8_bin NOT NULL,
    PRIMARY KEY (`id`)
  ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
  
  /*Data for the table `user` */
  
  insert  into `user`(`id`,`create_time`,`deleted`,`update_time`,`password`,`role`,`username`) values (2,'2019-11-17 03:56:16',0,'2019-11-17 03:56:16','fdda31b5bac946779b53faad86fbf4a5','administrator','administrator'),(3,'2019-11-17 04:31:21',0,'2019-11-17 04:31:21','5547d0d3964efedf3da1176e0d2107e0','administrator','Administrator');
  
  /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
  /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
  /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
  /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
  