spring boot增强性说明

spring boot热重启

  1. 安装devtools
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional>
</dependency>
  1. 设置IDEA自动构建工程
    打开settings–>Compiler–>Build project automatically

常见注解

1、@GetMapping,@PostMapping,@PutMapping,@DeleteMapping…

简化常见HTTP方法的隐射,并更好的表达被注解方法的语义

2、@RequestMapping

RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
RequestMapping注解有六个属性,下面我们把它分成三类进行说明。

  1. value,method:
    value:指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明)
    method:指定请求的method类型, GET、POST、PUT、DELETE等
    还有一个注意的,@RequestMapping的默认属性为value,所以@RequestMapping(value="/example")和@RequestMapping("/example")是等价的。
  2. consumes,produces:
    consumes:指定处理请求的提交内容类型(Content-Type),例如application/json, text/html
    produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回
  3. params,headers:
    params:指定request中必须包含某些参数值是,才让该方法处理
    headers:指定request中必须包含某些指定的header值,才能让该方法处理请求。

3、@RestController

@RestController相当于是@Controller + @ResponseBody

4、@ResponseBody

@ResponseBody这个注解通常使用在控制层(controller)的方法上,其作用是将方法的返回值以特定的格式(通常为json)写入到response的body区域,进而将数据返回给客户端。当方法上面没有写ResponseBody,底层会将方法的返回值封装为ModelAndView对象。

5、@Autowired

可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 通过 @Autowired的使用来消除 set ,get方法

成员变量注入

public class Test{@Autowiredprivate A a;
}

setter注入

public class Test{private A a;@Autowiredpublic void setA(A a){this.a = a;}
}

构造函数注入

public class Test{private A a;@Autowiredpublic Test(A a){this.a = a;}
}

被动推断注入方式

bytype(默认注入方式)

byname

  1. 找不到任何一个bean,直接报错
  2. 找到一个bean,直接注入
  3. 找到多个bean,并不一定会报错,会按照名字推断选择哪个bean

主动选择注入

使用@Qualifier指定注入一个bean

6、@Component

通过类路径扫描来自动侦测以及自动装配到Spring容器中

7、@Service

用在类上,注册为一个bean。用于标注业务层组件(通常定义的service层就用这个注解)

8、@Controller

用于标注控制器层组件(通常定义的controller层就用这个注解)

9、@Repository

用于标注存储层组件(通常定义的数据库层就用这个注解)

10、@Configuration

搭配@Bean注解,将一个bean加入到容器中。用来替代bean的xml配置

MySQL.java

public class MySQL implements IConnect{private String ip;private Integer port;public MySQL(String ip,Integer port){this.ip=ip;this.port=port;}@Overridepublic void connect(){}
}

IConnect.java

public interface IConnect{void connect();
}

DatabaseConfiguration.java

主要作用:读取配置文件、将MySQL的bean加入到容器中

@Configuration
public class DatabaseConfiguration{@Value("${mysql.ip}")private String ip;@Value("${mysql.port}")private Integer port;@Beanpublic IConnect mysql(){return new MySQL(this.ip,this.port);}
}

11、@ComponentScan

包扫描策略。SpringBoot默认的包扫描策略:从启动类所在包开始,扫描当前包及其子级包下的所有文件

常用参数含义

basePackages与value: 用于指定包的路径,进行扫描(默认参数)
basePackageClasses: 用于指定某个类的包的路径进行扫描
includeFilters: 包含的过滤条件
FilterType.ANNOTATION:按照注解过滤
FilterType.ASSIGNABLE_TYPE:按照给定的类型
FilterType.ASPECTJ:使用ASPECTJ表达式
FilterType.REGEX:正则
FilterType.CUSTOM:自定义规则
excludeFilters: 排除的过滤条件,用法和includeFilters一样
nameGenerator: bean的名称的生成器
useDefaultFilters: 是否开启对@Component,@Repository,@Service,@Controller的类进行检测

12、@Primary

提高bean的优先级。对于同一个接口,可能会有不同的实现类,默认就只会采取其中一种的情况,这个时候@Primary的作用就出来

13、@Conditional

自定义条件注解,基本使用举例:

MySQL.java

public class MySQL{private String ip;private Integer port;
}

DatabaseConfiguration.java

@Configuration
public class DatabaseConfiguration{@Bean@Conditional({MySQLCondition.class})public IConnect mysql(){return new MySQL();}
}

MySQLCondition.java

public class MySQLCondition implements Condition{@Overridepublic boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {return false;}
}

根据MySQLCondition中matches方法返回的boolean值判断是否将MySQL加入到容器中

ConditionContext接口定义

public interface ConditionContext {// 获取Bean定义BeanDefinitionRegistry getRegistry();// 获取Bean工程,因此就可以获取容器中的所有bean@NullableConfigurableListableBeanFactory getBeanFactory();// environment 持有所有的配置信息Environment getEnvironment();// 资源信息ResourceLoader getResourceLoader();// 类加载信息@NullableClassLoader getClassLoader();
}

14、ConditionalOnProperty

成品条件注解

通过其属性name、havingValue及matchIfMissing来实现的

其中name用来从配置文件中读取某个属性值。
如果该值为空,则返回false;
如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。

当matchIfMissing为true时,如果配置文件中不存在name填写的属性(注意不是属性值),则返回true。matchIfMissing默认值为false

如果返回值为false,则该configuration不生效;为true则生效。

15、常见成品条件注解

@ConditionOnProperty
@ConditionalOnBean  当SpringIoc容器内存在指定的Bean的条件
@ConditionalOnClass
@ConditionalOnExpression  基于SpEL表达式作为判断条件
@ConditionalOnJava  基于JVM版本作为判断条件
@ConditionalOnJndi  在JNDI存在时查找指定的位置
@ConditionalOnMissingBean  当SpringIoc容器内不存在指定Bean的条件
@ConditionalOnMissingClass  当SpringIoc容器内不存在指定Class的条件
@ConditionalOnNotWebApplication  当前项目不是Web项目的条件
@ConditionalOnProperty  指定的属性是否有指定的值
@ConditionalOnResource  类路径是否有指定的值
@ConditionalOnSingleCandidate   当指定Bean在SpringIoc容器内只有一个,或者虽然有多个但是指定首选的Bean
@ConditionalOnWebApplication  当前项目是Web项目的条件

16、@ControllerAdvice

通过@ControllerAdvice注解可以将对于控制器的全局配置放在同一个位置。

注解了@ControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。

@ExceptionHandler:用于全局处理控制器里的异常。
@InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。
@ModelAttribute:本来作用是绑定键值对到Model中,此处让全局的@RequestMapping都能获得在此处设置的键值对

@ControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。

17、@ExceptionHandler

@ExceptionHandler作用于方法上,用于统一处理方法抛出的异常。@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常

18、@ResponseStatus

修改Response的HTTP状态码。参数code为HttpStatus的枚举

19、@PropertySource

通过@PropertySource注解加载指定的配置文件。与@ConfigurationProperties两个注解的配合使用。
@PropertySource(value=“classpath:config/exception-code.properties”)

20、@ConfigurationProperties

设置读取配置文件的前缀信息,配合@PropertySource使用
@ConfigurationProperties(prefix = “lin”)

21、@PathVariable

通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中
URL 中的 {xxx} 占位符可以通过@PathVariable(name=“xxx“) 绑定到操作方法的入参中。

22、@RequestParam

用于将URL中查询参数赋值给方法中的形参。defaultValue可以设置默认值
通过@PathVariable(value=“xxx“) 绑定到操作方法的入参中。

23、lombok常用注解

  1. @Getter 生成getter函数
  2. @Setter 生成setter函数
  3. @NoArgsConstructor 生成无参构造函数
  4. @AllArgsConstructor 生成全参数构造函数
  5. @RequiredArgsConstructor 生成一个包含常量,和标识了@NotNull的变量的构造方法。生成的构造方法是私有的private
  6. @NotNull 被注释的元素不能为null
  7. @Builder 构造器模式。会自动生成private无参构造函数,如果没有添加public构造函数,则不能再通过new关键字创建对象

使用示例:

TestDTO.java

@Builder
class TestDTO{private String name;private Integer id;
}

TestController.java

class TestController{public void test(){TestDTO dto = TestDTO.builder().name("test").id(12).build();}
}

24、Hibernate-Validator常见参数校验注解

@Null   被注释的元素必须为 null
@NotNull    被注释的元素必须不为 null
@AssertTrue     被注释的元素必须为 true
@AssertFalse    被注释的元素必须为 false
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past   被注释的元素必须是一个过去的日期
@Future     被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式
Hibernate Validator 附加的 constraint
@NotBlank(message =)   验证字符串非null,且长度必须大于0
@Email  被注释的元素必须是电子邮箱地址
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内
@NotEmpty   被注释的字符串的必须非空
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

25、@Validated和@Valid

@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上

两者区别

@Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

建议:开启验证注解使用@Validated,级联校验使用@Valid

26、@Documented(元注解)

指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。

27、@Retention(元注解)

指明修饰的注解的生存周期,即会保留到哪个阶段

RetentionPolicy的取值包含以下三种:

SOURCE:源码级别保留,编译后即丢弃。
CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值。
RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用。

28、@Target(元注解)

指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里

ElementType的取值包含以下几种:

TYPE:类,接口或者枚举
FIELD:域,包含枚举常量
METHOD:方法
PARAMETER:参数
CONSTRUCTOR:构造方法
LOCAL_VARIABLE:局部变量
ANNOTATION_TYPE:注解类型
PACKAGE:包

29、@Constraint

指定自定义注解具体是那个关联类来进行验证,通过validatedBy指定

30、@RequestBody

用来接收前端传递给后端的json字符串中的数据的。在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个

31、JPA

  1. @Entity

表明该类 (UserEntity) 为一个实体类,它默认对应数据库中的表名是user_entity

  1. @Table

当实体类与其映射的数据库表名不同名时需要使用 @Table注解说明,该标注与 @Entity 注解并列使用

@Table注解的常用选项是 name,用于指明数据库的表名

@Table注解还有两个选项 catalog 和 schema 用于设置表所属的数据库目录或模式,通常为数据库名
3. @Column

定义了将成员属性映射到关系表中的哪一列和该列的结构信息

name:映射的列名。如:映射tbl_user表的name列,可以在name属性的上面或getName方法上面加入;
unique:是否唯一;
nullable:是否允许为空;
length:对于字符型列,length属性指定列的最大字符长度;
insertable:是否允许插入;
updatetable:是否允许更新;
columnDefinition:定义建表时创建此列的DDL;
secondaryTable:从表名。如果此列不建在主表上(默认是主表),该属性定义该列所在从表的名字
  1. @Id

指定表的主键

  1. @Transient

该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性. 如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,

  1. @GeneratedValue
    主要为一个实体生成一个唯一标识的主键(JPA要求每一个实体Entity,必须有且只有一个主键)@GeneratedValue提供了主键的生成策略。

strategy属性:

AUTO主键由程序控制, 是默认选项 ,不设置就是这个
IDENTITY 主键由数据库生成, 采用数据库自增长, Oracle不支持这种方式
SEQUENCE 通过数据库的序列产生主键, MYSQL  不支持
Table 提供特定的数据库产生主键, 该方式更有利于数据库的移植
  1. @OneToMany
    实现数据库中一对多关系,用于一方。参数:

mappedBy:用于双向关联时使用,否则会引起数据不一致的问题。指定多方(关系维护端)里面导航属性的名字

fetch:可取的值有FetchType.EAGER和FetchType.LAZY,前者表示主类被加载时加载,后者表示被访问时才会加载

cascade:CascadeType.PERSIST(级联新建)、CascadeType.REMOVE(级联删除)、CascadeType.REFRESH(级联刷新)、CascadeType.MERGE(级联更新)、CascadeType.ALL(选择全部)

  1. @JoinColumn
    与@Column用法相同,区别是@JoinColumn作用的属性必须是实体类。用于指定外键

  2. @ManyToMany
    表示此类是多对多关系的一边,mappedBy 属性定义了此类为双向关系的维护端,注意:mappedBy 属性的值为此关系的另一端的属性名。

  3. @JoinTable

用于声明多对多关系中第三方表

name                 指定该连接表的表名
JoinColumns         属性值可接受多个@JoinColumn,用于配置连接表中外键列的信息,这些外键列参照当前实体对应表的主键列
inverseJoinColumns  属性值可接受多个@JoinColumn,用于配置连接表中外键列的信息,这些外键列参照当前实体的关联实体对应表的主键列
targetEntity            属性指定关联实体的类名。在默认情况下,Hibernate将通过反射来判断关联实体的类名
catalog                 置将该连接表放入指定的catalog中。如果没有指定该属性,连接表将放入默认的catalog
schema                  置将该连接表放入指定的schema中。如果没有指定该属性,连接表将放入默认的schema
uniqueConstraints   属性用于为连接表增加唯一约束
indexes                 属性值为@Index注解数组,用于为该连接表定义多个索引
  1. @MappedSuperclass

    1. 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
    2. 标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。
  2. @JsonIgnore
    在实体类向前台返回数据时用来忽略不想传递给前台的属性或接口。

  3. @Where
    实现查询过滤,在实体类上、实体属性上、查询语句上都有应用。

  4. @Query
    JPQL,主要用于复杂查询,命名规则查询不太好是实现的时候

32、@SuppressWarnings

主要用在取消一些编译器产生的警告对代码左侧行列的遮挡,有时候这会挡住我们断点调试时打的断点。

关键字 用途
all to suppress all warnings (抑制所有警告)
boxing to suppress warnings relative to boxing/unboxing operations (抑制装箱、拆箱操作时候的警告)
cast to suppress warnings relative to cast operations (抑制映射相关的警告)
dep-ann to suppress warnings relative to deprecated annotation (抑制启用注释的警告)
deprecation to suppress warnings relative to deprecation (抑制过期方法警告)
fallthrough to suppress warnings relative to missing breaks in switch statements (抑制确在switch中缺失breaks的警告)
finally to suppress warnings relative to finally block that don’t return (抑制finally模块没有返回的警告)
hiding to suppress warnings relative to locals that hide variable(抑制相对于隐藏变量的局部变量的警告)
incomplete-switch to suppress warnings relative to missing entries in a switch statement (enum case)(忽略没有完整的switch语句)
nls to suppress warnings relative to non-nls string literals( 忽略非nls格式的字符)
null to suppress warnings relative to null analysis( 忽略对null的操作)
rawtypes to suppress warnings relative to un-specific types when using generics on class params( 使用generics时忽略没有指定相应的类型)
restriction to suppress warnings relative to usage of discouraged or forbidden references( 抑制禁止使用劝阻或禁止引用的警告)
serial to suppress warnings relative to missing serialVersionUID field for a serializable class( 忽略在serializable类中没有声明serialVersionUID变量)
static-access to suppress warnings relative to incorrect static access( 抑制不正确的静态访问方式警告)
synthetic-access to suppress warnings relative to unoptimized access from inner classes( 抑制子类没有按最优方法访问内部类的警告)
unchecked to suppress warnings relative to unchecked operations( 抑制没有进行类型检查操作的警告)
unqualified-field-access to suppress warnings relative to field access unqualified( 抑制没有权限访问的域的警告)
unused to suppress warnings relative to unused code( 抑制没被使用过的代码的警告)

33、@Converter

34、@Convert

可将不是基本数据类型的数据按照一定的格式转换成可存入数据库的基本类型,类似于自动拆装箱操作

35、@PostConstruct

用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。执行顺序:构造函数–>@Autowired–>@PostConstruct–>init()。

重点理论

  1. 单纯interface可以统一方法的调用,但是它不能统一对象的实例化
  2. 面向对象主要做两件事:实例化对象、调用方法(完成业务逻辑)
  3. 只有一段代码中没有new的出项,才能保持代码的相对稳定,才能逐步实现OCP
  4. 上面的这句话只是表象,实质是一段代码如果要保持稳定,就不应该负责对象的实例化
  5. 对象实例化是不可能消除的
  6. 把对象实例化的过程,转移到其他的代码片段里
  7. 代码总是会存在不稳定,隔离这些不稳定,保证其他的代码是稳定的
  8. 变化造成了不稳定
  9. 配置文件属于系统外部的,而不属于代码本身

面向对象中变化的应对方案

  1. 制定一个interface,然后用多个类实现同一个interface(策略模式)
  2. 只有一个类,通过修改属性来解决变化。(最好也指定一个interface,这个类去实现这个interface,预防变化)

为什么将变化隔离到配置文件中

  1. 配置文件的集中性
  2. 配置文件清晰,没有业务逻辑

策略模式的变化方案

  1. byname 通过切换bean的name
  2. 使用@Qualifier指定bean
  3. 有选择的只注入一个bean(注释掉某个bean上的@Component注解)
  4. 使用@Primary注解,提高某一个bean的优先级

IOC DI

DI 依赖注入(Dependency Injection)

组件依赖一个对象的时候,不再通过new关键字创建对象,而是由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台

常见注入方式:

  1. 属性注入(setter注入)
  2. 构造注入(构造函数注入)
  3. 接口注入

理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

谁依赖于谁:当然是应用程序依赖于IoC容器;
为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;
注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

有关(马丁 福勒 Martin Fowler)关于DI的解释

[英文版]https://martinfowler.com/articles/injection.html

[中文版]https://blog.csdn.net/weixin_34128501/article/details/93465956

IOC 控制反转(Inversion of Control)

IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

IOC具体实现:需要有一个容器,把对象加入到容器里面,还需要负责把容器里面的对象注入到代码片段中

IOC目的:

  1. IOC抽象意义:IOC将控制权交给用户
  2. 灵活的OCP

SpringBoot自动配置/装配

  1. 原理是什么
  2. 为什么要有自动装配

SpringBoot全局异常处理机制

异常分类

Throwable异常基类,所有错误或异常的超类

Error 错误,无法通过代码处理
Exception 异常

CheckedException 编译阶段异常(不存在具体的类,默认Exception为编译时异常)
RuntimeException 运行时异常

HTTP状态码汇总

HTTP状态码总的分为五类:

1开头:信息状态码
2开头:成功状态码
3开头:重定向状态码
4开头:客户端错误状态码
5开头:服务端错误状态码

1XX:信息状态码

状态码 含义 描述
100 继续 初始的请求已经接受,请客户端继续发送剩余部分
101 切换协议 请求这要求服务器切换协议,服务器已确定切换

2XX:成功状态码

状态码 含义 描述
200 成功 服务器已成功处理了请求
201 已创建 请求成功并且服务器创建了新的资源
202 已接受 服务器已接受请求,但尚未处理
203 非授权信息 服务器已成功处理请求,但返回的信息可能来自另一个来源
204 无内容 服务器成功处理了请求,但没有返回任何内容
205 重置内容 服务器处理成功,用户终端应重置文档视图
206 部分内容 服务器成功处理了部分GET请求

3XX:重定向状态码

状态码 含义 描述
300 多种选择 针对请求,服务器可执行多种操作
301 永久移动 请求的页面已永久跳转到新的url
302 临时移动 服务器目前从不同位置的网页响应请求,但请求仍继续使用原有位置来进行以后的请求
303 查看其他位置 请求者应当对不同的位置使用单独的GET请求来检索响应时,服务器返回此代码
304 未修改 自从上次请求后,请求的网页未修改过
305 使用代理 请求者只能使用代理访问请求的网页
307 临时重定向 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求

4XX:客户端错误状态码

状态码 含义 描述
400 错误请求 服务器不理解请求的语法
401 未授权 请求要求用户的身份演验证
403 禁止 服务器拒绝请求
404 未找到 服务器找不到请求的页面
405 方法禁用 禁用请求中指定的方法
406 不接受 无法使用请求的内容特性响应请求的页面
407 需要代理授权 请求需要代理的身份认证
408 请求超时 服务器等候请求时发生超时
409 冲突 服务器在完成请求时发生冲突
410 已删除 客户端请求的资源已经不存在
411 需要有效长度 服务器不接受不含有效长度表头字段的请求
412 未满足前提条件 服务器未满足请求者在请求中设置的其中一个前提条件
413 请求实体过大 由于请求实体过大,服务器无法处理,因此拒绝请求
414 请求url过长 请求的url过长,服务器无法处理
415 不支持格式 服务器无法处理请求中附带媒体格式
416 范围无效 客户端请求的范围无效
417 未满足期望 服务器无法满足请求表头字段要求

5XX:服务端错误状态码

状态码 含义 描述
500 服务器错误 服务器内部错误,无法完成请求
501 尚未实施 服务器不具备完成请求的功能
502 错误网关 服务器作为网关或代理出现错误
503 服务不可用 服务器目前无法使用
504 网关超时 网关或代理服务器,未及时获取请求
505 不支持版本 服务器不支持请求中使用的HTTP协议版本

UnifyResponse 统一错误相应

{"code": 10001,"message": "xxxx","request": "GET url"
}

异常信息配置文件

  1. 在resources/config目录下创建异常信息配置文件exception-code.properties
lin.codes[9999]=服务器异常
  1. 自定义配置类管理配置文件
    ExceptionCodeConfiguration
@PropertySource(value="classpath:config/exception-code.properties")
@ConfigurationProperties(prefix = "lin")
@Setter
@Getter
@Component
public class ExceptionCodeConfiguration{private Map<Integer,String> codes=new HashMap<>();public String getMessage(int code){String message = codes.get(code);return message;}
}

全局异常处理代码

捕获异常类

@ControllerAdvice
public class GlobalExceptionAdvice{@Autowiredprivate ExceptionCodeConfiguration codeConfiguration;/*捕获通用异常/未知异常(通过@ExceptionHandler注解的value值确定)*/@ExceptionHandler(value=Exception.class)@ResponseBody@ResponseStatus(code=HttpStatus.INTERNAL_SERVER_ERROR)public UnifyResponse handleException(HttpServletReques req,Exception e){String requestUrl = req.getRequestURL();String method = req.getMethod();System.out.println(e);//方便开发阶段调试return new UnifyResponse(9999,codeConfiguration.getMessage(9999),method+" "+requestUrl);}/*捕获自定义的HttpException异常*/@ExceptionHandler(HttpException.class)@ResponseBodypublic ResponseEntity<UnifyResponse> handleHttpException(HttpServletReques req,HttpException e){String requestUrl = req.getRequestURL();String method = req.getMethod();UnifyResponse message = new UnifyResponse(e.getCode(),codeConfiguration.getMessage(e.getCode()),method+" "+requestUrl);//设置消息体HttpHeaders headers = new HttpHeaders();//设置headerheaders.setContentType(MediaType.APPLICATION_JSON);HttpStatus httpStatus = HttpStatus.resolve(e.getHttpStatusCode());//用于设置HttpStatusCode。因为httpStatusCode不确定,所以无法使用@ResponseStatus注解ResponseEntity<UnifyResponse> r= new ResponseEntity<>(message,headers,httpStatus);return r;}/*捕获body参数校验异常*/@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(code=HttpStatus.HttpStatus.BAD_REQUEST)@ResponseBodypublic UnifyResponse handleBeanValidation(HttpServletReques req,MethodArgumentNotValidException e){String requestUrl = req.getRequestURL();String method = req.getMethod();// 获取所有的错误信息List<ObjectError> errors = e.getBindingResult().getAllErrors();String message = this.formatAllErrorMessages(errors);return new UnifyResponse(10001,message,method+" "+requestUrl);}/*捕获URL参数校验异常*/@ExceptionHandler(ConstraintViolationException.class)@ResponseStatus(code=HttpStatus.HttpStatus.BAD_REQUEST)@ResponseBodypublic UnifyResponse handleConstraintException(HttpServletReques req,ConstraintViolationException e){String requestUrl = req.getRequestURL();String method = req.getMethod();// 获取所有的错误信息// for(ConstraintViolation error: e.getContraintViolations()){//    // }String message = e.getMessage();return new UnifyResponse(10001,message,method+" "+requestUrl);}private String formatAllErrorMessages(List<ObjectError> errors){StringBuffer errorMsg = new StringBuffer();errorMsg.forEach(error->errorMsg.append(error.getDefaultMessage()).append(";"));return errorMsg.toString();}
}

Http异常基类

@Getter
public class HttpException extends RuntimeException{protected Integer code;protected Integer httpStatusCode = 500;
}

具体异常类

public class NotFoundException extends HttpException{public NotFoundException(int code){this.httpStatusCode = 404;this.code = code;}
}

UnifyResponse

@Getter
public class UnifyResponse{private int Code;private String message;private String request;public UnifyResponse(int code,String message,String request){this.code = code;this.message = message;this.request = request;}
}

根据目录结构自动生成路由前缀

//RequestMappingHandlerMapping处理带有@RequestMapping注解的controller
public class AutoPreUrlMapping extends RequestMappingHandlerMapping{private String apiPackagePath = "com.lin.missyou.api";// 应该将此值放到配置文件中//定义和生成请求的路由信息@Overrideprotected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {RequestMappingInfo mappingInfo = super.getMappingForMethod(method, handlerType);if (mappingInfo != null) {String prefix = this.getPrefix(handlerType);// prefix:  /v1return RequestMappingInfo.paths(prefix).build().combine(mappingInfo);// 将/v1加入到路由中}return null;}private String getPrefix(Class<?> handlerType) {String packageName = handlerType.getPackage().getName();// packageName:  com.lin.missyou.api.v1String dotPath = packageName.replaceAll(this.apiPackagePath, "");return dotPath.replace(".", "/");}
}

将AutoPrefixUrlMapping加入到容器中

@Component
public class AutoPrefixConfiguration implements WebMvcRegistrations {@Overridepublic RequestMappingHandlerMapping getRequestMappingHandlerMapping() {return new AutoPrefixUrlMapping();}
}

参数校验

获取HTTP请求的参数

  1. 接受URL路径中的参数(URL:http://localhost:8080/test/12)
class Test{@GetMapping("/test/{id}")public String test(@PathVariable(name="id") Integer id){// URL中的参数{id}命名和方法中参数命名一样,@PathVariable可以不指定name}
}
  1. 接受URL查询参数(URL:http://localhost:8080/test?id=12)
class Test{@GetMapping("/test")public String test(@RequestParam(value="id") Integer id){// URL中的参数{id}命名和方法中参数命名一样,@RequestParam可以不指定value}
}
  1. 接受Body里面的json参数

Body里面的json参数

{"id": 12,"name": "test"}
class TestController{@PostMapping("/test")public String test(@RequestBody TestDTO testDTO){}
}@Getter
@Setter
class TestDTO{private Integer id;private String name;
}

使用@Validated注解进行基础参数校验

JSR303校验的message模板配置

在resources下创建一个固定文件名为ValidationMessages.properties的文件

id.positive = id必须是正整数
// 模板参数。${validatedValue}表示用户传的真实参数值,{max}{min},自定义校验注解中设置的值
token.password = password不符合规范:当前值是${validatedValue},最大值{max},最小值{min}
@Validated
public class TestController{@GetMapping("/test/{id}")public void test(@PathVariable @Max(value=10,message="{id.positive}") Integer id){}
}

普通用法

@Validated
public class TestController{@GetMapping("/test/{id}")public void test(@PathVariable @Max(value=10,message="不能超过10") Integer id){}
}

验证HTTP Body中的参数与级联校验

例一:

@Validated
class TestController{@PostMapping("/test")public String test(@RequestBody @Validated TestDTO testDTO){}
}@Getter
@Setter
class TestDTO{private Integer id;@Length(max=10,min=2)private String name;
}

例二:

@Validated
class TestController{@PostMapping("/test")public String test(@RequestBody @Validated TestDTO testDTO){}
}@Getter
@Setter
class TestDTO{private Integer id;@Length(max=10,min=2)private String name;@Valid //使用@Valid注解实现级联校验private TestDemoDTO testDemo;
}@Getter
@Setter
class TestDemoDTO{@Length(max=10,min=2)private String name;
}

自定义校验注解

自定义注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Constraint(validateBy=PasswordValidator.class)
public @interface PasswordEqual{int min() default 4;int max() default 6;// 模板方法String message() default "passwords are not equal";//验证未通过时返回的消息Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}

关联类

public class PasswordValidator implements ConstraintValidator<PasswordEqual,PersonDTO>{// 第一个参数,自定义注解的类型// 第二个参数,自定义注解修饰的目标的类型private int min;private int max;// 验证方法(主方法)@Overridepublic boolean isValid(PersonDTO personDTO,ConstraintValidatorContext constraintValidatorContext){String password1 = personDTO.getPassword1();String password2 = personDTO.getPassword2();return password1.equals(password2);}// 可以获取到自定义注解里面的参数@Overridepublic void initialize(PasswordEqual constraintAnnotation){this.min = constraintAnnotation.min();this.max = constraintAnnotation.max();}
}

使用自定义PasswordEqual注解(PersonDTO.java)

@Getter
@PasswordEqual(min=2)
public class PersonDTO{String password1;String password2;
}

多环境配置文件

application.yml:生产环境和开发环境配置都会生效
application-dev.yml:开发环境配置生效
application-prod.yml:生产环节配置生效

在application.yml启用某个环境的配置生效(下面举例使开发环境配置生效):

spring:profiles:active: dev

JPA

创建数据表的3种主要方式

  1. 可视化管理工具(navicat,mysql workbench)
  2. 手写SQL语句
  3. 通过Model模型类创建

ORM生成数据库

@Entity
@Table(name = "banner")
public class Banner{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)private long id;@Column(length = 16)private String name;@Transient // 不做隐射,不会在表中生成该字段private String description;
}

配置jpa下hibernate->ddl-auto为update,然后运行springboot程序,则会在数据库中生成banner表

数据库连接

jdbc数据库连接

spring:datasource:url: jdbc:mysql://localhost:3306/sleeve?characterEncoding=utf-8&serverTimezone=GMT%2B8username: rootpassword: 123456

jpa配置

ddl-auto可选参数

create 启动时删数据库中的表,然后创建,退出时不删除数据表
create-drop 启动时删数据库中的表,然后创建,退出时删除数据表 如果表不存在报错
update 如果启动时表格式不一致则更新表,原有数据保留
validate 项目启动表结构进行校验 如果不一致则报错
spring:jpa:hibernate:ddl-auto: none

pom.xml安装依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>

逆向生成Entity

IDEA链接数据库

View–>Tool Windows–>Database–点击加号–>Data Source–>选择数据库(MYSQL)–>填写HOST、PORT、USERNAME、PASSWORD,确定则可以链接数据库

逆向生成Entity

先找到View下面的Persistence,如果没有找到通过如下方法添加

File–>Project Structure–>Modules–>右键点击工程名–>Add–>JPA–>Default JPA provider–>选择Hibernate

开始生成Entity

View–>Persistence–>右键点击工程名–>Generate persistence Mapping–>By Database Schema–>Choose Data Source选择数据源(IDEA链接的数据库)–>Package选择生成的Entity存放目录–>Database Schema Mapping勾选需要生成的Entity

简化实体字段

  1. 利用@Setter、@Getter替代方法
  2. 删除掉equals、hashCode方法
  3. 在主键上添加@Id注解
  4. 建议修改字段类型(int–>Long,Timestamp–>Date,byte–>Boolean)
  5. 添加数据库关联关系

提取BaseEntity基类

@Setter
@Getter
@MappedSuperclass // 表明这个类不是一个Entity,而是一个Entity的父类
public abstract class BaseEntity{@JsonIgnore // 序列化时不会被序列化 private Date createTime;@JsonIgnoreprivate Date updateTime;@JsonIgnoreprivate Date deleteTime;
}

数据库中create_time、update_time、delete_time

create_time默认值设置CURRENT_TIMESTAMP,创建记录时自动填写时间

update_time默认值设置CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,创建和修改时自动填写时间

delete_time默认值设置NULL,当删除记录时,填写删除时时间,既表示该记录被删除,也表示改记录删除的时间

Jaskson序列化库配置

spring:jackson:property-naming-strategy: SNAKE_CASE //返回的键以下划线方式返回serialization:WRITE_DATES_AS_TIMESTAMPS: true // 返回的时间以时间戳返回

一对多的实现

单向一对多

一方:

@Entity
public class Banner{@Idprivate Long id;@Column(length = 16)private String name;@OneToMany@JoinColumn(name="bannerId")private List<BannerItem> items;
}

多方:

@Entity
public class BannerItem{@Idprivate Long id;private String name;private Long bannerId;
}

双向一对多

一方(关系被维护端):

@Entity
public class Banner{@Idprivate Long id;@Column(length = 16)private String name;@OneToMany(mappedBy="banner")// 指定多方里面导航属性的名字private List<BannerItem> items;
}

多方(关系维护端):

@Entity
public class BannerItem{@Idprivate long id;private String name;private long bannerId;// 如果不指定@JoinColume注解中,insertable和updatable为false,那么就不能显式的指定外键bannerId。而是由JPA自动生成外键bannerId(不建议这种方式)@ManyToOne@JoinColumn(insertable=false,updatable=false,name="bannerId")private Banner banner;
}

多对多的实现

单向多对多

@Entity
public class Spu{@Idprivate Long id;private String name;
}@Entity
public class Theme{@Idprivate Long id;private String name;@ManyToMany@JoinTable(name="theme_spu",joinColumns=@JoinColumn(name="theme_id"),inverseJoinColumns=@JoinColumn(name="spu_id"))// 指定(第三方表表名,第三张表一个外键名称,第三张表另一个外键名称)private List<Spu> spuList;
}

双向多对多

// 关系被维护端
@Entity
public class Spu{@Idprivate Long id;private String name;@ManyToMany(mappedBy="spuList")private List<Theme> themeList;
}// 关系维护端
@Entity
public class Theme{@Idprivate Long id;private String name;@ManyToMany@JoinTable(name="theme_spu",joinColumns=@JoinColumn(name="theme_id"),inverseJoinColumns=@JoinColumn(name="spu_id"))// 指定(第三方表表名,第三张表一个外键名称,第三张表另一个外键名称)private List<Spu> spuList;
}

Repository定义

public interface BannerRepository extends JpaRepository<Banner,Long>{// 第一个参数,模型类。第二个参数主键类型    // 调用此方法,可以根据id查询banner模型Banner findOneById(Long id);
}

查询规则

Keyword Sample JPQL snippet
And findByLastnameAndFirstname where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstnameIs,findByFirstnameEquals where x.firstname = ?1
Between findByStartDateBetween where x.startDate between ?1 and ?2
LessThan findByAgeLessThan where x.age < ?1
LessThanEqual findByAgeLessThanEqual where x.age ⇐ ?1
GreaterThan findByAgeGreaterThan where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual where x.age >= ?1
After findByStartDateAfter where x.startDate > ?1
Before findByStartDateBefore where x.startDate < ?1
IsNull findByAgeIsNull where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull where x.age not null
Like findByFirstnameLike where x.firstname like ?1
NotLike findByFirstnameNotLike where x.firstname not like ?1
StartingWith findByFirstnameStartingWith where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc where x.age = ?1 order by x.lastname desc
Not findByLastnameNot where x.lastname <> ?1
In findByAgeIn(Collection ages) where x.age in ?1
NotIn findByAgeNotIn(Collectionage) where x.age not in ?1
TRUE findByActiveTrue() where x.active = true
FALSE findByActiveFalse() where x.active = false
IgnoreCase findByFirstnameIgnoreCase where UPPER(x.firstame) = UPPER(?1)

IDEA控制台显示执行的SQL语句

在dev配置文件设置

spring:jpa:properties:hibernate:show_sql: trueformat_sql: true

数据库设计及优化原则

数据库设计

不要把数据库当作表去思考,而应该把表当作模型或者实体。一张表对应面向对象中的一个对象

第一步:
从业务中找到业务对象(比如优惠券Coupon这个业务对象,就应该有coupon这张表)

第二步:
思考对象与对象之间的关系(通过外键建立联系)

一对一
一对多
多对多

第三步:细化设计
有哪些字段,字段限制,长度、小数点、唯一索引等

数据库优化

一个表的记录不能太多,处理方式:

  1. 建立索引
  2. 水平分表,拆为多张表记录

字段不能太多:

  1. 垂直分割,通过数据库一对一的关系,将一张表分为多张表

数据库的优化主要不是体现在数据库设计上,而是在查询方式上(比如尽量少用like查询)

简单粗暴方式:缓存,避免减少查询数据库

VO视图层对象

定义VO视图层

@Getter
@Setter
public class SpuSimplifyVO{private Long id;private String name;
}

使用

public class SpuController{public SpuSimplifyVO getSpu(){Spu spu = spuService.getSpu();SpuSimplifyVO vo  = new SpuSimplifyVO();BeanUtils.copyProperties(spu,vo);return vo;}
}

处理List属性拷贝

public class SpuController{public List<SpuSimplifyVO> getSpu(){Mapper mapper = DozerBeanMapperBuilder.buildDefault();List<SpuSimplifyVO> vos = new ArrayList<>();List<Spu> spuList = spuService.getListSpu();spuList.forEach(s->{SpuSimplifyVO vo = mapper.map(s, SpuSimplifyVO.class);vos.add(vo);});return vos;}
}

DozerBeanMapper:

DozerBeanMapper很好的一个对象转换的组件。它可以将一个对象递归拷贝到另外一个对象。既支持简单的对象映射,也支持复杂的对象映射。

<dependency><groupId>com.github.dozermapper</groupId><artifactId>dozer-core</artifactId><version>6.5.0</version>
</dependency>

服务端分页

分页参数

PC: 页码(page)、每条的条数(size)
移动端: 开始的位置(start)、一次获取的条数(count)

移动端参数的转换

public class CommonUtil{public static PageCounter convertToPageParameter(Integer start, Integer count){int pageNum = start / count;PageCounter pageCounter = PageCounter.builder().page(pageNum).count(count).build();return pageCounter;}
}@Getter
@Setter
@Builder
public class PageCounter{private Integer page;private Integer count;
}

JPA分页查询

@Service
public class SpuService{@AutowiredSpuRepository spuRepository;public Page<Spu> getPagingSpu(Integer pageNum, Integer size){Pageable page = PageRequest.of(pageNum, size, Sort.by("createTime").descending());// 构建Pageable查询规则return this.spuRepository.findAll(page);}
}

封装Paging分页对象

@Getter
@Setter
@NoArgsConstructor
public class Paging<T>{private Long total; // 总数量private Integer count; // 当前请求的数据应该有多少条private Integer page; // 页码private Integer totalPage; //总共有多少页private List<T> items; // 查询的数据public Paging(Page<T> pageT){this.initPageParameters(pageT);this.items = pageT.getContent();}void initPageParameters(Page<T> pageT){this.total = pageT.getTotalElements();this.count = pageT.getSize();this.page = pageT.getNumber();this.totalPage = pageT.getTotalPages();}
}

PagingDozer对象封装(分页属性拷贝)

public class PagingDozer<T, K> extends Paging{@SuppressWarnings("unchecked")public PagingDozer(Page<T> pageT, Class<K> classK){this.initPageParameters(pagetT);List<T> tList = pageT.getContent();Mapper mapper = DozerBeanMapperBuilder.buildDefault();List<K> voList = new ArrayList<>();tList.forEach(t->{K vo = mapper.map(t, classK);voList.add(vo);});this.setItems(voList);}
}

无限级分类的数据表设计

常用设计方式

在数据表中设计一个parent_id,用于表示此节点的父节点(存储父节点记录的ID值),这样就可以表达出整个分类。

缺点:
当需要查询一个节点下4、5级子节点的时候,或者查询一个节点的根节点,需要做多次查询

优点:
数据表设计简单,占用的数据库存储较小。当分类级数较小时,推荐此方法

路径表示法(类比http协议)

在数据表中设备一个path字段,用于表示从根节点到此节点完整的路径(比如:node1_id/node2_id/node3_id)

缺点:
牺牲了数据存储空间资源。需要用代码拼接查询的条件

优点:
这种设计比较灵活,不仅可以存储路径,还可以存储其他信息,比如存储节点的名称等(node1_id#name/node2_id#name/node3_id#name)

通用泛型类映射方案

在MYSQL8版本中,可以直接存储JSON类型数据。隐射到JPA的模型类中,通常是对应的String类型,返回到前端就会以字符串的形式存在,需要避免这种情况,还是应该按JSON格式的类型返回给前端

以下方案中,spu表中存在一个以JSON类型的spec字段

方案A,会失去面向对象中类的义务能力

如果Spec中除了基本的属性,还存在一些其他的业务方法,如果将spec转换为Map或者List,那么spec则会失去这些业务方法

单体JSON对象,用Map数据类型映射

@Entity
public class Spu{@Convert(converter = MapAndJson.class)private Map<String, Object> spec;
}
@Converter
public class MapAndJson implements AttributeConverter<Map<String, Object>, String>{@Autowiredprivate ObjectMapper mapper; // springboot自带的jackson序列化库@Overridepublic String convertToDatabaseColumn(Map<String, Object> stringObjectMap){try{return mapper.writeValueAsString(stringObjectMap);}catch (JsonProcessingException e){e.printStackTrace();throw new ServerErrorException(9999);}}@Overridepublic Map<String, Object> convertToEntityAttribute(String s){try{if(s == null){return null;}return mapper.readValue(s, HashMap.class);}catch (JsonProcessingException e){e.printStackTrace();throw new ServerErrorException(9999);}}
}

数组类型JSON,用List数据类型映射

@Entity
public class Spu{@Convert(converter = ListAndJson.class)private List<Object> spec;
}
@Converter
public class ListAndJson implements AttributeConverter<List<Object>, String>{@Autowiredprivate ObjectMapper mapper; // springboot自带的jackson序列化库@Overridepublic String convertToDatabaseColumn(List<Object> stringObject){try{return mapper.writeValueAsString(stringObject);}catch (JsonProcessingException e){e.printStackTrace();throw new ServerErrorException(9999);}}@Overridepublic List<Object> convertToEntityAttribute(String s){try{if(s == null){return null;}return mapper.readValue(s, List.class);}catch (JsonProcessingException e){e.printStackTrace();throw new ServerErrorException(9999);}}
}

方案B、泛型。解决方案A中失去Spec的业务能力,但是需要重写Getter和Setter

在模型类中,通过Getter和Setter重写完成对数据的序列化和反序列化

先定义一个Spec模型类,用于映射spu表中spec字段

@Getter
@Setter
public class Spec{private Long keyId;private String key;private Long valueId;private String value;
}
@Entity
public class Spu{private String spec;public List<Spec> getSpec(){if(this.spec == null){return (Collections).emptyList();}return GenericAndJson.jsonToList(this.spec, new TypeReference<List<Spec>>(){});}public void setSpec(List<Spec> specList){if(specList.isEmpty){return;}this.spec = GenericAndJson.objectToJson(specList);}
}
@Component
public class GenericAndJson{private static ObjectMapper mapper;// 静态成员变量不能直接注入,使用Setter方式注入@Autowiredpublic void setMapper(ObjectMapper mapper){GenericAndJson.mapper = mapper;}public static <T> String objectToJson(T o){try{return GenericAndJson.mapper.writeValueAsString(o);}catch (JsonProcessingException e){e.printStackTrace();throw new ServerErrorException(9999);}}// 单体Json反序列化public static <T> T jsonToObject(String s, Class<T> classT){if(s == null){return null;}try{return GenericAndJson.mapper.readValue(s, classT);}catch (JsonProcessingException e){e.printStackTrace();throw new ServerErrorException(9999);}}// 数组Json反序列化(将List<T>整体作为一个泛型T),可以兼容单体Json反序列化public static <T> T jsonToList(String s, TypeReference<T> tr){if(s == null){return null;}try{return GenericAndJson.mapper.readValue(s, tr);}catch (JsonProcessingException e){e.printStackTrace();throw new ServerErrorException(9999);}}// 数组Json反序列化(将List<T>中T作为泛型)public static <T> List<T> jsonToList(String s){if(s == null){return null;}try{// 泛型T没有被转换为Spec,而是被转换为默认的LinkedHashMapreturn GenericAndJson.mapper.readValue(s, new TypeReference<List<T>>(){});}catch (JsonProcessingException e){e.printStackTrace();throw new ServerErrorException(9999);}}
}

Java8中Stream详解

为什么需要Stream

Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。
Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。
同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。
通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。
所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

Stream使用详解

简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。

流的构造与转换

常见的几种方法

// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();

基本数值型

目前有三种对应的包装类型Stream:IntStream、LongStream、DoubleStream。
当然我们也可以用 Stream、Stream >、Stream,
但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。

IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);

流转换为其他数据结构

// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream.collect(Collectors.joining()).toString();

流的操作

map/flatMap

// 把所有的单词转换为大写
List<String> output = wordList.stream().
map(String::toUpperCase).
collect(Collectors.toList());
// flatMap 把 inputStream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。
Stream<List<Integer>> inputStream = Stream.of(Arrays.asList(1),Arrays.asList(2, 3),Arrays.asList(4, 5, 6));
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());

filter

// 留下偶数
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens =
Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

forEach

Stream.of(1,2,3).forEach(System.out::println);

findFirst

// 用于获取含有Stream中的第一个元素的Optional,如果Stream为空,则返回一个空的Optional
Optional<Integer> any = Stream.of(1, 2, 3, 4).findFirst();

reduce

// 字符串连接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值,返回Optional
Optional<Integer> sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = "ace"
String concat = Stream.of("a", "B", "c", "D", "e", "F").filter(x -> x.compareTo("Z") > 0).reduce("", String::concat);

limit/skip

// 返回 Stream 的前面 n 个元素
// 打印结果  1,2
Stream.of(1,2,3,4,5)
.limit(2)
.forEach(System.out::println);
// 滤掉原Stream中的前N个元素,返回剩下的元素所组成的新Stream
// 打印结果  3,4,5
Stream.of(1,2,3,4,5)
.skip(2)
.forEach(System.out::println);

sorted

1、sorted() 默认使用自然序排序, 其中的元素必须实现Comparable 接口
2、sorted(Comparator<? super T> comparator) :我们可以使用lambada 来创建一个Comparator 实例。可以按照升序或着降序来排序元素。

简单使用

Stream.of(5, 4, 3, 2, 1).sorted().forEach(System.out::println);// 打印结果1,2,3,4,5
@Getter
public class Student implements Comparable<Student> {private int id;private String name;private int age;public Student(int id, String name, int age) {this.id = id;this.name = name;this.age = age;}@Overridepublic int compareTo(Student o) {return name.compareTo(o.getName());}
}List<Student> list = new ArrayList<Student>();
list.add(new Student(1, "Mahesh", 12));
list.add(new Student(2, "Suresh", 15));
list.add(new Student(3, "Nilesh", 10));// 按照默认规则正序
List<Student> slist = list.stream().sorted().collect(Collectors.toList());// 按照默认规则反序
List<Student> slist = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());// 按照指定规则正序
List<Student> slist = list.stream().sorted(Comparator.comparing(Student::getAge)).collect(Collectors.toList());// 按照指定规则反序
List<Student> slist = list.stream().sorted(Comparator.comparing(Student::getAge).reversed()).collect(Collectors.toList());

min/max/distinct

Optional<Integer> min = Stream.of(1, 2, 3, 4, 5).min(Integer::compareTo);
System.out.println("min:" + min.get());// 打印结果:min:1
Optional<Integer> max = Stream.of(1, 2, 3, 4, 5).max(Integer::compareTo);
System.out.println("max:" + max.get());// 打印结果:max:5
// 去重
Stream.of(1,2,3,1,2,3).distinct().forEach(System.out::println); // 打印结果:1,2,3

match

allMatch:Stream 中全部元素符合传入的 predicate,返回 true
anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true
noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true
boolean allMatch = Stream.of(1, 2, 3, 4).allMatch(integer -> integer > 0);
System.out.println("allMatch: " + allMatch); // 打印结果:allMatch: trueboolean anyMatch = Stream.of(1, 2, 3, 4).anyMatch(integer -> integer > 3);
System.out.println("anyMatch: " + anyMatch); // 打印结果:anyMatch: true

Optional

  1. 简化代码
  2. 强制要求考虑空值的情况

创建Optional

// 创建空值Optional
Optional<String> empty = Optional.empty();
// 创建有值Optional,不能存在null
Optional<String> t1 = Optional.of("1");
// 创建有值Optional,可以存在null
Optional<String> t2 = Optional.ofNullable(null);

直接调用String s = t2.get()进行取值,在这里会直接报错,
如果不使用Optional,s会出项空值,然后返回到上层调用栈,
当函数调用栈变深,再出现空指针异常,问题很难以排查

使用Optional

// t2不为空时才会打印
t2.ifPresent(System.out::println);
// t2为空时,会给s赋值为默认值
String s = t2.orElse("默认值")
// t2为空时,会抛出一个异常
String s2 = t2.orElseThrow(RuntimeException::new);// orElse无论Optional是否为空都会执行B方法。orElseGet只有在Optional为空时才会执行B方法
Optional.of("A").orElse(B());
Optional.of("A").orElseGet(() -> B());

consumer、supplier、function、predicate

cousumer:有参数,没有返回值

stream = Stream.of("aaa", "bbb", "ccc", "ddd");
stream.forEach(System.out::println);

supplier:无参数,有返回值

Optional<String> t2 = Optional.ofNullable("A");
String s2 = t2.orElseThrow(RuntimeException::new);

function:有参数,有返回值

// map参数就是一个function
List<String> output = wordList.stream().
map(String::toUpperCase).
collect(Collectors.toList());

predicate:返回值是一个boolean值

Stream<Integer> stream = Stream.of(1, 23, 3, 4, 5, 56, 6, 6);
List<Integer> list = stream.filter(i -> i > 5).collect(Collectors.toList());

令牌与权限

权限、分组和用户的关系

用户不和权限有关系,分组才和权限有关系,用户必须属于一个分组

用户登录(微信登陆)

controller

@RequestMapping("/token")
@RestController
public class TokenController{@Autowiredprivate WxAuthenticationService wxAuthenticationService;@PostMappingpublic Map<String, String> getToken(@RequestBody @Validated TokenGetDTO userData){Map<String, String> map = new HashMap<>();String token = null;switch (userData.getLoginType()){case USER_WX:token = wxAuthenticationService.code2Session(userData.getAccount);break;case USER_Email:break;default:throw new NotFoundException(10003);}map.put("token", token);return map;}@PostMapping("/verify")public Map<String, Boolean> verify(@RequestBody TokenDTO token) {Map<String, Boolean> map = new HashMap<>();Boolean valid = JwtToken.verifyToken(token.getToken());map.put("is_valid", valid);return map;}}

service

@Service
public class WxAuthenticationService{@Autowiredprivate ObjectMapper mapper;@Autowiredprivate UserRepository userRepository;@Value("${wx.code2session}")private String code2SessionUrl;@Value("${wx.appid}")private String appid;@Value("${wx.appsecret}")private String appsecret;public String code2Session(String code){String url = MessageFormat.format(this.code2SessionUrl, this.appid, this.appsecret, code);RestTemplate rest = new RestTemplate();Map<String, Object> session = new HashMap<>();String sessionText = rest.getForObject(url, String.class);try{session = mapper.readValue(sessionText, Map.class);}catch (JsonProcessingException e){e.printStackTrace();}return this.registerUser(session);}private String registerUser(Map<String,Object> session){String openid = (String)session.get("openid");if(openid == null){throw new ParameterException(20004);}Optional<User> userOptional = userRepository.findByOpenid(openid);if(userOptional.isPresent()){return JwtToken.makeToken(userOptional.get().getId());}User user = User.builder().openid(openid).build();userRepository.save(user);Long uid = user.getId();return JwtToken.makeToken(userOptional.get().getId());}}

JWT,Auth0

安装Auth0

<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.8.1</version>
</dependency>
public class JwtToken{private static Integer defaultScope = 8;private static String jwtKey;@Value("${missyou.security.jwt-key}")public void setJwtKey(String jwyKey){JwtToken.jwtKey = jwyKey;}private static Integer expiredTimeIn;@Value("${missyou.security.expired-time-in}")public void setExpiredTimeIn(Integer expiredTimeIn){JwtToken.expiredTimeIn = expiredTimeIn;}public static Boolean verifyToken(String token) {try {Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);JWTVerifier verifier = JWT.require(algorithm).build();verifier.verify(token);} catch (JWTVerificationException e) {return false;}return true;}public static Optional<Map<String, Claim>> getClaims(String token){DecodedJWT decodedJWT;Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);JWTVerifier jwtVerifier = JWT.require(algorithm).build();try{decodedJWT = jwtVerifier.verify(token);}catch (JWTVerificationException e){return Optional.empty();}return Optional.of(decodedJWT.getClaims());}public static String makeToken(Long uid, Integer scope){return JwtToken.getToken(uid, scope);}public static String makeToken(Long uid){return JwtToken.getToken(uid, JwtToken.defaultScope);}private static String getToken(Long uid, Integer scope){Algorithm algorithm = Algorithm.HMAC256(JwtToken.jwtKey);Map<String, Date> map = JwtToken.calculateExpiredIssues();String token = JWT.create().withClaim("uid", uid).withClaim("scope", scope).withExpiresAt(map.get("expiredTime")).withIssuedAt(map.get("now")).sign(algorithm);return token;}private static Map<String, Date> calculateExpiredIssues(){Map<String, Date> map = new HashMap<>();Calendar calender = Calender.getInstance();Date now = calender.getTime();calender.add(Calendar.SECOND, JwtToken.expiredTimeIn);map.put("now", now);map.put("expiredTime", calender.getTime());return map;}
}

DTO,用户传参

@Getter
@Setter
public class TokenGetDTO{@NotBlank(message = "account不允许为空")private String account;@TokenPassword(max=30, message = "{token.password}")private String password;// 登录方式,LoginType为枚举private LoginType type;
}

LoginType枚举

public enum LoginType{USER_WX(0, "微信登陆"), USER_Email(1, "邮箱登陆");private Integer value;LoginType(Integer value, String description){this.value = value;}}

自定义校验注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD})
@Constraint(validatedBy = TokenPasswordValidator.class)
public @interface TokenPassword{String message() default "字段不符合要求";int min() default 6;int max() default 32;Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}public class TokenPasswordValidator implements ConstraintValidator<TokenPassword,String>{private Integer min;private Integer max;@Overridepublic void initialize(TokenPassword constraintAnnotation){this.min = constraintAnnotation.min();this.max = constraintAnnotation.max();}@Overridepublic boolean isValid(String s, ConstraintValidatorContext c){if(StringUtils.isEmpty(s)){return true;}return s.length() >= this.min && s.length() <= this.max;}
}

拦截

HTTP请求
filter->interceptor->aop->controller->aop->interceptor->filter

interceptor拦截

  1. 获取到请求的token
  2. 验证token
  3. 读取token中scope
  4. 读取API @ScopeLevel level
  5. 对比scope

@ScopeLevel注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ScopeLevel {int value() default 4;
}
public class PermissionInterceptor extends HandlerInterceptorAdapter {@Autowiredprivate UserService userService;public PermissionInterceptor() {super();}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Optional<ScopeLevel> scopeLevel = this.getScopeLevel(handler);if (!scopeLevel.isPresent()) {return true;}Optional<String> bearerToken = this.getTokenByRequest(request);bearerToken.orElseThrow(() -> new UnAuthenticatedException(10004));if (!bearerToken.get().startsWith("Bearer")) {throw new UnAuthenticatedException(10004);}String[] tokens = bearerToken.get().split(" ");if (!(tokens.length == 2)) {throw new UnAuthenticatedException(10004);}String token = tokens[1];Optional<Map<String, Claim>> optionalMap = JwtToken.getClaims(token);Map<String, Claim> map = optionalMap.orElseThrow(() -> new UnAuthenticatedException(10004));Boolean valid = this.hasPermission(scopeLevel.get(), map);if (valid) {this.setToThreadLocal(map);}return valid;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {super.postHandle(request, response, handler, modelAndView);}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {LocalUser.clear();super.afterCompletion(request, response, handler, ex);}private Optional<ScopeLevel> getScopeLevel(Object handler) {if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;ScopeLevel scopeLevel = handlerMethod.getMethod().getAnnotation(ScopeLevel.class);if (scopeLevel == null) {return Optional.empty();}return Optional.of(scopeLevel);}return Optional.empty();}private Optional<String> getTokenByRequest(HttpServletRequest request) {String token = request.getHeader("Authorization");if (StringUtils.isEmpty(token)) {return Optional.empty();}return Optional.of(token);}private Boolean hasPermission(ScopeLevel scopeLevel, Map<String, Claim> map) {Integer level = scopeLevel.value();Integer scope = map.get("scope").asInt();if (level > scope) {throw new ForbiddenException(10005);}return true;}private void setToThreadLocal(Map<String, Claim> map) {Long uid = map.get("uid").asLong();Integer scope = map.get("scope").asInt();User user = userService.getUserById(uid);LocalUser.set(user, scope);}
}

延迟消息队列(redis)

redis键空间通知(keyspace notification)

命令行实现键空间通知

1、 开启键空间通知
修改redis配置文件redis.conf中notify-keyspace-events参数,输入的参数中至少要有一个 K 或者 E

notify-keyspace-events 的参数可以是以下字符的任意组合

K  & 键空间通知,所有通知以 `__keyspace@<db>__` 为前缀  \\
E  & 键事件通知,所有通知以 `__keyevent@<db>__` 为前缀  \\
g  & DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知 \\
$  & 字符串命令的通知 \\
l  & 列表命令的通知 \\
s  & 集合命令的通知 \\
h  & 哈希命令的通知 \\
z  & 有序集合命令的通知 \\
x  & 过期事件:每当有过期键被删除时发送 \\
e  & 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送 \\
A  & 参数 g$lshzxe 的别名,即all

以notify-keyspace-events Ex为例,启动redis服务时,指定conf文件(redis-server redis.conf)

2、订阅事件

// 当有值过期时间到时,会发出一个通知
psubscribe __keyevent@0__:expired

3、 设置一个带过期时间的值

setex name 10 value

10秒钟过后会有一个带name事件的通知消息

springboot实现键空间通知

1、 安装redis依赖

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

2、 配置springboot链接redis

spring:redis:localhost: localhostport: 6379database: 7password:listen-pattern: __keyevent@7__:expired

3、 写入redis带过期时间的值

@Autowired
private StringRedisTemplate stringRedisTemplate;
private void sendToRedis(){try{stringRedisTemplate.opsForValue().set(key,value,timeout,TimeUnit.SECDNDS);}catch(Exception e){e.printStackTrace();}
}

4、 监听键空间通知

// 监听器
public class TopicMessageListener implements MessageListener{@Overridepublic void onMessage(Message message,byte[] bytes){// 当redis中有值过期,会触发这个函数byte[] body = message.getBody();byte[] channel = message.getChannel();String expiredKey = new String(body);String topic = new String(channel);}
}
// 将监听器加入到springboot容器中
@Configuration
public class MessageListenerConfiguration{@Value("${spring.redis.listen-pattern}")private String pattern;/**参数:redis链接信息*/@Beanpublic RedisMessageListenerContainer listenerContainer(RedisConnectionFactory redisConnection){RedisMessageListenerContainer container = new RedisMessageListenerContainer();// 建立redis连接container.setConnectionFacory(redisConnection);// 监听主题实例化Topic topic = new PatternTopic(this.pattern);// 添加监听器(监听器,监听主题)container.addMessageListener(new TopicMessageListener(),topic);return container;}
}

RocketMQ延迟消息队列

常见消息队列:RabbitMQ、RocketMQ、Kafka
RocketMQ默认延迟消息延迟时间级别,可通过conf/broker.conf修改

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

RocketMQ的启动与关闭

先进入RocketMQ安装目录下的bin目录

  1. 启动namesrv
sh mqnamesrv
后台启动
nohup sh mqnamesrv &
  1. 启动borker
sh mqbroker -n localhost:9876
后台启动
nohup sh mqbroker &
  1. 关闭
sh mqshutdown namesrv
sh mqshutdown broker

简单示例

  1. 生产者
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
  1. 消费者
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer

Spring Boot使用RocketMQ实现延迟消息队列

  1. 安装依赖
<dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-client</artifactId><version>4.7.0</version>
</dependency>
  1. 生产者
@Component
public class ProducerSchedule{private DefaultMQProducer producer;@Value("${rocketmq.producer.producer-group}")private String producerGroup;@Value("${rocketmq.namesrv-addr}")private String namesrvAddr;@PostConstructpublic void defaultMQProducer(){if(this.producer == null){this.producer = new DefaultMQProducer(this.producerGroup);this.producer.setNamesrvAddr(this.namesrvAddr);}try{this.producer.start();System.out.println("----producer start----");}catch (MQClientException e){e.printStackTrace();}}public String send(String topic, String messageText){Message message = new Message(topic, messageText.getBytes());message.setDelayTimeLevel(9);SendResult result = this.producer.send(message);return result.getMsgId();}
}
  1. 消费者
@Component
public class ConsumerSchedule implements CommandLineRunner{@Value("${rocketmq.producer.producer-group}")private String consumerGroup;@Value("${rocketmq.namesrv-addr}")private String namesrvAddr;public void messageListener(){DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(this.consumerGroup);consumer.setNamesrvAddr(this.namesrvAddr);consumer.subscribe("TopicTest", "*");consumer.setConsumeMessageBatchMaxSize(1);consumer.registerMessageListener((MessageListenerConcurrently)(messages, context) -> {for(Message message:messages){}return  ConsumeConcurrentlyStatus.CONSUME_SUCCESS;});consumer.start();}@Overridepublic void run(String... args)throws Exception{this.messageListener();}
}

Spring Boot打包部署

在pom.xml中添加插件

<groupId>com.example</groupId>
<artifactId>springboot-upload</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<!--注意把packaging标签改为jar,此标签也可不写,默认打包方式为jar。--><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><fork>true</fork></configuration></plugin></plugins>
</build>

IDEA插件打包

View–>Tool windows–>Maven–>双击package–>等待Build Success即可

maven命令行打包

cd到根目录(pom.xml同级)

执行打包命令 mvn clean package (跳过测试类命令 mvn clean package -Dmaven.test.skip=true)

启动项目

java -jar springboot-upload-0.0.1-SNAPSHOT.jar

后台运行:nohup java -jar springboot-upload-0.0.1-SNAPSHOT.jar &

选择读取不同配置文件:java -jar springboot-upload-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev

spring boot增强性学习记录相关推荐

  1. spring boot要如何学习?

    spring boot要如何学习? 链接:https://www.zhihu.com/question/53729800/answer/255785661 . 推荐以 Spring Boot 教程与 ...

  2. Spring Boot Log4j2 日志学习

    简介 Java 中比较常用的日志工具类,有: Log4j. SLF4j. Commons-logging(简称jcl). Logback. Log4j2(Log4j 升级版). Jdk Logging ...

  3. 基于Spring Boot配置文件的日志记录示例样本

    我们希望在Spring Boot中为不同的配置文件使用不同的日志记录配置,例如在本地运行中,我们只希望控制台日志记录和用于生产,我们希望文件记录日志支持每天滚动日志文件. 我想出了一个示例logbac ...

  4. 基于Spring Boot Profile的日志记录示例样本

    我们希望在Spring Boot中为不同的配置文件使用不同的日志记录配置,例如在本地运行中,我们只希望控制台日志记录和用于生产,我们希望文件记录日志支持每天滚动日志文件. 我提出了一个示例logbac ...

  5. Spring Boot+Vue项目学习总结

    介绍 最近要做一个网站项目,前后端都用什么开发好呢?什么火就用什么呗,后端Spring Boot火,就用Spring Boot:而前端Vue.js很好的实现了前后端分离,多么高大上,就用Vue了.可问 ...

  6. Spring Boot 整合 WebSocket 使用记录

    这里写自定义目录标题 前言 WebSocket 简介 WebSocket 客户端(javascript前端)实现 javascript 实现 window.location获取URL中各部分 http ...

  7. 【Spring Boot】Spring Boot Logging 示例 | 日志记录

    文章目录 logging.level | 设置日志级别 logging.file | 指定输出日志文件的路径和名称 logging.path | 指定输出日志文件的路径 logging.pattern ...

  8. spring boot +vue用什么记录登录状态_为什么很多Spring Boot开发者放弃了Tomcat

    前言 在 Spring Boot 框架中,我们使用最多的是 Tomcat,这是 Spring Boot 默认的容器技术,而且是内嵌式的 Tomcat.同时,Spring Boot 也支持 Undert ...

  9. 【Spring Boot 2.0学习之旅-15】SpringBoot2.0响应式编程

    SpringBoot2.0响应式编程 一.SpringBoot2.0 响应式编程基础知识 Spring WebFlux官方文档 SpringBoot WebFlux文档 1.什么是Spring Web ...

最新文章

  1. (转)I 帧和 IDR 帧的区别
  2. Facebook加入AI芯片大战,挖走Google芯片产品开发负责人
  3. 【裴蜀定理】[HAOI2011]向量
  4. 函数嵌套 lisp表达式求值
  5. js时间搓化为今天明天_打乒乓球的搓球技巧!你掌握了吗?
  6. php 字符串去html,PHP strip_tags() 去字符串中的 HTML、XML 以及 PHP 标签的函数
  7. Qt for Python使用Qt中的Properties
  8. java调用sap接口_(二)通过JAVA调用SAP接口 (增加一二级参数)
  9. python 科学计算设计_用Python做科学计算 高清晰PDF
  10. [Ext JS 4] Grid 组件
  11. NumPy学习笔记之zeros_like()函数(包含zeros函数)
  12. vmware之VMware Remote Console (VMRC) SDK(三)
  13. 疯狂讲义java_《疯狂Java讲义》 1-概述
  14. Java链表——插入和删除
  15. python怎样定义数组_终于知道python如何定义二维数组
  16. cox回归模型python实现_生存分析Cox回归模型(比例风险模型)的spss操作实例
  17. 基于百度短语音API的语音识别实现
  18. 阿里云服务器ECS的建站完整过程
  19. wind 10 安装node环境
  20. 非拜占庭容错共识算法

热门文章

  1. js文件报错:Uncaught TypeError: Cannot read property 'split' of null
  2. 关于谢尔宾斯基三角形(Sierpinski triangle)的讲解
  3. 关于大型网站技术演进的思考(十五)--网站静态化处理—前后端分离—中(7)...
  4. svs文件转换为tiff文件
  5. 吃透nginx 403 forbidden报错
  6. 高等教育心理学:学生情感与意志的发展
  7. Java远程DEBUG调试教程
  8. 比亚迪“财大气粗”,弹指挥间收购6座锂矿,为何如此看好电车?
  9. window40系统怎么重装不下服务器,Win10系统异常不想重装怎么办 四种系统修复方法哪种比较好...
  10. 2023最新PHP表白情侣恋爱表白纪念单+UI非常美观