linyb极客之路

linyb极客之路 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

linyb极客之路 发布了文章 · 10月10日

springcloud本地开发的微服务如何调用远程k8s的微服务

前言

一般来说k8s使用的容器网络与开发者的所在的办公网络并不能直接连通,如何在开发环境访问k8s的服务,就成为我们日常开发绕不开的坎。下边就介绍几种可以方便我们在本地环境调用k8s服务方案

方案一:Telepresence

1、Telepresence简介

Telepresence是一款为Kubernetes微服务框架提供快速本地化开发功能的开源软件。它的工作原理是在本地和 Kubernetes 集群中搭建一个透明的双向代理,它将集群中的数据卷、环境变量、网络都代理到了本地。其官网如下

https://www.telepresence.io/

2、Telepresence能帮我们实现什么
  • 本地服务可以完全访问远程群集中的其他服务;
  • 本地服务可以完全访问Kubernetes的环境变量,Secrets和ConfigMap;
  • K8S中运行的远程服务也可以完全访问本地服务。
3、实践步骤

a安装kubectl命令行工具,并配置本地可以访问Kubernetes集群

b安装Telepresence工具

c通过Telepresence工具启动本地服务

ps: 因为Telepresence目前主要支持Mac和linux环境,对window虽然也支持,但用window的安装方式,相比其他两种繁琐一些。而我的本地环境是window环境,因此我没采用这种方式。如果对如何利用Telepresence访问k8s感兴趣的朋友可以查看如下链接

Telepresence:让微服务本地开发不再难

自从用上 Telepresence 后,本地调试 Kubernetes 中的微服务不再是梦!

方案二:Kt Connect

1、Kt Connect简介

KT Connect ( Kubernetes Developer Tool ) 是轻量级的面向 Kubernetes 用户的开发测试环境治理辅助工具。其核心是通过建立本地到集群以及集群到本地的双向通道,从而提升在持续交付生命周期中开发环节的效率问题以及开发测试环境的复用问题。其官网如下

https://alibaba.github.io/kt-connect/#/

2、Kt Connect能帮我们实现什么

a、直接访问Kubernetes集群

开发者通过KT可以直接连接Kubernetes集群内部网络,在不修改代码的情况下完成本地开发与联调测试

b、转发集群流量到本地

开发者可以将集群中的流量转发到本地,从而使得集群中的其它服务可以联调本地

c、Service Mesh支持

对于使用Istio的开发者,KT支持创建一个指向本地的Version版本

d、基于SSH的轻量级VPN网络

KT使用shhuttle作为网络连接实现,实现轻量级的SSH VPN网络

e、作为kubectl插件,集成到Kubectl

开发者也可以直接将ktctl集成到kubectl中

3、实践步骤

a安装kubectl命令行工具,并配置本地可以访问Kubernetes集群

以在window环境安装kubectl命令行工具为例(ps:本文的k8s是直接使用云厂商的k8s服务)

3.1、 下载kubectl

请到kubernetes版本发布页面下载与集群版本对应的或者更新的kubectl。其下载链接如下

https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/README.md

3.2、 安装kubectl后,配置一下环境变量 ,并用管理员cmd命令验证一下安装是否成功

C:\WINDOWS\system32>kubectl version --client
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.9", GitCommit:"4fb7ed12476d57b8437ada90b4f93b17ffaeed99", GitTreeState:"clean", BuildDate:"2020-07-15T16:18:16Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"windows/amd64"}

3.2、 配置config文件

在C:UsersAdministrator目录下新建.kube文件夹,并在该文件夹下新建config文件,并把kubeconfig内容拷贝到config文件中。

3.3、 验证是否可以访问Kubernetes集群

C:\WINDOWS\system32>kubectl cluster-info
Kubernetes master is running at https://apiserver地址
CoreDNS is running at https:/apiserver地址/api/v1/namespaces/kube-system/services/coredns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

其详细的安装步骤可以查看如下文档

https://www.kubernetes.org.cn/installkubectl

如果是使用云厂商的k8s服务,比如是使用华为云cce,则可以根据如下文档进行安装

https://support.huaweicloud.com/usermanual-cce/cce_01_0107.html

如果是使用阿里云的k8s服务,则可以根据如下文档进行安装

https://help.aliyun.com/document_detail/86494.html

b、安装KT Connect

以在window安装为例,下载Windows可执行文件,并解压.exe文件到PATH路径下。其下载地址如下

https://alibaba.github.io/kt-connect/#/zh-cn/nightly

把下载的ktctl_windows_amd64.exe重命名为ktctl.exe

c、验证KT Connect是否安装成功

C:\WINDOWS\system32>ktctl -v
KT Connect version 0.0.10

d、通过KT Connect创建SOCKS5代理,其执行命令如下

ktctl --debug --image=registry.cn-hangzhou.aliyuncs.com/rdc-incubator/kt-connect-shadow:stable --namespace=dev connect --method=socks5

ps: 不指定镜像的话,默认就是

registry.cn-hangzhou.aliyuncs.com/rdc-incubator/kt-connect-shadow:stable

影子服务.png
e、在idea如何进行联调

这边有两种方式,一种是在IDEA的工作目录下使用ktctl启动本地到集群的socks5代理服务:

ktctl --debug --image=registry.cn-hangzhou.aliyuncs.com/rdc-incubator/kt-connect-shadow:stable --namespace=dev connect --method=socks5

在运行完成后ktctl会自动在当前工作区生成.jvmrc文件,如下所示:

-DsocksProxyHost=127.0.0.1
-DsocksProxyPort=2223

IDEA使用.jvmrc自动设置Java启动参数,下载并安装最新版本的JVM Inject插件
在这里插入图片描述
JVM Inject插件会在Java程序启动时自动读取.jvmrc文件,并追加到程序的启动参数中,如下所示:

java ...省略的其他输出... -Djava.rmi.server.hostname=127.0.0.1 -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=2223 "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=58609:/Applications/IntelliJ IDEA.app/Contents/bin" ...省略的其他输出...  sample.Application

另外一种我们也可以采用如下配置
idea配置kt-connect.png
通过设置-Dhttp.proxyHost和-Dhttp.proxyPort启动参数,在Java程序中所有网络请求完全通过KT Connect进行转发。从而可以直接在代码中访问Kubernetes集群中的服务。

f、验证

示例中xxl-job是部署在k8s集群内,当我们本地服务没有使用kt-connect代理,启动项目,则报了如下错

>>>>>>>>>>> xxl-job registry fail, registryParam:RegistryParam{registryGroup='EXECUTOR', registryKey='stock-service-executor', registryValue='http://192.168.1.2:9993/'}, registryResult:ReturnT [code=500, msg=xxl-rpc remoting error(Can't connect to SOCKS proxy:Connection refused: connect), for url : http://dev-job-admin.dev.svc.cluster.local:8060/api/registry, content=null]

当开启代理后,控制台输出如下内容

 >>>>>>>>>>> xxl-job register jobhandler success, name:shardingJobHandler, jobHandler:com.xxl.job.core.handler.impl.MethodJobHandler@35f35c59[class com.linyb.stock.handler.SampleXxlJob#shardingJobHandler]

说明我们本地服务已经成功访问k8s集群中的服务

其他方案

通过vpn或者类似iptable工具来进行转发

总结

以上的方案,我在实际落地时,是选用kt-connect,感兴趣的朋友可以尝试一下。其实在开发环境时,也可以直接使用docker-compose来进行服务编排,复杂度也不是那么高

参考文档

https://www.telepresence.io/

https://alibaba.github.io/kt-...

https://www.kubernetes.org.cn/

https://www.v2ex.com/t/584314

查看原文

赞 0 收藏 0 评论 0

linyb极客之路 发布了文章 · 9月19日

java应用线上诊断神器--Arthas

前言

1、什么是Arthas?

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱(截止2020.9.19 github star是23K)。通过Arthas我们可以在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。

2、Arthas有哪些特性

  • 实时查看系统的运行状况
  • 查看函数调用的参数,返回值和异常
  • 代码在线热更新
  • 秒解类冲突问题,定位类加载路径
  • 快速定位应用的热点,生成火焰图
  • 在线诊断,点开网页诊断线上应用

3、Arthas能帮我们解决什么问题

当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  • 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  • 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  • 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  • 是否有一个全局视角来查看系统的运行状况?
  • 有什么办法可以监控到JVM的实时运行状态?
  • 怎么快速定位应用的热点,生成火焰图?

4、安装

下载arthas-boot.jar,然后用java -jar的方式启动:

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

本文的示例项目是运行在docker,因此就采用了另外的方式

docker exec -it  ${containerId} /bin/bash -c "wget https://arthas.aliyun.com/arthas-boot.jar && java -jar arthas-boot.jar"

不过在执行的过程中,可能会出现

/bin/bash: wget: command not found

解决方案如下

进入容器的/bin/bash
docker exec -it  ${containerId} /bin/bash
apt-get update
apt-get install wget

命令解读

1、帮助命令相关

help(查看命令帮助信息

会列这个命令,源于个人习惯吧。每当学习一个新东西,都会习惯看下帮助,通读一下
help.png
当你想了解具体命令的详细用法,以thread为例,输入

help thread

thread命令详解.png
就会有详细的thread参数、例子介绍。感觉本文的精华就是这个了,毕竟你想要其他命令,直接

help 命令

但为了水文,就再介绍几类命令

2、jvm相关

dashboard(实时展示当前系统诸如线程、内存占用、GC等信息的面板,默认每个5秒刷新一下面板

dashboard.png
:按ctrl+c可以退出面板

jvm(查看当前JVM信息比如gc回收次数以及耗时等)

jvm.png

thread(查看当前线程信息,查看线程的堆栈

a、 查看当前最忙的前N个线程并打印堆栈

thread -n 3

thread查看繁忙的线程.png
上述的命令实现的效果就和我们以往输入

top -H -p pid
printf '%x\n'pid
jstack pid |grep 'nid' -C5 –color

类似

b、 thread -b, 找出当前阻塞其他线程的线程

注: 目前只支持找出synchronized关键字阻塞住的线程, 如果是java.util.concurrent.Lock, 目前还不支持

c、 thread --state ,查看指定状态的线程

thread根据state查看线程.png

3、日志相关

logger(查看logger信息,更新logger level

3.1、 查看logger信息
logger.png
3.2、 动态更新logger level

修改日志级别步骤

a、 查找当前类的classloader hashcode

sc -d com.example.springdemo.user.service.impl.UserServiceImpl | grep classLoaderHash

b、 用OGNL获取logger

ognl -c 31cefde0 '@com.example.springdemo.user.service.impl.UserServiceImpl@log'

查看日志level.png
从上图可以知道com.example.springdemo.user.service.impl.UserServiceImpl@log实际使用的是logback。
可以看到level=null,则说明实际最终的level是从root logger里来的。

c、 单独设置UserServiceImpl的logger level
把日志级别变更为warn


ognl -c 31cefde0 '@com.example.springdemo.user.service.impl.UserServiceImpl@log.setLevel(@ch.qos.logback.classic.Level@WARN)'

日志变更.png
可以看出日志级别已经改为warn

4、class/classloader相关

jad(反编译指定已加载类的源码)

jad反编译.png

sc(查看JVM已加载的类信息)

mc(内存编译器,编译.java文件生成.class)

redefine(加载外部的.class文件,redefine jvm已加载的类)

为啥介绍这几个,因为这几个组合起来就可以实现动态在线更新代码了。其步骤如下

a、jad反编译要更新的代码


jad --source-only com.example.springdemo.user.service.impl.UserServiceImpl > /tmp/UserServiceImpl.java

b、sc查找加载要更新代码的ClassLoader

sc -d com.example.springdemo.user.service.impl.UserServiceImpl | grep classLoaderHash

c、保存好/tmp/UserServiceImpl.java之后,使用mc(Memory Compiler)命令来编译,并且通过--classLoaderClass参数指定ClassLoader


mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/UserServiceImpl.java -d /tmp

d、使用redefine命令重新加载新编译好的UserServiceImpl.class

redefine /tmp/com/example/springdemo/user/service/impl/UserServiceImpl.class

5、监控相关

monitor(方法执行监控,可以监控方法的调用次数、成功次数、失败次数、平均响应时间、失败率)

注: 这是一个非实时返回命令,统计周期,默认值为120秒

monitor -c 5 com.example.springdemo.user.service.impl.UserServiceImpl getUserById

monitor.png

watch(观察指定方法的调用情况。能观察到的范围为:返回值、抛出异常、入参)
watch com.example.springdemo.user.service.impl.UserServiceImpl getUserById "{params,returnObj}" -x 2

watch.png
watch参数说明
watch参数说明.png

trace(方法内部调用路径,并输出方法路径上的每个节点上耗时)

注: trace 能方便的帮助你定位和发现因 RT 高而导致的性能问题缺陷,但其每次只能跟踪一级方法的调用链路。

trace com.example.springdemo.user.service.impl.UserServiceImpl getUserById

trace.png

stack(输出当前方法被调用的调用路径)
stack com.example.springdemo.user.service.impl.UserServiceImpl getUserById

stack.png

tt(方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测)

这个命令的厉害之处在于记录下当前方法的每次调用环境现场,并能进行重放

tt -t com.example.springdemo.user.service.impl.UserServiceImpl getUserById

tt.png
tt表格字段说明.png
b、选择一个index进行重放

tt -i 1000 -p

tt重放.png
:这些监控命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 stop 或将增强过的类执行 reset 命令。

总结

本文主要介绍Arthas的的一些用法,而这些方法在官网都有很详细的介绍。如果对Arthas感兴趣的朋友的,可以访问官网

https://arthas.aliyun.com/zh-cn/

同时也推荐大家访问

https://arthas.aliyun.com/doc...

里面除了可以在线实操Arthas还可以查看相关用户案例,有助于大家进一步上手Arthas

参考文档

https://arthas.aliyun.com/zh-cn/
查看原文

赞 0 收藏 0 评论 0

linyb极客之路 发布了文章 · 9月12日

springboot2结合mybatis拦截器实现主键自动生成

前言

前阵子和朋友聊天,他说他们项目有个需求,要实现主键自动生成,不想每次新增的时候,都手动设置主键。于是我就问他,那你们数据库表设置主键自动递增不就得了。他的回答是他们项目目前的id都是采用雪花算法来生成,因此为了项目稳定性,不会切换id的生成方式。

朋友问我有没有什么实现思路,他们公司的orm框架是mybatis,我就建议他说,不然让你老大把mybatis切换成mybatis-plus。mybatis-plus就支持注解式的id自动生成,而且mybatis-plus只是对mybatis进行增强不做改变。朋友还是那句话,说为了项目稳定,之前项目组没有使用mybatis-plus的经验,贸然切换不知道会不会有什么坑。后面没招了,我就跟他说不然你用mybatis的拦截器实现一个吧。于是又有一篇吹水的创作题材出现。

前置知识

在介绍如何通过mybatis拦截器实现主键自动生成之前,我们先来梳理一些知识点

1、mybatis拦截器的作用

mybatis拦截器设计的初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动mybatis固有的逻辑

2、Interceptor拦截器

每个自定义拦截器都要实现

org.apache.ibatis.plugin.Interceptor

这个接口,并且自定义拦截器类上添加@Intercepts注解

3、拦截器能拦截哪些类型
  • Executor:拦截执行器的方法。
  • ParameterHandler:拦截参数的处理。
  • ResultHandler:拦截结果集的处理。
  • StatementHandler:拦截Sql语法构建的处理。
4、拦截的顺序

a、不同类型拦截器的执行顺序

Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

b、多个拦截器拦截同种类型同一个目标方法,执行顺序是后配置的拦截器先执行

比如在mybatis配置如下

    <plugins>
        <plugin interceptor="com.lybgeek.InterceptorA" />
        <plugin interceptor="com.lybgeek.InterceptorB" />
    </plugins>

则InterceptorB先执行。

如果是和spring做了集成,先注入spring ioc容器的拦截器,则后执行。比如有个mybatisConfig,里面有如下拦截器bean配置

 @Bean
    public InterceptorA interceptorA(){
        return new InterceptorA();
    }

    @Bean
    public InterceptorB interceptorB(){
        return new InterceptorB();
    }

则InterceptorB先执行。当然如果你是直接用@Component注解这形式,则可以配合@Order注解来控制加载顺序

5、拦截器注解介绍

@Intercepts:标识该类是一个拦截器

@Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法。
@Signature注解属性中的type表示对应可以拦截四种类型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的一种;method表示对应类型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的哪类方法;args表示对应method中的参数类型

6、拦截器方法介绍

a、 intercept方法

public Object intercept(Invocation invocation) throws Throwable

这个方法就是我们来执行我们自己想实现的业务逻辑,比如我们的主键自动生成逻辑就是在这边实现。

Invocation这个类中的成员属性target就是@Signature中的type;method就是@Signature中的method;args就是@Signature中的args参数类型的具体实例对象

b、 plugin方法

public Object plugin(Object target)

这个是用返回代理对象或者是原生代理对象,如果你要返回代理对象,则返回值可以设置为

Plugin.wrap(target, this);
this为拦截器

如果返回是代理对象,则会执行拦截器的业务逻辑,如果直接返回target,就是没有拦截器的业务逻辑。说白了就是告诉mybatis是不是要进行拦截,如果要拦截,就生成代理对象,不拦截是生成原生对象

c、 setProperties方法

public void setProperties(Properties properties)

用于在Mybatis配置文件中指定一些属性

主键自动生成思路

1、定义一个拦截器

主要拦截

 `Executor#update(MappedStatement ms, Object parameter)`} 

这个方法。mybatis的insert、update、delete都是通过这个方法,因此我们通过拦截这个这方法,来实现主键自动生成。其代码块如下

@Intercepts(value={@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
public class AutoIdInterceptor implements Interceptor {}

2、判断sql操作类型

Executor 提供的方法中,update 包含了 新增,修改和删除类型,无法直接区分,需要借助 MappedStatement 类的属性 SqlCommandType 来进行判断,该类包含了所有的操作类型

public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

当SqlCommandType类型是insert我们才进行主键自增操作

3、填充主键值

3.1、编写自动生成id注解
Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoId {
    /**
     * 主键名
     * @return
     */
    String primaryKey();

    /**
     * 支持的主键算法类型
     * @return
     */
    IdType type() default IdType.SNOWFLAKE;

    enum IdType{
        SNOWFLAKE
    }
}
3.2、 雪花算法实现

我们可以直接拿hutool这个工具包提供的idUtil来直接实现算法。

引入

 <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
Snowflake snowflake = IdUtil.createSnowflake(0,0);
long value = snowflake.nextId();
3.3、填充主键值

其实现核心是利用反射。其核心代码片段如下

   ReflectionUtils.doWithFields(entity.getClass(), field->{
                    ReflectionUtils.makeAccessible(field);
                    AutoId autoId = field.getAnnotation(AutoId.class);
                    if(!ObjectUtils.isEmpty(autoId) && (field.getType().isAssignableFrom(Long.class))){
                        switch (autoId.type()){
                            case SNOWFLAKE:
                                SnowFlakeAutoIdProcess snowFlakeAutoIdProcess = new SnowFlakeAutoIdProcess(field);
                                snowFlakeAutoIdProcess.setPrimaryKey(autoId.primaryKey());
                                finalIdProcesses.add(snowFlakeAutoIdProcess);
                                break;
                        }
                    }
                });
public class SnowFlakeAutoIdProcess extends BaseAutoIdProcess {

    private static Snowflake snowflake = IdUtil.createSnowflake(0,0);


    public SnowFlakeAutoIdProcess(Field field) {
        super(field);
    }

    @Override
    void setFieldValue(Object entity) throws Exception{
        long value = snowflake.nextId();
        field.set(entity,value);
    }
}

如果项目中的mapper.xml已经的insert语句已经含有id,比如

insert into sys_test( `id`,`type`, `url`,`menu_type`,`gmt_create`)values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})

则只需到填充id值这一步。拦截器的任务就完成。如果mapper.xml的insert不含id,形如

insert into sys_test( `type`, `url`,`menu_type`,`gmt_create`)values( #{type}, #{url},#{menuType},#{gmtCreate})

则还需重写insert语句以及新增id参数

4、重写insert语句以及新增id参数(可选)

4.1 重写insert语句

方法一:
从 MappedStatement 对象中获取 SqlSource 对象,再从从 SqlSource 对象中获取获取 BoundSql 对象,通过 BoundSql#getSql 方法获取原始的sql,最后在原始sql的基础上追加id

方法二:

引入

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

通过

com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser

获取相应的表名、需要insert的字段名。然后重新拼凑出新的insert语句

4.2 把新的sql重置给Invocation

其核心实现思路是创建一个新的MappedStatement,新的MappedStatement绑定新sql,再把新的MappedStatement赋值给Invocation的args[0],代码片段如下

 private void resetSql2Invocation(Invocation invocation, BoundSqlHelper boundSqlHelper,Object entity) throws SQLException {
        final Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        MappedStatement newStatement = newMappedStatement(statement, new BoundSqlSqlSource(boundSqlHelper));
        MetaObject msObject =  MetaObject.forObject(newStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory());
        msObject.setValue("sqlSource.boundSqlHelper.boundSql.sql", boundSqlHelper.getSql());

            args[0] = newStatement;

    }
4.3 新增id参数

其核心是利用

org.apache.ibatis.mapping.ParameterMapping

核心代码片段如下

  private void setPrimaryKeyParaterMapping(String primaryKey) {
           ParameterMapping parameterMapping = new ParameterMapping.Builder(boundSqlHelper.getConfiguration(),primaryKey,boundSqlHelper.getTypeHandler()).build();
           boundSqlHelper.getBoundSql().getParameterMappings().add(parameterMapping);
       }
5、将mybatis拦截器注入到spring容器

可以直接在拦截器上加

@org.springframework.stereotype.Component

注解。也可以通过

 @Bean
    public AutoIdInterceptor autoIdInterceptor(){
        return new AutoIdInterceptor();
    }
6、在需要实现自增主键的实体字段上加如下注解
@AutoId(primaryKey = "id")
    private Long id;

测试

1、对应的测试实体以及单元测试代码如下
@Data
public class TestDO implements Serializable {
    private static final long serialVersionUID = 1L;

    @AutoId(primaryKey = "id")
    private Long id;
    private Integer type;
    private String url;
    private Date gmtCreate;
    private String menuType;
}
@Autowired
    private TestService testService;

    @Test
    public void testAdd(){
        TestDO testDO = new TestDO();
        testDO.setType(1);
        testDO.setMenuType("1");
        testDO.setUrl("www.test.com");
        testDO.setGmtCreate(new Date());
        testService.save(testDO);
        testService.get(110L);
    }

    @Test
    public void testBatch(){
        List<TestDO> testDOList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            TestDO testDO = new TestDO();
            testDO.setType(i);
            testDO.setMenuType(i+"");
            testDO.setUrl("www.test"+i+".com");
            testDO.setGmtCreate(new Date());
            testDOList.add(testDO);
        }

        testService.saveBatch(testDOList);
    }
2、当mapper的insert语句中含有id,形如下
<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
        insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`)
        values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})
    </insert>

以及批量插入sql

<insert id="saveBatch"  parameterType="java.util.List" useGeneratedKeys="false">
        insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`)
        values
        <foreach collection="list" item="test" index="index" separator=",">
            ( #{test.id},#{test.gmtCreate},#{test.type}, #{test.url},
            #{test.menuType})
        </foreach>
    </insert>

查看控制台sql打印语句

15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save - ==>  Preparing: insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`) values( ?,?, ?,?,? ) 
15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save - ==> Parameters: 356829258376544258(Long), 1(Integer), www.test.com(String), 1(String), 2020-09-11 15:52:04.738(Timestamp)
15:52:04 [main] DEBUG com.nlybgeek.dao.TestDao.save - <==    Updates: 1
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==>  Preparing: insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`) values ( ?,?,?, ?, ?) , ( ?,?,?, ?, ?) , ( ?,?,?, ?, ?) 
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==> Parameters: 356829258896637961(Long), 2020-09-11 15:52:04.847(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356829258896637960(Long), 2020-09-11 15:52:04.847(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356829258896637962(Long), 2020-09-11 15:52:04.847(Timestamp), 2(Integer), www.test2.com(String), 2(String)
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==    Updates: 3

查看数据库
sql语句.png

3、当mapper的insert语句中不含id,形如下
<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
        insert into sys_test(`type`, `url`,`menu_type`,`gmt_create`)
        values(#{type}, #{url},#{menuType},#{gmtCreate})
    </insert>

以及批量插入sql

<insert id="saveBatch"  parameterType="java.util.List" useGeneratedKeys="false">
        insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`)
        values
        <foreach collection="list" item="test" index="index" separator=",">
            (#{test.gmtCreate},#{test.type}, #{test.url},
            #{test.menuType})
        </foreach>
    </insert>

查看控制台sql打印语句

15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - ==>  Preparing: insert into sys_test(`type`,`url`,`menu_type`,`gmt_create`,id) values (?,?,?,?,?) 
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - ==> Parameters: 1(Integer), www.test.com(String), 1(String), 2020-09-11 15:59:46.741(Timestamp), 356831196144992264(Long)
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - <==    Updates: 1
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==>  Preparing: insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`,id) values (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?) 
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==> Parameters: 2020-09-11 15:59:46.845(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356831196635725829(Long), 2020-09-11 15:59:46.845(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356831196635725828(Long), 2020-09-11 15:59:46.845(Timestamp), 2(Integer), www.test2.com(String), 2(String), 356831196635725830(Long)
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==    Updates: 3

从控制台我们可以看出,当mapper.xml没有配置id字段时,则拦截器会自动帮我们追加id字段

查看数据库
sql语句1.png

总结

本文虽然是介绍mybatis拦截器实现主键自动生成,但文中更多讲解如何实现一个拦截器以及主键生成思路,并没把intercept实现主键方法贴出来。其原因主要是主键自动生成在mybatis-plus里面就有实现,其次是有思路后,大家就可以自己实现了。最后对具体实现感兴趣的朋友,可以查看文末中demo链接

参考文档

mybatis拦截器
mybatis插件实现自定义改写表名
mybatis拦截器,动态修改sql语句

demo链接

https://github.com/lyb-geek/s...
查看原文

赞 0 收藏 0 评论 0

linyb极客之路 发布了文章 · 9月10日

mybatis升级为mybatis-plus踩到的坑

前言

最近使用RuoYi-Vue来做后台管理脚手架。RuoYi-Vue 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Spring Security、MyBatis、Jwt、Vue),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、代码生成等。在线定时任务配置;支持集群,支持多数据源。其官方文档如下

http://doc.ruoyi.vip/

感兴趣的朋友,可以点链接查看。这个平台目前的orm框架是mybatis,而项目组的orm框架是mybatis-plus。为了统一技术栈,项目组就决定把若依的orm框架升级为mybatis-plus。因为之前就有过把mybatis升级为mybatis-plus的经验,就感觉这个升级是很简单。但是在改造后,运行程序却报了形如下异常

Invalid bound statement (not found): com.lybgeek.admin.file.mapper.FileMapper.insert

排查

从异常的字面意思是说,FIleMapper中的insert方法没有绑定。查看FileMapper.xml配置,确实没有发现绑定insert这个sql语句块。那是否加上insert的sql语句块,就能解决问题?加上确实是能解决问题。

但如果用过mybatis-plus的朋友,应该会知道,mybatis-plus中BaseMapper已经帮我们封装好了一系列的单表增删改查,我们无需写配置,就可以实现单表增删改查。所以在xml配置insert是治标不治本。

那要如何排查呢?

1、方向一:是否是包冲突引起?

利用maven helper插件包冲突

包冲突.png
从图可以看出不是包冲突引起的。

注: 因为之前吃过包冲突的亏,因此在把若依的orm改成mybatis-plus之前,就已经去除跟mybatis相关的 jar冲突了

方向二:是不是引入不同类包的BaseMapper

我们引入的必须是

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

而不是

import com.baomidou.mybatisplus.mapper.BaseMapper;

不过出现这个问题,通常也是引入不同版本的mybatis-plus jar才会出现。如果你是只用3+以上版本,他引入就只有

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
方向三:通用方法(断点调试)

其实代码排查最怕就是异常栈被吃了,如果有异常信息,排查方向相对比较好找。比如这个异常,其异常栈信息为

Caused by: org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.lybgeek.admin.file.mapper.FileMapper.insert
    at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:235)
    at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:53)
    at org.apache.ibatis.binding.MapperProxy.lambda$cachedInvoker$0(MapperProxy.java:107)
    at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)
    at org.apache.ibatis.binding.MapperProxy.cachedInvoker(MapperProxy.java:94)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:85)
    at com.sun.proxy.$Proxy129.insert(Unknown Source)
    at com.baomidou.mybatisplus.extension.service.IService.save(IService.java:59)
    at com.baomidou.mybatisplus.extension.service.IService$$FastClassBySpringCGLIB$$f8525d18.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)

我们从异常栈信息,我们可以知道这个异常从

org.apache.ibatis.binding.MapperMethod

这个类抛出,于是我们可以把断点先设置到这边。通过源码我们可以得知org.apache.ibatis.mapping.MappedStatement空了,导致报了如上异常,而MappedStatement又是由

org.apache.ibatis.session.Configuration

提供。而Configuration是通过

org.apache.ibatis.session.SqlSessionFactory

进行设置。然后继续排查,就会发现

com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration

这个自动装配类。里面有这么一段代码

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }
        applyConfiguration(factory);
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }
        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }
        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        if (this.properties.getTypeAliasesSuperType() != null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }
        Resource[] mapperLocations = this.properties.resolveMapperLocations();
        if (!ObjectUtils.isEmpty(mapperLocations)) {
            factory.setMapperLocations(mapperLocations);
        }

        // TODO 对源码做了一定的修改(因为源码适配了老旧的mybatis版本,但我们不需要适配)
        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
        if (!ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
        }
        Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);

        // TODO 自定义枚举包
        if (StringUtils.hasLength(this.properties.getTypeEnumsPackage())) {
            factory.setTypeEnumsPackage(this.properties.getTypeEnumsPackage());
        }
        // TODO 此处必为非 NULL
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
        // TODO 注入填充器
        this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
        // TODO 注入主键生成器
        this.getBeanThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerator(i));
        // TODO 注入sql注入器
        this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
        // TODO 注入ID生成器
        this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
        // TODO 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
        factory.setGlobalConfig(globalConfig);
        return factory.getObject();
    }

作者在注释上都写了,要用

 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean

于是查看若依代码,发现在若依中的mybatis配置类中有配置如下代码片段

 @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
    {
        String typeAliasesPackage = env.getProperty("mybatis.type-aliases-package");
        String mapperLocations = env.getProperty("mybatis.mapper-locations");
        String configLocation = env.getProperty("mybatis.config-location");
        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);

        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
        return sessionFactory.getObject();
    }

从MybatisPlusAutoConfiguration的源码中,我们可以得知,当项目已经有配置SqlSessionFactory。mybatis-plus将不会自动帮我们注入SqlSessionFactory,而使用我们自己定义的SqlSessionFactory。而若依项目配置的SqlSessionFactory不是MybatisSqlSessionFactoryBean

修复

1、方法一

把mybatis的SqlSessionFactoryBean替换成mybatis-plus的MybatisSqlSessionFactoryBean

2、方法二

去掉项目中sqlSessionFactory。这样mybatis-plus就会自动帮我们注入sqlSessionFactory

总结

可能有朋友会觉得遇到异常问题,直接通过搜索引擎找答案不就可以了。这确实是一个挺好的方法,但有时候可能搜索半天都没找到答案,我们就可以通过异常信息栈、以及调试线程栈,就可以得出一些比较有用的信息。出现异常并不可怕,可怕的是出了问题,异常日志信息被吞,都不知道从何排查。最后安利一下若依这个脚手架,管理后台开发神器,谁用谁知道

查看原文

赞 0 收藏 0 评论 0

linyb极客之路 发布了文章 · 8月30日

springboot2之优雅处理返回值

前言

最近项目组有个老项目要进行前后端分离改造,应前端同学的要求,其后端提供的返回值格式需形如

{
  "status": 0,
  "message": "success",
  "data": {
    
  }
}

方便前端数据处理。要实现前端同学这个需求,其实也挺简单的,仅需做如下改造,新增一个返回对象,形如

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Result<T> {
    public static final int success = 0;
    public static final int fail = 1;
    private int status = success;
    private String message = "success";
    private T data;


}

然后controller改造成如下


@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {


  @Autowired
  private UserService userService;

  @PostMapping(value="/add")
  public Result<UserDTO> addUser(@Valid UserDTO userDTO, BindingResult bindingResult){
    Result<UserDTO> result = new Result<>();
    if (bindingResult.hasErrors()){
      return getUserFailResult(bindingResult, result);
    }
    saveUser(userDTO, result);

    return result;

  }
}

仅仅需要这么改造就可以满足前端同学的述求。但这边存在一个问题就是,这个项目后端接口的contoller之前都是直接返回业务bean对象,形如下

@RestController
@Api(tags = "用户管理")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping(value="/get/{id}")
    @ApiOperation("根据用户ID查找用户")
    @ApiImplicitParam(value = "用户id",name = "id",required = true,paramType = "path")
    public UserDTO getUserById(@PathVariable("id") Long id){
        UserDTO dto = userService.getUserById(id);
        log.info("{}",dto);
        return dto;

    }
    }

如果按上面的思路

把UserDTO改造成Result<UserDTO>

虽然可以满足需求,但问题是后端这样的接口有好几十个,按这种改法很明显工作量比较大,更重要的不符合开闭原则--对扩展开放,对修改关闭。那有没有优雅一点的处理方式呢?答案是有的,利用
@RestControllerAdvice+ResponseBodyAdvice就可以满足我们的需求

改造

1、在改造前,先简单介绍一下@RestControllerAdvice和ResponseBodyAdvice

@RestControllerAdvice

@RestControllerAdvice这个注解是spring 4.3版本之后新增的注解。用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping。利用他可以来做异常统一处理。如果使用的spring低于4.3,那可以使用@ControllerAdvice+@ResponseBody。@ControllerAdvice是spring 3.2版本后就提供的注解,其实现的功能和@RestControllerAdvice类似。
其详细的参考文档,可以查看链接@RestControllerAdvice文档以及@ControllerAdvice文档

ResponseBodyAdvice

这个是spring4.1版本之后,新增的接口。其作用是允许在执行@ResponseBody或ResponseEntity控制器方法之后但在使用HttpMessageConverter编写正文之前自定义响应。可以直接在RequestMappingHandlerAdapter和ExceptionHandlerExceptionResolver中注册实现,也可以在@ControllerAdvice或者@RestControllerAdvice中注解。其详细参考文档可以查看链接ResponseBodyAdvice文档

2、编写一个通用的响应实体

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Result<T> {
    public static final int success = 0;
    public static final int fail = 1;
    private int status = success;
    private String message = "success";
    private T data;


}

3、编写一个类上加上@RestControllerAdvice并实现ResponseBodyAdvice接口。用来统一处理响应值

@RestControllerAdvice(basePackages = "com.github.lybgeek")
@Slf4j
public class ResponseAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if(Objects.isNull(o)){
            return Result.builder().message("success").build();
        }

        if(o instanceof Result){
            return o;
        }

        return Result.builder().message("success").data(o).build();
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> exceptionHandler(HttpServletRequest request, Exception e) {
        log.error(e.getMessage(), e);
        return Result.builder().message(e.getMessage()).status(Result.fail).build();
    }

    /**
     * 针对业务异常统一处理
     * @param request
     * @param bizException
     * @return
     */
    @ExceptionHandler(BizException.class)
    @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED)
    public Result<?> bizExceptionHandler(HttpServletRequest request, BizException bizException) {
            int errorCode = bizException.getCode();
            log.error("catch bizException {}", errorCode);
            return Result.builder().message(bizException.getMessage()).status(errorCode).build();
    }


    /**
     * 针对Validate校验异常统一处理
     * @param request
     * @param methodArgumentNotValidException
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Result<?> methodArgumentNotValidExceptionExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException methodArgumentNotValidException) {
        Result result = new Result();
        log.error("catch methodArgumentNotValidException :" + methodArgumentNotValidException.getMessage(), methodArgumentNotValidException);
        return ResultUtils.INSTANCE.getFailResult(methodArgumentNotValidException.getBindingResult(),result);
    }

    /**
     * 针对Assert断言异常统一处理
     * @param request
     * @param illegalArgumentExceptionException
     * @return
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED)
    public Result<?> illegalArgumentExceptionHandler(HttpServletRequest request, IllegalArgumentException illegalArgumentExceptionException) {
        log.error("illegalArgumentExceptionException:"+illegalArgumentExceptionException.getMessage(), illegalArgumentExceptionException);
        return Result.builder().message(illegalArgumentExceptionException.getMessage()).status(Result.fail).build();
    }

测试验证

1、编写业务DTO
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel
public class UserDTO implements Serializable {

  @NotNull(message = "编号不能为空",groups = {Update.class, Delete.class})
  @ApiModelProperty(value = "编号",name = "id",example = "1")
  private Long id;

  @NotBlank(message = "用户名不能为空",groups = {Add.class})
  @ApiModelProperty(value = "用户名",name = "userName",example = "zhangsan")
  private String userName;

  @NotBlank(message = "姓名不能为空",groups = {Add.class})
  @ApiModelProperty(value = "姓名",name = "realName",example = "张三")
  private String realName;

  @NotBlank(message = "密码不能为空",groups = {Add.class})
  @Size(max=32,min=6,message = "密码长度要在6-32之间",groups = {Add.class})
  @ApiModelProperty(value = "密码",name = "password",example = "123456")
  private String password;

  @NotNull(message = "性别不能为空",groups = {Add.class})
  @ApiModelProperty(value = "性别",name = "gender",example = "1")
  @EnumValid(target = Gender.class, message = "性别取值必须为0或者1",groups = {Add.class,Update.class})
  private Integer gender;

  @ApiModelProperty(value = "邮箱",name = "email",example = "zhangsan@qq.com")
  @Pattern(regexp = "^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\\.[a-zA-Z0-9]{2,6}$",message = "不满足邮箱正则表达式",groups = {Add.class,Update.class})
  private String email;



}
2、编写业务controller
@RestController
@Api(tags = "用户管理")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping(value="/get/{id}")
    @ApiOperation("根据用户ID查找用户")
    @ApiImplicitParam(value = "用户id",name = "id",required = true,paramType = "path")
    public UserDTO getUserById(@PathVariable("id") Long id){
        UserDTO dto = userService.getUserById(id);
        log.info("{}",dto);
        return dto;

    }

    @PostMapping(value="/add")
    @ApiOperation("添加用户")
    public UserDTO add(@RequestBody @Validated({Add.class}) UserDTO userDTO){
        log.info("{}",userDTO);
        return userService.save(userDTO);
    }

    @PostMapping(value="/update")
    @ApiOperation("更新用户")
    public UserDTO update(@RequestBody @Validated({Update.class}) UserDTO userDTO){
        log.info("{}",userDTO);
        return userService.save(userDTO);
    }

    @DeleteMapping(value="/detele")
    @ApiOperation("删除用户")
    public boolean delete(@Validated({Delete.class}) UserDTO userDTO){
        log.info("id:{}",userDTO.getId());
        return userService.delete(userDTO.getId());
    }
}

注: 业务service就不贴了和文章内容关系不大。如果感兴趣的朋友,可以从文末提供的链接进行查看

3、利用swagger在线接口文档进行测试

a:正常响应时,返回值形如下

{
  "status": 0,
  "message": "success",
  "data": {
    "id": 1,
    "userName": "zhangsan",
    "realName": "张三",
    "password": "123456",
    "gender": 1,
    "email": "zhangsan@qq.com"
  }
}

b:当数据校验异常时,返回值形如下

{
  "status": 1,
  "message": "姓名不能为空;",
  "data": null
}

c:当业务异常时,返回值形如下

{
  "status": 1,
  "message": "user is not found by id :3",
  "data": null
}

总结

本文主要介绍了如何利用@RestControllerAdvice和ResponseBodyAdvice来统一处理返回值。本文代码示例还实现了分组校验,自定义校验,利用mdc traceId日志埋点,如果对这些内容感兴趣的朋友,可以查看文末项目链接

demo链接

https://github.com/lyb-geek/s...
查看原文

赞 0 收藏 0 评论 0

linyb极客之路 发布了文章 · 8月9日

消息队列的消费幂等性如何保证

什么是幂等?

任意多次执行所产生的影响均与一次执行的影响相同就可以称为幂等

什么是消息幂等?

当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响

为什么我们要保证幂等性,不保证幂等性,会不会有问题?

这个问题其实没法准确回答。回答这个问题的根源得从业务场景上进行分析。比如正常业务情况下,我们是不允许同个订单重复支付,这种业务场景我们就需要确保幂等性。再比如日志记录,这种业务场景,我们可能就不需要做幂等判断。

因此是否要保证幂等性,得基于业务进行考量

消息队列的消费幂等性如何保证?

没法保证。前面说了要保证幂等性,得基于业务场景进行考量。消息队列他本身就不是给你用来做业务幂等性用的。如果你要实现业务幂等性,靠消息队列是没法帮你完成的,你自己得根据自身业务场景,来实现幂等。

常用的业务幂等性保证方法

1、利用数据库的唯一约束实现幂等

比如将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等

2、去重表

这个方案本质也是根据数据库的唯一性约束来实现。其实现大体思路是:首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重现重复消费,数据库会抛唯一约束异常,操作就会回滚

3、利用redis的原子性

每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中

4、多版本(乐观锁)控制

此方案多用于更新的场景下。其实现的大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1

5、状态机机制

此方案多用于更新且业务场景存在多种状态流转的场景

6、token机制

生产者发送每条数据的时候,增加一个全局唯一的id,这个id通常是业务的唯一标识,比如订单编号。在消费端消费时,则验证该id是否被消费过,如果还没消费过,则进行业务处理。处理结束后,在把该id存入redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。

演示

例子使用springboot2加kafka来演示一下使用token机制如何实现消费端幂等

1、application.yml
spring:
  redis:
    host: localhost
    port: 6379
    # 连接超时时间(毫秒)
    timeout: 10000
    jedis:
      pool:
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 10
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 100
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
    password:
  kafka:
    # 以逗号分隔的地址列表,用于建立与Kafka集群的初始连接(kafka 默认的端口号为9092)
    bootstrap-servers: localhost:9092
    producer:
      # 发生错误后,消息重发的次数。
      retries: 0
      #当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
      batch-size: 16384
      # 设置生产者内存缓冲区的大小。
      buffer-memory: 33554432
      # 键的序列化方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      # 值的序列化方式
      value-serializer: com.github.lybgeek.kafka.serialization.ObjectSerializer
      # acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
      # acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
      # acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
      acks: 1
    consumer:
      # 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
      auto-commit-interval: 1S
      # 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
      # latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
      # earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
      auto-offset-reset: earliest
      # 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移量
      enable-auto-commit: false
      # 键的反序列化方式
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 值的反序列化方式
      value-deserializer: com.github.lybgeek.kafka.serialization.ObjectDeserializer
    listener:
      # 在侦听器容器中运行的线程数。
      concurrency: 1
      #listner负责ack,每调用一次,就立即commit
      ack-mode: manual_immediate
2、实现kafka的自定义序列和反序列

:kakfa默认的序列化和反序列方式是StringSerializer和StringDeserializer。我们要改造成支持对象的序列化和反序列化

a、序列化

public class ObjectSerializer implements Serializer<Object> {


    @Override
    public byte[] serialize(String topic, Object object) {
        return BeanUtils.serialize(object);
    }

    @Override
    public void close() {
    }

    @Override
    public void configure(Map<String, ?> map, boolean b) {

    }

b、反序列化

public class ObjectDeserializer implements Deserializer<Object> {

    @Override
    public Object deserialize(String topic, byte[] bytes) {
        return BeanUtils.deserialize(bytes);
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map, boolean b) {

    }
}
3、消息对象
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MessageDTO<T> implements Serializable {

    private String messageId;


    private T data;
}
4、生产者

:本例子简单模拟生产者多次生产同个消息,进而达到多次消费的效果

@Slf4j
@Component
public class KafkaProducer implements CommandLineRunner {


    @Autowired
    private KafkaTemplate kafkaTemplate;

    private int threadNum = 2;

    private ExecutorService executorService = Executors.newFixedThreadPool(threadNum);

    private CountDownLatch countDownLatch = new CountDownLatch(threadNum);


    @Override
    public void run(String... args) throws Exception {
          send();
    }


    private void send(){
        for(int i = 0; i < threadNum; i++){
            executorService.submit(()->{
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                   log.error(e.getMessage(),e);
                }
                String messageId = "b14701b8-4b08-4bbd-8a4e-70f76a432e99";

                MessageDTO messageDTO = MessageDTO.builder().messageId(messageId).data("hello").build();
                kafkaTemplate.send(Constant.TOPIC,messageDTO);
            });

            countDownLatch.countDown();
        }

    }
}
5、消费者
@Component
@Slf4j
public class KafkaConsumer {

    @Autowired
    private RedisUtils redisUtils;

    @KafkaListener(id = "msgId",topics = {Constant.TOPIC})
    public void receive(ConsumerRecord<String, MessageDTO<String>> record,Acknowledgment ack){

        boolean isRepeateConsume = checkRepeateConsume(record.value().getMessageId());
        if(isRepeateConsume){
            log.error("重复消费。。。。");
            //手工确认
            ack.acknowledge();
            return;
        }


       doBiz(record,ack);
    }

    private boolean checkRepeateConsume(String messageId){
        Object consumeStatus = redisUtils.get(messageId);
        /**
         * 1、如果redis存在消息ID,且消费状态为已消费,则说明是重复消费,此时消费端丢弃该消息
         */
        if(Objects.nonNull(consumeStatus) && "已消费".equals(consumeStatus.toString())){
           // log.error("重复消费。。。。");
            return true;
        }

        /**
         * 2、如果redis不存在消息id,或者状态不是已消费,则从业务方面进行判重
         *
         *  业务判重的可以考虑如下方法:
         *  如果该业务是存在状态流转,则采用状态机策略进行判重。
         *  如果该业务不是状态流转类型,则在新增时,根据业务设置一个唯一的属性,比如根据订单编号的唯一性;
         *  更新时,可以采用多版本策略,在需要更新的业务表上加上版本号
         */
        return checkRepeateByBiz(messageId);
    }



    /**
     * 模拟业务消费
     * @param messageDTO
     * @param ack
     */
    private void doBiz(ConsumerRecord<String, MessageDTO<String>> record,Acknowledgment ack){
        System.out.println("------模拟业务处理-----------");
        System.out.println("--------执行业务处理:"+record.value()+"------------");
        System.out.println("--------------1、业务处理完毕-----------");
        try {
            redisUtils.setEx(record.value().getMessageId(), "已消费",12, TimeUnit.HOURS);
            System.out.println("-------------2、业务处理完毕后,把全局ID存入redis,并设置值为已消费");
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("----------3、业务处理完毕后,消费端手工确认");
        //手工确认
        ack.acknowledge();

    }

}
6、效果
2020-08-09 16:25:32.701  INFO 9552 --- [    msgId-0-C-1] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
------模拟业务处理-----------
--------执行业务处理:MessageDTO(messageId=b14701b8-4b08-4bbd-8a4e-70f76a432e99, data=hello)------------
--------------1、业务处理完毕-----------
-------------2、业务处理完毕后,把全局ID存入redis,并设置值为已消费
----------3、业务处理完毕后,消费端手工确认
2020-08-09 16:25:36.021 ERROR 9552 --- [    msgId-0-C-1] c.g.l.kafka.consumer.KafkaConsumer       : 重复消费。。。。

总结

消息队列没法帮你做到消费端的幂等性,消费端的幂等性得基于业务场景进行实现。不过消息队列必须得保证消息不能丢,至少保证被消费一次,不然消息都丢了,没数据搞啥业务幂等。在实现消费端处理业务时,要确保消费端是采用手工确认应答机制,而不是自动应答机制。这样能够确保消费端一旦业务处理失败,生产者还能再次发送同个消息给消费端

demo链接

https://github.com/lyb-geek/s...
查看原文

赞 8 收藏 6 评论 0

linyb极客之路 发布了文章 · 7月24日

springboot2配置文件定义${user.name}内容失效问题探究

前言

在朋友的项目有个自定义配置文件user.yml,其内容如下

user:
  userId: 1
  name: 张三
  email: zhangsan@qq.com

其映射实体内容为如下

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@PropertySource(value = "user.yml",encoding = "utf-8",factory = CustomYmlPropertySourceFactory.class)
@ConfigurationProperties(prefix = "user")
@Configuration
public class User {

    private String name;

    private Long userId;

    private String email;
}

项目启动后,输出的user内容为

User(name=Administrator, userId=1, email=zhangsan@qq.com)

很明显name的内容不是我们想要的

排查

源码.png
从跟踪的源码可以发现有个systemProperties配置排在user.yml前面。systemProperties这是个啥东西,见名之意,这明显就是系统属性配置。而systemProperties里面又有啥内容,我们继续跟踪下
源码2.png
源码3.png
从源码可以看出systemProperties里面有个key为user.name,value为Administrator。

从这边我们可以看出我们控制台打印出来的内容其实是systemProperties的内容。由此我们可以推断出当系统变量和自定义配置变量都有一样的key时,将以系统变量的值为准。

看到这边也许有朋友说你这是在胡说八道,就凭这个现象就得出这个结论。那好为了证明这个结论,我们不得继续断点排查下去。不过在断点之前,我们去spring官网溜溜,看有没有啥收获。进官网我们可以看到有这么一段话
环境变量优于自定义变量.png
这段话的意思是默认情况下,系统属性优先于环境变量。 因此,如果在调用env.getProperty(“ my-property”)的过程中在两个地方都同时设置了my-property属性,则系统属性值“ wins”并返回。 请注意,属性值不会合并,而是会被前面的条目完全覆盖。

看吧,官网它自己也这么说

如果我们想要自定义的属性优于系统属性,要怎么做

解法.png
这段也是从官网截图来的,其意思是整个机制是可配置的。 也许您具有要集成到此搜索中的自定义属性源。 为此,实现并实例化您自己的PropertySource并将其添加到当前环境的PropertySources集中

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

这个是官方解法。

我这边在提供2种解法。

  • bean初始化之前,修改属性文件加载顺序
  • 在bean初始化后,变更bean的属性值

其实现代码如下

ntBeanFactoryPostProcesser implements BeanFactoryPostProcessor, ApplicationContextAware, BeanPostProcessor {

    private ApplicationContext applicationContext;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        //方法三:在bean初始化后,变更bean的属性值
//        if("user".equals(beanName)){
//            User user = (User)bean;
//            System.out.println("----------------before---------------------");
//            System.out.println("user-->"+user);
//            System.out.println("----------------after---------------------");
//            String propertySourceName = "user.yml";
//            PropertySource propertySource = getPropertySource(propertySourceName);
//            if(!ObjectUtils.isEmpty(propertySource)){
//               user.setName(String.valueOf(propertySource.getProperty("user.name")));
//            }
//            System.out.println("user-->"+user);
//
//        }
        return bean;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        printPropertySourceNames();
       String propertySourceName = "user.yml";
        PropertySource propertySource = getPropertySource(propertySourceName);
        if(!ObjectUtils.isEmpty(propertySource)){
            //方法一 bean初始化之前,修改属性文件加载顺序
            getEnvironment().getPropertySources().remove(propertySourceName);
            getEnvironment().getPropertySources().addFirst(propertySource);
        }

        // 方法二 新增一个PropertySource,并把他的加载顺序置为第一位
//        Map<String, Object> propertiesSource = new HashMap<>();
//        propertiesSource.put("user.name", "张三");
//        PropertySource newPropertySource = new MapPropertySource("newPropertySource", propertiesSource);
//        getEnvironment().getPropertySources().addFirst(newPropertySource);





    }

    private PropertySource getPropertySource(String propertySourceName){
        return getEnvironment().getPropertySources().get(propertySourceName);
    }

    private AbstractEnvironment getEnvironment(){
        return (AbstractEnvironment)applicationContext.getEnvironment();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }


    private void printPropertySourceNames(){
        getEnvironment().getPropertySources().stream().forEach(p-> System.out.println(p.getName()));
    }



}

改完后,我们看下控制台此时输出的内容为

User(name=张三, userId=1, email=zhangsan@qq.com)

总结

其实要想自定义文件属性值不和系统变量的值产生冲突,最快捷的方法,就是让自定义文件属性的key和系统变量的key不一样就好。能少写代码就尽量少写

查看原文

赞 1 收藏 1 评论 0

linyb极客之路 发布了文章 · 7月17日

为什么要避免大事务以及大事务如何解决?

什么是大事务

运行时间比较长,长时间未提交的事务就可以称为大事务

大事务产生的原因

  • 操作的数据比较多
  • 大量的锁竞争
  • 事务中有其他非DB的耗时操作
  • 。。。

大事务造成的影响

  • 并发情况下,数据库连接池容易被撑爆
  • 锁定太多的数据,造成大量的阻塞和锁超时
  • 执行时间长,容易造成主从延迟
  • 回滚所需要的时间比较长
  • undo log膨胀
  • 。。。

如何查询大事务

:本文的sql的操作都是基于mysql5.7版本

以查询执行时间超过10秒的事务为例:

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>10

如何避免大事务

通用解法
  • 在一个事务里面, 避免一次处理太多数据
  • 在一个事务里面,尽量避免不必要的查询
  • 在一个事务里面, 避免耗时太多的操作,造成事务超时。一些非DB的操作,比如rpc调用,消息队列的操作尽量放到事务之外操作
基于mysql5.7的解法
  • 在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放
  • 通过SETMAX_EXECUTION_TIME命令, 来控制每个语句查询的最长时间,避免单个语句意外查询太长时间
  • 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill
  • 在业务功能测试阶段要求输出所有的general_log,分析日志行为提前发现问题
  • 设置innodb_undo_tablespaces值,将undo log分离到独立的表空间。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便

附录查询事务相关语句

:sql语句都是基于mysql5.7版本

# 查询所有正在运行的事务及运行时间
select t.*,to_seconds(now())-to_seconds(t.trx_started) idle_time from INFORMATION_SCHEMA.INNODB_TRX t

# 查询事务详细信息及执行的SQL
select now(),(UNIX_TIMESTAMP(now()) - UNIX_TIMESTAMP(a.trx_started)) diff_sec,b.id,b.user,b.host,b.db,d.SQL_TEXT from information_schema.innodb_trx a inner join information_schema.PROCESSLIST b
on a.TRX_MYSQL_THREAD_ID=b.id and b.command = 'Sleep'
inner join performance_schema.threads c ON b.id = c.PROCESSLIST_ID
inner join performance_schema.events_statements_current d ON d.THREAD_ID = c.THREAD_ID;

# 查询事务执行过的所有历史SQL记录
SELECT
  ps.id 'PROCESS ID',
  ps.USER,
  ps.HOST,
  esh.EVENT_ID,
  trx.trx_started,
  esh.event_name 'EVENT NAME',
  esh.sql_text 'SQL',
  ps.time 
FROM
  PERFORMANCE_SCHEMA.events_statements_history esh
  JOIN PERFORMANCE_SCHEMA.threads th ON esh.thread_id = th.thread_id
  JOIN information_schema.PROCESSLIST ps ON ps.id = th.processlist_id
  LEFT JOIN information_schema.innodb_trx trx ON trx.trx_mysql_thread_id = ps.id 
WHERE
  trx.trx_id IS NOT NULL 
  AND ps.USER != 'SYSTEM_USER' 
ORDER BY
  esh.EVENT_ID;
  
 # 简单查询事务锁
 select * from sys.innodb_lock_waits
 
 # 查询事务锁详细信息
 SELECT
  tmp.*,
  c.SQL_Text blocking_sql_text,
  p.HOST blocking_host 
FROM
  (
  SELECT
    r.trx_state wating_trx_state,
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_Id waiting_thread,
    r.trx_query waiting_query,
    b.trx_state blocking_trx_state,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query 
  FROM
    information_schema.innodb_lock_waits w
    INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
    INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id 
  ) tmp,
  information_schema.PROCESSLIST p,
  PERFORMANCE_SCHEMA.events_statements_current c,
  PERFORMANCE_SCHEMA.threads t 
WHERE
  tmp.blocking_thread = p.id 
  AND t.thread_id = c.THREAD_ID 
  AND t.PROCESSLIST_ID = p.id 

参考

MySQL-长事务详解

面试官:你知道大事务会带来什么问题以及如何解决么?

查看原文

赞 9 收藏 7 评论 0

linyb极客之路 关注了用户 · 7月17日

Kerwin @kexianming

公众号:是Kerwin啊
掘金:https://juejin.im/user/5c729b...
欢迎骚扰~

关注 403

linyb极客之路 关注了专栏 · 7月17日

HLQ_Struggle

享受技术带来的快乐~

关注 646

认证与成就

  • 获得 18 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 3月31日
个人主页被 603 人浏览