知了小站 - IT人的小站 - Spring教程 https://www.ydyno.com/tag/spring/ 全局ID生成器:SpringBoot2.x 集成百度 uidgenerator https://www.ydyno.com/archives/1276.html 2021-03-07T22:27:00+08:00 因为升级 使用springboot2.x java 11 的关系,根据官方文档和网上其他作者配置的怎么也配置不成功,最后自己一步一步升级引入依赖,修改增加接口注入来源,最后成功。升级成功后的源码地址:https://github.com/foxiswho/java-spring-boot-uid-generator-baidu部分升级说明这里的升级,是升级 官方 代码依赖官方代码地址:https://github.com/baidu/uid-generator升级spring boot 版本: 2.0.7.RELEASE升级 mybatis,mybatis-spring 版本升级 mysql-connector-java 版本:8.0.12升级 junit 版本创建数据库存导入官网数据库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";mysql 配置信息更改因为升级到8.x ,配置文件部分也要跟着修改 uid-generator 下,测试文件夹下的资源包 uid/mysql.properties以下修改为mysql.driver=com.mysql.cj.jdbc.Driver修改完成后,配置好数据库相关参数,这样单元测试即可执行成功案例计划将全局生成唯一ID作为一个服务提供者,供其他微服务使用调用这里创建了一个项目,项目中包含两个子项目一个是 uid-generator 官方本身,当然你也可以不需要放到本项目中,直接使用官方的自行打包即可,一个是 uid-provider 服务提供者以下说明的主要是服务提供者创建 子项目 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>复制 mapper先在 uid-provider 项目资源包路径下创建 mapper 文件夹,然后到官方 uid-generator 资源包路径下 META-INF/mybatis/mapper/WORKER_NODE.xml 复制 WORKER_NODE.xml 文件,粘贴到该文件夹 mapper 内cache id 配置文件先在 uid-provider 项目资源包路径下创建 uid 文件夹,然后到官方 uid-generator 测试 [注意:这里是测试资源包] 资源包路径下 uid/cached-uid-spring.xml 复制 cached-uid-spring.xml 文件,粘贴到该文件夹 uid 内最后根据需要 配置参数,可以看官方说明创建 spring boot 启动入口主要就是加上注解 @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原文地址:https://foxwho.blog.csdn.net/article/details/90200602 Spring 的 Controller 是单例还是多例?怎么保证并发的安全? https://www.ydyno.com/archives/1261.html 2021-02-28T12:14:00+08:00 答案如下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 和 @Resource 注解的区别与使用的正确姿势 https://www.ydyno.com/archives/1255.html 2021-02-13T13:53:00+08:00 今天使用Idea写代码的时候,看到之前的项目中显示有warning的提示,去看了下,是如下代码?@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; } }基于Setter的依赖注入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,加入了自己的白话理解!原文链接:https://juejin.cn/post/6844904064212271117 Spring Boot 如何优雅的校验参数 https://www.ydyno.com/archives/1201.html 2019-12-12T09:22:00+08:00 前言做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注解即可验证,在运行时触发,本文不对自定义注解做过多的解释,下篇文章介绍自定义注解message 提示信息groups 分组payload 针对于Bean然后添加 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注解,注意不是增加在参数前面。作者:孙罗蒙链接:https://lqcoder.com/p/4cd8a59d.html Spring Boot 自定义异步线程池的两种方式 https://www.ydyno.com/archives/1199.html 2019-11-06T14:08:00+08:00 SpringBoot 使用异步线程池方式如下第一种创建自定义线程池配置类,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 的方法是不生效的例子中的参数根据具体的需求修改源码本文源码:https://github.com/elunez/spring-boot-learn RESTful 规范 Api 最佳设计实践 https://www.ydyno.com/archives/1194.html 2019-10-23T18:45:00+08:00 RESTful是目前比较流行的接口路径设计规范,基于HTTP,一般使用JSON方式定义,通过不同HttpMethod来定义对应接口的资源动作,如:新增(POST)、删除(DELETE)、更新(PUT、PATCH)、查询(GET)等。路径设计在RESTful设计规范内,每一个接口被认为是一个资源请求,下面我们针对每一种资源类型来看下API路径设计。路径设计的注意事项如下所示:资源名使用复数资源名使用名词路径内不带特殊字符避免多级URL新增资源请求方式示例路径POSThttps://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" : "恒宇少年" }通过返回的唯一标识来操作该资源的其他数据接口。删除资源请求方式示例路径备注DELETEhttps://api.yuqiyu.com/v1/users批量删除资源DELETEhttps://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更新资源请求方式示例路径备注PUThttps://api.yuqiyu.com/v1/users/{id}更新单个资源的全部元素PATCHhttps://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": "山东济南" }'查询单个资源请求方式示例路径备注GEThttps://api.yuqiyu.com/v1/users/{id}查询单个资源GEThttps://api.yuqiyu.com/v1/users?name={name}非唯一标识查询资源唯一标识查询单个资源curl https://api.yuqiyu.com/v1/users/1通过唯一标识查询资源时,使用路径方式传递标识值,体现出层级关系。非唯一标识查询单个资源curl https://api.yuqiyu.com/v1/users?name=恒宇少年查询资源数据时不仅仅都是通过唯一标识值作为查询条件,也可能会使用资源对象内的某一个元素作为查询条件。分页查询资源请求方式示例路径GEThttps://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=恒宇少年动作资源有时我们需要有动作性的修改某一个资源的元素内容,比如:重置密码。请求方式示例路径备注POSThttps://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格式,接口返回的是XML429客户端请求次数超过限额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; }data由于每一个API的响应数据类型不一致,所以在上面采用的泛型的泛型进行返回,data可以返回任意类型的数据。code业务逻辑异常码,比如:USER_NOT_FOUND(用户不存在)这是接口的约定errorMsg对应code值得描述。timestamp请求响应的时间戳总结RESTful是API的设计规范,并不是所有的接口都应该遵循这一套规范来设计,不过我们在设计初期更应该规范性,这样我们在后期阅读代码时根据路径以及请求方式就可以了解接口的主要完成的工作。作者:恒宇少年链接:https://www.jianshu.com/p/35f1d3222cde来源:简书 Spring boot 整合 FreeMarker 实现代码生成功能 https://www.ydyno.com/archives/1177.html 2019-10-17T17:14:00+08:00 在我们开发一个新的功能的时候,会根据表创建Entity,Controller,Service,Repository等代码,其中很多步骤都是重复的,并且特别繁琐。这个时候就需要一个代码生成器帮助我们解决这个问题从而提高工作效率,让我们更致力于业务逻辑。设计原理在我们安装数据库后会有几个默认的数据库,其中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