知了小站 - IT人的小站 - 2019年 IT人的小站,分享开发经验,记录成长轨迹。 2019-12-30T21:47:00+08:00 Typecho https://www.ydyno.com/feed/atom/2019/ <![CDATA[前端 axios 中 qs 介绍与使用]]> https://www.ydyno.com/archives/1217.html 2019-12-30T21:47:00+08:00 2019-12-30T21:47:00+08:00 知了小站 https://www.ydyno.com 首先 qs 是一个 npm 仓库所管理的包,可通过 npm install qs 命令进行安装

地址: https://www.npmjs.com/package/qs

qs.parse()

qs.parse() 将URL解析成对象的形式

const Qs = require('qs');
let url = 'method=query_sql_dataset_data&projectId=85&appToken=7d22e38e-5717-11e7-907b-a6006ad3dba0';
Qs.parse(url);
console.log(Qs.parse(url));

qs.stringify()

qs.stringify() 将对象序列化成URL的形式,以&进行拼接

const Qs = require('qs');
let obj= {
     method: "query_sql_dataset_data",
     projectId: "85",
     appToken: "7d22e38e-5717-11e7-907b-a6006ad3dba0",
     datasetId: " 12564701"
   };
Qs.stringify(obj);
console.log(Qs.stringify(obj));

那么当我们需要传递数组的时候,我们就可以通过下面方式进行处理:

默认情况下,它们给出明确的索引,如下代码:

qs.stringify({ a: ['b', 'c', 'd'] });
// 'a[0]=b&a[1]=c&a[2]=d'

也可以进行重写这种默认方式为false

qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false });
// 'a=b&a=c&a=d'

当然,也可以通过arrayFormat 选项进行格式化输出,如下代码所示:

qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'

在这里需要注意的是,JSON中同样存在stringify方法,但是两者之间的区别是很明显的,如下所示:

{"uid":"cs11","pwd":"000000als","username":"cs11","password":"000000als"}

uid=cs11&pwd=000000als&username=cs11&password=000000als

如上所示,前者是采用 JSON.stringify(param) 进行处理,后者是采用 Qs.stringify(param) 进行处理的。

对于JSON.stringify和JSON的使用可参见

https://blog.csdn.net/suwu150/article/details/76100120

原文地址:https://www.jianshu.com/p/67223e177aa6

]]>
<![CDATA[解决 Vue 在 History 模式部署在 Nginx 上刷新报 404 的问题]]> https://www.ydyno.com/archives/1215.html 2019-12-27T17:16:00+08:00 2019-12-27T17:16:00+08:00 知了小站 https://www.ydyno.com 教程适用于history模式,假设域名假设为:auauz.net

原配置

server {
    listen 80;
    server_name auauz.net;
    root /www/wwwroot/eladmin-web/dist;
    index index.html;
    error_page 404 /index.html;
}

修改如下

server {

    listen 80;
    server_name auauz.net;
    
    location / {
        root /www/wwwroot/eladmin-web/dist;
        try_files $uri $uri/ @router;
        index index.html;
    }
    
    location @router {
        rewrite ^.*$ /index.html last;
    }
}
]]>
<![CDATA[EL-ADMIN v2.4 发布,升级 cli3,新增监控模块,运维管理等]]> https://www.ydyno.com/archives/1210.html 2019-12-23T10:15:00+08:00 2019-12-23T10:15:00+08:00 知了小站 https://www.ydyno.com EL-ADMIN 基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue 的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持前端菜单动态路由。

v2.4 版本更新内容如下

后端

1、新增服务监控模块 eladmin-monitor,监控服务器的负载情况 (by @zhy6599)

2、代码生成器优化,支持配置更多信息,支持打包下载与预览生成

(1) 界面

(2) 配置

(3) 预览

3、加入了运维管理 (by @zhy6599)

4、用户加入昵称与性别字段

5、免费图床支持同步功能

6、新增清空日志功能,需要相应的权限才能操作 (#217 by @zoulejiu)

7、security 优化,密码加密方式采用BCryptPasswordEncoder方式:SHA-256 +随机盐+密钥对密码进行加密

7、前后端用户登陆密码rsa加密,后端解密,避免撞库

8、@Query新增 NOT_EQUALBETWEENNOT_NULL查询

9、匿名访问统一使用 @AnonymousAccess

去除 @PreAuthorize("hasRole('anonymous')")、@PreAuthorize("@el.check('anonymous')") 的匿名访问效果

10、新增单点登录,多设备登录 (#217 by @zoulejiu)

以最后登录为准,之前的登录都会被踢掉,可以使用single.login =false 关闭

11、代码优化,2.4版本完全遵循阿里巴巴代码规范

前端

1、前端升级为 vue-cli3 ,加入主题配置,目录结构优化

2、移除 initData.js,封装 crud.js ,只需要引入该 js 即可实现所有增删改查的功能(详情查看源码)

3、封装crud通用组件,建议使用,2.4 前端页面几乎都是使用这种形式 (#57 by @moxun1639)

(1) 组件路径

(2) 如何使用

<template>
  <div class="app-container">
    <!--工具栏-->
    <div class="head-container">
      <div v-if="crud.props.searchToggle">
        <!-- 搜索 -->
        <el-input v-model="query.value" clearable placeholder="输入搜索内容" style="width: 200px;" class="filter-item" @keyup.enter.native="crud.toQuery" />
        <!--略-->
        <rrOperation :crud="crud" />
      </div>
      <!--如果想在工具栏加入更多按钮,可以使用插槽方式, slot = 'left' or 'right'-->
      <crudOperation :permission="permission" />
      <!--表单组件-->
      <el-dialog :close-on-click-modal="false" :before-close="crud.cancelCU" :visible.sync="crud.status.cu > 0" :title="crud.status.title" width="500px">
        <!--略-->
        <div slot="footer" class="dialog-footer">
          <el-button type="text" @click="crud.cancelCU">取消</el-button>
          <el-button :loading="crud.cu === 2" type="primary" @click="crud.submitCU">确认</el-button>
        </div>
      </el-dialog>
      <!--表格渲染-->
      <el-table ref="table" v-loading="crud.loading" :data="crud.data" size="small" style="width: 100%;" @selection-change="crud.selectionChangeHandler">
        <el-table-column type="selection" width="55" />
        <el-table-column v-if="columns.visible('createTime')" prop="createTime" label="createTime">
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.createTime) }}</span>
          </template>
        </el-table-column>
        <el-table-column v-permission="['admin','genTest:edit','genTest:del']" label="操作" width="150px" align="center">
          <template slot-scope="scope">
            <udOperation :data="scope.row" :permission="permission" />
          </template>
        </el-table-column>
      </el-table>
      <!--分页组件-->
      <pagination />
    </div>
  </div>
</template>

<script>
import crudGenTest from '@/api/genTest'
import CRUD, { presenter, header, form, crud } from '@crud/crud'
import rrOperation from '@crud/RR.operation'
import crudOperation from '@crud/CRUD.operation'
import udOperation from '@crud/UD.operation'
import pagination from '@crud/Pagination'

// crud交由presenter持有
const defaultCrud = CRUD({ title: '测试生成', url: 'api/genTest', sort: 'id,desc', crudMethod: { ...crudGenTest }})
const defaultForm = { id: null, name: null, sex: null, createTime: null }
export default {
  name: 'GenTest',
  components: { pagination, crudOperation, rrOperation, udOperation },
  mixins: [presenter(defaultCrud), header(), form(defaultForm), crud()],
  data() {
    return {
      permission: {
        add: ['admin', 'genTest:add'],
        edit: ['admin', 'genTest:edit'],
        del: ['admin', 'genTest:del']
      },
      rules: {
        id: [
          { required: true, message: 'ID不能为空', trigger: 'blur' }
        ],
        name: [
          { required: true, message: '名称不能为空', trigger: 'blur' }
        ]
      },
      queryTypeOptions: [
        { key: 'name', display_name: '名称' },
        { key: 'sex', display_name: '性别' }
      ]
    }
  },
  methods: {
    // 获取数据前设置好接口地址
    [CRUD.HOOK.beforeRefresh]() {
      const query = this.query
      if (query.type && query.value) {
        this.crud.params[query.type] = query.value
      }
      return true
    }
  }
}
</script>

(3) 界面如下

3、异常日志详情加入代码高亮显示,生产环境移除console

4、其他升级可以查看演示页面

修复

1、修复查询时输入 []{} 符号报错的问题#185

2、修复本地存储预览关闭按钮被header遮挡的问题#188

3、修复Menu排序问题 (#194 by CCCY0)

4、修复ip2region 不会自动关闭连接问题 (#217 by @zoulejiu)

5、QueryHelp 中 RIGHT_LIKE 缺少 break 的问题 (#214 by jhpx)

指南

  • 新版本代码生成器:链接
  • 新版本服务监控:链接
  • 新版本运维管理:链接
  • 新版本免费图床:链接
  • 新版本@Query查询:链接
]]>
<![CDATA[ES6 语法大全 export,import,for.of循环,promise等等]]> https://www.ydyno.com/archives/1209.html 2019-12-19T14:31:00+08:00 2019-12-19T14:31:00+08:00 知了小站 https://www.ydyno.com 变量
let 局部变量
const 常量
var 全局变量

字符串的拓展

let str = "123"
str.includes("1")//true  includes方法 是否包含
str.startsWith("2")//false 是否以2开头
str.endsWith("2")//false 是否以2结尾

解构表达式

//数组解构
let arr = [1,2,3]
const [x,y,z] = arr;// x,y,z对应 1,2,3 
//对象解构
const person = {
    name:"jack",
    age:21,
    language:['java','php'],
}
let {name,age,language} = person 
//自定义命名
let {name:n,age:a,language} = person 

函数的优化

//参数上面的优化=1,指当b没有值时默认为1
function test(a,b=1){
console.log(a+b)
}

箭头函数

//Demo1  单个参数
var demo1= fucntion demo1(obj){
console.log(obj)
}
箭头函数简化为: var demo1= obj =>console.log(obj);

//Demo2  两个参数
var sum =function(a,b){
    console.log(a+n)
}
箭头函数简化为: var sum = (a,b)=>console.log(obj);

//Demo3 没有参数
let sayHello = ()=>console.log("hello!");

//Demo4 代码不止一行 使用 {}
var sum = (a,b)=>{
    console.log(a+n);
    console.log(a+n)
}

//Demo5 对象的函数简写
let person ={
    name:“jeck”;
    //原来
    eat:function(food){
        console.log(this.name +food)
    }
    //箭头函数
    eat2:food=>console.log(this.name +food)
    
    //简写版
    eat3(food){
    console.log(this.name +food)
    }
}
//Demo6:箭头函数配合解构表达式
let person ={
    name:“jeck”;
    eat2:food=>console.log(this.name +food)
}
function test1(person){
    console.log(person.name);
}
//简化调用函数 使用{}传参数,传入对象
var test1=({name})=>console.log(name);
test1(person );

map和reduce函数

map 让原来的集合经过 map 中的函数 进行处理回调

let arr = ['1','2','3'];
arr.map(s=>parseInt(s))//字符串转化为内证书 

//reduce() 接收一个函数和一个初始值
第一个参数时上一次reduce的处理结果
第二个参数是数组中要处理的下一个元素
const arr = [1,20,30,40]
arr.reduce((a,b)=>a+b)

拓展运算符(三个点…)

将一个数组转为用逗号分隔的参数序列

function add(a,b){
return a+b;
}
var number = [1,2];

//数组合并
var arrs=[...[1,2,3],...[4,5,6]];//[1,2,3,4,5,6]
//将字符串转为数组
console.log([...'hello'])//['h','e','l','l','o']

promise

// 函数格式
const promise = new promise(function(resolve,reject){
    //操作
    //if(success){
        resolve(value);//成功
    }else{
        reject(error)//失败
    }
})
//执行完了在执行一些东西的话
promise.then(function(value){
    //异步回调
}).catch(function(error){
    //异常回调
})

set和map

set 只能保存不同元素,相同的元素会被忽略

let set = new set();
let set = new set([2,3,4,5]);
//map接受一个数组,数组中的元素时键值对
let map = new map([
['key','value'],
['key1','value1'],
])

for.of循环

for(let obj of h){
    console.log(obj)
}

模块化export import

// export 导出命令
calss Util{
    sum=(a,b)=>a+b;
}
export default Util
    
// import加载
import Util from './Util'

原文链接:https://blog.csdn.net/qq_35349982/article/details/103581101

]]>
<![CDATA[Spring Boot 如何优雅的校验参数]]> https://www.ydyno.com/archives/1201.html 2019-12-12T09:22:00+08:00 2019-12-12T09:22:00+08:00 知了小站 https://www.ydyno.com 前言

做web开发有一点很烦人就是要校验参数,基本上每个接口都要对参数进行校验,比如一些格式校验 非空校验都是必不可少的。如果参数比较少的话还是容易 处理的一但参数比较多了的话代码中就会出现大量的 IF ELSE就比如下面这样:

FlTYTfd_JcDlPLt3cN1H0BaCmc3p.jpg

这个例子只是校验了一下空参数。如果需要验证邮箱格式和手机号格式校验的话代码会更多,所以介绍一下 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

注意:

  1. @NotNull 适用于任何类型被注解的元素必须不能与NULL
  2. @NotEmpty 适用于String Map或者数组不能为Null且长度必须大于0
  3. @NotBlank 只能用于String上面 不能为null,调用trim()后,长度必须大于0

使用

模拟用户注册封装了一个 UserDTO

当提交数据的时候如果使用以前的做法就是 IF ELSE判断参数使用 validator则是需要增加注解即可。

例如非空校验:

FhOCMMeVfAdXSHmy01XSHbojA6Vo.jpg

然后需要在 controller方法体添加 @Validated不加 @Validated校验会不起作用

FqdrNw4zwQfFk4RSsh-mAIprovXo.jpg

然后请求一下请求接口,把 Email参数设置为空

参数:

{
    "userName":"luomengsun",
    "mobileNo":"11111111111",
    "sex":1,
    "age":21,
    "email":""
}

返回结果:

Fo-tqzpQ1EQhSOx0BxHyL2i0hzJD.jpg

后台抛出异常

FrnnwbvROzAQqdY8ygndAkqFwKko.jpg

这样是能校验成功,但是有个问题就是返回参数并不理想,前端也并不容易处理返回参数,所以我们添加一下全局异常处理,然后添加一下全局统一返回参数这样比较规范。

添加全局异常

创建一个 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 主要进行验证逻辑

Fhhegx1Jehf6Q0gqp3WAypj8vgzJ.jpg

上面调用了 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

]]>
<![CDATA[Spring Boot 自定义异步线程池的两种方式]]> https://www.ydyno.com/archives/1199.html 2019-11-06T14:08:00+08:00 2019-11-06T14:08:00+08:00 知了小站 https://www.ydyno.com SpringBoot 使用异步线程池方式如下

第一种

  1. 创建自定义线程池配置类,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;
    }
}
  1. 创建任务处理类AsyncTask
@Component
@Slf4j
public class AsyncTask {

    /**
     * myAsyncTaskPool 线程池的方法名,此处如果不写,会使用Spring默认的线程池
     * @param i
     */
    @Async("myAsyncTaskPool")
    public void run(int i){
       log.info("我是:" + i);
    }
}
  1. 测试线程池AppTests
@SpringBootTest
class AppTests {

    @Autowired
    private AsyncTask asyncTask;

    @Test
    void test(){
        for (int i = 0; i < 100; i++) {
            asyncTask.run(i);
        }
    }
}

运行查看效果

第二种

第二种方式是重写 spring 默认线程池,使用这种方式的好处是可以直接使用 @Async 注解

  1. 创建配置类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());
        };
    }
}
  1. 修改AsyncTask 类,在类中加入方法run1
@Async
public void run1(int i){
    log.info("我是:" + i);
}
  1. 测试,在AppTests 中加入方法test1
@Test
void test1(){
    for (int i = 0; i < 100; i++) {
        asyncTask.run1(i);
    }
}

运行查看效果

注意

  1. 同类中调用带有@Async 的方法是不生效的
  2. 例子中的参数根据具体的需求修改

源码

本文源码:https://github.com/elunez/spring-boot-learn

]]>
<![CDATA[EL-ADMIN v2.3 发布,新增在线用户管理,多项优化]]> https://www.ydyno.com/archives/1197.html 2019-11-04T17:16:00+08:00 2019-11-04T17:16:00+08:00 知了小站 https://www.ydyno.com EL-ADMIN 是基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、Redis、Vue的前后端分离的权限管理系统,项目采用按功能分模块开发方式, 权限控制采用 RBAC 方式,前端菜单动态路由。新版更新内容如下:

后端

  1. 代码优化,优化大量Idea警告,代码更严谨 #134
  2. 加入实体基类(BaseEntity)、DTO基类(BaseDTO),按需继承 #137
  3. 新增基于Redis的在线用户管理,可强制下线用户 #6
  4. 新增退出登录接口,退出登录后Token不再有效
  5. 图形验证码更换,由随机验证码模式,改为算术验证
  6. 日志管理加入浏览器字段,获取Ip地址优化,局域网内支持获取到主机地址
  7. 菜单与权限调整,权限管理作为按钮存入菜单表
  8. 增加匿名访问注解,扩展PreAuthorize 匿名注解 #159
  9. 自定义权限校验,@PreAuthorize("@el.check('dept:list')"),根据个人习惯可自行选择
  10. 代码生成器优化,前后端默认添加导出功能,input时间组件自动判定
  11. 自定义异步线程池(重写spring默认线程池),使用自定义线程池执行定时任务,避免程序OOM
  12. 免费图床优化,通过MD5判断图片是否重复上传,如果图片存在则返回历史图片

前端

  1. 简化数据字典的使用,由混入改为全局,支持单组件内多字典同时使用 #37
  2. 存储管理增加图片路径提示和图片预览 #40
  3. 树形表格更换,采用 element-ui 自带的树形表格组件

修复

  1. 解决Gson找不到包的bug #141
  2. 解决ip2region.db路径不正确的问题 #146

2.3版本指南:https://docs.auauz.net/#/sjzn

预览地址:https://auauz.net/

项目源码

后端源码前端源码
Githubhttps://github.com/elunez/eladminhttps://github.com/elunez/eladmin-web
码云https://gitee.com/elunez/eladminhttps://gitee.com/elunez/eladmin-web
]]>
<![CDATA[Java 8:一文掌握 Lambda 表达式]]> https://www.ydyno.com/archives/1196.html 2019-10-25T22:06:00+08:00 2019-10-25T22:06:00+08:00 知了小站 https://www.ydyno.com 本文将介绍 Java 8 新增的 Lambda 表达式,包括 Lambda 表达式的常见用法以及方法引用的用法,并对 Lambda 表达式的原理进行分析,最后对 Lambda 表达式的优缺点进行一个总结。

1. 概述

Java 8 引入的 Lambda 表达式的主要作用就是简化部分匿名内部类的写法。

能够使用 Lambda 表达式的一个重要依据是必须有相应的函数接口。所谓函数接口,是指内部有且仅有一个抽象方法的接口。

Lambda 表达式的另一个依据是类型推断机制。在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。

2. 常见用法

2.1 无参函数的简写

无参函数就是没有参数的函数,例如 Runnable 接口的 run() 方法,其定义如下:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

在 Java 7 及之前版本,我们一般可以这样使用:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
        System.out.println("Jimmy");
    }
}).start();

从 Java 8 开始,无参函数的匿名内部类可以简写成如下方式:

() -> {
    执行语句
}

这样接口名和函数名就可以省掉了。那么,上面的示例可以简写成:

new Thread(() -> {
    System.out.println("Hello");
    System.out.println("Jimmy");
}).start();

当只有一条语句时,我们还可以对代码块进行简写,格式如下:

() -> 表达式

注意这里使用的是表达式,并不是语句,也就是说不需要在末尾加分号。

那么,当上面的例子中执行的语句只有一条时,可以简写成这样:

new Thread(() -> System.out.println("Hello")).start();

2.2 单参函数的简写

单参函数是指只有一个参数的函数。例如 View 内部的接口 OnClickListener 的方法 onClick(View v),其定义如下:

public interface OnClickListener {
    /**
     * Called when a view has been clicked.
     *
     * @param v The view that was clicked.
     */
    void onClick(View v);
}

在 Java 7 及之前的版本,我们通常可能会这么使用:

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        v.setVisibility(View.GONE);
    }
});

从 Java 8 开始,单参函数的匿名内部类可以简写成如下方式:

([类名 ]变量名) -> {
    执行语句
}

其中类名是可以省略的,因为 Lambda 表达式可以自己推断出来。那么上面的例子可以简写成如下两种方式:

view.setOnClickListener((View v) -> {
    v.setVisibility(View.GONE);
});
view.setOnClickListener((v) -> {
    v.setVisibility(View.GONE);
});

单参函数甚至可以把括号去掉,官方也更建议使用这种方式:

变量名 -> {
    执行语句
}

那么,上面的示例可以简写成:

view.setOnClickListener(v -> {
    v.setVisibility(View.GONE);
});

当只有一条语句时,依然可以对代码块进行简写,格式如下:

([类名 ]变量名) -> 表达式

类名和括号依然可以省略,如下:

变量名 -> 表达式

那么,上面的示例可以进一步简写成:

view.setOnClickListener(v -> v.setVisibility(View.GONE));

2.3 多参函数的简写

多参函数是指具有两个及以上参数的函数。例如,Comparator 接口的 compare(T o1, T o2) 方法就具有两个参数,其定义如下:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

在 Java 7 及之前的版本,当我们对一个集合进行排序时,通常可以这么写:

List<Integer> list = Arrays.asList(1, 2, 3);
Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});

从 Java 8 开始,多参函数的匿名内部类可以简写成如下方式:

([类名1 ]变量名1, [类名2 ]变量名2[, ...]) -> {
    执行语句
}

同样类名可以省略,那么上面的例子可以简写成:

Collections.sort(list, (Integer o1, Integer o2) -> {
    return o1.compareTo(o2);
});
Collections.sort(list, (o1, o2) -> {
    return o1.compareTo(o2);
});

当只有一条语句时,依然可以对代码块进行简写,格式如下:

([类名1 ]变量名1, [类名2 ]变量名2[, ...]) -> 表达式

此时类名也是可以省略的,但括号不能省略。如果这条语句需要返回值,那么 return 关键字是不需要写的。

因此,上面的示例可以进一步简写成:

Collections.sort(list, (o1, o2) -> o1.compareTo(o2));

最后呢,这个示例还可以简写成这样:

Collections.sort(list, Integer::compareTo);

咦,这是什么特性?这就是我们下面要讲的内容:方法引用。

3. 方法引用

方法引用也是一个语法糖,可以用来简化开发。

在我们使用 Lambda 表达式的时候,如果“->”的右边要执行的表达式只是调用一个类已有的方法,那么就可以用「方法引用」来替代 Lambda 表达式。

方法引用可以分为 4 类:

  • 引用静态方法;
  • 引用对象的方法;
  • 引用类的方法;
  • 引用构造方法。

下面按照这 4 类分别进行阐述。

3.1 引用静态方法

当我们要执行的表达式是调用某个类的静态方法,并且这个静态方法的参数列表和接口里抽象函数的参数列表一一对应时,我们可以采用引用静态方法的格式。

假如 Lambda 表达式符合如下格式:

([变量1, 变量2, ...]) -> 类名.静态方法名([变量1, 变量2, ...])

我们可以简写成如下格式:

类名::静态方法名

注意这里静态方法名后面不需要加括号,也不用加参数,因为编译器都可以推断出来。下面我们继续使用 2.3 节的示例来进行说明。

首先创建一个工具类,代码如下:

public class Utils {
    public static int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

注意这里的 compare() 函数的参数和 Comparable 接口的 compare() 函数的参数是一一对应的。然后一般的 Lambda 表达式可以这样写:

Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));

如果采用方法引用的方式,可以简写成这样:

Collections.sort(list, Utils::compare);

3.2 引用对象的方法

当我们要执行的表达式是调用某个对象的方法,并且这个方法的参数列表和接口里抽象函数的参数列表一一对应时,我们就可以采用引用对象的方法的格式。

假如 Lambda 表达式符合如下格式:

([变量1, 变量2, ...]) -> 对象引用.方法名([变量1, 变量2, ...])

我们可以简写成如下格式:

对象引用::方法名

下面我们继续使用 2.3 节的示例来进行说明。首先创建一个类,代码如下:

public class MyClass {
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

当我们创建一个该类的对象,并在 Lambda 表达式中使用该对象的方法时,一般可以这么写:

MyClass myClass = new MyClass();
Collections.sort(list, (o1, o2) -> myClass.compare(o1, o2));

注意这里函数的参数也是一一对应的,那么采用方法引用的方式,可以这样简写:

MyClass myClass = new MyClass();
Collections.sort(list, myClass::compare);

此外,当我们要执行的表达式是调用 Lambda 表达式所在的类的方法时,我们还可以采用如下格式:

this::方法名

例如我在 Lambda 表达式所在的类添加如下方法:

private int compare(Integer o1, Integer o2) {
    return o1.compareTo(o2);
}

当 Lambda 表达式使用这个方法时,一般可以这样写:

Collections.sort(list, (o1, o2) -> compare(o1, o2));

如果采用方法引用的方式,就可以简写成这样:

Collections.sort(list, this::compare);

3.3 引用类的方法

引用类的方法所采用的参数对应形式与上两种略有不同。如果 Lambda 表达式的“->”的右边要执行的表达式是调用的“->”的左边第一个参数的某个实例方法,并且从第二个参数开始(或无参)对应到该实例方法的参数列表时,就可以使用这种方法。

可能有点绕,假如我们的 Lambda 表达式符合如下格式:

(变量1[, 变量2, ...]) -> 变量1.实例方法([变量2, ...])

那么我们的代码就可以简写成:

变量1对应的类名::实例方法名

还是使用 2.3 节的例子, 当我们使用的 Lambda 表达式是这样时:

Collections.sort(list, (o1, o2) -> o1.compareTo(o2));

按照上面的说法,就可以简写成这样:

Collections.sort(list, Integer::compareTo);

3.4 引用构造方法
当我们要执行的表达式是新建一个对象,并且这个对象的构造方法的参数列表和接口里函数的参数列表一一对应时,我们就可以采用「引用构造方法」的格式。

假如我们的 Lambda 表达式符合如下格式:

([变量1, 变量2, ...]) -> new 类名([变量1, 变量2, ...])

我们就可以简写成如下格式:

类名::new

下面举个例子说明一下。Java 8 引入了一个 Function 接口,它是一个函数接口,部分代码如下:

@FunctionalInterface
public interface Function<T, R> {
    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    // 省略部分代码
}

我们用这个接口来实现一个功能,创建一个指定大小的 ArrayList。一般我们可以这样实现:

Function<Integer, ArrayList> function = new Function<Integer, ArrayList>() {
    @Override
    public ArrayList apply(Integer n) {
        return new ArrayList(n);
    }
};
List list = function.apply(10);

使用 Lambda 表达式,我们一般可以这样写:

Function<Integer, ArrayList> function = n -> new ArrayList(n);

使用「引用构造方法」的方式,我们可以简写成这样:

Function<Integer, ArrayList> function = ArrayList::new;

4. 自定义函数接口

自定义函数接口很容易,只需要编写一个只有一个抽象方法的接口即可,示例代码:

@FunctionalInterface
public interface MyInterface<T> {
    void function(T t);
}

上面代码中的 @FunctionalInterface 是可选的,但加上该注解编译器会帮你检查接口是否符合函数接口规范。就像加入 @Override 注解会检查是否重写了函数一样。

5. 实现原理

经过上面的介绍,我们看到 Lambda 表达式只是为了简化匿名内部类书写,看起来似乎在编译阶段把所有的 Lambda 表达式替换成匿名内部类就可以了。但实际情况并非如此,在 JVM 层面,Lambda 表达式和匿名内部类其实有着明显的差别。

5.1 匿名内部类的实现

匿名内部类仍然是一个类,只是不需要我们显式指定类名,编译器会自动为该类取名。比如有如下形式的代码:

public class LambdaTest {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World");
            }
        }).start();
    }
}

编译之后将会产生两个 class 文件:

LambdaTest.class
LambdaTest$1.class

使用 javap -c LambdaTest.class 进一步分析 LambdaTest.class 的字节码,部分结果如下:

public static void main(java.lang.String[]);
  Code:
     0: new           #2                  // class java/lang/Thread
     3: dup
     4: new           #3                  // class com/example/myapplication/lambda/LambdaTest$1
     7: dup
     8: invokespecial #4                  // Method com/example/myapplication/lambda/LambdaTest$1."<init>":()V
    11: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
    14: invokevirtual #6                  // Method java/lang/Thread.start:()V
    17: return

可以发现在 4: new #3 这一行创建了匿名内部类的对象。

5.2 Lambda 表达式的实现

接下来我们将上面的示例代码使用 Lambda 表达式实现,代码如下:

public class LambdaTest {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello World")).start();
    }
}

此时编译后只会产生一个文件 LambdaTest.class,再来看看通过 javap 对该文件反编译后的结果:

public static void main(java.lang.String[]);
  Code:
     0: new           #2                  // class java/lang/Thread
     3: dup
     4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
     9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
    12: invokevirtual #5                  // Method java/lang/Thread.start:()V
    15: return

从上面的结果我们发现 Lambda 表达式被封装成了主类的一个私有方法,并通过 invokedynamic 指令进行调用。

因此,我们可以得出结论:Lambda 表达式是通过 invokedynamic 指令实现的,并且书写 Lambda 表达式不会产生新的类。

既然 Lambda 表达式不会创建匿名内部类,那么在 Lambda 表达式中使用 this 关键字时,其指向的是外部类的引用。

6. 优缺点

优点:

  • 可以减少代码的书写,减少匿名内部类的创建,节省内存占用。
  • 使用时不用去记忆所使用的接口和抽象函数。

缺点:

  • 易读性较差,阅读代码的人需要熟悉 Lambda 表达式和抽象函数中参数的类型。
  • 不方便进行调试。

参考

原文链接:https://blog.csdn.net/u013541140/article/details/102710138

]]>
<![CDATA[RESTful 规范 Api 最佳设计实践]]> https://www.ydyno.com/archives/1194.html 2019-10-23T18:45:00+08:00 2019-10-23T18:45:00+08:00 知了小站 https://www.ydyno.com RESTful是目前比较流行的接口路径设计规范,基于HTTP,一般使用JSON方式定义,通过不同HttpMethod来定义对应接口的资源动作,如:新增(POST)、删除(DELETE)、更新(PUT、PATCH)、查询(GET)等。

路径设计

在RESTful设计规范内,每一个接口被认为是一个资源请求,下面我们针对每一种资源类型来看下API路径设计。

路径设计的注意事项如下所示:

  1. 资源名使用复数
  2. 资源名使用名词
  3. 路径内不带特殊字符
  4. 避免多级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格式,接口返回的是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;
}
  • data

由于每一个API的响应数据类型不一致,所以在上面采用的泛型的泛型进行返回,data可以返回任意类型的数据。

  • code

业务逻辑异常码,比如:USER_NOT_FOUND(用户不存在)这是接口的约定

  • errorMsg

对应code值得描述。

  • timestamp

请求响应的时间戳

总结

RESTful是API的设计规范,并不是所有的接口都应该遵循这一套规范来设计,不过我们在设计初期更应该规范性,这样我们在后期阅读代码时根据路径以及请求方式就可以了解接口的主要完成的工作。

作者:恒宇少年
链接:https://www.jianshu.com/p/35f1d3222cde
来源:简书

]]>
<![CDATA[Spring boot 整合 FreeMarker 实现代码生成功能]]> https://www.ydyno.com/archives/1177.html 2019-10-17T17:14:00+08:00 2019-10-17T17:14:00+08:00 知了小站 https://www.ydyno.com 在我们开发一个新的功能的时候,会根据表创建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());

QQ截图20191018201655.png
知道表的数据后,可以查询出表字段的详细数据,这里用 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";

QQ截图20191018202200.png
有了表字段信息的数据后,通过程序将数据库表字段类型转换成Java语言的字段类型,再通过FreeMarker创建模板,将数据写入到模板,输出成文件即可实现代码生成功能。

代码实现

这里只贴出核心代码,源码可查询文末地址,首先创建一个新的spring boot 项目,选择如下依赖
QQ截图20191018210142.png
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>

项目结构如下
QQ截图20191019102804.png

教程开始

修改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

]]>