1-SpringBoot2.0入门

1.1-SpringBoot2.x依赖环境和版本新特性

简介:讲解新版本依赖环境和springboot2新特性概述

1.2-快速创建SpringBoot2.x应用之手工创建web应用

简介:使用Maven手工创建SpringBoot2.x应用

1.3-快速创建SpringBoot2.x应用之工具类自动创建web应用

简介:使用构建工具自动生成项目基本架构 工具自动创建:http://start.spring.io/

1.4-SpringBoot2.x的依赖默认Maven版本

简介:讲解SpringBoot2.x的默认Maven依赖版本

2-SpringBoot接口Http协议开发实战

2.1-SpringBoot2.xHTTP请求配置讲解

简介:SpringBoot2.xHTTP请求注解讲解和简化注解配置技巧

  • @RestController and @RequestMapping是springMVC的注解,不是springboot特有的
  • @RestController = @Controller+@ResponseBody
  • @SpringBootApplication = @Configuration+@EnableAutoConfiguration+@ComponentScanlocalhost:8080

2.2-开发必备工具PostMan接口工具介绍和使用

用户在开发或者调试网络程序或者是网页B/S模式的程序的时候是需要一些方法来跟踪网页请求的,用户可以使用一些网络的监视工具比如著名的Firebug等网页调试工具。今天给大家介绍的这款网页调试工具不仅可以调试简单的css、html、脚本等简单的网页基本信息,它还可以发送几乎所有类型的HTTP请求!Postman在发送网络HTTP请求方面可以说是Chrome插件类产品中的代表产品之一。

简介:模拟Http接口测试工具PostMan安装和讲解

2.3-HTTP接口GET请求实战

简介:讲解springboot接口,http的get请求,各个注解使用

  • GET请求

    • 1、单一参数@RequestMapping(path = “/{id}”, method = RequestMethod.GET)
public String getUser(@PathVariable String id ) {} 
@RequestMapping(path = "/{depid}/{userid}", method = RequestMethod.GET) 
//可以同时指定多个提交方法
public String getUser(@PathVariable("depid")String departmentID,@PathVariable("userid") String userid)
@RequestMapping(path = "/{city_id}/{user_id}", method = RequestMethod.GET)
public Object findUser(@PathVariable("city_id") String cityId,
@PathVariable("user_id") String userId) {
params.clear();
params.put("cityId", cityId);
params.put("userId", userId);

return params;
}
  • Restful协议的GET请求

    • @GetMapping注解

      @GetMapping(value = "/v1/page_user1")
      public Object pageUser(int from, int size) {
      params.clear();
      params.put("from", from);
      params.put("size", size);
      return params;
      }
    • 参数设置默认值

      @GetMapping(value = "/v2/page_user2")
      public Object pageUser2(@RequestParam(defaultValue = "0", name = "page") int from, int size) {
      params.clear();
      params.put("from", from);
      params.put("size", size);
      return params;
      }
    • 获取http的头部

      @GetMapping("/v1/get_header")
      public Object getHeader(@RequestHeader("access_token") String accessToken, String id) {
      params.clear();
      params.put("access_token", accessToken);
      params.put("id", id);
      return params;
      }
    • 使用HttpServletRequest

      @GetMapping("/v1/test_request")
      public Object testRequest(HttpServletRequest request) {
      params.clear();
      String id = request.getParameter("id");
      params.put("id", id);
      return params;
      }

2.4-HTTP其他提交方法请求实战

简介:讲解http请求post,put, delete提交方式

2.4.1-Post请求

@PostMapping("/v1/login")
public Object login(String id, String pwd) {
params.clear();
params.put("id", id);
params.put("pwd", pwd);
return params;
}

2.4.2-Put请求

@PutMapping("/v1/put")
public Object put(String id) {
params.clear();
params.put("id", id);
return params;
}

2.4.3-Delete请求

@DeleteMapping("/v1/del")
public Object del(String id) {
params.clear();
params.put("id", id);
return params;
}

2.5-Jackson使用

简介:介绍常用json框架和注解的使用,自定义返回json结构和格式

  • 常用框架 阿里 fastjson,谷歌gson等
  • JavaBean序列化为Json,
    • 性能:Jackson > FastJson > Gson > Json-lib 同个结构
    • Jackson、FastJson、Gson类库各有优点,各有自己的专长
    • 空间换时间,时间换空间
  • jackson处理相关自动
    • 指定字段不返回:@JsonIgnore
    • 指定日期格式:@JsonFormat(pattern=”yyyy-MM-dd hh:mm:ss”,locale=”zh”,timezone=”GMT+8”)
    • 空字段不返回:@JsonInclude(Include.NON_NUll)
    • 指定别名:@JsonProperty

2.6-SpringBoot目录文件结构

简介:讲解SpringBoot目录文件结构和官方推荐的目录规范

  • 目录讲解

    • src/main/java:存放代码
    • src/main/resources
    • static: 存放静态文件,比如 css、js、image, (访问方式 http://localhost:8080/js/main.js)
    • templates:存放静态页面jsp,html,tpl
    • config:存放配置文件,application.properties
    • resources:
  • 引入依赖 Thymeleaf

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    //注意:如果不引人这个依赖包,html文件应该放在默认加载文件夹里面,
    //比如resources、static、public这个几个文件夹,才可以访问
  • 同个文件的加载顺序,静态资源文件 Spring Boot 默认会挨个从

    • META/resources >
    • resources >
    • static >
    • public

    里面找是否存在相应的资源,如果有则直接返回。

  • 默认配置

  • 静态资源文件存储在CDN

2.7-SpringBoot文件上传实战

简介:讲解HTML页面文件上传和后端处理实战

  • springboot文件上传 MultipartFile file,源自SpringMVC

    • 静态页面直接访问:localhost:8080/index.html
      • 注意点:如果想要直接访问html页面,则需要把html放在springboot默认加载的文件夹下面
    • MultipartFile 对象的transferTo方法,用于文件保存(效率和操作比原先用FileOutStream方便和高效)

    访问路径 http://localhost:8080/images/39020dbb-9253-41b9-8ff9-403309ff3f19.jpeg

  • 文件上传

2.8-jar包方式运行web项目文件上传和访问

简介:讲解SpingBoot2.x使用 java -jar运行方式的图片上传和访问处理

  • 文件大小配置,启动类里面配置

    @Bean  
    public MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    //单个文件最大
    factory.setMaxFileSize("10240KB"); //KB,MB
    // 设置总上传数据总大小
    factory.setMaxRequestSize("1024000KB");
    return factory.createMultipartConfig();
    }
  • 打包成jar包,需要增加maven依赖

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>


    如果没加相关依赖,执行maven打包,运行后会报错:no main manifest attribute, in XXX.jar
    GUI:反编译工具,作用就是用于把class文件转换成java文件
  • 文件上传和访问需要指定磁盘路径

    application.properties中增加下面配置
    1) web.images-path=/Users/jack/Desktop
    2) spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/test/,file:${web.upload-path}
  • 文件服务器:fastdfs,阿里云oss,nginx搭建一个简单的文件服务器等

2.9-构建RESTful API与单元测试

首先,回顾并详细说明一下在快速入门中使用的@Controller@RestController@RequestMapping注解。如果您对Spring MVC不熟悉并且还没有尝试过快速入门案例,建议先看一下快速入门的内容。

  • @Controller:修饰class,用来创建处理http请求的对象
  • @RestController:Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式
  • @RequestMapping:配置url映射。现在更多的也会直接用以Http Method直接关联的映射注解来定义,比如:GetMappingPostMappingDeleteMappingPutMapping

下面我们通过使用Spring MVC来实现一组对User对象操作的RESTful API,配合注释详细说明在Spring MVC中如何映射HTTP请求、如何传参、如何编写单元测试。

RESTful API具体设计如下:

![RESTful API](https://gitee.com/lemon-cs/images/raw/master/RESTful API.jpg)

@RestController
@RequestMapping(value = "/users") // 通过这里配置使下面的映射都在/users下
public class UserController {

// 创建线程安全的Map,模拟users信息的存储
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());

/**
* 处理"/users/"的GET请求,用来获取用户列表
*
* @return
*/
@GetMapping("/")
public List<User> getUserList() {
// 还可以通过@RequestParam从页面中传递参数来进行查询条件或者翻页信息的传递
List<User> r = new ArrayList<User>(users.values());
return r;
}

/**
* 处理"/users/"的POST请求,用来创建User
*
* @param user
* @return
*/
@PostMapping("/")
public String postUser(@RequestBody User user) {
// @RequestBody注解用来绑定通过http请求中application/json类型上传的数据
users.put(user.getId(), user);
return "success";
}

/**
* 处理"/users/{id}"的GET请求,用来获取url中id值的User信息
*
* @param id
* @return
*/
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// url中的id可通过@PathVariable绑定到函数的参数中
return users.get(id);
}

/**
* 处理"/users/{id}"的PUT请求,用来更新User信息
*
* @param id
* @param user
* @return
*/
@PutMapping("/{id}")
public String putUser(@PathVariable Long id, @RequestBody User user) {
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}

/**
* 处理"/users/{id}"的DELETE请求,用来删除User
*
* @param id
* @return
*/
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
users.remove(id);
return "success";
}

}

单元测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter21ApplicationTests {

private MockMvc mvc;

@Before
public void setUp() {
mvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
}

@Test
public void testUserController() throws Exception {
// 测试UserController
RequestBuilder request;

// 1、get查一下user列表,应该为空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));

// 2、post提交一个user
request = post("/users/")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"name\":\"测试大师\",\"age\":20}");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));

// 3、get获取user列表,应该有刚才插入的数据
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[{\"id\":1,\"name\":\"测试大师\",\"age\":20}]")));

// 4、put修改id为1的user
request = put("/users/1")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"测试终极大师\",\"age\":30}");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));

// 5、get一个id为1的user
request = get("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("{\"id\":1,\"name\":\"测试终极大师\",\"age\":30}")));

// 6、del删除id为1的user
request = delete("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));

// 7、get查一下user列表,应该为空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));

}

}

2.10-使用Swagger2构建强大的API文档

随着前后端分离架构和微服务架构的流行,我们使用Spring Boot来构建RESTful API项目的场景越来越多。通常我们的一个RESTful API就有可能要服务于多个不同的开发人员或开发团队:IOS开发、Android开发、Web开发甚至其他的后端服务等。为了减少与其他团队平时开发期间的频繁沟通成本,传统做法就是创建一份RESTful API文档来记录所有接口细节,然而这样的做法有以下几个问题:

  • 由于接口众多,并且细节复杂(需要考虑不同的HTTP请求类型、HTTP头部信息、HTTP请求内容等),高质量地创建这份文档本身就是件非常吃力的事,下游的抱怨声不绝于耳。
  • 随着时间推移,不断修改接口实现的时候都必须同步修改接口文档,而文档与代码又处于两个不同的媒介,除非有严格的管理机制,不然很容易导致不一致现象。

为了解决上面这样的问题,本文将介绍RESTful API的重磅好伙伴Swagger2,它可以轻松的整合到Spring Boot中,并与Spring MVC程序配合组织出强大RESTful API文档。它既可以减少我们创建文档的工作量,同时说明内容又整合入实现代码中,让维护文档和修改代码整合为一体,可以让我们在修改代码逻辑的同时方便的修改文档说明。另外Swagger2也提供了强大的页面测试功能来调试每个RESTful API。具体效果如下图所示:

Swagger

  1. 添加swagger-spring-boot-starter依赖

    pom.xml中加入依赖,具体如下:

    <dependency>
    <groupId>com.spring4all</groupId>
    <artifactId>swagger-spring-boot-starter</artifactId>
    <version>1.9.0.RELEASE</version>
    </dependency>
  2. 应用主类中添加@EnableSwagger2Doc注解,具体如下

    @EnableSwagger2Doc
    @SpringBootApplication
    public class Chapter22Application {

    public static void main(String[] args) {
    SpringApplication.run(Chapter22Application.class, args);
    }

    }
  3. application.properties中配置文档相关内容,比如

    swagger.title=spring-boot-starter-swagger
    swagger.description=Starter for swagger 2.x
    swagger.version=1.4.0.RELEASE
    swagger.license=Apache License, Version 2.0
    swagger.licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.html
    swagger.termsOfServiceUrl=https://github.com/dyc87112/spring-boot-starter-swagger
    swagger.contact.name=didi
    swagger.contact.url=http://blog.didispace.com
    swagger.contact.email=dyc87112@qq.com
    swagger.base-package=com.didispace
    swagger.base-path=/**

    各参数配置含义如下:

    • swagger.title:标题
    • swagger.description:描述
    • swagger.version:版本
    • swagger.license:许可证
    • swagger.licenseUrl:许可证URL
    • swagger.termsOfServiceUrl:服务条款URL
    • swagger.contact.name:维护人
    • swagger.contact.url:维护人URL
    • swagger.contact.email:维护人email
    • swagger.base-package:swagger扫描的基础包,默认:全扫描
    • swagger.base-path:需要处理的基础URL规则,默认:/**

    更多配置说明可见官方说明:https://github.com/SpringForAll/spring-boot-starter-swagger

  4. 启动应用,访问:http://localhost:8080/swagger-ui.html,就可以看到如下的接口文档页面:

    Swagger

  5. 添加文档内容

    在整合完Swagger之后,在http://localhost:8080/swagger-ui.html页面中可以看到,关于各个接口的描述还都是英文或遵循代码定义的名称产生的。这些内容对用户并不友好,所以我们需要自己增加一些说明来丰富文档内容。如下所示,我们通过@Api@ApiOperation注解来给API增加说明、通过@ApiImplicitParam@ApiModel@ApiModelProperty注解来给参数增加说明。

    @Api(tags = "用户管理")
    @RestController
    @RequestMapping(value = "/users") // 通过这里配置使下面的映射都在/users下
    public class UserController {

    // 创建线程安全的Map,模拟users信息的存储
    static Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());

    @GetMapping("/")
    @ApiOperation(value = "获取用户列表")
    public List<User> getUserList() {
    List<User> r = new ArrayList<>(users.values());
    return r;
    }

    @PostMapping("/")
    @ApiOperation(value = "创建用户", notes = "根据User对象创建用户")
    public String postUser(@RequestBody User user) {
    users.put(user.getId(), user);
    return "success";
    }

    @GetMapping("/{id}")
    @ApiOperation(value = "获取用户详细信息", notes = "根据url的id来获取用户详细信息")
    public User getUser(@PathVariable Long id) {
    return users.get(id);
    }

    @PutMapping("/{id}")
    @ApiImplicitParam(paramType = "path", dataType = "Long", name = "id", value = "用户编号", required = true, example = "1")
    @ApiOperation(value = "更新用户详细信息", notes = "根据url的id来指定更新对象,并根据传过来的user信息来更新用户详细信息")
    public String putUser(@PathVariable Long id, @RequestBody User user) {
    User u = users.get(id);
    u.setName(user.getName());
    u.setAge(user.getAge());
    users.put(id, u);
    return "success";
    }

    @DeleteMapping("/{id}")
    @ApiOperation(value = "删除用户", notes = "根据url的id来指定删除对象")
    public String deleteUser(@PathVariable Long id) {
    users.remove(id);
    return "success";
    }

    }

    @Data
    @ApiModel(description="用户实体")
    public class User {

    @ApiModelProperty("用户编号")
    private Long id;
    @ApiModelProperty("用户姓名")
    private String name;
    @ApiModelProperty("用户年龄")
    private Integer age;

    }

    完成上述代码添加后,启动Spring Boot程序,访问:http://localhost:8080/swagger-ui.html,就能看到下面这样带中文说明的文档了(其中标出了各个注解与文档元素的对应关系以供参考):

    Swagger用户管理

    Swagger更新用户

  6. API文档访问与调试
    在上图请求的页面中,我们看到user的Value是个输入框?是的,Swagger除了查看接口功能外,还提供了调试测试功能,我们可以点击上图中右侧的Model Schema(黄色区域:它指明了User的数据结构),此时Value中就有了user对象的模板,我们只需要稍适修改,点击下方“Try it out!”按钮,即可完成了一次请求调用!

    此时,你也可以通过几个GET请求来验证之前的POST请求是否正确。

    相比为这些接口编写文档的工作,我们增加的配置内容是非常少而且精简的,对于原有代码的侵入也在忍受范围之内。因此,在构建RESTful API的同时,加入Swagger来对API文档进行管理,是个不错的选择。

2.11-JSR-303实现请求参数校验

请求参数的校验是很多新手开发非常容易犯错,或存在较多改进点的常见场景。比较常见的问题主要表现在以下几个方面:

  • 仅依靠前端框架解决参数校验,缺失服务端的校验。这种情况常见于需要同时开发前后端的时候,虽然程序的正常使用不会有问题,但是开发者忽略了非正常操作。比如绕过前端程序,直接模拟客户端请求,这时候就会突然在前端预设的各种限制,直击各种数据访问接口,使得我们的系统存在安全隐患。
  • 大量地使用if/else语句嵌套实现,校验逻辑晦涩难通,不利于长期维护。

所以,针对上面的问题,建议服务端开发在实现接口的时候,对于请求参数必须要有服务端校验以保障数据安全与稳定的系统运行。同时,对于参数的校验实现需要足够优雅,要满足逻辑易读、易维护的基本特点。

接下来,我们就在本篇教程中详细说说,如何优雅地实现Spring Boot服务端的请求参数校验。

JSR-303

在开始动手实践之前,我们先了解一下接下来我们将使用的一项标准规范:JSR-303

什么是JSR?

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

JSR-303定义的是什么标准?

JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

Bean Validation中内置的constraint

Bean Validation

Hibernate Validator附加的constraint

快速入门

我们先来做一个简单的例子,比如:定义字段不能为Null。只需要两步

第一步:在要校验的字段上添加上@NotNull注解,具体如下:

@Data
@ApiModel(description="用户实体")
public class User {

@ApiModelProperty("用户编号")
private Long id;

@NotNull
@ApiModelProperty("用户姓名")
private String name;

@NotNull
@ApiModelProperty("用户年龄")
private Integer age;

}

第二步:在需要校验的参数实体前添加@Valid注解,具体如下:

@PostMapping("/")
@ApiOperation(value = "创建用户", notes = "根据User对象创建用户")
public String postUser(@Valid @RequestBody User user) {
users.put(user.getId(), user);
return "success";
}

完成上面配置之后,启动应用,并用POST请求访问localhost:8080/users/接口,body使用一个空对象,{}。你可以用Postman等测试工具发起,也可以使用curl发起,比如这样:

curl -X POST \
http://localhost:8080/users/ \
-H 'Content-Type: application/json' \
-H 'Postman-Token: 72745d04-caa5-44a1-be84-ba9c115f4dfb' \
-H 'cache-control: no-cache' \
-d '{

}'

不出意外,你可以得到如下结果:

{
"timestamp": "2019-10-05T05:45:19.221+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.user.age",
"NotNull.age",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"user.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
}
],
"defaultMessage": "不能为null",
"objectName": "user",
"field": "age",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotNull.user.name",
"NotNull.name",
"NotNull.java.lang.String",
"NotNull"
],
"arguments": [
{
"codes": [
"user.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "不能为null",
"objectName": "user",
"field": "name",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"message": "Validation failed for object='user'. Error count: 2",
"path": "/users/"
}

其中返回内容的各参数含义如下:

  • timestamp:请求时间
  • status:HTTP返回的状态码,这里返回400,即:请求无效、错误的请求,通常参数校验不通过均为400
  • error:HTTP返回的错误描述,这里对应的就是400状态的错误描述:Bad Request
  • errors:具体错误原因,是一个数组类型;因为错误校验可能存在多个字段的错误,比如这里因为定义了两个参数不能为Null,所以存在两条错误记录信息
  • message:概要错误消息,返回内容中很容易可以知道,这里的错误原因是对user对象的校验失败,其中错误数量为2,而具体的错误信息就定义在上面的errors数组中
  • path:请求路径

请求的调用端在拿到这个规范化的错误信息之后,就可以方便的解析并作出对应的措施以完成自己的业务逻辑了。

尝试一些其他校验

在完成了上面的例子之后,我们还可以增加一些校验规则,比如:校验字符串的长度、校验数字的大小、校验字符串格式是否为邮箱等。下面我们就来定义一些复杂的校验定义,比如:

@Data
@ApiModel(description="用户实体")
public class User {

@ApiModelProperty("用户编号")
private Long id;

@NotNull
@Size(min = 2, max = 5)
@ApiModelProperty("用户姓名")
private String name;

@NotNull
@Max(100)
@Min(10)
@ApiModelProperty("用户年龄")
private Integer age;

@NotNull
@Email
@ApiModelProperty("用户邮箱")
private String email;

}

发起一个可以出发nameageemail都校验不通过的请求,比如下面这样:

curl -X POST \
http://localhost:8080/users/ \
-H 'Content-Type: application/json' \
-H 'Postman-Token: 114db0f0-bdce-4ba5-baf6-01e5104a68a3' \
-H 'cache-control: no-cache' \
-d '{
"name": "abcdefg",
"age": 8,
"email": "aaaa"
}'

我们将得到如下的错误返回:

{
"timestamp": "2019-10-05T06:24:30.518+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Size.user.name",
"Size.name",
"Size.java.lang.String",
"Size"
],
"arguments": [
{
"codes": [
"user.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
},
5,
2
],
"defaultMessage": "个数必须在2和5之间",
"objectName": "user",
"field": "name",
"rejectedValue": "abcdefg",
"bindingFailure": false,
"code": "Size"
},
{
"codes": [
"Min.user.age",
"Min.age",
"Min.java.lang.Integer",
"Min"
],
"arguments": [
{
"codes": [
"user.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
},
10
],
"defaultMessage": "最小不能小于10",
"objectName": "user",
"field": "age",
"rejectedValue": 8,
"bindingFailure": false,
"code": "Min"
},
{
"codes": [
"Email.user.email",
"Email.email",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"user.email",
"email"
],
"arguments": null,
"defaultMessage": "email",
"code": "email"
},
[],
{
"defaultMessage": ".*",
"codes": [
".*"
],
"arguments": null
}
],
"defaultMessage": "不是一个合法的电子邮件地址",
"objectName": "user",
"field": "email",
"rejectedValue": "aaaa",
"bindingFailure": false,
"code": "Email"
}
],
"message": "Validation failed for object='user'. Error count: 3",
"path": "/users/"
}

errors数组中的各个错误明细中,知道各个字段的defaultMessage,可以看到很清晰的错误描述。

3-SpringBoot热部署devtool和配置文件自动注入实战

3.1-使用Dev-tool热部署

简介:介绍什么是热部署,使用springboot结合dev-tool工具,快速加载启动应用

  • 什么是热部署?

    • 在应用运行的时升级软件,无需重新启动的方式有两种,热部署热加载

      对于Java应用程序来说,热部署就是在服务器运行时重新部署项目,热加载即在在运行时重新加载class,从而升级应用。

    • 热加载的实现原理主要依赖java的类加载机制,在实现方式可以概括为在容器启动的时候起一条后台线程,定时的检测类文件的时间戳变化,如果类的时间戳变掉了,则将类重新载入。

      对比反射机制,反射是在运行时获取类信息,通过动态的调用来改变程序行为; 热加载则是在运行时通过重新加载改变类信息,直接改变程序行为。

      热部署原理类似,但它是直接重新加载整个应用,这种方式会释放内存,比热加载更加干净彻底,但同时也更费时间。

  • 添加依赖

    • ```xmlorg.springframework.bootspring-boot-devtoolstrue

      - 添加配置

      - ```properties
      #热部署
      spring.devtools.restart.enabled=true
      spring.devtools.restart.additional-paths=src/main/java
      #关闭缓存,及时刷新
      #spring.thymeleaf.cache=false
      #排除无需热部署目录
      #spring.devtools.restart.exclude=static/**,public/**
      #srping.devtools.restart.exclude=WEB-INF/**
  • IDEA配置

3.2-SpringBoot配置文件

简介:SpringBoot2.x常见的配置文件 xml、yml、properties的区别和使用

  • xml、properties、json、yaml
  • 常见的配置文件 xx.yml, xx.properties,
    • 1)YAML(Yet Another Markup Language) 写 YAML 要比写 XML 快得多(无需关注标签或引号) 使用空格 Space 缩进表示分层,不同层次之间的缩进可以使用不同的空格数目 注意:key后面的冒号,后面一定要跟一个空格,树状结构 application.properties示例 server.port=8090
      server.session-timeout=30
      server.tomcat.max-threads=0
      server.tomcat.uri-encoding=UTF-8
  • application.yml示例 server:
    port: 8090
    session-timeout: 30
    tomcat.max-threads: 0
    tomcat.uri-encoding: UTF-8
  • 默认示例文件仅作为指导。 不要将整个内容复制并粘贴到您的应用程序中,只挑选您需要的属性。
  • 参考:https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/#common-application-properties
  • 如果需要修改,直接复制对应的配置文件,加到application.properties里面

3.3-SpringBoot注解配置文件自动映射到属性和实体类

简介:讲解使用@value注解配置文件自动映射到属性和实体类

  • 1、配置文件加载

    • 方式一

      • 1、Controller上面配置

        @PropertySource({"classpath:resource.properties"})

      • 2、增加属性

        @Value("${test.name}") 
        private String name;
    • 方式二:实体类配置文件

      • 1、添加 @Component 注解;

      • 2、使用 @PropertySource 注解指定配置文件位置;

      • 3、使用 @ConfigurationProperties 注解,设置相关属性;

      • 4、必须 通过注入IOC对象Resource 进来 , 才能在类中使用获取的配置文件值。

        @Autowired private ServerSettings serverSettings;

  • 例子

    1.配置类

    @Setter
    @Getter
    @Component
    @PropertySource({"classpath:application.properties"})
    @ConfigurationProperties
    public class ServerSettings {
    //应用名称
    @Value("${test.name}")
    private String name;
    //域名地址
    @Value("${test.domain}")
    private String domain;
    }

    2.配置文件application.properties

    #配置文件注入
    test.name=springboot
    test.domain=www.fangpeng.com

    3.注入测试

    @RestController
    public class TestController {
    @Autowired
    private ServerSettings serverSettings;

    @GetMapping("/v1/test_properties")
    public Object testProperties() {
    return serverSettings;
    }
    }

    常见问题:
    1、配置文件注入失败,Could not resolve placeholder
    解决:根据springboot启动流程,会有自动扫描包没有扫描到相关注解,
    默认Spring框架实现会从声明@ComponentScan所在的类的package进行扫描,来自动注入,
    因此启动类最好放在根路径下面,或者指定扫描包范围
    spring-boot扫描启动类对应的目录和子目录
    2、注入bean的方式,属性名称和配置文件里面的key一一对应,就用加@Value 这个注解
    如果不一样,就要加@value("${XXX}")

  • 第二种方式

    @Configuration
    @ConfigurationProperties(prefix="test") //配置文件的属性前缀
    @PropertySource(value="classpath:resource.properties")
    public class ServerSettings {
    private String name;
    private String domain;
    }

4-Springboot单元测试进阶实战和自定义异常处理

4.1-SpringBootTest单元测试实战

简介:讲解SpringBoot的单元测试

1、引入相关依赖


<!--springboot程序测试依赖,如果是自动创建项目默认添加-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

2、编写单元测试用例

@SpringBootTest(classes = {ApiApplication.class}) //启动整个springboot工程
public class SpringBootTestDemo {

@Test //Test注解标记方法为测试方法,以便构建工具和IDE能够识别并执行它们
public void testOne() {
System.out.println("test hello 1");
assertEquals(1, 1);
}

/** 其他注解
* @BeforeAll——只执行一次,执行的时机是在所有测试和@BeforeEach注解方法之前
* @BeforeEach——在每个测试执行之前执行
* @AfterEach——在每个测试执行之后执行
* @AfterAll——只执行一次,执行时机是在所有测试和@AfterEach注解方法之后
**/

@BeforeAll
public void testBeforeAll() {
System.out.println("testBefore");
}

@AfterAll
public void testAfterAll() {
System.out.println("testAfter");
}

}

3、Junit5断言

  • assertAll:断言所有提供的可执行文件都不会抛出异常。若提供的标题(heading),其将包含在MultipleFailuresError的消息字符串中。
  • assertArrayEquals:断言期望的和实际的XX类型数组是相等的。若失败,将显示提供的失败消息。
  • assertDoesNotThrow:虽然从测试方法抛出的任何异常都会导致测试失败,但在某些用例中,显式断言测试方法中的给定代码块不会抛出异常会很有用。若提供的标题(heading),其将包含在MultipleFailuresError的消息字符串中。
  • assertEquals:断言预期和实际是相等的。如有必要,将从提供的messageSupplier中懒惰地检索失败消息。
  • assertFalse:断言提供的条件不是真。失败并显示提供的失败消息。
  • assertIterableEquals:断言预期和实际的迭代是完全相同的。类似于检查assertArrayEquals(Object [],Object [],String)中的完全相等,如果遇到两个迭代(包括期望和实际),则它们的迭代器必须以相同的顺序返回相等的元素。注意:这意味着迭代器不需要是同一类型。
  • assertNotNull:断言提供的条件不为null。
  • assertNotSame:断言预期和实际不会引用同一个对象。
  • assertNull:断言提供的实际为null。
  • assertSame:断言预期和实际引用同一个对象。
  • assertThrows:断言所提供的可执行代码块的执行会引发expectedType的异常并返回异常。如果没有抛出异常,或者抛出了不同类型的异常,则此方法将失败。如果不想对异常实例执行其他检查,只需忽略返回值。
  • assertTimeout:断言在超出给定超时之前,所提供的可执行代码块的执行完成。注意:可执行代码块将在与调用代码相同的线程中执行。因此,如果超过超时,则不会抢先中止执行可执行代码块。
  • assertTimeoutPreemptively:断言在超出给定超时之前,所提供的可执行代码块的执行完成。注意:可执行代码块将在与调用代码不同的线程中执行。此外,如果超过超时,则可抢占地执行可执行代码块。
  • assertTrue:断言提供的条件为true。
  • fail:使用给定的失败消息以及根本原因进行测试失败。泛型返回类型V允许此方法直接用作单语句lambda表达式,从而避免需要实现具有显式返回值的代码块。 由于此方法在其return语句之前抛出AssertionFailedError,因此该方法实际上永远不会向其调用者返回值。

4.2-SpringBoot测试之MockMvc讲解

简介: 讲解MockMvc类的使用和模拟Http请求实战

  • 增加类注解 @AutoConfigureMockMvc@SpringBootTest
  • 相关API
    • perform:执行一个RequestBuilder请求
    • andExpect:添加ResultMatcher->MockMvcResultMatchers验证规则 andReturn:最后返回相应的MvcResult->Response
    • andReturn:最后返回相应的MvcResult->Response

4.2.1-什么是Mock?

在面向对象的程序设计中,模拟对象(英语:mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。

4.2.2-为什么使用Mock对象?

使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。

在以下情况可以采用模拟对象来替代真实对象:

  • 真实对象的行为是不确定的(例如,当前的时间或温度);
  • 真实对象很难搭建起来;
  • 真实对象的行为很难触发(例如,网络错误);
  • 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
  • 真实的对象是用户界面,或包括用户界面在内;
  • 真实的对象使用了回调机制;
  • 真实对象可能还不存在;
  • 真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。

注:使用Mockito一般分三个步骤:

  1. 模拟测试类所需的外部依赖;
  2. 执行测试代码;
  3. 判断执行结果是否达到预期;

4.2.3-MockMvc

MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。

接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。

主要有两个实现:StandaloneMockMvcBuilderDefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。

4.2.4-在SpringBoot中使用

  1. 引入依赖(jar包),创建SpringBoot项目中默认引入的spring-boot-starter-test间接引入了spring-test,因此无需再额外引入jar包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
  1. 创建Controller类并编写相关方法
public class TestController {

//测试MockMvc
@GetMapping("/test/mockmvc")
public String testMock() {
return "test_mockmvc";
}

}
  1. 编写测试类。实例化MockMvc有两种形式,一种是使用StandaloneMockMvcBuilder,另外一种是使用DefaultMockMvcBuilder。

    • 测试类及初始化MockMvc初始化:

      @SpringBootTest
      @AutoConfigureMockMvc
      public class HelloWorldTest {

      @Autowired
      private MockMvc mockMvc;

      @Autowired
      private WebApplicationContext webApplicationContext;

      @Before
      public void setup() {
      // 实例化方式一
      mockMvc = MockMvcBuilders.standaloneSetup(new HelloWorldController()).build();
      // 实例化方式二
      // mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
      }
    • 单元测试方法

      @SpringBootTest(classes = {ApiApplication.class})
      @AutoConfigureMockMvc
      public class MockMvcTestDemo {

      @Autowired
      private MockMvc mockMvc;

      @Test
      public void apiTest() throws Exception {
      MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/test/mockmvc"))
      .andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
      int status = mvcResult.getResponse().getStatus();
      System.out.println(status);
      }

      @Test
      public void testHello() throws Exception {

      /*
      * 1、mockMvc.perform执行一个请求。
      * 2、MockMvcRequestBuilders.get("XXX")构造一个请求。
      * 3、ResultActions.param添加请求传值
      * 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
      * 5、ResultActions.andExpect添加执行完成后的断言。
      * 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
      * 比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
      * 7、ResultActions.andReturn表示执行完成后返回相应的结果。
      */
      mockMvc.perform(MockMvcRequestBuilders
      .get("/hello")
      // 设置返回值类型为utf-8,否则默认为ISO-8859-1
      .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
      .param("name", "Tom"))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.content().string("Hello Tom!"))
      .andDo(MockMvcResultHandlers.print());
      }

      }

4.3-SpringBoot个性化启动banner设置和debug日志

简介:自定义应用启动的趣味性日志图标和查看调试日志

1、启动获取更多信息 java -jar xxx.jar --debug

2、修改启动的banner信息
1)在类路径下增加一个banner.txt,里面是启动要输出的信息
2)在applicatoin.properties增加banner文件的路径地址
spring.banner.location=banner.txt

3)官网地址 https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/#boot-features-banners
  • 输出denbug日志

    1. 修改application.properties配置文件

      logging.level.root=debug
      或者 指定路径
      logging.level.com.XXX.XXX.mapper=debug
    2. 修改logback-spring.xml配置文件

      增加<logger name="com.XXX.XXX.mapper" level="DEBUG"></logger>

      <?xml version="1.0" encoding="UTF-8"?>
      <configuration debug="false">
      <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
      <property name="LOG_HOME" value="/test/log" />
      <!-- 控制台输出 -->
      <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
      </encoder>
      </appender>
      <!-- 按照每天生成日志文件 -->
      <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!--日志文件输出的文件名-->
      <FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
      <!--日志文件保留天数-->
      <MaxHistory>30</MaxHistory>
      </rollingPolicy>
      <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
      </encoder>
      <!--日志文件最大的大小-->
      <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <MaxFileSize>10MB</MaxFileSize>
      </triggeringPolicy>
      </appender>
      <!-- show parameters for hibernate sql 专为 Hibernate 定制 -->
      <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" />
      <logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="DEBUG" />
      <logger name="org.hibernate.SQL" level="DEBUG" />
      <logger name="org.hibernate.engine.QueryParameters" level="DEBUG" />
      <logger name="org.hibernate.engine.query.HQLQueryPlan" level="DEBUG" />

      <!--myibatis log configure-->
      <logger name="com.apache.ibatis" level="TRACE"/>
      <logger name="java.sql.Connection" level="DEBUG"/>
      <logger name="java.sql.Statement" level="DEBUG"/>
      <logger name="java.sql.PreparedStatement" level="DEBUG"/>

      <!-- 日志输出级别 -->
      <root level="INFO">
      <appender-ref ref="STDOUT" />
      <appender-ref ref="FILE" />
      </root>
      </configuration>

4.4-SpringBoot配置全局异常

4.4.1-模拟全局异常

  1. 编写异常代码段

    @RequestMapping(value = "/api/v1/test_ext")
    public Object index() {
    int i = 1/0;
    return new User("11", "123456", "1", 18, new Date());
    }
  2. 返回结果如下

4.4.2-异常注解介绍

@ControllerAdvice 顾名思义,这是一个增强的 Controller。需要配合@ExceptionHandler使用,
当将异常抛到controller时,可以对异常进行统一处理,规定返回的json格式或是跳转到一个错误页面。

使用这个 Controller ,可以实现三个方面的功能:

  1. 全局异常处理
  2. 全局数据绑定
  3. 全局数据预处理

如果是返回json数据 则用 RestControllerAdvice,就可以不加 @ResponseBody

//捕获全局异常,处理所有不可知的异常
@ExceptionHandler(value=Exception.class)注解用来指明异常的处理类型

4.4.3-处理全局异常

@RestControllerAdvice
public class CustomExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionHandler.class);

//捕获全局异常,处理所有不可知的异常
@ExceptionHandler(value = Exception.class)
public Object handleException(Exception e, HttpServletRequest request) {
LOGGER.error("url {}, msg {}",request.getRequestURL(), e.getMessage());
Map<String, Object> map = new HashMap<>();
map.put("code", 100);
map.put("msg", e.getMessage());
map.put("url", request.getRequestURL());
return map;
}
}

返回结果如下:

4.4.4-处理自定义异常

  1. 自定义异常类

    //自定义异常类
    @Data
    public class MyException extends RuntimeException {
    private String code;
    private String msg;

    public MyException(String code, String msg){
    super(msg);
    this.code = code;
    this.msg = msg;
    }

    public MyException(String msg){
    super(msg);
    this.msg = msg;
    }
    }
  2. 处理自定义异常

    //处理自定义异常
    @ExceptionHandler(value = MyException.class)
    public Object handleMyException(MyException e, HttpServletRequest request) {
    Map<String, Object> map = new HashMap<>();
    map.put("code", 100);
    map.put("msg", e.getMsg());
    map.put("url", request.getRequestURL());
    return map;
    }
  3. 返回自定义页面

    //处理自定义异常
    @ExceptionHandler(MyException.class)
    public Object handleMyExceptio(Exception e, HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.setViewName("error.html");
    modelAndView.addObject("msg", e.getMessage());
    return modelAndView;
    }

5-SpringBoot部署war项目到tomcat9和启动原理

5.1-SpringBoot启动方式和部署war项目到tomcat9

  1. IDE启动

  2. jar包方式启动

    //maven插件:
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>

    如果没有加,则执行jar包 ,报错如下

    java -jar spring-boot-demo-0.0.1-SNAPSHOT.jar
    no main manifest attribute, in spring-boot-demo-0.0.1-SNAPSHOT.jar

    jar包项目结构

    example.jar
    |
    +-META-INF
    | +-MANIFEST.MF
    +-org
    | +-springframework
    | +-boot
    | +-loader
    | +-<spring boot loader classes>
    +-BOOT-INF
    +-classes
    | +-mycompany
    | +-project
    | +-YourClasses.class
    +-lib
    +-dependency1.jar
    +-dependency2.jar

    目录结构讲解
    https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/#executable-jar-jar-file-structure

  3. war包方式启动

    • 在pom.xml中将打包形式 jar 修改为warwar

      构建项目名称springboot_demo

    • tocmat下载和安装 https://tomcat.apache.org/download-90.cgi

    • 修改启动类

      public class DemoApplication extends SpringBootServletInitializer {

      @Override
      protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
      return application.sources(DemoApplication.class);
      }

      public static void main(String[] args) throws Exception {
      SpringApplication.run(DemoApplication.class, args);
      }

      }
    • 打包项目,启动tomcat

5.2-SpringBoot启动原理

6-SpringBoot拦截器和 Servlet3.0自定义Filter、Listener

6.1-SpringBoot过滤器和Servlet3.0配置过滤器

  1. SpringBoot启动默认加载的Filter
characterEncodingFilter
hiddenHttpMethodFilter
httpPutFormContentFilter
requestContextFilter
  1. Filter优先级

Ordered.HIGHEST_PRECEDENCE
Ordered.LOWEST_PRECEDENCE

低位值意味着更高的优先级 Higher values are interpreted as lower priority
自定义Filter,避免和默认的Filter优先级一样,不然会冲突

注册Filter的bean FilterRegistrationBean
同模块里面有相关默认Filter
web->servlet->filter

  1. 自定义Filter
    • 使用Servlet3.0的注解进行配置
    • 启动类里面增加 @ServletComponentScan,进行扫描
    • 新建一个Filter类,implements Filter,并实现对应的接口
    • @WebFilter 标记一个类为filter,被spring进行扫描
      urlPatterns:拦截规则,支持正则
    • 控制chain.doFilter的方法的调用,来实现是否通过放行
      不放行,web应用resp.sendRedirect(“/index.html”);
      场景:权限控制、用户登录(非前端后端分离场景)等
@WebFilter(urlPatterns = "/api/*", filterName = "loginFilter")
public class LoginFilter implements Filter {

//容器加载的时候调用
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init loginFilter");
}

//请求被拦截的时候调用
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("doFilter loginFilter");

HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String username = request.getParameter("username");

if ("fangpeng".equals(username)) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
response.sendRedirect("/index.html");
return;
}
}

//容器被销毁的时候调用
@Override
public void destroy() {
System.out.println("destroy loginFilter");
}
}

6.2-Servlet3.0注解自定义原生Servlet

@WebServlet(name = "userServlet",urlPatterns = "/test/customs")
public class UserServlet extends HttpServlet{

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("custom sevlet");
resp.getWriter().flush();
resp.getWriter().close();
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}

6.3-自定义监听器Listener

  • 常用的监听器

    servletContextListenerhttpSessionListenerservletRequestListener

  • Servlet注解自定义监听器

    @WebListener
    public class RequestListener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    // TODO Auto-generated method stub
    System.out.println("======requestDestroyed========");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
    System.out.println("======requestInitialized========");
    }
    @WebListener
    public class CustomContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
    System.out.println("======contextInitialized======");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
    System.out.println("contextDestroyed");
    }
    }

6.4-自定义拦截器Interceptor

  1. @Configuration

    • 继承WebMvcConfigurationAdapter(SpringBoot2.X之前旧版本)
    • SpringBoot2.X 新版本配置拦截器 implements WebMvcConfigurer
  2. 自定义拦截器 HandlerInterceptor

    • preHandle:调用Controller某个方法之前
    • postHandle:Controller之后调用,视图渲染之前,如果控制器Controller出现了异常,则不会执行此方法
    • afterCompletion:不管有没有异常,这个afterCompletion都会被调用,用于资源清理
  3. 按照注册顺序进行拦截,先注册,先被拦截

  4. 拦截器不生效常见问题:

    • 是否有加@Configuration
    • 拦截路径是否有问题 ***
    • 拦截器最后路径一定要 “/**”, 如果是目录的话则是 ”/ */“
  5. Filter

    是基于函数回调 doFilter(),而Interceptor则是基于AOP思想
    Filter在只在Servlet前后起作用,而Interceptor够深入到方法前后、异常抛出前后等

    依赖于Servlet容器即web应用中,而Interceptor不依赖于Servlet容器所以可以运行在多种环境。

    在接口调用的生命周期里,Interceptor可以被多次调用,而Filter只能在容器初始化时调用一次。

    Filter和Interceptor的执行顺序:

    过滤前->拦截前->action执行->拦截后->过滤后

public class LoginInterceptor implements HandlerInterceptor {

//进入controller方法之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("LoginInterceptor=====>preHandle");
return HandlerInterceptor.super.preHandle(request, response, handler);
}

//调用完controller之后,试图渲染之前
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("LoginInterceptor====>postHandle");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("LoginInterceptor====>afterCompletion");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
@Configuration
public class CustomWebMvcConfigure implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/api/*/**");
//.excludePathPatterns("/api/*/**") =====> 排除某路径不被拦截
//拦截全部 /*/*/**
WebMvcConfigurer.super.addInterceptors(registry);
}
}

7-数据库操作之整合Mybatis和事务

7.1-SpringBoot持久化数据方式

  1. 原始java访问数据库JDBC(开发流程会很麻烦)

    • 注册驱动/加载驱动——Class.forName("com.mysql.jdbc.Driver")

    • 建立连接

      Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/dbname","root","root");

    • 创建Statement

    • 执行SQL语句

    • 处理结果集

    • 关闭连接,释放资源

  2. apache dbutils框架

    比JDBC简单点
    官网:https://commons.apache.org/proper/commons-dbutils/

  3. jpa框架

    spring-data-jpa
    jpa在复杂查询的时候性能不是很好

  4. Hiberante

    解释:ORM:对象关系映射Object Relational Mapping
    企业大都喜欢使用hibernate

  5. Mybatis框架

    互联网行业通常使用mybatis
    不提供对象和关系模型的直接映射,半ORM

7.2-SpringBoot2.x整合Mybatis3.x注解实战

  1. 使用starter, maven仓库地址:http://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter

  2. 加入依赖(可以用 http://start.spring.io/ 下载)

    <!-- 引入starter-->
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
    <scope>runtime</scope>
    </dependency>

    <!-- MySQL的JDBC驱动包 -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>
    <!-- 引入第三方数据源 -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.6</version>
    </dependency>
  3. 加入配置文件

    #mybatis.type-aliases-package=com.fangpeng.base_project.domain
    #可以自动识别
    #spring.datasource.driver-class-name =com.mysql.jdbc.Driver

    spring.datasource.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8
    spring.datasource.username=root
    spring.datasource.password=password
    #如果不使用默认的数据源 (com.zaxxer.hikari.HikariDataSource)
    spring.datasource.type =com.alibaba.druid.pool.DruidDataSource

    加载配置,注入到sqlSessionFactory等都是springBoot帮我们完成

  4. 启动类增加mapper扫描

    @MapperScan("com.fangpeng.base_project.mapper")

    技巧:保存对象,获取数据库自增id
    @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
  5. 开发mapper
    参考语法 http://www.mybatis.org/mybatis-3/zh/java-api.html

    public interface UserMapper {

    //推荐使用#{}取值,不要用${}取值,因为存在SQL注入风险
    @Insert("INSERT INTO user(name,phone,create_time,age) VALUES(#{name}, #{phone}, #{createTime}, #{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    int insert(User user);

    }
  6. 开发service

    @Service
    public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public int add(User user) {
    userMapper.insert(user);
    int id = user.getId();
    return id;
    }
    }
  7. 开发controller

    @RestController
    @RequestMapping("/api/v1/user")
    public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private UserMapper userMapper;

    @GetMapping("add")
    public Object add() {
    User user = new User();
    user.setAge(24);
    user.setCreateTime(new Date());
    user.setName("kobe");
    user.setPhone("10086");
    int id = userService.add(user);

    return JsonData.buildSuccess(id);
    }

    }
  8. sql脚本

    CREATE TABLE `user` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(128) DEFAULT NULL COMMENT '名称',
    `phone` varchar(16) DEFAULT NULL COMMENT '用户手机号',
    `create_time` datetime DEFAULT NULL COMMENT '创建时间',
    `age` int(4) DEFAULT NULL COMMENT '年龄',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

    相关资料:

    http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/#Configuration
    

    https://github.com/mybatis/spring-boot-starter/tree/master/mybatis-spring-boot-samples

    整合问题集合:

    https://my.oschina.net/hxflar1314520/blog/1800035
    https://blog.csdn.net/tingxuetage/article/details/80179772

7.3-SpringBoot整合Mybatis实操和打印SQL语句

  1. 控制台打印sql语句

    #增加打印sql语句,一般用于本地开发测试
       `mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl`
    
  2. 增加mapper代码

    @Select("SELECT * FROM user")
    @Results({
    @Result(column = "create_time",property = "createTime")
    //javaType = java.util.Date.class
    })
    List<User> getAll();

    @Select("SELECT * FROM user WHERE id = #{id}")
    @Results({
    @Result(column = "create_time",property = "createTime")
    })
    User findById(Long id);

    @Update("UPDATE user SET name=#{name} WHERE id =#{id}")
    void update(User user);

    @Delete("DELETE FROM user WHERE id =#{userId}")
    void delete(Long userId);
  3. 增加API

    @GetMapping("find_all")
    public Object findAll(){
    return JsonData.buildSuccess(userMapper.getAll());
    }

    @GetMapping("find_by_Id")
    public Object findById(long id){
    return JsonData.buildSuccess(userMapper.findById(id));
    }

    @GetMapping("del_by_id")
    public Object delById(long id){
    userMapper.delete(id);
    return JsonData.buildSuccess();
    }

    @GetMapping("update")
    public Object update(String name,int id){
    User user = new User();
    user.setName(name);
    user.setId(id);
    userMapper.update(user);
    return JsonData.buildSuccess();
    }

7.4-事务介绍和常见的隔离级别,传播行为

  1. 什么是事务?

    指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
    简单的说,事务就是并发控制的单位,是用户定义的一个操作序列。
    而一个逻辑工作单元要成为事务,就必须满足ACID属性。
    A:原子性(Atomicity)

    事务中的操作要么都不做,要么就全做。
    

    C:一致性(Consistency)

    事务执行的结果必须是从数据库从一个一致性状态转换到另一个一致性状态。
    

    I:隔离性(Isolation)

    一个事务的执行不能被其他事务干扰
    

    D:持久性(Durability)

    一个事务一旦提交,它对数据库中数据的改变就应该是永久性的
    
  2. 事务的隔离级别

    • 读未提交(Read Uncommitted):保证了读取过程中不会读取到非法数据

      ​ 引发脏读(读取了未提交的数据)

    • 读已提交(Read Committed

              这是大多数数据库系统默认的隔离级别,但不是MySQL默认的
              只能看见已经提交事务所做的改变
              引发不可重复读,不可重读读意味着我们同一事务执行完全相同的select语句时可能看到不一样的结果。
              ——>导致这种情况的原因可能有:(1)有一个交叉的事务有新的commit,导致了数据的改变;(2)一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit
                      多个commit提交时,只读一次出现结果不一致
      
    • 可重复读(Repeatable Read):保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据

      ​ 这是MySQL的默认事务隔离级别
      ​ 它确保同一事务的多个实例在并发读取数据时,看到同样的数据行
      ​ 此级别可能出现的问题–幻读(Phantom Read),当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
      ​ InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题

    • 可串行化(Serializable):最严格,串行处理,消耗资源大

      ​ 这是最高的隔离级别
      ​ 它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它在每个读的数据行上加上共享锁。
      ​ 可能导致大量的超时现象和锁竞争

  3. 常见的传播行为

    PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务,最常见的选择。

    PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。

    PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。

    PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起, 两个事务之间没有关系,一个异常,一个提交,不会同时回滚

    PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

    PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常

7.5-SpringBoot整合mybatis之事务处理实战

  • service逻辑引入事务
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int addAccount() {
User user = new User();
user.setAge(88);
user.setCreateTime(new Date());
user.setName("事务测试");
user.setPhone("000121212");

userMapper.insert(user);
int a = 1/0;

return user.getId();
}

8-SpringBoot整合Redis

8.1-分布式缓存Redis介绍

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

  1. redis官网 https://redis.io/download

  2. 新手入门redis在线测试工具:http://try.redis.io/

8.2-源码编译安装Redis4.x

  1. 快速安装 https://redis.io/download#installation

        wget http://download.redis.io/releases/redis-4.0.9.tar.gz
           tar xzf redis-4.0.9.tar.gz
           cd redis-4.0.9
           make
    

    启动服务端:src/redis-server
    启动客户端:src/redis-cli

  2. 默认是本地访问的,需要开放外网访问

    1)打开redis.conf文件在NETWORK部分修改
          注释掉bind 127.0.0.1可以使所有的ip访问redis
          修改 protected-mode,值改为no
    

8.3-SpringBoot整合redis实战

  1. 官网:https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/#boot-features-redis
    集群文档:https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#cluster

  2. springboot整合redis相关依赖引入

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  3. 相关配置文件配置

    #=========redis基础配置=========
    spring.redis.database=0
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    # 连接超时时间 单位 ms(毫秒)
    spring.redis.timeout=3000

    #=========redis线程池设置=========
    # 连接池中的最大空闲连接,默认值也是8。
    spring.redis.pool.max-idle=200

    #连接池中的最小空闲连接,默认值也是0。
    spring.redis.pool.min-idle=200

    # 如果赋值为-1,则表示不限制;pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
    spring.redis.pool.max-active=2000

    # 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时
    spring.redis.pool.max-wait=1000
  4. 常见redistemplate种类讲解和缓存实操(使用自动注入)

    • 注入模板

      @Autowired
      private StirngRedisTemplate strTplRedis
    • 类型String,List,Hash,Set,ZSet

      对应的方法分别是opsForValue()、opsForList()、opsForHash()、opsForSet()、opsForZSet()
      

8.5-Redis配置类和工具类

  1. RedisTemplate的自动配置(源代码如下)

    @Configuration(
    proxyBeanMethods = false
    )
    @ConditionalOnClass({RedisOperations.class})
    @EnableConfigurationProperties({RedisProperties.class})
    @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
    public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
    name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    RedisTemplate<Object, Object> template = new RedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
    }

    @Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
    }
    }

    通过源码可以看出,SpringBoot自动帮我们在容器中生成了一个RedisTemplate和一个StringRedisTemplate。但是,这个RedisTemplate的泛型是<Object,Object>,写代码不方便,需要写好多类型转换的代码;我们需要一个泛型为<String,Object>形式的RedisTemplate。并且,这个RedisTemplate没有设置数据存在Redis时,key及value的序列化方式。

        看到这个@ConditionalOnMissingBean注解后,就知道如果Spring容器中有了RedisTemplate对象了,这个自动配置的RedisTemplate不会实例化。因此我们可以直接自己写个配置类,配置RedisTemplate。
    
  2. 重新写一个Redis配置类

    //Redis配置类
    @Configuration
    public class RedisConfig {

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
    template.setConnectionFactory(factory);
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    // key采用String的序列化方式
    template.setKeySerializer(stringRedisSerializer);
    // hash的key也采用String的序列化方式
    template.setHashKeySerializer(stringRedisSerializer);
    // value序列化方式采用jackson
    template.setValueSerializer(jackson2JsonRedisSerializer);
    // hash的value序列化方式采用jackson
    template.setHashValueSerializer(jackson2JsonRedisSerializer);
    template.afterPropertiesSet();
    return template;
    }

    }
  3. 封装Redis工具类

    @Component
    public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    // =============================common============================

    /**
    * 指定缓存失效时间
    *
    * @param key 键
    * @param time 时间(秒)
    * @return
    */
    public boolean expire(String key, long time) {
    try {
    if (time > 0) {
    redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 根据key 获取过期时间
    *
    * @param key 键 不能为null
    * @return 时间(秒) 返回0代表为永久有效
    */
    public long getExpire(String key) {
    return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
    * 判断key是否存在
    *
    * @param key 键
    * @return true 存在 false不存在
    */
    public boolean hasKey(String key) {
    try {
    return redisTemplate.hasKey(key);
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 删除缓存
    *
    * @param key 可以传一个值 或多个
    */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
    if (key != null && key.length > 0) {
    if (key.length == 1) {
    redisTemplate.delete(key[0]);
    } else {
    redisTemplate.delete(CollectionUtils.arrayToList(key));
    }
    }
    }

    // ============================String=============================

    /**
    * 普通缓存获取
    *
    * @param key 键
    * @return
    */
    public Object get(String key) {
    return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
    * 普通缓存放入
    *
    * @param key 键
    * @param value 值
    * @return true成功 false失败
    */
    public boolean set(String key, Object value) {
    try {
    redisTemplate.opsForValue().set(key, value);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 普通缓存放入并设置时间
    *
    * @param key 键
    * @param value 值
    * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
    * @return true成功 false 失败
    */
    public boolean set(String key, Object value, long time) {
    try {
    if (time > 0) {
    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    } else {
    set(key, value);
    }
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 递增
    *
    * @param key 键
    * @param delta 要增加几(大于0)
    * @return
    */
    public long incr(String key, long delta) {
    if (delta < 0) {
    throw new RuntimeException("递增因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
    * 递减
    *
    * @param key 键
    * @param delta 要减少几(小于0)
    * @return
    */
    public long decr(String key, long delta) {
    if (delta < 0) {
    throw new RuntimeException("递减因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, -delta);
    }

    // ================================Map=================================

    /**
    * HashGet
    *
    * @param key 键 不能为null
    * @param item 项 不能为null
    * @return
    */
    public Object hget(String key, String item) {
    return redisTemplate.opsForHash().get(key, item);
    }

    /**
    * 获取hashKey对应的所有键值
    *
    * @param key 键
    * @return 对应的多个键值
    */
    public Map<Object, Object> hmget(String key) {
    return redisTemplate.opsForHash().entries(key);
    }

    /**
    * HashSet
    *
    * @param key 键
    * @param map 对应多个键值
    * @return true 成功 false 失败
    */
    public boolean hmset(String key, Map<String, Object> map) {
    try {
    redisTemplate.opsForHash().putAll(key, map);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * HashSet 并设置时间
    *
    * @param key 键
    * @param map 对应多个键值
    * @param time 时间(秒)
    * @return true成功 false失败
    */
    public boolean hmset(String key, Map<String, Object> map, long time) {
    try {
    redisTemplate.opsForHash().putAll(key, map);
    if (time > 0) {
    expire(key, time);
    }
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 向一张hash表中放入数据,如果不存在将创建
    *
    * @param key 键
    * @param item 项
    * @param value 值
    * @return true 成功 false失败
    */
    public boolean hset(String key, String item, Object value) {
    try {
    redisTemplate.opsForHash().put(key, item, value);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 向一张hash表中放入数据,如果不存在将创建
    *
    * @param key 键
    * @param item 项
    * @param value 值
    * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
    * @return true 成功 false失败
    */
    public boolean hset(String key, String item, Object value, long time) {
    try {
    redisTemplate.opsForHash().put(key, item, value);
    if (time > 0) {
    expire(key, time);
    }
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 删除hash表中的值
    *
    * @param key 键 不能为null
    * @param item 项 可以使多个 不能为null
    */
    public void hdel(String key, Object... item) {
    redisTemplate.opsForHash().delete(key, item);
    }

    /**
    * 判断hash表中是否有该项的值
    *
    * @param key 键 不能为null
    * @param item 项 不能为null
    * @return true 存在 false不存在
    */
    public boolean hHasKey(String key, String item) {
    return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
    * hash递增 如果不存在,就会创建一个 并把新增后的值返回
    *
    * @param key 键
    * @param item 项
    * @param by 要增加几(大于0)
    * @return
    */
    public double hincr(String key, String item, double by) {
    return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
    * hash递减
    *
    * @param key 键
    * @param item 项
    * @param by 要减少记(小于0)
    * @return
    */
    public double hdecr(String key, String item, double by) {
    return redisTemplate.opsForHash().increment(key, item, -by);
    }
    // ============================set=============================

    /**
    * 根据key获取Set中的所有值
    *
    * @param key 键
    * @return
    */
    public Set<Object> sGet(String key) {
    try {
    return redisTemplate.opsForSet().members(key);
    } catch (Exception e) {
    e.printStackTrace();
    return null;
    }
    }

    /**
    * 根据value从一个set中查询,是否存在
    *
    * @param key 键
    * @param value 值
    * @return true 存在 false不存在
    */
    public boolean sHasKey(String key, Object value) {
    try {
    return redisTemplate.opsForSet().isMember(key, value);
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 将数据放入set缓存
    *
    * @param key 键
    * @param values 值 可以是多个
    * @return 成功个数
    */
    public long sSet(String key, Object... values) {
    try {
    return redisTemplate.opsForSet().add(key, values);
    } catch (Exception e) {
    e.printStackTrace();
    return 0;
    }
    }

    /**
    * 将set数据放入缓存
    *
    * @param key 键
    * @param time 时间(秒)
    * @param values 值 可以是多个
    * @return 成功个数
    */
    public long sSetAndTime(String key, long time, Object... values) {
    try {
    Long count = redisTemplate.opsForSet().add(key, values);
    if (time > 0)
    expire(key, time);
    return count;
    } catch (Exception e) {
    e.printStackTrace();
    return 0;
    }
    }

    /**
    * 获取set缓存的长度
    *
    * @param key 键
    * @return
    */
    public long sGetSetSize(String key) {
    try {
    return redisTemplate.opsForSet().size(key);
    } catch (Exception e) {
    e.printStackTrace();
    return 0;
    }
    }

    /**
    * 移除值为value的
    *
    * @param key 键
    * @param values 值 可以是多个
    * @return 移除的个数
    */
    public long setRemove(String key, Object... values) {
    try {
    Long count = redisTemplate.opsForSet().remove(key, values);
    return count;
    } catch (Exception e) {
    e.printStackTrace();
    return 0;
    }
    }
    // ===============================list=================================

    /**
    * 获取list缓存的内容
    *
    * @param key 键
    * @param start 开始
    * @param end 结束 0 到 -1代表所有值
    * @return
    */
    public List<Object> lGet(String key, long start, long end) {
    try {
    return redisTemplate.opsForList().range(key, start, end);
    } catch (Exception e) {
    e.printStackTrace();
    return null;
    }
    }

    /**
    * 获取list缓存的长度
    *
    * @param key 键
    * @return
    */
    public long lGetListSize(String key) {
    try {
    return redisTemplate.opsForList().size(key);
    } catch (Exception e) {
    e.printStackTrace();
    return 0;
    }
    }

    /**
    * 通过索引 获取list中的值
    *
    * @param key 键
    * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
    * @return
    */
    public Object lGetIndex(String key, long index) {
    try {
    return redisTemplate.opsForList().index(key, index);
    } catch (Exception e) {
    e.printStackTrace();
    return null;
    }
    }

    /**
    * 将list放入缓存
    *
    * @param key 键
    * @param value 值
    * @param time 时间(秒)
    * @return
    */
    public boolean lSet(String key, Object value) {
    try {
    redisTemplate.opsForList().rightPush(key, value);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 将list放入缓存
    *
    * @param key 键
    * @param value 值
    * @param time 时间(秒)
    * @return
    */
    public boolean lSet(String key, Object value, long time) {
    try {
    redisTemplate.opsForList().rightPush(key, value);
    if (time > 0)
    expire(key, time);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 将list放入缓存
    *
    * @param key 键
    * @param value 值
    * @param time 时间(秒)
    * @return
    */
    public boolean lSet(String key, List<Object> value) {
    try {
    redisTemplate.opsForList().rightPushAll(key, value);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 将list放入缓存
    * å
    *
    * @param key 键
    * @param value 值
    * @param time 时间(秒)
    * @return
    */
    public boolean lSet(String key, List<Object> value, long time) {
    try {
    redisTemplate.opsForList().rightPushAll(key, value);
    if (time > 0)
    expire(key, time);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 根据索引修改list中的某条数据
    *
    * @param key 键
    * @param index 索引
    * @param value 值
    * @return
    */
    public boolean lUpdateIndex(String key, long index, Object value) {
    try {
    redisTemplate.opsForList().set(key, index, value);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 移除N个值为value
    *
    * @param key 键
    * @param count 移除多少个
    * @param value 值
    * @return 移除的个数
    */
    public long lRemove(String key, long count, Object value) {
    try {
    Long remove = redisTemplate.opsForList().remove(key, count, value);
    return remove;
    } catch (Exception e) {
    e.printStackTrace();
    return 0;
    }
    }
    }

9-SpringBoot整合定时任务和异步任务

9.1-SpringBoot定时任务schedule

  1. 常见定时任务

    • Java自带的java.util.Timer类

      timer:配置比较麻烦,时间延后问题
      timertask:不推荐

    • Quartz框架

      配置更简单
      xml或者注解

    • SpringBoot使用注解方式开启定时任务

      • 启动类里面 @EnableScheduling开启定时任务,自动扫描
      • 定时任务业务类 加注解 @Component被容器扫描
      • 定时执行的方法加上注解 @Scheduled(fixedRate=2000) 定期执行一次
  2. 定时任务schedule

    SpringBoot内置了定时任务Scheduled,能够很好的实现定时任务。

    • 在SpringBoot应用添加@EnableScheduling注解启动定时任务
    @SpringBootApplication
    @EnableScheduling
    public class SpringbootTestApplication {
    public static void main(String[] args) {
    SpringApplication.run(SpringbootTestApplication.class, args);
    }
    }
    • 添加测试定时任务的代码
    @Component
    public class ScheduledTask {

    @Scheduled(cron = "5 0 0 * * ?")
    public void scheduledTask1(){
    System.out.println("定时任务1");
    }

    @Scheduled(initialDelay = 1000 * 10,fixedDelay = 1000 * 5)
    public void scheduledTask2(){
    System.out.println("任务2执行时间:"+System.currentTimeMillis());
    System.out.println("定时任务2");
    try {
    Thread.sleep(2*1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("任务2结束时间:"+System.currentTimeMillis());
    }

    @Scheduled(initialDelay = 1000 * 10,fixedRate = 1000 * 5)
    public void scheduledTask3(){
    System.out.println("任务3执行时间:"+System.currentTimeMillis());
    System.out.println("定时任务3");
    try {
    Thread.sleep(2*1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("任务3结束时间:"+System.currentTimeMillis());
    }
    }

    注:

    • corn表达式在linux使用广泛,具体可以参考cron表达式详解以及在线Cron表达式生成器
    • initialDelay:启动后多久开始执行,单位时毫秒
    • fixedRate:下次执行时间,任务开始运行的时候就计时
    • fixedDelay:下次执行时间,fixedDelay等任务进行完了才开始计时,上一次执行结束时间点后xx秒再次执行
    • fixedDelayString: 字符串形式,可以通过配置文件指定

    9.2-SpringBoot异步任务

    1. 启动类里面使用@EnableAsync注解开启功能,自动扫描

      @EnableAsync
      @SpringBootApplication
      public class Application {

      public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
      }
      }
    2. 定义异步任务类并使用@Component标记组件被容器扫描,异步方法加上@Async

          注意点:
                 1)要把异步任务封装到类里面,不能直接写到Controller
                 2)增加Future<String> 返回结果 AsyncResult<String>("task执行完成");  
                 3)如果需要拿到结果 需要判断全部的 task.isDone()
      

      @EnableAsync 表示支持异步任务,springboot对于异步,定时,缓存,切面等的配置都是通过在启动类上加 @EnableXXX来配置的。

      @Async表示该方法会异步执行,也就是说主线程会直接跳过该方法,而是使用线程池中的线程来执行该方法。

      @Component
      public class AsyncTask {

      @Async
      public Future<String> execTaskA() throws InterruptedException {
      System.out.println("TaskA开始");
      long star = new Date().getTime();
      Thread.sleep(5000);

      long end = new Date().getTime();
      System.out.println("TaskA结束,耗时毫秒数:" + (end - star));
      return new AsyncResult<>("TaskA结束");
      }

      @Async
      public Future<String> execTaskB() throws InterruptedException {
      System.out.println("TaskB开始");
      long star = new Date().getTime();
      Thread.sleep(3000);

      long end = new Date().getTime();
      System.out.println("TaskB结束,耗时毫秒数:" + (end - star));
      return new AsyncResult<>("TaskB结束");
      }

      @Async
      public Future<String> execTaskC() throws InterruptedException {
      System.out.println("TaskC开始");
      long star = new Date().getTime();
      Thread.sleep(4000);

      long end = new Date().getTime();
      System.out.println("TaskC结束,耗时毫秒数:" + (end - star));
      return new AsyncResult<>("TaskC结束");
      }

      }
    3. 通过注入方式,注入到controller里面,如果测试前后区别则改为同步则把Async注释掉

      @RestController
      public class TaskController {
      @Autowired
      private AsyncTask asyncTask;
      @GetMapping("/testTask")
      public void testTask() throws InterruptedException {
      long start = new Date().getTime();
      System.out.println("任务开始,当前时间" +star );

      Future<String> taskA = asyncTask.execTaskA();
      Future<String> taskB = asyncTask.execTaskB();
      Future<String> taskC = asyncTask.execTaskC();

      //间隔一秒轮询 直到 A B C 全部完成
      while (true) {
      if (taskA.isDone() && taskB.isDone() && taskC.isDone()) {
      break;
      }
      Thread.sleep(1000);
      }

      long end = new Date().getTime();
      System.out.println("任务结束,当前时间" + end);
      System.out.println("总耗时:"+(end-start));
      }
      }

9.3-SpringBootz整合Quartz

Quartz是一款功能强大的任务调度器,可以实现较为复杂的调度功能,如每月一号执行、每天凌晨执行、每周五执行等等,还支持分布式调度。本文使用Springboot+Mybatis+Quartz实现对定时任务的增、删、改、查、启用、停用等功能。并把定时任务持久化到数据库以及支持集群。

Quartz的3个基本要素

  • Scheduler:调度器。所有的调度都是由它控制。
  • Trigger: 触发器。决定什么时候来执行任务。
  • JobDetail & Job: JobDetail定义的是任务数据,而真正的执行逻辑是在Job中。使用JobDetail + Job而不是Job,这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail & Job 方式,sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。
  1. 引入依赖jar包

    <!--QuartZ-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
  2. 添加配置application-quartz.yml

    spring:
    #配置数据源
    datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/testquartz?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: password
    quartz:
    #持久化到数据库方式
    job-store-type: jdbc
    initialize-schema: embedded
    properties:
    org:
    quartz:
    scheduler:
    instanceName: MyScheduler
    instanceId: AUTO
    jobStore:
    class: org.quartz.impl.jdbcjobstore.JobStoreTX
    driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    tablePrefix: QRTZ_
    isClustered: true
    clusterCheckinInterval: 10000
    useProperties: false
    threadPool:
    class: org.quartz.simpl.SimpleThreadPool
    threadCount: 10
    threadPriority: 5
    threadsInheritContextClassLoaderOfInitializingThread: true
  3. 实现Job接口并且在execute方法中实现自己的业务逻辑

    public class HelloworldJob extends QuartzJobBean {
    @Autowired
    HelloworldService helloworldService;
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    helloworldService.printHelloWorld();
    System.out.println("Hello world! :" + jobExecutionContext.getJobDetail().getKey());
    }
    }
  4. 添加配置类

    @Configuration
    public class QuartzConfig {
    @Bean
    public JobDetail myJobDetail(){
    JobDetail jobDetail = JobBuilder.newJob(HiJob.class)
    .withIdentity("myJob1","myJobGroup1")
    //JobDataMap可以给任务execute传递参数
    .usingJobData("job_param","job_param1")
    .storeDurably()
    .build();
    return jobDetail;
    }
    @Bean
    public Trigger myTrigger(){
    Trigger trigger = TriggerBuilder.newTrigger()
    .forJob(myJobDetail())
    .withIdentity("myTrigger1","myTriggerGroup1")
    .usingJobData("job_trigger_param","job_trigger_param1")
    .startNow()
    //.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
    .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ? 2018"))
    .build();
    return trigger;
    }
    }
  5. Quartz使用同一组数据库表作集群只需要配置相同的instanceName实例名称,以及设置org.quartz.jobStore.isClustered = true
    启动两个节点后关闭其中正在跑任务的节点,另一个节点会自动检测继续运行定时任务

注:多任务的问题,多个JobDetail使用同一个Trigger报错:Trigger does not reference given job!

​ 一个Job可以对应多个Trigger,但多个Job绑定一个Trigger报错。

10-Logback日志框架介绍和SpringBoot整合

10.1-新日志框架LogBack介绍

  1. 常用处理java的日志组件 slf4j,log4j,logback,common-logging 等

  2. logback介绍:

    ​ 基于Log4j基础上大量改良,不能单独使用,推荐配合日志框架SLF4J来使用
    ​ logback当前分成三个模块:logback-core,logback-classic和logback-access;
    ​ logback-core是其它两个模块的基础模块

  3. Logback的核心对象:

    Logger:日志记录器
       Appender:指定日志输出的目的地,目的地可以是控制台,文件
       Layout:日志布局 格式化日志信息的输出
    
  4. 日志级别:DEBUG < INFO < WARN < ERROR

    ===========log4j示例===========		
    ### 设置###
    log4j.rootLogger = debug,stdout,D,E

    ### 输出信息到控制抬 ###
    log4j.appender.stdout = org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.Target = System.out
    log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n

    ### 输出DEBUG 级别以上的日志到=D://logs/error.log ###
    log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
    log4j.appender.D.File = D://logs/log.log
    log4j.appender.D.Append = true
    log4j.appender.D.Threshold = DEBUG
    log4j.appender.D.layout = org.apache.log4j.PatternLayout
    log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n

    ### 输出ERROR 级别以上的日志到=D://logs/error.log ###
    log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
    log4j.appender.E.File =E://logs/error.log
    log4j.appender.E.Append = true
    log4j.appender.E.Threshold = ERROR
    log4j.appender.E.layout = org.apache.log4j.PatternLayout
    log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
  5. Log4j日志转换为logback在线工具(支持log4j.properties转换为logback.xml,不支持 log4j.xml转换为logback.xml) https://logback.qos.ch/translator/

10.2-SpringBoot2.x日志讲解和Logback

  1. 官网介绍:https://docs.spring.io/spring-boot/docs/2.1.0.BUILD-SNAPSHOT/reference/htmlsingle/#boot-features-logging

    各个组件案例:https://logback.qos.ch/manual/index.html

  2. 分析SpringBoot启动日志

    • 默认情况下,Spring Boot将日志输出到控制台
  3. 整合Logback实战

    • 创建 日志文件logback-spring.xml,官方推荐 -spring.xml结尾
      默认加载加载配置顺序 logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
  4. 注释:

    <configuration> 子节点
       <appender></appender>                       
       <logger></logger>
       <root></root>(要加在最后)        
    

Logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true" scan="true" scanPeriod="1 seconds">

<contextName>logback</contextName>
<!--定义参数,后面可以通过${app.name}使用-->
<property name="app.name" value="logback_test"/>
<!--ConsoleAppender 用于在屏幕上输出日志-->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!--定义了一个过滤器,在LEVEL之下的日志输出不会被打印出来-->
<!--这里定义了DEBUG,也就是控制台不会输出比ERROR级别小的日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<!-- encoder 默认配置为PatternLayoutEncoder -->
<!--定义控制台输出格式-->
<encoder>
<pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern>
</encoder>
</appender>

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--定义日志输出的路径-->
<!--这里的scheduler.manager.server.home 没有在上面的配置中设定,所以会使用java启动时配置的值-->
<!--比如通过 java -Dscheduler.manager.server.home=/path/to XXXX 配置该属性-->
<file>${scheduler.manager.server.home}/logs/${app.name}.log</file>
<!--定义日志滚动的策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--定义文件滚动时的文件名的格式-->
<fileNamePattern>${scheduler.manager.server.home}/logs/${app.name}.%d{yyyy-MM-dd.HH}.log.gz
</fileNamePattern>
<!--60天的时间周期,日志量最大20GB-->
<maxHistory>60</maxHistory>
<!-- 该属性在 1.1.6版本后 才开始支持-->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<!--每个日志文件最大100MB-->
<maxFileSize>100MB</maxFileSize>
</triggeringPolicy>
<!--定义输出格式-->
<encoder>
<pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern>
</encoder>
</appender>

<!--root是默认的logger 这里设定输出级别是debug-->
<root level="trace">
<!--定义了两个appender,日志会通过往这两个appender里面写-->
<appender-ref ref="stdout"/>
<appender-ref ref="file"/>
</root>

<!--对于类路径以 com.example.logback 开头的Logger,输出级别设置为warn,并且只输出到控制台-->
<!--这个logger没有指定appender,它会继承root节点中定义的那些appender-->
<logger name="com.example.logback" level="warn"/>

<!--通过 LoggerFactory.getLogger("mytest") 可以获取到这个logger-->
<!--由于这个logger自动继承了root的appender,root中已经有stdout的appender了,自己这边又引入了stdout的appender-->
<!--如果没有设置 additivity="false" ,就会导致一条日志在控制台输出两次的情况-->
<!--additivity表示要不要使用rootLogger配置的appender进行输出-->
<logger name="mytest" level="info" additivity="false">
<appender-ref ref="stdout"/>
</logger>

<!--由于设置了 additivity="false" ,所以输出时不会使用rootLogger的appender-->
<!--但是这个logger本身又没有配置appender,所以使用这个logger输出日志的话就不会输出到任何地方-->
<logger name="mytest2" level="info" additivity="false"/>
</configuration>

11-搜索框架ElasticSearch介绍和整合SpringBoot

11.1-搜索知识

  • mysql:like 模糊,性能问题

  • solr:针对企业,Lucene

  • elasticsearch:

    针对数据量特别大,PB,TB
    纯java开发,springboot使用,5.6版本
    es升级4->5版本,改动大,但是5版本后,改动不大

11.2-ElasticSearch介绍

Elasticsearch(ES)是一个基于Apache的开源索引库Lucene而构建的开源、分布式、具有RESTful接口的全文搜索引擎, 还是一个分布式文档数据库.

ES可以轻松扩展数以百计的服务器(水平扩展), 用于存储和处理数据. 它可以在很短的时间内存储、搜索和分析海量数据, 通常被作为复杂搜索场景下的核心引擎.

由于Lucene提供的API操作起来非常繁琐, 需要编写大量的代码, Elasticsearch对Lucene进行了封装与优化, 并提供了REST风格的操作接口, 开箱即用, 很大程度上方便了开发人员的使用.

  • elasticSearch主要特点
    1. 特点:全文检索,结构化检索,数据统计、分析,接近实时处理,分布式搜索(可部署数百台服务器),处理PB级别的数据,搜索纠错,自动完成
    2. 使用场景:日志搜索,数据聚合,数据监控,报表统计分析
    3. 国内外使用者:维基百科,Stack Overflow,GitHub

11.3-SpringBoot整合ElasticSearch

  1. 添加依赖
<!--elasticsearch-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
  1. 配置文件
spring:
data:
elasticsearch:
cluster-name: esCluster
cluster-nodes: 127.0.0.1:9300
#配置es节点信息,逗号分隔,如果没有指定,则启动ClientNode(9200端口是http查询使用的。9300集群使用。这里使用9300.)
  1. 创建实体类bean
@Data
@Document(indexName = "testgoods", type = "goods")
public class TestGoodsBo {

@Id
private long id;

//@Field(type = FieldType.Text)
private String name;

private BigDecimal price;

private long stock;

@Version
private Long version;
}
  • @Document注解

    @Persistent
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    public @interface Document {
    String indexName();//索引库的名称,个人建议以项目的名称命名
    String type() default "";//类型,个人建议以实体的名称命名
    short shards() default 5;//默认分区数
    short replicas() default 1;//每个分区默认的备份数
    String refreshInterval() default "1s";//刷新间隔
    String indexStoreType() default "fs";//索引文件存储类型
    }

    @Document作用于类上,经测试代码初始化时若es中没有对应的索引,则会在es中创建一个。

  • @Field注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @Documented
    @Inherited
    public @interface Field {

    FieldType type() default FieldType.Auto;#自动检测属性的类型
    FieldIndex index() default FieldIndex.analyzed;#默认情况下分词
    DateFormat format() default DateFormat.none;
    String pattern() default "";
    boolean store() default false;#默认情况下不存储原文
    String searchAnalyzer() default "";#指定字段搜索时使用的分词器
    String indexAnalyzer() default "";#指定字段建立索引时指定的分词器
    String[] ignoreFields() default {};#如果某个字段需要被忽略
    boolean includeInParent() default false;
    }

    @Field作用于属性上,经测试该注解的属性有时会与现有的属性冲突,造成异常,错误信息如下,所以建议es中映射已建立的情况下,不要使用该注解。

  • @Id@Version分别用来绑定es中的_id_version字段。

  1. 创建Repository(接口继承ElasticSearchRepository)
@Component
public interface GoodsRepository extends ElasticsearchRepository<TestGoodsBo,Long> , PagingAndSortingRepository<TestGoodsBo,Long> {

List<TestGoodsBo> findByNameAndPrice(String name, Long price);

List<TestGoodsBo> findByNameOrPrice(String name, Long price);

Page<TestGoodsBo> findByName(String name,Pageable page);

Page<TestGoodsBo> findByNameNot(String name,Pageable page);

Page<TestGoodsBo> findByPriceBetween(long price,Pageable page);

Page<TestGoodsBo> findByNameLike(String name,Pageable page);

@Query("{\"bool\" : {\"must\" : {\"term\" : {\"message\" : \"?0\"}}}}")
Page<TestGoodsBo> findByMessage(String message, Pageable pageable);
}

es的操作主要通过自定义的Repository对象完成,该对象可以通过继承模板接口ElasticsearchRepository实现,该模板提供了savefindByIdfindAllsearch等通用方法的实现,同时还支持通过规定的名称格式自定义操作方法.

  1. 使用
@Slf4j
@RestController
@RequestMapping(value = "/search")
public class SearchController {

@Autowired
private GoodsRepository repository;

@Autowired
private ElasticsearchTemplate elasticsearchTemplate;

@RequestMapping(value = "/insert")
public TestGoodsBo insert(@RequestBody TestGoodsBo bo) {
repository.save(bo);
return bo;
}

@RequestMapping(value = "/get")
public TestGoodsBo get() {
Optional<TestGoodsBo> result = repository.findById(1L);
return result.get();
}

@RequestMapping(value = "/find")
public List<TestGoodsBo> find(String name,Pageable page){
return repository.findByName(name, page);
}

@GetMapping(value = "/search")
public Page<TestGoodsBo> search(String name, @PageableDefault(value = 15, sort = { "id" }, direction = Sort.Direction.DESC)Pageable pageable) {
// //通过ElasticsearchTemplate实现
// QueryBuilder queryBuilder = QueryBuilders.matchQuery("name", name);
// SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(queryBuilder).withHighlightFields().build();
// Page<TestGoodsBo> sampleEntities = elasticsearchTemplate.queryForPage(searchQuery, TestGoodsBo.class);

// //Pageable对象的手动实现
// Sort sort = new Sort(Sort.Direction.ASC,"name");
// Pageable page = PageRequest.of(0,10,sort);

Page<TestGoodsBo> sampleEntities = repository.search(QueryBuilders.matchQuery("name", name),pageable);
return sampleEntities;
}

}
  1. Pageable对象

该对象可以帮助我们完成分页和排序操作,有手动和自动两种方式实现:

  • 手动

    Sort sort = new Sort(Sort.Direction.ASC,"name");
    Pageable page = PageRequest.of(0,10,sort);
  • 自动

    @GetMapping(value = "/search")
    public Page<TestGoodsBo> search(String name, @PageableDefault(value = 15, sort = { "id" }, direction = Sort.Direction.DESC)Pageable pageable)

    自动方式可以在request传参的同时就根据传入的参数来组装Pageable对象,同时还能使用@PageableDefault注解设定默认值,因此更推荐使用。

    Spring支持的request参数如下:

    • page,第几页,从0开始,默认为第0页

    • size,每一页的大小,默认为20

    • sort,排序相关的信息,例如sort=firstname&sort=lastname,desc表示在按firstname正序排列基础上按lastname倒序排列

12-消息队列介绍和SpringBoot整合RockketMQ、ActiveMQ

12.1-JMS介绍和使用场景及基础编程模型

  1. 什么是JMS?

    Java消息服务(Java Message Service),Java平台中关于面向消息中间件的接口

  2. JMS是一种与厂商无关的 API,用来访问消息收发系统消息,它类似于JDBC(Java Database Connectivity)。这里,JDBC 是可以用来访问许多不同关系数据库的 API

  3. 使用场景

    • 跨平台
    • 多语言
    • 多项目
    • 解耦
    • 分布式事务
    • 流量控制
    • 最终一致性
    • RPC调用
  4. 概念

    • JMS提供者:Apache ActiveMQ、RabbitMQ、Kafka、Notify、MetaQ、RocketMQ
    • JMS生产者(Message Producer)
    • JMS消费者(Message Consumer)
    • JMS消息
    • JMS队列
    • JMS主题

    JMS消息通常有两种类型:点对点发布/订阅

  5. 编程模型

    MQ中需要用的一些类

    ConnectionFactory :连接工厂,JMS 用它创建连接
    Connection :JMS 客户端到JMS Provider 的连接
    Session: 一个发送或接收消息的线程
    Destination :消息的目的地;消息发送给谁
    MessageConsumer / MessageProducer: 消息接收者,消费者
    

12.2-ActiveMQ消息队列基础介绍

ActiveMQ是一种开源的基于JMS规范的一种消息中间件的实现,ActiveMQ的设计目标是提供标准的,面向消息的,能够跨越多语言和多系统的应用集成消息通信中间件。

特点:

  • 支持来自Java,C,C ++,C#,Ruby,Perl,Python,PHP的各种跨语言客户端和协议
  • 支持许多高级功能,如消息组,虚拟目标,通配符和复合目标
  • 完全支持JMS 1.1和J2EE 1.4,支持瞬态,持久,事务和XA消息
  • Spring支持,ActiveMQ可以轻松嵌入到Spring应用程序中,并使用Spring的XML配置机制进行配置
  • 支持在流行的J2EE服务器(如TomEE,Geronimo,JBoss,GlassFish和WebLogic)中进行测试
  • 使用JDBC和高性能日志支持非常快速的持久化

12.3-SpringBoot2整合ActiveMQ实战之点对点消息

  1. 添加依赖

    <!-- 整合消息队列ActiveMQ -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
    </dependency>

    <!-- 如果配置线程池则加入 -->
    <dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-pool</artifactId>
    </dependency>
  2. 配置文件

    #整合jms测试,安装在别的机器,防火墙和端口号记得开放
    spring.activemq.broker-url=tcp://127.0.0.1:61616

    #集群配置
    #spring.activemq.broker-url=failover:(tcp://localhost:61616,tcp://localhost:61617)

    spring.activemq.user=admin
    spring.activemq.password=admin
    #下列配置要增加依赖
    spring.activemq.pool.enabled=true
    spring.activemq.pool.max-connections=100
  3. 启动类添加@EnableJms注解

    @SpringBootApplication
    @EnableJms //启动消息队列
    public class ProductApplication {
    public static void main(String[] args) {
    SpringApplication.run(ProductApplication.class, args);
    }
    }
  4. QueueConfig定义消息队列

    import javax.jms.Queue;
    import org.apache.activemq.command.ActiveMQQueue;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class QueueConfig {

    //定义存放消息的队列
    @Bean
    public Queue queue() {
    return new ActiveMQQueue("ActiveMQQueue");
    }
    }
  5. ProviderController测试

    @RestController
    public class ProductController {

    //注入存放消息的队列,用于下列方法一
    @Autowired
    private Queue queue;

    //注入springboot封装的工具类
    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    @GetMapping("send")
    public void send(String message) {
    //方法一:添加消息到消息队列
    jmsMessagingTemplate.convertAndSend(queue, message);
    //方法二:这种方式不需要手动创建queue,系统会自行创建名为test的队列
    //jmsMessagingTemplate.convertAndSend("test", message);
    }

    }
  6. 消费者应用

    application.properties 和 ConsumerApplication 同 provider类似,如下为不同的ActiveConsumer:

    @Component
    public class ActiveConsumer {

    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    // 使用JmsListener配置消费者监听的队列,其中message是接收到的消息
    @JmsListener(destination = "ActiveMQQueue")
    // SendTo 会将此方法返回的数据, 写入到 OutQueue 中去.
    @SendTo("SQueue")
    public String handleMessage(String message) {
    System.out.println("成功接受message" + message);
    return "成功接受message" + message;
    }
    }
  7. 模拟请求 localhost:8080/send?msg=123

12.4-SpringBoot整合ActiveMQ实战之发布订阅模式

  1. 需要加入配置文件,支持发布订阅模型,默认只支持点对点

    #default point to point
    #默认消费者并不会消费订阅发布类型的消息,这是由于springboot默认采用的是p2p模式进行消息的监听
    spring.jms.pub-sub-domain=true
  2. 新建JMS配置类

    @Configuration
    public class JmsConfig {
    public final static String TOPIC = "springboot.topic.test";
    public final static String QUEUE = "springboot.queue.test";

    @Bean
    public Topic topic() {
    return new ActiveMQTopic(TOPIC);
    }

    @Bean
    public Queue queue() {
    return new ActiveMQQueue(QUEUE);
    }
    // topic模式的ListenerContainer
    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerTopic(ConnectionFactory activeMQConnectionFactory) {
    DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
    bean.setPubSubDomain(true);
    bean.setConnectionFactory(activeMQConnectionFactory);
    return bean;
    }
    // queue模式的ListenerContainer
    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerQueue(ConnectionFactory activeMQConnectionFactory) {
    DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
    bean.setConnectionFactory(activeMQConnectionFactory);
    return bean;
    }
    }
  3. 生产者

    @Service("producer")
    public class Producer {
    @Autowired
    private JmsMessagingTemplate jmsTemplate;
    // 发送消息,destination是发送到的队列,message是待发送的消息
    public void sendMessage(Destination destination, final String message){
    jmsTemplate.convertAndSend(destination, message);
    }
    }
  4. 消费者类

    @Component
    public class Consumer {
    private final static Logger logger = LoggerFactory.getLogger(JMSConsumer3.class);

    @JmsListener(destination = JmsConfig.TOPIC,containerFactory = "jmsListenerContainerTopic")
    public void onTopicMessage1(String msg) {
    logger.info("接收到topic消息:{}",msg);
    }

    @JmsListener(destination = JmsConfig.TOPIC,containerFactory = "jmsListenerContainerTopic")
    public void onTopicMessage2(String msg) {
    logger.info("接收到topic消息:{}",msg);
    }

    @JmsListener(destination = JmsConfig.QUEUE,containerFactory = "jmsListenerContainerQueue")
    public void onQueueMessage(String msg) {
    logger.info("接收到queue消息:{}",text);
    }
    }
  5. 测试

    @Autowired
    private Topic topic;
    @Autowired
    private Queue queue;

    @Test
    public void testJms() {
    for (int i=0;i<10;i++) {
    jmsProducer.sendMessage(queue,"queue,world!" + i);
    jmsProducer.sendMessage(topic, "topic,world!" + i);
    }
    }

12.5-RocketMQ消息队列介绍

RocketMQ 是一款分布式、队列模型的消息中间件

特点:

  • 在高压下1毫秒内响应延迟超过99.6%。
  • 适合金融类业务,高可用性跟踪和审计功能。
  • 支持发布订阅模型,和点对点
  • 支持拉pull和推push两种消息模式
  • 单一队列百万消息
  • 支持单master节点,多master节点,多master多slave节点

概念:

  • Producer:消息生产者
  • Producer Group:消息生产者组,发送同类消息的一个消息生产组
  • Consumer:消费者
  • Consumer Group:消费同个消息的多个实例
  • Tag:标签,子主题(二级分类),用于区分同一个主题下的不同业务的消息
  • Topic:主题
  • Message:消息
  • Broker:MQ程序,接收生产的消息,提供给消费者消费的程序
  • Name Server:给生产和消费者提供路由信息,提供轻量级的服务发现和路由

官网地址http://rocketmq.apache.org/

12.6-Springboot2整合RocketMQ4.x实战

  1. 添加依赖

    <dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    </dependency>
  2. 配置文件

    rocketmq:
    # 生产者配置
    producer:
    isOnOff: on
    # 发送同一类消息的设置为同一个group,保证唯一
    groupName: FeePlatGroup
    # 服务地址
    namesrvAddr: 10.1.1.207:9876
    # 消息最大长度 默认1024*4(4M)
    maxMessageSize: 4096
    # 发送消息超时时间,默认3000
    sendMsgTimeout: 3000
    # 发送消息失败重试次数,默认2
    retryTimesWhenSendFailed: 2
    # 消费者配置
    consumer:
    isOnOff: on
    # 官方建议:确保同一组中的每个消费者订阅相同的主题。
    groupName: FeePlatGroup
    # 服务地址
    namesrvAddr: 10.1.1.207:9876
    # 接收该 Topic 下所有 Tag
    topics: FangPlatTopic~*;
    consumeThreadMin: 20
    consumeThreadMax: 64
    # 设置一次消费消息的条数,默认为1条
    consumeMessageBatchMaxSize: 1

    # 配置 Group Topic Tag
    fang-plat:
    fang-plat-group: FangPlatGroup
    fang-plat-topic: FangPlatTopic
    fang-account-tag: FangAccountTag
  3. 生产者配置

    //RocketMQ生产者配置
    @Configuration
    public class ProducerConfig {
    private static final Logger LOG = LoggerFactory.getLogger(ProducerConfig.class) ;
    @Value("${rocketmq.producer.groupName}")
    private String groupName;
    @Value("${rocketmq.producer.namesrvAddr}")
    private String namesrvAddr;
    @Value("${rocketmq.producer.maxMessageSize}")
    private Integer maxMessageSize ;
    @Value("${rocketmq.producer.sendMsgTimeout}")
    private Integer sendMsgTimeout;
    @Value("${rocketmq.producer.retryTimesWhenSendFailed}")
    private Integer retryTimesWhenSendFailed;
    @Bean
    public DefaultMQProducer getRocketMQProducer() {
    DefaultMQProducer producer;
    producer = new DefaultMQProducer(this.groupName);
    producer.setNamesrvAddr(this.namesrvAddr);
    //如果需要同一个jvm中不同的producer往不同的mq集群发送消息,需要设置不同的instanceName
    if(this.maxMessageSize!=null){
    producer.setMaxMessageSize(this.maxMessageSize);
    }
    if(this.sendMsgTimeout!=null){
    producer.setSendMsgTimeout(this.sendMsgTimeout);
    }
    //如果发送消息失败,设置重试次数,默认为2次
    if(this.retryTimesWhenSendFailed!=null){
    producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
    }
    try {
    producer.start();
    } catch (MQClientException e) {
    e.printStackTrace();
    }
    return producer;
    }
    }
  4. 消费者配置

    //消费者配置
    @Configuration
    public class ConsumerConfig {
    private static final Logger LOG = LoggerFactory.getLogger(ConsumerConfig.class) ;
    @Value("${rocketmq.consumer.namesrvAddr}")
    private String namesrvAddr;
    @Value("${rocketmq.consumer.groupName}")
    private String groupName;
    @Value("${rocketmq.consumer.consumeThreadMin}")
    private int consumeThreadMin;
    @Value("${rocketmq.consumer.consumeThreadMax}")
    private int consumeThreadMax;
    @Value("${rocketmq.consumer.topics}")
    private String topics;
    @Value("${rocketmq.consumer.consumeMessageBatchMaxSize}")
    private int consumeMessageBatchMaxSize;
    @Resource
    private RocketMsgListener msgListener;
    @Bean
    public DefaultMQPushConsumer getRocketMQConsumer(){
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
    consumer.setNamesrvAddr(namesrvAddr);
    consumer.setConsumeThreadMin(consumeThreadMin);
    consumer.setConsumeThreadMax(consumeThreadMax);
    consumer.registerMessageListener(msgListener);
    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
    consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
    try {
    String[] topicTagsArr = topics.split(";");
    for (String topicTags : topicTagsArr) {
    String[] topicTag = topicTags.split("~");
    consumer.subscribe(topicTag[0],topicTag[1]);
    }
    consumer.start();
    }catch (MQClientException e){
    e.printStackTrace();
    }
    return consumer;
    }
    }
  5. 消费者监听配置

    //消费监听配置
    @Component
    public class RocketMsgListener implements MessageListenerConcurrently {
    private static final Logger LOGGER = LoggerFactory.getLogger(RocketMsgListener.class) ;
    @Resource
    private ParamConfigService paramConfigService ;
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {
    if (CollectionUtils.isEmpty(list)){
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
    MessageExt messageExt = list.get(0);
    LOG.info("接受到的消息为:"+new String(messageExt.getBody()));
    int reConsume = messageExt.getReconsumeTimes();
    // 消息已经重试了3次,如果不需要再次消费,则返回成功
    if(reConsume ==3){
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
    if(messageExt.getTopic().equals(paramConfigService.feePlatTopic)){
    String tags = messageExt.getTags() ;
    switch (tags){
    case "FeeAccountTag":
    LOGGER.info("开户 tag == >>"+tags);
    break ;
    default:
    LOGGER.info("未匹配到Tag == >>"+tags);
    break;
    }
    }
    // 消息消费成功
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
    }
  6. 配置参数绑定

    @Service
    public class ParamConfigService {
    @Value("${fang-plat.fang-plat-group}")
    public String fangPlatGroup ;
    @Value("${fang-plat.fang-plat-topic}")
    public String fangPlatTopic ;
    @Value("${fang-plat.fang-account-tag}")
    public String fangAccountTag ;
    }
  7. 测试

    @Service
    public class FeePlatMqServiceImpl implements FeePlatMqService {
    @Resource
    private DefaultMQProducer defaultMQProducer;
    @Resource
    private ParamConfigService paramConfigService ;
    @Override
    public SendResult openAccountMsg(String msgInfo) {
    // 可以不使用Config中的Group
    defaultMQProducer.setProducerGroup(paramConfigService.fangPlatGroup);
    SendResult sendResult = null;
    try {
    Message sendMsg = new Message(paramConfigService.fangPlatTopic,
    paramConfigService.fangAccountTag,
    "fang_open_account_key", msgInfo.getBytes());
    sendResult = defaultMQProducer.send(sendMsg);
    } catch (Exception e) {
    e.printStackTrace();
    }
    return sendResult ;
    }
    }

13-SpringBoot多环境配置

13.1-SpringBoot多环境配置介绍

  1. 不同环境使用不同配置
    例如数据库配置,在开发的时候,我们一般用开发数据库,而在生产环境的时候,我们是用正式的数据
  2. 配置文件存放路径
    classpath根目录的“/config”包下
    classpath的根目录下
  3. spring boot允许通过命名约定按照一定的格式(application-{profile}.properties)来定义多个配置文件
  4. spring.profiles.active=dev来指定加载哪个环境的配置文件

14-SpringBoot2.0响应式编程(Webflux)

14.1-什么是reactive响应式编程(反应式编程)?

是一种异步编程范式,它关注数据流和变化的传播。这意味着可以通过使用编程语言轻松地表示静态(例如数组)和动态(例如事件发射器)数据流。

响应式编程是一种流行的编程方法,编写代码是基于对变化的反应。它的灵感来自于我们的日常生活,也即我们如何采取行动以及与他人沟通。

我们在执行日常生活活动时,我们会尽可能多任务,但大脑无法处理多任务,不管我们如何努力去做。我们人类实现多任务的唯一办法是在时间线上在任务之间切换。事实上,我们总是切换任务,即使我们没有意识到它。

例如,要执行一个任务:在星巴克喝一杯咖啡饮料,你需要发出一个命令,等待它准备好,然后接受你的饮料。当你在等待的时候,你很可能会找到别的事情做。这是最简单的执行任务的反应(响应)形式,你会在你等待来自咖啡师的“响应”时做别的事情,当你的咖啡已经准备好后,会叫你的名字时。

响应编程能够简化编程,它依赖于事件,代码运行的顺序不是代码行的顺序,而是和一个以上的事件有关,这些事件发生是以随着时间的推移的序列。我们把这一系列事件称为“流”。

何为事件?例如,你知道某个名人总是在发送有趣微博,每次他推发一条微博我们可以称之为一个“事件”。如果你看看这位名人微博系列,你会发现其实是一个随着时间的推移(一系列的事件)发生的一序列的“事件”,响应式编程就是因为我们得“响应”这些事件而得以命名。

  • 依赖于事件,事件驱动(Event-driven)
  • 一系列事件称为“流”
  • 异步
  • 非阻塞
  • 观察者模式

例子

int A = B + C;

A被赋值为B和C的值。这时,如果我们改变B的值,A的值并不会随之改变。而如果我们运用一种机制,当B或者C的值发现变化的时候,A的值也随之改变,这样就实现了”响应式“。

14.2-SpringBoot2.x响应式编程webflux介绍

  1. Spring WebFlux是Spring Framework 5.0中引入的新的反应式Web框架,与Spring MVC不同,它不需要Servlet API,完全异步和非阻塞,并 通过Reactor项目实现Reactive Streams规范。

    • 传统的Servlet

      servlet由servlet container进行生命周期管理。container启动时构造servlet对象并调用servlet init()进行初始化;container关闭时调用servlet destory()销毁servlet;container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。

    缺点:

    servlet是一个简单的网络编程模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的,但是一旦并发上升,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型就较为吃力。

    例如:

    spring webmvc是基于servlet之上的一个路由模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet),并由该servlet进行路由。所以spring webmvc无法摆脱servlet模型的弊端。

    • Webflux

      Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的work线程中执行,而不阻塞的操作依然可以在Loop线程中进行处理,大大提高了Loop线程的利用率。

      Spring WebFlux是Spring Framework 5.0中引入的新的反应式Web框架,与Spring MVC不同,它不需要Servlet API,完全异步和非阻塞,并 通过Reactor项目实现Reactive Streams规范。

      响应式与非响应式区别:

  2. Flux和Mono

    • 简单业务而言:和其他普通对象差别不大,复杂请求业务,就可以提升性能

    • 通俗理解:

      • Mono 表示的是包含 0 或者 1 个元素的异步序列

        mono->单一对象 User 如: redis->用户ID->唯一的用户Mono<User>

      • Flux 表示的是包含 0 到 N 个元素的异步序列

        flux->数组列表对象 List<User> 如: redis->男性用户->Flux<User>

      • Flux 和 Mono 之间可以进行转换

  3. Spring WebFlux有两种风格:基于功能和基于注解的。基于注解非常接近Spring MVC模型

    • 基于注解的方式

      业务层Service:调用了ReactiveRedisTemplate对数据进行操作

      @Service
      public class ReactiveServiceImpl implements ReactiveService {

      @Autowired
      private ReactiveRedisTemplate reactiveRedisTemplate;

      private static final String USER_KEY = "entity:user";

      @Override
      public Flux<User> findAll() {
      return reactiveRedisTemplate.opsForHash().values(USER_KEY);
      }

      @Override
      public Mono<User> queryByUUID(String name) {
      return reactiveRedisTemplate.opsForHash().get(USER_KEY,name);
      }

      @Override
      public Mono<Boolean> add(User user) {
      String uuid = generateUUID();
      user.setUuid(uuid);
      return reactiveRedisTemplate.opsForHash().put(USER_KEY,user.getUuid(),user);
      }

      private String generateUUID() {
      return UUID.randomUUID().toString().replace("-","");
      }

      @Override
      public Mono<Boolean> update(User user) {
      return reactiveRedisTemplate.opsForHash().put(USER_KEY,user.getUuid(),user);
      }

      @Override
      public Mono<Boolean> delete(String uuid) {
      return reactiveRedisTemplate.opsForHash().delete(uuid);
      }
      }

      Controller层:

      @RestController
      @RequestMapping("/user")
      public class ReactiveController {

      @Autowired
      private ReactiveService reactiveService;
      // 查询所有
      @GetMapping("/find/all")
      public Flux<User> findAll() {
      return reactiveService.findAll();
      }
      // 查询单个
      @GetMapping("/query/{uuid}")
      public Mono<User> queryByName(@PathVariable("uuid") String uuid) {
      return reactiveService.queryByUUID(uuid);
      }
      // 添加用户
      @PostMapping("/add")
      public Mono<Boolean> add(@RequestBody User user) {
      return reactiveService.add(user);
      }
      // 更新用户
      @PutMapping("/update")
      public Mono<Boolean> update(@RequestBody User user) {
      return reactiveService.update(user);
      }
      // 删除用户
      @DeleteMapping("/delete/{uuid}")
      public Mono<Boolean> delete(@PathVariable("uuid") String uuid) {
      return reactiveService.delete(uuid);
      }
      }

      和使用mvc没有任何的区别,唯一的区别在于返回的对象是MonoFlux,简单点理解,返回单个数据就是Mono,多个就使用Flux

      启动项目可以看到实际上使用的是Netty服务器

    • 基于功能(函数式)

      处理请求的类,实现具体的业务逻辑,接口 ServerRequest 表示的是一个 HTTP 请求体。通过ServerRequest 对象可获取到请求的相关信息,如请求路径、查询参数和请求内容等。方法 的返回值是一个 Mono对象。接口 ServerResponse 用来表示 HTTP 响应。ServerResponse 中包含了很多静态方法来创建不同 HTTP 状态码的响应对象。

      涉及几个比较重要的类如:RouterFunction、HandlerFunction和DispatcherHandler

      RouterFunction就是一个路由函数,可以理解为将请求和具体的HandlerFunction做一个映射;

      1. 先创建RouterFunction

        @Component
        public class UserFunctionRouter {

        @Autowired
        private UserHandler userHandler;

        @Bean("userRouter")
        public RouterFunction router() {
        RouterFunction<ServerResponse> routerFunction = route()
        .GET("/user/find/all", accept(MediaType.APPLICATION_JSON_UTF8), userHandler::findAll)
        .GET("/user/query/{uuid}", accept(MediaType.APPLICATION_JSON), userHandler::queryByName)
        .POST("/user/add", accept(MediaType.APPLICATION_JSON_UTF8),userHandler::add)
        .PUT("/user/update", accept(MediaType.APPLICATION_JSON_UTF8),userHandler::update)
        .DELETE("/user/delete/{uuid}",accept(MediaType.APPLICATION_JSON_UTF8), userHandler::delete)
        .build();
        return routerFunction;
        }
        }

        将具体的请求路径和具体的handler做了映射,这样会根据用户具体的请求路径找具体的handler,其实就是具体的方法。和mvc的@RequestMapping功能上是一样的。但是这个需要注意的是返回的结果是ServerResponse,请求是ServerRequest,这个也可以和mvc的HttpServletRequest、HttpServletResponse对应起来,都是封装用户的请求信息,其实和mvc都还是能对应起来的,只是编程方式不太一样。

      2. 然后创建HandlerFunction

        @Slf4j
        @Component
        public class UserHandler {

        @Autowired
        private UserRepository userRepository;

        public Mono findAll(ServerRequest serverRequest) {
        Flux<User> flux = userRepository.findAll();
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(flux, User.class);
        }
        // 查询单个
        public Mono queryByUUID(ServerRequest serverRequest) {
        String uuid = serverRequest.pathVariable("uuid");
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userRepository.queryByUUID(uuid),User.class);
        }
        // 添加用户
        public Mono add(ServerRequest serverRequest) {
        // 将请求体转成指定Momo对象
        Mono<User> mono = serverRequest.bodyToMono(User.class);
        String uuid = generateUUID();
        // 方法2
        Mono<Object> safeUser = mono.doOnNext(u -> u.setUuid(uuid)).map(user -> {return userRepository.saveUser(user);});
        // User user = createUser(serverRequest);
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(safeUser,Object.class);
        //方法1
        // Mono<User> userMono = mono.doOnNext(u -> u.setUuid(uuid)).doOnSuccess(user -> userRepository.saveNoReturn(user));
        // return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(userMono,User.class);
        }
        private User createUser(ServerRequest serverRequest) {
        User user = new User();
        Optional<String> userId = serverRequest.queryParam("userId");
        Optional<String> userName = serverRequest.queryParam("userName");
        Optional<String> age = serverRequest.queryParam("age");
        Optional<String> sex = serverRequest.queryParam("sex");
        Optional<String> uuid = serverRequest.queryParam("uuid");
        if (userId.isPresent()) user.setUserId(userId.get());
        if (userName.isPresent()) user.setUserName(userName.get());
        if (age.isPresent()) user.setAge(Integer.valueOf(age.get()));
        if (sex.isPresent()) user.setSex(sex.get());
        if (uuid.isPresent()) {
        user.setUuid(uuid.get());
        } else {
        user.setUuid(generateUUID());
        }
        return user;
        }
        // 更新用户
        public Mono update(ServerRequest serverRequest) {
        Mono<User> mono = serverRequest.bodyToMono(User.class);
        User user = createUser(serverRequest);
        return ServerResponse.ok().body(userRepository.update(user),Boolean.class);
        }
        // 删除用户
        public Mono delete(ServerRequest serverRequest) {
        String uuid = serverRequest.pathVariable("uuid");
        return ServerResponse.ok().body(userRepository.delete(uuid),Long.class);
        }
        private String generateUUID() {
        return UUID.randomUUID().toString().replace("-","");
        }
        }
  4. Spring WebFlux应用程序不严格依赖于Servlet API,因此它们不能作为war文件部署,也不能使用src/main/webapp目录

  5. 可以整合多个模板引擎

    除了REST Web服务外,您还可以使用Spring WebFlux提供动态HTML内容。Spring WebFlux支持各种模板技术,包括Thymeleaf,FreeMarker。
    

14.3-SpringBoot2.x webflux实战

  1. WebFlux中,请求和响应不再是WebMVC中的ServletRequest和ServletResponse,而是ServerRequest和ServerResponse

  2. 加入依赖,如果同时存在spring-boot-starter-web,则会优先用spring-boot-starter-web

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
  3. 编写测试类UserController

    @RestController
    @RequestMapping("/api/v1/user")
    public class UserController {

    @GetMapping("/test")
    public Mono<String> test() {
    return Mono.just("hello webflux!");
    }

    }
  4. 启动方式默认是Netty,8080端口

    测试:localhost:8080/api/v1/user/test

14.4-WebFlux客户端WebClient

WebClient是一个响应式客户端,它提供了RestTemplate的替代方法。它公开了一个功能齐全、流畅的API,并依赖于非阻塞I / O,使其能够比RestTemplate更高效地支持高并发性。WebClient非常适合流式的传输方案,并且依赖于较低级别的HTTP客户端库来执行请求,是可插拔的。

与RestTemplate相比,WebClient是:

  • 非阻塞,Reactive的,并支持更高的并发性和更少的硬件资源。
  • 提供利用Java 8 lambdas的函数API。
  • 支持同步和异步方案。
  • 支持从服务器向上或向下流式传输。

RestTemplate不适合在非阻塞应用程序中使用,因此Spring WebFlux应用程序应始终使用WebClient。在大多数高并发场景中,WebClient也应该是Spring MVC中的首选,并且用于编写一系列远程,相互依赖的调用。

Reactive方法:

@Service
public class UserServiceImpl implements UserService {

private WebClient client = WebClient.create("http://localhost:8080");

public Mono<User> getUser(Long id){
return client.get()
.uri("/user/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(User.class);
}

}

15-SpringBoot2.0服务器端主动推送SSE技术

15.1-服务端推送常用技术介绍

  1. 客户端轮询:ajax定时拉取

    ajax长时间和服务端保持通讯太占内存

  2. 服务端主动推送:WebSocket

    全双工的,本质上是一个额外的tcp连接,建立和关闭时握手使用http协议,其他数据传输不使用http协议
       更加复杂一些,适用于需要进行复杂双向数据通讯的场景
    

    ​ websocket可以进行服务端和前端双向通讯,写法较为复杂

  3. 服务端主动推送:SSE (Server Send Event)

    html5新标准,用来从服务端实时推送数据到浏览器端,
       直接建立在当前http连接上,本质上是保持一个http长连接,轻量协议
       简单的服务器数据推送的场景,使用服务器推送事件    
       学习资料:http://www.w3school.com.cn/html5/html_5_serversentevents.asp
    

15.2-SpringBoot2.x服务端主动推送SSE

  1. 后端代码

    @RestController
    public class SSEController {
    //produces = "text/event-stream;charset=UTF-8"一定要带上
    @RequestMapping(value = "/get_data", produces = "text/event-stream;charset=UTF-8")
    public String push() {

    try {
    Thread.sleep(1000);
    }catch (Exception e) {
    e.printStackTrace();
    }

    double moeny = Math.random()*10;
    System.out.println(String.format("%.2f",moeny));
    DecimalFormat df = new DecimalFormat(".00");
    String price = df.format(moeny);
    //!!!注意,EventSource返回的参数必须以data:开头,"\n\n"结尾,不然onmessage方法无法执行。
    return "data:猪肉价格行情:" + price +"元"+ "\n\n";
    }
    }
  2. 前段代码

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    <script type="text/javascript">
    //需要判断浏览器支不支持,可以去w3c进行查看

    var source = new EventSource('/get_data');
    source.onmessage = function (event) {
    console.info(event.data);
    document.getElementById('result').innerText = event.data
    };
    </script>
    </head>

    <body>
    <div id="result"></div>
    </body>

    </html>

16-SpringBoot2.x监控Actuator

Spring Boot Actuatorspring boot项目一个监控模块,提供了很多原生的端点,包含了对应用系统的自省和监控的集成功能,可以查看应用配置的详细信息,比如应用程序上下文里全部的Bean健康指标环境变量各类重要度量指标等等,这些都是使用可HTTP进行请求访问。通过这些监控信息,我们就能随时了解应用的运行情况了。

Actuator 是 Spring Boot 提供的对应用系统的自省和监控功能。通过 Actuator,可以使用数据化的指标去度量应用的运行情况,比如查看服务器的磁盘、内存、CPU等信息,系统的线程、gc、运行状态等等。

Actuator 通常通过使用 HTTP 和 JMX 来管理和监控应用,大多数情况使用 HTTP 的方式。

  1. 添加依赖

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. 添加配置

    server:
    port: 8080
    servlet:
    context-path: /demo
    # actuator 监控配置
    management:
    #actuator端口 如果不配置做默认使用上面8080端口
    server:
    port: 9090
    endpoints:
    web:
    exposure:
    #默认值访问health,info端点 用*可以包含全部端点
    include: "*"
    #修改访问路径 2.0之前默认是/; 2.0默认是/actuator可以通过这个属性值修改
    base-path: /actuator

    配置完成启动项目后就可以通过postman或者直接在预览器输入路径等方式来查看应用的运行状态了。
    当项目启动时,访问[http://127.0.0.1:9090/actuator]地址

    注意:如果没有配置 actuator端口,采用默认访问地址:http://127.0.0.1:8080/demo/actuator

    如果看到类似下面的内容,说明actuator已经生效了

    {
    "_links": {
    "self": {
    "href": "http://127.0.0.1:9090/actuator",
    "templated": false
    },
    "auditevents": {
    "href": "http://127.0.0.1:9090/actuator/auditevents",
    "templated": false
    },
    "beans": {
    "href": "http://127.0.0.1:9090/actuator/beans",
    "templated": false
    },
    "caches-cache": {
    "href": "http://127.0.0.1:9090/actuator/caches/{cache}",
    "templated": true
    },
    "caches": {
    "href": "http://127.0.0.1:9090/actuator/caches",
    "templated": false
    },
    "health": {
    "href": "http://127.0.0.1:9090/actuator/health",
    "templated": false
    },
    "health-component": {
    "href": "http://127.0.0.1:9090/actuator/health/{component}",
    "templated": true
    },
    "health-component-instance": {
    "href": "http://127.0.0.1:9090/actuator/health/{component}/{instance}",
    "templated": true
    },
    "conditions": {
    "href": "http://127.0.0.1:9090/actuator/conditions",
    "templated": false
    },
    "configprops": {
    "href": "http://127.0.0.1:9090/actuator/configprops",
    "templated": false
    },
    "env": {
    "href": "http://127.0.0.1:9090/actuator/env",
    "templated": false
    },
    "env-toMatch": {
    "href": "http://127.0.0.1:9090/actuator/env/{toMatch}",
    "templated": true
    },
    "info": {
    "href": "http://127.0.0.1:9090/actuator/info",
    "templated": false
    },
    "loggers": {
    "href": "http://127.0.0.1:9090/actuator/loggers",
    "templated": false
    },
    "loggers-name": {
    "href": "http://127.0.0.1:9090/actuator/loggers/{name}",
    "templated": true
    },
    "heapdump": {
    "href": "http://127.0.0.1:9090/actuator/heapdump",
    "templated": false
    },
    "threaddump": {
    "href": "http://127.0.0.1:9090/actuator/threaddump",
    "templated": false
    },
    "metrics-requiredMetricName": {
    "href": "http://127.0.0.1:9090/actuator/metrics/{requiredMetricName}",
    "templated": true
    },
    "metrics": {
    "href": "http://127.0.0.1:9090/actuator/metrics",
    "templated": false
    },
    "scheduledtasks": {
    "href": "http://127.0.0.1:9090/actuator/scheduledtasks",
    "templated": false
    },
    "httptrace": {
    "href": "http://127.0.0.1:9090/actuator/httptrace",
    "templated": false
    },
    "mappings": {
    "href": "http://127.0.0.1:9090/actuator/mappings",
    "templated": false
    }
    }
    }
  3. 建议

    • 只能访问几个url

      原因:
      出于安全考虑,除/ health和/ info之外的所有执行器默认都是禁用的。 management.endpoints.web.exposure.include属性可用于启用执行器

    • 建议
      在设置management.endpoints.web.exposure.include之前,请确保暴露的执行器不包含敏感信息和/
      或通过将其放置在防火墙进行控制,不对外进行使用

      禁用的端点将从应用程序上下文中完全删除。如果您只想更改端点所暴露的技术,请改用 include和exclude属性
      例子:

      开启全部:`management.endpoints.web.exposure.include=*`
      开启某个:`management.endpoints.web.exposure.include=metrics`
      关闭某个:`management.endpoints.web.exposure.exclude=metrics`
      

      或者用springadmin进行管理

      相关资料:https://www.cnblogs.com/ityouknow/p/8440455.html
      

      或者用自己编写脚本监控

      CPU、内存、磁盘、nginx的http响应状态码200,404,5xx 
      
    • 介绍常用的几个

      `/health`     查看应用健康指标
        `/actuator/metrics`    查看应用基本指标列表
        `/actuator/metrics/{name}`        通过上述列表,查看具体 查看具体指标
        `/actuator/env`        显示来自Spring的 ConfigurableEnvironment的属性