spring boot增强性说明
spring boot热重启
- 安装devtools
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
- 设置IDEA自动构建工程
打开settings–>Compiler–>Build project automatically
常见注解
1、@GetMapping,@PostMapping,@PutMapping,@DeleteMapping…
简化常见HTTP方法的隐射,并更好的表达被注解方法的语义
2、@RequestMapping
RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
RequestMapping注解有六个属性,下面我们把它分成三类进行说明。
- value,method:
value:指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明)
method:指定请求的method类型, GET、POST、PUT、DELETE等
还有一个注意的,@RequestMapping的默认属性为value,所以@RequestMapping(value="/example")和@RequestMapping("/example")是等价的。 - consumes,produces:
consumes:指定处理请求的提交内容类型(Content-Type),例如application/json, text/html
produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回 - 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{
@Autowired
private A a;
}
setter注入
public class Test{
private A a;
@Autowired
public void setA(A a){
this.a = a;
}
}
构造函数注入
public class Test{
private A a;
@Autowired
public Test(A a){
this.a = a;
}
}
被动推断注入方式
bytype(默认注入方式)
byname
- 找不到任何一个bean,直接报错
- 找到一个bean,直接注入
- 找到多个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;
}
@Override
public 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;
@Bean
public 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{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return false;
}
}
根据MySQLCondition中matches方法返回的boolean值判断是否将MySQL加入到容器中
ConditionContext接口定义
public interface ConditionContext {
// 获取Bean定义
BeanDefinitionRegistry getRegistry();
// 获取Bean工程,因此就可以获取容器中的所有bean
@Nullable
ConfigurableListableBeanFactory getBeanFactory();
// environment 持有所有的配置信息
Environment getEnvironment();
// 资源信息
ResourceLoader getResourceLoader();
// 类加载信息
@Nullable
ClassLoader 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常用注解
- @Getter 生成getter函数
- @Setter 生成setter函数
- @NoArgsConstructor 生成无参构造函数
- @AllArgsConstructor 生成全参数构造函数
- @RequiredArgsConstructor 生成一个包含常量,和标识了@NotNull的变量的构造方法。生成的构造方法是私有的private
- @NotNull 被注释的元素不能为null
- @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
- @Entity
表明该类 (UserEntity) 为一个实体类,它默认对应数据库中的表名是user_entity
- @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:从表名。如果此列不建在主表上(默认是主表),该属性定义该列所在从表的名字
- @Id
指定表的主键
- @Transient
该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性. 如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,
- @GeneratedValue
主要为一个实体生成一个唯一标识的主键(JPA要求每一个实体Entity,必须有且只有一个主键)@GeneratedValue提供了主键的生成策略。
strategy属性:
AUTO主键由程序控制, 是默认选项 ,不设置就是这个
IDENTITY 主键由数据库生成, 采用数据库自增长, Oracle不支持这种方式
SEQUENCE 通过数据库的序列产生主键, MYSQL 不支持
Table 提供特定的数据库产生主键, 该方式更有利于数据库的移植
- @OneToMany
实现数据库中一对多关系,用于一方。参数:
mappedBy:用于双向关联时使用,否则会引起数据不一致的问题。指定多方(关系维护端)里面导航属性的名字
fetch:可取的值有FetchType.EAGER和FetchType.LAZY,前者表示主类被加载时加载,后者表示被访问时才会加载
cascade:CascadeType.PERSIST(级联新建)、CascadeType.REMOVE(级联删除)、CascadeType.REFRESH(级联刷新)、CascadeType.MERGE(级联更新)、CascadeType.ALL(选择全部)
-
@JoinColumn
与@Column用法相同,区别是@JoinColumn作用的属性必须是实体类。用于指定外键 -
@ManyToMany
表示此类是多对多关系的一边,mappedBy 属性定义了此类为双向关系的维护端,注意:mappedBy 属性的值为此关系的另一端的属性名。 -
@JoinTable
用于声明多对多关系中第三方表
name 指定该连接表的表名
JoinColumns 属性值可接受多个@JoinColumn,用于配置连接表中外键列的信息,这些外键列参照当前实体对应表的主键列
inverseJoinColumns 属性值可接受多个@JoinColumn,用于配置连接表中外键列的信息,这些外键列参照当前实体的关联实体对应表的主键列
targetEntity 属性指定关联实体的类名。在默认情况下,Hibernate将通过反射来判断关联实体的类名
catalog 置将该连接表放入指定的catalog中。如果没有指定该属性,连接表将放入默认的catalog
schema 置将该连接表放入指定的schema中。如果没有指定该属性,连接表将放入默认的schema
uniqueConstraints 属性用于为连接表增加唯一约束
indexes 属性值为@Index注解数组,用于为该连接表定义多个索引
-
@MappedSuperclass
- 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
- 标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。
-
@JsonIgnore
在实体类向前台返回数据时用来忽略不想传递给前台的属性或接口。 -
@Where
实现查询过滤,在实体类上、实体属性上、查询语句上都有应用。 -
@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()。
重点理论
- 单纯interface可以统一方法的调用,但是它不能统一对象的实例化
- 面向对象主要做两件事:实例化对象、调用方法(完成业务逻辑)
- 只有一段代码中没有new的出项,才能保持代码的相对稳定,才能逐步实现OCP
- 上面的这句话只是表象,实质是一段代码如果要保持稳定,就不应该负责对象的实例化
- 对象实例化是不可能消除的
- 把对象实例化的过程,转移到其他的代码片段里
- 代码总是会存在不稳定,隔离这些不稳定,保证其他的代码是稳定的
- 变化造成了不稳定
- 配置文件属于系统外部的,而不属于代码本身
面向对象中变化的应对方案
- 制定一个interface,然后用多个类实现同一个interface(策略模式)
- 只有一个类,通过修改属性来解决变化。(最好也指定一个interface,这个类去实现这个interface,预防变化)
为什么将变化隔离到配置文件中
- 配置文件的集中性
- 配置文件清晰,没有业务逻辑
策略模式的变化方案
- byname 通过切换bean的name
- 使用@Qualifier指定bean
- 有选择的只注入一个bean(注释掉某个bean上的@Component注解)
- 使用@Primary注解,提高某一个bean的优先级
IOC DI
DI 依赖注入(Dependency Injection)
组件依赖一个对象的时候,不再通过new关键字创建对象,而是由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台
常见注入方式:
- 属性注入(setter注入)
- 构造注入(构造函数注入)
- 接口注入
理解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目的:
- IOC抽象意义:IOC将控制权交给用户
- 灵活的OCP
SpringBoot自动配置/装配
- 原理是什么
- 为什么要有自动装配
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"
}
异常信息配置文件
- 在resources/config目录下创建异常信息配置文件exception-code.properties
lin.codes[9999]=服务器异常
- 自定义配置类管理配置文件
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{
@Autowired
private 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)
@ResponseBody
public 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();//设置header
headers.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)
@ResponseBody
public 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)
@ResponseBody
public 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";// 应该将此值放到配置文件中
//定义和生成请求的路由信息
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo mappingInfo = super.getMappingForMethod(method, handlerType);
if (mappingInfo != null) {
String prefix = this.getPrefix(handlerType);// prefix: /v1
return RequestMappingInfo.paths(prefix).build().combine(mappingInfo);// 将/v1加入到路由中
}
return null;
}
private String getPrefix(Class<?> handlerType) {
String packageName = handlerType.getPackage().getName();// packageName: com.lin.missyou.api.v1
String dotPath = packageName.replaceAll(this.apiPackagePath, "");
return dotPath.replace(".", "/");
}
}
将AutoPrefixUrlMapping加入到容器中
@Component
public class AutoPrefixConfiguration implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new AutoPrefixUrlMapping();
}
}
参数校验
获取HTTP请求的参数
- 接受URL路径中的参数(URL:http://localhost:8080/test/12)
class Test{
@GetMapping("/test/{id}")
public String test(@PathVariable(name="id") Integer id){
// URL中的参数{id}命名和方法中参数命名一样,@PathVariable可以不指定name
}
}
- 接受URL查询参数(URL:http://localhost:8080/test?id=12)
class Test{
@GetMapping("/test")
public String test(@RequestParam(value="id") Integer id){
// URL中的参数{id}命名和方法中参数命名一样,@RequestParam可以不指定value
}
}
- 接受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;
// 验证方法(主方法)
@Override
public boolean isValid(PersonDTO personDTO,ConstraintValidatorContext constraintValidatorContext){
String password1 = personDTO.getPassword1();
String password2 = personDTO.getPassword2();
return password1.equals(password2);
}
// 可以获取到自定义注解里面的参数
@Override
public 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种主要方式
- 可视化管理工具(navicat,mysql workbench)
- 手写SQL语句
- 通过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%2B8
username: root
password: 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
简化实体字段
- 利用@Setter、@Getter替代方法
- 删除掉equals、hashCode方法
- 在主键上添加@Id注解
- 建议修改字段类型(int–>Long,Timestamp–>Date,byte–>Boolean)
- 添加数据库关联关系
提取BaseEntity基类
@Setter
@Getter
@MappedSuperclass // 表明这个类不是一个Entity,而是一个Entity的父类
public abstract class BaseEntity{
@JsonIgnore // 序列化时不会被序列化
private Date createTime;
@JsonIgnore
private Date updateTime;
@JsonIgnore
private 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{
@Id
private Long id;
@Column(length = 16)
private String name;
@OneToMany
@JoinColumn(name="bannerId")
private List<BannerItem> items;
}
多方:
@Entity
public class BannerItem{
@Id
private Long id;
private String name;
private Long bannerId;
}
双向一对多
一方(关系被维护端):
@Entity
public class Banner{
@Id
private Long id;
@Column(length = 16)
private String name;
@OneToMany(mappedBy="banner")// 指定多方里面导航属性的名字
private List<BannerItem> items;
}
多方(关系维护端):
@Entity
public class BannerItem{
@Id
private 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{
@Id
private Long id;
private String name;
}
@Entity
public class Theme{
@Id
private 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{
@Id
private Long id;
private String name;
@ManyToMany(mappedBy="spuList")
private List<Theme> themeList;
}
// 关系维护端
@Entity
public class Theme{
@Id
private 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: true
format_sql: true
数据库设计及优化原则
数据库设计
不要把数据库当作表去思考,而应该把表当作模型或者实体。一张表对应面向对象中的一个对象
第一步:
从业务中找到业务对象(比如优惠券Coupon这个业务对象,就应该有coupon这张表)
第二步:
思考对象与对象之间的关系(通过外键建立联系)
一对一
一对多
多对多
第三步:细化设计
有哪些字段,字段限制,长度、小数点、唯一索引等
数据库优化
一个表的记录不能太多,处理方式:
- 建立索引
- 水平分表,拆为多张表记录
字段不能太多:
- 垂直分割,通过数据库一对一的关系,将一张表分为多张表
数据库的优化主要不是体现在数据库设计上,而是在查询方式上(比如尽量少用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{
@Autowired
SpuRepository 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>{
@Autowired
private ObjectMapper mapper; // springboot自带的jackson序列化库
@Override
public String convertToDatabaseColumn(Map<String, Object> stringObjectMap){
try{
return mapper.writeValueAsString(stringObjectMap);
}catch (JsonProcessingException e){
e.printStackTrace();
throw new ServerErrorException(9999);
}
}
@Override
public 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>{
@Autowired
private ObjectMapper mapper; // springboot自带的jackson序列化库
@Override
public String convertToDatabaseColumn(List<Object> stringObject){
try{
return mapper.writeValueAsString(stringObject);
}catch (JsonProcessingException e){
e.printStackTrace();
throw new ServerErrorException(9999);
}
}
@Override
public 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方式注入
@Autowired
public 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,而是被转换为默认的LinkedHashMap
return 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;
}
@Override
public 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: true
boolean anyMatch = Stream.of(1, 2, 3, 4)
.anyMatch(integer -> integer > 3);
System.out.println("anyMatch: " + anyMatch); // 打印结果:anyMatch: true
Optional
- 简化代码
- 强制要求考虑空值的情况
创建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{
@Autowired
private WxAuthenticationService wxAuthenticationService;
@PostMapping
public 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{
@Autowired
private ObjectMapper mapper;
@Autowired
private 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;
@Override
public void initialize(TokenPassword constraintAnnotation){
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}
@Override
public 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拦截
- 获取到请求的token
- 验证token
- 读取token中scope
- 读取API @ScopeLevel level
- 对比scope
@ScopeLevel注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ScopeLevel {
int value() default 4;
}
public class PermissionInterceptor extends HandlerInterceptorAdapter {
@Autowired
private UserService userService;
public PermissionInterceptor() {
super();
}
@Override
public 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;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
super.postHandle(request, response, handler, modelAndView);
}
@Override
public 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: localhost
port: 6379
database: 7
password:
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{
@Override
public 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链接信息
*/
@Bean
public 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目录
- 启动namesrv
sh mqnamesrv
后台启动
nohup sh mqnamesrv &
- 启动borker
sh mqbroker -n localhost:9876
后台启动
nohup sh mqbroker &
- 关闭
sh mqshutdown namesrv
sh mqshutdown broker
简单示例
- 生产者
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
- 消费者
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
Spring Boot使用RocketMQ实现延迟消息队列
- 安装依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.0</version>
</dependency>
- 生产者
@Component
public class ProducerSchedule{
private DefaultMQProducer producer;
@Value("${rocketmq.producer.producer-group}")
private String producerGroup;
@Value("${rocketmq.namesrv-addr}")
private String namesrvAddr;
@PostConstruct
public 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();
}
}
- 消费者
@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();
}
@Override
public 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