升级成功后的源码地址:
https://github.com/foxiswho/java-spring-boot-uid-generator-baidu
这里的升级,是升级 官方 代码依赖
官方代码地址:https://github.com/baidu/uid-generator
导入官网数据库SQL https://github.com/baidu/uid-generator/blob/master/src/main/scripts/WORKER_NODE.sql
也就是一张表
我这里是在 demo
库中,创建了这张表
DROP TABLE IF EXISTS WORKER_NODE;
CREATE TABLE WORKER_NODE
(
ID BIGINT NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',
HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name',
PORT VARCHAR(64) NOT NULL COMMENT 'port',
TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',
LAUNCH_DATE DATE NOT NULL COMMENT 'launch date',
MODIFIED TIMESTAMP NOT NULL COMMENT 'modified time',
CREATED TIMESTAMP NOT NULL COMMENT 'created time',
PRIMARY KEY(ID)
)
COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB;
如果报错,基本上是时间问题,因为mysql 低版本控制比较严格,解决方法有多种方式
方式一:
直接把TIMESTAMP改成DATETIME 即可
方式二:
执行SQL 语句前先执行:
set sql_mode="NO_ENGINE_SUBSTITUTION";
因为升级到8.x ,配置文件部分也要跟着修改 uid-generator
下,测试文件夹下的资源包 uid/mysql.properties
以下修改为
mysql.driver=com.mysql.cj.jdbc.Driver
修改完成后,配置好数据库相关参数,这样单元测试即可执行成功
计划将全局生成唯一ID作为一个服务提供者,供其他微服务使用调用
这里创建了一个项目,项目中包含两个子项目一个是 uid-generator
官方本身,当然你也可以不需要放到本项目中,直接使用官方的自行打包即可,一个是 uid-provider
服务提供者
以下说明的主要是服务提供者
如何创建 略
POM配置文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>java-spring-boot-uid-generator-baidu</artifactId>
<groupId>com.foxwho.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>uid-provider</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--for Mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.12</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.foxwho.demo</groupId>
<artifactId>uid-generator</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
先在 uid-provider
项目资源包路径下创建 mapper
文件夹,然后到官方 uid-generator
资源包路径下 META-INF/mybatis/mapper/WORKER_NODE.xml
复制 WORKER_NODE.xml
文件,粘贴到该文件夹 mapper
内
先在 uid-provider
项目资源包路径下创建 uid
文件夹,然后到官方 uid-generator
测试 [注意:这里是测试资源包] 资源包路径下 uid/cached-uid-spring.xml
复制 cached-uid-spring.xml
文件,粘贴到该文件夹 uid
内
最后根据需要 配置参数,可以看官方说明
主要就是加上注解 @MapperScan("com.baidu.fsg.uid")
让 mybatis
能扫描到 Mapper
类的包的路径
package com.foxwho.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
@MapperScan("com.baidu.fsg.uid")
public class ConsumerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConsumerApplication.class).run(args);
}
}
package com.foxwho.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource(locations = { "classpath:uid/cached-uid-spring.xml" })
public class UidConfig {
}
package com.foxwho.demo.service;
import com.baidu.fsg.uid.UidGenerator;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UidGenService {
@Resource(name = "cachedUidGenerator")
private UidGenerator uidGenerator;
public long getUid() {
return uidGenerator.getUID();
}
}
主要说明一下 @Resource(name = "cachedUidGenerator")
以往错误都是少了这里,没有标明注入来源
package com.foxwho.demo.controller;
import com.foxwho.demo.service.UidGenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UidController {
@Autowired
private UidGenService uidGenService;
@GetMapping("/uidGenerator")
public String UidGenerator() {
return String.valueOf(uidGenService.getUid());
}
@GetMapping("/")
public String index() {
return "index";
}
}
server.port=8080
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
从启动入口,启动,然后访问浏览器
http://localhost:8080/uidGenerator
页面输出
13128615512260612
]]>controller 默认是单例的,不要使用非静态的成员变量,否则会发生数据逻辑混乱。正因为单例所以不是线程安全的。
我们下面来简单的验证下:
package com.riemann.springbootdemo.controller;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author riemann
* @date 2019/07/29 22:56
*/
@Controller
public class ScopeTestController {
private int num = 0;
@RequestMapping("/testScope")
public void testScope() {
System.out.println(++num);
}
@RequestMapping("/testScope2")
public void testScope2() {
System.out.println(++num);
}
}
我们首先访问 http://localhost:8080/testScope
,得到的答案是1;
然后我们再访问 http://localhost:8080/testScope2
,得到的答案是 2。
得到的不同的值,这是线程不安全的。
接下来我们再来给 controller 增加作用多例 @Scope("prototype")
package com.riemann.springbootdemo.controller;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author riemann
* @date 2019/07/29 22:56
*/
@Controller
@Scope("prototype")
public class ScopeTestController {
private int num = 0;
@RequestMapping("/testScope")
public void testScope() {
System.out.println(++num);
}
@RequestMapping("/testScope2")
public void testScope2() {
System.out.println(++num);
}
}
我们依旧首先访问 http://localhost:8080/testScope
,得到的答案是1;
然后我们再访问 http://localhost:8080/testScope2
,得到的答案还是 1。
相信大家不难发现 :
单例是不安全的,会导致属性重复使用。
1、不要在 controller 中定义成员变量。
2、万一必须要定义一个非静态成员变量时候,则通过注解@Scope(“prototype”)
,将其设置为多例模式。
3、在 Controller 中使用 ThreadLocal
变量
spring bean作用域有以下5个:
1、singleton:单例模式,当 spring 创建 applicationContex t容器的时候,spring 会欲初始化所有的该作用域实例,加上 lazy-init 就可以避免预处理;
2、prototype:原型模式,每次通过 getBean 获取该 bean 就会新产生一个实例,创建后 spring 将不再对其管理;
下面是在web项目下才用到的
3、request:搞 web 的大家都应该明白 request 的域了吧,就是每次请求都新产生一个实例,和 prototype 不同就是创建后,接下来的管理,spring 依然在监听;
4、session:每次会话,同上;
5、global session:全局的 web 域,类似于 servlet 中的 application。
原文地址:https://blog.csdn.net/riemann_/article/details/97698560
]]>@Autowire
private JdbcTemplate jdbcTemplate;
提示的警告信息
Field injection is not recommended Inspection info: Spring Team recommends: "Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies".
这段是Spring工作组的建议,大致翻译一下:
属性字段注入的方式不推荐,检查到的问题是:Spring团队建议:"始终在bean中使用基于构造函数的依赖项注入,
始终对强制性依赖项使用断言
如图:Field注入警告
虽然当前有关Spring Framework(5.0.3)的文档仅定义了两种主要的注入类型,但实际上有三种:
public class UserServiceImpl implents UserService{
private UserDao userDao;
@Autowire
public UserServiceImpl(UserDao userDao){
this.userDao = userDao;
}
}
public class UserServiceImpl implents UserService{
private UserDao userDao;
@Autowire
public serUserDao(UserDao userDao){
this.userDao = userDao;
}
}
public class UserServiceImpl implents UserService{
@Autowire
private UserDao userDao;
}
基于字段的依赖注入方式会在Idea当中吃到黄牌警告,但是这种使用方式使用的也最广泛,因为简洁方便.您甚至可以在一些Spring指南中看到这种注入方法,尽管在文档中不建议这样做.(有点执法犯法的感觉)
如图
1、对于有final修饰的变量不好使
Spring的IOC对待属性的注入使用的是set形式,但是final类型的变量在调用class的构造函数的这个过程当中就得初始化完成,这个是基于字段的依赖注入做不到的地方.只能使用基于构造函数的依赖注入的方式
2、掩盖单一职责的设计思想
我们都知道在OOP的设计当中有一个单一职责思想,如果你采用的是基于构造函数的依赖注入的方式来使用Spring的IOC的时候,当你注入的太多的时候,这个构造方法的参数就会很庞大,类似于下面.当你看到这个类的构造方法那么多参数的时候,你自然而然的会想一下:这个类是不是违反了单一职责思想?.但是使用基于字段的依赖注入不会让你察觉,你会很沉浸在@Autowire当中
public class VerifyServiceImpl implents VerifyService{
private AccountService accountService;
private UserService userService;
private IDService idService;
private RoleService roleService;
private PermissionService permissionService;
private EnterpriseService enterpriseService;
private EmployeeService employService;
private TaskService taskService;
private RedisService redisService;
private MQService mqService;
public SystemLogDto(AccountService accountService,
UserService userService,
IDService idService,
RoleService roleService,
PermissionService permissionService,
EnterpriseService enterpriseService,
EmployeeService employService,
TaskService taskService,
RedisService redisService,
MQService mqService) {
this.accountService = accountService;
this.userService = userService;
this.idService = idService;
this.roleService = roleService;
this.permissionService = permissionService;
this.enterpriseService = enterpriseService;
this.employService = employService;
this.taskService = taskService;
this.redisService = redisService;
this.mqService = mqService;
}
}
3、与Spring的IOC机制紧密耦合
当你使用基于字段的依赖注入方式的时候,确实可以省略构造方法和setter这些个模板类型的方法,但是,你把控制权全给Spring的IOC了,别的类想重新设置下你的某个注入属性,没法处理(当然反射可以做到).本身Spring的目的就是解藕和依赖反转,结果通过再次与类注入器(在本例中为Spring)耦合,失去了通过自动装配类字段而实现的对类的解耦,从而使类在Spring容器之外无效.
4、隐藏依赖性
当你使用Spring的IOC的时候,被注入的类应当使用一些public类型(构造方法,和setter类型方法)的方法来向外界表达:我需要什么依赖.但是基于字段的依赖注入的方式,基本都是private形式的,private把属性都给封印到class当中了.
5、无法对注入的属性进行安检
基于字段的依赖注入方式,你在程序启动的时候无法拿到这个类,只有在真正的业务使用的时候才会拿到,一般情况下,这个注入的都是非null的,万一要是null怎么办,在业务处理的时候错误才爆出来,时间有点晚了,如果在启动的时候就暴露出来,那么bug就可以很快得到修复(当然你可以加注解校验).如果你想在属性注入的时候,想根据这个注入的对象操作点东西,你无法办到.我碰到过的例子:一些配置信息啊,有些人总是会配错误,等到了自己测试业务阶段才知道配错了,例如线程初始个数不小心配置成了3000,机器真的是狂叫啊!这个时候就需要再某些Value注入的时候做一个检测机制.
通过上面,我们可以看到,基于字段的依赖注入方式有很多缺点,我们应当避免使用基于字段的依赖注入.推荐的方法是使用基于构造函数和基于setter的依赖注入.对于必需的依赖项,建议使用基于构造函数的注入,以使它们成为不可变的,并防止它们为null。对于可选的依赖项,建议使用基于Setter的注入
翻译自 field-injection-is-not-recommended,加入了自己的白话理解!
]]>做web开发有一点很烦人就是要校验参数,基本上每个接口都要对参数进行校验,比如一些格式校验 非空校验都是必不可少的。如果参数比较少的话还是容易 处理的一但参数比较多了的话代码中就会出现大量的 IF ELSE
就比如下面这样:
这个例子只是校验了一下空参数。如果需要验证邮箱格式和手机号格式校验的话代码会更多,所以介绍一下 validator
通过注解的方式进行校验参数。
<!--版本自行控制,这里只是简单举例-->
<dependency>
<groupId>javax. validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.0. Final</version>
</ dependency>
<dependency>
<groupId>org. hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1. Final</vers ion>
</dependency>
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(value) | 被注释的元素必须符合指定的正则表达式 |
Hibernate Validator
附加的 constraint
注解 | 详细信息 |
---|---|
@Email | 被注释的元素必须是电子邮箱地址 |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
@NotBlank | 验证字符串非null,且长度必须大于0 |
注意:
@NotNull
适用于任何类型被注解的元素必须不能与NULL@NotEmpty
适用于String Map或者数组不能为Null且长度必须大于0@NotBlank
只能用于String上面 不能为null,调用trim()后,长度必须大于0模拟用户注册封装了一个 UserDTO
当提交数据的时候如果使用以前的做法就是 IF ELSE
判断参数使用 validator
则是需要增加注解即可。
例如非空校验:
然后需要在 controller
方法体添加 @Validated
不加 @Validated
校验会不起作用
然后请求一下请求接口,把 Email
参数设置为空
参数:
{
"userName":"luomengsun",
"mobileNo":"11111111111",
"sex":1,
"age":21,
"email":""
}
返回结果:
后台抛出异常
这样是能校验成功,但是有个问题就是返回参数并不理想,前端也并不容易处理返回参数,所以我们添加一下全局异常处理,然后添加一下全局统一返回参数这样比较规范。
创建一个 GlobalExceptionHandler
类,在类上方添加 @RestControllerAdvice
注解然后添加以下代码:
/**
* 方法参数校验
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ReturnVO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error(e.getMessage(), e);
return new ReturnVO().error(e.getBindingResult().getFieldError().getDefaultMessage());
}
此方法主要捕捉 MethodArgumentNotValidException
异常然后对异常结果进行封装,如果需要在自行添加其他异常处理。
添加完之后我们在看一下运行结果,调用接口返回:
{
"code": "9999",
"desc": "邮箱不能为空",
"data": null
}
OK 已经对异常进行处理。
如果想要校验邮箱格式或者手机号的话也非常简单。
/**
* 邮箱
*/
@NotBlank(message = "邮箱不能为空")
@NotNull(message = "邮箱不能为空")
@Email(message = "邮箱格式错误")
private String email;
校验手机号使用正则进行校验,然后限制了一下位数
/**
* 手机号
*/
@NotNull(message = "手机号不能为空")
@NotBlank(message = "手机号不能为空")
@Pattern(regexp ="^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
@Max(value = 11,message = "手机号只能为{max}位")
@Min(value = 11,message = "手机号只能为{min}位")
private String mobileNo;
查看一下运行结果
传入参数:
{
"userName":"luomengsun",
"mobileNo":"111111a",
"sex":1,
"age":21,
"email":"1212121"
}
返回结果:
{
"code": "9999",
"desc": "邮箱格式错误",
"data": null
}
上面的注解只有这么多,如果有特殊校验的参数我们可以使用 Validator
自定义注解进行校验
首先创建一个 IdCard
注解类
@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdCardValidator.class)
public @interface IdCard {
String message() default "身份证号码不合法";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
在 UserDTO
中添加 @IdCard
注解即可验证,在运行时触发,本文不对自定义注解做过多的解释,下篇文章介绍自定义注解
然后添加 IdCardValidator
主要进行验证逻辑
上面调用了 is18ByteIdCardComplex
方法,传入参数就是手机号,验证身份证规则自行百度
然后使用
@NotNull(message = "身份证号不能为空")
@IdCard(message = "身份证不合法")
private String IdCardNumber;
就比如上面我们定义的 UserDTO
中的参数如果要服用的话怎么办?
在重新定义一个类然后里面的参数要重新添加注解?
Validator
提供了分组方法完美了解决 DTO
服用问题
现在我们注册的接口修改一下规则,只有用户名不能为空其他参数都不进行校验
先创建分组的接口
public interface Create extends Default {
}
我们只需要在注解加入分组参数即可例如:
/**
* 用户名
*/
@NotBlank(message = "用户姓名不能为空",groups = Create.class)
@NotNull(message = "用户姓名不能为空",groups = Create.class)
private String userName;
@NotBlank(message = "邮箱不能为空",groups = Update.class)
@NotNull(message = "邮箱不能为空",groups = Update.class)
@Email(message = "邮箱格式错误",groups = Update.class)
private String email;
然后在修改Controller在@Validated中传入Create.class
@PostMapping("/user")
public ReturnVO userRegistra(@RequestBody @Validated(Create.class) UserDTO userDTO){
ReturnVO returnVO = userService.userRegistra(userDTO);
return returnVO ;
}
然后调用传入参数:
{
"userName":"",
}
返回参数:
{
"code": "9999",
"desc": "用户姓名不能为空",
"data": null
}
OK 现在只对Create的进行校验,而 Updata
组的不校验,如果需要复用 DTO
的话可以使用分组校验
在开发的时候一定遇到过单个参数的情况,在参数前面加上注解即可
@PostMapping("/get")
public ReturnVO getUserInfo(@RequestParam("userId") @NotNull(message = "用户ID不能为空") String userId){
return new ReturnVO().success();
}
然后在 Controller
类上面增加 @Validated
注解,注意不是增加在参数前面。
]]>作者:孙罗蒙
AsyncTaskExecutePool
@EnableAsync
@Configuration
public class AsyncTaskExecutePool {
//核心线程池大小
private final int corePoolSize = 10;
//最大线程数
private final int maxPoolSize = 15;
//队列容量
private final int queueCapacity = 50;
//活跃时间/秒
private final int keepAliveSeconds = 60;
@Bean
public Executor myAsyncTaskPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池大小
executor.setCorePoolSize(corePoolSize);
//最大线程数
executor.setMaxPoolSize(maxPoolSize);
//队列容量
executor.setQueueCapacity(queueCapacity);
//活跃时间
executor.setKeepAliveSeconds(keepAliveSeconds);
//设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
executor.setWaitForTasksToCompleteOnShutdown(true);
//线程名字前缀
executor.setThreadNamePrefix("my-async1--");
// setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务
// CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
AsyncTask
@Component
@Slf4j
public class AsyncTask {
/**
* myAsyncTaskPool 线程池的方法名,此处如果不写,会使用Spring默认的线程池
* @param i
*/
@Async("myAsyncTaskPool")
public void run(int i){
log.info("我是:" + i);
}
}
AppTests
@SpringBootTest
class AppTests {
@Autowired
private AsyncTask asyncTask;
@Test
void test(){
for (int i = 0; i < 100; i++) {
asyncTask.run(i);
}
}
}
运行查看效果
第二种方式是重写 spring
默认线程池,使用这种方式的好处是可以直接使用 @Async
注解
AsyncTaskExecutePool1
并且实现AsyncConfigurer
类@Slf4j
@EnableAsync
@Configuration
public class AsyncTaskExecutePool1 implements AsyncConfigurer {
//核心线程池大小
private final int corePoolSize = 10;
//最大线程数
private final int maxPoolSize = 15;
//队列容量
private final int queueCapacity = 50;
//活跃时间/秒
private final int keepAliveSeconds = 60;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池大小
executor.setCorePoolSize(corePoolSize);
//最大线程数
executor.setMaxPoolSize(maxPoolSize);
//队列容量
executor.setQueueCapacity(queueCapacity);
//活跃时间
executor.setKeepAliveSeconds(keepAliveSeconds);
//设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
executor.setWaitForTasksToCompleteOnShutdown(true);
//线程名字前缀
executor.setThreadNamePrefix("my-async-");
// setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务
// CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
/**
* 异步任务异常处理
* @return
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
log.error("===="+throwable.getMessage()+"====", throwable);
log.error("exception method:"+method.getName());
};
}
}
AsyncTask
类,在类中加入方法run1
@Async
public void run1(int i){
log.info("我是:" + i);
}
AppTests
中加入方法test1
@Test
void test1(){
for (int i = 0; i < 100; i++) {
asyncTask.run1(i);
}
}
运行查看效果
@Async
的方法是不生效的在RESTful设计规范内,每一个接口被认为是一个资源请求,下面我们针对每一种资源类型来看下API路径设计。
路径设计的注意事项如下所示:
请求方式 | 示例路径 |
---|---|
POST | https://api.yuqiyu.com/v1/users |
新增资源使用POST方式来定义接口,新增资源数据通过RequestBody方式进行传递,如下所示:
curl -X POST -H 'Content-Type: application/json' https://api.yuqiyu.com/v1/users -d '{
"name": "恒宇少年",
"age": 25,
"address": "山东济南"
}'
新增资源后接口应该返回该资源的唯一标识,比如:主键值。
{
"id" : 1,
"name" : "恒宇少年"
}
通过返回的唯一标识来操作该资源的其他数据接口。
请求方式 | 示例路径 | 备注 |
---|---|---|
DELETE | https://api.yuqiyu.com/v1/users | 批量删除资源 |
DELETE | https://api.yuqiyu.com/v1/users/{id} | 删除单个资源 |
删除资源使用DELETE方式来定义接口。
根据主键值删除单个资源
curl -X DELETE https://api.yuqiyu.com/v1/users/1
将资源的主键值通过路径的方式传递给接口。
删除多个资源
curl -X DELETE -H 'Content-Type: application/json' https://api.yuqiyu.com/v1/users -d '{
"userIds": [
1,
2,
3
]
}'
删除多个资源时通过RequestBody方式进行传递删除条件的数据列表,上面示例中通过资源的主键值集合作为删除条件,当然也可以通过资源的其他元素作为删除的条件,比如:name
请求方式 | 示例路径 | 备注 |
---|---|---|
PUT | https://api.yuqiyu.com/v1/users/{id} | 更新单个资源的全部元素 |
PATCH | https://api.yuqiyu.com/v1/users/{id} | 更新单个资源的部分元素 |
在更新资源数据时使用PUT方式比较多,也是比较常见的,如下所示:
curl -X PUT -H 'Content-Type: application/json' https://api.yuqiyu.com/v1/users/1 -d '{
"name": "恒宇少年",
"age": 25,
"address": "山东济南"
}'
请求方式 | 示例路径 | 备注 |
---|---|---|
GET | https://api.yuqiyu.com/v1/users/{id} | 查询单个资源 |
GET | https://api.yuqiyu.com/v1/users?name={name} | 非唯一标识查询资源 |
唯一标识查询单个资源
curl https://api.yuqiyu.com/v1/users/1
通过唯一标识查询资源时,使用路径方式传递标识值,体现出层级关系。
非唯一标识查询单个资源
curl https://api.yuqiyu.com/v1/users?name=恒宇少年
查询资源数据时不仅仅都是通过唯一标识值作为查询条件,也可能会使用资源对象内的某一个元素作为查询条件。
请求方式 | 示例路径 |
---|---|
GET | https://api.yuqiyu.com/v1/users?page=1&size=20 |
分页查询资源时,我们一般需要传递两个参数作为分页的条件,page代表了当前分页的页码,size则代表了每页查询的资源数量。
curl https://api.yuqiyu.com/v1/users?page=1&size=20
如果分页时需要传递查询条件,可以继续追加请求参数。
https://api.yuqiyu.com/v1/users?page=1&size=20&name=恒宇少年
有时我们需要有动作性的修改某一个资源的元素内容,比如:重置密码。
请求方式 | 示例路径 | 备注 |
---|---|---|
POST | https://api.yuqiyu.com/v1/users/{id}/actions/forget-password | - |
用户的唯一标识在请求路径中进行传递,而修改后的密码通过RequestBody方式进行传递,如下所示:
curl -X POST -H 'Content-Type: application/json' https://api.yuqiyu.com/v1/users/1/actions/forget-password -d '{
"newPassword": "123456"
}'
版本号是用于区分Api接口的新老标准,比较流行的分别是接口路径、头信息这两种方式传递。
我们在部署接口时约定不同版本的请求使用HTTP代理转发到对应版本的接口网关,常用的请求转发代理比如使用:Nginx等。
这种方式存在一个弊端,如果多个版本同时将请求转发到同一个网关时,会导致具体版本的请求转发失败,我们访问v1时可能会转发到v2,这并不是我们期望的结果,当然可以在网关添加一层拦截器,通过提取路径上班的版本号来进行控制转发。
# v1版本的请求
curl https://api.yuqiyu.com/v1/users/1
# v2版本的请求
curl https://api.yuqiyu.com/v2/users/1
我们可以将访问的接口版本通过HttpHeader的方式进行传递,在网关根据提取到的头信息进行控制转发到对应版本的服务,这种方式资源路径的展现形式不会因为版本的不同而变化。
# v1版本的请求
curl -H 'Accept-Version:v1' https://api.yuqiyu.com/users/1
# v2版本的请求
curl -H 'Access-Version: v2' https://api.yuqiyu.com/users/1
这两个版本的请求可能请求参数、返回值都不一样,但是请求的路径是一样的。
版本头信息的Key可以根据自身情况进行定义,推荐使用Accpet形式,详见 Versioning REST Services。
在RESTful设计规范内我们需要充分的里面HttpStatus请求的状态码来判断一个请求发送状态,本次请求是否有效,常见的HttpStatus状态码如下所示:
状态码 | 发生场景 |
---|---|
200 | 请求成功 |
201 | 新资源创建成功 |
204 | 没有任何内容返回 |
400 | 传递的参数格式不正确 |
401 | 没有权限访问 |
403 | 资源受保护 |
404 | 访问的路径不正确 |
405 | 访问方式不正确,GET请求使用POST方式访问 |
410 | 地址已经被转移,不可用 |
415 | 要求接口返回的格式不正确,比如:客户端需要JSON格式,接口返回的是XML |
429 | 客户端请求次数超过限额 |
500 | 访问的接口出现系统异常 |
503 | 服务不可用,服务一般处于维护状态 |
针对不同的状态码我们要做出不同的反馈,下面我们先来看一个常见的参数异常错误响应设计方式:
# 发起请求
curl -X POST -H 'Content-Type: application/json' https://api.yuqiyu.com/v1/users -d '{
"name": "",
"age": 25,
"address": "山东济南"
}'
# 响应状态
HttpStatus 200
# 响应内容
{
"code": "400",
"message": "用户名必填."
}
在服务端我们可以控制不同状态码、不同异常的固定返回格式,不应该将所有的异常请求都返回200,然后对应返回错误,正确的方式:
# 发起请求
curl -X POST -H 'Content-Type: application/json' https://api.yuqiyu.com/v1/users -d '{
"name": "",
"age": 25,
"address": "山东济南"
}'
# 响应状态
HttpStatus 400
# 响应内容
{
"error": "Bad Request",
"message": "用户名必填."
}
接口的响应格式应该统一。
每一个请求成功的接口返回值外层格式应该统一,在服务端可以采用实体方式进行泛型返回。
如下所示:
/**
* Api统一响应实体
* {@link #data } 每个不同的接口响应的数据内容
* {@link #code } 业务异常响应状态码
* {@link #errorMsg} 业务异常消息内容
* {@link #timestamp} 接口响应的时间戳
*
* @author 恒宇少年 - 于起宇
*/
@Data
public class ApiResponse<T> implements Serializable {
private T data;
private String code;
private String errorMsg;
private Long timestamp;
}
由于每一个API的响应数据类型不一致,所以在上面采用的泛型的泛型进行返回,data可以返回任意类型的数据。
业务逻辑异常码,比如:USER_NOT_FOUND(用户不存在)这是接口的约定
对应code值得描述。
请求响应的时间戳
RESTful是API的设计规范,并不是所有的接口都应该遵循这一套规范来设计,不过我们在设计初期更应该规范性,这样我们在后期阅读代码时根据路径以及请求方式就可以了解接口的主要完成的工作。
作者:恒宇少年
链接:https://www.jianshu.com/p/35f1d3222cde
来源:简书
在我们安装数据库后会有几个默认的数据库,其中information_schema这个数据库中保存了MySQL服务器所有数据库的信息,如:数据库名、数据库表、表的数据信息与访问权限等。
information_schema的表tables记录了所有数据库的表的信息
information_schema的表columns记录了所有数据库的表字段详细的信息
我们代码中可以可以通过Sql语句查询出当前数据库中所有表的信息,这里已 eladmin 为例。
# 显示部分数据:表名称、数据库引擎、编码、表备注、创建时间
select table_name ,create_time , engine, table_collation, table_comment from information_schema.tables
where table_schema = (select database());
知道表的数据后,可以查询出表字段的详细数据,这里用 job 表为例
sql语句如下:
# 显示部分数据:字段名称、字段类型、字段注释、字段键类型等
select column_name, is_nullable, data_type, column_comment, column_key, extra from information_schema.columns
where table_schema = (select database()) and table_name = "job";
有了表字段信息的数据后,通过程序将数据库表字段类型转换成Java语言的字段类型,再通过FreeMarker创建模板,将数据写入到模板,输出成文件即可实现代码生成功能。
这里只贴出核心代码,源码可查询文末地址,首先创建一个新的spring boot 项目,选择如下依赖
Maven完整依赖如下
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- 配置管理工具 -->
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
项目结构如下
修改Spring boot 配置文件 application.yml,如下
service:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/eladmin?serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
jpa:
show-sql: true
在 resources 目录下创建 Mysql 字段与 Java字段对应关系的配置文件 generator.properties
,生成代码时字段转换时使用
tinyint=Integer
smallint=Integer
mediumint=Integer
int=Integer
integer=Integer
bigint=Long
float=Float
double=Double
decimal=BigDecimal
bit=Boolean
char=String
varchar=String
tinytext=String
text=String
mediumtext=String
longtext=String
date=Timestamp
datetime=Timestamp
timestamp=Timestamp
在 vo 包下创建临时 Vo 类 ColumnInfo,该类的功能用于接收Mysql字段详细信息
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ColumnInfo {
/** 数据库字段名称 **/
private Object columnName;
/** 允许空值 **/
private Object isNullable;
/** 数据库字段类型 **/
private Object columnType;
/** 数据库字段注释 **/
private Object columnComment;
/** 数据库字段键类型 **/
private Object columnKey;
/** 额外的参数 **/
private Object extra;
}
在 util 包下创建字段工具类 ColumnUtil,该类的功能用于转换mysql类型为Java字段类型,同时添加驼峰转换方法,将表名转换成类名
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
/**
* sql字段转java
*
* @author jie
* @date 2019-01-03
*/
public class ColumnUtil {
private static final char SEPARATOR = '_';
/**
* 获取配置信息
*/
public static PropertiesConfiguration getConfig() {
try {
return new PropertiesConfiguration("generator.properties");
} catch (ConfigurationException e) {
e.printStackTrace();
}
return null;
}
/**
* 转换mysql数据类型为java数据类型
* @param type
* @return
*/
public static String cloToJava(String type){
Configuration config = getConfig();
return config.getString(type,null);
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld"
* toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCamelCase(String s) {
if (s == null) {
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld"
* toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCapitalizeCamelCase(String s) {
if (s == null) {
return null;
}
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
}
在 util 包下创建代码生成工具类 GeneratorUtil,该类用于将获取到的Mysql字段信息转出Java字段类型,并且获取代码生成的路径,读取 Template,并且输出成文件,代码如下:
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.*;
import java.time.LocalDate;
import java.util.*;
/**
* 代码生成
*
* @author jie
* @date 2019-01-02
*/
@Slf4j
public class GeneratorUtil {
private static final String TIMESTAMP = "Timestamp";
private static final String BIGDECIMAL = "BigDecimal";
private static final String PK = "PRI";
private static final String EXTRA = "auto_increment";
/**
* 生成代码
* @param columnInfos
* @param pack
* @param author
* @param tableName
* @throws IOException
*/
public static void generatorCode(List<ColumnInfo> columnInfos, String pack, String author, String tableName) throws IOException {
Map<String, Object> map = new HashMap<>();
map.put("package", pack);
map.put("author", author);
map.put("date", LocalDate.now().toString());
map.put("tableName", tableName);
// 转换为小写开头的的类名, hello_world == helloWorld
String className = ColumnUtil.toCapitalizeCamelCase(tableName);
// 转换为大写开头的类名, hello_world == HelloWorld
String changeClassName = ColumnUtil.toCamelCase(tableName);
map.put("className", className);
map.put("changeClassName", changeClassName);
// 是否包含 Timestamp 类型
map.put("hasTimestamp", false);
// 是否包含 BigDecimal 类型
map.put("hasBigDecimal", false);
// 是否为自增主键
map.put("auto", false);
List<Map<String, Object>> columns = new ArrayList<>();
for (ColumnInfo column : columnInfos) {
Map<String, Object> listMap = new HashMap<>();
listMap.put("columnComment", column.getColumnComment());
listMap.put("columnKey", column.getColumnKey());
String colType = ColumnUtil.cloToJava(column.getColumnType().toString());
String changeColumnName = ColumnUtil.toCamelCase(column.getColumnName().toString());
if (PK.equals(column.getColumnKey())) {
map.put("pkColumnType", colType);
map.put("pkChangeColName", changeColumnName);
}
if (TIMESTAMP.equals(colType)) {
map.put("hasTimestamp", true);
}
if (BIGDECIMAL.equals(colType)) {
map.put("hasBigDecimal", true);
}
if (EXTRA.equals(column.getExtra())) {
map.put("auto", true);
}
listMap.put("columnType", colType);
listMap.put("columnName", column.getColumnName());
listMap.put("isNullable", column.getIsNullable());
listMap.put("changeColumnName", changeColumnName);
columns.add(listMap);
}
map.put("columns", columns);
Configuration configuration = new Configuration(Configuration.VERSION_2_3_23);
configuration.setClassForTemplateLoading(GeneratorUtil.class, "/template");
Template template = configuration.getTemplate("Entity.ftl");
// 获取文件路径
String filePath = getAdminFilePath(pack, className);
File file = new File(filePath);
// 生成代码
genFile(file, template, map);
}
/**
* 定义文件路径以及名称
*/
private static String getAdminFilePath(String pack, String className) {
String ProjectPath = System.getProperty("user.dir") + File.separator;
String packagePath = ProjectPath + File.separator + "src" + File.separator + "main" + File.separator + "java" + File.separator;
if (!ObjectUtils.isEmpty(pack)) {
packagePath += pack.replace(".", File.separator) + File.separator;
}
return packagePath + "entity" + File.separator + className + ".java";
}
private static void genFile(File file, Template template, Map<String, Object> params) throws IOException {
File parentFile = file.getParentFile();
// 创建目录
if (null != parentFile && !parentFile.exists()) {
parentFile.mkdirs();
}
//创建输出流
Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
//输出模板和数据模型都对应的文件
try {
template.process(params, writer);
} catch (TemplateException e) {
e.printStackTrace();
}
}
}
在 resources 的 template 目录下创建 framework 模板 Entity.ftl,代码如下:
package ${package}.entity;
import lombok.Data;
import javax.persistence.*;
<#if hasTimestamp>
import java.sql.Timestamp;
</#if>
<#if hasBigDecimal>
import java.math.BigDecimal;
</#if>
import java.io.Serializable;
/**
* @author ${author}
* @date ${date}
*/
@Entity
@Data
@Table(name="${tableName}")
public class ${className} implements Serializable {
<#if columns??>
<#list columns as column>
<#if column.columnComment != ''>
// ${column.columnComment}
</#if>
<#if column.columnKey = 'PRI'>
@Id
<#if auto>
@GeneratedValue(strategy = GenerationType.IDENTITY)
</#if>
</#if>
@Column(name = "${column.columnName}"<#if column.columnKey = 'UNI'>,unique = true</#if><#if column.isNullable = 'NO' && column.columnKey != 'PRI'>,nullable = false</#if>)
private ${column.columnType} ${column.changeColumnName};
</#list>
</#if>
}
创建服务类 GeneratorService,该类用于获取数据库表的源数据
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 代码生成服务
*/
@Service
public class GeneratorService {
@PersistenceContext
private EntityManager em;
public List<ColumnInfo> getColumns(String tableName) {
StringBuilder sql = new StringBuilder("select column_name, is_nullable, data_type, column_comment, column_key, extra from information_schema.columns where ");
if(!ObjectUtils.isEmpty(tableName)){
sql.append("table_name = '").append(tableName).append("' ");
}
sql.append("and table_schema = (select database()) order by ordinal_position");
Query query = em.createNativeQuery(sql.toString());
List result = query.getResultList();
List<ColumnInfo> columnInfos = new ArrayList<>();
for (Object o : result) {
Object[] obj = (Object[])o;
columnInfos.add(new ColumnInfo(obj[0],obj[1],obj[2],obj[3],obj[4],obj[5]));
}
return columnInfos;
}
}
由于没有前端页面,所以只能在测试类中演示代码生成功能,GeneratorDomeApplicationTests 修改如下
import com.ydyno.util.GeneratorUtil;
import com.ydyno.vo.ColumnInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.List;
@SpringBootTest
class GeneratorDomeApplicationTests {
@Autowired
private GeneratorService generatorService;
@Test
void genTest() throws IOException {
String tableName = "job";
String pack = "com.ydyno";
String author = "Zheng Jie";
List<ColumnInfo> columnInfos = generatorService.getColumns(tableName);
GeneratorUtil.generatorCode(columnInfos,pack,author,tableName);
}
}
执行后,查看创建好的Entity
]]>