wenmo8 发布的文章

开心一刻

有一天螃蟹出门,不小心撞倒了泥鳅
泥鳅很生气地说:你是不是瞎啊!
螃蟹说:不是啊,我是螃蟹

开心一刻

概述

maven-shade-plugin
官网已经介绍的很详细了,我给大家简单翻译一下

This plugin provides the capability to package the artifact in an uber-jar, including its dependencies and to
shade
- i.e. rename - the packages of some of the dependencies.

这段话简明扼要的概述了 maven-shade-plugin 的功能

  1. 能够将项目连同其依赖,一并打包到一个
    uber-jar

    uber-jar 就是一个超级 jar,不仅包含我们的工程代码,还包括依赖的 jar,和
    spring-boot-maven-plugin
    类似

  2. 能够对依赖 jar 中的包名进行重命名

    这个功能就有意思了,后面我们详说

maven-shade-plugin 必须和 Maven 构建生命周期的 package 阶段绑定,那么当 Maven 执行
mvn package
时会自动触发 maven-shade-plugin;使用很简单,在
pom.xml
添加该插件依赖即可

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.6.0</version>
    <executions>
        <execution>
            <!-- 和 maven package 阶段绑定 -->
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>

            <configuration>
                <!-- 按需自定义配置 -->
            </configuration>
        </execution>
    </executions>
</plugin>

phase

goal
按如上固定配置,
configuration
才是我们自由发挥的平台;有了基本了解后,我们再结合官方提供的
Examples
来看看 maven-shade-plugin 具体能干啥

选择打包内容

假设我们有项目
maven-shade-plugin-demo
,其项目结构如下

项目结构

如果不做任何剔除,可以按如下配置进行全打包

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <!-- 和 package 阶段绑定 -->
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <!-- 按需自定义配置 -->
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

执行
mvn package
后,我们会看到两个包

全打包

maven-shade-plugin-demo-1.0-SNAPSHOT.jar
就是 uber-jar;解压可看其结构

uber-jar结构

不仅包括
package
、还包括各种配置文件、元文件,统统打包进 uber-jar;而
original-maven-shade-plugin-demo-1.0-SNAPSHOT.jar
则是不包括依赖 jar 的原始项目包;如果我们比较细心的话,会发现打包的时候告警了

全打包告警

意思是说 hutool jar 包中有
META-INF/MANIFEST.MF
,而
maven-shade-plugin-demo
打包成 jar 后也包含
META-INF/MANIFEST.MF
,两者重复了,只会将其中一个复制进 uber jar;默认情况下,是将我们项目的 jar 中的
META-INF/MANIFEST.MF
复制进 uber jar

默认用项目的MANIFEST

那如果我们想保留 hutool 下的 MANIFEST.MF,而去掉 maven-shade-plugin-demo 中的 MANIFEST.MF,该如何处理呢?只需要微调下
configuration

<configuration>
    <filters>
        <filter>
            <artifact>com.qsl:maven-shade-plugin-demo</artifact>
            <excludes>
                <exclude>META-INF/*.MF</exclude>
            </excludes>
        </filter>
    </filters>
</configuration>

此时 uber jar 中的 MANIFEST.MF 就来自 hutool jar 了

换成hutool下的MANIFEST

回到前面的
configuration
配置,我们需要明白其每个子标签的含义

  1. filter
    :过滤器,可以配置多个

  2. artifact
    :复合标识符,用来匹配 jar,简单点说,就是匹配 jar 的
    匹配规则

    按 Maven 的坐标:groupId:artifactId[[:type]:classifier] 进行配置,
    groupId:artifactId
    必配,
    [[:type]:classifier]
    选配;支持通配符
    *

    ?
    ,例如:
    <artifact>*:*</artifact>
    (相当于匹配上所有jar)

  3. exclude
    :排除项,也就是不会复制进 uber-jar;支持通配符配置

  4. include
    :包含项,也就是
    只有
    这些会被复制进 uber-jar;支持通配符配置

我们实战下,假设我们项目结构如下所示

明细配置项目结构

configuration
配置如下

<configuration>
    <filters>
        <filter>
            <artifact>com.qsl:maven-shade-plugin-demo</artifact>
            <excludes>
                <exclude>com/qsl/test/**</exclude>
                <exclude>com/qsl/Entry.class</exclude>
            </excludes>
        </filter>
        <filter>
            <artifact>cn.hutool:hutool-all</artifact>
            <includes>
                <include>cn/hutool/Hutool.class</include>
                <include>cn/hutool/json/**</include>
            </includes>
        </filter>
    </filters>
</configuration>

执行
mvn package
后,uber-jar 内部结构你们能想到吗?我们来看看实际结果

明细配置后uber-jar结构

是不是和跟你们想的一样?

除了手动指定
filter
外,此插件还支持自动移除项目中没有使用到的依赖类,以此来最小化 uber jar 的体积;configuration 配置如下

<configuration>
    <minimizeJar>true</minimizeJar>
</configuration>

我们在
StringUtil
中引入 hutool 的 StrUtil(相当于项目依赖了 StrUtil)

package com.qsl.util;

import cn.hutool.core.util.StrUtil;

/**
 * @author: 青石路
 */
public class StringUtil {

    public static boolean isBlank(String str) {
        return StrUtil.isBlank(str);
    }
}

然后打包,uber-jar 内部结构如下所示

最小依赖

从 maven-shade-plugin 1.6 开始,
minimizeJar
会保留
filter

include
配置的类,但是要注意:

inlcude 默认会排除所有不在 include 配置中的类

这就会导致问题,我们来看个案例,我们引入
logback
依赖,但代码中未用到它,而我们又想将其下的 class 复制进 uber-jar,另外我们还想将 hutool 的
cn/hutool/json
包下的全部类都复制进 uber-jar,并且开启
minimizeJar
,是不是按如下配置?

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.3.14</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <!-- 和 package 阶段绑定 -->
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <minimizeJar>true</minimizeJar>
                        <filters>
                            <filter>
                                <artifact>ch.qos.logback:logback-classic</artifact>
                                <includes>
                                    <include>**</include>
                                </includes>
                            </filter>
                            <filter>
                                <artifact>cn.hutool:hutool-all</artifact>
                                <includes>
                                    <include>cn/hutool/json/**</include>
                                </includes>
                            </filter>
                        </filters>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

打包后看 uber-jar 目录结构

最小依赖_include

hutool 的
core
包没有复制进来,这是因为我们对 hutool 配置了
include
,默认把最小依赖的
core
包给排除掉了,那怎么办呢?插件提供了配置
<excludeDefaults>false</excludeDefaults>
来处理此种情况,它会覆盖
include
默认排除行为

<filter>
    <artifact>cn.hutool:hutool-all</artifact>
    <excludeDefaults>false</excludeDefaults>
    <includes>
        <include>cn/hutool/json/**</include>
    </includes>
</filter>

这样配置之后,既能包含 hutool 的 json 包,又能包含最小依赖的 core 包

false
通常配合
true
来使用,不然

<configuration>
    <filters>
        <filter>
            <artifact>cn.hutool:hutool-all</artifact>
            <excludeDefaults>false</excludeDefaults>
            <includes>
                <include>cn/hutool/json/**</include>
            </includes>
        </filter>
    </filters>
</configuration>

这么配置有何意义?

重定位 class

如果 uber-jar 被其他项目依赖,而我们的 uber-jar 又是保留了依赖 jar 的 class 的全类名,那么就可能类重复而导致类加载冲突;比如项目A依赖了我们的
maven-shade-plugin-demo
,还依赖了 B.jar,两个 jar 中都存在
cn.hutool.core.util.StrUtil.class
,但 api 完全不一样,根据
双亲委派模型
,只会成功加载其中某个
cn.hutool.core.util.StrUtil.class
,那么另一个的 api 则使用不了。为了解决这个问题,插件提供了重定位功能,通过创建 class 字节码的私有副本,按新配置的 package,打包进 uber-jar

我们来看个案例,假设我们只需要 hutool 的 core 包,将其下所有的 class 按
com.qsl.core
包打包进 uber-jar,可以按如下配置

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <!-- 和 package 阶段绑定 -->
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <relocations>
                            <relocation>
                                <pattern>cn.hutool.core</pattern>
                                <shadedPattern>com.qsl.core</shadedPattern>
                            </relocation>
                        </relocations>
                        <filters>
                            <filter>
                                <artifact>cn.hutool:hutool-all</artifact>
                                <includes>
                                    <include>cn/hutool/core/**</include>
                                </includes>
                            </filter>
                        </filters>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

打包后 uber-jar 目录结构如下

relocate

我们来看下 uber-jar 中的 StringUtil.class

StringUtil中的StrUtil路径

依赖的
StrUtil
也被正确调整了,是不是很牛皮?

有点东西

此时项目A 依赖 B.jar 的同时,又依赖我们的
maven-shade-plugin-demo
,就不会类重名了(package 不一致了)

relocation
同样支持
exclude

include

<configuration>
    <relocations>
        <relocation>
            <pattern>cn.hutool.core</pattern>
            <shadedPattern>com.qsl.core</shadedPattern>
            <!-- exclude 指定的不重定向,其他重定向 -->
            <excludes>
                <exclude>cn.hutool.core.util.ObjUtil</exclude>
                <!-- 一个*只会过滤包下的class,两个*会过滤包下的class和子包 -->
                <exclude>cn.hutool.core.date.**</exclude>
            </excludes>
        </relocation>
        <relocation>
            <pattern>cn.hutool.json</pattern>
            <shadedPattern>com.qsl.json</shadedPattern>
            <!-- include 指定的重定向,其他不重定向 -->
            <includes>
                <include>cn.hutool.json.JSONUtil</include>
                <!-- 一个*只会过滤包下的class,两个*会过滤包下的class和子包 -->
                <include>cn.hutool.json.xml.**</include>
            </includes>
        </relocation>
    </relocations>
    <filters>
        <filter>
            <artifact>cn.hutool:hutool-all</artifact>
            <includes>
                <include>cn/hutool/core/**</include>
                <include>cn/hutool/json/**</include>
            </includes>
        </filter>
    </filters>
</configuration>

此时 uber-jar 的目录结构是怎样的?你们自己去试!

生成附属包

前面已经介绍过,打包后会生成两个包

全打包


original
开头的那个明显不是按 Maven 坐标命名的,所以它是不能够
install
到本地或者远程仓库的;如果需要将两个 jar 都
install
到仓库中,那么就需要用到插件的
Attaching the Shaded Artifact
(生成附属包)功能

<configuration>
    <relocations>
        <relocation>
            <pattern>cn.hutool.core</pattern>
            <shadedPattern>com.qsl.core</shadedPattern>
            <!-- exclude 指定的不重定向,其他重定向 -->
            <excludes>
                <exclude>cn.hutool.core.util.ObjUtil</exclude>
                <!-- 一个*只会过滤包下的class,两个*会过滤包下的class和子包 -->
                <exclude>cn.hutool.core.date.**</exclude>
            </excludes>
        </relocation>
        <relocation>
            <pattern>cn.hutool.json</pattern>
            <shadedPattern>com.qsl.json</shadedPattern>
            <!-- include 指定的重定向,其他不重定向 -->
            <includes>
                <include>cn.hutool.json.JSONUtil</include>
                <!-- 一个*只会过滤包下的class,两个*会过滤包下的class和子包 -->
                <include>cn.hutool.json.xml.**</include>
            </includes>
        </relocation>
    </relocations>
    <filters>
        <filter>
            <artifact>cn.hutool:hutool-all</artifact>
            <includes>
                <include>cn/hutool/core/**</include>
                <include>cn/hutool/json/**</include>
            </includes>
        </filter>
    </filters>
    <shadedArtifactAttached>true</shadedArtifactAttached>
    <shadedClassifierName>qsl</shadedClassifierName>
</configuration>

部署到仓库的 jar 如下

生成附属包

可执行 JAR

这个就比较简单了,我们直接看配置

<configuration>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>com.qsl.Entry</mainClass>
        </transformer>
    </transformers>
</configuration>

如上配置会将
Main-Class
写进 uber-jar 的 MANIFEST.MF,还可以通过
manifestEntries
自定义属性

<configuration>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <manifestEntries>
                <mainClass>com.qsl.Entry</mainClass>
                <Build-Author>qsl</Build-Author>
            </manifestEntries>
        </transformer>
    </transformers>
</configuration>

打包之后,uber-jar 的 MANIFEST.MF 内容如下

可执行jar

资源转换器

Resource Transformers
已经介绍的很详细了,我就不一一介绍了,挑几个个人认为比较重要的简单讲一下

ServicesResourceTransformer

合并
META-INF/services/
下的文件,并对文件中的 class 进行重定向;我们来看个例子,hutool 下有文件
cn.hutool.aop.proxy.ProxyFactory

services_proxyFactory

我们也自定义一个

自定义QslFactory

configuration 配置如下

<configuration>
    <relocations>
        <relocation>
            <pattern>cn.hutool.aop</pattern>
            <shadedPattern>com.qsl.aop</shadedPattern>
        </relocation>
    </relocations>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
    </transformers>
</configuration>

打包后,hutool 与 uber-jar 的 cn.hutool.aop.proxy.ProxyFactory 文件内容差异如下

services_合并后前后对比

如果不配置
ServicesResourceTransformer
,结果是怎样,你们自己去试

AppendingTransformer

将多个同名文件的内容合并追加到一起(不配置的情况下会覆盖,最终文件内容只是其中某个文件的内容),configuration 配置如下

<configuration>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.factories</resource>
        </transformer>
    </transformers>
</configuration>

打包后文件内容合并如下

Append转换器

XmlAppendingTransformer

ResourceBundleAppendingTransformer
功能类似,只是针对的文件内容格式略微有点特殊,就不演示了,你们自行去测试

同包同名 class 共存

回到我们的主题,如果我们项目依赖的 jar 中出现了同名的 class (包名和类名均相同),根据
双亲委派模型
,只会加载其中某一个 class,虽然两个 class 同名了,但功能完全不一样,另一个未被加载的 class 的功能则用不了,如果想同时使用这两个同名 class 的功能,我们该如何处理?

甲方扔给两个存在包名与类名均相同的Jar包,要在工程中同时使用怎么办?

文中给出了几种解决方案(注意看评论区),最高效最实用的当属
maven-shade-plugin
;假设我们项目依赖的 A.jar 和 B.jar 都存在
com.qsl.Hello.class
,我们可以新建一个项目,名字叫
qsl-a
,没有任何代码,仅仅依赖 A.jar,然后利用 maven-shade-plugin 的
Relocating Classes
功能对 A.jar 中存在重名的 class 进行重定向,例如

<configuration>
    <relocations>
        <relocation>
            <pattern>com.qsl</pattern>
            <shadedPattern>com.qsla</shadedPattern>
        </relocation>
        <includes>
            <include>com.qsl.Hello</include>
        </includes>
    </relocations>
</configuration>

然后打包得到 uber jar(qsl-a.jar),项目依赖从 A.jar 更改成 qsl-a.jar,B.jar 依赖继续保留,那么项目中可用的 Hello.class 就包括

com.qsl.Hello(B.jar)

com.qsla.Hello(qsl-a.jar)

问题是不是就得到解决了?更实际的案例,敬请期待我下篇博客

总结

  1. maven-shade-plugin 的输入目标是
    项目原始jar
    以及
    项目依赖的所有jar
    ,而输出目标是
    uber-jar
    ,所以 maven-shade-plugin 的规则对
    项目原始jar
    是无效的
  2. minimizeJar
    针对的只是
    class
    ,其他类型的文件不受此约束
  3. 同 class 共存问题,可以利用 maven-shade-plugin 的 Relocating Classes 功能,将其中一个或多个 jar 重新打包成新的 jar,保证类名相同但包名不同,然后项目依赖新的 jar,变相解决了同 class 共存问题
  4. 示例项目:
    maven-shade-plugin-demo

上篇文章很多小伙伴留言也讲到自己被公司裁员,还有的细心的小伙伴
说去年九月就被裁了,在看一下文章的发布时间,绷不住了。
先和大家说一下,我已经找到工作,因为最近工作一直都很忙,加上自己也比较懒,所以就拖了很久才写的。之前没说就是为了方便写后面的文章。也是通过本文分享一下自己找工作的方法,希望可以给大家一点帮助。

开始找工作,基本都是
Boss不回、前程堪忧
。因为现在的行情就是不太好,公司的需求减少,大量的公司裁员。
在职的时候和大多数程序员一样,每天按部就班的工作,偶尔吐槽公司,也不敢离职找工作。等到被裁的时候,心里一片茫然

开始的一个月,就两三个面试。而且基本都是一面就挂了。基本的都是准备不足,但是一个月后,半个月的时候我就拿了 4 个offer,这里面也有运气和自己的策略问题。

面临的问题

  • 八股文不会,或者背了就忘。
  • 简历准备不够,只是写简单项目经历
  • boss 不回就投递少。
  • 外包填了几个信息没信之后,就很少回。

解决方案

被裁之后本来心气就低,特别又遇到现在这种行情,基本公司都是已读不回或者外包要填写各种各样的信息,填完就没消息了,就很容易气馁。

脸皮要厚

首先就要海投,不管什么公司,只要差不多符合要求的公司就投递,boss 不回就多投,一天最低要投递几十份简历。先要找到机会去面试,有机会面试,找到面试的感觉,根据面试的反馈做调整,聊天沟通几千家,投递一百多的简历,才有几次面试。

当时找工作的时候,外包的回复比较多,不过都是要求填写各种信息,还会问各种问。然后问完了就消息了。我后面从一个 HR 朋友才知道,原来外包的 HR 都是按照招的人数拿提成,他们有一个表格,填完表格之后,就直接交给用人部门,一个岗位招十个人,他可能聊了几百的候选人,这种情况下没有下文也正常。

针对外包 HR,聊了几次之后,基本都知道他们要什么信息,都会把新增存在聊天记录里面。下次要这些信息的时候就方便很多。

背八股文

大部分面试的都会问到八股文。

  • 比较常见的八股文可以看看
    https://cyc2018.xyz/
  • 图解网络、操作系统、计算机组成
    小林coding

图表加深记忆

如果光背八股文,没有给成套的体系的话,基本的都是背了就忘。我的记忆性不是很好,会辅助一下图表理解八股文,比如 HashMap 的数据结构,使用图表和流程就可以快速的理解和掌握,

比如 HashMap 结构:

先通过图表结构,大概了解他的结构,在通过一些流程图获取数据添加到流程:

通过上面的的方法,写了几篇关于 HashMap 的文章:

知识成体系

很多知识点会有很多的关联性。比如 HashMap 适用于并发度不高的情况,而如果想要保证线程安全就需要使用 ConCurrentHashMap,再看看 add 和 get 方法是如何线程线程安全,一环扣一环。

不同的知识点,底层都是有关联性,比如 ArrayList 和 Redis 的简单字符串的扩容,当数据不够的时候,都会扩大自己的 1.5,这样的都是为了减少扩容的次数。

带着问题或者解决的难点去看八股文,比如 MQ 基本都会问消息的
可靠性

不可重复
,MQ 主要就是做一个消息的传递,在正常情况下,消息都能正常消费一次,但是如果服务器重启了,或者接收的服务重启,这都可能导致消息不可靠,带着问题,最好本地搭建服务,模拟消息丢失的情况,进而解决这个问题。
把这个问题解决的思路和过程改成自己在工作中解决了这种问题,再加一点自己的思考,这样比纯背题加分很多。

准备简历

简历主要展示两个技能:

  • 项目经验
  • 技术技能

项目经验

项目包含几个点:

  • 项目背景

让面试官和 hr 知道解决了什么问题,hr 会通过项目来匹配相同的经历的面试者。简单点就是说,这个项目解决了那些问题,整个项目是如何运的。

项目背景或者需求一般都是产品最了解,程序员大部分时间都用在如何实现功能上,用在需求的理解比较少。了解项目又会技术就给面试官印象比较深,面试通过的概率也比较高。

  • 你参与的角色,负责模块

参与的模块,负责那些代码,不要简单写自己做了xx模块,而是从一整个项目出发,解决那些问题。比如一个数据分析系统。这是修改前项目职责:

  • 订单商品维度销量统计
  • 订单炼厂维度销售统计
  • 订单客户统计
  • 订单新客户统计

流水账记录实现的功能,没有了解到具体的需求和目标。好的技术是要先了解需求、技术设计、功能实现、配合同事完成任务对接。

下面是优化后的项目职责:

  • 和业务团队收集需求,明确数据分析的需求和指标。
  • 设计并实现数据统计和多维度分析统计。
  • 主要统计每天或每周的订单、商品、供应商、业务员、新客户等销售数据。
  • 配合前端提供图形化展示,帮助业务团队快速洞察问题。

相对一个只会干活的程序员,一个会思考需求的来源、项目解决的难点和痛点、设计技术实现方案以及高效的沟通的优秀程序员肯定的更受青睐,而且修改后的项目职责也相对更加的专业和规范。

技术技能

先看一下技术技能对比,这是修改前的技能:

修改前技能问题:

  • 前后端都熟悉,没有突出的技能,现在很多公司都是前后端分离,没有突出的技能,面试那关估计就被刷掉。
  • 技能比较简单,工作5,6年就不适用简单的技能了。
  • 技术技能需要关联到相关的业务技能和沟通技能。

修改上面的问题,这是优化后的技能:

技能除了介绍技术之外,更重要的要介绍自己的工作经验,主要涉及的业务方向。此外博客也是一个很好的展示技能。

准备完毕,开始面试

一共面试了 6 家,其中拿到了 4 个 offer,因为是去年面试的,过程只记了一个大概。

某喜到家

这是第一家面试的公司,是一家 o2o的公司,面试架构组职位。主要问了一些项目,如何实现,几个八股文,微服务的有什么缺点,分布式事务的替换方案。面试出奇的顺利,有的问题不太熟悉的,面试官也会给点提示。一下午技术面试过了两轮,hr面试也过了两轮。然后就回去等通知。

过了一天之后,通知我面试通过了。但是他们的上班时间是上午9点半到中午12点,中午休息2个小时。下午两点到六点,晚上休息半个个小时,六点半点到8点,而且还是大小周。给出的薪资相对之前基本是没涨(虽然工资涨了,工作时间也增加了),还是先拿个offer保底,再继续找工作。

这么长的工作时间,应该也很难招到人。工资也卡的比较死。这种情况自然面试也比较容易就通过了。

银行外包

面试过了一天,来了一个珠海外包的电话面试,主要问了一些八股文:

  • SpringBoot自动装配原理
  • 线程池线程数量设置,拒绝策略
  • JMM

然后介绍自己的项目经历,自己负责的模块,如何实现对应的技术模块。印象比较的深一个点,是自己写博客地址,就问了下是不是自己写的,说看了还是写的挺不错的。而且还说:
我最近几天面试了十多个人,你是唯一一个让我感觉挺满意,背八股文不是应付任务一样的背,还会结合一些实际的工作常见使用。做项目也有自己的想法,而且还会把自己的心得、总结写成博客。
当时就感觉自己写了这么久的博客有人表扬还是很开心的。

二面时候,也是问了一个技术的问题,面试也过了。给的薪资还可以,比上面的 o2o 的工资好一点。但是毕竟外包还是工作比较累,还是拿个offer,再继续找工作。

某外贸公司

这是一家跨境电商的公司,主营的是电子烟,想要做一个独立站,跨境销售产品。面试形式是笔试 + 面试。比如主要写了一些八股文:

  • CPU飙升到 100% 如何处理
  • MQ 消息可靠性如何保证
  • Mysql 事务隔离级别,以及各自的问题
  • HashMap、ConCurrentHashMap、Hashtable 的区别

面试主要问项目经验和一些八股文,对 HashMap 的细节一直问的比较细节。后面人事总监面试也过了,第二天回复面试也通过了。给的薪资比前面都高,基本上对这个工资是比较满意,也准备拿了 offer 就准备上班了。

半路杀出个珠宝公司

本来就准备下周就去新的公司报道了,在 boss 上又来了一个面试,反正也没啥事,多面试几次也没事。前面几次面试基本上都是问
项目 + 八股文
,都问出经验了,一面主要是问项目经验,自己的负责的模块用哪些技术实现。最后就问了一下如何实现点赞功能,要看到每天点赞的人数、每个人点赞的数量,这么统计的话,就不能使用 Redis 统计了,只能使用关系型数据库,比如 Mysql 实现。

一面过了,就来到了技术总监的面试,主要讲项目,还叫我在画板上画下流程图,面试也通过了,之后就是 HR 谈薪资。薪资和之前的外贸公司差不多,但是这个公司包吃住,相当于涨了三千的工资。

最后选择

最后就在外贸公司和珠宝公司选择,两个公司都是大小周,(后面面试的公司基本都是大小周)无论是薪资还是福利方面,珠宝公司都更好,而且相对来说珠宝行业也比较稳定点,所以最后就选择了珠宝公司。

总结

断断续续终于写完几篇失业日记,最开始失业的不适应、迷茫,后面找工作的屡屡碰壁,一次又一次的失败。简历一遍一遍的修改,疯狂打招呼,疯狂投简历。那个时候也不知道什么时候能找到工作,开始找工作的 10 月份的时候感觉过年都可能找不到工作,没想到 11 月份,半个月就拿了 4 个 offer,就像《阿甘正传》的那句台词一样:

Life was like a box of chocolate. You never know what you're gonna get

一 、 概述

PeerJS 是一个基于浏览器
WebRTC功能实现的js功能包,简化了WebrRTC的开发过程,对底层的细节做了封装,直接调用API即可,再配合surging 协议组件化从而做到稳定,高效可扩展的微服务,再利用RtmpToWebrtc 引擎组件可以做到不仅可以利用httpflv 观看rtmp推流直播,还可以采用基于
Webrtc
的peerjs 进行观看,那么今天要讲的是如何结合开发语音视频通话功能。放到手机和电脑上都可以实现语音视频通话。

一键运行打包成品下载:
https://pan.baidu.com/s/1MVsjKtVYpUonauAz9ZXtPg?pwd=1q2g

测试用户:fanly

测试密码:123456

为了让大家节约时间,能尽快运行产品看到效果,上面有 一键运行打包成品可以进行下载测试运行。

二、如何测试运行

以下是目录结构,

IDE:consul 注册中心

kayak.client: 网关

kayak.server:微服务

apache-skywalking-apm:skywalking链路跟踪

以上是目录结构, 不需要进入管理界面配置网络组件,启动后自带端口96的ws协议主机,只要打开video文件夹,里面有两个语音通话的html测试文件,在同一一个局域网只要输入对方的name就可以进行语音通话

打开界面如图

三、基于surging如何开发

以上是没有开发环境的进行下载进行下载测试,那么正在使用surging 的如何开发此功能呢?

1. 创建服务接口,继承于
IServiceKey

 [ServiceBundle("Device/{Service}")]public  interfaceIChatService : IServiceKey
{
}

2. 创建中间服务,继承于WSBehavior, IChatService

  internal classChatService : WSBehavior, IChatService
{
private static readonly ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>();private static readonly ConcurrentDictionary<string, string> _clients = new ConcurrentDictionary<string, string>();protected override voidOnOpen()
{
var _name = Context.QueryString["name"];if (!string.IsNullOrEmpty(_name))
{
_clients[ID]
=_name;
_users[_name]
=ID;
}
}
protected override voidOnError( WebSocketCore.ErrorEventArgs e)
{
var msg =e.Message;
}
protected override voidOnMessage(MessageEventArgs e)
{
if(_clients.ContainsKey(ID))
{
var message = JsonConvert.DeserializeObject<Dictionary<string, object>>(e.Data);//消息类型 message.TryGetValue("type",out object@type);
message.TryGetValue(
"toUser", out objecttoUser);
message.TryGetValue(
"fromUser", out objectfromUser);
message.TryGetValue(
"msg", out objectmsg);
message.TryGetValue(
"sdp", out objectsdp);
message.TryGetValue(
"iceCandidate", out objecticeCandidate);


Dictionary
<String, Object> result = new Dictionary<String, Object>();
result.Add(
"type", @type);//呼叫的用户不在线 if (!_users.ContainsKey(toUser?.ToString()))
{
result[
"type"]= "call_back";
result.Add(
"fromUser", "系统消息");
result.Add(
"msg", "Sorry,呼叫的用户不在线!");this.Client().SendTo(JsonConvert.SerializeObject(result), ID);return;
}
//对方挂断 if ("hangup".Equals(@type))
{
result.Add(
"fromUser", fromUser);
result.Add(
"msg", "对方挂断!");
}
//视频通话请求 if ("call_start".Equals(@type))
{
result.Add(
"fromUser", fromUser);
result.Add(
"msg", "1");
}
//视频通话请求回应 if ("call_back".Equals(type))
{
result.Add(
"fromUser", toUser);
result.Add(
"msg", msg);
}
//offer if ("offer".Equals(type))
{
result.Add(
"fromUser", toUser);
result.Add(
"sdp", sdp);
}
//answer if ("answer".Equals(type))
{
result.Add(
"fromUser", toUser);
result.Add(
"sdp", sdp);
}
//ice if ("_ice".Equals(type))
{
result.Add(
"fromUser", toUser);
result.Add(
"iceCandidate", iceCandidate);
}
this.Client().SendTo(JsonConvert.SerializeObject(result), _users.GetValueOrDefault(toUser?.ToString()));
}
}
protected override voidOnClose(CloseEventArgs e)
{
if( _clients.TryRemove(ID, out stringname))
_users.TryRemove (name,
out stringvalue);
}

}

3.设置surgingSettings的WSPort端口配置,默认96

以上就是利用websocket协议中转消息,下面是页面如何编号,代码如下:

<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL-->
<htmlxmlns:th="http://www.thymeleaf.org">
<head>
    <metacharset="UTF-8">
    <title>WebRTC + WebSocket</title>
    <metaname="viewport"content="width=device-width,initial-scale=1.0,user-scalable=no">
    <style>html,body{margin:0;padding:0;
        }#main{position:absolute;width:370px;height:550px;
        }#localVideo{position:absolute;background:#757474;top:10px;right:10px;width:100px;height:150px;z-index:2;
        }#remoteVideo{position:absolute;top:0px;left:0px;width:100%;height:100%;background:#222;
        }#buttons{z-index:3;bottom:20px;left:90px;position:absolute;
        }#toUser{border:1px solid #ccc;padding:7px 0px;border-radius:5px;padding-left:5px;margin-bottom:5px;
        }#toUser:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}#call{width:70px;height:35px;background-color:#00BB00;border:none;margin-right:25px;color:white;border-radius:5px;
        }#hangup{width:70px;height:35px;background-color:#FF5151;border:none;color:white;border-radius:5px;
        }
    </style>
</head>
<body>
    <divid="main">
        <videoid="remoteVideo"playsinline autoplay></video>
        <videoid="localVideo"playsinline autoplay muted></video>

        <divid="buttons">
            <inputid="toUser"placeholder="输入在线好友账号"/><br/>
            <buttonid="call">视频通话</button>
            <buttonid="hangup">挂断</button>
        </div>
    </div>
</body>
<!--可引可不引-->
<!--<script th:src="@{/js/adapter-2021.js}"></script>-->
<scripttype="text/javascript"th:inline="javascript">let username= "fanly";
let localVideo
=document.getElementById('localVideo');
let remoteVideo
=document.getElementById('remoteVideo');
let websocket
= null;
let peer
= null;

WebSocketInit();
ButtonFunInit();
/*WebSocket*/ functionWebSocketInit(){//判断当前浏览器是否支持WebSocket if('WebSocket' inwindow) {
websocket
= newWebSocket("ws://127.0.0.1:961/device/chat?name="+username);
}
else{
alert(
"当前浏览器不支持WebSocket!");
}
//连接发生错误的回调方法 websocket.onerror= function(e) {
alert(
"WebSocket连接发生错误!");
};
//连接关闭的回调方法 websocket.onclose= function() {
console.error(
"WebSocket连接关闭");
};
//连接成功建立的回调方法 websocket.onopen= function() {
console.log(
"WebSocket连接成功");
};
//接收到消息的回调方法 websocket.onmessage=asyncfunction(event) {
let { type, fromUser, msg, sdp, iceCandidate }
=JSON.parse(event.data.replace(/\n/g,"\\n").replace(/\r/g,"\\r"));

console.log(type);
if(type=== 'hangup') {
console.log(msg);
document.getElementById(
'hangup').click();return;
}
if(type=== 'call_start') {
let msg
= "0" if(confirm(fromUser+ "发起视频通话,确定接听吗")==true){
document.getElementById(
'toUser').value=fromUser;
WebRTCInit();
msg
= "1"}

websocket.send(JSON.stringify({
type:
"call_back",
toUser:fromUser,
fromUser:username,
msg:msg
}));
return;
}
if(type=== 'call_back') {if(msg=== "1"){
console.log(document.getElementById(
'toUser').value+ "同意视频通话");//创建本地视频并发送offer let stream=await navigator.mediaDevices.getUserMedia({ video:true, audio:true})
localVideo.srcObject
=stream;
stream.getTracks().forEach(track
=>{
peer.addTrack(track, stream);
});

let offer
=await peer.createOffer();
await peer.setLocalDescription(offer);
let newOffer
=offer;

newOffer[
"fromUser"]=username;
newOffer[
"toUser"]=document.getElementById('toUser').value;
websocket.send(JSON.stringify(newOffer));
}
else if(msg=== "0"){
alert(document.getElementById(
'toUser').value+ "拒绝视频通话");
document.getElementById(
'hangup').click();
}
else{
alert(msg);
document.getElementById(
'hangup').click();
}
return;
}
if(type=== 'offer') {
let stream
=await navigator.mediaDevices.getUserMedia({ video:true, audio:true});
localVideo.srcObject
=stream;
stream.getTracks().forEach(track
=>{
peer.addTrack(track, stream);
});

await peer.setRemoteDescription(
newRTCSessionDescription({ type, sdp }));
let answer
=await peer.createAnswer();
let newAnswer
=answer;

newAnswer[
"fromUser"]=username;
newAnswer[
"toUser"]=document.getElementById('toUser').value;
websocket.send(JSON.stringify(newAnswer));

await peer.setLocalDescription(answer);
return;
}
if(type=== 'answer') {
peer.setRemoteDescription(
newRTCSessionDescription({ type, sdp }));return;
}
if(type=== '_ice') {
peer.addIceCandidate(iceCandidate);
return;
}

}
}
/*WebRTC*/ functionWebRTCInit(){
peer
= newRTCPeerConnection();//ice peer.onicecandidate= function(e) {if(e.candidate) {
websocket.send(JSON.stringify({
type:
'_ice',
toUser:document.getElementById(
'toUser').value,
fromUser:username,
iceCandidate: e.candidate
}));
}
};
//track peer.ontrack= function(e) {if(e&&e.streams) {
remoteVideo.srcObject
=e.streams[0];
}
};
}
/*按钮事件*/ functionButtonFunInit(){//视频通话 document.getElementById('call').onclick= function(e){
document.getElementById(
'toUser').style.visibility= 'hidden';

let toUser
=document.getElementById('toUser').value;if(!toUser){
alert(
"请先指定好友账号,再发起视频通话!");return;
}
if(peer== null){
WebRTCInit();
}

websocket.send(JSON.stringify({
type:
"call_start",
fromUser:username,
toUser:toUser,
}));
}
//挂断 document.getElementById('hangup').onclick= function(e){
document.getElementById(
'toUser').style.visibility= 'unset';if(localVideo.srcObject){
const videoTracks
=localVideo.srcObject.getVideoTracks();
videoTracks.forEach(videoTrack
=>{
videoTrack.stop();
localVideo.srcObject.removeTrack(videoTrack);
});
}
if(remoteVideo.srcObject){
const videoTracks
=remoteVideo.srcObject.getVideoTracks();
videoTracks.forEach(videoTrack
=>{
videoTrack.stop();
remoteVideo.srcObject.removeTrack(videoTrack);
});
//挂断同时,通知对方 websocket.send(JSON.stringify({
type:
"hangup",
fromUser:username,
toUser:document.getElementById(
'toUser').value,
}));
}
if(peer){
peer.ontrack
= null;
peer.onremovetrack
= null;
peer.onremovestream
= null;
peer.onicecandidate
= null;
peer.oniceconnectionstatechange
= null;
peer.onsignalingstatechange
= null;
peer.onicegatheringstatechange
= null;
peer.onnegotiationneeded
= null;

peer.close();
peer
= null;
}

localVideo.srcObject
= null;
remoteVideo.srcObject
= null;
}
}
</script> </html>

以上是页面的代码,如需要添加其它账号测试只要更改
username
,或者ws地址也可以更改标记红色的区域。

三、总结

本人正在开发平台,如有疑问可以联系作者,QQ群:744677125

LOTO
示波器统计曲线和故障分析pass/fail测试

虚拟示波器可以应用在工业自动化检测中,除了常规的检测波形和测量值参数以外,由多个行业客户定制和验证的统计曲线和故障分析(pass/fail)功能也为工业自动化检测带来极大的便利。

(一)故障分析(pass/fail)的基础:统计曲线功能

在信号检测的自动化测量中,大部分时间是关心某个测量值随时间变化的趋势,比如在开机检测后,波形的峰峰值是如何变化的。虚拟示波器的统计曲线功能,可以绘制出你关注的某些测量值的变化趋势曲线,如下图所示,示波器测试的信号最大值随着时间变化,从最低的0.49V逐渐变高,一直到4.73V,然后又降低到最低,接着缓缓升高并震荡:

通过这样的统计曲线,我们可以看到被监测的测量值的变化过程和趋势,从而为后面的故障分析做基础。

统计曲线功能的入口在“非标功能”中的“统计/故障判断”中,如下图所示:

(二)统计曲线功能可以观察哪些测量值:

理论上所有测量值,比如“最大值,最小值,峰峰值,有效值,平均值,频率,周期,占空比,正负脉宽,上升时间,下降时间”等等,都可以进行统计曲线的绘制,监测它们的变化趋势曲线。但是 虚拟示波器软件的标准版并没有开放所有这些测量值的统计曲线功能,根据型号不同和客户定制的情况不同,只开放了部分测量值的统计曲线功能。这些可以在统计曲线的配置页面看到。有些示波器型号支持多台级联的情况下,多台设备多通道的测量值的统计曲线绘制:

勾选上的测量值就可以在统计曲线绘图区看到对应的曲线,以不同的颜色区分。并且绘图区会在上下空白处用对应的颜色显示对应曲线的最大和最小数值,如下图所示:

(三)统计曲线的控制和现实

统计曲线只有在点击了“开始统计”按钮以后才会开始对测量值进行统计,这个按钮就会变成“停止统计”,点击了“停止统计”以后,就会停止统计曲线的绘制。

为了方便工业自动化测试,这个开始统计或停止统计按钮也可以不通过鼠标点击实现,可以由键盘快捷键或者示波器的IO口实现。

对应的键盘快捷键是“shift”+“z”, 对应的IO口是GPIO功能的IO2,也就是DE2扩展口的4脚。需要注意的是,如果需要IO2控制这个统计开始停止按钮,需要勾选对应的选项,如下图所示:

勾选了“IO2”开始后,IO2引脚的GPIO会被自动设置为输入,这个输入信号遵循3.3V TTL数字信号逻辑,由低电平跳变到高电平时,会被识别为点击了“开始统计”按钮,相反,这个输入信号由高电平跳变为低电平时,会被识别为点击了“停止统计”按钮。

“开始统计”被点击或者触发后,会清零之前的统计曲线波形和相关的数据,如果开启了故障pass/fail测试,也会清零故障信息。

(四)故障分析pass/fail测试

在上面的统计曲线的基础上,我们可以为测量值对应的每条统计曲线设定曲线的上下限,在上下限范围内的统计曲线变化被认为是正常的,也就是pass,一旦超过上下限的范围,则认为有故障发生,也就是fail。

故障分析的设定是在如下位置:

pass/fail测试的结果会在统计曲线绘图区的下方通过色块和文字表示出来,如下图所示:

具体是哪个或者哪几个测量值产生了fault的故障,我们也可以在下面的信息栏里看到,会显示“通道号:测量值”形式的故障信息。

为了方便客户在工业自动化的信号检测中,更方便的自动化处理故障分析,比如使用实体的报警灯,或者喇叭,或者和PLC联动实现某些动作,故障发生后,除了在 示波器的上位机软件上显示外,还可以使用IO口输出。我们可以在下图所示位置,选中IO3警报,就会自动将示波器的GPIO功能的IO3,也就是扩展口DE2的10脚设置为输出,IO3同样也是遵循3.3V TTL 数字逻辑。

默认的情况下,如果是PASS状态,那么IO3输出低电平,如果是fail状态,那么IO3将输出高电平。如果需要的是相反的逻辑,那么可以在故障的设置页面勾选“IO3警报逻辑反向”选项:

(五)状态清除

统计曲线的历史数据和波形,以及故障分析的结果等,都可以通过点击按钮“清除”进行清空。清空后波形和数据将清零,如果勾选了IO3警报的话,那么IO3的输出状态也会被清除。除了手动点击这个清除之外,“停止统计”后的点击“开始统计”时,也会对统计和故障信息自动清除,如果勾选了“IO2开始”,那么从停止到开始的IO状态切换,也会对统计和故障信息自动清除。

(六)设置记忆/保存和导入

以上的统计曲线的设置和故障分析的设置都是可以记忆和另存为配置文件的,配置文件可以手动导入回来。这样在工业自动化检测时会更加便利。关于这部分内容我们会在其他部分专门描述。

关于统计曲线和故障分析的使用,可以参考以下视频演示:

《 示波器 软件功能 演示 之 测量值统计曲线功能演示 以及 自动化检测应用实例》
https://www.bilibili.com/video/BV1RJ411C73h/

《示波器-统计曲线2-故障联动-自动化检测 自动化集成 信号监测判断 pass/fail测试》

https://www.bilibili.com/video/BV1h5411u7zP/?vd_source=c0bedc6c664e75d3c19935cbda8abe19

《示波器 统计曲线 故障检测 PASS/Fail 功能优化3 工业自动化 产线检测 自动化检测》

https://www.bilibili.com/video/BV1Hx4y1b73n/?vd_source=c0bedc6c664e75d3c19935cbda8abe19

《示波器_多台级联配置记忆+统计故障分析配置记忆功能(1)》

https://www.bilibili.com/video/BV1QcvXe1EG6/?vd_source=c0bedc6c664e75d3c19935cbda8abe19

《示波器_多台级联配置记忆+统计故障分析配置记忆功能(2)》

https://www.bilibili.com/video/BV1iDvQeWEYk/?vd_source=c0bedc6c664e75d3c19935cbda8abe19

《示波器更新: Pass/Fail测试_统计曲线功能(3) GPIO控制 自动化检测 工业检测》

https://www.bilibili.com/video/BV1pqvQeAEvk/?vd_source=c0bedc6c664e75d3c19935cbda8abe19

接手新项目一言难尽,别的不说单单就一个
@Transactional
注解用的一塌糊涂,五花八门的用法,很大部分还失效无法回滚。

有意识的在涉及事务相关方法上加
@Transactional
注解,是个好习惯。不过,很多同学只是下意识地添加这个注解,一旦功能正常运行,很少有人会深入验证异常情况下事务是否能正确回滚。@Transactional 注解虽然用起来简单,但这货总是能在一些你意想不到的情况下失效,防不胜防!

我把这些事务问题归结成了三类:
不必要

不生效

不回滚
,接下用一些demo演示下各自的场景。

不必要

1. 无需事务的业务

在没有事务操作的业务方法上使用 @Transactional 注解,比如:用在仅有查询或者一些 HTTP 请求的方法,虽然加上影响不大,但从编码规范的角度来看还是不够严谨,建议去掉。

@Transactional
public String testQuery() {
    standardBak2Service.getById(1L);
    return "testB";
}

2. 事务范围过大

有些同学为了省事直接将 @Transactional 注解加在了类上或者抽象类上,这样做导致的问题就是
类内的方法或抽象类的实现类中所有方法全部都被事务管理
。增加了不必要的性能开销或复杂性,建议按需使用,只在有事务逻辑的方法上加@Transactional。

@Transactional
public abstract class BaseService {
}

@Slf4j
@Service
public class TestMergeService extends BaseService{

    private final TestAService testAService;

    public String testMerge() {

        testAService.testA();

        return "ok";
    }
}

如果在类中的方法上添加 @Transactional 注解,它将覆盖类级别的事务配置。例如,类级别上配置了只读事务,方法级别上的 @Transactional 注解也会覆盖该配置,从而启用读写事务。

@Transactional(readOnly = true)
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    @Transactional
    public String testMerge() {

        testAService.testA();

        testBService.testB();
        return "ok";
    }
}    

不生效

3. 方法权限问题

不要把 @Transactional注解加在 private 级别的方法上!

我们知道 @Transactional 注解依赖于Spring AOP切面来增强事务行为,这个 AOP 是通过代理来实现的,而 private 方法恰恰不能被代理的,所以 AOP 对 private 方法的增强是无效的,@Transactional也就不会生效。

@Transactional
private String testMerge() {

    testAService.testA();

    testBService.testB();

    return "ok";
}

那如果我在 testMerge() 方法内调用 private 的方法事务会生效吗?

答案:事务会生效

@Transactional
public String testMerge() throws Exception {

    ccc();
    
    return "ok";
}

private void ccc() {
    testAService.testA();

    testBService.testB();
}

4. 被用 final 、static 修饰方法

和上边的原因类似,被用
final

static
修饰的方法上加 @Transactional 也不会生效。

  • static 静态方法属于类本身的而非实例,因此代理机制是无法对静态方法进行代理或拦截的
  • final 修饰的方法不能被子类重写,事务相关的逻辑无法插入到 final 方法中,代理机制无法对 final 方法进行拦截或增强。

这些都是java基础概念了,使用时要注意。

@Transactional
public static void b() {
}

@Transactional
public final void b() {
}

5. 同类内部方法调用问题

注意了
,这种情况经常发生啊!

同类内部方法间的调用是 @Transactional 注解失效的重灾区,网上你总能看到方法内部调用另一个同类的方法时,
这种调用是不会经过代理的
,因此事务管理不会生效。但这说法比较片面,要分具体情况。

比如:testMerge() 方法开启事务,调用同类非事务的方法 a() 和 b() ,此时 b() 抛异常,根据事务的传播性 a()、b() 事务均生效。

@Transactional
public String testMerge() {

    a();

    b();

    return "ok";
}

public void a() {
    standardBakService.save(testAService.buildEntity());
}

public void b() {
    standardBak2Service.save(testBService.buildEntity2());
    throw new RuntimeException("b error");
}

如果 testMerge() 方法未开启事务,并且在同类中调用了非事务方法 a() 和事务方法 b(),当 b() 抛出异常时,a() 和 b() 的事务都不会生效。因为这种调用直接通过
this
对象进行,未经过代理,因此事务管理无法生效。这经常出问题的!

public String testMerge() {

    a();

    b();

    return "ok";
}

public void a() {
    standardBakService.save(testAService.buildEntity());
}

@Transactional
public void b() {
    standardBak2Service.save(testBService.buildEntity2());
    throw new RuntimeException("b error");
}

5.1 独立的 Service 类

要想 b() 方法的事务生效也容易,最简单的方法将它剥离放在独立的Service类注入使用,交给spring管理就行了。不过,这种方式会创建很多类。

@Slf4j
@Service
public class TestBService {

      @Transactional
      public void b() {
          standardBak2Service.save(testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

5.2 自注入方式

或者通过自己注入自己的方式解决,尽管解决了问题,逻辑看起来很奇怪,它破坏了依赖注入的原则,虽然 spring 支持我们这样用,还是要注意下循环依赖的问题。

@Slf4j
@Service
public class TestMergeService {
      @Autowired
      private TestMergeService testMergeService;

      public String testMerge() {

          a();

          testMergeService.b();

          return "ok";
      }

      public void a() {
          standardBakService.save(testAService.buildEntity());
      }

      @Transactional
      public void b() {
          standardBak2Service.save(testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

5.3 手动获取代理对象

b() 方法它不是没被代理嘛,那我们手动获取代理对象调用 b() 方法也可以。通过
AopContext.currentProxy()
方法返回当前的代理对象实例,这样调用代理的方法时,就会经过 AOP 的切面,@Transactional注解就会生效了。

@Slf4j
@Service
public class TestMergeService {

      public String testMerge() {

          a();

         ((TestMergeService) AopContext.currentProxy()).b();

          return "ok";
      }

      public void a() {
          standardBakService.save(testAService.buildEntity());
      }

      @Transactional
      public void b() {
          standardBak2Service.save(testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

6. Bean 未被 spring 管理

上边我们知道 @Transactional 注解通过 AOP 来管理事务,而 AOP 依赖于代理机制。因此,
Bean 必须由Spring管理实例!
要确保为类加上如
@Controller

@Service

@Component
注解,让其被Spring所管理,这很容易忽视。

@Service
public class TestBService {

    @Transactional
    public String testB() {
        standardBak2Service.save(entity2);
        return "testB";
    }
}

7. 异步线程调用

如果我们在 testMerge() 方法中使用异步线程执行事务操作,通常也是无法成功回滚的,来个具体的例子。

testMerge() 方法在事务中调用了 testA(),testA() 方法中开启了事务。接着,在 testMerge() 方法中,我们通过一个新线程调用了 testB(),testB() 中也开启了事务,并且在 testB() 中抛出了异常。

此时的回滚情况是怎样的呢?

@Transactional
public String testMerge() {

    testAService.testA();

    new Thread(() -> {
        try {
            testBService.testB();
        } catch (Exception e) {
//                e.printStackTrace();
            throw new RuntimeException();
        }
    }).start();

    return "ok";
}

@Transactional
public String testB() {
    DeepzeroStandardBak2 entity2 = buildEntity2();

    dataImportJob2Service.save(entity2);

    throw new RuntimeException("test2");
}

@Transactional
public String testA() {
    DeepzeroStandardBak entity = buildEntity();
    standardBakService.save(entity);
    return "ok";
}

答案是:testA() 和 testB() 中的事务都不会回滚。

testA() 无法回滚是因为没有捕获到新线程中 testB()抛出的异常;testB()方法无法回滚,是因为事务管理器只对当前线程中的事务有效,因此在新线程中执行的事务不会回滚。

由于在多线程环境下,Spring 的事务管理器不会跨线程传播事务,事务的状态(如事务是否已开启)是存储在线程本地的
ThreadLocal
来存储和管理事务上下文信息。这意味着每个线程都有一个独立的事务上下文,事务信息在不同线程之间不会共享。

8. 不支持事务的引擎

不支持事务的数据库引擎不在此次
Review
范围内,只做了解就好。我们通常使用的关系型数据库,如 MySQL,默认使用支持事务的
InnoDB
引擎,而非事务的
MyISAM
引擎则使用较少。

以前开启启用 MyISAM 引擎是为了提高查询效率。不过,现在非关系型数据库如
Redis

MongoDB

Elasticsearch
等中间件提供了更高性价比的解决方案。

不回滚

9. 用错传播属性

@Transactional
注解有个关键的参数
propagation
,它控制着事务的传播行为,有时事务传播参数配置错误也会导致事务的不回滚。

propagation 支持 7 种事务传播特性:

  • REQUIRED

    默认的传播行为
    ,如果当前没有事务,则创建一个新事务;如果存在事务,则加入当前事务。
  • MANDATORY
    :支持当前事务,如果不存在则抛出异常
  • NEVER
    :非事务性执行,如果存在事务,则抛出异常
  • REQUIRES_NEW
    :无论当前是否存在事务,都会创建一个新事务,原有事务被挂起。
  • NESTED
    :嵌套事务,被调用方法在一个嵌套的事务中运行,这个事务依赖于当前的事务。
  • SUPPORTS
    :如果当前存在事务,则加入;如果没有,就以非事务方式执行。
  • NOT_SUPPORTED
    :以非事务方式执行,如果当前存在事务,将其挂起。

为了加深印象,我用案例来模拟下每种特性的使用场景。

REQUIRED

REQUIRED 是默认的事务传播行为。如果 testMerge() 方法开启了事务,那么其内部调用的 testA() 和 testB() 方法也将加入这个事务。如果 testMerge() 没有开启事务,而 testA() 和 testB() 方法上使用了 @Transactional 注解,这些方法将各自创建新的事务,只控制自身的回滚。

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    @Transactional
    public String testMerge() {

        testAService.testA();

        testBService.testB();

        return "ok";
    }
}

@Transactional
public String testA() {
    log.info("testA");
    DeepzeroStandardBak entity = buildEntity();
    standardBakService.save(entity);
    return "ok";
}

@Transactional
public String testB() {
    log.info("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    standardBak2Service.save(entity2);
    throw new RuntimeException("testB");
}

MANDATORY

MANDATORY 传播特性简单来说就是只能被开启事务的上层方法调用,例如 testMerge() 方法未开启事务调用 testB() 方法,那么将抛出异常;testMerge() 开启事务调用 testB() 方法,则加入当前事务。

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    public String testMerge() {

        testAService.testA();

        testBService.testB();

        return "ok";
    }
}

@Transactional
public String testA() {
    log.info("testA");
    DeepzeroStandardBak entity = buildEntity();
    standardBakService.save(entity);
    return "ok";
}

@Transactional(propagation = Propagation.MANDATORY)
public String testB() {
    log.info("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    standardBak2Service.save(entity2);
    throw new RuntimeException("testB");
}

抛出的异常信息

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

NEVER

NEVER 传播特性是强制你的方法只能以非事务方式运行,如果方法存在事务操作会抛出异常,我实在是没想到有什么使用场景。

@Transactional(propagation = Propagation.NEVER)
public String testB() {
    log.info("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    standardBak2Service.save(entity2);
//        throw new RuntimeException("testB");
    return "ok";
}

抛出的异常信息

org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

REQUIRES_NEW

我们在使用 Propagation.REQUIRES_NEW 传播特性时,不论当前事务的状态如何,调用该方法都会创建一个新的事务。

例如,testMerge() 方法开始一个事务,调用 testB() 方法时,它会暂停 testMerge() 的事务,并启动一个新的事务。如果 testB() 方法内部发生异常,新事务会回滚,但原先挂起的事务不会受影响。这意味着,挂起的事务不会因为新事务的回滚而受到影响,也不会因为新事务的失败而回滚。


@Transactional
public String testMerge() {

    testAService.testA();

    testBService.testB();

    return "ok";
}

@Transactional
public String testA() {
    log.info("testA");
    DeepzeroStandardBak entity = buildEntity();
    standardBakService.save(entity);
    return "ok";
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public String testB() {
    log.info("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    standardBak2Service.save(entity2);
    throw new RuntimeException("testB");
}

NESTED

方法的传播行为设置为 NESTED,其内部方法会开启一个新的嵌套事务(子事务)。在没有外部事务的情况下
NESTED

REQUIRED
效果相同;存在外部事务的情况下,一旦外部事务回滚,它会创建一个嵌套事务(子事务)。

也就是说外部事务回滚时,子事务会跟着回滚;但子事务的回滚不会对外部事务和其他同级事务造成影响。

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    @Transactional
    public String testMerge() {

        testAService.testA();

        testBService.testB();

        throw new RuntimeException("testMerge");
        return "ok";
    }
}

@Transactional
public String testA() {
    log.info("testA");
    DeepzeroStandardBak entity = buildEntity();
    standardBakService.save(entity);
    return "ok";
}

@Transactional(propagation = Propagation.NESTED)
public String testB() {
    log.info("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    standardBak2Service.save(entity2);
    throw new RuntimeException("testB");
}

NOT_SUPPORTED

NOT_SUPPORTED
事务传播特性表示该方法必须以非事务方式运行。当方法 testMerge() 开启事务并调用事务方法 testA() 和 testB() 时,如果 testA() 和 testB() 的事务传播特性为 NOT_SUPPORTED,那么 testB() 将以非事务方式运行,并挂起当前的事务。

默认传播特性的情况下 testB() 异常事务加入会导致 testA() 回滚,而挂起的意思是说,testB() 其内部一旦抛出异常,不会影响 testMerge() 中其他 testA() 方法的回滚。

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    @Transactional
    public String testMerge() {

        testAService.testA();

        testBService.testB();

        return "ok";
    }
}

@Transactional
public String testA() {
    log.info("testA");
    DeepzeroStandardBak entity = buildEntity();
    standardBakService.save(entity);
    return "ok";
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public String testB() {
    log.info("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    standardBak2Service.save(entity2);
    throw new RuntimeException("testB");
}

SUPPORTS

如果当前方法的事务传播特性是
SUPPORTS
,那么只有在调用该方法的上层方法开启了事务的情况下,该方法的事务才会有效。如果上层方法没有开启事务,那么该方法的事务特性将无效。

例如,如果入口方法 testMerge() 没有开启事务,而 testMerge() 调用的方法 testA() 和 testB() 的事务传播特性为 SUPPORTS,那么由于 testMerge() 没有事务,testA() 和 testB() 将以非事务方式执行。即使在这些方法上加上
@Transactional
注解,也不会回滚异常。

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    public String testMerge() {

        testAService.testA();

        testBService.testB();

        return "ok";
    }
}

@Transactional(propagation = Propagation.SUPPORTS)
public String testA() {
    log.info("testA");
    DeepzeroStandardBak entity = buildEntity();
    standardBakService.save(entity);
    return "ok";
}

@Transactional(propagation = Propagation.SUPPORTS)
public String testB() {
    log.info("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    standardBak2Service.save(entity2);
    throw new RuntimeException("testB");
}

10. 自己吞了异常

在整个 review 的过程中我发现导致事务不回滚的场景,多数是开发同学在业务代码中手动 try...catch 捕获了异常,然后又没抛出异常....

比如:testMerge() 方法开启了事务,并调用了非事务方法 testA() 和 testB(),同时在 testMerge() 中捕获了异常。如果 testB() 中发生了异常并抛出,但 testMerge() 捕获了这个异常而没有继续抛出,Spring 事务将无法捕获到异常,从而无法进行回滚。

@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;
    @Transactional
    public String testMerge() {

        try {
            testAService.testA();

            testBService.testB();

        } catch (Exception e) {
            log.error("testMerge error:{}", e);
        }
        return "ok";
    }
}

@Service
public class TestAService {

    public String testA() {
        standardBakService.save(entity);
        return "ok";
    }
}

@Service
public class TestBService {

    public String testB() {
        standardBakService.save(entity2);
        
        throw new RuntimeException("test2");
    }
}

为了确保 Spring 事务能够正常回滚,需要我们在 catch 块中主动重新抛出它能够处理的 RuntimeException 或者 Error 类型的异常。

@Transactional
public String testMerge() {

    try {
        testAService.testA();

        testBService.testB();

    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        throw new RuntimeException(e);
    }
    return "ok";
}

捕获异常并不意味着一定不会回滚
,这取决于具体情况。

例如,当 testB() 方法上也加上了 @Transactional 注解时,如果在该方法中发生异常,事务会捕获到这个异常。由于事务传播的特性,testB() 的事务会合并到上层方法的事务中。因此,即使在 testMerge() 中捕获了异常而未抛出,事务仍然可以成功回滚。

@Transactional
public String testB() {

    DeepzeroStandardBak2 entity2 = buildEntity2();

    dataImportJob2Service.save(entity2);

    throw new RuntimeException("test2");
    // return "ok";
}

但这有个提前,必须在 testMerge() 方法上添加 @Transactional 注解以启用事务。如果 testMerge() 方法没有开启事务,不论其内部是否使用 try 块,都只能部分回滚 testB(),而 testA() 将无法回滚。

11. 事务无法捕获的异常

Spring 的事务默认会回滚
RuntimeException
及其子类,以及
Error
类型的异常。

如果抛出的是其他类型的异常,例如
checked exceptions
(检查型异常),即继承自 Exception 但不继承自 RuntimeException 的异常,比如
SQLException

DuplicateKeyException
,事务将不会回滚。

所以,我们在主动抛出异常时,要确保该异常是事务能够捕获的类型。

@Transactional
public String testMerge() throws Exception {
    try {
        testAService.testA();

        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
//            throw new RuntimeException(e);
        throw new Exception(e);
    }
    return "ok";
}

如果你非要抛出默认情况下不会导致事务回滚的异常,务必要在
@Transactional
注解的
rollbackFor
参数中明确指定该异常,这样才能进行回滚。

@Transactional(rollbackFor = Exception.class)
public String testMerge() throws Exception {
    try {
        testAService.testA();

        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
//            throw new RuntimeException(e);
        throw new Exception(e);
    }
    return "ok";
}

问问你身边的同学,哪些异常属于运行时异常,哪些属于检查型异常,十有八九他们可能无法给出准确的回答!

所以减少出现 bug 的风险,我建议使用 @Transactional 注解时,将 rollbackFor 参数设置为
Exception

Throwable
,这样可以扩大事务回滚的范围。

12. 自定义异常范围问题

针对不同业务定制异常类型是比较常见的做法,@Transactional 注解的 rollbackFor 参数支持自定义的异常,但我们往往习惯于将这些自定义异常继承自 RuntimeException。

那么这就出现和上边同样的问题,事务的范围不足,许多异常类型仍然无法触发事务回滚。

@Transactional(rollbackFor = CustomException.class)
public String testMerge() throws Exception {
    try {
        testAService.testA();

        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
//            throw new RuntimeException(e);
        throw new Exception(e);
    }
    return "ok";
}

想要解决这个问题,可以在 catch 中主动抛出我们自定义的异常。

@Transactional(rollbackFor = CustomException.class)
public String testMerge() throws Exception {
    try {
        testAService.testA();

        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        throw new CustomException(e);
    }
    return "ok";
}

13. 嵌套事务问题

还有一种场景就是嵌套事务问题,比如,我们在 testMerge() 方法中调用了事务方法 testA() 和事务方法 testB(),此时不希望 testB() 抛出异常让整个 testMerge() 都跟着回滚;这就需要单独 try catch 处理 testB() 的异常,不让异常在向上抛。

@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;
    @Transactional
    public String testMerge() {
    
        testAService.testA();

        try {
            testBService.testB();
        } catch (Exception e) {
            log.error("testMerge error:{}", e);
        }
        return "ok";
    }
}

@Service
public class TestAService {

    @Transactional
    public String testA() {
        standardBakService.save(entity);
        return "ok";
    }
}

@Service
public class TestBService {

    @Transactional
    public String testB() {
        standardBakService.save(entity2);
        
        throw new RuntimeException("test2");
    }
}

总结

上面的关于 @Transactional 注解的使用注意事项是我在代码审查和搜集网络观点后整理出的。之前我也写过类似的文章,但当时内容不够全面。这次的补充更为详尽,涵盖了更多细节。开发工作只是整体工作量的一小部分,更多时间实际上花在了自测和验证上。希望这些案例对大家有所收获,少踩坑。