介绍
这是使用 Rust 的 MdBook 搭建的,并基于 Github Actions 发布更新。
里面包含了博主大部分的笔记和知识,当然也有很多还没有整理上来。有时间会一点点慢慢深入学习,然后完善后发不上来。有不对之处欢迎 Issue 提出,会予以修正。
基本指令
-
查看
conda版本conda --version -
更新最新版
condaconda update conda -
查看当前环境所有包
conda list -
查询 包有那些版本。
conda search [包名] -
更新当前环境所有包和指定包到最新版本
conda update --all conda update [package_name] -
安装包在当前环境
conda install [package_name] conda install [package_name]=1.0.0 # 指定版本包 -
删除包在当前环境
conda remove [package_name]
环境相关
-
查看虚拟环境
conda env list -
创建虚拟环境。 conda create -n [环境名] python=[版本号]
conda create -n python_3.9 python=3.9 -
复制环境。 conda create --name [复制后新名] --clone [被复制环境名]
conda create --name Py_3.9 --clone python_3.9 -
激活虚拟环境
conda activate python_3.9 -
退出虚拟环境
conda deactivate -
删除虚拟环境。 conda remove -n [虚拟名] --all
conda remove -n python_3.9 --all
常用指令
-
查看版本
pip --version -
升级 pip
pip install -U pip -
搜索包版本
pip search [package_name] -
查看已安装的包
pip list pip list --outdatea # 列出所有过期的库 pip freeze # 显示pip安装的包及版本库 pip freeze > d:/out.txt # 写入到文件 -
安装包
pip install [package_name] # 最新版本 pip install [package_name]==1.21.2 # 指定版本 pip install [package_name]>=1.21.2 # 最小版本 pip install [package_name] --ignore-installed # 忽略 numpy 包是否已安装,都将重新安装 -
从
requirements.txt安装pip install -r requirements.txt -
升级包
pip install --upgrade [package_name] -
下载包
pip uninstall [package_name] -
显示包信息
pip show [package_name] # 显示包的详情 pip show -f [package_name] # 显示包所在目录
MiniConda3安装与配置
下载地址: https://docs.conda.io/projects/miniconda/en/latest/
-
下载并安装
miniconda -
测试是否安装成功
conda --version conda config --show # 查看所有配置信息 -
配置显示通道URL。(win:C:/users/【用户名】/.condarc;linux:~/.condarc)
conda config --set show_channel_urls yeswin 系统下,此时才会出现
.condarc文件 -
配置是否进入
base环境# 关闭自动进入base环境 conda config --set auto_activate_base false # 开启自动进入base环境 conda config --set auto_activate_base true -
打开
.condarc文件-
清华源
channels: - defaults show_channel_urls: true default_channels: - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/msys2/ custom_channels: conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud或另一种方式,提高优先级
channels: - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/msys2/ - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/bioconda/ - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/menpo/ - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/ - defaults show_channel_urls: true -
交大源
channels: - defaults show_channel_urls: true default_channels: - https://mirrors.sjtug.sjtu.edu.cn/anaconda/pkgs/main/ - https://mirrors.sjtug.sjtu.edu.cn/anaconda/pkgs/free/ - https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud/msys2/ custom_channels: conda-forge: https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud msys2: https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud bioconda: https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud menpo: https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud pytorch: https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud simpleitk: https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud或另一种方式,高优先级:
channels: - https://mirrors.sjtug.sjtu.edu.cn/anaconda/pkgs/main/ - https://mirrors.sjtug.sjtu.edu.cn/anaconda/pkgs/free/ - https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud/conda-forge/ - https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud/msys2/ - https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud/bioconda/ - https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud/menpo/ - https://mirrors.sjtug.sjtu.edu.cn/anaconda/cloud/pytorch/ - defaults show_channel_urls: true
-
-
conda config --get channels # 获取已有的通道 conda config –set show_channel_urls yes # 搜索时显示通道地址 -
修改包保存路径。
envs_dirs: - E://conda//pkg # 包存储路径
pip 安装和配置
创建conda环境会默认安装pip
若没有安装conda,需要安装python 解释器,python新版本的都自带pip,无需另外安装。
-
配置清华源
在
C:/users/[用户]文件夹下,创建文件夹pip,再在其目录下创建pip.ini文件,并输入如下内容。[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host = pypi.tuna.tsinghua.edu.cn
Pytorch
Pytorch目前是由Facebook人工智能学院提供支持服务的。
该框架将 Torch 中高效而灵活的 GPU 加速后端库与直观的 Python 前端相结合,后者专注于快速原型设计、可读代码,并支持尽可能广泛的深度学习模型。
TensorFlow
TensorFlow由Google智能机器研究部门Google Brain团队研发的;TensorFlow编程接口支持Python和C++。随着1.0版本的公布,相继支持了Java、Go、R和Haskell API的alpha版本。
Keras
Keras是基于Tensorflow用纯python编写的深度学习框架,也就是说它是在Tensorflow的基础上再次集成的;所以,他的代码会更加简洁方便,适于初学者;但因为它是在Tensorflow的框架上再次封装的,那么运行速度肯定就没有Tensorflow快了。
SAM
FACEBOOK_SAM:https://github.com/facebookresearch/segment-anything?tab=readme-ov-file
MOBILE_SAM:https://github.com/ChaoningZhang/MobileSAM
FAST_SAM:
samexporter
关于利用samexporter将模型导出成onnx
-
下载并安装依赖
git clone https://github.com/vietanhdev/samexporter cd samexporter pip install -e . pip install -r requirements.txt -
修正依赖版本(可选)
pip uninstall onnxruntime pip install onnxruntime==1.15.1 -
创建文件夹,将模型放到original_models
mkdir original_models mkdir output_models -
转换
Use "quantized" models for faster inference and smaller model size. However, the accuracy may be lower than the original models.
# mobile_sam encoder python -m samexporter.export_encoder --checkpoint original_models/mobile_sam.pt --output output_models/mobile_sam/mobile_sam.encoder.onnx --model-type mobile --quantize-out output_models/mobile_sam/mobile_sam.encoder.quant.onnx --use-preprocess # sam_vit_b_01ec64 decoder python -m samexporter.export_decoder --checkpoint original_models/mobile_sam.pt --output output_models/mobile_sam/mobile_sam.decoder.onnx --model-type mobile --quantize-out output_models/mobile_sam/mobile_sam.decoder.quant.onnx --return-single-mask # sam_vit_b_01ec64 encoder python -m samexporter.export_encoder --checkpoint original_models/sam_vit_b_01ec64.pth \ --output output_models/sam_vit_b_01ec64.encoder.onnx \ --model-type vit_b \ --quantize-out output_models/sam_vit_b_01ec64.encoder.quant.onnx \ --use-preprocess # sam_vit_b_01ec64 decoder python -m samexporter.export_decoder --checkpoint original_models/sam_vit_b_01ec64.pth --output output_models/sam_vit_b_01ec64.decoder.onnx --model-type vit_b --quantize-out output_models/sam_vit_b_01ec64.decoder.quant.onnx --return-single-mask
OkHttp3
Maven引入
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version>
</dependency>
Utils
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OkHttpUtils {
private static final Logger log = LoggerFactory.getLogger(OkHttpUtils.class);
public static MediaType JSON = MediaType.parse("application/json; charset=utf-8");
public static MediaType XML = MediaType.parse("application/xml; charset=utf-8");
private OkHttpUtils() {
}
public static String get(String url, Map<String, String> queries) {
return get(url, (Map) null, queries);
}
public static String get(String url, Map<String, String> header, Map<String, String> queries) {
String queriesStr = buildQueries(url, queries);
Request.Builder builder = buildRequest(header);
Request request = builder.url(queriesStr).build();
return getBody(request);
}
public static String post(String url, Map<String, String> params) {
return post(url, (Map) null, params);
}
public static String post(String url, Map<String, String> header, Map<String, String> params) {
Request.Builder builder = buildRequest(header);
Request request = builder.url(url).post(buildFormBody(params)).build();
return getBody(request);
}
public static String postJson(String url, String json) {
return postJson(url, (Map) null, json);
}
public static String postJson(String url, Map<String, String> header, String json) {
return postContent(url, header, json, JSON);
}
public static String postXml(String url, String xml) {
return postXml(url, (Map) null, xml);
}
public static String postXml(String url, Map<String, String> header, String xml) {
return postContent(url, header, xml, XML);
}
public static String postContent(String url, Map<String, String> header, String content, MediaType mediaType) {
RequestBody requestBody = RequestBody.create(mediaType, content);
Request.Builder builder = new Request.Builder();
if (header != null && header.keySet().size() > 0) {
header.forEach(builder::addHeader);
}
Request request = builder.url(url).post(requestBody).build();
return getBody(request);
}
public static byte[] postContentByte(String url, Map<String, String> header, String content, MediaType mediaType) {
RequestBody requestBody = RequestBody.create(mediaType, content);
Request.Builder builder = new Request.Builder();
if (header != null && header.keySet().size() > 0) {
header.forEach(builder::addHeader);
}
Request request = builder.url(url).post(requestBody).build();
return getBodyByte(request);
}
private static String getBody(Request request) {
String responseBody = "";
Response response = null;
String var4;
try {
OkHttpClient okHttpClient = getClient(request);
response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
return responseBody;
}
var4 = response.body().string();
} catch (Exception var8) {
log.error("okhttp3 post error >> ex = {}", var8.getMessage());
return responseBody;
} finally {
if (response != null) {
response.close();
}
}
return var4;
}
private static OkHttpClient getClient(Request request) {
int timeout = 30;
String timeoutHeader = request.header("timeout");
if (null != timeoutHeader && !timeoutHeader.isEmpty()) {
timeout = Integer.parseInt(timeoutHeader);
}
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.build();
return okHttpClient;
}
public static byte[] downloadMultipartPost(String url, Map<String, String> params, Map<String, File> files) {
return downloadMultipartPost(url, (Map) null, params, files);
}
public static byte[] downloadMultipartPost(String url, Map<String, String> header, Map<String, String> params, Map<String, File> files) {
Request.Builder builder = buildRequest(header);
Request request = builder.url(url).post(buildMultipartBody(params, files)).build();
return getBodyByte(request);
}
public static byte[] downloadPost(String url, Map<String, String> params) {
return downloadPost(url, (Map) null, params);
}
public static byte[] downloadPost(String url, Map<String, String> header, Map<String, String> params) {
return downloadPost(url, header, params, true);
}
public static byte[] downloadPost(String url, Map<String, String> header, Map<String, String> params, boolean needEncode) {
Request.Builder builder = buildRequest(header);
Request request = builder.url(url).post(buildFormBody(params, needEncode)).build();
return getBodyByte(request);
}
public static byte[] downloadPostJson(String url, String json) {
return postContentByte(url, null, json, JSON);
}
public static byte[] downloadPostJson(String url, Map<String, String> header, String json) {
return postContentByte(url, header, json, JSON);
}
private static byte[] getBodyByte(Request request) {
byte[] responseBytes = null;
Response response = null;
try {
OkHttpClient okHttpClient = getClient(request);
response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
return responseBytes;
}
responseBytes = response.body().bytes();
} catch (Exception var8) {
log.error("okhttp3 post error >> ex = {}", var8.getMessage());
return responseBytes;
} finally {
if (response != null) {
response.close();
}
}
return responseBytes;
}
public static RequestBody buildFormBody(Map<String, String> params) {
return buildFormBody(params, true);
}
public static RequestBody buildFormBody(Map<String, String> params, boolean needEncode) {
FormBody.Builder formBuilder = new FormBody.Builder();
if (params != null && params.keySet().size() > 0) {
if (needEncode) {
params.forEach(formBuilder::add);
} else {
params.forEach(formBuilder::addEncoded);
}
}
return formBuilder.build();
}
public static RequestBody buildMultipartBody(Map<String, String> params, Map<String, File> files) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
if (params != null && params.keySet().size() > 0) {
params.forEach(builder::addFormDataPart);
}
if (files != null && files.keySet().size() > 0) {
files.forEach((s, file) -> {
try {
builder.addFormDataPart(s, file.getName(), RequestBody.create(MediaType.parse(Files.probeContentType(file.toPath())), file));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
return builder.build();
}
public static Request.Builder buildRequest(Map<String, String> header) {
Request.Builder builder = new Request.Builder();
if (header != null && header.keySet().size() > 0) {
header.forEach(builder::addHeader);
}
return builder;
}
public static String buildQueries(String url, Map<String, String> queries) {
StringBuffer sb = new StringBuffer(url);
if (queries != null && queries.keySet().size() > 0) {
queries.forEach((k, v) -> {
sb.append("&").append(k).append("=").append(v);
});
}
return sb.toString();
}
}
关于@Retention
说明该注解类的生命周期
- RetentionPolicy.SOURCE : 注解只保留在源文件中,将会被编译器丢弃。
- RetentionPolicy.CLASS : 注解记录被编译器记录在class文件中,但不需要在运行时由VM保留。默认选项。
- RetentionPolicy.RUNTIME : 注解记录被编译器记录在class文件中,同时在运行时由VM保留。可以通过反射获得注解。
关于@Target
说明该注解可以被声明在那些元素之前
- ElementType.TYPE:说明该注解只能被声明在一个类前。
- ElementType.FIELD:说明该注解只能被声明在一个类的字段前。
- ElementType.METHOD:说明该注解只能被声明在一个类的方法前。
- ElementType.PARAMETER:说明该注解只能被声明在一个方法参数前。
- ElementType.CONSTRUCTOR:说明该注解只能声明在一个类的构造方法前。
- ElementType.LOCAL_VARIABLE:说明该注解只能声明在一个局部变量前。
- ElementType.ANNOTATION_TYPE:说明该注解只能声明在一个注解类型前。
- ElementType.PACKAGE:说明该注解只能声明在一个包名前。
关于自定义验证
请查看基础知识: 基础.md
@Constraint
主要作用就是帮助我们来处理验证逻辑的,指定使用什么验证器处理。
@Documented
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraint {
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
ConstraintValidator接口
public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A constraintAnnotation) {
}
boolean isValid(T var1, ConstraintValidatorContext var2);
}
- ConstraintValidator 是一个接口,接受两个泛型。
- 接受的注解类
- 校验的数据类型
- 提供两个方法。
default void initialize(A constraintAnnotation) {}。初始化一些数据信息。boolean isValid(T var1, ConstraintValidatorContext var2);。传递两个参数,第一个是数据信息,第二个上下文信息。验证通过则返回TRUE
手机号正则校验注解
-
创建注解
@PhoneNumber/** * 手机号校验 */ @Documented @Constraint( validatedBy = {PhoneNumberValidator.class} ) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface PhoneNumber { // 提示信息 String message() default "电话号码格式不正确"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } -
创建类
PhoneNumberValidator实现校验接口ConstraintValidator/*** * phoneNumber 校验器 */ public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> { private static final Pattern PATTERN = Pattern.compile(^1[3|4|5|6|7|8|9][0-9]\d{8}$); @Override public void initialize(PhoneNumber constraintAnnotation) { ConstraintValidator.super.initialize(constraintAnnotation); } @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { if (value != null) { Matcher matcher = PATTERN.matcher(value); return matcher.find(); } return true; } } -
使用
@NotBlank(message = "请填写手机号") @PhoneNumber private String mobile;
准备工作
- 获取线程的方法
private void getThreadName() {
getThreadName(-1);
}
private void getThreadName(Integer ier) {
log.info("ThreadName:{},value:{}", Thread.currentThread().getName(), ier);
}
- 完整测试代码
@Slf4j
public class StreamParallelTest {
@Test
public void parallelTest() throws InterruptedException {
IntStream range = IntStream.range(1, 10);
range.parallel().forEach(item -> {
getThreadName();
});
log.info("主线程参与协作了");
log.info("sleep 5000 start");
Thread.sleep(5000);
log.info("sleep 5000 end");
range = IntStream.range(1, 10);
range.mapToObj(e -> CompletableFuture.runAsync(() -> {
getThreadName(e);
})).forEach(CompletableFuture::join);
log.info("主线程阻塞了,但是线程一直同一个");
log.info("sleep 5000 start");
Thread.sleep(5000);
log.info("sleep 5000 end");
range = IntStream.range(1, 10);
CompletableFuture[] completableFutures = range.mapToObj(e -> CompletableFuture.runAsync(() -> {
getThreadName(e);
})).toArray(CompletableFuture[]::new);
CompletableFuture.allOf(completableFutures).join();
log.info("主线程阻塞了");
log.info("sleep 5000 start");
Thread.sleep(5000);
log.info("sleep 5000 end");
ExecutorService executorService = Executors.newFixedThreadPool(10);
range = IntStream.range(1, 10);
completableFutures = range.mapToObj(e -> CompletableFuture.runAsync(() -> {
getThreadName(e);
}, executorService)).toArray(CompletableFuture[]::new);
CompletableFuture.allOf(completableFutures).join();
log.info("主线程阻塞了");
log.info("sleep 5000 start");
Thread.sleep(5000);
log.info("sleep 5000 end");
}
private void getThreadName() {
getThreadName(-1);
}
private void getThreadName(Integer ier) {
log.info("ThreadName:{},value:{}", Thread.currentThread().getName(), ier);
}
}
parallelStrem
使用 parallel 启动并发。
IntStream range = IntStream.range(1, 10);
range.parallel().forEach(item -> {
getThreadName();
});
log.info("主线程参与协作了");
输出结果:
18:05:36.140 [ForkJoinPool.commonPool-worker-1] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-1,value:-1
18:05:36.140 [ForkJoinPool.commonPool-worker-5] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-5,value:-1
18:05:36.146 [ForkJoinPool.commonPool-worker-1] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-1,value:-1
18:05:36.140 [ForkJoinPool.commonPool-worker-4] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-4,value:-1
18:05:36.140 [main] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:main,value:-1
18:05:36.140 [ForkJoinPool.commonPool-worker-2] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-2,value:-1
18:05:36.140 [ForkJoinPool.commonPool-worker-3] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-3,value:-1
18:05:36.140 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:-1
18:05:36.140 [ForkJoinPool.commonPool-worker-6] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-6,value:-1
18:05:36.147 [main] INFO top.zsmile.test.basic.stream.StreamParallelTest - 主线程参与协作了
调用方是主线程,当使用 parallel 启动并发时,主线程会和 ForkJoinPool.commonPool 参与并发的执行工作,同步执行。
结合CompletableFuture使用
这里区分两种情况
- 通过
CompletableFuture.join启动方法执行 - 通过
CompletableFuture.allOf(CompletableFuture[] future).join启动异步执行
range = IntStream.range(1, 10);
range.mapToObj(e -> CompletableFuture.runAsync(() -> {
getThreadName(e);
})).forEach(CompletableFuture::join);
log.info("主线程阻塞了,但是线程一直同一个");
log.info("sleep 5000 start");
Thread.sleep(5000);
log.info("sleep 5000 end");
range = IntStream.range(1, 10);
CompletableFuture[] completableFutures = range.mapToObj(e -> CompletableFuture.runAsync(() -> {
getThreadName(e);
})).toArray(CompletableFuture[]::new);
CompletableFuture.allOf(completableFutures).join();
log.info("主线程阻塞了");
输出结果
18:05:41.154 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:1
18:05:41.155 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:2
18:05:41.155 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:3
18:05:41.155 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:4
18:05:41.155 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:5
18:05:41.155 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:6
18:05:41.155 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:7
18:05:41.155 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:8
18:05:41.155 [ForkJoinPool.commonPool-worker-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-7,value:9
18:05:41.155 [main] INFO top.zsmile.test.basic.stream.StreamParallelTest - 主线程等待了,但是执行线程一直同一个
18:05:41.155 [main] INFO top.zsmile.test.basic.stream.StreamParallelTest - sleep 5000 start
18:05:46.164 [main] INFO top.zsmile.test.basic.stream.StreamParallelTest - sleep 5000 end
18:05:46.166 [ForkJoinPool.commonPool-worker-2] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-2,value:3
18:05:46.166 [ForkJoinPool.commonPool-worker-3] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-3,value:1
18:05:46.166 [ForkJoinPool.commonPool-worker-4] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-4,value:2
18:05:46.166 [ForkJoinPool.commonPool-worker-2] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-2,value:6
18:05:46.166 [ForkJoinPool.commonPool-worker-3] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-3,value:7
18:05:46.166 [ForkJoinPool.commonPool-worker-1] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-1,value:5
18:05:46.166 [ForkJoinPool.commonPool-worker-4] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-4,value:8
18:05:46.166 [ForkJoinPool.commonPool-worker-2] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-2,value:9
18:05:46.166 [ForkJoinPool.commonPool-worker-5] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:ForkJoinPool.commonPool-worker-5,value:4
18:05:46.166 [main] INFO top.zsmile.test.basic.stream.StreamParallelTest - 主线程等待了
- 可以看出当我们使用的
CompletableFuture.join时,都是由同一个线程进行处理。 - 使用
CompletableFuture.allOf(CompletableFuture[] future).join时,会实现真正的异步多线程执行。 - 这两种方法都会阻塞主线程,但主线程不参与协同处理任务。
CompletableFuture + Executor
这里使用 CompletableFuture.allOf(CompletableFuture[] future).join 以及 固定线程池 Executor.newFixedThreadPool(10)
ExecutorService executorService = Executors.newFixedThreadPool(10);
range = IntStream.range(1, 10);
completableFutures = range.mapToObj(e -> CompletableFuture.runAsync(() -> {
getThreadName(e);
}, executorService)).toArray(CompletableFuture[]::new);
CompletableFuture.allOf(completableFutures).join();
log.info("主线程等待了");
输出结果
18:26:49.295 [pool-1-thread-2] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-2,value:2
18:26:49.295 [pool-1-thread-1] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-1,value:1
18:26:49.295 [pool-1-thread-3] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-3,value:3
18:26:49.295 [pool-1-thread-4] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-4,value:4
18:26:49.295 [pool-1-thread-5] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-5,value:5
18:26:49.295 [pool-1-thread-6] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-6,value:6
18:26:49.295 [pool-1-thread-7] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-7,value:7
18:26:49.295 [pool-1-thread-8] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-8,value:8
18:26:49.295 [pool-1-thread-9] INFO top.zsmile.test.basic.stream.StreamParallelTest - ThreadName:pool-1-thread-9,value:9
18:26:49.295 [main] INFO top.zsmile.test.basic.stream.StreamParallelTest - 主线程等待了
- 和
CompletableFuture.allOf(CompletableFuture[] future).join一样,不会阻塞主线程,都需要等待线程池执行完返回才继续下一步。 - 区别在于使用了自定义的线程池。这样可以更好地自主管理线程池资源、任务调度。
总结
- 如果不希望阻塞主线程,那么可以使用
CompletableFuture和Executor的方式进行实现。 - 如果是IO密集型,那么可以使用
CompletableFuture和自定义线程池,提高线程异步执行。 - 如果是CPU密集型,那么推荐使用
Stream.parallel,因为实现简单直观、效率也高,异步任务数和CPU核心数相同。
什么是Stream
Stream 是 JDK 8 以后新提供的流式API操作, 侧重对于源数据计算能力的封装,并且支持串行与并行两种操作方式 。
Stream 流操作可以分为三种:
- 创建
Stream流 - 中间操作
- 终端操作(会结束
Stream流)
创建方式
Stream 创建方式主要有几种方式:
- Collection.stream()
- Array.stream(T array)
- Stream的静态方法
- Stream.of
- Stream.iterate
- Stream.generate
创建的 Stream 流默认都是串行序列。
- 可以通过
Stream.isParallel来判断是否是并行流。 - 通过
Stream.parallel可以转为并行流。
Collection.stream()
List<String> array = Arrays.asList("a", "b", "c");
Stream<String> stream = array.stream();
Array.stream(T array)
int[] arr = {1, 2, 3, 4};
IntStream stream1 = Arrays.stream(arr);
Stream.of
Stream<String> q = Stream.of("q", "w", "e");
Stream.iterate
Stream<Integer> iterate = Stream.iterate(0, integer -> integer + 1);
Stream.generate
Stream<Double> limit = Stream.generate(Math::random).limit(10);
操作分类
在 Stream 中,流操作的分为 中间操作 和 终端操作。
- 中间操作:对流中的数据进行各种各样的处理,可以连续操作,每个操作的返回值都是
Stream对象。又可以分为以下两种:- 有状态:该操作只有拿到所有元素才能正常操作下去。例如去重方法
distinct,必须拿到所有的值,才能判断当前值是否重复。 - 无状态:元素的处理不受之前元素的影响。即不会记录元素状态,只处理当前元素。
- 有状态:该操作只有拿到所有元素才能正常操作下去。例如去重方法
- 终端操作:执行结束后,会关闭这个流,流中的数据都会销毁。
- 短路:获取到预期的结果时,就会返回,不会将所有数据都处理。
- 非短路:会将所有数据都按预期操作一遍,才能获取到最终结果。
所以其特性如下:
- 不存储数据,按照特定的规则对数据进行计算处理,一般会输出结果;
- 不改变数据源,通常会产生一个新的集合或值;
- 具有延迟特性,只有调用终端操作,中间操作才会执行。
| 操作分类 | 特征 | 方法 |
| 中间操作 | 操作分类 | 方法 |
| 操作分类 | 方法 | |
| 终端操作 | 操作分类 | 方法 |
| 操作分类 | 方法 |
注意事项
终端操作后,不能再继续操作
一旦一个 Stream 执行了终端操作之后,后续便不可以再读这个流执行其他的操作了 。
@Test
public void Streamttt() {
Stream<User> stream = userList.stream();
log.info("终端操作 count => {}", stream.count());
log.info("再执行终端操作 foreach");
stream.forEach(System.out::println);
}
输出结果
18:16:53.449 [main] INFO top.zsmile.test.basic.stream.StreamTest - 终端操作 count => 3
18:16:53.455 [main] INFO top.zsmile.test.basic.stream.StreamTest - 再执行终端操作 foreach
java.lang.IllegalStateException: stream has already been operated upon or closed
因为 stream 已经被执行count()终端方法了,所以对 stream 再执行foreach方法的时候,就会报错**stream has already been operated upon or closed**。
并行Stream
使用并行流,可以有效利用计算机的硬件资源。并行流通过将整个Stream划分为多个片段任务,然后对各个分片进行处理后,最后将其结果合并。
并行流类似多线程并行处理,所以多线程场景下的问题,都会存在,比如锁冲突等,必须要保证**线程安全**
Stream API
数据准备
@Data
public class User {
private String name;
private Integer age;
private Integer score;
private List<String> roles;
public static User of(String name, Integer age, Integer score, List<String> roles) {
User user = new User();
user.setAge(age);
user.setScore(score);
user.setName(name);
user.setRoles(roles);
return user;
}
public static User of(String name, Integer age, List<String> roles) {
User user = new User();
user.setAge(age);
user.setName(name);
user.setRoles(roles);
return user;
}
}
private static List<User> userList;
private static List<User> userList2;
@Before
private void init() {
userList = new ArrayList<>();
userList.add(User.of("张三", 18, Arrays.asList("ADMIN", "USER")));
userList.add(User.of("李四", 19, Arrays.asList("TEMP:USER")));
userList.add(User.of("王五", 20, Arrays.asList("USER")));
userList2 = new ArrayList<>();
userList2.add(User.of("张三", 18, 55, Arrays.asList("ADMIN", "USER")));
userList2.add(User.of("李四", 19, 100, Arrays.asList("TEMP:USER")));
userList2.add(User.of("甲", 30, 100, Arrays.asList("TEMP:USER")));
userList2.add(User.of("乙", 35, 90, Arrays.asList("TEMP:USER")));
userList2.add(User.of("王五", 20, 80, Arrays.asList("USER")));
userList2.add(User.of("丁", 20, 90, Arrays.asList("TEMP:USER")));
}
遍历
在 Stream 中,peek 和 foreach 都可以对元素进行遍历处理。
**但是 peek 属于中间方法,而 foreach 属于终端方法。**这意味着 peek 作为中途的一个操作步骤,没有办法直接执行得到结果,后面还必须有其他终端方法才会执行。而foreach 作为无返回值的终端方法,则可以执行相关操作。
foreach
log.info("start foreach");
userList.stream().forEach(System.out::println);
log.info("end foreach");
输出结果
16:58:42.666 [main] INFO top.zsmile.test.basic.stream.StreamTest - start foreach
User(name=张三, age=18, roles=[ADMIN, USER])
User(name=李四, age=19, roles=[TEMP:USER])
User(name=王五, age=20, roles=[USER])
16:58:42.709 [main] INFO top.zsmile.test.basic.stream.StreamTest - end foreach
peek
log.info("start peek");
userList.stream().peek(System.out::println);
log.info("end peek");
log.info("start peek -> count");
userList.stream().peek(System.out::println).count();
log.info("end peek -> count");
输出结果
17:09:23.759 [main] INFO top.zsmile.test.basic.stream.StreamTest - start peek
17:09:23.760 [main] INFO top.zsmile.test.basic.stream.StreamTest - end peek
17:09:23.760 [main] INFO top.zsmile.test.basic.stream.StreamTest - start peek -> count
User(name=张三, age=18, roles=[ADMIN, USER])
User(name=李四, age=19, roles=[TEMP:USER])
User(name=王五, age=20, roles=[USER])
17:09:23.763 [main] INFO top.zsmile.test.basic.stream.StreamTest - end peek -> count
过滤 filter
Filter 条件过滤,仅保留符合符合条件的数据。因为 filter 接收的是一个 Predicate 接口,该接口的 boolean test(T t) 方法对给定的参数判断后,返回 Boolean 。
/**
* 过滤年龄小于19岁的用户
*/
@Test
public void filter() {
log.info("原数据:");
userList.stream().forEach(System.out::println);
log.info("过滤后数据:");
userList.stream().filter(item -> item.getAge() >= 19).forEach(System.out::println);
}
输出结果:
17:33:34.739 [main] INFO top.zsmile.test.basic.stream.StreamTest - 原数据:
User(name=张三, age=18, roles=[ADMIN, USER])
User(name=李四, age=19, roles=[TEMP:USER])
User(name=王五, age=20, roles=[USER])
17:33:34.787 [main] INFO top.zsmile.test.basic.stream.StreamTest - 过滤后数据:
User(name=李四, age=19, roles=[TEMP:USER])
User(name=王五, age=20, roles=[USER])
去重 distinct
去除集合中重复的元素,该方法没有参数,去重的规则与 HashSet 相同。
@Test
public void distinct() {
List<String> strings = Arrays.asList("A", "B", "C", "D", "A", "B");
log.info("原数据");
strings.stream().forEach(System.out::println);
log.info("去重后数据");
strings.stream().distinct().forEach(System.out::println);
}
排序 sorted
对集合中的数据进行排序,有默认排序和自定义排序。
- 默认排序是按照基本类型排序,字符串按照字典升序,数字按照大小升序
- 自定义排序,接收一个
Comparator接口,该接口的int compare(T o1, T o2);接收两个参数,将两个数比较的差额进行增减后,返回一个Integer,如果为负数降序,正数升序。
@Test
public void sorted() {
List<Integer> list = Arrays.asList(1, 5, 6, 3, 4, 8, 48);
log.info("基本类型Integer默认排序后数据:");
list.stream().sorted().forEach(System.out::println);
List<String> list2 = Arrays.asList("a", "cc", "b", "e", "s", "m");
log.info("基本类型String默认排序后数据:");
list2.stream().sorted().forEach(System.out::println);
log.info("自定义排序后数据:");
userList.stream().sorted((o1, o2) -> {
System.out.println(o2.getAge() + o1.getAge());
return o2.getAge() + o1.getAge();
}).forEach(System.out::println);
}
输出结果:
18:18:53.594 [main] INFO top.zsmile.test.basic.stream.StreamTest - 基本类型Integer默认排序后数据:
1
3
4
5
6
8
48
18:18:53.654 [main] INFO top.zsmile.test.basic.stream.StreamTest - 基本类型String默认排序后数据:
a
b
cc
e
m
s
18:18:53.655 [main] INFO top.zsmile.test.basic.stream.StreamTest - 自定义排序后数据:
18:18:53.657 [main] INFO top.zsmile.test.basic.stream.StreamTest - 排序比较值:37
18:18:53.660 [main] INFO top.zsmile.test.basic.stream.StreamTest - 排序比较值:39
User(name=张三, age=18, roles=[ADMIN, USER])
User(name=李四, age=19, roles=[TEMP:USER])
User(name=王五, age=20, roles=[USER])
扩展-Comparator
关于Comparator的使用
Comparator 的 thenComparing 用在 comparing 及相关方法之后, 是以上次排序为结果进行再排序。
thenComparing 和 comparing 的参数相同。
- public static <T, U extends Comparable<? super U>> Comparator
comparing(Function<? super T, ? extends U> keyExtractor) - public static <T, U> Comparator
comparing( Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator) - 这里有两个参数,第一个接收
Function函数,第二个参数为排序方式。
Java8 提供了以下几种常见的排序方式:
| 方法名 | 含义 |
|---|---|
| naturalOrder | 自然顺序,正序排列 |
| reverseOrder | 自然顺序,倒序排列 |
| nullsFirst | 自然顺序排序,如果有null,则null在最前。 注意:如果字符串类型为空,则对该字段进行排序时,会抛出异常。 |
| nullsLast | 与nullsFirst类似,但null值会排在最后 |
@Test
public void sorted2() {
log.info("扩展:Comparator。");
log.info("先按照年龄正序,再按照成绩排序倒叙");
userList2.stream().sorted(Comparator.comparing(User::getAge).thenComparing(User::getScore, Comparator.reverseOrder())).forEach(System.out::println);
log.info("使用reversed()方法");
userList2.stream().sorted(Comparator.comparing(User::getAge).thenComparing(User::getScore).reversed()).forEach(System.out::println);
}
输出结果:
20:19:51.251 [main] INFO top.zsmile.test.basic.stream.StreamTest - 扩展:Comparator。
20:19:51.255 [main] INFO top.zsmile.test.basic.stream.StreamTest - 先按照年龄正序,再按照成绩排序倒叙
User(name=张三, age=18, score=55, roles=[ADMIN, USER])
User(name=李四, age=19, score=100, roles=[TEMP:USER])
User(name=丁, age=20, score=90, roles=[TEMP:USER])
User(name=王五, age=20, score=80, roles=[USER])
User(name=甲, age=30, score=100, roles=[TEMP:USER])
User(name=乙, age=35, score=90, roles=[TEMP:USER])
20:19:51.319 [main] INFO top.zsmile.test.basic.stream.StreamTest - 使用reversed()方法
User(name=乙, age=35, score=90, roles=[TEMP:USER])
User(name=甲, age=30, score=100, roles=[TEMP:USER])
User(name=丁, age=20, score=90, roles=[TEMP:USER])
User(name=王五, age=20, score=80, roles=[USER])
User(name=李四, age=19, score=100, roles=[TEMP:USER])
User(name=张三, age=18, score=55, roles=[ADMIN, USER])
注意事项
这里需要注意 reverse() 和 Comparator.reverseOrder() 的区别
reverse()是将排序结果反转,而Comparator.reverseOrder()是直接进行排序。- 建议使用
Comparator.reverseOrder(),更容易理解,并符合排序逻辑。
限制和跳过 limit&skip
- limit:截取数据流头 n 个数据
- skip:跳过数据流头 n 个数据
@Test
public void limitAndSkip() {
log.info("原数据");
userList.stream().forEach(System.out::println);
log.info("取前两个");
userList.stream().limit(2).forEach(System.out::println);
log.info("跳过第一个");
userList.stream().skip(1).forEach(System.out::println);
log.info("获取中间一个");
userList.stream().skip(1).limit(1).forEach(System.out::println);
}
映射
stream 流中的映射都是属于中间操作,分为两类:
- map:map对集合中的每个类进行操作,一般用于类型转换和结果映射,某种程度上,和
foreach效果相近。 - flatMap: 与
map相同的是对集合中的每个数据进行操作;不同的是,flatMap可以将每个数据转换为任意数量个的输出值,即扁平化。
map
@Test
public void map() {
log.info("原数据");
userList.stream().forEach(System.out::println);
log.info("将 User 转化为 String ,值来源于User::getName");
userList.stream().map(User::getName).forEach(System.out::println);
}
输出结果
20:53:17.999 [main] INFO top.zsmile.test.basic.stream.StreamTest - 原数据
User(name=张三, age=18, roles=[ADMIN, USER])
User(name=李四, age=19, roles=[TEMP:USER])
User(name=王五, age=20, roles=[USER])
20:53:18.091 [main] INFO top.zsmile.test.basic.stream.StreamTest - 将 User 转化为 String ,值来源于User::getName
张三
李四
王五
flatMap
@Test
public void flatMap() {
log.info("原数据");
userList.stream().forEach(System.out::println);
log.info("将 User 转化为 String ,值来源于User::getName");
log.info("使用flatMap 重复上面操作");
userList.stream().flatMap(item -> Stream.of(item.getName())).forEach(System.out::println);
}
输出结果
20:53:18.093 [main] INFO top.zsmile.test.basic.stream.StreamTest - 使用flatMap 重复上面操作
张三
李四
王五
差异
下面这个案例可以看出,flatMap 会将数组进行扁平化输出。 如果不使用 flatMap 返回值为 List<List<String>> 类型,使用之后为 List<String>。
@Test
public void mapAndFlatMap() {
log.info("原数据");
userList.stream().forEach(System.out::println);
log.info("体现 Map 和 flatMap 差异");
userList.stream().map(item -> item.getRoles()).forEach(System.out::println);
userList.stream().flatMap(item -> item.getRoles().stream()).forEach(System.out::println);
}
输出结果
20:56:47.573 [main] INFO top.zsmile.test.basic.stream.StreamTest - 原数据
User(name=张三, age=18, roles=[ADMIN, USER])
User(name=李四, age=19, roles=[TEMP:USER])
User(name=王五, age=20, roles=[USER])
20:56:47.630 [main] INFO top.zsmile.test.basic.stream.StreamTest - 体现 Map 和 flatMap 差异
[ADMIN, USER]
[TEMP:USER]
[USER]
ADMIN
USER
TEMP:USER
USER
匹配 match
- matchAll:集合中的所有元素是否都满足条件,才返回 True
- matchAny:集合中的元素是否有满足条件,才返回 True
- matchNone:集合中的所有元素都不满足条件,才返回 True
@Test
public void match() {
log.info("原数据");
userList.stream().forEach(System.out::println);
log.info("判断是否都已成年,{}", userList.stream().allMatch(item -> item.getAge() >= 18));
log.info("判断是否有19岁以上的,{}", userList.stream().anyMatch(item -> item.getAge() >= 19));
log.info("判断是未成年的,{}", userList.stream().noneMatch(item -> item.getAge() < 18));
}
Optional
操作与说明
创建Optional
创建Optional 主要有三种方式:
- Optional.empty。创建一个空的
Optional - Optional.of(T t)。创建一个存在T类型值的
Optional,注意不能传入null值,否则会报 空指针异常。 - Optional.ofNullable(T t)。创建一个存在T类型值的
Optional,允许传入null值,会返回一个Optional.empty
@Test
public void option() {
Optional<Object> empty = Optional.empty();
log.info("创建一个空的 Optional ==> {}", empty);
Optional<String> s = Optional.of("1");
log.info("创建一个有值的 Optional ==> {}", s);
// 传入null值会抛出 java.lang.NullPointerException
// Optional<String> s2 = Optional.of(null);
// log.info("创建一个为null的 Optional ==> {}", s2);
Optional<Object> o = Optional.ofNullable(null);
log.info("创建一个为null的 Optional ==> {}", o);
}
空检验
- isPresent():判断
Optional是否为空,为空返回false,否则返回true - ifPresent(Consumer<? super T> consumer):不为空则执行操作处理。
@Test
public void optionEmpty() {
Optional<Object> empty = Optional.empty();
log.info("isPresent => {}", empty.isPresent());
Optional<String> s = Optional.of("1");
s.ifPresent(item -> {
log.info("我的值 {}", item);
});
}
输出结果
isPresent => false
我的值 1
获取值
- get。获取
Optional中的对象,当Optional为空时报错No value present - orElse。 不论容器是否为空,只要调用该方法, 则对象
other一定存在。但是为空时返回Optional的值,不为空才返回other - orElseGet。 只有当容器为空时,才调用
supplier.get()方法产生对象。 - orElseThrow。当容器为空时,才调用
supplier.get()方法抛出异常。
@Test
public void optionGet() {
Optional<String> s = Optional.of("1");
log.info("创建一个有值的 Optional ==> {}", s);
Optional<Object> o = Optional.ofNullable(null);
log.info("创建一个为null的 Optional ==> {}", o);
try {
log.info("get => {}", s.get());
log.info("get => {}", o.get());
} catch (Exception ex) {
log.error("null get =>{}", ex.getMessage());
}
log.info("orElse => {}", s.orElse(pp("2")));
log.info("orElseGet => {}", s.orElseGet(() -> {
log.info(" 我被执行了 s1");
return "s1";
}));
log.info("orElse => {}", o.orElse(2));
log.info("orElseGet => {}", o.orElseGet(() -> {
log.info("s2");
return "s2";
}));
o.orElseThrow(() ->{
return new RuntimeException("空的异常");
});
}
private String pp(String str) {
log.info("我被执行了");
return str;
}
输出结果
17:03:21.566 [main] INFO top.zsmile.test.basic.stream.StreamTest - 创建一个有值的 Optional ==> Optional[1]
17:03:21.577 [main] INFO top.zsmile.test.basic.stream.StreamTest - 创建一个为null的 Optional ==> Optional.empty
17:03:21.578 [main] INFO top.zsmile.test.basic.stream.StreamTest - get => 1
17:03:21.578 [main] ERROR top.zsmile.test.basic.stream.StreamTest - null get =>No value present
17:03:21.578 [main] INFO top.zsmile.test.basic.stream.StreamTest - 我被执行了
17:03:21.578 [main] INFO top.zsmile.test.basic.stream.StreamTest - orElse => 1
17:03:21.633 [main] INFO top.zsmile.test.basic.stream.StreamTest - orElseGet => 1
17:03:21.633 [main] INFO top.zsmile.test.basic.stream.StreamTest - orElse => 2
17:03:21.633 [main] INFO top.zsmile.test.basic.stream.StreamTest - s2
17:03:21.633 [main] INFO top.zsmile.test.basic.stream.StreamTest - orElseGet => s2
java.lang.RuntimeException: 空的异常
// ....
可以从结果中看到,Optional 不为空时,orElse 依然调用方法并产生了一个String对象,orElseGet 没有调用方法。
所以可见orElseGet 更优, 但代价就是需要传入一个Supplier<T>类型的参数。
过滤
Optional<T> filter(Predicate<? super T> predicate) 如果容器不为空,则返回原先的值,如果predicate 返回 true,则返回原先的值,否则返回 empty
Optional<String> s = Optional.of("2");
log.info("原数据 {}", s);
Optional<String> s1 = s.filter(item -> {
if (item.equalsIgnoreCase("1")) {
return true;
} else {
return false;
}
});
log.info("filter {}", s1);
输出结果
17:45:34.594 [main] INFO top.zsmile.test.basic.stream.StreamTest - 原数据 Optional[2]
17:45:34.666 [main] INFO top.zsmile.test.basic.stream.StreamTest - filter Optional.empty
映射
- map:如果
Optional不为空,则将原先的值映射为新的对象,并用一个新的Optional存放。 - flatMap:如果
Optional不为空,则将原先的值映射为新的Optional对象。 - 二者区别在于:
map允许返回空值,而flat不允许为空。map的Function接口方法都是返回 Object,而flatMap返回的是Optional对象。
查找 find
- findFirst:获取第一个匹配的元素
- findAny:获取任意一个,经实验,多数为第一个
@Test
public void find() {
List<Double> collect = Stream.generate(Math::random).limit(999999).distinct().collect(Collectors.toList());
Optional<Double> first = collect.stream().findFirst();
log.info("findFirst => {}", first);
Optional<Double> any = collect.stream().findAny();
log.info("findAny => {}", any);
}
输出结果
11:15:02.155 [main] INFO top.zsmile.test.basic.stream.StreamTest - findFirst => Optional[0.7103524491747812]
11:15:02.161 [main] INFO top.zsmile.test.basic.stream.StreamTest - findAny => Optional[0.7103524491747812]
归并 reduce
reduce 有三个重要概念:
- Identity(初始值定义):归并操作的初始值,如果
Stream为空,也是默认返回值。 - accumulator(累进器):定义两个参数,第一个是上一次归并操作的返回值,第二个是
Stream的下一个元素。 - combiner(组合器):调用一个函数来组合归并操作的结果,当归并是用来并行执行或当累进器及实现类型不匹配时,才调用。
reduce 有三个重载方法。
- Optional
reduce(BinaryOperator accumulator); - 接收一个
BinaryOperator的 lambda表达式,返回一个Optional - 当执行结果为空时,返回的是
Optional.empty
- 接收一个
- T reduce(T identity, BinaryOperator
accumulator); - 与上一个不同的,它定义了一个初始值,并且返回值是与初始值类型相同
- 当执行结果为空时,返回的是默认值
- U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator combiner);
- 主要是为了解决并发和复杂类型的计算,如例子所言,我想统计所有人的年龄总和,可是光用前两种参数,并没有办法执行统计后的数据类型。
@Test
public void reduce() {
List<Integer> collect = Stream.iterate(0, integer -> integer + 1).limit(10).collect(Collectors.toList());
log.info("原数据");
collect.stream().forEach(System.out::println);
log.info("总计:{}", collect.stream().reduce((a, b) -> a + b));
log.info("总计:{}", collect.stream().skip(10).reduce((a, b) -> a + b));
log.info("总计:{}", collect.stream().skip(10).reduce(0, (a, b) -> a + b));
log.info("原数据2");
userList.stream().forEach(System.out::println);
// 注意:这个写法无法通过编译,Stream的实现类型是 User,而累进器返回的类型是Integer,所以这需要另外使用Integer::sum 做组合操作。
// userList.stream().reduce(0, (a, b) -> a + b.getAge())
log.info("总计年龄:{}", userList.stream().reduce(0, (a, b) -> a + b.getAge(), Integer::sum));
}
输出结果
11:42:38.197 [main] INFO top.zsmile.test.basic.stream.StreamTest - 原数据
0
1
2
3
4
5
6
7
8
9
11:42:38.202 [main] INFO top.zsmile.test.basic.stream.StreamTest - 总计:Optional[45]
11:42:38.205 [main] INFO top.zsmile.test.basic.stream.StreamTest - 总计:Optional.empty
11:42:38.206 [main] INFO top.zsmile.test.basic.stream.StreamTest - 总计:0
11:42:38.206 [main] INFO top.zsmile.test.basic.stream.StreamTest - 原数据2
User(name=张三, age=18, roles=[ADMIN, USER])
User(name=李四, age=19, roles=[TEMP:USER])
User(name=王五, age=20, roles=[USER])
11:42:38.207 [main] INFO top.zsmile.test.basic.stream.StreamTest - 总计年龄:57
总结
合理正确的使用 Stream 流,可以给我们带来:
- 代码简洁。更贴合数据流处理的思想。
- 逻辑解耦。可以将各个任务分成多段流式进行处理。
- 延迟执行。只有调用终端方法时,前面编写的中间操作才会执行,避免不必要的操作损耗。
当然也会有些新的问题
- 调试不方便
- 需要适应语法,熟悉lambda的使用,才能更高效开发。
概述
- 面向对象编程思想
- 一切事物都是对象。
- 对象是其属性和操作的封装体。
- 强调的是对象,必须通过对象的形式来做事,一般情况下会比较复杂。
- 函数编程思想
- 函数强调的是按照输入量、输出量,使用输入量计算得出输出量。【拿什么东西做什么事情】
- 同样执行线程任务,使用函数编程思想,可以直接通过传递一段代码给线程对象执行,不需要创建任务对象。
函数式接口
Functional interface 接口也称 SAM 接口,即Single Abstract Method interfaces,有且只有一个抽象方法的接口,但可以有多个非抽象方法的接口。
-
在 Java 8 中专门有一个包放函数式接口
java.util.function,该包下的所有接口都有@FunctionalInterface注解,提供函数式接口。 -
在其他包中也有函数式接口,其中一些没有
@FunctionalInterface注解,但是只要符合函数式接口的定义就是函数式接口,与是否有@FunctionalInterface注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。例如Runnable接口,也是没有使用@FunctionalInterface注解
什么是lambda
lambda 表达式,也可以称之为闭包或者匿名函数,是 Java8 引入的新特性。
lambda 允许把函数作为一个方法的参数(函数作为参数传入方法中执行)
lambda 使用前提
-
必须有相应的函数式接口。函数接口是指有且只有一个抽象方法的接口。
-
类型推断机制。在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显示指定。也就是方法的参数和局部变量的类型必须为
lambda对应的接口类型,才能使用lambda表达式表示该接口的实例。
语法
简写
(parameters) -> expression
(parameters) ->{ statements; }
重要特征:
- **可选类型声明:**不需要声明参数类型,编译器可以统一识别参数值。
- **可选的参数圆括号:**一个参数无需定义圆括号,但多个参数需要定义圆括号。
- **可选的大括号:**如果主体包含了一个语句,就不需要使用大括号。
- **可选的返回关键字:**如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。
方法引用
如果我们使用lambda 表达式的时候,如果要执行的表达式只是调用一个类已有的方法,那么就可以用方法引用的方式来替代。
重要特种:
- **引用静态方法:**类名::静态方法名。如果静态方法需要传参数,只要确保参数是一一对应的,编译器会自行推断出来。
- 引用对象方法:对象引用::方法名。如果执行的类是调用
lambda表达式所在的类的方法时,可以才作用一下写法this::方法名。 - 引用构造方法:类名::new。
实战
替代匿名内部类
class LambdaTest {
public static void main(String[] args) {
// 匿名内部类, Runnable
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程输出");
}
});
// lambda
Thread thread1 = new Thread(() -> {
System.out.println("lambda线程输出");
});
}
}
原理分析
那么用 lambda 来替代匿名内部类有什么差别呢?我们通过编译一下来看看
# 编译class文件
javac -encoding UTF-8 LambdaTest.java
可以看见产生了两个class文件。
-
LambdaTest.class
import top.zsmile.test.basic.lambda.LambdaTest.1; public class LambdaTest { public LambdaTest() { } public static void main(String[] var0) { new Thread(new 1()); new Thread(() -> { System.out.println("lambda线程输出"); }); } } -
LambdaTest$1.class。这是一个匿名内部类。
final class LambdaTest$1 implements Runnable { LambdaTest$1() { } public void run() { System.out.println("线程输出"); } }
那么我们来看看这个代码字节码,看看程序内部是怎么运行的。
javap -c “.\LambdaTest.class”
javap -c “.\LambdaTest$1.class”
这里我们只看主文件的字节码。
Compiled from "LambdaTest.java"
public class top.zsmile.test.basic.lambda.LambdaTest {
public top.zsmile.test.basic.lambda.LambdaTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class top/zsmile/test/basic/lambda/LambdaTest$1
7: dup
8: invokespecial #4 // Method top/zsmile/test/basic/lambda/LambdaTest$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: astore_1
15: new #2 // class java/lang/Thread
18: dup
19: invokedynamic #6, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
24: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
27: astore_2
28: return
}
由这个字节码信息可以分析出:
-
根据
4: new #3这行可以看出,这里使用了LambdaTest$1.class类,new了一个新的对象。 -
根据
19: invokedynamic #6, 0可以看出,lambda是通过invodedynamic实现的,并且不会生成新的类和对象。invodedynamic是Jvm提供的一种指令语法。在这里作用是创建并调用Runnable接口 run 方法。而新增的
invokedynamic指令,配合新增的方法句柄(Method Handles,它可以用来描述一个跟类型A无关 的方法m的签名,甚至不包括方法名称,这样就可以做到我们使用方法m的签名,但是直接执行的时候调用 的是相同签名的另一个方法b),可以在运行时再决定由哪个类来接收被调用的方法。在此之前,只能使用反射来实现类似的功能。该指令使得可以出现基于Jvm的动态语言,让Jvm更加强大。而且在Jvm上实现动态调用机制,不会破坏原有的调用机制。这样既很好的支持了Scala、Clojure这些JVM上的动态语言,又可以支持代码里的动态lambda表达式。简单来说就是以前设计某些功能的时候把做法写死在了字节码里,后来想改也改不了了。 所以这次给lambda语法设计翻译到字节码的策略是就用invokedynamic来作个弊,把实际的翻译策略隐 藏在Jdk 库的实现里(MetaFactory)可以随时改,而在外部的标准上大家只看到一个固定的 invokedynamic。
-
既然
lambda不会生成新的类,那么这里的this指向外部类的。简单上个demo。可以看出使用匿名内部类时,this指向当前类,而lambda没有生成新的类,所以指向的外部类,所以无法在静态static方法中使用,因为static方法没有实例对象。public class LambdaTest { public void run(){ new Thread(new Runnable() { @Override public void run() System.out.println(this); System.out.println("线程输出"); } }).start(); // lambda new Thread(() -> { System.out.println(this); System.out.println("lambda线程输出"); }).start(); } public static void main(String[] args) { new LambdaTest().run(); } } // 输出结果 // top.zsmile.test.basic.lambda.LambdaTest$1@1232cc0 // 线程输出 // top.zsmile.test.basic.lambda.LambdaTest@7e8572a1 // lambda线程输出
为什么不能修改外部变量
lambda 表达式的局部变量可以不用声明为 final ,但是必须不可被 lambda 内部的代码修改(即具有隐性的 final )。可是我们有时又能修改局部变量的值,这是为什么呢?
其实这和变量的类型和 final 的作用有关联,如果我们使用一些常用的类型。例如:
- 基础类型:int,float等
- 简单引用类型:String,Integer等
- 复杂对象引用
- 静态类型引用
上个简单的例子说明一下;
public class LambdaTest {
public void run() {
String fff = "";
new Thread(() -> {
this.name = "456";
// fff="456";
//Variable used in lambda expression should be final or effectively final
System.out.println(this.name);
System.out.println("lambda线程输出");
}).start();
}
}
这里提到了 final or effectively final。
对于一个变量,如果没有给它加final修饰,而且没有对它的二次赋值,那么这个变量就是effectively final(有效的不会变的),如果加了final那肯定是不会变了哈。
那么如果我们想要在内部更改外部的属性,有什么方式?
- 使用对象的属性
- 使用引用的属性
- 使用静态的属性
总结
lambda 的优点:
- 简化了部分的写法,使代码更为简洁紧凑
- 减少匿名内部类的创建,节省内存开支
- 不需要记忆所使用的接口和抽象函数
lambda 的缺点:
- 易读性较差,阅读代码的人需要熟悉
lambda表达式和抽象函数中参与的类型。 - 不具备匿名内部类可扩展的特性
引用
- https://www.oracle.com/java/technologies/java8.html
- https://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&mid=2651144680&idx=5&sn=cea5cf2b8748e4e94894558914bf26ec&chksm=bdb8b9bb8acf30ad90011a970dd18acbe2be555609d348601d76d566e4be7e4aad2b6c719e7a&scene=27#wechat_redirect
- https://www.infoq.cn/article/LlrBgvdmYPGNsVDOZuCZ
- https://xie.infoq.cn/article/a7a4b91228653c64f866367ca
- https://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247550085&idx=5&sn=d13402dde3e283ce0140515d6347873f&chksm=9bd3a91daca4200bae75e718a36fc5aaa85f19d03f6436f1c64f86b6d1848051c4a9f6414754&scene=27#wechat_redirect
- https://xie.infoq.cn/article/b47fa71dff45feb333a31c6dc
- https://blog.csdn.net/qq_31635851/article/details/116484594
消除if-else
消除单个IF-else
- 定义函数式接口
创建一个名为IfElseFunction的函数式接口,接口的参数为两个Runnable接口。这两个两个Runnable接口分别代表了为true或false时要进行的操作。
- 编写判断方法
在里面设置了一个静态方法 isTrueOrFalse,接收 boolean 类型传参,并返回 IfElseFunction 函数接口。
@FunctionalInterface
public interface IfElseFunction {
void trueOrFalseHandle(Runnable trueRun, Runnable falseRun);
static IfElseFunction isTrueOrFalse(boolean bool) {
return (tr, fr) -> {
if (bool) {
tr.run();
} else {
fr.run();
}
};
}
}
- 使用方式
@Test
public void test4() {
// 判断是否
IfElseFunction.isTrueOrFalse(true).trueOrFalseHandle(() -> {
System.out.println("我是对的");
}, () -> {
System.out.println("我是错的");
});
// 可以稍微转换一下,用来判断空值并做操作
String name = null;
IfElseFunction.isTrueOrFalse(name != null).trueOrFalseHandle(() -> {
System.out.println("我的名字是:" + name);
}, () -> {
System.out.println("我不知道我的名字");
});
}
消除多个if-else
还是用静态方法 IfElseFunction 接口,我们稍微改写一下使用方式即可。这里面可以看出使用了策略模式。
private static Map<String, Runnable> map;
@Before
public void before() {
map = new HashMap<>();
map.put("A", () -> {
System.out.println("I’m A");
});
map.put("B", () -> {
System.out.println("I’m B");
});
map.put("C", () -> {
System.out.println("I’m C");
});
map.put("D", () -> {
System.out.println("I’m D");
});
}
@Test
public void test5() {
String searchKey = "F";
IfElseFunction.isTrueOrFalse(map.get(searchKey) != null).trueOrFalseHandle(map.get(searchKey), () -> {
System.out.println("找不到用户");
});
}
前言
在 lambda 中了解了什么是函数式接口,以及 @FunctionalInterface 注解。
我们知道函数式接口中,有且只有一个抽象方法的接口,但可以有多个非抽象方法的接口。
而 @FunctionalInterface 注解是可以检查一个接口是否为函数式接口。只是在编译时强制规范使用。 如果接口是函数接口,编译通过;如果不是,编译失败。
我们自定义函数式接口时,@FuncationalInterface 注解是可选的,就算我们不写这个注解,只要保证是函数式接口也是可以的。但是建议加上。
Java 8 在 java.util.function 包下预定了大量的函数式接口供我们使用。常用的有:
- 生产型 Supplier 接口
- Function 接口
- Consumer 接口
- Predicate 接口
生产型 Supplier
java.util.function.Supplier<T> 接口仅包含一个无参的方法 T get()。用来获取一个泛型参指定类型的对象数据。
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
案例
@Test
public void test() {
String string = supplierGet(() -> {
return "supplier test";
});
System.out.println(string);
}
private static <T> T supplierGet(Supplier<T> sup) {
return sup.get();
}
消费型 Consumer
java.util.function.Consumer<T> 接口与 Supplier 接口相反,它是消费一个数据,不产生输出,其类型是一个泛型。Consumer 中包含抽象方法void accept(T t) 意思接受一个指定泛型的数据进行处理,此外还有一个默认实现方法 Consumer<T> andThen(Consumer<? super T> after)。
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
案例_accept
@Test
public void test() {
consumerAccept("qweqweqwe", (item) -> {
String s = new StringBuilder(item.toString()).reverse().toString();
System.out.println(s);
});
}
private static <T> void consumerAccept(T name, Consumer<T> com) {
com.accept(name);
}
}
这里无论 Consumer 都会将其做一次反转后,进行打印。
案例_andThen
@Test
public void test2() {
consumerAndThen("qwE123", (item) -> {
System.out.println("先执行了我");
String s = new StringBuilder(item.toString()).reverse().toString();
System.out.println(s);
}, item -> {
System.out.println("后执行了我");
String s = item.toUpperCase().toLowerCase();
System.out.println(s);
});
}
private static <T> void consumerAndThen(T name, Consumer<T> com, Consumer<T> com2) {
com.andThen(com2).accept(name);
}
输出结果
先执行了我
321Ewq
后执行了我
qwe123
这里需要注意的点是:
- 都是对原数据进行了操作,如果是基本类型值不变,如果是引用对象,只修改了引用对象的内部值。
- 在
consumer内部修改方法剧本变量的值,会提示异常Variable used in lambda expression should be final or effectively final。这是一个隐式的final。 - 这里可以做链式消费数据,但是需要主要处理对象的主体。
判断型 Predicate
java.util.function.Predicate<T> 接口是接受一个特定泛型的数据,从而获得一个 boolean 结果。其中包含一个抽象方法 boolean test(T t), 根据传入参数做检查后,返回true/false。2
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
案例_test
@Test
public void test() {
boolean checkBool = predicateTest("str", item -> {
if (item instanceof String) {
return true;
}
return false;
});
System.out.println(checkBool);
}
private static <T> boolean predicateTest(T obj, Predicate<T> predicate) {
return predicate.test(obj);
}
案例_negate
Predicate 提供了一个 negate,意为取反,即将返回boolean进行非处理。
@Test
public void tes2() {
boolean checkBool = predicateNegate("str", item -> {
if (item instanceof String) {
return true;
}
return false;
});
System.out.println(checkBool);
}
private static <T> boolean predicateNegate(T obj, Predicate<T> predicate) {
// 等效于 !predicate.test(obj);
return predicate.negate().test(obj);
}
案例_and
Predicate 接口中有一个方法 and,表示并且关系,同时满足返回 TRUE。
/**
* 1. 判断是否为String类型
* 2. 判断是否以STR开始
* 两个调价你必须同时满足
*/
@Test
public void tes3() {
boolean checkBool = predicateAnd(Integer.valueOf(123), item -> {
System.out.println("type");
if (String.class.isInstance(item) ) {
return true;
}
return false;
}, item -> {
System.out.println("starts");
if (item.toString().toUpperCase().startsWith("STR")) {
return true;
}
return false;
});
System.out.println(checkBool);
}
private static <T> boolean predicateAnd(T obj, Predicate<T> predicate, Predicate<T> predicate2) {
// 等效于 predicate.test(obj) && predicate2.test(obj);
return predicate.and(predicate2).test(obj);
}
案例_or
Predicate 接口中有一个方法 or,表示或关系,满足一个返回 TRUE。
/**
* 1. 判断是否为Integer类型
* 2. 判断是否以STR开始
* 两个条件满足一个
*/
@Test
public void test4() {
boolean checkBool = predicateOr(String.valueOf("STR111"), item -> {
System.out.println("type");
if (Integer.class.isInstance(item)) {
return true;
}
return false;
}, item -> {
System.out.println("starts");
if (item.toString().toUpperCase().startsWith("STR")) {
return true;
}
return false;
});
System.out.println(checkBool);
}
private static <T> boolean predicateOr(T obj, Predicate<T> predicate, Predicate<T> predicate2) {
// 等效于 predicate.test(obj) || predicate2.test(obj);
return predicate.or(predicate2).test(obj);
}
类型转换 Function
ava.util.function.Function<T,R> 用来根据一个类型的数据得到另一个类型的数据,前者 T 称为前置条件/传入参数,后者 R 成为后置条件/返回参数。提供了一个最主要的抽象方法 R apply(T t),根据 T 类型的数据获取 R 类型的结果。使用场景:类型转换。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
案例_apply
@Test
public void test() {
Integer integer = functionTest("123123", item -> {
return Integer.valueOf(item);
});
System.out.println(integer);
}
public static <T, R> R functionTest(T obj, Function<T, R> function) {
return function.apply(obj);
}
案例_compose
接口中的默认方法 compose 用来进行组合操作。传入一个 function 先执行,并将处理结果传入下一个 function
/**
* 1. 执行apply(4),将“4” 传入到fc2 转换为 Integer输出
* 2. 在执行fc1 的apply方法,将Integer 4传入。
*/
@Test
public void test2() {
Function<Integer, Integer> fc1 = i -> i * 2;
Function<String, Integer> fc2 = i -> Integer.valueOf(i);
System.out.println(fc1.compose(fc2).apply("4"));
}
案例_andThen
默认方法 andThen 用来进行组合操作。传入一个 function 后执行,并将处理结果传入下一个 function。与compose 功能相反,andThen 添加到后面执行,compose 插入到前面执行
/**
* 1. 接收到 int 4 后,乘以2后,得出8返回
* 2. 将 int 8 传入,转换为 String 返回
*/
@Test
public void test3() {
Function<Integer, Integer> fc1 = i -> i * 2;
Function<Integer, String> fc2 = i -> i + "";
System.out.println(fc1.andThen(fc2).apply(4));
}
final
用法
- 被final修饰的类不可以被继承
- 被final修饰的方法不可以被重写
- 被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
- 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
- 被final修饰的常量,在编译阶段会存入常量池中
两个重排序规则:
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
注意
final和static修饰
用final和static修饰的属性变量(特别是在单例模式),static里面出现trycatch时,需要throw异常,否则会编译错误。
解释: 当属性被static和final同时修饰时,该属性属于类属性(类常量),就是说在类被加载进内存时就需要分配内存(初始化完成)。而构造函数是在被实例化的时候才会执行,对比static代码块是在类被加载的时候执行,且只执行这一次
一道面试题分析
最近看面试题的时候,看到这样一道题目。觉得有意思,就分析了一下看看。
public class FinalStaticClass {
public static void main(String[] args) {
System.out.println(Price.P.Price);//result:-2.7
}
}
class Price {
/**
* method1: give apple add final, result:17.3;
* method2: Swap 18 to 19, result:17.3;
*/
static Price P = new Price(2.7);
static double apple = 20;
double Price;
public Price(double orange) {
Price = apple - orange;
}
}
我们先看一下执行结果。输出2.7
为什么不是17.3呢?让我们先执行一下以下两个指令,生成反汇编文件:
javac .\FinalStaticClass
javap -c -verbose .\FinalStaticClass.class > FinalStaticClass.txt
javap -c -verbose .\Price.class >Price.txt
分析
让我们查看一下Price.txt
// from 75 to 82
0: new #4 // class top/zsmile/jvm/base/Price
3: dup
4: ldc2_w #5 // double 2.7d
7: invokespecial #7 // Method "<init>":(D)V
10: putstatic #8 // Field P:Ltop/zsmile/jvm/base/Price;
13: ldc2_w #9 // double 20.0d
16: putstatic #2 // Field apple:D
19: return
// Constant Pool
#1 = Methodref #11.#25 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#26 // top/zsmile/jvm/base/Price.apple:D
#3 = Fieldref #4.#27 // top/zsmile/jvm/base/Price.Price:D
#4 = Class #28 // top/zsmile/jvm/base/Price
#5 = Double 2.7d
#7 = Methodref #4.#29 // top/zsmile/jvm/base/Price."<init>":(D)V
#8 = Fieldref #4.#30 // top/zsmile/jvm/base/Price.P:Ltop/zsmile/jvm/base/Price;
#9 = Double 20.0d
这里可以看出执行过程是:
- new Price
- 将double 2.7d 入栈
- 执行类初始化,构造方法
- 将double 2.7d 出栈,存入 Price.P
- 将double 20.0d 入栈
- 将double 20.0d 出栈,存入 Price.apple
我们可以看出,在执行构造方法计算 Price(D)的时候,apple还没有赋值20。我们知道对于数字基本类型的时候,默认值都为0。于是在执行构造函数的时候,相当于:0-2.7,所以得出结果-2.7。
这是因为在随后的类加载的初始化阶段,由于static代码块执行顺序是由字段在源文件中出现的顺序决定的,所以会先执行new Price(2.7),分配对象空间并对其做初始化,此时apple的值为0,所以最终结果是-2.7。
那么我们有什么方法解决这个问题?(暂时想到2种)
- 交换P和apple
- 给apple 添加关键字final。
方法1(交换p和apple)
交换后,Price代码如下:
class Price {
static double apple = 20;
static Price P = new Price(2.7);
double Price;
public Price(double orange) {
Price = apple - orange;
}
}
// result : 17.3
老规矩生成汇编代码:
javac .\FinalStaticClass
javap -c -verbose .\Price.class >Price2.txt
让我们查看一下Price.txt
// from 75 to 82
0: ldc2_w #4 // double 20.0d
3: putstatic #2 // Field apple:D
6: new #6 // class top/zsmile/jvm/base/Price
9: dup
10: ldc2_w #7 // double 2.7d
13: invokespecial #9 // Method "<init>":(D)V
16: putstatic #10 // Field P:Ltop/zsmile/jvm/base/Price;
19: return
这里我们可以看到:
- 将20赋值给apple
- new Price。dup是交换栈的操作,可以看做将栈顶空出。
- 将2.7赋值给P。
- 执行构造函数,
方法2(给apple加上final关键字)
添加后,代码如下:
class Price {
/**
* method1: give apple add final keyword, result:17.3;
* method2: Swap 18 to 19, result:17.3;sfsdfsdf
*/
static Price P = new Price(2.7);
final static double apple = 20;
double Price;
public Price(double orange) {
Price = apple - orange;
}
}
javac .\FinalStaticClass
javap -c -verbose .\Price.class >Price3.txt
还是看Price文件。不过这次看两个地方
// from 43 to 46
static final double apple;
descriptor: D
flags: ACC_STATIC, ACC_FINAL
ConstantValue: double 20.0d
// from 74 to 80
0: new #2 // class top/zsmile/jvm/base/Price
3: dup
4: ldc2_w #6 // double 2.7d
7: invokespecial #8 // Method "<init>":(D)V
10: putstatic #9 // Field P:Ltop/zsmile/jvm/base/Price;
13: return
上面代码我们可以得出:apple已经有初始值了,值为20;static代码中只有存入p和执行构造函数。 也就是说现在构造函数的执行,是 20-2.7=17.3。
因为在类加载过程中,会将类中的字面量存入常量池中。而在遇到final关键字时,在编译时,会为该静态属性赋予ConstantValue属性,ConstantValue的作用是使JVM自动为静态变量赋值。
类变量,有两种方式赋值:
- 类构造器方法
- ConstantValue。将会在类加载的准备阶段被赋予所指定的值(这是final static字段必须赋值的原因)
老规矩写的不清晰的地方,可以指出,我会不定时更新和修改。一起加油.
所有的伟大,都来自于一个勇敢的开始。
前言
正常情况下,Java 对象都是保存在 Jvm 的堆内存中的,也就是 Jvm 不在了后,对象也会跟着销毁了。
而序列化相当于提供了一种解决方案,让我们可以在 Jvm 停止前,将对象持久化到磁盘上。这样当再次需要这个对象的时候,我们可以从磁盘中读取出来,反序列化为对象使用。
但是序列化和反序列化,不仅要求类实现了 Serializable 或 Externalizable 序列化接口,还取决于类路径以及类属性是否一致,此外还有一个非常重要的一点就是 序列化ID 要求一致,这个序列化ID就是 SerialVersionUID
作用
在进⾏反序列化时, JVM会把传来的字节流中的
serialVersionUID与本地相应实体类的serialVersionUID进⾏⽐较, 如果相同就认为是⼀致的, 可以进⾏反序列化, 否则就会出现序列化版本不⼀致的异常, 即是InvalidCastException。
当实现java.io.Serializable接口的类没有显式地定义⼀个serialVersionUID变量时候, Java序列化机制会根据编译的Class⾃动⽣成⼀个 serialVersionUID 作序列化版本⽐较⽤, 这种情况下, 如果Class⽂件没有发⽣变化, 就算再编译多次, serialVersionUID 也不会变化的。 但是如果一个Class文件发生变化,那么serialVersionUID 就会发生变化。
也就是说,我们的类实现了 SerialVersion 接口,但是没有定义 SerialVersionUID ,然后序列化。在序列化后,由于某些原因,对类做了变更(比如增加了一个字段),那么重启应用后,对之前序列化的类做反序列化,就会抛出异常。
显式定义
如果我们没有在Class中明确定义一个SerialVersionUID 会有什么问题呢?
先上我们序列化和反序列化代码:
@Test
public void serializationTest() {
SerializationIdClazz serializationClazz = new SerializationIdClazz("SerializationIdClazz", 20);
try {
//序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
objectOutputStream.writeObject(serializationClazz);
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void deserializationTest() {
try {
// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
SerializationIdClazz newClazz = (SerializationIdClazz) objectInputStream.readObject();
log.info("反序列化后对象:{}", newClazz);
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException exception) {
exception.printStackTrace();
}
}
然后是我们测试类。r
@Slf4j
@Data
class SerializationIdClazz implements Serializable {
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 构造器
*/
public SerializationIdClazz(String name) {
log.info("SerializationIdClazz Construct:name={}", name);
this.name = name;
}
public SerializationIdClazz(String name, Integer age) {
log.info("SerializationIdClazz Construct:name={},age={}", name, age);
this.name = name;
this.age = age;
}
}
不定义 SerialVersionUID ,我们直接进行序列化操作,然后对该类增加一个属性(性别sex)。
@Slf4j
@Data
class SerializationIdClazz implements Serializable {
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private int sex;
/**
* 构造器
*/
public SerializationIdClazz(String name) {
log.info("SerializationIdClazz Construct:name={}", name);
this.name = name;
}
public SerializationIdClazz(String name, Integer age) {
log.info("SerializationIdClazz Construct:name={},age={}", name, age);
this.name = name;
this.age = age;
}
}
执行结果:
java.io.InvalidClassException: SerializationIdClazz; local class incompatible: stream classdesc serialVersionUID = 3361313294555402592, local class serialVersionUID = -4142228557208270752
抛出了 InvalidClassException 并且前后自动生成的 serialVersionUID 是不一样的。
所以,一旦类实现了Serializable,就建议明确的定义一个serialVersionUID。不然在修改类的时候,就会发生异常。
设置字段不参加序列化
对于不想进行序列化的变量,可以使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 transient 还有几点注意:
- transient 只能修饰变量,不能修饰类和方法。
- transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。
- static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
原理分析
我们看一下源码针对于读取时,serialVersionUID 不一致抛 InvalidClassException异常。
s=>start: 开始
e=>end: 结束
readObject=>operation: readObject
readObject0=>operation: readObject0
readOrdinaryObject=>operation: readOrdinaryObject
readClassDesc=>operation: readClassDesc
readNonProxyDesc=>operation: readNonProxyDesc
initNonProxy=>operation: initNonProxy
s->readObject->readObject0->readOrdinaryObject->readClassDesc->readNonProxyDesc->initNonProxy->e
我们直接看异常抛出的关键代码
/**
* Initializes class descriptor representing a non-proxy class.
*/
void initNonProxy(ObjectStreamClass model,
Class<?> cl,
ClassNotFoundException resolveEx,
ObjectStreamClass superDesc)
throws InvalidClassException
{
// 当前model的序列化ID,如果为空则重新计算。
long suid = Long.valueOf(model.getSerialVersionUID());
// 省略部分代码
if (model.serializable == osc.serializable &&
!cl.isArray() &&
suid != osc.getSerialVersionUID()) {
throw new InvalidClassException(osc.name,
"local class incompatible: " +
"stream classdesc serialVersionUID = " + suid +
", local class serialVersionUID = " +
osc.getSerialVersionUID());
}
}
比较两个serialVersionUID 是否一致。如果不相等则抛出异常。
SerialVersionUID不是被static变量修饰了吗
static 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。 static 变量是属于类的而不是对象。你反序列之后,static 变量的值就像是默认赋予给了对象一样,看着就像是 static 变量被序列化,实际只是假象罢了。
生成serialVersionUID
生成serialVersionUID 的方式主要有2种:
-
使用默认的1L。
private static final Long serialVersionUID = 999L; -
根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段 。可以借助IDE工具生成。
IDEA设置后,才能自动生成
- 进入 Settings 设置。

- 在实现了
Serializable的类使用快捷键Alt + Enter。

总结
- 当一个类实现
serializable接口时,最好定义serialVersionUID serialVersionUID主要是用来验证版本一致性的。所以兼容升级时,不要修改serialVersionUID。- 非兼容升级时,要修改
serialVersionUID。但是要做好异常处理。
反射对单例的破坏
测试的单例类:
@Data
class Singleton implements Serializable{
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
测试方法:
@Test
public void test1() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Singleton instance = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton = constructor.newInstance();
log.info("instance==singleton ==> {}", instance == singleton);
}
输出结果:
ReflexTest - instance==singleton ==> false
我们可以知道反射会破坏单例性,而且这种方式是无法避免的,那么序列化对单例的破坏呢?
序列化对单例的破坏
单例的测试类:
@Data
class Singleton implements Serializable{
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
测试方法:
@Test
public void test2() throws Exception {
Singleton singleton = Singleton.getInstance();
try {
//序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
objectOutputStream.writeObject(singleton);
objectOutputStream.close();
// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
Singleton newClazz = (Singleton) objectInputStream.readObject();
log.info("反序列化后对象:{}", newClazz);
objectInputStream.close();
log.info("newClazz==singleton ==> {}", newClazz == singleton);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException exception) {
exception.printStackTrace();
}
}
输出结果:
ReflexTest - 反序列化后对象:Singleton()
ReflexTest - newClazz==singleton ==> false
通过对 Singleton 的序列化和反序列化,生成了一个新的对象,破坏了 Singleton 的单例性。那么我们可以通过深入理解一下反序列化过程中发生了什么?ObjectInputStream 如何起作用的?
ObjectInputStream
分析一下 ObjectInputputStream 的 readObject 方法执行情况到底是怎样的。
大致流程
st=>start: 开始
e=>end: 结束
readObject=>operation: readObject
readObject0=>operation: readObject0
switchType=>condition: 判断类别为对象 TC_OBJECT
readObject=>operation: readObject
op=>operation: 其它操作
readOrdinaryObject=>operation: readOrdinaryObject
st->readObject->readObject0->switchType
switchType(yes)->readOrdinaryObject->e
switchType(no)->op
核心代码分析
// readOrdinaryObject
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
// 检查引用的类实例是否允许反序列化
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
// 加载类
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
// 类是否可以在运行时进行初始化,可以则调用初始化,这里就会破坏单例模式
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
// 如果类描述符实现了 Externalizable 接口,则调用 readExternalData 进行读取数据。
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
// 如果 hasReadResolveMethod 检查是否设置了 readResolve方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
// 调用readResolve方法
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
// 过滤替换对象
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
这里简单做一个小结:
- 反序列化的时候,会先检查一下类型是什么?如果是Object方法,就会调用readOrdinaryObject方法,如果是其他类型,就调用对应的方法,详情可查看
readObject0方法 readOrdinaryObject方法里面会先读取类描述,然后检查类是否允许反序列化- 通过 isInstantiable 的调用检查类是否可以在运行初始化,如果可以就会进行初始化。这里通过反射破坏了单例。
- 如果类描述符实现了 Externalizable 接口,则调用 readExternalData 进行读取数据。
- 调用 hasReadResolveMethod 检查是否设置了 readResolve 方法,如果设置了则调用readResolve方法。这里防止序列化破坏
防止序列化破坏单例
修改 Singleton 类增加 readResolve 方法。
@Data
class Singleton implements Serializable {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
// 防止序列化破坏单例
private Object readResolve(){
return getInstance();
}
}
再次调用测试方法,输出结果如下:
ReflexTest - 反序列化后对象:Singleton()
ReflexTest - newClazz==singleton ==> true
前言
-
什么是序列化和反序列化
- Serializatable(序列化):将 Java 对象转化为字节序列进行传输/保存的过程。
- DeSerialization(反序列化):将字节序列转化为 Java 对象。
-
意义
- 序列化机制允许将实现序列化机制的Java 对象准换回字节序列,这些字节序列可以保存到磁盘上,也通过通过网络传输后,再转换回原来的对象。
- 该机制使得对象可以脱离程序运行而独立存在。
-
常见用途
- 将对象的字节序列保存到磁盘上。 建议:程序里的每个 JavaBean 对象都实现 Serializable 接口
- 在网络通信中传输对象的字节序列。
实现
Java 中,对象如果想要实现序列化,必须要实现以下两个接口之一:
- Serializable
- Externalizable
Serializable
Serializable 的序列化和反序列化大致有以下4种,后3种可以提供不同程度(越来越灵活)的序列化。
- 通过 ObjectOutputStream 和 ObjectInputStream 默认的序列化和反序列化方式
- 使用 transient 关键字,可选序列化的字段。
- Serializable 序列化类定义了 writeObject(ObjectOutputStream out) 和 readObject(ObjectInputStream in),则通过类定义的方法进行操作。
- private void writeObject(java.io.ObjectOutputStream out) throws IOException;
- private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
- private void readObjectNoData() throws ObjectStreamException;
- Serializable 序列化类定义了 writeReplace 和 readResolve 方法。
- ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
- ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
默认方式
// Test Class
@Slf4j
@Data
class SerializationClazz implements Serializable {
// private static String staticVar = "123";
private static final Long serialVersionUID = 1L;
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private int sex;
private final String fff = "1";
private void writeObject(ObjectOutputStream out) throws IOException {
log.info("writeObject");
//将名字反转写入二进制流
out.writeObject(new StringBuffer(this.name).reverse().toString());
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
log.info("readObject");
this.name = (String) in.readObject();
this.age = in.readInt();
}
/**
* 构造器
*/
public SerializationClazz(String name) {
log.info("SerializationClazz Construct:name={}", name);
this.name = name;
}
public SerializationClazz(String name, Integer age) {
log.info("SerializationClazz Construct:name={},age={}", name, age);
this.name = name;
this.age = age;
}
public SerializationClazz(String name, Integer age, int sex) {
log.info("SerializationClazz Construct:name={},age={},sex={}", name, age, sex);
this.name = name;
this.age = age;
this.sex = sex;
}
}
// Test Function
@Test
public void SerializationTest() {
SerializationClazz serializationClazz = new SerializationClazz("Serialization", 20);
try {
// 序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
objectOutputStream.writeObject(serializationClazz);
objectOutputStream.close();
// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
SerializationClazz newClazz = (SerializationClazz) objectInputStream.readObject();
log.info("反序列化后对象:{}", newClazz);
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException exception) {
exception.printStackTrace();
}
}
我们可以看到:
- 这个对象的所有属性(包括private和引用的属性),除了static和final修饰的,都可以被序列化和反序列化。
transient 关键字
使用transient关键字选择不需要序列化的字段。
// Test Class
@Slf4j
@Data
class SerializationClazz implements Serializable {
// private static String staticVar = "123";
private static final Long serialVersionUID = 1L;
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private transient int sex;
private void writeObject(ObjectOutputStream out) throws IOException {
log.info("writeObject");
//将名字反转写入二进制流
out.writeObject(new StringBuffer(this.name).reverse().toString());
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
log.info("readObject");
this.name = (String) in.readObject();
this.age = in.readInt();
}
/**
* 构造器
*/
public SerializationClazz(String name) {
log.info("SerializationClazz Construct:name={}", name);
this.name = name;
}
public SerializationClazz(String name, Integer age) {
log.info("SerializationClazz Construct:name={},age={}", name, age);
this.name = name;
this.age = age;
}
public SerializationClazz(String name, Integer age, int sex) {
log.info("SerializationClazz Construct:name={},age={},sex={}", name, age, sex);
this.name = name;
this.age = age;
this.sex = sex;
}
}
// Test Function
@Test
public void SerializationTest() {
SerializationClazz serializationClazz = new SerializationClazz("Serialization", 20);
try {
// 序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
objectOutputStream.writeObject(serializationClazz);
objectOutputStream.close();
// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
SerializationClazz newClazz = (SerializationClazz) objectInputStream.readObject();
log.info("反序列化后对象:{}", newClazz);
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException exception) {
exception.printStackTrace();
}
}
从输出结果,我们可以看出transient修饰的sex属性,Java 序列化的时候,会忽略该字段属性。 对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
使用 transient 关键字,我们是能很方便的忽略某些字段,但是有时候我们希望进行编解码或者加密时,transient 的灵活度并不能满足我们的要求。
序列化类重写 writeObject 和 readObject 方法
/**
* Test writeObject and readObject
*/
@Slf4j
@Data
class SerializationClazz2 implements Serializable {
// private static String staticVar = "123";
private static final Long serialVersionUID = 1L;
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private int sex;
private void writeObject(ObjectOutputStream out) throws IOException {
log.info("writeObject");
out.writeObject(new StringBuilder(this.name).reverse());
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
log.info("readObject");
this.name = ((StringBuilder) in.readObject()).reverse().toString();
this.age = in.readInt();
}
/**
* 构造器
*/
public SerializationClazz2(String name) {
log.info("SerializationClazz Construct:name={}", name);
this.name = name;
}
public SerializationClazz2(String name, Integer age) {
log.info("SerializationClazz Construct:name={},age={}", name, age);
this.name = name;
this.age = age;
}
public SerializationClazz2(String name, Integer age, int sex) {
log.info("SerializationClazz Construct:name={},age={},sex={}", name, age, sex);
this.name = name;
this.age = age;
this.sex = sex;
}
}
// Test Function
@Test
public void SerializationTest2() {
SerializationClazz2 serializationClazz = new SerializationClazz2("Serialization", 20, 1);
try {
//序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
objectOutputStream.writeObject(serializationClazz);
objectOutputStream.close();
// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
SerializationClazz2 newClazz = (SerializationClazz2) objectInputStream.readObject();
log.info("反序列化后对象:{}", newClazz);
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException exception) {
exception.printStackTrace();
}
}
- 通过 ObjectOutputStream 会调用对象的 writeObject 方法,并将要序列化的属性写入。
- 通过 InputOutputStream 会调用对象的 readObject 方法,将序列化值读取出来。需要注意的是这里需要按照写入的顺序进行读取,并且写入的规则是什么样子的,需要按写入规则反转进行读取。
- 我们没有序列化的属性,会在反序列化后填入类型的默认值。
此外,关于 readObjectNoData 方法,我们可以看一下他的使用场景。
- Serializable对象反序列化时,由于序列化与反序列化提供的类版本不同,序列化的class的super class不同于序列化时的class的super class;
- 序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。
如果发生上述场景的时候,没有定义readObjectNoData,那么就会初始化成类型的默认值;如果这时定义 readObjectNoData,那么 readObjectNoData 会替代 readObject 方法生效。
writeReplace 和 readResolve 方法
writeReplace
在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象。
/**
* Test writeReplace
*/
@Slf4j
@Data
class SerializationClazz3 implements Serializable {
// private static String staticVar = "123";
private static final Long serialVersionUID = 1L;
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private int sex;
private Object writeReplace() throws IOException {
log.info("writeReplace");
return new SerializationClazz3("writeReplace", 2, 2);
}
private void writeObject(ObjectOutputStream out) throws IOException {
log.info("writeObject");
out.writeObject(this.name);
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
log.info("readObject");
this.name = in.readObject().toString();
this.age = in.readInt();
}
/**
* 构造器
*/
public SerializationClazz3(String name) {
log.info("SerializationClazz3 Construct:name={}", name);
this.name = name;
}
public SerializationClazz3(String name, Integer age) {
log.info("SerializationClazz3 Construct:name={},age={}", name, age);
this.name = name;
this.age = age;
}
public SerializationClazz3(String name, Integer age, int sex) {
log.info("SerializationClazz3 Construct:name={},age={},sex={}", name, age, sex);
this.name = name;
this.age = age;
this.sex = sex;
}
}
// Test Func
@Test
public void SerializationTest3() {
SerializationClazz3 serializationClazz = new SerializationClazz3("Serialization", 222, 1);
try {
//序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
objectOutputStream.writeObject(serializationClazz);
objectOutputStream.close();
// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
SerializationClazz3 newClazz = (SerializationClazz3) objectInputStream.readObject();
log.info("反序列化后对象:{}", newClazz);
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException exception) {
exception.printStackTrace();
}
}
输出结果:
SerializationClazz3 - SerializationClazz3 Construct:name=Serialization,age=222,sex=1
SerializationClazz3 - writeReplace
SerializationClazz3 - SerializationClazz3 Construct:name=writeReplace,age=2,sex=2
SerializationClazz3 - writeObject
SerializationClazz3 - readObject
SerializationTest - 反序列化后对象:SerializationClazz3(name=writeReplace, age=2, sex=0)
- writeReplace先于writeObject执行
- writeReplace返回的对象可以是任意类型
readResolve
反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用。
/**
* Test readResolve
*/
@Slf4j
@Data
class SerializationClazz4 implements Serializable {
// private static String staticVar = "123";
private static final Long serialVersionUID = 1L;
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private int sex;
private Object readResolve() throws IOException {
log.info("readResolve");
return new SerializationClazz4("readResolve", 1, 1);
}
/**
* 构造器
*/
public SerializationClazz4(String name) {
log.info("SerializationClazz4 Construct:name={}", name);
this.name = name;
}
public SerializationClazz4(String name, Integer age) {
log.info("SerializationClazz4 Construct:name={},age={}", name, age);
this.name = name;
this.age = age;
}
public SerializationClazz4(String name, Integer age, int sex) {
log.info("SerializationClazz4 Construct:name={},age={},sex={}", name, age, sex);
this.name = name;
this.age = age;
this.sex = sex;
}
}
// Test Func
@Test
public void SerializationTest4() {
SerializationClazz4 serializationClazz = new SerializationClazz4("Serialization", 222, 1);
try {
//序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
objectOutputStream.writeObject(serializationClazz);
objectOutputStream.close();
// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
SerializationClazz4 newClazz = (SerializationClazz4) objectInputStream.readObject();
log.info("反序列化后对象:{}", newClazz);
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException exception) {
exception.printStackTrace();
}
}
- 执行顺序readObject 先于 readResolve执行
- readResolve常用来反序列单例类,保证单例类的唯一性。(详见)
注意:
- readResolve与writeReplace的访问修饰符可以是private、protected、public,如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。
- 通常建议对于final修饰的类重写readResolve方法没有问题;否则,重写readResolve使用private修饰。
Externalizable
Externalizable 继承自 Serializable,使用 Externalizable 接口需要实现 writeExternal 以及 readExternal 方法。
测试类:
@Slf4j
@Data
class ExternalizationClazz implements Externalizable {
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
public void writeExternal(ObjectOutput out) throws IOException {
log.info("writeExternal");
out.writeObject(this.name + "writeExternal");
out.writeInt(age);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
log.info("readExternal");
this.name = in.readObject().toString() + "readExternal";
this.age = in.readInt();
}
private Object writeReplace() throws IOException {
log.info("writeReplace");
return new ExternalizationClazz("writeReplace", 2);
}
private void writeObject(ObjectOutputStream out) throws IOException {
log.info("writeObject");
out.writeObject(this.name + "writeObject");
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
log.info("readObject");
this.name = in.readObject().toString() + "readObject";
this.age = in.readInt();
}
private Object readResolve() throws IOException {
log.info("readResolve");
return new ExternalizationClazz("readResolve", 9);
}
/**
* 构造器
*/
public ExternalizationClazz(){
}
public ExternalizationClazz(String name, Integer age) {
log.info("ExternalizationClazz Construct:name={},age={}", name, age);
this.name = name;
this.age = age;
}
}
测试方法:
@Test
public void test() {
ExternalizationClazz clazz = new ExternalizationClazz("Test", 11);
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.txt"));
objectOutputStream.writeObject(clazz);
objectOutputStream.flush();
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("out.txt"));
ExternalizationClazz newClazz = (ExternalizationClazz) objectInputStream.readObject();
objectInputStream.close();
log.info("clazz => {}", clazz);
log.info("newClazz => {}", newClazz);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
输出结果
ExternalizationClazz - ExternalizationClazz Construct:name=Test,age=11
ExternalizationClazz - writeReplace
ExternalizationClazz - ExternalizationClazz Construct:name=writeReplace,age=2
ExternalizationClazz - writeExternal
ExternalizationClazz - readExternal
ExternalizationClazz - readResolve
ExternalizationClazz - ExternalizationClazz Construct:name=readResolve,age=9
ExternalizationTest - clazz => ExternalizationClazz(name=Test, age=11)
ExternalizationTest - newClazz => ExternalizationClazz(name=readResolve, age=9)
可以看出
-
writeObject和readReadObject是Serializable接口继承后独有的方法。因此在Externalizable实现后不起作用,具体可看readOrdinaryObject源码// 如果类描述符实现了 Externalizable 接口,则调用 readExternalData 进行读取数据。 if (desc.isExternalizable()) { // 调用 readExternal 方法 readExternalData((Externalizable) obj, desc); } else { // 调用 readObject 方法 readSerialData(obj, desc); } -
Externalizable接口的实现方式一定要有默认的无参构造函数。否则会抛出no valid constructor异常。
Serializable VS Externalizable
-
Serializable 是标识接口;Externalizable 继承了Serializable 接口,实现该接口需重写 readExternal 和 writerExternal 方法。
public interface Serializable { } public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; } -
Serializable提供多种方式序列化:- 默认序列化,
transient关键字忽略某些属性序列化 readObject和writeObject方法自定义序列化
Externalizable只提供了readExternal和writeExternal方法序列化当然还有公共的都能使用的自定义序列化方法
writeReplace和readResolve方法。 - 默认序列化,
-
Serializable无需无参构造法方法;而Externalizable需要提供无参构造方法,否则将抛出no valid contructor异常 -
Externalizable不需要产生序列化ID( serialVersionUID),而Serializable需要产生序列化版本( serialVersionUID)。这与他们的序列化方式有关。 -
相比较
Serializable,Externalizable序列化、反序列化速度更快,内存更低。 -
大多数场景下推荐使用
Serializable,而且Serializable提供的默认序列方式,可以简化序列化和反序列化操作。而Externalizable适用于需要完全控制序列化和反序列化的地方,以及关注性能和效率的地方。
前言
JDK 8 里面提供了一个新的 LocalDateTime API,加强了对时间的管理。直接可以说就是为了替代 Date 使用。那么我们通过源码了解一下二者有何差异。
对比
可读性差
直接打印,不执行格式化。
Date date = new Date();
LocalDateTime localDateTime = LocalDateTime.now();
log.info("Date => {}, year={}, month={}", date, date.getYear(), date.getMonth());
log.info("LocalDateTime => {}, year={}, month={}", localDateTime, localDateTime.getYear(), localDateTime.getMonthValue());
// 输出结果
Date => Tue Oct 04 15:20:16 CST 2022, year=122, month=9
LocalDateTime => 2022-10-04T15:20:16.709, year=2022, month=10
-
Date获取出来的时间不够直观;LocalDateTime可以直观看出当前时间
-
Date获取的年份还需要再次计算(加上1900),才能得出正确年份;LocalDateTime可以直接得出当前年份
-
Date获取的月份从0计算少一个月;而LocalDateTime获取的月份是准确的。
-
最重要的是:Date里面有很多方法已经被弃用了,而LocalDateTime本身还更多的方法等
- localDateTime.getDayOfMonth()。获取当前月的第几天
- localDateTime.getDayOfWeek()。获取当前星期几
指定年月生成
// 指定年月生成对象
Date pointDate = new Date(2022, 1, 1);
LocalDateTime pointDateTime = LocalDateTime.of(2022, 1, 1, 0, 0);
log.info("pointDate => {}", pointDate);
log.info("pointDateTime => {}", pointDateTime);
可以看到 Date 连构造函数都被弃用了,
日期格式化(重点)
// 格式化输出
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("Date format =>{}", simpleDateFormat.format(date));
log.info("LocalDateTime format =>{}", localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
Date 使用的是SimpleDateFormat,而LocalDateTime 是本身提供了方法。如果仅仅只是用法上的不同,那么无伤大雅。我们来看一下两者有什么区别。
SimpleDateFormat
我们先看SimpleDateFormat的部分源码。
/**
Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized externally.*/
public class SimpleDateFormat extends DateFormat {
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* <p>Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* <code>DateFormat</code>.
* @serial
*/
protected Calendar calendar;
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
// etc
}
}
从上面的源代码中可以看出几点。
-
非线程安全的。官方说明DateForamt不是同步的。建议为每个线程创建单独的格式实例,如果多线程要使用同一个实例,则必须从外部同步它。
-
Calendar 是共享变量,没有做线程安全控制,多个线程同事调用format时,会调用 Calendar.setTime,导致多个线程执行时,会冲突覆盖 Calendar 的值,最终返回错误结果。
-
由前面两点推断出,如果说每个线程需要一个单独的SimpleDateFormat,那么将会导致创建和销毁对象,开销大。
-
对SimpleDataFormat 的format和parse方法加同步锁,会导致性能变差。
简而言之,SimpleDateForamt 是需要我们自己来保证多线程安全的。
DateTimeFormatter
既然SimpleDataFormat 是线程不安全的,那么看看 DateTimeFormatter 是怎么样的呢?
/*
* This class is immutable and thread-safe.
*
* @since 1.8
*/
public final class DateTimeFormatter {
public static DateTimeFormatter ofPattern(String pattern) {
return new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter();
}
/**
* Completes this builder by creating the formatter.
*
* @param locale the locale to use for formatting, not null
* @param chrono the chronology to use, may be null
* @return the created formatter, not null
*/
private DateTimeFormatter toFormatter(Locale locale, ResolverStyle resolverStyle, Chronology chrono) {
Objects.requireNonNull(locale, "locale");
while (active.parent != null) {
optionalEnd();
}
CompositePrinterParser pp = new CompositePrinterParser(printerParsers, false);
return new DateTimeFormatter(pp, locale, DecimalStyle.STANDARD,
resolverStyle, null, chrono, null);
}
}
- 从类定义上,可以很明显的看出,它是不可变的,而且也是线程安全的。
- 我们调用ofPattern时,最终会调用toFormatter方法,会创建一个新的对象,也就是说,不会使用缓存对象。
简而言之,对象不可变,线程安全,用就完事了。
线程不安全(重点)
其实不单单日期格式化的类是有差别的,单单Date和LocalDateTime本身我们也能更好的取舍。
public class Date
implements java.io.Serializable, Cloneable, Comparable<Date>{}
/*
* This class is immutable and thread-safe.
*/
public final class LocalDateTime{}
- Date 是可变对象,线程不安全
- LocalDateTime 是不可变对象,线程安全。
LocalDateTime使用
创建事件对象
log.info("==========创建时间对象==========");
// 获取当前时间
LocalDateTime localDateTime = LocalDateTime.now();
// 指定时间
LocalDateTime pointDateTime = LocalDateTime.of(2022, 1, 1, 0, 0);
获取年月日时分秒信息
log.info("==========获取时间信息==========");
log.info("Year : {}",localDateTime.getYear());
log.info("Month : {}",localDateTime.getMonth());
log.info("Day : {}",localDateTime.getDayOfMonth());
log.info("Hour : {}",localDateTime.getHour());
log.info("Minute : {}",localDateTime.getMinute());
log.info("Second : {}",localDateTime.getSecond());
转换类型
log.info("==========转换时间==========");
log.info("LocalDate : {}",localDateTime.toLocalDate());
log.info("LocalTime : {}",localDateTime.toLocalTime());
修改时间
PS:修改后会生成新的对象,而不影响原先的对象。
log.info("==========加减时间==========");
log.info("操作年 : {}", localDateTime.plusYears(1));
log.info("操作月 : {}", localDateTime.plusMonths(1));
log.info("操作日 : {}", localDateTime.plusDays(1));
log.info("操作时 : {}", localDateTime.plusHours(1));
log.info("操作分 : {}", localDateTime.plusMinutes(1));
log.info("操作秒 : {}", localDateTime.plusSeconds(1));
log.info("==========设置时间==========");
log.info("操作年 : {}", localDateTime.withYear(2021));
log.info("操作月 : {}", localDateTime.withMonth(1));
log.info("操作日 : {}", localDateTime.withDayOfMonth(1));
log.info("操作时 : {}", localDateTime.withHour(1));
log.info("操作分 : {}", localDateTime.withMinute(1));
log.info("操作秒 : {}", localDateTime.withSecond(1));
格式化
log.info("==========格式化==========");
log.info("BASIC_ISO_DATE : {}", localDateTime.format(DateTimeFormatter.BASIC_ISO_DATE));
log.info("ISO_DATE_TIME : {}", localDateTime.format(DateTimeFormatter.ISO_DATE_TIME));
log.info("自定义 : {}",localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
反解析
log.info("==========反解析==========");
log.info("parse : {}", localDateTime.parse("2021-01,01 11:22-02",DateTimeFormatter.ofPattern("yyyy-MM,dd HH:mm-ss")));
日期间隔(Period)
log.info("==========间隔(Period)==========");
LocalDate beforeLocalDate = LocalDate.of(2021, 1, 1);
LocalDate afterLocalDate = LocalDate.of(2022, 12, 1);
log.info("first : {}", beforeLocalDate);
log.info("sec : {}", afterLocalDate);
Period period = Period.between(beforeLocalDate, afterLocalDate);
log.info("period => {}", period);
log.info("period Year : {}", period.getYears());
log.info("period Month : {}", period.getMonths());
log.info("period Day : {}", period.getDays());
log.info("period TotalMonths : {}", period.toTotalMonths());
时间间隔(Duration)
log.info("==========时间间隔(Duration)==========");
LocalDateTime beforeLocalDateTime = LocalDateTime.of(2021, 1, 1,1,1);
LocalDateTime afterLocalDateTime = LocalDateTime.of(2022, 12, 1,1,1);
log.info("first : {}", beforeLocalDateTime);
log.info("sec : {}", afterLocalDateTime);
Duration duration = Duration.between(beforeLocalDateTime, afterLocalDateTime);
log.info("duration => {}", duration);
log.info("duration to days : {}", duration.toDays());
log.info("duration to hours : {}", duration.toHours());
log.info("duration to millis : {}", duration.toMillis());
log.info("duration seconds : {}", duration.getSeconds());
时间比较
log.info("==========时间比较==========");
log.info("first : {}", beforeLocalDateTime);
log.info("sec : {}", afterLocalDateTime);
log.info("now : {}", localDateTime);
log.info("first isAfter sec: {}", beforeLocalDateTime.isAfter(afterLocalDateTime));
log.info("sec isAfter now: {}", afterLocalDateTime.isAfter(localDateTime));
log.info("first isBefore sec: {}", beforeLocalDateTime.isBefore(afterLocalDateTime));
log.info("sec isBefore now: {}", afterLocalDateTime.isBefore(localDateTime));
总结
官方推出LocalDateTime也是为了能够替代Date使用,尽管网上有些人说性能上Date更好一点,但是LocalDateTime所带来的好处,远不是这一点性能上可以超越的。所以在使用上见仁见智,我本身也更偏向使用LocalDateTime。
前言
最近研究关于classloader,实现对模块的热插拔和类替换等功能。然后意外发现一个用于处理JAR包的官方库。
相关概念
什么是JarUrlConnetion
jarUrlConnection通过Jar协议建立一个访问jar包的连接,可以访问这个jar包内部数据
jar协议格式
jar协议的格式如:jar:{archive-url}!/{entry}。
- archive-url是文件地址
- !/是分隔符
- entry是jar包内部的目录或者文件
示例:
- 访问远程http服务器
- 整个jar包。jar:http://www.zsmile.top/test.jar!/
- jar包内的文件。jar:http://www.zsmile.top/test.jar!/Hello.class
- jar包内的目录。jar:http://www.zsmile.top/test.jar!/top/smile/
- 访问本地文件系统
- 整个jar包。jar:file://E:/test.jar!/
- jar包内的文件。jar:file://E:/test.jar!/Hello.class
- jar包内的目录。jar:file://E:/test.jar!/top/smile/
功能
获取JarFile
URL url = new URL("jar:file://E:/test.jar!/");
JarURLConnection conn = (JarURLConnection) url.openConnection();
JarFile jarfile = conn.getJarFile();
遍历JarEntry
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
System.out.println(jarEntry.getName() + " is directory :" + jarEntry.isDirectory());
}
执行class文件
// 获取class数据流
InputStream inputStream = jarFile.getInputStream(jarEntry);
int available = inputStream.available();
byte[] bytes = new byte[available];
inputStream.read(bytes);
// 借助classloader的defineClass将byte转换成class对象
Class<?> aClass = defineClass("top.zsmile.jvm.classloader.Hello", bytes, 0, bytes.length);
// 通过反射调用方法
Method method = aClass.getMethod("main", String[].class);
method.invoke(null, (Object) null);
Manifest
Manifest mainfest = jarfile.getManifest();
// 获取并遍历属性
System.out.println("manifest Attributes:");
Attributes mainAttributes = manifest.getMainAttributes();
Set<Map.Entry<Object, Object>> entries = mainAttributes.entrySet();
for (Map.Entry<Object, Object> entry : entries) {
System.out.println(entry.getKey() + "=>" + entry.getValue());
}
// 获取并遍历entry(不知道怎么用)
Map<String, Attributes> entries1 = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries1.entrySet()) {
System.out.println(entry.getKey() + "=>" + entry.getValue());
}
// 获取主函数
String mainClassName = manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
双亲委托

其实上图也就几个步骤
- 先检查类是否已经被加载过
- 若没有加载则调用父加载器的loadClass()方法进行加载
- 若父加载器为空则默认使用启动类加载器作为父加载器
- 如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
查看loadClass的方法,我们可以看到:
- 当我们遵循双亲委派机制时,自定义的classloader需要实现findClass方法。
- 当我们打破双亲委派机制,则需要实现loadClass方法
好处
- 避免java类的重复加载
- 避免用户自定义的包名类对jvm核心api破坏,保障安全。若是不使用双亲委托,用户可能会意外实现某些核心类(Object)等,导致破坏Jvm的稳定运行。
- 基于JVM标识每个类的唯一性需要与类加载器一同判断,那么我们通过自定义类的方式,可以隔离class使用,提高我们开发灵活性。
如何理解双亲委派模型的被破化?
想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。
在这之前看一下classloader 提供的3个常用方法的区别:
- loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中
- findClass() 根据名称或位置加载.class字节码
- defineClass() 把字节码转化为Class。
loadClass
双亲委托模型是在JDK1.2之后出现的。在此之前类加载器和抽象类java.lang.ClassLoader就已经存在了。所以为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),(在上面的源代码中也可以看见)。 而在双亲委托模型未设计出前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为JVM在进行类加载的时候,会调用加载器私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()方法。
private Class loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
而在JDK1.2双亲委托出现之后,不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法完成加载,在loadClass()方法父类加载失败,则会调用自己的findClass方法完成加载,这样就保证写出来的类加载器是符合双亲委托规则的。
二次破坏
二次破环是该双亲委托模弊端引起的。该模型能很好的解决各个类加载器对基础类的统一问题。因为它们总是作为被用户代码调用的API,但是问题就来了。如果基础类又要调用回用户的代码,该如何实现?。
典型例子:JNDI服务,它的代码由启动类加载器加载(在rt.jar中),但JNDI目的就是对整个程序的资源进行几种管理和查找,需要调用由每个不同独立厂商实现并且部署在应用程序的ClassPath下的JNDI接口提供者的代码。但是在应用启动时候读取rt.jar包时候,是不认识这些三方厂商定义的类的,那么如何解决?
java设计团队引入了一个新设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时候,还未设置,将会从父线程中继承一个。如果在应用程序全局范围都没有设置,默认是appClassLoader类加载器。
class Thread implements Runnable {
...
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
@CallerSensitive
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
//
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
/**
* Returns a reference to the currently executing thread object.
*
* @return the currently executing thread.
*/
public static native Thread currentThread(); //用于获取当前现在运行程序的线程
...
}
相关问题
隔离问题
大家觉得一个运行程序中有没有可能同时存在两个包名和类名完全一致的类?
JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个”类”不是由一个 ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是 ClassLoader 隔离。
java.lang.ClassCastException: android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager
当碰到这种问题时可以通过 instance.getClass().getClassLoader(); 得到 ClassLoader,看 ClassLoader 是否一样。
代码演示
准备两个jar
编写HelloWord.java文件,并编译文件,打包成HelloWord.jar
package top.zsmile.jvm.classloader;
public class HelloWord {
public static void sayHello(){
System.out.println("Hello World1");
}
public static void main(String[] args) {
sayHello();
}
}
编写HelloWord.java文件,并编译文件,打包成HelloWord-copy.jar
package top.zsmile.jvm.classloader;
public class HelloWord {
public static void sayHello(){
System.out.println("Hello World2");
}
public static void main(String[] args) {
sayHello();
}
}
注意如果填写了包名,则需要在jar包内部填写按照包路径放置class文件。
测试jar隔离
public void isolate() {
try {
final String jarPath = "file:/D:\\project\\B.Smile\\geek-study1\\project\\src\\main\\java\\top\\zsmile\\jvm\\classloader\\HelloWord.jar";
final String jarPath2 = "file:/D:\\project\\B.Smile\\geek-study1\\project\\src\\main\\java\\top\\zsmile\\jvm\\classloader\\HelloWord-copy.jar";
final String className = "top.zsmile.jvm.classloader.HelloWord";
ClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(jarPath)}, getClass().getClassLoader());
ClassLoader urlClassLoader2 = new URLClassLoader(new URL[]{new URL(jarPath2)}, getClass().getClassLoader());
System.out.println("HelloWord.jar ======" + urlClassLoader);
Class<?> aClass = Class.forName(className, true, urlClassLoader);
runMain(aClass);
// Object o = aClass.newInstance();
System.out.println();
System.out.println("HelloWord2.jar ====== " + urlClassLoader2);
Class<?> aClass2 = Class.forName(className, true, urlClassLoader2);
runMain(aClass2);
// Object o2 = aClass2.newInstance();
System.out.println();
System.out.println("judge helloWord.class equals helloWord2.class");
System.out.println(aClass.equals(aClass2));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void runMain(Class mainClass) {
try {
Method main = mainClass.getMethod("main", String[].class);
main.invoke(null, (Object) null);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
输出结果
HelloWord.jar ======java.net.URLClassLoader@4617c264
Hello World1
HelloWord2.jar ====== java.net.URLClassLoader@5305068a
Hello World2
judge helloWord.class equals helloWord2.class
false
这样我们可以看到不同classloader的class是相互隔离的,这样我们可以基于该机制,实现对class版本的管理。
加载不同jar包中公共类
现在 Host 工程包含了 common.jar, jar1.jar, jar2.jar,并且 jar1.jar 和 jar2.jar 都包含了 common.jar,我们通过 ClassLoader 将 jar1, jar2 动态加载进来,这样在 Host 中实际是存在三份 common.jar,如下图

们怎么保证 common.jar 只有一份而不会造成上面3中提到的 ClassLoader 隔离的问题呢,其实很简单,有三种方式:
- 我们只要让加载 jar1 和 jar2 的 ClassLoader 的 parent 为同一个 ClassLoader,并且该 ClassLoader 加载过 common.jar,通过上面 1 中我们知道根据双亲委托,最后都会首先被 parentClassLoader加载。
- 我们重写 jar1 和 jar2 的 ClassLoader,在 loadClass 函数中我们先去某个含有 common.jar 的 ClassLoader 中 load 即可,其实就是把上面的 parentClassLoader 换掉了而已。
- 在生成 jar1 和 jar2 时把 common.jar 去掉,只保留 host 中一份,以 host ClassLoader 为 parentClassLoader 即可。
引用
- https://www.cnblogs.com/sidesky/p/11385188.html
- https://blog.csdn.net/briblue/article/details/54973413
如何排查找不到Jar包的问题?
有时候我们会面临明明已经把某个jar加入到了环境里,可以运行的时候还是找不到。 那么我们有没有一种方法,可以直接看到各个类加载器加载了哪些jar,以及把哪些路径加到了classpath里?答案是肯定的,代码如下:
package top.zsmile.jvm.classloader;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
public class JvmClassLoaderPrintPath {
public static void main(String[] args) {
// 启动类加载器
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("启动类加载器");
for (URL url : urls) {
System.out.println(" ==> " + url.toExternalForm());
}
// 扩展类加载器
printClassLoader("扩展类加载器", JvmClassLoaderPrintPath.class.getClassLoader().getParent());
// 应用类加载器
printClassLoader("应用类加载器", JvmClassLoaderPrintPath.class.getClassLoader());
}
public static void printClassLoader(String name, ClassLoader CL) {
if (CL != null) {
System.out.println(name + " ClassLoader ‐> " + CL.toString());
printURLForClassLoader(CL);
} else {
System.out.println(name + " ClassLoader ‐> null");
}
}
public static void printURLForClassLoader(ClassLoader CL) {
Object ucp = insightField(CL, "ucp");
Object path = insightField(ucp, "path");
ArrayList ps = (ArrayList) path;
for (Object p : ps) {
System.out.println(" ==> " + p.toString());
}
}
private static Object insightField(Object obj, String fName) {
try {
Field f = null;
if (obj instanceof URLClassLoader) {
f = URLClassLoader.class.getDeclaredField(fName);
} else {
f = obj.getClass().getDeclaredField(fName);
}
f.setAccessible(true);
return f.get(obj);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
从打印结果,我们可以看到三种类加载器各自默认加载了哪些jar包和包含了哪些 classpath的路径。
如何排查类的方法不一致的问题?
假如我们确定一个jar或者class已经在classpath里了,但是却总是提示 java.lang.NoSuchMethodError ,这是怎么回事呢?很可能是加载了错误的或者重复加载了不同版本的jar包。这时候,用前面的方法就可以先排查一下,加载了具体什么jar,然后是不是不同路径下有重复的class文件,但是版本不一样。
怎么看到加载了哪些类,以及加载顺序?
还是针对上一个问题,假如有两个地方有Hello.class,一个是新版本,一个是旧的, 怎么才能直观地看到他们的加载顺序呢?也没有问题,我们可以直接打印加载的类清 单和加载顺序。
只需要在类的启动命令行参数加上 ‐XX:+TraceClassLoading 或者 ‐verbose 即 可,注意需要加载java命令之后,要执行的类名之前,不然不起作用。例如:
$ java ‐XX:+TraceClassLoading jvm.HelloClassLoader
怎么调整或修改ext和本地加载路径?
从前面的例子我们可以看到,假如什么都不设置,直接执行java命令,默认也会加载 非常多的jar包,怎么可以自定义加载哪些jar包呢?比如我的代码很简单,只加载rt.jar 行不行?答案是肯定的。
java ‐Dsun.boot.class.path="D:\Program Files\Java\jre1.8.0_231\lib\rt.jar"
其中命令行参数 ‐Dsun.boot.class.path 表示我们要指定启动类加载器加载什么, 最基础的东西都在rt.jar这个包了里,所以一般配置它就够了。 参数 ‐Djava.ext.dirs 表示扩展类加载器要加载什么,一般情况下不需要的话可以直接配置为空即可。
怎么运行期加载额外的jar包或者class呢?
简单说就是不使用命令行参数的情况下,怎么用代码来运行时改变加载类的路径和方 式。假如说,在 d:/app/jvm 路径下,有我们刚才使用过的Hello.class文件,怎么在 代码里能加载这个Hello类呢?
- 一个是前面提到的自定义ClassLoader的方式
- 还有一个就是直接在当前 的应用类加载器里,使用 URLClassLoader类的方法addURL,不过这个方法是protected的,需要反射处理一下,然后又因为程序在启动时并没有显示加载Hello类,所以在添加完了classpath以后,没法直接显式初始化,需要使用Class.forName的方式来拿到已经加载的Hello类(Class.forName("jvm.Hello")默认会初始化并执行静态代码块)
package top.zsmile.jvm.classloader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class JvmAppClassLoaderAddUrl {
public static void main(String[] args) {
String appPath = "file:/d:/app/";
URLClassLoader urlClassLoader = (URLClassLoader) JvmAppClassLoaderAddUrl.class.getClassLoader();
try {
Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addURL.setAccessible(true);
URL url = new URL(appPath);
addURL.invoke(urlClassLoader, url);
Class.forName("top.zsmile.jvm.classloader.Hello"); // 效果跟Class.forName("jvm.Hello").newInstance()一样
} catch (Exception e) {
e.printStackTrace();
}
}
}
前言
写好的代码经过编译变成了字节码,并且可以打包成Jar 文件。
然后就可以让JVM去加载需要的字节码,变成持久代/元数据区上的Class对象,接着 才会执行我们的程序逻辑。
我们可以用java命令指定主启动类,或者是Jar包,通过约定好的机制,JVM就会自动去加载对应的字节码(可能是class文件,也可能是Jar包)。
我们知道Jar包打开后实际上就等价于一个文件夹,里面有很多class文件和资源文件,但是为了方便就打包成zip格式。 当然解压了之后照样可以直接用java命令来执 行。
把Hello.class和依赖的其他文件一起打包成jar文件
示例 1: 将 class 文件和java源文件归档到一个名为hello.jar 的档案中:
jar cvf hello.jar Hello.class Hello.java
示例 2: 归档的同时,通过 e 选项指定jar的启动类 Hello :
jar cvfe hello.jar Hello Hello.class Hello.java
然后通过 ‐jar 选项来执行jar包:
$ java ‐jar hello.jar
按照Java语言规范和Java虚拟机规范的定义, 我们用 “ 类加载 (Class Loading)” 来表示: 将class/interface名称映射为Class对象的一整个过程。 这个过程还可以划分为更具体的阶段: 加载,链接和初始化(loading, linking and initializing)。
类的声明周期和加载过程

(加载,验证,准备,解析,初始化)统称为类加载,(验证、准备、解析)链接
- 加载:找 Class 文件
- 校验:验证格式、依赖。验证字节码文件格式(java版本等)
- 准备:静态字段、方法表
- 解析:符号解析为引用
- 初始化:构造器、静态变量复制、静态代码块
- 使用
- 卸载
加载
加载阶段也可以称为“装载”阶段。
- 根据明确知道的class完全限定名, 来获取二进制classfile格式的字节流,简单点说就是 找到文件系统中/jar包中/或存在于任何地方的“ class文件 ”。 如果找不到二进制表示 形式,则会抛出 NoClassDefFound 错误。
- 装载阶段并不会检查 classfile 的语法和格式。
- 类加载的整个过程主要由JVM和Java 的类加载系统共同完成, 当然具体到loading 阶 段则是由JVM与具体的某一个类加载器(java.lang.classLoader)协作完成的。
校验
链接过程的第一个阶段是 *校验 ,确保class文件里的字节流信息符合当前虚拟机的要 求,不会危害虚拟机的安全。
- 校验过程检查 classfile 的语义,判断常量池中的符号,并执行类型检查
- 主要目的是判断字节码的合法性,比如 magic number, 对版本号进行验证。
- 这些检查 过程中可能会抛出 VerifyError , ClassFormatError 或 UnsupportedClassVersionError 。
因为classfile的验证属是链接阶段的一部分,所以这个过程中可能需要加载其他类, 在某个类的加载过程中,JVM必须加载其所有的超类和接口。
- 如果类层次结构有问题(例如,该类是自己的超类或接口,死循环了),则JVM将抛出 ClassCircularityError
- 而如果实现的接口并不是一个 interface,或者声明的超类是一个 interface,也会抛出 IncompatibleClassChangeError 。
准备
这个阶段将会创建静态字段, 并将其初始化为标准默认值(比如 null 或者 0值 ),并分配方法表,即在方法区中分配这些变量所使用的内存空间。
==请注意,准备阶段并未执行任何Java代码。==
public static int i = 1;
*在准备阶段 i 的值会被初始化为0,后面在类初始化阶段才会执行赋值为1; 但是下面如果使用final作为静态常量,某些JVM的行为就不一样了:
public static final int i = 1;
对应常量i,在准备阶段就会被赋值1,其实这样还是比较puzzle,例如其他语言 (C#)有直接的常量关键字const,让告诉编译器在编译阶段就替换成常量,类似于宏指令,更简单。
解析
进入可选的解析符号引用阶段。解析常量池,主要有以下四种:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
简单的来说就是我们编写的代码中,当一个变量引用某个对象的时候,这个引用在 .class 文件中是以符号引用来存储的(相当于做了一个索引记录)。
在解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直接引用,那引用的目标必定在堆中存在。
加载一个class时,需要加载所有的super类和super接口。
初始化
==JVM规范明确规定, 必须在类的首次“主动使用”时才能执行类初始化==
初始化的过程包括执行:
- 类构造器方法
- static静态变量赋值语句
- static静态代码块
如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初 始化。所以其实在java中初始化一个类,那么必然先初始化过 java.lang.Object 类,因为所有的java类都继承自java.lang.Object。
只要我们尊重语言的语义,在执行下一步操作之前完成装载,链接和初始化这些 步骤, 如果出错就按照规定抛出相应的错误,类加载系统完全可以根据自己的策略,灵活地进行符号解析等链接过程。
为了提高性能,HotSpot JVM 通常要等到类初始化时才去装载和链接类。 因此, 如果A类引用了B类,那么加载A类并不一定会去加载B类(除非需要进行验证)。
主动对B类执行第一条指令时才会导致B类的初始化,这就需要先完成对B类的装载和链接。
类加载时机
了解了类的加载过程,我们再看看类的初始化何时会被触发呢?JVM 规范枚举了下述 多种触发情况:
- 显式调用类加载
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main方法所在的类;
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类,就是new一个类的时候要初始化;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 隐式调用类加载
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初 始化,会触发该接口的初始化;
- 使用反射 API 对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
- 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
以下几种情况不会执行类初始化:(可能会加载)
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始 化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取Class对象,不会触发类的初始化,Hello.class不会让Hello类初始化。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName(“jvm.Hello”)默认会加载Hello类。
- 通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是不初始化)。
示例:
- 诸如 Class.forName(), classLoader.loadClass() 等Java API, 反射API, 以及 JNI_FindClass 都可以启动类加载。
- JVM本身也会进行类加载。比如在JVM启动时加载核心类,java.lang.Object, java.lang.Thread 等等。
类加载器机制
类加载过程可以描述为“通过一个类的全限定名a.b.c.XXClass来获取描述此类的Class对象”,这个过程由“类加载器(ClassLoader)”来完成。
这样的好处在于,子类加载器可以复用父加载器加载的类。
系统自带的类加载器分为三种:
- 启动类加载器(BootstrapClassLoader)
- 扩展类加载器(ExtClassLoader)
- 应用类加载器(AppClassLoader)
==一般启动类加载器是由JVM内部实现的,在Java的API里无法拿到,但是我们可以侧面看到和影响它。==
后2种类加载器在Oracle Hotspot JVM里,都 是在中 --sun.misc.Launcher-- 定义的,扩展类加载器和应用类加载器一般都继承自 URLClassLoader 类,这个类也默认实现了从各种不同来源加载class字节码转换成 Class的方法。
graph BT
URLClassLoader-->ClassLoader
ExClassLoader-->URLClassLoader
AppClassLoader-->URLClassLoader
- 启动类加载器(bootstrap class loader): 它用来加载 Java 的核心类,是用原生C++代码来实现的,并不继承自java.lang.ClassLoader(负责加载JDK中jre/lib/rt.jar里所有的class)。它可以看做是JVM自带的,我们再代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它, 如果打印出来就是个 null 。举例来说,java.lang.String是由启动类加载器加载的,所以 String.class.getClassLoader()就会返回null。但是后面可以看到可以通过命令行参数影响它加载什么。
- 扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext 或者由java.ext.dirs系统属性指定的目录中的JAR包的类,代码里直接获取它的父类加载器为null(因为无法拿到启动类加载器)。可以将多个项目共有的jar包放扩展类目录下
- 应用类加载器(app class loader):它负责在JVM启动时加载来自Java命令的 classpath或者cp选项、java.class.path系统属性指定的jar包和类路径。在应用程序代码里可以通过ClassLoader的静态方法getSystemClassLoader()来获取应用 类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。
此外还可以自定义类加载器。如果用户自定义了类加载器,则自定义类加载器都以应 用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载 器是有层次关系的,启动加载器又叫根加载器,是扩展加载器的父加载器,但是直接 从ExClassLoader里拿不到它的引用,同样会返回null。
graph BT
ExtClassLoader-->BootstrapClassLoader
AppClassLoader-->ExtClassLoader
CustomClassLoader1-->AppClassLoader
CustomClassLoader2-->AppClassLoader
类加载机制有三个特点:
- 双亲委托:当一个自定义类加载器需要加载一个类,比如java.lang.String,它很懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载 器如果发现自己还有父加载器,会一直往前找,这样只要上级加载器,比如启动类加载器已经加载了某个类比如java.lang.String,所有的子加载器都不需要自己加载了。如果几个类加载器都没有加载到指定名称的类,那么会抛出ClassNotFountException异常。
- 负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
- 缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。
添加引用类的几种方式
- 放到 Jdk 目录下的 lib/ext(这是扩展类加载器的加载路径) 或者 -Djava.ext.dirs 指定一个扩展类的目录
- java -cp/classpath 或者 class文件放到当前路径(应用类加载器)
- 自定义Classloader加载
- 拿到当前执行类的classLoader,反射掉哦那个addUrl 方法添加jar或者路径(JDK9无效)
public class JvmAppClassLoaderAddUrl {
public static void main(String[] args) {
String appPath = "file:/d:/app/";
URLClassLoader urlClassLoader = (URLClassLoader) JvmAppClassLoaderAddUrl.class.getClassLoader();
try {
Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addURL.setAccessible(true);
URL url = new URL(appPath);
addURL.invoke(urlClassLoader, url);
Class.forName("top.zsmile.jvm.classloader.Hello"); // 效果跟Class.forName("jvm.Hello").newInstance()一样
} catch (Exception e) {
e.printStackTrace();
}
}
}
jdk9以后应用类加载器、扩展类加载器和URL加载器,这三个是平级的了。不能存在一个继承关系。所以无法将应用类加载器和扩展类加载器转换成URLClassLoader。
但是呢提供了另一种方式,将上面繁杂的代码变得更简单了。
class.forName("top.zsmile.jvm.classloader.Hello")",new UrlClassLoader());
在JDK9中,整个JDK都基于模块化进行构建,以前的rt.jar, tool.jar被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。
打印三类加载器的信息
public class JvmClassLoaderPrintPath {
public static void main(String[] args) {
// 启动类加载器
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("启动类加载器");
for (URL url : urls) {
System.out.println(" ==> " + url.toExternalForm());
}
// 扩展类加载器
printClassLoader("扩展类加载器", JvmClassLoaderPrintPath.class.getClassLoader().getParent());
// 应用类加载器
printClassLoader("应用类加载器", JvmClassLoaderPrintPath.class.getClassLoader());
}
public static void printClassLoader(String name, ClassLoader CL) {
if (CL != null) {
System.out.println(name + " ClassLoader ‐> " + CL.toString());
printURLForClassLoader(CL);
} else {
System.out.println(name + " ClassLoader ‐> null");
}
}
public static void printURLForClassLoader(ClassLoader CL) {
Object ucp = insightField(CL, "ucp");
Object path = insightField(ucp, "path");
ArrayList ps = (ArrayList) path;
for (Object p : ps) {
System.out.println(" ==> " + p.toString());
}
}
private static Object insightField(Object obj, String fName) {
try {
Field f = null;
if (obj instanceof URLClassLoader) {
f = URLClassLoader.class.getDeclaredField(fName);
} else {
f = obj.getClass().getDeclaredField(fName);
}
f.setAccessible(true);
return f.get(obj);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
其中启动类加载器,只能在oraclejdk和 openjdk中获取,其他版本jdk可能不提供这样的接口。
自定义类加载示例
同时我们可以自行实现类加载器来加载其他格式的类,对加载方式、加载数据的格式进行自定义处理,只要能通过classloader返回一个Class实例即可。这就大大增强了加 载器灵活性。
比如我们试着实现一个可以用来处理简单加密的字节码的类加载器,用 来保护我们的class字节码文件不被使用者直接拿来破解。
我们先来看看我们希望加载的一个Hello类:
package jvm;
public class Hello {
static {
System.out.println("Hello Class Initialized!");
}
}
假设这个类的内容非常重要,我们不想把编译到得到的Hello.class给别人,但是我们还是想别人可以调用或执行这个类,应该怎么办呢?一个简单的思路是,我们把这个类的class文件二进制作为字节流先加密一下,然后尝试通过自定义的类加载器来加载加密后的数据。
package jvm;
import java.util.Base64;
public class HelloClassLoader extends ClassLoader {
public static void main(String[] args) {
try {
new HelloClassLoader().findClass("jvm.Hello").newInstance(); // 加载并初始化Hello类
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String helloBase64 = "yv66vgAAADQAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2N"+ "hbFZhcmlhYmxlVGFibGUBAAR0aGlzAQALTGp2bS9IZWxsbzsBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAHAAgHABkMABoAGwEAGEhlb" + "GxvIENsYXNzIEluaXRpYWxpemVkIQcAHAwAHQAeAQAJanZtL0hlbGxvAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2" + "YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgAAAAAAAgABAAcACA" + "ABAAkAAAAvAAEAAQAAAAUqtwABsQAAAAIACgAAAAYAAQAAAAMACwAAAAwAAQAAAAUADAANAAAACAAOAAgAAQAJAAAAJQACAAAAAAAJsgACEgO2AASxAAAAAQAK" + "AAAACgACAAAABgAIAAcAAQAPAAAAAgAQ";
byte[] bytes = decode(helloBase64);
return defineClass(name,bytes,0,bytes.length);
}
public byte[] decode(String base64){
return Base64.getDecoder().decode(base64);
}
}
$ java jvm.HelloClassLoader
Hello Class Initialized!
==需要说明的是两个没有关系的自定义类加载器之间加载的类是不共享的(只共享父类加载器,兄弟之间不共享)==,这样就可以实现不同的类型沙箱 的隔离性,我们可以用多个类加载器,各自加载同一个类的不同版本,大家可以相互之间不影响彼此,从而在这个基础上可以实现类的动态加载卸载,热插拔的插件机制等,具体信息大家可以参考OSGi等模块化技术。
sun.misc.launcher
java虚拟机的入口
Launcher初始化
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
从这段Launcher构造器的代码中,我们可以获得一下信息:
- Launcher初始化了ExtClassLoader 和 AppClassloader,并将ExtClassLoader作为AppClassloader的父加载器(注意这里不是继承关系的)
- 这里可以看到的是,并没有显式的将BootstrapClassloader作为ExtClassloader的父加载器。但是我们可以从classloader的loadClass方法中看到,会调用native方法,实际就是从BootstrapClassloader获取(因为BootstrapClassloader实际是C++实现,并没有继承Classloader的)
- 将线程的默认类加载器设置为AppClassloader
- 这里没有初始化BootstrapClassloader,但可以有这样的代码 System.getProperty("sun.boot.class.path") 来加载一些包。
输出一下System.getProperty("sum.boot.class.path")的内容,我们可以看到就bootstrapClassloader的内容。也可以通过sun.misc.Launcher.getBootstrapClassPath()获取路径信息
==> file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/resources.jar
==> file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/rt.jar
==> file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/sunrsasign.jar
==> file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jsse.jar
==> file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jce.jar
==> file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/charsets.jar
==> file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/lib/jfr.jar
==> file:/C:/Program%20Files/Java/jdk1.8.0_201/jre/classes
这里还有初始化一个BootClassPathHolder
private static class BootClassPathHolder {
static final URLClassPath bcp;
private BootClassPathHolder() {
}
static {
URL[] var0;
if (Launcher.bootClassPath != null) {
var0 = (URL[])AccessController.doPrivileged(new PrivilegedAction<URL[]>() {
public URL[] run() {
File[] var1 = Launcher.getClassPath(Launcher.bootClassPath);
int var2 = var1.length;
HashSet var3 = new HashSet();
for(int var4 = 0; var4 < var2; ++var4) {
File var5 = var1[var4];
if (!var5.isDirectory()) {
var5 = var5.getParentFile();
}
if (var5 != null && var3.add(var5)) {
MetaIndex.registerDirectory(var5);
}
}
return Launcher.pathToURLs(var1);
}
});
} else {
var0 = new URL[0];
}
bcp = new URLClassPath(var0, Launcher.factory, (AccessControlContext)null);
bcp.initLookupCache((ClassLoader)null);
}
}
下面是我的分析:
- 当BootClassPath(即System.getProperty("sum.boot.class.path"))时,执行器读取这些文件并加载成File[]
- 遍历file[],判断是否为路径,如果不是路径,则获取上层路径(D:/test/1.jar => D:/test)
- 当不为null时,将其添加到set(用于去重)并判断是否添加成功。如果添加成功,则进入到MetaIndex.registerDirectory将路径传输进去,读取目录下meta-index(用来提供jvm启动的加载速度),并添加到jarMap里面。
- initLookupCache这个方法将查找到的classPath缓存到jvm中。其中null对应到的是bootstrapClassloader。这里后面有时间可以深入一下jvm的源码。
ExtClassloader
这里提取了部分代码
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
return new Launcher.ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
private static URL[] getExtURLs(File[] var0) throws IOException {
Vector var1 = new Vector();
for(int var2 = 0; var2 < var0.length; ++var2) {
String[] var3 = var0[var2].list();
if (var3 != null) {
for(int var4 = 0; var4 < var3.length; ++var4) {
if (!var3[var4].equals("meta-index")) {
File var5 = new File(var0[var2], var3[var4]);
var1.add(Launcher.getFileURL(var5));
}
}
}
}
URL[] var6 = new URL[var1.size()];
var1.copyInto(var6);
return var6;
}
这里可以看出几点:
- ExtClassloader 的父加载器为null
- 加载路径为java.ext.dirs
- 初始化方式与BootClassPathHolder有相似的地方,都采用了meta-index记载
- ExtClassloader 继承了 URLClassloader
AppClassLoader
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}
这里可以看出:
- AppClassloader 加载路径为java.class.path
- 重载了一下loadClass方法
- 校验class文件
- 查找类是否存在
前言
==Jvm是一个完整的计算机模型,所以自然就需要有对应的内存模型,这个模型被称为 “ Java内存模型 ”,对应的英文是“ Java Memory Model ”,简称 JMM 。==
Java内存模型规定了JVM应该如何使用计算机内存(RAM)。
广义来讲, Java内存模型分为两个部分:
Jvm内存结构- JMM与线程规范
其中,JVM内存结构是底层实现,也是我们理解和认识 JMM 的基础。 大家熟知的堆内存、栈内存等运行时数据区的划分就可以归为JVM内存结构。
JVM内存结构
JVM内部使用的Java内存模型, 在逻辑上将内存划分为 线程栈 (thread stacks) 和 **堆内存 (heap)**两个部分。

==JVM中,每个正在运行的线程,都有自己的线程栈。 线程栈包含了当前正在执行的方法链/调用链上的所有方法的状态信息。==
线程栈又被称为“ 方法栈 ”或“ 调用栈 ”(call stack)。
线程栈里面保存了调用链上正在执行的所有方法中的局部变量。
- 每个线程都只能访问自己的线程栈。
- 每个线程都不能访问(看不见)其他线程的局部变量
即使两个线程正在执行完全相同的代码,但每个线程都会在自己的线程栈内创建对应代码中声明的局部变量。 所以每个线程都有一份自己的局部变量副本。
- 所有原生类型的局部变量都存储在线程栈中,因此对其他线程是不可见的。
- 线程可以将一个原生变量值的副本传给另一个线程,但不能共享原生局部变量本身。
- 堆内存中包含了Java代码中创建的所有对象,不管是哪个线程创建的。 其中也涵 盖了包装类型(例如 Byte , Integer , Long 等)。
- 不管是创建一个对象并将其赋值给局部变量, 还是赋值给另一个对象的成员变量,创建的对象都会被保存到堆内存中。
下图演示了线程栈上的调用栈和局部变量,以及存储在堆内存中的对象:

- 如果是原生数据类型的局部变量,那么它的内容就全部保留在线程栈上。
- 如果是对象引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。
- 对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生数值, 还是对象引用。
- 类的静态变量则和类定义一样都保存在堆中
栈内存的结构

每启动一个线程,JVM就会在栈空间栈分配对应的 线程栈, 比如 1MB 的空间( ‐Xss1m )。
java虚拟机提供了参数 -Xss来指定线程的最大空间,这个参数也直接决定了方法调用的最大深度。
线程栈也叫做Java方法栈。如果使用了JNI方法,则会分配一个单独的本地方法栈 (Native Stack)
线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如A调用B,B调用C。每执行到一个方法,就会创建对应的 栈帧 (Frame)
栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本上就能确定。
比如 返回值 需要有一个空间存放吧,每个 局部变量 都需要对应的地址空间,此外 还有给指令使用的 操作数栈 ,以及class指针(标识这个栈帧对应的是哪个类的方法, 指向非堆里面的Class对象)
小结
==方法中使用的原生数据类型和对象引用地址在栈上存储;对象、对象成员 与类定义、静态变量在堆上。==
**堆内存又称为“ 共享堆 ”,**堆中的所有对象,可以被所有线程访问,只要他们能拿到对象的引用地址。
- 如果一个线程可以访问某个对象时,也就可以访问该对象的成员变量。
- 如果两个线程同时调用某个对象的同一方法,则它们都可以访问到这个对象的成员变量,但每个线程的局部变量副本是独立的
虽然各个线程自己使用的局部变量都在自己的栈上,但是大家可以共享堆上的对象,特别地各个不同线程访问同一个对象实例的基础类型的成员变量,会给每 个线程一个变量的副本
堆内存的结构
堆内存是所有线程共用的内存空间,理论上大家都可以访问里面的内容。
但JVM的具体实现一般会有各种优化。比如将逻辑上的Java堆,划分为 堆(Heap)和 非堆(Non‐Heap) 两个部分。这种划分的依据在于,==我们编写的Java代码,基本上只能使用Heap这部分空间,发生内存分配和回收的主要区域也在这部分,所以有一种说法,这里的Heap也叫GC管理的堆(GC Heap)。==
堆
GC理论中有一个重要的思想,叫做分代。 经过研究发现,程序中分配的对象,要么 用过就扔,要么就能存活很久很久。
因此,JVM将Heap内存分为年轻代(Young generation)和老年代(Old generation, 也叫 Tenured)两部分。
年轻代还划分为3个内存池,新生代(Eden space)和存活区(Survivor space), 在大部分 GC算法中有2个存活区(S0,S1),在我们可以观察到的任何时刻,S0和S1总有一个是空的,但一般较小,也不浪费多少空间
==具体实现对新生代还有优化,那就是TLAB(Thread Local Allocation Buffer), 给每个线程先划定一小片空间,你创建的对象先在这里分配,满了再换。这能极大降低并发资 源锁定的开销。==
非堆
java 官方工具 jconsole 可以看到这样的定义。,
NonHeap本质上还是Heap,只是一般不归GC管理,里面划分为3个内存池。
- 元数据区,Metaspace, 以前叫持久代(永久代, Permanent generation), Java8换了个名字叫 Metaspace.
- Java8将方法区移动到了Meta区里面,而方法又是class的一部分。。。和CCS交叉
- CCS, Compressed Class Space,存放class信息的,和 Metaspace 有交叉。
- Code Cache, 存放 JIT编译器编译后的本地机器代码。
JMM
背景
JMM 规范对应的是 JSR-133 《Java 语言规范》 的 Memory Model 章节
JMM规范明确定义了不同的线程之间,通过哪些方式,在什么时候可以看见其他线程保存到共享变量中的值;以及在必要时,如何对共享变量的访问进行同步。这样的好处是屏蔽各种硬件平台和操作系统之间的内存访问差异,实现了Java并发程序真正的跨平台。
简介
JMM定义了一些术语和规定:
-
被多个线程共享使用的内存称之为**“共享内存”或"堆内存"**
-
所有的对象(包括内部的实例成员变量),static变量,以及数组,都必须存放到堆内存中。
-
局部变量,方法的形参/入参,异常处理语句的入参不允许在线程之间共享,所以不受到内存模型的影响。
-
多个线程同时对一个变量执行读取/写入时,这时候只要有某个线程执行的时写操作,那么就会引起 “冲突”
-
可以被其他线程影响或感知的操作,成为线程间的交互行为,可分为:读取、写入、同步、外部操作等。
- 同步操作包括:
- 对 volatile变量的读写
- 对管程(monitor) 的锁定与解锁
- 线程的起始操作和结尾操作
- 外部操作包括
- 对线程执行环境之外的操作,比如停止其他线程。
- 同步操作包括:
JMM 规范的是线程间的交互操作,而不管线程内对局部变量进行的操作。
内存屏障
背景(CPU指令与乱序执行)
计算机按支持的指令大致可以分为两类:
- 精简指令集计算机(RISC) , 代表是如今大家熟知的 ARM 芯片,功耗低,运算能力相对较弱。
- 复杂指令集计算机(CISC) , 代表作是Intel的X86芯片系列,比如奔腾,酷睿,至强,以及AMD的CPU。特点是性能强劲,功耗高。(实际上从奔腾4架构开始,对外是复杂指令集,内部实现则是精简指令集,所以主频才能大幅度提高)
写过程序的人都知道,同样的计算,可以有不同的实现方式。 硬件指令设计同样如此,比如说我们的系统需要实现某种功能,那么复杂点的办法就是在CPU中封装一个逻辑运算单元来实现这种的运算,对外暴露一个专用指令。
当然也可以偷懒,不实现这个指令,而是由程序编译器想办法用原有的那些基础的,通用指令来模拟和拼凑出这个功能。那么随着时间的推移,实现专用指令的CPU指令集就会越来越复杂,被称为复杂指令集。 而偷懒的CPU指令集相对来说就会少很多,甚至砍掉了很多指令,所以叫精简指令集计算机。
不管哪一种指令集,CPU的实现都是采用流水线的方式。如果CPU一条指令一条指令地执行,那么很多流水线实际上是闲置的。简单理解,可以类比一个KFC的取餐窗口就是一条流水线。于是硬件设计人员就想出了一个好办法: “ 指令乱序 ”。
CPU完全可以根据需要,通过内部调度把这些指令打乱了执行,充分利用流水线资源,只要最终结果是等价的,那么程序的正确性就没有问题。但这在如今多CPU内核的时代,随着复杂度的提升,并发执行的程序面临了很多问题。

CPU是多个核心一起执行,同时JVM中还有多个线程在并发执行,这种多对多让局面变得异常复杂,稍微控制不好,程序的执行结果可能就是错误的。
作用
前面提到了CPU会在合适的时机,按需要对将要进行的操作重新排序,但是有时候这个重排机会导致我们的代码跟预期不一致。
内存屏障可分为 读屏障 和 写屏障 ,用于控制可见性。 常见的 内存屏障 包括:
- #LoadLoad
- #StoreStore
- #LoadStore
- #StoreLoad
==这些屏障的主要目的,是用来短暂屏蔽CPU的指令重排序功能。==
和CPU约定好,看见这些指令时,就要保证这个指令前后的相应操作不会被打乱。
- 比如看见 #LoadLoad ,那么屏障前面的Load指令就一定要先执行完,才能执行屏障后面的Load指令。
- 比如我要先把a值写到A字段中,然后再将b值写到B字段对应的内存地址。如果要严格保障这个顺序,那么就可以在这两个Store指令之间加入一个 #StoreStore 屏障。
- 遇到 #LoadStore 屏障时,CPU自废武功,短暂屏蔽掉指令重排序功能。
- #StoreLoad 屏障, 能确保屏障之前执行的所有store操作,都对其他处理器可 见; 在屏障后面执行的load指令,都能取得到最新的值。换句话说,有效阻止屏障之前的store指令,与屏障之后的load指令乱序、即使是多核心处理器,在执行这些操作时的顺序也是一致的
==代价最高的是 #StoreLoad 屏障, 它同时具有其他几类屏障的效果,可以用来代替另外三种内存屏障。==
如何理解呢?
就是只要有一个CPU内核收到这类指令,就会做一些操作,同时发出一条广播, 给某个内存地址打个标记,其他CPU内核与自己的缓存交互时,就知道这个缓存不是最新的,需要从主内存重新进行加载处理。
总结
- JVM的内存区域分为: 堆内存 和 栈内存 ;
- 栈内存
- 堆内存的实现可分为两部分: 堆(Heap) 和 非堆(Non‐Heap) ;
- 堆主要由GC负责管理,按分代的方式一般分为: 老年代+年轻代;年轻代=新生代 +存活区;
- CPU有一个性能提升的利器: 指令重排序 ;
- JMM规范对应的是 JSR133, 现在由Java语言规范和JVM规范来维护;
- 内存屏障的分类与作用。

前言
栈上分配是java虚拟机提供的一项优化技术,基本思想是,对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以将它们分配在栈上,而不是分配在堆上。
好处是可以在函数调用结束后自行销毁,不需要垃圾回收器的介入,从而提高性能。
逃逸分析
栈上分配的技术基础是 逃逸分析。逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。
下面就是一个逃逸对象。因为 静态变量u有可能被任何线程访问。
private static User u;
public static void alloc(){
u = new User();
u.id = 1;
u.name = "test";
}
下面就是一个非逃逸对象。因为 变量u 以局部变量存在,并且没有做公开/返回等操作。
public static void alloc(){
User u = new User();
u.id = 1;
u.name = "test";
}
对于这种情况,虚拟机就有可能将User分配到栈上,而不在堆上。
测试
测试代码:
public class OnStackTest {
public static class User {
public int id = 0;
public String name = "";
}
public static void alloc() {
User u = new User();
u.id = 1;
u.name = "test";
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e - b);
}
}
该代码执行了1一次alloc()方法创建对象,由于User对象需要占用16字节的空间,因此累计分配空间将达到1.5G。如果堆空间小于这个值,就必然发生GC。使用参数调试一下。
-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UserTLAB -XX:+EliminateAllocations
单独标签的作用可以查看启动参数说明。
这里程序执行后,会没有任何形式的GC输出。说明在执行过程中,分配过程被优化。
如果关闭逃逸分析或者标量替换中任何一个,再次执行程序,会输出大量的GC日志,说明栈上分配依赖逃逸分析和标量替换的实现。
总结
对于大量的零散小对象,栈上分配提供了一种很好的对象分配优化策略,栈上分配速度快,并可以有效避免垃圾回收带来的负面影响(STW),但由于栈空间较小,因此对于大对象也不适合在栈上分配
概述
- 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
- 直接内存是在Java堆外的、直接向系统申请的内存区间。
- JVM通过NIO进行通信,使用DirectByteBuffer进行操作,用于数据缓冲区。因为可以避免 Java 堆和 Native 堆之间来回复制数据,在一些场景可以带来显著的性能提高。
- Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
- 虚拟机参数设置:-DirectMemorySize可以设置直接内存大小,默认与堆内存的最大值-Xmx参数值一致。
- 可能导致OutOfMemoryError异常。 在配置 JVM 参数时,会根据机器的实际内存设置
-Xmx等信息,但往往会忽略直接内存(默认为-Xmx),这可能使得各个内存区域的综合大于物理内存限制,导致扩容时出现OOM。
缺点
- 不受JVM内存回收管理,需手动回收
- 分配与回收回收成本高(属于操作系统内存)
- 不好控制,频繁操作,容易内存溢出。
- 不适合储存复杂对象。
优点
- 一般情况下访问直接内存的速度会比JVM堆快。即读写性能高。
- 读写频繁的场合,使用直接内存更能提高性能。
- 大内存更适合使用。
- 因为不受jvm控制,可以减少stw时间。
差别说明
非直接缓冲区(堆内存)
- 当我们发起一次文件读取操作时,与操作系统交互,需要由用户态切换内核态,这时需要拷贝到内核态指定的内存空间,再将数据拷贝到jvm的堆内存。
- 需要两份内存空间。

直接缓冲区(直接内存)
-
本质是减少内存之间的拷贝次数和共用一份内存空间。
-
通过directByteBuffer操作,申请到一个直接内存地址。这个直接内存地址,实际是直接在内核态进行设备间的内存操作,而不必拷贝到jvm内存中。
-
当我们发起一次文件读操作时,通过directByteBuffer获取到一个直接内存空间,并把文件加载到这篇空间里面,这里就不用再进行内存拷贝了。
-
简单理解,直接内存可以看作是一片内核态和用户态公用的,属于内核态的内存空间。

内存溢出
DirectByteBuffer
public class DirectMemoryOomDemo {
static int _100Mb = 1024*1024*100;
public static void main(String[] args) {
List<ByteBuffer> byteBuffers = new ArrayList<>();
int i=0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
byteBuffers.add(byteBuffer);
i++;
}
}finally {
System.out.println(i);
}
}
}
HeapByteBuffer
public class HeapMemoryOomDemo {
static int _100Mb = 1024*1024*100;
public static void main(String[] args) {
List<ByteBuffer> byteBuffers = new ArrayList<>();
int i=0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocate(_100Mb);
byteBuffers.add(byteBuffer);
i++;
}
}finally {
System.out.println(i);
}
}
}
分配与回收
速度比较
小对象大数量
/**
* 比较DirectByteBuffer和HeapByteBuffer
*/
public class CompareDemo {
private static final int capacity = 1024;
private static final int len = 1024 * 1000;
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1000);
System.gc();
createDirectMemory();
Thread.sleep(1000);
System.gc();
Thread.sleep(1000);
createHeapMemory();
}
public static void createDirectMemory() {
long start = System.currentTimeMillis();
ByteBuffer[] byteBuffers = new ByteBuffer[len];
for (int i = 0; i < len; i++) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(capacity);
byteBuffers[i] = byteBuffer;
}
long end = System.currentTimeMillis();
System.out.println("DirectByteBuffer创建完成,耗时:" + (end - start));
}
public static void createHeapMemory() {
long start = System.currentTimeMillis();
ByteBuffer[] byteBuffers = new ByteBuffer[len];
for (int i = 0; i < len; i++) {
ByteBuffer byteBuffer = ByteBuffer.allocate(capacity);
byteBuffers[i] = byteBuffer;
}
long end = System.currentTimeMillis();
System.out.println("HeapByteBuffer创建完成,耗时:" + (end - start));
}
}
这里设置了大小为1k,数量为1024000。我们运行一下查看结果。
DirectByteBuffer创建完成,耗时:1726
HeapByteBuffer创建完成,耗时:1304
可以看到DirectByteBuffer在数量越多的情况下性能更差。
大对象小数量
private static final int capacity = 1024*1024*100;
private static final int len = 10;
这里延用前面的代码,并设置了大小为100M,数量为10。我们运行一下查看结果。
DirectByteBuffer创建完成,耗时:174
HeapByteBuffer创建完成,耗时:262
可以看到这种情况下DirectByteBuffer性能更具优势。
结论
- DirectByteBuffer不适用于频繁申请内存的场景,当数量过多,性能将直线下降,不如使用堆内存。
- DirectByteBuffer适用于大内存申请使用的情况,且服用内存空间,这样对运行性能更加友好。
System.gc
public class DirectMemoryGcDemo {
private static int _500MB = 1024 * 1024 * 500;
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
System.out.println("开始分配");
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_500MB);
long endtime = System.currentTimeMillis();
System.out.println("分配完成,耗时:" + (endtime - start));
System.in.read();
byteBuffer = null;
System.gc();
System.out.println("执行System.gc,程序未完全退出");
System.in.read();
System.out.println("程序退出");
}
}
执行程序后,分配完直接内存,会暂停,我们看一下任务管理器的情况。这里可以看到分配了一个将近500M的内存。
然后我们继续执行一下程序。
可以看见500M的java进程已经不见了。证明已经被我们System.gc()回收了。
unsafe内存管理
==通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。==
另外ByteBuffer,
public class DirectmemoryUnsafeDemo {
private final static int _100MB = 1024 * 1024 * 500;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
long base = unsafe.allocateMemory(_100MB);
unsafe.setMemory(base, _100MB, (byte) 0);
System.out.println("分配内存成功");
System.in.read();
unsafe.freeMemory(base);
System.out.println("释放内存");
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
}
ByteBuffer.cleaner()
public class DirectMemoryFreeDemo {
private static int _500MB = 1024 * 1024 * 500;
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
System.out.println("开始分配");
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_500MB);
long endtime = System.currentTimeMillis();
System.out.println("分配完成,耗时:" + (endtime - start));
System.in.read();
if (byteBuffer.isDirect()) {
System.out.println("byteBuffer是直接内存");
((DirectBuffer) byteBuffer).cleaner().clean();
}
System.out.println("执行Cleaner,程序未完全退出");
System.in.read();
System.out.println("程序退出");
}
}
使用场景
适用DirectByteBuffer(直接内存)的情况
ByteBuffer.allocateDirect(int capacity)
- 频繁的native IO,即java进程与本地磁盘,socket传输
- 不需要经常创建和销毁DirectByteBuffer对象(执行分配和销毁代价大)(使用池思想,复用DirectByteBuffer)
- DirectByteBuffer不会占用堆内存,也就是不受堆内存大小限制(受系统内存限制),只有在DirectByteBuffer回收时才释放缓冲区。
- 大文件造成大对象,对GC负担比较重的情况。
- 为了避免一直没有FULL GC,最终导致物理内存被耗完。我们会指定直接内存的最大值,通过-XX:MaxDirectMemerySize来指定,当达到阈值的时候,调用system.gc来进行一次full gc,把那些没有被使用的直接内存回收掉。
适用HeapByteBuffer(堆内存)的情况
ByteBuffer.allocate(int capacity)
- 数据仅在java进程中传输使用,不进行本地io操作。
- 体积小,对GC负担低,能够快速回收。
-
-Xmx
-
-Xms
-
-Xss。
- 线程栈空间大小。
- 实例:-Xss1m。表示分配1m空间给每个线程使用。
-
-server
-
-XX:+DoExcapeAnalysis
-
逃逸分析。需在-server 模式下使用。
-
-XX:+DoExcapeAnalysis启用,-XX:-DoExcapeAnalysis禁用。默认禁用
-
-
-XX:+PrintGC
- 打印GC日志
-
-XX:+EliminateAllocations
- 标量替换(默认打开)。作用是允许对象打散分配在栈上。
- 比如对象拥有id和name两个字段,那么这两个字段会被视为两个独立的局部变量进行分配。
-
-XX:-UserTLAB(关闭TLAB)
- TLAB(Thread Local Allocation Buffer),线程本地分配缓冲区。
前言
jvm启动参数分为三类:
- 以 - 开头的为标准参数,所有的jvm都要实现这些参数,并向后兼容。(-D设置系统属性)
- 以 -X 开头的为非标准参数,基本都是传给JVM的,默认JVM实现这些参数的功能,但是不保证所有JVM实现都满足,且不保证向后兼容。可以使用java -X 查看当前jvm支持的非标准参数。(-Xmx8g 设置堆内存8g)
- 以 -XX 开头的为非稳定参数,专门用于控制JVM的行为,跟具体的JVM实现有关,随时可能在下一个版本取消。
- -XX:+-Flag形式,+-是对布尔值进行开关
- -XX:key=value形式,指定某个选项的值
特点和作用
下面按照一些特点和作用来分类
- 系统属性参数
- 运行模式参数
- 堆内存设置参数
- GC 设置参数
- 分析诊断参数
- javaAgent 参数
系统属性参数
系统属性参数主要是给我们系统提供一些环境变量和传递传递系统内需要使用的一些开关或者数值。
环境变量就是配置在系统变量里比如path等信息里面,用于影响所有启动项目的。
常用参数
- -Dfile.encoding=UTF-8。设置编码类型
- -Duser.timezone=GMT+08。设置时区
- -Dmaven.test.skip=ture。设置mavne跳过测试
- -Dio.netty.eventLoopThreads=8。设置netty线程数为8
自定义参数
java -Da=100 -jar xzxx.jar
等价于,在系统里执行下面代码
System.setProperty("a","100");
// 获取代码
String a = System.getProperty("a");
linux下,还可以这样启动
a=100 java -jar xzxx.jar
运行模式
Jvm运行时,采取的运行模式。
常用模式
- -server。设置jvm使用server模式,特点:
- 启动速度慢,但运行时性能和内存管理效率很高,适用生产环境
- 在具有64位能力的JDK环境将默认启用该模式,而忽略-client参数
- -client。设置jvm使用client模式,特点:
- 在jdk1.7之前在32位的x86机器上的默认值是-client选项。
- 启动速度比较快,但运行时性能和内存管理效率不高,通常用与客户端应用程序或pc应用开发和调试。
- -Xint。在解释模式(interpreted mode)下运行,特点是:
- -Xint标记会强制JVM解释执行所有的字节码
- 通常运行速度偏低,一般低10倍或者更多。
- -Xcomp。与Xint相反,是编译模式,特点是:
- JVM会在第一次使用时把所有的字节码编译成本地代码,从而带来最大程度的优化
- 注意预热的问题
- -Xmixed。混合模式,将解释模式和编译模式混合使用,特点是:
- 由JVM决定,这是jvm的默认模式,也是推荐模式
- 使用java -version可以看到mixed mode等信息
堆内存
配置堆内存分配情况
常用
- -Xmx。指定最大堆内存。如 -Xmx4g。这只是限制了 Heap 部分的最大值为4g。这个内存不包括栈内存,也不包括堆外使用的内存
- -Xms, 指定堆内存空间的初始大小。 如 -Xms4g。 而且指定的内存大小,并不是操作系统实际分配的初始值,而是GC先规划好,用到才分配。专用服务器上需要保持-Xms和-Xmx一致,否则启动后会有几次FullGC产生。当两者不一致时,堆内存扩容可能会导致性能抖动。
- -Xmn, 等价于 -XX:NewSize,使用 G1 垃圾收集器 不应该设置该选项,在其他的某些业务场景下可以设置。==官方建议设置为 -Xmx 的 1/2 ~ 1/4。==。默认Young区和Old区是1:2,所以是1/3.
- -XX:MaxPermSize=size, 这是 JDK1.7 之前使用的。Java8 默认允许的 Meta空间无限大,此参数无效。
- -XX:MaxMetaspaceSize=size, Java8 默认不限制 Meta 空间,一般不允许设置该选项。
- -XX:MaxDirectMemorySize=size,系统可以使用的最大堆外内存,这个参数跟 -Dsun.nio.MaxDirectMemorySize 效果相同。
- -Xss, 设置每个线程栈的字节数,影响栈的深度。 例如 -Xss1m 指定线程栈为1MB,与-XX:ThreadStackSize=1m 等价。 默认是1M
性能优化注意点
- 专用服务器上需要保持 –Xms 和 –Xmx 一致。否则可能导致:
- 应用刚启动可能就有好几个 FullGC。当两者配置不一致时,堆内存扩容可能会导致性能抖动。
- 当内存还没到达 -Xmx配置的最大内存时,系统内存已经不够用了,然后溢出的问题。
- -Xmn/-XX:NewSize。官方建阳设置为 -Xmx 的 1/2 ~ 1/4
- 如果我们发现某些线程的调用栈发生了内存溢出,基本就是调用深度过深,比如死循环、2个方法来回调用,最终导致使用的总内存超出了Xss的限制(1M),就会溢出了。
- 尽量不要在线上做java应用程序的混合部署。每个应用都放在虚拟机或者docker里面,做到资源隔离,这样进程之间不会抢内存资源。
- -Xmx在一般情况下可以配置内存的60%-80%,一定要留20-30%的预留空间,需要预留给堆外内存以及其它应用使用。
GC相关
常用
- -XX:+UseG1GC:使用 G1 垃圾回收器
- -XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器
- -XX:+UseSerialGC:使用串行垃圾回收器
- -XX:+UseParallelGC:使用并行垃圾回收器
- -XX:+UnlockExperimentalVMOptions -XX:+UseZGC。Java 11+。
- -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC。Java 12+
各版本默认GC
- java7 - Parallel GC
- java8 - Parallel GC
- java9 - G1 GC
- java10 - G1 GC
- java11 - ZGC
分析诊断
常用
- -XX:+-HeapDumpOnOutOfMemoryError 选项,当 OutOfMemoryError 产生,即内存溢出(堆内存或持久代) 时,自动 Dump 堆内存快照。
- 示例用法: java -XX:+HeapDumpOnOutOfMemoryError -Xmx256m ConsumeHeap
- -XX:HeapDumpPath 选项,与 HeapDumpOnOutOfMemoryError 搭配使用,指定内存溢出时 Dump 文件的目录。
- 如果没有指定则默认为启动 Java 程序的工作目录。
- 示例用法: java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/ConsumeHeap 自动 Dump 的 hprof 文件会存储到 /usr/local/ 目录下。
- -XX:OnError 选项,发生致命错误时(fatal error)执行的脚本。
- 例如, 写一个脚本来记录出错时间, 执行一些命令,或者 curl 一下某个在线报警的 url。
- 示例用法:java -XX:OnError="gdb - %p" MyApp。可以发现有一个 %p 的格式化字符串,表示进程 PID。
- -XX:OnOutOfMemoryError 选项,抛出 OutOfMemoryError 错误时执行的脚本。
- -XX:ErrorFile=filename 选项,致命错误的日志文件名,绝对路径或者相对路径。
远程调试
- -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1506。
JavaAgent
Agent 是 JVM 中的一项黑科技,可以通过无侵入方式来做很多事情,比如注入 AOP 代码,执行统计等等,权限非常大。这里简单介绍一下配置选项,详细功能需要专门来讲。
如何设置 agent
- -agentlib:libname[=options] 启用 native 方式的 agent,参考 LD_LIBRARY_PATH 路径。
- -agentpath:pathname[=options] 启用 native 方式的 agent。 -javaagent:jarpath[=options] 启用外部的 agent 库,比如 pinpoint.jar 等等。
- -Xnoagent 则是禁用所有 agent。
例子
CPU使用时间抽样分析
JAVA_OPTS="-agentlib:hprof=cpu=samples,file=cpu.samples.log"
参数说明
-
-Xmx
-
-Xms
-
-Xss。
- 线程栈空间大小。
- 实例:-Xss1m。表示分配1m空间给每个线程使用。
-
-server
-
-XX:+DoExcapeAnalysis
-
逃逸分析。需在-server 模式下使用。
-
-XX:+DoExcapeAnalysis启用,-XX:-DoExcapeAnalysis禁用。默认禁用
-
-
-XX:+PrintGC
- 打印GC日志
-
-XX:+EliminateAllocations
- 标量替换(默认打开)。作用是允许对象打散分配在栈上。
- 比如对象拥有id和name两个字段,那么这两个字段会被视为两个独立的局部变量进行分配。
-
-XX:-UserTLAB(关闭TLAB)
- TLAB(Thread Local Allocation Buffer),线程本地分配缓冲区。
- 前言
- Java字节码简介
- 获取字节码清单
- 解读字节码清单
- 查看class文件的常量池信息
- 查看方法信息
- 线程栈与字节码执行模型
- 方法体中的字节码解读
- 对象初始化指令:new指令, init 以及clinit 简介
- 栈内存操作指令
- 局部变量表
- 流程控制指令
- 算数运算指令与类型转换指令
- 方法调用指令和参数传递
- 总结
- 附录
前言
Java中的字节码,英文名为 bytecode ,是Java代码编译后的中间代码格式。JVM需要读取并解析字节码才能执行相应的任务。
从技术人员的角度看,Java字节码是JVM的指令集。JVM加载字节码格式的class文件,校验之后通过JIT编译器转换为本地机器代码执行。
对于卓越而有追求的程序员,都能深入去探索一些技术细节, 在需要的时候,可以在代码被执行前解读和理解中间形式的代码。对于Java来说,中间代码格式就是Java字节码。 了解字节码及其工作原理,对于编写高性能代码至关重 要,对于深入分析和排查问题也有一定作用,所以我们要想深入了解JVM来说,了解字节码也是夯实基础的一项基本功。
而对于工具领域和程序分析来说,字节码就是必不可少的基础知识了,通过修改字节码来调整程序的行为是司空见惯的事情。想了解分析器(Profiler),Mock框架,AOP 等工具和技术这一类工具,则必须完全了解Java字节码。
Java字节码简介
有一件有趣的事情,就如名称所示, **Java bytecode **由单字节( byte )的指令组成, 理论上最多支持 256 个操作码(opcode)。实际上Java只使用了200左右的操作码, 还有一些操作码则保留给调试操作。
操作码, 下面称为 指令 , 主要由 类型前缀 和 操作名称 两部分组成。
例如,' i ' 前缀代表 ‘ integer ’,所以,' iadd ' 很容易理解, 表示对整数执行加 法运算。
根据指令的性质,主要分为四个大类
- 栈操作指令,包括与局部变量交互的指令
- 程序流程控制指令
- 对象操作指令,包括方法调用指令
- 算术运算以及类型转换指令
获取字节码清单
- 我们先创建一个
HelloByteCode.java文件如下:
package top.zsmile.test.jvm.bytecode;
public class HelloByteCode {
public static void main(String[] args) {
HelloByteCode obj = new HelloByteCode();
}
}
- 通过
javac将.java文件编译成.class文件。或者在 IDEA或者Eclipse等集成开发工具自动编译,基本上是等效的。只要能找到对应的 class即可。
javac 不指定 -d 参数编译后生成的 .class 文件默认和源代码在同一个目录。
注意: javac 工具默认开启了优化功能,生成的字节码中没有局部变量表(LocalVariableTable),相当于 局部变量名称被擦除。如果需要这些调试信息, 在编译时请加上 -g 选项。有兴趣的同学可以试试两种方式的区别,并对比结果。
javac .\HelloByteCode.java
# 生成在demo 目录下,需要先创建目录
javac -d demo .\HelloByteCode.java
- 可以用 javap 工具来获取 class 文件中的指令清单。 javap 是标准JDK 内置的一 款工具, 专门用于反编译class文件。
JDK自带工具的详细用法, 请使用: javac -help 或者 javap -help 来查看; 其他类似。
然后使用 javap 工具来执行反编译, 获取字节码清单:
javap -c HelloByteCode # 当前 目录执行
# demo 目录下
javap -c demo.HelloByteCode
javap -c demo/HelloByteCode
javap -c demo/HelloByteCode.class
解读字节码清单
Compiled from "HelloByteCode.java"
public class top.zsmile.test.jvm.bytecode.HelloByteCode {
public top.zsmile.test.jvm.bytecode.HelloByteCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class top/zsmile/test/jvm/bytecode/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
}
可以看到,反编译后的代码清单中, 有一个默认的构造函数 HelloByteCode() , 以及 main 方法
自动生成的构造函数,其方法体应该是空的,但这里看到里面有一些指令。为什么呢?
再次回顾Java知识, 每个构造函数中都会先调用 super 类的构造函数对吧? 但这不是JVM自动执行的, 而是由程序指令控制,所以默认构造函数中也就有一些字节码指令来干这个事情。
基本上,这几条指令就是执行 super() 调用;
public top.zsmile.test.jvm.bytecode.HelloByteCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
// java/lang/Object是默认继承了Object 类,是在编译时期就确定了的。
查看class文件的常量池信息
在 javap 命令中使用 -verbose 选项时, 还显示常量池信息。
javap -c -verbose HelloByteCode
常量池 大家应该都听说过, 英文是 Constant pool 。这里做一个强调: 大多数时候指的是 运行时常量 池 。但运行时常量池里面的常量是从哪里来的呢? 主要就是由 class 文件中的 常量池结构体 组成的。
-verbose用于输出常量池和本地变量信息
Classfile /D:/project/B.Smile/SmileX-boot/smilex-study/src/test/java/top/zsmile/test/jvm/bytecode/HelloByteCode.class
Last modified 2023-6-17; size 317 bytes
MD5 checksum f051f7adba144f74795cb61735f4ac41
Compiled from "HelloByteCode.java"
public class top.zsmile.test.jvm.bytecode.HelloByteCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
其中显示了很多关于class文件信息: 编译时间, MD5校验和, 从哪个 .java 源文件编译得来,符合哪 个版本的Java语言规范等等。
还可以看到 ACC_PUBLIC 和 ACC_SUPER 访问标志符。
- ACC_PUBLIC 标志很容易理解:这个类是 public 类,因此用这个标志来表示。
- ACC_SUPER 标志是怎么回事呢? 这就是历史原因, JDK1.0 的BUG修正中引入 ACC_SUPER 标志来修正 invokespecial 指令调用 super 类方法的问题,从 Java 1.1 开始, 编译器一般都会自动生成 ACC_SUPER 标志。
指令后面跟着的#1,#2,#3编号都是对常量池的引用。
==总结一下,常量池就是一个常量的大字典,使用编号的方式把程序里用到的各类常量统一管理起来,这样在字节码操作里,只需要引用编号即可。==
查看方法信息
在 javap 命令中使用 -verbose 选项时, 还显示方法的更多信息:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
可以看到方法描述: ([Ljava/lang/String;)V :
- 其中小括号内是入参信息/形参信息,
- 左方括号表述数组,
- L 表示对象,
- 后面的 java/lang/String 就是类名称
- 小括号后面的 V 则表示这个方法的返回值是 void
- 方法的访问标志也很容易理解 flags: ACC_PUBLIC, ACC_STATIC ,表示public和static
还可以看到执行该方法时需要的栈(stack)深度是多少,需要在局部变量表中保留多少个槽位, 还有方法的参数个数: stack=2, locals=2, args_size=1。把上面这些整合起来其实就是一个方法:
public static void main(java.lang.String[]);
注:实际上我们一般把一个方法的修饰符+名称+参数类型清单+返回值类型,合在一起叫“方法签名”, 即这些信息可以完整的表示一个方法。
看编译器自动生成的无参构造函数字节码:
public top.zsmile.jvm.bytecode.HelloByteCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltop/zsmile/jvm/bytecode/HelloByteCode;
你会发现一个奇怪的地方, 无参构造函数的参数个数居然不是0: stack=1, locals=1,args_size=1 。
这是因为在 Java 中, 如果是静态方法则没有 this 引用。 对于非静态方法, this 将被分配到局部变量 表的第0号槽位中, 关于局部变量表的细节,下面再进行介绍
有反射编程经验的同学可能比较容易理解: Method#invoke(Object obj, Object... args) ;
有JavaScript编程经验的同学也可以类比: fn.apply(obj, args) && fn.call(obj, arg1,arg2);
这里新建了一个静态方法进行对比
public static void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String test
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
这里可以看到 args_size=0,因为静态方法没有this引用。
线程栈与字节码执行模型
JVM是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM stack),用于存储 栈帧 (Frame)。
每一次方法调用,JVM都会自动创建一个栈帧。 栈帧 由 操作数栈 , 局部变量数组 以及一个 真数据区(class引用、异常处理表)组成。 class引用指向当前方法在运行时常量池中对应的class。异常处理表对应的是发生异常时,方便找到异常处理的地方。

局部变量数组 也称为 局部变量表 (LocalVariableTable), 其中包含了方法的参数,以及局部变量。 局部变 量数组的大小在编译时就已经确定: 和局部变量+形参的个数有关,还要看每个变量/参数占用多少个字节。 操作数栈是一个LIFO结构的栈, 用于压入和弹出值。 它的大小也在编译时确定。
有一些操作码/指令可以将值压入“操作数栈”; 还有一些操作码/指令则是从栈中获取操作数,并进行处理, 再将结果压入栈。操作数栈还用于接收调用其他方法时返回的结果值。
方法体中的字节码解读
0: new #5 // class top/zsmile/jvm/bytecode/HelloByteCode
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: return
==间隔不相等的原因是,有一部分操作码会附带有操作数, 也会占用字节码数组中的空间。==
例如, new 就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。 因此,下一条指令 dup 的索引从 3 开始

每个操作码/指令都有对应的十六进制(HEX)表示形式, 如果换成十六进制来表示,则方法体可表示为HEX字 符串。例如上面的方法体表示成十六进制如下所示:

甚至我们还可以在支持十六进制的编辑器中打开class文件,可以在其中找到对应的字符串:

粗暴一点,我们可以通过HEX编辑器直接修改字节码,尽管这样做会有风险,但如果只修改一个数值的话 应该会很有趣。
其实要使用编程的方式,方便和安全地实现字节码编辑和修改还有更好的办法,那就是使用ASM和 Javassist之类的字节码操作工具,也可以在类加载器和Agent上面做文章
对象初始化指令:new指令, init 以及clinit 简介
我们都知道 new 是Java编程语言中的一个关键字, 但其实在字节码中,也有一个指令叫做 new 。 当我 们创建类的实例时,编译器会生成类似下面这样的操作码:
0: new #5 // class top/zsmile/jvm/bytecode/HelloByteCode
3: dup
4: invokespecial #6 // Method "<init>":()V
当你同时看到 new, dup 和 invokespecial 指令在一起时,那么一定是在创建类的实例对象!
为什么是三条指令而不是一条呢?这是因为:
- new 指令只是创建对象,但没有调用构造函数
- invokespecial 指令用来调用某些特殊方法的, 当然这里调用的是构造函数。
- dup 指令用于复制栈顶的值
==由于构造函数调用不会返回值,所以如果没有dup指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题, 接下来的代码就无法对其进行处理。==
这就是为什么要事先复制引用的原因,为的是在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段。因此,接下来的那条指令一般是以下几种:
- astore {N} or astore_{N} – 赋值给局部变量,其中 {N} 是局部变量表中的位置。
- putfield – 将值赋给实例字段
- putstatic – 将值赋给静态字段
==在调用构造函数的时候,其实还会执行另一个类似的方法
实际上,还有一些情况会触发静态初始化, 详情请参考JVM规范: [http://docs.oracle.com/javase/specs/jvms/se8/html/]
栈内存操作指令
- dup 指令, 复制栈顶的值, 并将复制的值压入栈。
- pop 指令则从栈中删除最顶部的值。
- swap 指令可交换栈顶两个元素的值,例如A和B交换位置(图中示例4);
- dup_x1 指令, 复制栈顶的值, 并将复制的值插入到最上面2个值的下方。(图中示例5);
- dup2_x1 指令, 复制栈顶1个64位/或2个32位的值, 并将复制的值按照原始顺序,插入原始值下面一 个32位值的下方(图中示例6)。

dup_x1 和 dup2_x1 指令看起来稍微有点复杂。而且为什么要设置这种指令呢? 在栈中复制最顶部的 值?
请看一个实际案例:怎样交换2个double类型的值?
==需要注意的是, 一个double值占两个槽位,也就是说如果栈中有两个double值,它们将占用4个槽位。==
要执行交换,你可能想到了 swap 指令,但问题是 swap 只适用于单字(one-word, 单字一般指32位4个 字节, 64位则是双字),所以不能处理double类型, 但Java中又没有 swap2 指令。
怎么办呢? 解决方法就是使用 dup2_x2 指令, 将操作数栈顶部的double值, 复制到栈底double值的下方, 然后再使用 pop2 指令弹出栈顶的double值。结果就是交换了两个 double 值。 示意图如下图所示:

dup , dup_x1 , dup2_x1 指令补充说明
- dup指令
官方说明是: 复制栈顶的值, 并将复制的值压入栈.
操作数栈的值变化情况(方括号标识新插入的值):
..., value →
..., value [,value]
- dup_x1 指令
官方说明是: 复制栈顶的值, 并将复制的值插入到最上面2个值的下方。
操作数栈的值变化情况(方括号标识新插入的值):
..., value2, value1 →
..., [value1,] value2, value1
- dup2_x1 指令
官方说明是: 复制栈顶 1个64位/或2个32位的值, 并将复制的值按照原始顺序,插入原始值下面一个32位值的 下方。
操作数栈的值变化情况(方括号标识新插入的值):
# 情景1: value1, value2, and value3都是分组1的值(32位元素)
..., value3, value2, value1 →
..., [value2, value1,] value3, value2, value1
# 情景2: value1 是分组2的值(64位,long或double), value2 是分组1的值(32位元素)
..., value2, value1 →
..., [value1,] value2, value1

局部变量表
**stack **主要用于执行指令,而局部变量则用来保存中间结果,两者之间可以直接交互。
javac -g demp/*.java
-g 参数生成调试信息
==关于 LocalVariableTable 有个有意思的事情, 就是最前面的槽位会被方法参数占用。==
在这里,因为 main 是静态方法,所以槽位0中并没有设置为 this 引用的地址。 但是对于非静态方法 来说, this 会将分配到第0号槽位中

理解这些字节码的诀窍在于:
- 给局部变量赋值时,需要使用相应的指令来进行 store ,如 astore_1 。 store 类的指令都会删除栈顶值。
- 相应的 load 指令则会将值从局部变量表压入操作数栈,但并不会删除局部变量表中的值。
垃圾回收扩展
局部变量表中的变量也是重要的垃圾回收节点,只要被局部变量表中直接或间接引用的对象都是不会被回收的。
下面看个例子,来分析一下。
// 用6M空间测试
int size = 6 * 1024 * 1024;
public void gc1() {
byte[] a = new byte[size];
System.gc();
}
public void gc2() {
byte[] a = new byte[size];
a = null;
System.gc();
}
public void gc3() {
{
byte[] a = new byte[size];
}
System.gc();
}
public void gc4() {
{
byte[] a = new byte[size];
}
int c = 10;
System.gc();
}
public void gc5() {
gc1();
System.gc();
}
- gc1。申请空间后,立即进行回收,此时byte数组被变量a引用无法回收。
- gc2。申请空间后,先将变量a设置为null,使得byte数组没有被引用,所以直接被回收
- gc3。申请空间后,虽然变量a在局部作用域内,当执行回收时,变量a已经失效,但是局部变量表中依然还有引用指向,所以无法回收。
- gc4。和gc4不同的是,回收前对变量c进行赋值,这时因为变量a已失效,所以变量c直接使用了变量a的slot,所以byte数组被回收
- gc5。执行gc1的时候,在gc1中没有释放byte数组,所以在gc1中无法回收byte数组。但是当gc1返回后,栈帧销毁,所以在gc5中可以顺利回收byte数组。
分析方法:可以使用参数 -XX:+PrintGC 执行上述几个函数,在输出日志中查看垃圾回收后堆的大小变化,判断byte数组是否被回收。
流程控制指令
流程控制指令主要是分支和循环在用, 根据检查条件来控制程序的执行流程。
public class MovingAverage {
private int count = 0;
private double sum = 0.0d;
public void submit(double value) {
this.count++;
this.sum += value;
}
public double getAvg() {
if (0 == this.count) {
return sum;
}
return this.sum / this.count;
}
}
一般是 If-Then-Else 这种三元运算符(ternary operator),
Java中的各种循环,甚至异常处的理操作码都可归属于 程序流程控制
0: new #2 // class top/zsmile/bytecode/MovingAverage
3: dup
4: invokespecial #3 // Method top/zsmile/bytecode/MovingAverage."<init>":()V
7: astore_1
8: getstatic #4 // Field numbers:[I
11: astore_2
12: aload_2
13: arraylength
14: istore_3
15: iconst_0
16: istore 4
18: iload 4
20: iload_3
21: if_icmpge 43
24: aload_2
25: iload 4
27: iaload
28: istore 5
30: aload_1
31: iload 5
33: i2d
34: invokevirtual #5 // Method top/zsmile/bytecode/MovingAverage.submit:(D)V
37: iinc 4, 1
40: goto 18
43: aload_1
44: invokevirtual #6 // Method top/zsmile/bytecode/MovingAverage.getAvg:()D
47: dstore_2
48: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 30
line 8: 37
line 11: 43
line 12: 48
LocalVariableTable:
Start Length Slot Name Signature
30 7 5 number I
0 49 0 args [Ljava/lang/String;
8 41 1 ma Ltop/zsmile/bytecode/MovingAverage;
48 1 2 avg D
位置 [8~16] 的指令用于循环控制。
我们从代码的声明从上往下看, 在最后面的LocalVariableTable 中:
- 0 号槽位被main方法的参数 args 占据了。
- 1 号槽位被 ma 占用了。
- 5 号槽位被 number 占用了。
- 2 号槽位是for循环之后才被 avg 占用的
通过分析字节码指令可以看出,在 2 , 3 , 4 槽位有3个匿名的局部变量( astore_2 , istore_3 , istore 4 等指令)
- 2 号槽位的变量保存了 numbers 的引用值,占据了 2 号槽位。
- 3 号槽位的变量, 由 arraylength 指令使用, 得出循环的长度。
- 4 号槽位的变量, 是循环计数器, 每次迭代后使用 iinc 指令来递增。
如果我们的JDK版本再老一点, 则会在 2 , 3 , 4 槽位发现三个源码中没有出现的变量: arr$ , len$ , i$ , 也就是循环变量
18: iload 4
20: iload_3
21: if_icmpge 43
这段指令将局部变量表中 4 号槽位 和 3 号槽位的值加载到栈中,并调用 if_icmpge 指令来比较他们的值。
==【 if_icmpge 解读: if, integer, compare, great equal】, 如果一个数的值大于或等于另一个值,则程序执 行流程跳转到 pc=43 的地方继续执行。==
算数运算指令与类型转换指令
Java字节码中有许多指令可以执行算术运算。实际上,指令集中有很大一部分表示都是关于数学运算的。对 于所有数值类型( int , long , double , float ),都有加,减,乘,除,取反的指令。
那么 byte 和 char , boolean 呢? JVM 是当做 int 来处理的。另外还有部分指令用于数据类型之 间的转换。
算术操作码和类型
类型转换操作码
==唯一不需要将数值load到操作数栈的指令是 iinc ,它可以直接对 LocalVariableTable 中的值进行 运算。 其他的所有操作均使用栈来执行==
方法调用指令和参数传递
这里列举了各种用于方法调用的指令:
- invokestatic ,顾名思义,这个指令用于调用某个类的静态方法,这也是方法调用指令中最快的一 个。
- invokespecial , 我们已经学过了, invokespecial 指令用来调用构造函数,但也可以用于调用 同一个类中的 private 方法, 以及可见的超类方法。
- invokevirtual ,如果是具体类型的目标对象, invokevirtual 用于调用公共,受保护和打包 私有方法。
- invokeinterface,当要调用的方法属于某个接口时,将使用 invokeinterface 指令。
那么 invokevirtual 和 invokeinterface 有什么区别呢?这确实是个好问题。 为什么需要 invokevirtual 和 invokeinterface 这两种指令呢? 毕竟所有的接口方法都是公共方法, 直接 使用 invokevirtual 不就可以了吗?
这么做是源于对方法调用的优化。JVM必须先解析该方法,然后才能调用它。
- 使用 invokestatic 指令,JVM就确切地知道要调用的是哪个方法:因为调用的是静态方法,只能 属于一个类。
- 使用 invokespecial 时, 查找的数量也很少, 解析也更加容易, 那么运行时就能更快地找到所需 的方法.
使用 invokevirtual 和 invokeinterface 的区别不是那么明显。
想象一下,类定义中包含一个方 法定义表, 所有方法都有位置编号。下面的示例中:A类包含 method1和method2方法; 子类B继承A,继 承了method1,覆写了method2,并声明了方法method3。
//method1和method2方法在类A和类B中处于相同的索引位置。
class A
1: method1
2: method2
class B extends A
1: method1
2: method2
3: method3
那么,在运行时只要调用 method2,一定是在位置2处找到它.
现在我们来解释 invokevirtual 和 invokeinterface 之间的本质区别。
假设有一个接口X声明了methodX方法, 让B类在上面的基础上实现接口X:
class B extends A implements X
1: method1
2: method2
3: method3
4: methodX
新方法methodX位于索引4处,在这种情况下,它看起来与method3没什么不同。
但如果还有另一个类C也实现了X接口,但不继承A,也不继承B:
class C implements X
1: methodC
2: methodX
类C中的接口方法位置与类B的不同,这就是为什么运行时在 invokinterface 方面受到更多限制的原因。
==与 invokinterface 相比, invokevirtual 针对具体的类型方法表是固定的,所以每次都可以精确 查找,效率更高(具体的分析讨论可以参见参考材料的第一个链接)。==
JDK7新增的方法调用指令invokedynamic
随着JDK 7的发布,字节码指令集新增了 invokedynamic 指令。这条新增加的指令是实现“动态类型语言”(Dynamically Typed Language)支持而进行的改进之一,同时也是 JDK 8以后支持的lambda表达式的实现基础。
我们知道在不改变字节码的情况下,我们在Java语言层面想调用一个类A的方法m,只有两个办法:
- 使用 A a=new A(); a.m() ,拿到一个A类型的实例,然后直接调用方法;
- 通过反射,通过A.class.getMethod拿到一个Method,然后再调用这个 Method.invoke 反射调用;
这两个方法都需要显式的把方法m和类型A直接关联起来,假设有一个类型B,也有一个一模一样的方法签名 的m方法,怎么来用这个方法在运行期指定调用A或者B的m方法呢?这个操作在JavaScript这种基于原型的语言里或者是C#这种有函数指针/方法委托的语言里非常常见,Java里是没有直接办法的。Java里我们一般 建议使用一个A和B公有的接口IC,然后IC里定义方法m,A和B都实现接口IC,这样就可以在运行时把A和B 都当做IC类型来操作,就同时有了方法m,这样的“强约束”带来了很多额外的操作。
而新增的invokedynamic指令,配合新增的方法句柄(Method Handles,它可以用来描述一个跟类型A无关 的方法m的签名,甚至不包括方法名称,这样就可以做到我们使用方法m的签名,但是直接执行的时候调用 的是相同签名的另一个方法b),可以在运行时再决定由哪个类来接收被调用的方法。在此之前,只能使用 反射来实现类似的功能。该指令使得可以出现基于JVM的动态语言,让jvm更加强大。而且在JVM上实现动 态调用机制,不会破坏原有的调用机制。这样既很好的支持了Scala、Clojure这些JVM上的动态语言,又可以支持代码里的动态lambda表达式
简单来说就是以前设计某些功能的时候把做法写死在了字节码里,后来想改也改不了了。
所以这次给lambda语法设计翻译到字节码的策略是就用invokedynamic来作个弊,把实际的翻译策略隐 藏在JDK的库的实现里(metafactory)可以随时改,而在外部的标准上大家只看到一个固定的 invokedynamic。
总结
我们可以通过字节码对整个代码的运行情况进行分析,有些时候不同的写法实现的相同功能(for和foreach),可以通过对字节码的分析判断其性能上的差距。因为编译后会将 java 所提供的语法糖去掉。
常用的指令有如下几个:
-
javac编译.java文件。如果需要更多调试信息,可以加上-g。javac xxx.java javac -g xxx.java -
javap。反编译.class文件,解析出字节码清单。可以通过-verbose查询更多信息(常量池信息,方法签名等)javap -c xxx.class # or javap -c xxx javap -c -verbose xxx.class -
通过分析方法信息,我们可以注意到
- 执行该方法时需要的栈(stack)深度是多少,需要在局部变量表中保留多少个槽位, 还有方法的参数个数: stack=2, locals=2, args_size=1。
args_size。无参静态方法和无参对象方法的区别在于,无参对象方法,会将this引用的指向通过args_size的第一个槽位传入 方法中使用,所以无参对象方法的args_size=1.
-
关于对象初始化时,会调用构造方法,其实还会执行另一个类似的方法
,甚至在执行构造函数之前就执行了。 还有一个可能执行的方法是该类的静态初始化方法 , 但 。并不能被直接调用,而 是由这些指令触发的: new , getstatic , putstatic or invokestatic -
各种用于方法调用的指令
- invokestatic ,顾名思义,这个指令用于调用某个类的静态方法,这也是方法调用指令中最快的一 个。
- invokespecial , 我们已经学过了, invokespecial 指令用来调用构造函数,但也可以用于调用 同一个类中的 private 方法, 以及可见的超类方法。
- invokevirtual ,如果是具体类型的目标对象, invokevirtual 用于调用公共,受保护和打包 私有方法。
- invokeinterface,当要调用的方法属于某个接口时,将使用 invokeinterface 指令。
- invokedynamic,jdk7中新增的。这条新增加的指令是实现“动态类型语言”(Dynamically Typed Language)支持而进行的改进之一,同时也是 JDK 8以后支持的lambda表达式的实现基础。可以查看一下关于lambda表达式编译出来的源码可以看出差距。
-
关于线程栈,每次方法调用都会生成一个栈帧,里面都会包括:
- 操作栈。
- 局部变量表。
- 帧数据区(class引用、异常处理表)。用户获取class的常量池,以及异常发生后的处理方式。
局部变量表和操作栈是可以互相交互的,操作栈是用来执行指令的,而局部变量表是用来记录状态的。他们通过
store存储到局部变量表中,通过load加载到操作栈里。需要注意的是:store会删除栈顶的数据,而load不会删除局部变量表中的数据
附录
关于整形入栈指令(iconst,bipush,sipush,ldc)
- iconst。用于取值1~5
- bipush。用于取值-128~127
- sipush。用于取值-32768~32767
- ldc。用于取值-2147483648~2147483647
有几点需要注意:
- 取值-1时采用iconst_m1指令
- ldc指令是从常量池中获取值的,也能用来获取字符串
查看工具
jclasslib
TODO
简介
CompletableFuture 是 java.io.conrrent 库在 JDK8 中新增的并发工具,同传统的 Future 相比,支持流式计算、函数式编程、聚合计算、完成通知和自定义异常处理等特性。
CompletableFuture 实现了 CompletionStage 和 Future。CompletionStage 是对 Future 的扩展,增强了流式处理、异步回调、组合处理的能力,使得在处理多任务的协同工作时更加顺利。
CompletableFuture 和 FutureTask 属于 Future 接口的实现类,都可以获取线程的执行结果。

CompletableFuture创建
构造函数
最简单的方式是通过构造函数直接 new 一个 CompletableFutrue 实例。
CompletableFuture<Object> completableFuture = new CompletableFuture<>();
Object join = completableFuture.join();
System.out.println(join);
// 无输出
但需要注意的是,新创建的 CompletableFuture 没有计算结果时,当前线程执行 join 方法,会一直阻塞。
我们可以通过 complete 方法给当前线程设置结果。
CompletableFuture<Object> completableFuture = new CompletableFuture<>();
completableFuture.complete("create");
Object join = completableFuture.join();
System.out.println(join);
// 输出create
或者交由另外的线程设置计算结果,这样就实现了线程间协作。
CompletableFuture<Object> completableFuture = new CompletableFuture<>();
new Thread(() -> {
try {
System.out.println("开始等待");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束等待");
completableFuture.complete("create");
}).start();
System.out.println("获取值");
Object join = completableFuture.join();
Thread.sleep(1000);
System.out.println(join);
输出结果
获取值
开始等待
结束等待
create
completedFuture创建
CompletableFuture.completedFuture 设置一个计算结果,这样 CompletableFuture 相当于已经执行过了 complete 了。当然,一般情况下这个使用较少。
/**
* Returns a new CompletableFuture that is already completed with
* the given value.
*
* @param value the value
* @param <U> the type of the value
* @return the completed CompletableFuture
*/
public static <U> CompletableFuture<U> completedFuture(U value) {
return new CompletableFuture<U>((value == null) ? NIL : value);
}
这里来看一下案例
CompletableFuture<String> completedFuture = CompletableFuture.completedFuture("completedFuture");
String join1 = completedFuture.join();
log.info("result=>{}",join1);
completedFuture = CompletableFuture.completedFuture(null);
log.info("result=>{}",completedFuture.join());
输出结果
result=>completedFuture
result=>null
runAsync创建
CompletableFuture.runAsync 要求传入 Runnable 类型的参数,因此没有返回值。所以 runAsync 适合使用在不需要返回值的计算场景中。
CompletableFuture.runAsync 有两个方法签名。其中第二种会允许传入一个线程池,当没有传入自定义线程池时,runAsync 默认会使用内部的 ForkJoinPool 线程池。
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return asyncRunStage(asyncPool, runnable);
}
public static CompletableFuture<Void> runAsync(Runnable runnable,
Executor executor) {
return asyncRunStage(screenExecutor(executor), runnable);
}
简单案例:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
getThreadName(0);
});
log.info("result=>{}", future.join());
// 输出结果null。
注意这里是会阻塞调用 CompletableFuture.join 的线程来等待结果返回。
supplyAsync创建
CompletableFuture.supplyAsycn 方法与 CompletableFuture.runAsync 类似,区别在于 supplyAsync 接收一个 Supplier 类型的参数,会生成一个返回值,所以 supplyAsync 适用于需要返回值的计算场景中。
简单案例
@Test
public void supplyAsync() {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
getThreadName(0);
return "success";
});
log.info("result=>{}", future.join());
}
输出结果
[ForkJoinPool.commonPool-worker-1] INFO top.zsmile.test.basic.threads.CompletableFutureTest - ThreadName:ForkJoinPool.commonPool-worker-1,value:0
[main] INFO top.zsmile.test.basic.threads.CompletableFutureTest - result=>success
从输出结果可以看出,CompletableFuture 生成的异步线程会打印执行线程信息,并将结果返回给主线程打印。
结果获取
CompletableFutrue 有4类获取结果的方法:
public T get() throws InterruptedException, ExecutionException。获取结果。如果出现异常则抛出。public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException。获取结果,并允许传入超时字段,如果时间内没有获取到计算结果,则抛出TimeoutException异常,此时结果值为null。注意:就算抛出异常,但任务也不会中断,而是继续执行public T join()。获取结果,且不会抛出异常。public T getNow(T valueIfAbsent)。立即获取结果,如果CompletableFuture的结果未计算完成,则返回传入结果。注意:就算抛出异常,但任务也不会中断,而是继续执行
验证一下。
@Test
public void getResult() throws ExecutionException, InterruptedException {
// get
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
getThreadName(0);
return "success";
});
log.info("result=>{}", future.get());
// get
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
getThreadName(0);
try {
int i = 1;
while (i <= 5) {
Thread.sleep(1000);
log.debug("sleep1 1000");
i++;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return "success";
});
try {
log.info("result1=>{}", future1.get(2000, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
log.error("timeout1 => {}", e.getMessage());
}
// join
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
getThreadName(0);
return "success";
});
log.info("result2=>{}", future2.join());
// getNow
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
getThreadName(0);
try {
int i = 1;
while (i <= 5) {
Thread.sleep(1000);
log.debug("sleep2 1000");
i++;
}
log.info("over");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "success";
});
log.info("result3=>{}", future3.getNow("6666"));
Thread.sleep(10000);
log.info("result3=>{}", future3.getNow("6666"));
}
输出结果
11:12:26.581 [ForkJoinPool.commonPool-worker-1] INFO top.zsmile.test.basic.threads.CompletableFutureTest - ThreadName:ForkJoinPool.commonPool-worker-1,value:0
11:12:26.585 [main] INFO top.zsmile.test.basic.threads.CompletableFutureTest - result=>success
11:12:26.586 [ForkJoinPool.commonPool-worker-1] INFO top.zsmile.test.basic.threads.CompletableFutureTest - ThreadName:ForkJoinPool.commonPool-worker-1,value:0
11:12:27.601 [ForkJoinPool.commonPool-worker-1] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep1 1000
11:12:28.592 [main] ERROR top.zsmile.test.basic.threads.CompletableFutureTest - timeout1 => null
11:12:28.592 [ForkJoinPool.commonPool-worker-2] INFO top.zsmile.test.basic.threads.CompletableFutureTest - ThreadName:ForkJoinPool.commonPool-worker-2,value:0
11:12:28.592 [main] INFO top.zsmile.test.basic.threads.CompletableFutureTest - result2=>success
11:12:28.592 [main] INFO top.zsmile.test.basic.threads.CompletableFutureTest - result3=>6666
11:12:28.592 [ForkJoinPool.commonPool-worker-2] INFO top.zsmile.test.basic.threads.CompletableFutureTest - ThreadName:ForkJoinPool.commonPool-worker-2,value:0
11:12:28.607 [ForkJoinPool.commonPool-worker-1] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep1 1000
11:12:29.603 [ForkJoinPool.commonPool-worker-2] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep2 1000
11:12:29.618 [ForkJoinPool.commonPool-worker-1] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep1 1000
11:12:30.614 [ForkJoinPool.commonPool-worker-2] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep2 1000
11:12:30.630 [ForkJoinPool.commonPool-worker-1] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep1 1000
11:12:31.615 [ForkJoinPool.commonPool-worker-2] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep2 1000
11:12:31.631 [ForkJoinPool.commonPool-worker-1] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep1 1000
11:12:32.623 [ForkJoinPool.commonPool-worker-2] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep2 1000
11:12:33.630 [ForkJoinPool.commonPool-worker-2] DEBUG top.zsmile.test.basic.threads.CompletableFutureTest - sleep2 1000
11:12:33.630 [ForkJoinPool.commonPool-worker-2] INFO top.zsmile.test.basic.threads.CompletableFutureTest - over
11:12:38.605 [main] INFO top.zsmile.test.basic.threads.CompletableFutureTest - result3=>success
join和get的区别
另外 join 和 get 相同之处都是阻塞获取结果,那么这个异常抛出的差别是什么呢?
@Test
public void getOrJoin() {
try {
CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> 1 / 0);
log.info("join result => {}", integerCompletableFuture.join());
} catch (Exception e) {
e.printStackTrace();
}
try {
CompletableFuture<Integer> integerCompletableFuture2 = CompletableFuture.supplyAsync(() -> 1 / 0);
log.info("get result => {}", integerCompletableFuture2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
输出结果
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
...
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
...
这里 join 抛出 CompletionException 异常,而 get 抛出 ExecutionException 异常,既然都抛出了异常,那么他们的差别在于哪里呢?
join抛出的是unchecked异常,即RuntimeException异常,这种异常在检查期间不会抛出,也不会强制要求开发者进行捕获。并且会将异常包装成CancellationException和CompletionException异常。get抛出的是经过检查的异常,InterruptedException和ExecutionException。需要开发捕获处理。
流式计算
CompletableFuture 与 Future 最大的不同在于其对流式计算的支持,多个任务之间,可以关联,形成计算流。并对不同任务的计算结果进行处理。
CompletableFuture 流式方法的大致可以分为两类 带Async 和 不带Async的。带 Async 的会单独提交到线程池中。
前言
这里主要探讨中断常用的三个方法:
- interrupt()。在一个线程中调用需要中断现成的interrupt()方法,会对该线程发出信号,将中断状态标志为true
- isInterrupted()。判断当前线程的中断状态。
- interrupted()。将线程的中断状态恢复。
主要使用的阻塞三个方法:
- Object#wait。放弃锁+等待+重新获取锁
- Thread#join。【协作】等待某个线程执行完毕
- Thread#sleep。静态方法,线程休眠并让出CPU时间片
==注意:interrupt()不能中断在运行中的线程,它只能改变中断状态而已。实际完成的是让受阻塞的线程退出阻塞状态。==
确切的说:是被三种方法之一阻塞时,调用该线程的interrupt()方法,那么线程将抛出一个个InterruptedException中断异常,从而提早地终结被阻塞状态。
示例说明
public class Runner3 implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("我进入中断了,但我还在跑");
} else {
System.out.println("我没有进入中断");
}
}
}
public static void main(String[] args) {
Runner3 runner3 = new Runner3();
Thread thread3 = new Thread(runner3);
thread3.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread3.interrupt();
}
}
输出结果大致如下:
我没有进入中断
我没有进入中断
我进入中断了,但我还在跑
我进入中断了,但我还在跑
我进入中断了,但我还在跑
...
这里看到,执行interrupt()后,对线程执行中断后依然在执行,线程依然在运行。
我们调整一下run方法
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("我进入中断了,但我还在跑");
Thread.interrupted();//重置状态
} else {
System.out.println("我没有进入中断");
}
}
}
输出结果如下:
我没有进入中断
我没有进入中断
我进入中断了,但我还在跑
我没有进入中断
我没有进入中断
...
这里看到中断的状态重置了,那么我们如何去应用这个中断状态呢?
注意事项
- 当线程A执行到wait(),sleep(),join()时,抛出InterruptedException后,中断状态已经被系统复位了,线程A调用Thread.interrupted()返回的是false。
- 如果线程被调用了interrupt(),此时该线程并不在阻塞状态时,下次执行wait(),sleep(),join()时,一样会抛出InterruptedException,当然抛出后该线程的中断状态也会被系统复位。
案例1
public class Runner3 implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("我进入中断了,但我还在跑");
//
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("2"+Thread.currentThread().isInterrupted());
//输出false
}
} else {
System.out.println("我没有进入中断");
}
}
}
public static void main(String[] args) {
Runner3 runner3 = new Runner3();
Thread thread3 = new Thread(runner3);
thread3.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread3.interrupt();
}
}
执行上面的代码,我们可以看到在抛出异常后,Thread.currentThread().isInterrupted()输出为false,证明线程的中断状态已经复位了。
另外因为我们是先执行了interrupt()然后再进入睡眠状态,但是依然抛出了异常。
Object#wait 和 Thread.sleep 差异在哪里
因为Object#wait方法会阻塞线程,所以当我们执行interrupt时,会抛出InterruptedException异常。
那么Object#wait方法阻塞线程会导致的差异在哪里?
==最主要的差别在于sleep方法没有释放锁,而wait方法释放了锁,使得其它线程可以使用同步控制块或者方法。==
总结
- 调用interrupt方法,会改变中断状态,但不会影响线程的运行状态。
- 当执行了interrupt方法改变中断状态后,线程若执行Object#wait,Thread#sleep和Thread#join都会抛出InterruptedException异常,然后复位中断状态
- 当执行了interrupt方法改变中断状态后,线程未阻塞,且将要执行Object#wait,Thread#sleep和Thread#join阻塞线程时,都会抛出InterruptedException异常,复位中断状态。
前言
Java 中线程池的4中拒绝策略
- AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常,这是线程池默认的拒绝策略。于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
- DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略
- DiscardOldestPolicy: 如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
- CallerRunsPolicy: 有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
- 新提交的任务不会丢失,也就不会造成业务损失
- 由于谁提交任务谁就要负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
利用 ThreadFactory 统一处理异常
线程池的使用必不可少,使用无返回值 execute() 方法时,线程执行发生异常的话,需要记录日志,方便回溯,一般做法是在线程执行方法内 try/catch 处理 ,如下所示:
@Test
public void thread() {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
threadPoolExecutor.execute(() -> {
try {
log.info(Thread.currentThread().getName());
int i = (1 / 0);
} catch (Exception e) {
log.error("发生异常 => {}", e);
}
});
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
因为线程池一般情况下,都不会在一个项目里创建多个线程池,那么随着线程池在项目中多个地方提交任务时,使用 try/catch 进行异常捕获和记录,对于整个线程池管理非常不方便。我们可以通过利用 ThreadFactory ,在创建线程时,指定 setUncaughtExceptionHandler 异常处理方法,这样就可以做到全局处理异常。
@Test
public void test() {
ThreadFactory threadFactory = r -> {
Thread thread = new Thread(r);
thread.setName("testThread");
thread.setUncaughtExceptionHandler((t, err) -> {
log.info(t.getName());
log.error("err => {}", err);
});
return thread;
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), threadFactory);
// threadPoolExecutor.setThreadFactory(threadFactory);
threadPoolExecutor.execute(() -> {
log.info(Thread.currentThread().getName());
int i = (1 / 0);
});
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
线程池状态
- RUNNING
- 线程池会接收新任务,并处理阻塞队列里面的任务
- 通过调用
shutdown()方法,会切换到SHUTDOWN状态 - 通过调用
shutdownNow()方法,会切换到STOP状态
- SHUTDOWN
- 线程池不会接收新任务,但会处理阻塞队列中的任务;
- 队列为空,并且线程池中执行任务也为空,进入
TIDYING状态;
- STOP
- 线程池不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
- 线程池中执行任务为空,进入
TIDYING状态;
- TIDYING
- 该状态表明所有任务已经运行终止,记录的任务数量为 0。
terminated()执行完毕,进入TERMINATED状态
- TERMINATED
- 该状态表示线程池彻底终止
线程池实现原理(TODO)
JDK、JRE、JVM的关系
JDK
JDK(Java Development Kit) 是用于开发 Java 应用程序的软件开发工具集合,包括 了 Java 运行时的环境(JRE)、解释器(Java)、编译器(javac)、Java 归档 (jar)、文档生成器(Javadoc)等工具。简单的说我们要开发Java程序,就需要安 装某个版本的JDK工具包。
JRE
JRE(Java Runtime Enviroment )提供 Java 应用程序执行时所需的环境,由 Java 虚拟机(JVM)、核心类、支持文件等组成。简单的说,我们要是想在某个机器上运 行Java程序,可以安装JDK,也可以只安装JRE,后者体积比较小。
JVM
Java Virtual Machine(Java 虚拟机)有三层含义,分别是:
- JVM规范要求 满足 JVM
- 规范要求的一种具体实现(一种计算机程序)
- 一个 JVM 运行实例,在命令提示符下编写 Java 命令以运行 Java 类时,都会创建一 个 JVM 实例,我们下面如果只记到JVM则指的是这个含义;如果我们带上了某种JVM 的名称,比如说是Zing JVM,则表示上面第二种含义
JDK 与 JRE、JVM 之间的关系
就范围来说,JDK > JRE > JVM:
- JDK = JRE + 开发工具
- JRE = JVM + 类库

三者在开发运行Java程序时的交互关系:
- 简单的说,就是通过JDK开发的程序,编译以后,可以打包分发给其他装有JRE的机器上去运行。
- 运行的程序,则是通过java命令启动的一个JVM实例,代码逻辑的执行都运行在这个JVM实例上。
Java程序的开发运行过程为:
- 我们利用 JDK (调用 Java API)开发Java程序,编译成字节码或者打包程序
- 然后可以用 JRE 则启动一个JVM实例,加载、验证、执行 Java 字节码以及依赖库, 运行Java程序
- 而JVM 将程序和依赖库的Java字节码解析并变成本地代码执行,产生结果
JDK的发展过程与版本变迁
JDK版本列表
| JDK版本 | 发布时间 | 代号 | 备注 |
|---|---|---|---|
| 1 | 1996年1月23日 | Oak(橡树) | 初代版本,伟大的一个里程碑,但是是纯解释运行,使用JIT,性能比较差,速度慢 |
| 1.1 | 1997年2月19日 | Sparkl er(宝石) | JDBC、支持内部类、RMI、反射等等 |
| 1.2 | 1998年12月8日 | Playground(操场) | 集合框架、JIT等等 |
| 1.3 | 2000年5月8日 | Kestrel(红隼) | 对Java的各个方面都做了大量优化和增强 |
| 1.4 | 2004年2月6日 | Merlin(隼) | XML处理、支持IPV6、正则表达式,引入nio和CMS垃圾回收器 |
| 5 | 2004年9月30日 | Tiger(老虎) | 泛型、增强for语句、自动拆装箱、可变参数、静态导入、注解 |
| 6 | 2006年12月11日 | Mustang(野马) | 支持脚本语言、JDBC4.0 |
| 7 | 2011年7月28日 | Dolphin(海豚) | switch支持String类型、泛型推断、nio 2.0开发包、数 |
| 8 | 2014年3月18日 | Spider(蜘蛛) | Lambda表达式、接口默认方法、Stream API、新的日期API、Nashorn引擎jjs,引入G1垃圾回收器 |
| 9 | 2017年9月22日 | Modularity(模块化) | 模块系统、HTTP 2 客户端、多版本兼容 JAR 包、私有接口方法、改进Stream API、响应式流(Reactive Streams) API |
| 10 | 2018年3月21日 | 引入关键字 var 局部变量类型推断、统一的垃圾回收接口 | |
| 11 | 2018年9月25日 | HTTP客户端(标准)、无操作垃圾收集器,支持ZGC垃圾回收器,首个LTS版本 | |
| 12 | 2019年3月19日 | 新增一个名为Shenandoah的垃圾回收器、扩展switch 语句的功能、改进G1垃圾回收器 | |
| 13 | 2019年9月17日 | 改进了CDS内存共享,ZGC归还系统内存,SocketAPI 和switch语句以及文本块表示 | |
| 14 | 开发中 | 继续对ZGC、G1改进,标记ParallelScavenge + SerialOld组合为过时的,移除CMS垃圾回收器 |
Java大事记
- 1995年5月23日,Java语言诞生
- 1996年1月,第一个JDKJDK1.0诞生
- 1997年2月18日,JDK1.1发布
- 1997年4月2日,JavaOne会议召开,参与者逾一万人,创当时全球同类会议规模 之纪录
- 1997年9月,Java开发者社区成员超过十万
- 1998年2月,JDK1.1被下载超过200万次
- 1998年12月8日,JAVA2企业平台J2EE发布
- 1999年6月,Sun公司发布Java的三个版本:标准版、企业版和微型版(J2SE、 J2EE、J2ME)
- 2000年5月8日,JDK1.3发布
- 2000年5月29日,JDK1.4发布
- 2002年2月26日,J2SE1.4发布,自此Java的计算能力有了大幅提升
- 2004年9月30日,J2SE1.5发布,是Java语言的发展史上的又一里程碑事件,Java并发包JUC也是这个版本引入的。为了表示这个版本的重要性,J2SE1.5更 名为J2SE5.0
- 2005年6月,发布Java SE 6,这也是一个比较长期使用的版本
- 2006年11月13日,Sun公司宣布Java全线采纳GNU General Public License Version 2,从而公开了Java的源代码
- 2009年04月20日,Oracle公司74亿美元收购Sun。取得java的版权
- 2011年7月28日,Oracle公司发布Java SE7.0的正式版
- 2014年3月18日,Oracle公司发布ava SE 8,这个版本是目前最广泛使用的版本
- 2017年9月22日,JDK9发布,API有了较大的调整,添加了对WebSocket和 HTTP/2的支持,此后每半年发布一个大版本
- 2018年3月21日,JDK10发布,最大的变化就是引入了var,如果你熟悉C#或 JavaScript/NodeJS就会知道它的作用
- 2018年9月25日,JDK11发布,引入ZGC,这个也是第一个公布的长期维护版本 LTS
- 2019年3月19日,JDK12发布,引入毫秒级停顿的Shenandoah GC
- 2019年9月17日,JDK13发布,改进了CDS内存共享,ZGC归还系统内
常规的JDK,一般指OpenJDK或者Oracle JDK,当然Oracle还有一个新的JVM叫 GraalVM,也非常有意思。除了Sun/Oracle的JDK以外,原BEA公司(已被Oracle 收购)的JRockit,IBM公司的J9,Azul公司的Zing JVM,阿里巴巴公司的分支版 本DragonWell等等。
编程语言类型
编程语言分类
程从底向上划分为最基本的三大类:机器语言、汇编 语言、高级语言。
graph TD
机器语言-->汇编语言
汇编语言-->高级语言
按《计算机编程语言的发展与应用》一文里的定义:计算机编程语言能够实现人与机器之间的交流和沟通,而计算机编程语言主要包括汇编语言、机器语言以及高级语 言,具体内容如下:
- 机器语言:这种语言主要是利用二进制编码进行指令的发送,能够被计算机快速地识别,其灵活性相对较高,且执行速度较为可观,机器语言与汇编语言之间的相似性较高,但由于具有局限性,所以在使用上存在一定的约束性。
- 汇编语言:该语言主要是以缩写英文作为标符进行编写的,运用汇编语言进行编写的一般都是较为简练的小程序,其在执行方面较为便利,但汇编语言在程序方 面较为冗长,所以具有较高的出错率。
- 高级语言:所谓的高级语言,其实是由多种编程语言结合之后的总称,其可以对多条指令进行整合,将其变为单条指令完成输送,其在操作细节指令以及中间过程等方面都得到了适当的简化,所以,整个程序更为简便,具有较强的操作性,而这种编码方式的简化,使得计算机编程对于相关工作人员的专业水平要求不断放宽。
总之:机器语言是直接给机器执行的二进制指令,每种CPU平台都有对应的机器语言;
而汇编语言则相当于是给机器执行的指令,按照人可以理解的助记符表示,这样代码就非常长,但是性能也很好;
高级语言则是为了方便人来理解,进而快速设计和实现程序代码,一般跟机器语言和汇编语言的指令已经完全没有关系了,代码编写完成后通过编译或解释,转换成汇编 码或机器码,之后再传递给计算机去执行。
所以机器语言和汇编语言都是跟目标机器的CPU架构有直接联系,而高级语言一般就没有关系了,高级语言高级就高级在,一份代码往往是可以跨不同的目标机器的CPU架构的,不管是x86还是其他CPU,尽管不同CPU支持的指令集略有不同,但是都在编译或解释过程之后,变成实际平台的目标代码,进而代码的开发者很大程度上不需要关心目标平台的差异性。
高级语言分类
如果按照有没有虚拟机来划分,高级编程语言可分为两类:
- 有虚拟机:Java,Lua,Ruby,部分JavaScript的实现等等
- 无虚拟机:C,C++,C#,Golang,以及大部分常见的编程语言
如果按照变量是不是有确定的类型,还是类型可以随意变化来划分,高级编程语言可以分为:
- 静态类型:Java,C,C++等等
- 动态类型:所有脚本类型的语言
如果按照是编译执行,还是解释执行,可以分为:
- 编译执行:C,C++,Golang,Rust,C#,Java,Scala,Clojure,Kotlin, Swift...等等
- 解释执行:JavaScript的部分实现和NodeJS,Python,Perl,Ruby...等等
C#和Java都是编译后生成了一种中间类型的目标代码(类似汇编),但不是 汇编或机器码,在C#中称为 微软中间语言 (MSIL),在Java里叫做 Java字节码 (Java bytecode)。
虽然一般把JavaScript当做解释执行语言,但如今不少实现引擎都支持编译,比如 Google V8和Oracle Nashorn
我们还可以按照语言特点分类:
- 面向过程:C,Basic,Pascal,Fortran等等
- 面向对象:C++,Java,Ruby,Smalltalk等等
- 函数式编程:LISP、Haskell、Erlang、OCaml、Clojure、F#等等
关于跨平台
因为我们希望所编写的代码和程序,在源代码级别或者编译后,可以运行在多种不同的系统平台上,而不需要为了各个平台的不同点而去实现两套代码。
一般来说解释型语言都是跨平台的,同一份脚本代码,可以由不同平台上的解释器解释执行。
但是对于编译型语言,存在两种级别的跨平台:
- 典型的源码跨平台(C++):

- 典型的二进制跨平台(Java字节码):

Java语言通过虚拟机技术率先解决了这个难题。 源码只需要编译一次,然后把编译后的class文件或jar包,部署到不同平台,就可以直接通过安装在这些系统中的JVM上面执行。
同时可以把依赖库(jar文件)一起复制到目标机器,慢慢地又有了可以在各个平台都直接使用的Maven中央库(类似于linux里的yum或aptget源,macos里的 homebrew,现代的各种编程语言一般都有了这种包依赖管理机制:python的pip,dotnet的nuget,NodeJS的npm,golang的dep,rust的cargo等等)。这样就实现了让同一个应用程序在不同的平台上直接运行的能力。
小结
- 脚本语言直接使用不同平台的解释器执行,称之为脚本跨平台,平台间的差异由不同平台上的解释器去解决。这样的话代码很通用,但是需要解释和翻译,效率较低。
- 编译型语言的代码跨平台,同一份代码,需要被不同平台的编译器编译成相应的二进制文件,然后再去分发和执行,不同平台间的差异由编译器去解决。编译产生的文件是直接针对平台的可执行指令,运行效率很高。但是在不同平台上编译复杂软件,依赖配置可能会产生很多环境方面问题,导致开发和维护的成本较高。
- 编译型语言的二进制跨平台,同一份代码,先编译成一份通用的二进制文件,然后分发到不同平台,由虚拟机运行时来加载和执行,这样就会综合另外两种跨平台语言的优势,方便快捷地运行于各种平台,虽然运行效率可能比起本地编译类 型语言要稍低一点。而这些优缺点也是Java虚拟机的优缺点
现代商业应用最宝贵的是时间和人力,对大部分系统来说,机器相对来说就不是那么值钱了。
关于运行时(Runtime)与虚拟机(VM)
Java运行时和JVM虚拟机,简单的说JRE就是Java的运行时,包括虚拟机和相关的库等资源。
可以说运行时提供了程序运行的基本环境,JVM在启动时需要加载所有运行时的核心库等资源,然后再加载我们的应用程序字节码,才能让应用程序字节码运行在JVM这个容器里。
但也有一些语言是没有虚拟机的,编译打包时就把依赖的核心库和其他特性支持,一起静态打包或动态链接到程序中,比如Golang和Rust,C#等。这样运行时就和程序指令组合在一起,成为了一个完整的应用程序,好处就是不需要 虚拟机环境,坏处是编译后的二进制文件没法直接跨平台了。
关于内存管理和垃圾回收(GC)
自从编程语言诞生以来,内存管理一直都是个非常重要的话题。因为内存资源总是有限而又宝贵的,只占用不释放,很快就会用完了。程序得不到可用内存就会崩溃(想 想C++里动不动就出现的野指针)。
内存管理就是内存的生命周期管理,包括内存的申请、压缩、回收等操作。
Java的内存管理就是GC,JVM的GC模块不仅管理内存的回收,也负责内存的分配和压缩整理。
Java程序的指令都运行在JVM上,而且我们的程序代码 并不需要去分配内存和释放内存(例如C/C++里需要使用的malloc/free),那么这些操作自然是由JVM帮我们搞定的。
JVM在我们创建Java对象的时候去分配新内存,并使用GC算法,根据对象的存活时间,在对象不使用之后,自动执行对象的内存回收操作。
对于Golang和Rust这些语言来说,其实也是存在垃圾回收的,但是它们没有虚拟机, 又是怎么实现的呢?
诀窍就在于运行时(Runtime),编译打包的时候,可以把内存使用分析的模块一起打包到应用程序中,在运行期间有专门的线程来分析内存使用情况,进而决定什么时 候执行GC,把不再使用的内存回收掉。这样就算是没有虚拟机,也可以实现GC。
而Rust语言则更进一步,直接在语言规范层面限制了所有变量的生命周期,如果超出了一个明确的范围,就会不可用,这样在编译期就能直接知道每个对象在什么时候应该分配内存,什么时候应该销毁并回收内存,做到了很精确并且很安全的内存管理。
总结
- C/C++完全相信而且惯着程序员,让大家自行管理内存,所以可以编写很自由的代码,但一个不小心就会造成内存泄漏等问题导致程序崩溃。
- Java/Golang完全不相信程序员,但也惯着程序员。所有的内存生命周期都由JVM/运行时统一管理。在绝大部分场景下,你可以非常自由的写代码,而且不 用关心内存到底是什么情况。内存使用有问题的时候,我们可以通过JVM来信息 相关的分析诊断和调整。
- Rust语言选择既不相信程序员,也不惯着程序员。 让你在写代码的时候,必须清楚明白的用Rust的规则管理好你的变量,好让机器能明白高效地分析和管理内存。 但是这样会导致代码不利于人的理解,写代码很不自由,学习成本也很高。
首先,Rust是有点反人类,否则不会一直都不火。然后,Rust之所以反人类,是因为人类这玩意既愚蠢,又自大,破事还贼多。你看C++就很相信人类,它要求人类自己把自己new出来的东西给delete掉。
C++:“这点小事我相信你可以的!”
人类:“没问题!包在我身上!”
然后呢,内存泄漏、double free、野指针满世界飘…… C++:“……”
Java选择不相信人类,但替人类把事办好。
Java:“别动,让我来,我有gc!” 人类:“你怎么做事这么慢呀?你怎么还stop the world了呀?你是不是不爱我了 呀?”
Java:“……”
Rust发现唯一的办法就是既不相信人类,也不惯着人类。
Rust:“按老子说的做,不做就不编译!”
人类:“你反人类!” Rust:“滚!
理论
什么是RPC?
RPC (Remote Procedure Call) 即远程过程调用。
RPC能够帮助我门将两个不同的服务器上的服务提供的方法,所需要的网络编程调用实现,使得将远程服务的方法像本地调用一样简单,并且忽略具体的底层网络实现细节。
RPC原理是什么?
RPC的核心功能组成大致可以看作以下几个部分:
- 客户端(消费者)。调用远程方法的一端。
- 客户端 Stub(桩)。代理类,将调用的服务的类,方法,参数和类型等信息传输到服务端。
- 网络传输。就是通过什么样的网络形式和数据协议进行传输,常见的实现方式有基于自定义TCP或HTTP。
- 服务端 Stub(桩)。实际接收到的请求信息后,调取对应的方法返回处理结果给客户端。
- 服务端(提供者)。提供远程方法的一端。
流程大致如下:
graph TD
客户端-->|1|客户端Stub
客户端Stub-->|2|网络传输
网络传输-->|3|服务端Stub
服务端Stub-->|4|服务端
服务端-->|5|服务端Stub
服务端Stub-->|6|网络传输
网络传输-->|7|客户端Stub
客户端Stub-->|8|客户端
- 服务消费端(Client)以本地调用的方式调用远程服务(Service)
- 消费端Stub(Client stub)接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):RpcRequest
- 消费端Stub(Client stub)找到远程服务地址,并将序列化后的消息发送给服务端Stub
- 服务端Stub(Server Stub)收到消息后,将消息反序列化为Java对象:RpcRequest
- 服务端Stub(Server Stub)根据对象中的类、方法、参数等信息调用本地方法。
- 服务端Stub(Server Stub)得到方法执行的结果并将其组装成能够进行网络传输的消息体:RpcResponse(序列化)发送至消费端
- 消费端Stub(Client Stub)接收到消息并将消息反序列化为Java对象:RpcResponse,这样就执行完整个调用流程了。
关于WIKI上的RPC流程介绍
- 客户端调用客户端stub(client stub)。这个调用是在本地,并将调用参数push到栈(stack)中。
- 客户端stub(client stub)将这些参数包装,并通过系统调用发送到服务端机器。打包的过程叫 marshalling。(常见方式:XML、JSON、二进制编码)
- 客户端本地操作系统发送信息至服务器。(可通过自定义TCP协议或HTTP传输)
- 服务器系统将信息传送至服务端stub(server stub)。
- 服务端stub(server stub)解析信息。该过程叫 unmarshalling。
- 服务端stub(server stub)调用程序,并通过类似的方式返回给客户端。
设计自己的RPC框架

相关技术
- Java
- 动态代理机制;
- 序列化机制以及各种序列化框架的对比,比如 hession2、kryo、protostuff;
- 线程池的使用;
- CompletableFuture 的使用;
-
Netty
- 使用 Netty 进行网络传输;
- ByteBuf 介绍;
- Netty 粘包拆包;
- Netty 长连接和心跳机制;
-
Zookeeper
- 基本概念;
- 数据结构;
- 如何使用 Netflix 公司开源的 Zookeeper 客户端框架 Curator 进行增删改查;
参考Dubbo
官网:https://cn.dubbo.apache.org/zh-cn/overview/home/
Dubbo节点简单说明:
- Provider: 暴露服务的服务提供方
- Consumer: 调用远程服务的服务消费方
- Registry: 服务注册与发现的注册中心
- Monitor: 统计服务的调用次数和调用时间的监控中心
- Container: 服务运行容器
Duboo 调用关系说明:
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
功能
- 注册中心 :注册中心负责服务地址的注册与查找,相当于目录服务。
- 网络传输 :既然我们要调用远程的方法,就要发送网络请求来传递目标类和方法的信息以及方法的参数等数据到服务提供端。
- 序列化和反序列化 :要在网络传输数据就要涉及到序列化。
- 动态代理 :屏蔽远程方法调用的底层细节。
- 负载均衡 : 避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题。
- 传输协议 :这个协议是客户端(服务消费方)和服务端(服务提供方)交流的基础。
注册中心
注册中心主要提供服务地址的注册和查找功能。服务端启动时将服务名称及地址注册到注册中心,消费端通过服务名称找到对应的服务地址。有了服务地址之后,消费端就可以通过网络请求服务端。
框架选型:
- zookeeper
- nacos
- apollo
- redis
zookeeper
ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。并且,ZooKeeper 将数据保存在内存中,性能是非常棒的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。
https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html
https://curator.apache.org/docs/about/
https://zhuanlan.zhihu.com/p/603185454
序列化/反序列化
要在网络传输数据就要涉及到序列化。为什么需要序列化和反序列化呢? 因为网络传输的数据必须是二进制的。因此,我们的 Java 对象没办法直接在网络中传输。为了能够让 Java 对象在网络中传输我们需要将其序列化为二进制的数据。我们最终需要的还是目标 Java 对象,因此我们还要将二进制的数据“解析”为目标 Java 对象,也就是对二进制数据再进行一次反序列化。
另外,不仅网络传输的时候需要用到序列化和反序列化,将对象存储到文件、数据库等场景都需要用到序列化和反序列化。
JDK 自带的序列化,只需实现 java.io.Serializable接口即可,不过这种方式不推荐,因为不支持跨语言调用并且性能比较差。
常见序列化:
- hessian
- kryo
- protostuff
负债均衡
负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题。从单机的角度提升服务器处理能力,无论是提升 CPU 处理能力,还是增加内存、磁盘等空间,都不能满足日益增长的大流量、高并发、海量数据在高性能、高可用性等方面的需求。因此,只能通过横向扩展,增加服务器,即采用集群和负载均衡架构,来共同分担访问压力、提升业务处理能力。
简单来说,就是数据中心内部会以集群模式构建各种服务,通过在入口部署负载均衡,对外提供高访问量服务,提高应用程序的可用性、可靠性和可扩展性。这就是负载均衡的产生背景,也是负载均衡技术架构设计的来源。
https://c.biancheng.net/view/9823.html
https://help.aliyun.com/document_detail/132403.html
传输协议
通过设计协议,我们定义需要传输哪些类型的数据, 并且还会规定每一种类型的数据应该占多少字节。这样我们在接收到二进制数据之后,就可以正确的解析出我们需要的数据。
通常一些标准的 RPC 协议包含下面这些内容:
- 魔数 : 通常是 4 个字节。这个魔数主要是为了筛选来到服务端的数据包,有了这个魔数之后,服务端首先取出前面四个字节进行比对,能够在第一时间识别出这个数据包并非是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。
- 序列化器编号 :标识序列化的方式,比如是使用 Java 自带的序列化,还是 json,kryo 等序列化方式。
- 消息体长度 : 运行时计算出来。
动态代理
代理模式就是: 我们给某一个对象提供一个代理对象,并由代理对象来代替真实对象做一些事情。你可以把代理对象理解为一个幕后的工具人。 举个例子:我们真实对象调用方法的时候,我们可以通过代理对象去做一些事情比如安全校验、日志打印等等。但是,这个过程是完全对真实对象屏蔽的。
RPC 的主要目的就是让我们调用远程方法像调用本地方法一样简单,我们不需要关心远程方法调用的细节比如网络传输。
那如何屏蔽远程方法的调用细节呢?那就是动态代理。
https://javaguide.cn/java/basis/proxy.html
网络传输
既然我们要调用远程的方法,就要发送网络请求来传递目标类和方法的信息以及方法的参数等数据到服务提供端。
常见通信框架:
- Java Socket(不推荐)
- Java Nio(不推荐)
- Netty(推荐使用)
Netty
Netty 是一个基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。 2它极大地简化并简化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。 3支持多种协议如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
目前主流定时任务框架(截至20240126)
- xxl-job
- powerjob
- quartz

RedisConnectionException异常
io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1:6379
方法1
追踪问题一直以为redis密码错误,最好尝试很多办法依旧没有解决,使用jedis连接却是正常的!!!
解决办法最后在redis 官网问题反馈里找到了答案:
因为使用的spring boot 高版本导致的:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5</version>
<relativePath/>
</parent>
降低版本就行了
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
Redisson NoClassDefFoundError RedisStreamCommands
在高版本的 redisson-spring-boot-starter v3.13.* 中,本地执行redisson锁正常,但是放到服务器上后提示异常。这是因为缺少 RedisStreamCommands 类,换成较低版本即可
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.5</version>
</dependency>
集成
springboot集成shiro实现权限控制
源码地址:SmileX-boot
Maven版本
引入springboot-start
<!-- springboot-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<!-- shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<shiro.version>1.9.0</shiro.version>
</dependency>
AuthenticationToken
/**
* TOKEN
*/
public class OAuth2Token implements AuthenticationToken {
private String token;
public OAuth2Token(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
Filter
/**
* 过滤器
*/
public class OAuth2Filter extends BasicHttpAuthenticationFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
return null;
}
return new OAuth2Token(token);
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
} catch (Exception e) {
JwtUtils.responseError(response, ResultCode.NO_AUTH, CommonConstant.S_INVALID_TOKEN);
return false;
}
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
//处理登录失败的异常
JwtUtils.responseError(response, ResultCode.NO_AUTH, CommonConstant.S_INVALID_TOKEN);
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader(CommonConstant.S_ACCESS_TOKEN);
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter(CommonConstant.S_ACCESS_TOKEN);
}
return token;
}
}
Realm
public class OAuth2Realm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 授权逻辑
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
return simpleAuthorizationInfo;
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
if (token == null) {
log.info("————————身份认证失败————————,IP地址记录:" + IPUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest()));
throw new AuthenticationException("身份验证失败");
}
// token验证逻辑
return new SimpleAuthenticationInfo(userId, token, getName());
}
}
Shiro配置
@Configuration
public class ShiroConfig {
/**
* 注入realm
* @return
*/
@Bean("oAuth2Realm")
public OAuth2Realm oAuth2Realm() {
OAuth2Realm realm = new OAuth2Realm();
return realm;
}
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", new OAuth2Filter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
return shiroFilterFactoryBean;
}
/**
* 配置拦截
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/sys/login", "anon");
chainDefinition.addPathDefinition("/**", "oauth2");
return chainDefinition;
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入realm 到securityManager
List<Realm> realms = new ArrayList<>();
realms.add(oAuth2Realm());
securityManager.setRealms(realms);
securityManager.setRememberMeManager(null);
return securityManager;
}
@Bean
protected CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
通用的工具
JwtUtils.responseError
public static void responseError(ServletResponse response, Integer code, String msg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
R fail = R.fail(code, msg);
try {
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
outputStream.print(JSON.toJSONString(fail));
outputStream.flush();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
shiro的几种CacheManager
看官方的文档,提供了三种CacheManager。
- MemoryConstrainedCacheManager(仅适用于单体应用)
- HazelcastCacheManager
- EhCacheManager
第三方提供的:
- https://github.com/alexxiyang/shiro-redis。基于jedis,文档
对于Hazelcast没有使用过,EhCache现在我使用的比较少,所以我这里
MemoryConstrainedCacheManager
使用
直接注入配置即可。
@Bean
protected CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
源码查看
这里简单查看一下MemoryConstrainedCacheManager的源码。
// 继承了一个AbstractCacheManager
public class MemoryConstrainedCacheManager extends AbstractCacheManager {
public MemoryConstrainedCacheManager() {
}
protected Cache createCache(String name) {
return new MapCache(name, new SoftHashMap());
}
}
public abstract class AbstractCacheManager implements CacheManager, Destroyable {
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap();
public AbstractCacheManager() {
}
public <K, V> Cache<K, V> getCache(String name) throws IllegalArgumentException, CacheException {
if (!StringUtils.hasText(name)) {
throw new IllegalArgumentException("Cache name cannot be null or empty.");
} else {
Cache cache = (Cache)this.caches.get(name);
if (cache == null) {
cache = this.createCache(name);
Cache existing = (Cache)this.caches.putIfAbsent(name, cache);
if (existing != null) {
cache = existing;
}
}
return cache;
}
}
}
public interface CacheManager {
<K, V> Cache<K, V> getCache(String var1) throws CacheException;
}
public interface Destroyable {
void destroy() throws Exception;
}
可以看见AbstractCacheManager实现了CacheManager和Destoryable接口,实现了获取缓存和销毁的操作。
同事在AbstractCacheManager的代码内,我们可以看到它是使用了ConcurrentHashMap作为并发存储容器的。
shiro-redis
使用
导入Maven依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.3.1</version>
<exclusions>
<!-- 排除shiro-core包,可选 -->
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
Springboot集成Shiro(启用和禁用Session)
这里主要是为了说明使用session和不用session的情况,如何取舍就看用户自身需求
- 如果使用了session,用户登陆一次,则session有效期内的请求则不会再验证token。
- 如果不适用session,用户每次请求都会验证 token。
Maven
引入springboot-start
<!-- springboot-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<!-- shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<shiro.version>1.9.0</shiro.version>
</dependency>
OAuth2Token
/**
* TOKEN
*/
public class OAuth2Token implements AuthenticationToken {
private String token;
public OAuth2Token(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
Realm
supports 方法是为了确保 OAuth2Realm 只对 OAuth2Token生效。
public class OAuth2Realm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 授权逻辑
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
return simpleAuthorizationInfo;
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
if (token == null) {
log.info("————————身份认证失败————————,IP地址记录:" + IPUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest()));
throw new AuthenticationException("身份验证失败");
}
// token验证逻辑
return new SimpleAuthenticationInfo(userId, token, getName());
}
}
Filter
/**
* oauth2过滤器
*
*/
public class OAuth2Filter extends BasicHttpAuthenticationFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
return null;
}
return new OAuth2Token(token);
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String token = getRequestToken(httpServletRequest);
if (StringUtils.isBlank(token)) {
JwtUtils.responseError(response, ResultCode.NO_AUTH, CommonConstant.S_INVALID_TOKEN);
return false;
}
return executeLogin(request, response);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
JwtUtils.responseError(response, ResultCode.NO_AUTH, throwable.getMessage() );
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader(CommonConstant.S_ACCESS_TOKEN);
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter(CommonConstant.S_ACCESS_TOKEN);
}
return token;
}
}
-
preHandle 方法中,获取Request请求的Method是否为Options是为了解决跨域问题,现在前端浏览器会在发起真正的请求前,发起一次Options请求,用来作为跨域预检。这里捕捉到了以后直接拦截并更改状态为成功后,直接返回。
-
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { AuthenticationToken token = createToken(request, response); if (token == null) { String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " + "must be created in order to execute a login attempt."; throw new IllegalStateException(msg); } try { Subject subject = getSubject(request, response); subject.login(token); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } }- 重写 createToken 方法是在因为在 exceuteLogin 方法的默认实现中,是通过 createToken 方法获取token信息,因为我们是基于http请求,在header中获取自定义属性的,这里就进行了重写。
- 重写 onLoginFailure方法是因为执行login方法后,会进入 Realm 类中的 doGetAuthenticationInfo 方法进行认证,当认证失败会抛出 AuthenticationException 异常,此时try-catch捕捉到异常后会调用 onLoginFailure 方法,这时就能根据需要放回异常信息给用户。
Shiro配置
@Slf4j
@Configuration
public class ShiroConfig {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
/**
* 注入realm
*
* @return
*/
@Bean("oAuth2Realm")
public OAuth2Realm oAuth2Realm() {
OAuth2Realm realm = new OAuth2Realm();
return realm;
}
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", new OAuth2Filter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
return shiroFilterFactoryBean;
}
/**
* 配置拦截
*
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/sys/login", "anon");
chainDefinition.addPathDefinition("/**", "oauth2");
return chainDefinition;
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入realm 到securityManager
List<Realm> realms = new ArrayList<>();
realms.add(oAuth2Realm());
securityManager.setRealms(realms);
securityManager.setCacheManager(cacheManager());
securityManager.setSessionManager(defaultSessionManager());
securityManager.setRememberMeManager(null);
return securityManager;
}
@Bean
public DefaultSessionManager defaultSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
//有效期30分钟
defaultWebSessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
defaultWebSessionManager.setCacheManager(cacheManager());
defaultWebSessionManager.setDeleteInvalidSessions(true);
return defaultWebSessionManager;
}
/***
* 单机使用
* @return
*/
protected CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
这里需要注意的是 defaultSessionManager ,因为shiro默认会使用自带的session管理,那这时session是永不过期的,那用户只需要通过一次token认证,就可以不用再认证了,那我们有什么办法呢?
-
设置defaultSessionManager的过期时间,定时清除session。
-
禁用默认session,使用JWT进行过期管理。
如何禁用session可以参考以下代码:我们只需要更改前面代码中的 securityManager方法,增加DefaultSubjectDAO来禁用默认session。
@Bean("securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入realm 到securityManager
List<Realm> realms = new ArrayList<>();
realms.add(oAuth2Realm());
/**
* 禁用session管理
* 根据需要判断,开启后,所有提交都需要验证Token,
* 否则Login成功后,根据cookie获取session。
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
securityManager.setRealms(realms);
securityManager.setCacheManager(cacheManager());
//设置session管理
// securityManager.setSessionManager(defaultSessionManager());
securityManager.setRememberMeManager(null);
return securityManager;
}
问题记录
Springboot+Shiro,使用postman会显示404,需要返回未授权响应(JSON格式)
问题描述
今天当我配置完springboot+shiro后,我用postman返回任何接口都会返回404。
结果显示:

配置信息:
@Bean(name = "filterShiroFilterRegistrationBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
return shiroFilterFactoryBean;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/sys/login", "anon");
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
很奇怪没有明白为什么会显示404,然后就去查找了官方的问题,发现在使用 authc 拦截时,如果没有通过验证,那么会重定向到 LoginUrl,可是我这里只想返回401无权限访问。
这里插一句题外话,如果把 shiroFilterFactoryBean 的beanName换成 shiroFilterFactoryBean,如下代码所示:
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
return shiroFilterFactoryBean;
}
那么这里的异常界面就会显示为:提示重定向次数过多。

定位原因
那么如何解决这个重定向 LoginUrl 问题呢?其实我们可以在官方文档默认过滤器找到答案。
我们看一下过滤器 authc 对应的过滤器 org.apache.shiro.web.filter.authc.FormAuthenticationFilter 的源码。
// 继承了 AuthenticatingFilter 认证过滤器
public class FormAuthenticationFilter extends AuthenticatingFilter {
// 无权限
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (this.isLoginRequest(request, response)) {
if (this.isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return this.executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the Authentication url [" + this.getLoginUrl() + "]");
}
this.saveRequestAndRedirectToLogin(request, response);
return false;
}
}
//保存请求并重定向到登录页面
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
saveRequest(request);
redirectToLogin(request, response);
}
// 跳转登录页面
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
String loginUrl = getLoginUrl();
WebUtils.issueRedirect(request, response, loginUrl);
}
}
这里提取了部分源码,可以看出当用户无权限访问路径时,authc过滤器会将我们的请求重定向到 loginUrl,我们可以在shiroFilterFactoryBean里面配置它。这样就可以跳转到对应的路径了。
shiroFilterFactoryBean.setLoginUrl("/login");
既然我们知道了使用 authc 过滤器时,无权限会发生跳转的原因,那么我们应该如何实现返回JSON呢?
解决方案
我们可以自定义拦截器继承 AuthenticatingFilter 类。
public class ShiroFilter extends AuthenticatingFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String token = getRequestToken(httpServletRequest);
if (StringUtils.isBlank(token)) {
String json = "{\"code\":401,\"msg\":\"非法token\",\"success\":false}";
httpServletResponse.getWriter().print(json);
return false;
}
return executeLogin(request, response);
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader(CommonConstant.S_ACCESS_TOKEN);
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter(CommonConstant.S_ACCESS_TOKEN);
}
return token;
}
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
return null;
}
return new OAuth2Token(token);
}
}
通过重新重写 onAccessDenied 方法,我们就能实现当验证失败时返回json的功能,告知用户无权限操作。这里还需要把过滤器加载到 ShiroFilterFactoryBean 中。
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 这里将过滤器类添加进去,名称为 myAuthc
Map<String, Filter> filters = new HashMap<>();
filters.put("myAuthc", new ShiroFilter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
return shiroFilterFactoryBean;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/sys/login", "anon");
// 将所有的都使用 myAuthc拦截
chainDefinition.addPathDefinition("/**", "myAuthc");
return chainDefinition;
}
让我们看一下效果。

总结
其实当我们使用authc过滤器,出现404的现象是正常的,因为授权失败了后会自动跳转登陆页面,那我们没有配置这个登陆页面时,就会出现404。所以我们可以通过自定义过滤器并使用的方式,从而返回JSON或者其它自己所需要的授权失败结果。
docx转html
这是自己写了的一个读取docx并处理成html的例子。导入 POI Maven
<!-- Apache Poi -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
已经实现如下功能:
- 段落水平对齐方式
- 图片读取并转为base64
- 字体类型,大小,高亮,颜色,加粗,下划线,斜体
- 表格读取、背景颜色设置,垂直居中,底纹处理。
注意事项:
- 低版本的高亮获取会提示null,升级高版本
4.1.2+即可解决。
@Test
public void poiTest() throws IOException {
// InputStream is = new FileInputStream("D:/test/doc/import.docx");
// String filename = "D:/test/doc/1111.docx";
String filename = "D:/test/doc/import.docx";
InputStream is = new FileInputStream(filename);
XWPFDocument doc = new XWPFDocument(is);
List<IBodyElement> bodyElements = doc.getBodyElements();
StringBuilder htmlStrBuild = new StringBuilder();
for (IBodyElement bodyElement : bodyElements) {
if (bodyElement.getElementType().equals(BodyElementType.PARAGRAPH)) {
XWPFParagraph paragraph = (XWPFParagraph) bodyElement;
htmlStrBuild.append(paragraphHandle(paragraph));
} else if (bodyElement.getElementType().equals(BodyElementType.TABLE)) {
XWPFTable table = (XWPFTable) bodyElement;
String tableStr = "<table cellpadding=\"0\" border=\"1\" cellspacing=\"0\">";
List<XWPFTableRow> rows = table.getRows();
for (XWPFTableRow row : rows) {
String rowStyle = "";
// 行高
if (row.getHeight() > 0) {
rowStyle += "height:" + (row.getHeight() / 10) + "px;";
}
tableStr += "<tr style=\"" + rowStyle + "\">";
List<XWPFTableCell> tableCells = row.getTableCells();
for (XWPFTableCell tableCell : tableCells) {
String cellStyle = "";
if (tableCell.getWidth() > 0) {
cellStyle += "width:" + tableCell.getWidth() + "px;";
}
// 垂直剧中
XWPFTableCell.XWPFVertAlign verticalAlignment = tableCell.getVerticalAlignment();
if (verticalAlignment != null) {
if (verticalAlignment.equals(XWPFTableCell.XWPFVertAlign.TOP)) {
cellStyle += "vertical-align:top;";
} else if (verticalAlignment.equals(XWPFTableCell.XWPFVertAlign.CENTER)) {
cellStyle += "vertical-align:center;";
} else if (verticalAlignment.equals(XWPFTableCell.XWPFVertAlign.BOTTOM)) {
cellStyle += "vertical-align:bottom;";
}
}
// pct-50
CTShd cellShd = getCellShd(tableCell);
if (cellShd != null) {
// 设置单元格背景色
String cellFillHex = getCellFillHex(tableCell);
if (cellFillHex != null) {
cellStyle += "background-color:#" + cellFillHex + ';';
}
// 设置单元格底纹
String cellColorHex = getCellColorHex(cellShd);
if (StringUtils.isBlank(cellColorHex)) {
cellColorHex = "000";
}
String shdValue = cellShd.getVal().toString();
if (shdValue.equals("solid")) {
cellStyle += String.format("background-color:#%s;", cellColorHex);
} else if (shdValue.startsWith("pct")) {
cellStyle += String.format("background:radial-gradient(circle, #%s 0.5px, transparent 0.5px);", cellColorHex);
Double num = Math.ceil(Double.valueOf(cellShd.getVal().toString().substring(3)) / 10);
cellStyle += String.format("background-size: %spx %spx;", num, num);
} else if (shdValue.equals("horzStripe")) {
cellStyle += String.format("background-image: -webkit-linear-gradient(#%s 50%%, transparent 50%%, transparent);", cellColorHex);
cellStyle += String.format("background-image: linear-gradient(#%s 50%%, transparent 50%%, transparent);", cellColorHex);
cellStyle += String.format("background-size: 3px 3px;");
} else if (shdValue.equals("vertStripe")) {
cellStyle += String.format("background-image: -webkit-linear-gradient(to right,#%s 50%%, transparent 50%%, transparent);", cellColorHex);
cellStyle += String.format("background-image: linear-gradient(to right,#%s 50%%, transparent 50%%, transparent);", cellColorHex);
cellStyle += String.format("background-size: 3px 3px;");
} else if (shdValue.equals("diagStripe")) {
cellStyle += String.format("background-image: -webkit-linear-gradient(-45deg,#%s 25%%, transparent 25%%,transparent 50%%,#%s 50%%,#%s 75%%,transparent 75%%, transparent);", cellColorHex, cellColorHex,cellColorHex);
cellStyle += String.format("background-image: linear-gradient(-45deg,#%s 25%%, transparent 25%%,transparent 50%%,#%s 50%%,#%s 75%%,transparent 75%%, transparent);", cellColorHex, cellColorHex,cellColorHex);
cellStyle += String.format("background-size: 5px 5px;");
} else if (shdValue.equals("reverseDiagStripe")) {
cellStyle += String.format("background-image: -webkit-linear-gradient(45deg,#%s 25%%, transparent 25%%,transparent 50%%,#%s 50%%,#%s 75%%,transparent 75%%, transparent);", cellColorHex, cellColorHex,cellColorHex);
cellStyle += String.format("background-image: linear-gradient(45deg,#%s 25%%, transparent 25%%,transparent 50%%,#%s 50%%,#%s 75%%,transparent 75%%, transparent);", cellColorHex, cellColorHex,cellColorHex);
cellStyle += String.format("background-size: 5px 5px;");
}
}
// 获取文本
tableStr += "<td style=\"" + cellStyle + "\">";
List<XWPFParagraph> paragraphs = tableCell.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
tableStr += paragraphHandle(paragraph);
}
tableStr += "</td>";
}
tableStr += "</tr>";
}
tableStr += "</table>";
htmlStrBuild.append(tableStr);
}
}
log.info(htmlStrBuild.toString());
}
private CTShd getCellShd(XWPFTableCell tableCell) {
if (tableCell.getCTTc() != null) {
if (tableCell.getCTTc().getTcPr() != null) {
return tableCell.getCTTc().getTcPr().getShd();
}
}
return null;
}
private String getCellFillHex(XWPFTableCell tableCell) {
if (tableCell.getCTTc() != null) {
if (tableCell.getCTTc().getTcPr() != null && getCellShd(tableCell) != null) {
return getCellFillHex(getCellShd(tableCell));
}
}
return null;
}
private String getCellFillHex(CTShd chd) {
if (chd != null) {
Object fill = chd.getFill();
if ("auto".equals(fill.toString())) {
return null;
} else if (fill instanceof byte[]) {
return CmdUtils.bytesToHexString((byte[]) fill);
}
}
return null;
}
private String getCellColorHex(XWPFTableCell tableCell) {
if (tableCell.getCTTc() != null) {
if (tableCell.getCTTc().getTcPr() != null && getCellShd(tableCell) != null) {
return getCellColorHex(getCellShd(tableCell));
}
}
return null;
}
private String getCellColorHex(CTShd chd) {
if (chd != null) {
Object color = chd.getColor();
if ("auto".equals(color.toString())) {
return null;
} else if (color instanceof byte[]) {
return CmdUtils.bytesToHexString((byte[]) color);
}
}
return null;
}
private String paragraphHandle(XWPFParagraph paragraph) {
StringBuilder htmlStrBuild = new StringBuilder();
// 段落属性
String paragraphStyle = "";
//对齐方式 alignment 枚举值
if (paragraph.getAlignment().equals(ParagraphAlignment.CENTER)) {
paragraphStyle += "text-align:center;";
} else if (paragraph.getAlignment().equals(ParagraphAlignment.LEFT)) {
paragraphStyle += "text-align:left;";
} else if (paragraph.getAlignment().equals(ParagraphAlignment.RIGHT)) {
paragraphStyle += "text-align:right;";
}
//获取段落所有的文本对象
List<XWPFRun> runs = paragraph.getRuns();
htmlStrBuild.append("<p style=\"" + paragraphStyle + "\">");
for (XWPFRun run : runs) {
// 图片处理
List<XWPFPicture> embeddedPictures = run.getEmbeddedPictures();
if (CollectionUtils.isNotEmpty(embeddedPictures)) {
for (XWPFPicture embeddedPicture : embeddedPictures) {
htmlStrBuild.append("<p>");
XWPFPictureData pictureData = embeddedPicture.getPictureData();
String encode = "data:" + pictureData.getPackagePart().getContentType() + ";base64," + new String(Base64Utils.encode(pictureData.getData()));
htmlStrBuild.append("<img src=\"" + encode + "\"/>");
htmlStrBuild.append("</p>");
}
}
if (StringUtils.isNotBlank(run.text())) {
String style = "";
//文本的颜色 color
if (StringUtils.isNotBlank(run.getColor())) {
style += "color:#" + run.getColor() + ";";
}
//文本 大小 fontSize
if (run.getFontSizeAsDouble() != null && run.getFontSizeAsDouble() > 0) {
style += "font-size:" + run.getFontSizeAsDouble().intValue() + "px;";
}
//文本 类型 fontFamily
if (StringUtils.isNotBlank(run.getFontFamily())) {
style += "font-family:" + run.getFontFamily() + ";";
}
//文本 加粗
if (run.isBold()) {
style += "font-weight: bolder;";
}
//文本 斜体
if (run.isItalic()) {
style += "font-style:italic;";
}
//文本 下划线
if (run.getUnderline().equals(UnderlinePatterns.SINGLE)) {
style += "text-decoration: underline;";
}
//文本 高亮
if (run.isHighlighted()) {
style += "background-color:" + run.getTextHighlightColor() + ";";
}
htmlStrBuild.append(String.format("<span style=\"%s\">%s</span>", style, run.text()));
}
}
htmlStrBuild.append("</p>");
return htmlStrBuild.toString();
}
Maven导入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.2.0</version>
</dependency>
指定字段(Integer)转文本渲染
- 配置
Converter文件
public class TypeConverter implements Converter<Integer> {
@Override
public Class supportJavaTypeKey() {
return null;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return null;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
return null;
}
@Override
public CellData convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
if (integer.equals(1)) {
return new CellData("安卓");
} else {
return new CellData("苹果");
}
}
}
- 再对应字段配置转换
@ExcelProperty(value = "充值端口", converter = TypeConverter.class)
private Integer sysType;
设置Strategy 属性
注解使用
- 设置列宽@ColumnWidth
@ColumnWidth(value=40)
private String name;
- 设置列属性。@ContentStyle
- 设置边框
- borderLeft
- borderRight
- borderTop
- borderBottom
- 设置对齐方式
- horizontalAlignment(水平对齐)
- verticalAlignment(垂直对齐)
- 设置自动换行
- wrapped。默认不换行
- 设置边框
设置列宽度
AbstractColumnWidthStyleStrategy
public class WithdrawExcelStrategy extends AbstractColumnWidthStyleStrategy {
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<CellData> list, Cell cell, Head head, Integer integer, Boolean aBoolean) {
Sheet sheet = writeSheetHolder.getSheet();
sheet.setColumnWidth(cell.getColumnIndex(), 10440);
}
}
列宽计算逻辑 :
- 单位是1/256个字符宽度 ,所以代码中需要乘以256,且两个参数都必须为整数。
- 正常的默认列宽是 (33英寸)
33*256。但因为字体大小、单元格边框等占用额外的像素,所以实际的列宽比设置的小0.78英寸。 - 举个例子,加入我们想要设置40英寸,那么就是
40.78*256,但是因为参数要求为整数,那么就是40.78*256=10439.68,向上取整,结果为10440。 - 最后。0.78是不同版本的误差值,这个可以根据个人情况决定误差允许范围。
前言
之前我们利用 jackson 全局配置,解决了我们程序中的 Long类型精度丢失问题和 LocalDateTime和Date类型的时间格式化问题。假设现在有这么一个需求。
1分钟内:刚刚
60分钟内: 40分钟前
24小时内: 15小时前
超过24小时:昨天22:42
超过24小时且跨过发布日期2天及以上:8-3 22:42
跨过发布日期年份: 2022-8-5 22:42
要求时间格式按照这样的形势返回。这时我们可以在每个方法返回前自己写格式化,处理完后再返回。那有什么更好的方式呢?这里我们可以采用自定义序列化类来实现。
jackson 提供的类
- JsonSerializer
。序列化类,T为传入的类型。 - JsonDeserializer
。反序列化类,T为处理完后传入系统的类型。
JsonSerializer
来看个简单的例子,这个例子将 LocalDateTime格式化后,通过JsonGenerator.writeString转字符串传输出去。
public class LocalDateTimeSerialize extends JsonSerializer<LocalDateTime> {
private static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-M-d HH:mm");
@Override
public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(localDateTime.format(format));
}
}
JsonDeserializer
来看个简单的例子,这个例子将 LocalDateTime格式化后,通过JsonParser.writeString转字符串传输出去。
public class LocalDateTimeDeserialize extends JsonDeserializer<LocalDateTime> {
private static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-M-d HH:mm");
public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String date = jp.getText();
try {
return format.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
如何使用
现在序列化类和反序列化类我们都有了,那么我们应该如何使用呢?
注解方式使用
在对应的类型上添加类注解。
- @JsonDeserialize
- @JsonSerialize
@Data
public class Message{
@JsonDeserialize(using = LocalDateTimeDeserialize.class)
@JsonSerialize(using = LocalDateTimeSerialize.class)
private LocalDateTime createTime;
}
全局注册
@JsonComponent
public class JacksonConfig {
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
// 简单类型转换
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
JavaTimeModule javaTimeModule = new JavaTimeModule();
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerialize());
module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserialize());
objectMapper.registerModules(module, javaTimeModule);
// 不序列化为null
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
}
一些实现的自定义序列化类
将LocalDateTime转化成相对时间
需求:
1分钟内:刚刚
60分钟内: 40分钟前
24小时内: 15小时前
超过24小时:昨天22:42
超过24小时且跨过发布日期2天及以上:8-3 22:42
跨过发布日期年份: 2022-8-5 22:42
实现:
/**
* 时间序列化注解
* 使用方法: @JsonSerialize(using = LocalDateTimeSerialize.class)
*/
public class LocalDateTimeSerialize extends JsonSerializer<LocalDateTime> {
private static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-M-d HH:mm");
private static final DateTimeFormatter format2 = DateTimeFormatter.ofPattern("M-d HH:mm");
private static final DateTimeFormatter format3 = DateTimeFormatter.ofPattern("昨天HH:mm");
/**
* 分钟内:刚刚
* 60分钟内: 40分钟前
* 24小时内: 15小时前
* 超过24小时:昨天22:42
* 超过24小时且跨过发布日期2天及以上:8-3 22:42
* 跨过发布日期年份: 2022-8-5 22:42
*/
@Override
public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
LocalDateTime now = LocalDateTime.now();
if (localDateTime.getYear() < now.getYear()) {
jsonGenerator.writeString(localDateTime.format(format));
return;
}
Duration duration = Duration.between(localDateTime, now);
if (duration.getSeconds() <= 60) {// 小于60s
jsonGenerator.writeString("刚刚");
} else if (duration.toMinutes() <= 60) {//小于一小时
jsonGenerator.writeString(duration.toMinutes() + "分钟前");
} else if (duration.toHours() < 24) {//小于24小时
jsonGenerator.writeString(duration.toHours() + "小时前");
} else if (duration.toDays() == 1) {
jsonGenerator.writeString(localDateTime.format(format3));
} else if (duration.toDays() >= 2) {
jsonGenerator.writeString(localDateTime.format(format2));
}else{
jsonGenerator.writeString(localDateTime.format(format2));
}
}
}
前言
最近发现一个问题,就是jfreechart在linux环境下,图表中文会出现乱码,然而poi的中文不会。
然后通过百度成功搜索到了一堆没用的文章。于是中和了一下这些文章,成功解决了这个问题。
确认并思考问题
因为都是说是linux环境下的字体文件,相比于window文件下的字体文件要少。导致中文的字体乱码。
那么我们就先查找一下linux下的字体库和window下的字体库
window 字体库:(C:\Windows\Fonts)
linux 字体库:
%JAVA_HOME%/jre/lib/fonts/
/usr/share/fonts/
从这里看出我们所需要的字体库远远不够,所以把window下我们需要的字体库添加到linux。
解决方案
-
我们进入到window 字体库:(C:\Windows\Fonts)下,将需要用到的字体文件拷贝到linux服务器,这里我们用到了三个字体文件(simsunb.ttf,simhei.ttf,simsun.ttc)
-
上传到jdk字体库中%JAVA_HOME%/jre/lib/fonts/。如下图
-
更新%JAVA_HOME%/jre/lib/fonts/fonts.dir文件
ttmkfdir -o fonts.dir
- 在%JAVA_HOME%/jre/lib/fonts执行以下命令,用于创建文件夹。
mkdir fallback
cd fallback
- 将字体文件拷贝到当前文件夹。
- 执行以下命令,生成font相关文件。并追加到外层目录的fonts.dir文件。
mkfontscale && mkfontdir
cat fonts.scale >> ../fonts.dir
- 进入/usr/share/fonts执行以下命令,创建文件夹。
cd /usr/share/fonts
mkdir zh_CN
cd zh_CN
mkdir TrueType
- 进入/usr/share/fonts/zh_CN/TrueType文件夹,上传字体文件。
- 执行下面操作指令,更新字体缓存。请将jdk路径换成自己的。
fc-cache /usr/share/fonts/zh_CN/TrueType/
fc-cache /usr/local/java/jdk1.8.0_261/jre/lib/fonts/
fc-cache /usr/local/java/jdk1.8.0_261/jre/lib/fonts/fallback/
- 重启java服务进程。 有的教程说是一定要重启机器。但是成本太高了,所以经过不断尝试最终通过这样的方式解决了问题。
引用
Linux下的JFreeChart出现中文乱码‘口口‘终极解决方案(亲测有效)
最后
老规矩写的不清晰的地方,可以指出,我会不定时更新和修改。一起加油.
所有的伟大,都来自于一个勇敢的开始。
前言
在Maven多模块的时候,管理依赖关系是非常重要的,各种依赖包冲突,查询问题起来非常复杂,于是就用到了
示例说明
在父模块中:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
</dependencies>
</dependencyManagement>
那么在子模块中只需要
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
说明
使用dependencyManagement可以统一管理项目的版本号,确保应用的各个项目的依赖和版本一致,不用每个模块项目都弄一个版本号,不利于管理,当需要变更版本号的时候只需要在父类容器里更新,不需要任何一个子项目的修改;如果某个子项目需要另外一个特殊的版本号时,只需要在自己的模块dependencies中声明一个版本号即可。子类就会使用子类声明的版本号,不继承于父类版本号。
与dependencies区别
- Dependencies相对于dependencyManagement,所有生命在dependencies里的依赖都会自动引入,并默认被所有的子项目继承。
- dependencyManagement里只是声明依赖,并不自动实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom;另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。
使用案例
spring-boot-dependencies 和 spring-boot-starter-parent。
我们在日常开发中经常使用的spring-boot-starter-parent(继承了spring-boot-dependencies)里面其实就提供了版本控制,所以我们在项目的pom文件中的 dependencies 中引入一些库时,可以不需要填入版本,会自动去使用 spring-boot-dependencies 中规定的版本。
前言
在很多的springboot项目中,我们都能看到pom中,有类似这样的一段代码:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
</parent>
因为我这里用的版本是2.3.5.RELEASE,可能会存在一定的出入。
那么这一段有什么作用呢?让我们通过查看对应版本的pom文件内容,分析一下看看。
功能
这里因为篇幅原因,我只截取关键的代码。
<!-- spring-boot-starter-parent-2.3.5.RELEASE.pom -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.5.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<resource.delimiter>@</resource.delimiter>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
<excludes>
<exclude>**/application*.yml</exclude>
<exclude>**/application*.yaml</exclude>
<exclude>**/application*.properties</exclude>
</excludes>
</resource>
</resources>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<classesDirectory>${project.build.outputDirectory}</classesDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>${start-class}</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>${start-class}</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<delimiters>
<delimiter>${resource.delimiter}</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
- 指定jdk版本为1.8
- 指定字符集为UTF-8
- 继承了spring-boot-dependencies.2.3.5.RELEASE。
- 针对以yml、yaml、properties结尾的文件资源过滤,包括以profile定义的不同环境的文件。
- 自动化的插件配置,
- 资源插件:maven-resources-plugin(处理资源文件到输出目录)
- 测试插件:maven-failsafe-plugin
- 打包插件:
- maven-jar-plugin(打包jar)
- maven-war-plugin(打包war)
- 依赖版本的管理。是由spring官方测试过后,较为兼容的版本,可以在spring-boot-dependencies.2.3.5.RELEASE中查看到。
spring-boot-dependencies中是通过 dependencyManagement 和 pluginManagement 对版本进行管理的。 对 dependencies 和 dependencyManagement 差别可以看一下这个文章
不继承spring-boot-parent
那么不使用parent会有什么后果呢?显而易见的,就是上面的parent提供的功能都要自己实现
基础配置
<properties>
<java.version>1.8</java.version>
<resource.delimiter>@</resource.delimiter>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
需要自己手动指定字符集和编译版本。
版本依赖
因为没有地方对依赖版本进行了控制,所以我们所有引入的依赖都需要自己填入版本。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
</dependencies>
这时项目在本地能够正常运行,因为依赖的配置都能获取到,但是无法打包成jar或者war包。
插件配置
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>${start-class}</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>${start-class}</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
我们就需要自己配置对应的插件和版本,像是打包war包所需要的web.xml文件,指定主类等,都需要手动配置。当然我们也可以借助其它工具来打包。
直接继承spring-boot-dependencies
实际上spring-boot-dependencies主要的功能就是提供了一个对依赖版本的管理。让我们对版本的控制可以简化,因为是由spring官方测试过的稳定兼容版本。然后我们自己还是需要做一个类似 spring-boot-parent 的pom文件,因为这是我们实际依赖的项目插件和配置。
说简单点了,实际就是dependencyManagement和dependencies的关系。
总结
这主要是springboot的主要理念:起步依赖、简化开发。我们只需要简单的通过继承spring-boot-parent,就能快速的解决编译、资源文件输出、配置文件、打包、版本依赖等管理,而不需要繁杂的配置。
maven 安装和配置
这里使用版本是 v3.9.6。
下载地址:https://maven.apache.org/download.cgi
- 解压到本地文件夹下
- 打开
config\settings.xml - 配置本地存储位置
<localRepository>D:/dev/repositories/maven</localRepository> - 配置阿里云镜像仓库
<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> - 私有仓库配置(带密码)
<servers> <server> <id>nexus-releases</id> <username>admin</username> <password>xxx</password> </server> </servers> - 配置java版本(以1.8为例子)
<profile> <id>JDK-1.8</id> <activation> <activeByDefault>true</activeByDefault> <jdk>1.8</jdk> </activation> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion> </properties> </profile> - 配置环境变量
- MAVEN_HOME。D:\dev\apache-maven-3.9.6
- PATH。添加 %MAVEN_HOME%\bin
多模块
packaging
在当前分布式和微服务日益发展壮大的背景下,我们需要更了解关于maven配置的细节,以及多模块下需要注意的地方。
目前 packaging 具有以下3中配置:
pom jar war
pom
在多模块项目中,我们会把某一类模块(例如 common 模块)聚合所有通用的模块,例如log、upload等,但时本身common模块不参与打包使用,只是作为一个聚合管理,那么这时我们可以将其设置为
<packagin>pom</packagin>
为什么一定要有一个父类的POM呢?大致有以下几点好处:
- 可以通过
Modules标签管理子模块的编译顺序(Maven 本身采用最短路径原则)。 - 将子模块的公共依赖包和版本交由父模块进行管理
- groupId\artifactid\version可以从父类获取,减少子模块POM的复杂性。
jar
当没有配置 packaging 时,默认值为jar,即采用jar方式打包.
这种打包方式意味着在maven build时会将这个项目中的所有java文件都进行编译形成.class文件,且按照原来的java文件层级结构在target目录下放置,最终压缩为一个jar文件。
war
war包与jar包非常相似,同样是编译后的.class文件按层级结构形成文件树后打包形成的压缩包。不同的是,它会将项目中依赖的所有jar包都放在WEB-INF/lib这个文件夹下。
可想而知,war包非常适合部署时使用,不再需要下载其他的依赖包,能够使用户拿到war包直接使用,因此它经常使用于微服务项目群中的入口项目的pom配置中。
异常记录
'packaging' with value 'jar' is invalid. aggregator projects require 'pom'
需要将父模块的packaging设置为pom即可解决
Mybatis 拦截器
核心组件
- Configuration:初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中。
- SqlSessionFactory:SqlSession工厂。
- SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
- Executor: MyBatis的执行器,用于执行增删改查操作。负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,它还处理二级缓存的操作。
- StatementHandler:MyBatis直接在数据库执行SQL脚本的对象。另外它也实现了MyBatis的一级缓存。
- ParameterHandler:负责将用户传递的参数转换成JDBC Statement所需要的参数。是MyBatis实现SQL入参设置的对象。
- ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。是MyBatis把ResultSet集合映射成POJO的接口对象。
- TypeHandler:负责Java数据类型和JDBC数据类型之间的映射和转换。
- MappedStatement:MappedStatement维护了一条<select|update|delete|insert>节点的封装。
- SqlSource :负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
- BoundSql:表示动态生成的SQL语句以及相应的参数信息。
生命周期
Mybatis 插件原理: 使用动态代理对以下四个目标对象进行包装,然后再有针对性的进行拦截,其生命周期分为包装和拦截。
- ParameterHandler:处理SQL的参数对象
- ResultSetHandler:处理SQL的返回结果集
- StatementHandler:数据库的处理对象,用于执行SQL语句
- Executor:MyBatis的执行器,用于执行增删改查操作
Mybatis 插件的周期:
- 通过
setProperties将配置好的Properties设置到插件上使用, - 通过
plugin.wrap(target,this)对插件对象进行拦截后,生成代理对象返回。 - 通过
intercept方法对代理 。注意:再上面四大对象创建的时候,对象都不是直接返回的,而且会生成他的代理,然后返回其代理对象。
graph TD;
User --> SqlSession
SqlSession --> User
SqlSession --> Executor
Executor --> SqlSession
Executor --> ParameterHandler
ParameterHandler --> StatementHandler
StatementHandler --> ResultSetHandler
ResultSetHandler --> Executor
如何使用
自定义 Mybatis 的拦截器步骤如下:
- 定义一个实现了
Interceptor接口的拦截器类,并实现其方法. - 添加
@Interceptors和@Signature注解,标记需要拦截的对象和方法,以及传入的方法参数 - 配置插件
- 在
Mybatis的全局配置XML 中 配置插件Plugin - 对于去XML文件的
SpringBoot项目,使用@Component注册;或者使用SqlSessionFactory.addInterceptor注入拦截器
- 在
Interceptor
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
}
}
- Object intercept(Invocation invocation) throws Throwable;
- 实现拦截的地方
- 通过
invocation.proceed()显式推进责任链的执行。
- Object plugin(Object target) ;
- 生成当前拦截器对该目标对象的代理。
- void setProperties(Properties properties)
- 用于设置拦截器的自定义参数。
此外对于自定义的 Interceptor 有两个重要的注解:
@Intercepts,用于表明当前对象是一个Interceptor。其值是一个@Signature数组,用于标识需要拦截的方法。@Signature,用于表明要拦截的接口、方法以及对应的参数类型。
下面是一个官方示例:
// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
执行过程
拦截器注入的方式
Mybatis
- 使用 @Component 注入 SpringBoot 容器。但无法控制执行顺序
@Component
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
public class UpdateInterceptor implements Interceptor {
}
- 使用 xml 配置。mybatis-config.xml
<plugins>
<plugin interceptor="UpdateInterceptor">
<property name="args1" value="参数示例"/>
</plugin>
</plugins>
- 使用
SqlSessionFactory。需要注意的是,这是倒序执行,后加入的先执行
@Resource
private List<SqlSessionFactory> sqlSessionFactories;
@PostConstruct
public void addInterceptor() {
sqlSessionFactories.forEach(item -> {
item.getConfiguration().addInterceptor(new DataScopeInterceptor());
item.getConfiguration().addInterceptor(new DataScopeInterceptor2());
});
}
Mybatis-Plus
- 使用
ConfigurationCustomizer设置执行顺序,需要注意的是倒序执行,后加入的先执行
@Configuration
public class MybatisConfig {
// 注册插件方式
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
//插件拦截链采用了责任链模式,执行顺序和加入连接链的顺序有关
configuration.addInterceptor(new UpdateInterceptor());
};
}
}
- 使用设置
Mybatis-Plus下的Innerterceptor
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 数据权限过滤
interceptor.addInnerInterceptor(new DataScopeInnerInterceptor());
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
拦截器使用案例
拦截填充创建时间和更新时间
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
public class UpdateInterceptor implements Interceptor {
@Autowired
private CommonAuthApi commonAuthApi;
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (invocation.getTarget() instanceof Executor) {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object params = invocation.getArgs()[1];
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
operateTime(params, sqlCommandType);
}
return invocation.proceed();
}
private void operateTime(Object params, SqlCommandType sqlCommandType) {
Map<String, Object> userInfo = commonAuthApi.queryUserInfo();
if (sqlCommandType == SqlCommandType.UPDATE || sqlCommandType == SqlCommandType.DELETE) {
if (params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
LocalDateTime now = LocalDateTime.now();
String username = userInfo.get("username").toString();
baseEntity.setUpdateTime(now);
baseEntity.setUpdateBy(username);
} else if (params instanceof Map) {
Map baseMap = (Map) params;
LocalDateTime now = LocalDateTime.now();
String username = userInfo.get("username").toString();
baseMap.put("updateTime", now);
baseMap.put("updateBy", username);
}
} else if (sqlCommandType == SqlCommandType.INSERT) {
if (params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
LocalDateTime now = LocalDateTime.now();
String username = userInfo.get("username").toString();
baseEntity.setCreateTime(now);
baseEntity.setCreateBy(username);
baseEntity.setUpdateTime(now);
baseEntity.setUpdateBy(username);
} else if (params instanceof Map) {
Map baseMap = (Map) params;
LocalDateTime now = LocalDateTime.now();
String username = userInfo.get("username").toString();
baseMap.put("createTime", now);
baseMap.put("createBy", username);
baseMap.put("updateTime", now);
baseMap.put("updateBy", username);
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
前言
我们都知道 Mybatis 底层封装了 JDBC API ,Mybatis 的工作原理及核心流程与 JDBC 的使用有很多相似之处,基于 JDBC 可以方便我们更加深入学习 Mybatis
JDBC 和 Mybatis
核心对象
我们先看 JDBC 我们会用到的5个核心对象:
DriverManager。驱动管理类,- 注册驱动
- 获取连接
Connection。数据库连接对象- 获取执行
SQL的对象 - 事务管理
- 获取执行
Statement。用来执行Sql语句,但是会存在Sql注入的问题PrepareStatement。用来执行Sql语句,但是他的出现解决了Sql注入的问题并提高了性能,一次编译,到处使用。其底层是对特殊字符做了转义。ResultSet。结果集对象,封装了Sql查询语句的结果并提供了操作查询结果数据的方法。
而 Mybatis 也有四大核心对象:
SqlSession。该对象包含了所有执行SQL语句的所有方法。Executor。根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。MappedStatement。该对象是对映射SQL的封装, 用于存储要映射的SQL语句的id、参数等信息。ResultHandler。用于对返回的结果进行处理,最终得到自己想要的数据格式。
额外说明
-
在JDBC中,Connection不直接执行SQL方法,而是利用Statement或者PrepareStatement来执行方法。在使用JDBC建立了连接之后,可以使用Connection接口的createStatement()方法来获取Statement对象,也可以调用prepareStatement()方法获得PrepareStatement对象,通过executeUpdate()方法来执行SQL语句。而在MyBatis中,SqlSession对象包含了执行SQL语句的所有方法,但是它是委托Executor执行的。从某种意义上来看,MyBatis里面的SqlSession类似于JDBC中的Connection,他们都是委托给其他类去执行。
-
虽然SqlSession对象包含了执行SQL语句的所有方法,但是它同样包括了:
<T> T getMapper(Class<T> type);
所以SqlSession也可以委托给映射器来执行数据的增删改查操作。如下代码所示:
// 获得mapper接口的代理对象
PersonMapper pm = session.getMapper(PersonMapper.class);
// 直接调用接口的方法,查询id为1的Peson数据
Person p2 = pm.selectPersonById(1);
这上面来看,SqlSession是不是也类似于JDBC中的Connection呢?
核心流程和工作原理

- 读取
MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。 - 加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件
mybatis-config.xml中加载。mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。 - 构造会话工厂。通过MyBatis的环境配置信息构建会话工厂
SqlSessionFactory。 - 创建会话对象。由
SqlSessionFactory创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。 Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。- 输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。
- 输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。
我们知道 Mybatis Interceptor 有四大核心对象。其实可以分别对应上上面核心流程的步骤5-8。
-
Executor:MyBatis的执行器,用于执行增删改查操作
-
StatementHandler:数据库的处理对象,用于执行SQL语句
-
ParameterHandler:处理SQL的参数对象
-
ResultSetHandler:处理SQL的返回结果集
参考链接
关于使用Left join 和 count时,无数据时,list 返回时出现一条空数据
- 创建msyql数据表
create table `tb_group`(
id bigint(20) unsigned not null AUTO_INCREMENT,
`name` varchar(255) CHARACTER set utf8mb4 COLLATE utf8mb4_bin DEfault Null comment "组名称",
primary key (`id`) using btree
)ENGINE=Innodb CHARSET=utf8mb4 COMMENT="分组表";
create table `tb_detail`(
id bigint(20) unsigned not null AUTO_INCREMENT,
group_id bigint(20) unsigned not null,
`name` varchar(255) CHARACTER set utf8mb4 COLLATE utf8mb4_bin DEfault Null comment "明细名称",
primary key (`id`) using btree
)ENGINE=Innodb CHARSET=utf8mb4 COMMENT="明细表";
- 插入数据
INSERT INTO `tb_group` (`id`, `name`) VALUES (1, '1');
INSERT INTO `tb_group` (`id`, `name`) VALUES (2, '2');
INSERT INTO `tb_detail` (`id`, `group_id`, `name`) VALUES (1, 1, '1-1');
INSERT INTO `tb_detail` (`id`, `group_id`, `name`) VALUES (2, 1, '1-2');
INSERT INTO `tb_detail` (`id`, `group_id`, `name`) VALUES (3, 2, '1-2');
- 实体类
/**
* 组实体类
*/
@Data
public class TbGroup{
private Long id;
private String name;
}
/**
* 明细实体类
*/
@Data
public class TbDetail{
private Long id;
private Long groupId;
private String name;
}
/**
* 组统计实体类
*/
@Data
public class TbGroupAndDetailCountVo{
private Long id;
private String name;
private Integer count;
}
- 编写mapper.xml
<select id="selectGroupAndDetailCount" result="TbGroupAndDetailCountVo">
select
tb_group.id,
tb_group.name,
count(1) `count`
from
tb_group left join tb_detail on tb_group.id = tb_detail.group_id
where tb_group.id =3
</select>
- 编写mapper.dao
List<TbGroupAndDetailCountVo> selectGroupAndDetailCount()
- 我们将下面的sql在mysql里面直接执行,都会看见同样的输出结果。

- 那通过mybatis映射后呢?

可以看见会有一条空数据count 0 。那么如何解决这个问题呢?
只需要在sql里面增加一句 group by tb_group.id即可。
select
tb_group.id,
tb_group.name,
count(1) `count`
from
tb_group left join tb_detail on tb_group.id = tb_detail.group_id
where tb_group.id =3
group by tb_group.id

Mybatis-plus下Mybatis 拦截器,更改sql后,无生效问题。
因为在项目里面集成了 mybatis-plus 但是我又不太想使用 mybatis-plus 的拦截器方法 InnerInterceptor ,偏向使用 Mybatis 的 Interceptor 。(完整代码可以参见 GIT项目)
mybatis-plus 的使用方法
- 数据域拦截器
public class DataScopeInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {
// ...
}
-
拦截器使用
-
方式一:
MybatisPlusInterceptor配置拦截器,可以控制优先顺序@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 数据权限过滤 interceptor.addInnerInterceptor(new DataScopeInnerInterceptor()); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } -
方式二:直接注入
Bean@Resource private DataScopeInterceptor dataScopeInterceptor; @Bean public PaginationInnerInterceptor paginationInterceptor() { return new PaginationInnerInterceptor(); } @Bean public OptimisticLockerInnerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInnerInterceptor(); } @Component public class DataScopeInnerInterceptor extends JsqlParserSupport implements InnerInterceptor { // ... }
-
问题原因
原先代码:通过 @Component 注入我们自己的拦截器,但是更改SQL后没有生效。
DataScopeInterceptor 没有注入成功 AND create_by = 17779983758 方法
2023-03-10 16:46:51.678 INFO 28452 --- [ XNIO-1 task-29] c.a.h.m.a.d.i.DataScopeInnerInterceptor : MappedStatement => org.apache.ibatis.mapping.MappedStatement@564722db,BoundSQL => org.apache.ibatis.mapping.BoundSql@7247474d,SQL => SELECT id FROM kb_goods WHERE del_flag = 0 AND (usable_flag = ?) AND create_by = 17779983758
2023-03-10 16:46:51.679 INFO 28452 --- [ XNIO-1 task-29] c.a.h.m.a.d.i.DataScopeInterceptor : MappedStatement => org.apache.ibatis.mapping.MappedStatement@564722db,BoundSQL => org.apache.ibatis.mapping.BoundSql@7c80a775,SQL => SELECT id FROM kb_goods WHERE del_flag = 0 AND (usable_flag = ?)
在我们使用 Mybatis-Plus 的注入方式时,会优先走 MybatisPlusInterceptor 的拦截器。我们看一下打印日志。虽然使用的都是同一个 MappedStatement,但是实际修改的 BoundSql 不是同一个实体,但是修改了后不起作用。
而且 MybatisPlusInterceptor 的优先级是高于我们自定义的 Interceptor 拦截器。
当我们在自定义的 Mybatis拦截器 里面更改了 SQL语句。但是 Mybatis-Plus 操作后,没有发生变化,因为MybatisPlus 本身的拦截器优先注入执行了,导致自定义的拦截器修改 BoundSQL 后没有生效。
解决方法
- 使用
Mybatis-plus提供的ConfigurationCustomizer类,提高自定义拦截器的执行顺序。
@Configuration
public class MybatisConfig {
// 注册插件方式
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
//插件拦截链采用了责任链模式,执行顺序和加入连接链的顺序有关
configuration.addInterceptor(new UpdateInterceptor());
};
}
}
- 使用
SqlSessionFactory手动更改执行顺序。
@Resource
private List<SqlSessionFactory> sqlSessionFactories;
@PostConstruct
public void addInterceptor() {
// MybatisPlus 拦截器的初始化
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new DataScopeInnerInterceptor());
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
sqlSessionFactories.forEach(item -> {
item.getConfiguration().addInterceptor(interceptor);
// 自定义拦截器,先执行的后加入
item.getConfiguration().addInterceptor(new DataScopeInterceptor());
});
}
前言
该文章基于我之前写的另一篇关于Druid多数据源的配置,若没有使用多数据,可以借鉴一下,主要是思路是创建新的连接池,然后替换掉原先的连接池,因为 Druid 初始化后不能更换连接再次初始化。
实现代码
@Slf4j
@RefreshScope
@Configuration
public class NacosListener implements InitializingBean {
@Value("${smilex.nacos.listener.dataId}")
private String dataId;
@Resource
private DataSourceProperties dataSourceProperties;
@Autowired
private NacosConfigManager nacosConfigManager;
@Resource
private DynamicDataSource dynamicDataSource;
@Override
public void afterPropertiesSet() throws Exception {
NacosConfigProperties nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
log.info(nacosConfigProperties.toString());
ConfigService configService = nacosConfigManager.getConfigService();
NacosConfigProperties.Config config = nacosConfigProperties.getSharedConfigs().get(0);
configService.addListener(dataId, config.getGroup(), new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
log.info(configInfo);
Yaml yaml = new Yaml();
JSONObject infoJson = new JSONObject(yaml.load(configInfo));
refreshDataSource(infoJson);
}
});
}
/**
* 刷新数据库
*/
private void refreshDataSource(JSONObject infoJson) {
JSONObject druid = getProperties(infoJson, "spring.datasource.druid");
log.info(druid.toJSONString());
if (druid != null) {
DataSourceProperties dataSourceProperties = druid.toJavaObject(DataSourceProperties.class);
// DataSource dataSource = (DataSource) dynamicDataSource.getDataSource(DynamicDataSourceConfig.MASTER);
// DataSourceFactory.createDataSource(dataSource, dataSourceProperties);
dynamicDataSource.replaceDataSource(DynamicDataSourceConfig.MASTER, dataSourceProperties);
}
}
private JSONObject getProperties(JSONObject infoJson, String properties) {
if (StringUtils.isNotBlank(properties)) {
return loopProperties(infoJson, properties.split("\\."), 0);
}
return null;
}
private JSONObject loopProperties(JSONObject infoJson, String[] properties, int index) {
if (infoJson.containsKey(properties[index])) {
if (index < properties.length - 1) {
return loopProperties(infoJson.getJSONObject(properties[index]), properties, index + 1);
} else {
return infoJson.getJSONObject(properties[index]);
}
}
return null;
}
}
基于版本 nacos server 2.2.2
什么是命名空间
namespace(命名空间)是nacos针对于企业级开发设计用来针对于不同环境的区分,比如企业开发时有测试环境、生产环境,因此为了保证不同环境配置实现隔离提出了namespace的概念。-
-
默认在
nacos中存在一个public命名空间,所有配置在没有指定命名空间时都在这个命名空间中获取配置。 -
在实际开发时可以针对于不同环境创建不同的
namespace。 -
默认空间不能删除
基本使用
新增命名空间


**“命名空间ID”**如果在新增时没有填写的话,则Nacos服务端会自动随机产生一个命名空间ID。
每个命名空间都有一个唯一ID,这个ID是读取配置时指定空间的唯一标识。
使用命名空间
nacos 与 Spring Cloud 整合时,其配置文件中命令空间ID的使用配置如下:
spring:
cloud:
nacos:
discovery:
# 命名空间
namespace: dev
config:
# 命名空间
namespace: dev
注意: namespace字段的值必须是命名空间ID(可以进行自定义名称),不能是命名空间名称
前言
nacos 中主要使用两种数据源:内置和外置。
有些时候我们使用内置数据源时,并不方便做数据查看和统一配置。所以我们会需要使用外置数据源进行配置
外置数据源
目前仅支持 Mysql
- 安装数据库,版本要求:5.6.5+
- 初始化mysql数据库,数据库初始化文件:
mysql-schema.sql - 3.修改
conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
### Deprecated configuration property, it is recommended to use `spring.sql.init.platform` replaced.
#spring.datasource.platform=mysql
spring.sql.init.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/smilex-nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
关于配置动态更新不起效果问题
spring:
cloud:
nacos:
discovery:
# 命名空间
namespace: dev
# 注册中鼎
server-addr: 127.0.0.1:8848
config:
# 命名空间
namespace: dev
# 配置中心地址
server-addr: 127.0.0.1:8848
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
# 换成这种就可以拉取,但是动态更新会异常
# - ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
关于jmenv.tbsite.net错误
-
问题描述:nacos未配置cluster.conf,直接执行 ./startup ,表示使用集群模式启动,导致异常
-
解决办法:
- 方法1:使用单机模式启动 ,执行命令
./startup -m standalone。 - 方法2:创建
cluster.conf(可不配置)。
- 方法1:使用单机模式启动 ,执行命令
-
关键错误信息:
[com.alibaba.nacos.core.cluster.ServerMemberManager]: Constructor threw exception; nested exception is ErrCode:500, ErrMsg:jmenv.tbsite.net -
完整错误信息:
"nacos is starting with cluster" ,--. ,--.'| ,--,: : | Nacos 2.2.2 ,`--.'`| ' : ,---. Running in cluster mode, All function modules | : : | | ' ,'\ .--.--. Port: 8848 : | \ | : ,--.--. ,---. / / | / / ' Pid: 6824 | : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.56.1:8848/nacos/index.html ' ' ;. ;.--. .-. | / / '' | |: :| : ;_ | | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io ' : | ; .' ," .--.; |' ; :__| : | `----. \ | | '`--' / / ,. |' | '.'|\ \ / / /`--' / ' : | ; : .' \ : : `----' '--'. / ; |.' | , .-./\ \ / `--'---' '---' `--`---' `----' 2024-01-09 11:24:11,379 INFO The server IP list of Nacos is [] 2024-01-09 11:24:12,394 INFO Nacos is starting... 2024-01-09 11:24:13,414 INFO Nacos is starting... 2024-01-09 11:24:14,421 INFO Nacos is starting... 2024-01-09 11:24:15,429 INFO Nacos is starting... 2024-01-09 11:24:16,435 INFO Nacos is starting... 2024-01-09 11:24:17,442 INFO Nacos is starting... 2024-01-09 11:24:18,449 INFO Nacos is starting... 2024-01-09 11:24:19,462 INFO Nacos is starting... 2024-01-09 11:24:20,463 INFO Nacos is starting... 2024-01-09 11:24:21,473 INFO Nacos is starting... 2024-01-09 11:24:21,799 INFO Nacos Log files: D:\dev\nacos-server-2.2.2\nacos\logs 2024-01-09 11:24:21,800 INFO Nacos Log files: D:\dev\nacos-server-2.2.2\nacos\conf 2024-01-09 11:24:21,800 INFO Nacos Log files: D:\dev\nacos-server-2.2.2\nacos\data 2024-01-09 11:24:21,803 ERROR Startup errors : org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:163) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:577) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:745) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:423) at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) at com.alibaba.nacos.Nacos.main(Nacos.java:35) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) at org.springframework.boot.loader.Launcher.launch(Launcher.java:108) at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) at org.springframework.boot.loader.PropertiesLauncher.main(PropertiesLauncher.java:467) Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:142) at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:104) at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:479) at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:211) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:182) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:160) ... 16 common frames omitted Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'distroFilterRegistration' defined in class path resource [com/alibaba/nacos/naming/web/NamingConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.web.servlet.FilterRegistrationBean]: Factory method 'distroFilterRegistration' threw exception; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'distroFilter': Unsatisfied dependency expressed through field 'distroMapper'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'distroMapper' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-naming-2.2.2.jar!/com/alibaba/nacos/naming/core/DistroMapper.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverMemberManager' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-core-2.2.2.jar!/com/alibaba/nacos/core/cluster/ServerMemberManager.class]: Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.alibaba.nacos.core.cluster.ServerMemberManager]: Constructor threw exception; nested exception is ErrCode:500, ErrMsg:jmenv.tbsite.net at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658) at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:486) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:212) at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:203) at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addServletContextInitializerBeans(ServletContextInitializerBeans.java:97) at org.springframework.boot.web.servlet.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:86) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:260) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:234) at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:53) at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5211) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1393) at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1383) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:916) at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:835) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1393) at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1383) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:916) at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:265) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) at org.apache.catalina.core.StandardService.startInternal(StandardService.java:430) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:930) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) at org.apache.catalina.startup.Tomcat.start(Tomcat.java:486) at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:123) ... 21 common frames omitted Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.web.servlet.FilterRegistrationBean]: Factory method 'distroFilterRegistration' threw exception; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'distroFilter': Unsatisfied dependency expressed through field 'distroMapper'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'distroMapper' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-naming-2.2.2.jar!/com/alibaba/nacos/naming/core/DistroMapper.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverMemberManager' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-core-2.2.2.jar!/com/alibaba/nacos/core/cluster/ServerMemberManager.class]: Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.alibaba.nacos.core.cluster.ServerMemberManager]: Constructor threw exception; nested exception is ErrCode:500, ErrMsg:jmenv.tbsite.net at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653) ... 61 common frames omitted Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'distroFilter': Unsatisfied dependency expressed through field 'distroMapper'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'distroMapper' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-naming-2.2.2.jar!/com/alibaba/nacos/naming/core/DistroMapper.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverMemberManager' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-core-2.2.2.jar!/com/alibaba/nacos/core/cluster/ServerMemberManager.class]: Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.alibaba.nacos.core.cluster.ServerMemberManager]: Constructor threw exception; nested exception is ErrCode:500, ErrMsg:jmenv.tbsite.net at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:660) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640) at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.resolveBeanReference(ConfigurationClassEnhancer.java:362) at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:334) at com.alibaba.nacos.naming.web.NamingConfig$$EnhancerBySpringCGLIB$$78c17639.distroFilter(<generated>) at com.alibaba.nacos.naming.web.NamingConfig.distroFilterRegistration(NamingConfig.java:44) at com.alibaba.nacos.naming.web.NamingConfig$$EnhancerBySpringCGLIB$$78c17639.CGLIB$distroFilterRegistration$3(<generated>) at com.alibaba.nacos.naming.web.NamingConfig$$EnhancerBySpringCGLIB$$78c17639$$FastClassBySpringCGLIB$$886156dc.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) at com.alibaba.nacos.naming.web.NamingConfig$$EnhancerBySpringCGLIB$$78c17639.distroFilterRegistration(<generated>) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ... 62 common frames omitted Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'distroMapper' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-naming-2.2.2.jar!/com/alibaba/nacos/naming/core/DistroMapper.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverMemberManager' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-core-2.2.2.jar!/com/alibaba/nacos/core/cluster/ServerMemberManager.class]: Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.alibaba.nacos.core.cluster.ServerMemberManager]: Constructor threw exception; nested exception is ErrCode:500, ErrMsg:jmenv.tbsite.net at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800) at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:657) ... 86 common frames omitted Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverMemberManager' defined in URL [jar:file:/D:/dev/nacos-server-2.2.2/nacos/target/nacos-server.jar!/BOOT-INF/lib/nacos-core-2.2.2.jar!/com/alibaba/nacos/core/cluster/ServerMemberManager.class]: Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.alibaba.nacos.core.cluster.ServerMemberManager]: Constructor threw exception; nested exception is ErrCode:500, ErrMsg:jmenv.tbsite.net at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:315) at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:296) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ... 99 common frames omitted Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.alibaba.nacos.core.cluster.ServerMemberManager]: Constructor threw exception; nested exception is ErrCode:500, ErrMsg:jmenv.tbsite.net at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:224) at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:117) at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:311) ... 113 common frames omitted Caused by: com.alibaba.nacos.api.exception.NacosException: java.net.UnknownHostException: jmenv.tbsite.net at com.alibaba.nacos.core.cluster.lookup.AddressServerMemberLookup.run(AddressServerMemberLookup.java:152) at com.alibaba.nacos.core.cluster.lookup.AddressServerMemberLookup.doStart(AddressServerMemberLookup.java:100) at com.alibaba.nacos.core.cluster.AbstractMemberLookup.start(AbstractMemberLookup.java:55) at com.alibaba.nacos.core.cluster.ServerMemberManager.initAndStartLookup(ServerMemberManager.java:224) at com.alibaba.nacos.core.cluster.ServerMemberManager.init(ServerMemberManager.java:171) at com.alibaba.nacos.core.cluster.ServerMemberManager.<init>(ServerMemberManager.java:152) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:211) ... 115 common frames omitted Caused by: java.net.UnknownHostException: jmenv.tbsite.net at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:184) at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) at java.net.Socket.connect(Socket.java:589) at sun.net.NetworkClient.doConnect(NetworkClient.java:175) at sun.net.www.http.HttpClient.openServer(HttpClient.java:463) at sun.net.www.http.HttpClient.openServer(HttpClient.java:558) at sun.net.www.http.HttpClient.<init>(HttpClient.java:242) at sun.net.www.http.HttpClient.New(HttpClient.java:339) at sun.net.www.http.HttpClient.New(HttpClient.java:357) at sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(HttpURLConnection.java:1220) at sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1156) at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1050) at sun.net.www.protocol.http.HttpURLConnection.connect(HttpURLConnection.java:984) at com.alibaba.nacos.common.http.client.request.JdkHttpClientRequest.execute(JdkHttpClientRequest.java:114) at com.alibaba.nacos.common.http.client.NacosRestTemplate.execute(NacosRestTemplate.java:482) at com.alibaba.nacos.common.http.client.NacosRestTemplate.get(NacosRestTemplate.java:72) at com.alibaba.nacos.core.cluster.lookup.AddressServerMemberLookup.syncFromAddressUrl(AddressServerMemberLookup.java:175) at com.alibaba.nacos.core.cluster.lookup.AddressServerMemberLookup.run(AddressServerMemberLookup.java:143) ... 125 common frames omitted 2024-01-09 11:24:21,806 WARN [WatchFileCenter] start close 2024-01-09 11:24:21,806 WARN [WatchFileCenter] start to shutdown this watcher which is watch : D:\dev\nacos-server-2.2.2\nacos\conf 2024-01-09 11:24:21,808 WARN [WatchFileCenter] already closed 2024-01-09 11:24:21,808 WARN [NotifyCenter] Start destroying Publisher 2024-01-09 11:24:21,809 WARN [NotifyCenter] Destruction of the end 2024-01-09 11:24:21,810 ERROR Nacos failed to start, please see D:\dev\nacos-server-2.2.2\nacos\logs\nacos.log for more details. 2024-01-09 11:24:21,876 WARN [ThreadPoolManager] Start destroying ThreadPool 2024-01-09 11:24:21,877 WARN [ThreadPoolManager] Destruction of the end
简介
java NIO中的Buffer用于和NIO通道进行交互。数据从通道中读取和写入到缓冲区。本质上是一块固定大小的内存区,其作用是是用来存储或传输使用。
java1.4之前,javaio最核心的点是在“数据流”
Buffer是什么
Buffer就是一个存储Byte基本类型的线性的有限序列。 就是说,他是一个固定大小的序列数组。
Buffer底层的本质是数组,我们以ByteBuffer为例,它的底层是:
final byte[] hb;
Buffer的重要属性
各属性关系: mark<=position<=limit=capacity
Position(位置)
- 当前操作的索引,会自动由相应的get()和put()函数更新。
- 默认为0
limit(限制)
- 表示不能读或写的第一个索引,即从这个索引开始,后续直到capacity都不能进行操作。
- 默认为缓冲区容量(capacity)。
capacity(容量)
- 表示缓冲区的容量,由构建时设置,不能小于0,小于0会抛出异常
mark(标记)
- 一个标记索引。默认值是-1
- 通过调用mark()来设定mark=position;调用reset()设定position=mark。
进阶操作
源代码:文件地址
创建Buffer
细分来说,创建Buffer可以使用四种方式
- 直接缓冲区(DirectByteBuffer)
- allocateDirect(int capacity)。
- 只有ByteBuffer具备。
- allocateDirect(int capacity)。
- 堆内存缓冲区(HeapBuffer)。
- java对象的底层存储单位是byte字节。但是因为java中还有int,double,short等基础类型,为了简化操作,nio中都是为他们实现了一套Buffer
- 操作方式
- allocate(int capacity)。
- wrap(
array) - wrap(
array,int offset,int length)
Buffer是一个接口,它下面有诸多实现,包括最基本的ByteBuffer和其他的基本类型封装的其他Buffer。
public class CreateBufferDemo {
public static void main(String[] args) {
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(8);
System.out.println(allocateDirect.hasArray() + "-" + allocateDirect);
ByteBuffer allocate = ByteBuffer.allocate(8);
System.out.println(allocate.hasArray() + "-" + allocate);
byte[] array = new byte[10];
ByteBuffer wrap = ByteBuffer.wrap(array);
System.out.println(wrap.hasArray() + "-" + wrap);
ByteBuffer wrap1 = ByteBuffer.wrap(array, 4, 3);
System.out.println(wrap1.hasArray() + "-" + wrap1);
}
}
让我们运行一下看看输出结果。
java.nio.DirectByteBuffer[pos=0 lim=8 cap=8]
java.nio.HeapByteBuffer[pos=0 lim=8 cap=8]
java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
java.nio.HeapByteBuffer[pos=4 lim=7 cap=10]
这里需要注意的有2点:
- 当我们使用wrap(
array,int offset,int length)进行创建Buffer时,offset表示的是偏移量即position,而limit=offset+length,所以我们要保证limit<=capacity,不然会抛出异常。 - 我们可以看到allocateDirect()生成的ByteBuffer是DirectByteBuffer,并且不为数组。这是为什么呢?因为DirectByteBuffer是通过unsafe向操作系统申请的空间,虽然操作结构上也是一致的,但是实际存储中,并非使用的是java内部的数组。而其它三个最终都是基于数组的。
读写数据
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.allocate(10);
System.out.println("create buffer:" + buffer);
buffer.put(new int[]{1, 2, 3, 4, 5});
System.out.println("put buffer after:" + buffer);
buffer.put(0, 6);
System.out.println("update buffer after:" + buffer);
buffer.flip();
System.out.println("flip buffer after:" + buffer);
printBuffer(buffer);
}
我们通过put写入数据,这时position会不断递增,这时我们可以通过flip切换读模式,即limit=position、position=0。
rewind(倒带)
rewind会将position赋值为0,并清空mark,但不会更改limit的值。
rewind buffer position:java.nio.HeapIntBuffer[pos=2 lim=5 cap=10]
rewind buffer position&mark:java.nio.HeapIntBuffer[pos=0 lim=5 cap=10]
由于rewind会将mark清空,所以不要在rewind()后执行reset操作,会抛出InvalidMarkException。
clear(清空)
clear会将buffer还原回默认状态,即position=0;limit=capacity;mark=-1;
compact(压缩)
有时候,我们会考虑将buffer的一部分数据释放,并重新填充。为了要实现这样的需求,我们会不断反复的读写数据,但是这样性能比较差,而buffer自带的compact会很好的解决这一点。
public class BufferDemo {
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.allocate(10);
System.out.println("create buffer:" + buffer);
buffer.put(new int[]{1, 2, 3, 4, 5});
System.out.println("put buffer after:" + buffer);
buffer.put(0, 6);
System.out.println("update buffer after:" + buffer);
buffer.flip();
System.out.println("flip buffer after:" + buffer);
printBuffer(buffer);
buffer.position(2);
System.out.println("compact buffer before:" + buffer);
buffer.compact();
System.out.println("compact buffer after:" + buffer);
printBuffer(buffer);
System.out.println("print buffer after:" + buffer);
buffer.position(0);
printBuffer(buffer);
System.out.println("print buffer after:" + buffer);
}
public static void printBuffer(IntBuffer buffer) {
System.out.print("buffer print:");
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
System.out.println();
}
}
代码输出:
create buffer:java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
put buffer after:java.nio.HeapIntBuffer[pos=5 lim=10 cap=10]
update buffer after:java.nio.HeapIntBuffer[pos=5 lim=10 cap=10]
flip buffer after:java.nio.HeapIntBuffer[pos=0 lim=5 cap=10]
buffer print:62345
compact buffer before:java.nio.HeapIntBuffer[pos=2 lim=5 cap=10]
compact buffer after:java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
buffer print:4500000
print buffer after:java.nio.HeapIntBuffer[pos=10 lim=10 cap=10]
buffer print:3454500000
print buffer after:java.nio.HeapIntBuffer[pos=10 lim=10 cap=10]
这里做了几件事情:
- 将position到limit的数据赋值到position为0的位置,即从buffer最前面开始覆盖,并丢弃了原先的数据。
- 将position设置为被复制的数据数量。即position=limit-position,也就是说这时position指向的是缓冲区最后一位保留数据后的位置。
- limit再次设置capacity,即整个buffer的大小,因此缓冲区可以再次填充数据。
可以使用这种类似先入先出(FIFO)队列的方式使用buffer。尽管不是非常高效的方法,但是compact对于使buffer与从端口中读取的数据包流的同步来说,还是一种较为便捷的方法。
duplicate(复制)
public class DuplicateBufferDemo {
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.allocate(10);
buffer.put(new int[]{0, 1, 2, 3, 4});
System.out.println("create buffer:" + buffer);
printBuffer(buffer, true);
IntBuffer duplicate1 = buffer.duplicate();
System.out.println("duplicate buffer:" + duplicate1);
printBuffer(duplicate1, true);
duplicate1.put(0, 7);
System.out.println("origin and duplicate buffer start=====");
printBuffer(buffer, true);
printBuffer(duplicate1, true);
System.out.println("origin and duplicate buffer end=====");
IntBuffer duplicate2 = buffer.asReadOnlyBuffer();
System.out.println("asReadOnlyBuffer buffer:" + duplicate2);
buffer.put(0, 8);
printBuffer(duplicate2, true);
try {
duplicate2.put(0, 6);
} catch (Exception e) {
System.err.println("asReadOnlyBuffer buffer update Exception:" + e.toString());
}
System.out.println("slice buffer before,origin buffer status:" + buffer);
buffer.position(3);
System.out.println("slice buffer before,origin buffer position 3 after:" + buffer);
IntBuffer slice = buffer.slice();
System.out.println("slice buffer:" + slice);
printBuffer(slice, false);
buffer.put(3, 13);
buffer.rewind();
slice.put(0, 23);
System.out.println("origin buffer:" + buffer);
printBuffer(buffer, false);
System.out.println("slice buffer:" + slice);
printBuffer(slice, true);
}
public static void printBuffer(IntBuffer buffer, boolean needFlip) {
if (needFlip) {
buffer.flip();
}
System.out.print("buffer print:");
while (buffer.hasRemaining()) {
System.out.print(buffer.get() + ",");
}
System.out.println();
}
}
输出结果:
create buffer:java.nio.HeapIntBuffer[pos=5 lim=10 cap=10]
buffer print:0,1,2,3,4,
duplicate buffer:java.nio.HeapIntBuffer[pos=5 lim=5 cap=10]
buffer print:0,1,2,3,4,
origin and duplicate buffer start=====
buffer print:7,1,2,3,4,
buffer print:7,1,2,3,4,
origin and duplicate buffer end=====
asReadOnlyBuffer buffer:java.nio.HeapIntBufferR[pos=5 lim=5 cap=10]
buffer print:8,1,2,3,4,
slice buffer before,origin buffer status:java.nio.HeapIntBuffer[pos=5 lim=5 cap=10]
slice buffer before,origin buffer position 3 after:java.nio.HeapIntBuffer[pos=3 lim=5 cap=10]
slice buffer:java.nio.HeapIntBuffer[pos=0 lim=2 cap=2]
buffer print:3,4,
origin buffer:java.nio.HeapIntBuffer[pos=0 lim=5 cap=10]
buffer print:8,1,2,23,4,
slice buffer:java.nio.HeapIntBuffer[pos=2 lim=2 cap=2]
buffer print:23,4,
asReadOnlyBuffer buffer update Exception:java.nio.ReadOnlyBufferException
buffer提供的复制有三种方式:
- duplicate()。拷贝原buffer的position、limit、mark和capacity,他是和原先的buffer是共享数据的。
- asReadOnlyBuffer()。
- 与duplicate类似,区别在于是只读的,并且也是与原buffer共享数据,修改原buffer也会影响到asReadOnlyBuffer。
- 修改只读buffer,会抛出异常ReadOnlyBufferException
- slice()。与duplicate类似,区别在于他拷贝的是从原buffer的poisition到limit的部分,同时也是共享数据的。
Configuration config = new Configuration();
config.setHostname("localhost");
config.setPort(9092);
config.setMaxFramePayloadLength(1024 * 1024);
config.setMaxHttpContentLength(1024 * 1024);
config.setExceptionListener(new MyExceptionListener());
final SocketIOServer server = new SocketIOServer(config);
server.setPipelineFactory(new MySocketIOChannelHandler());
server.addConnectListener(new ConnectListener() {
@Override
public void onConnect(SocketIOClient client) {
System.out.println("client连接");
}
});
server.addDisconnectListener(new DisconnectListener() {
@Override
public void onDisconnect(SocketIOClient client) {
System.out.println("client断开连接");
}
});
server.addEventListener("msg", byte[].class, new DataListener<byte[]>() {
@Override
public void onData(SocketIOClient client, byte[] data, AckRequest ackRequest) {
client.sendEvent("msg", data);
}
});
server.start();
Thread.sleep(Integer.MAX_VALUE);
server.stop();
当我们发送超过1M的文件时,会提示异常,但是当我们想捕获这个异常的时候,无法捕获到,先记录一下,异常如下:
[nioEventLoopGroup-3-3] WARN io.netty.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.handler.codec.TooLongFrameException: content length exceeded 1048576 bytes.
at io.netty.handler.codec.MessageAggregator.handleOversizedMessage(MessageAggregator.java:420)
at io.netty.handler.codec.MessageAggregator.invokeHandleOversizedMessage(MessageAggregator.java:404)
at io.netty.handler.codec.MessageAggregator.decode(MessageAggregator.java:293)
at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:88)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:102)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at com.corundumstudio.socketio.transport.PollingTransport.channelRead(PollingTransport.java:109)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at com.corundumstudio.socketio.handler.AuthorizeHandler.channelRead(AuthorizeHandler.java:137)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:102)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:326)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:300)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1422)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:931)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:700)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:635)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:552)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:514)
at io.netty.util.concurrent.SingleThreadEventExecutor$6.run(SingleThreadEventExecutor.java:1050)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
——————————————————————————————
20191119
发现是netty-socketio框架的问题,虽然框架内部的自定义HttpObjectAggregator重写handleOversizedMessage方法,但是在实际的运行过程中,是不进入该方法的,进入的还是MessageAggregator的handleOversizedMessage方法。先记录,继续用,后续再深入处理。
老规矩写的不清晰的地方,可以指出,我会不定时更新和修改。一起加油.
所有的伟大,都来自于一个勇敢的开始。
最近因为需要使用socket,然后觉得原生java的socket,太过于繁琐了,而且偶尔会阻塞(也许是我菜把)。然后搜索过后,踩了几天的坑,最终解决了。忍不住打算做一下博文,因为网络上的文章,都是我出现了这个问题,然后,哇好巧呀,我也是。然后PASS。懵逼 = =,解决方法呢?所以有了这个博文的诞生。写的不好尽情见谅。
首先关于配置可以参考https://blog.csdn.net/dubismile/article/details/86580858。
然后进入正题吧。当我们使用netty作为服务端与底层设备进行通信的使用,除开特殊因素外,可能碰到的第一个坑就是在于转码问题。之前用原生Java写socket的时候没有遇到过这个问题,然后这次遇到,加上对于这一个也不算懂,只能努力找问题了。
当我们使用调试助手依照16进制发送80,他的10进制大于127,这时候,我们netty获取到的值,就会变成efbfbd,如果看到是乱码的请使用以下代码获取:
ByteBufUtil.hexDump(((String) msg).getBytes())
发现跟我们发送的80完全不多,那么是在什么环节除了问题呢?让我们使用调试,取出Object msg的值编辑。惊了。居然变成这个不知道什么玩意了。起初的我一位是配置有问题,然后翻了一堆配置教程。。。然后没解决,后来想会不会是ChannelOption能设置读取出来的长度问题,然后再去了解和研究channelOption。再然后发现可能是byte的问题,那么就可能关于解码器和编码器的问题,因为Netty本身是内置了编码器和解码器的,然后我以为是我用的不对,就去了解编码器类型,然后替换尝试,都没有什么用。
于是乎,最后采取了自定义解码器了。先上代码了。
public class MyDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
//创建字节数组,buffer.readableBytes可读字节长度
byte[] b = new byte[buffer.readableBytes()];
//复制内容到字节数组b
buffer.readBytes(b);
//字节数组转字符串
String str = new String(b);
System.out.println(str);
out.add(bytesToHexString(b));
}
public String bytesToHexString(byte[] bArray) {
StringBuffer sb = new StringBuffer(bArray.length);
String sTemp;
for (int i = 0; i < bArray.length; i++) {
sTemp = Integer.toHexString(0xFF & bArray[i]);
if (sTemp.length() < 2)
sb.append(0);
sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
public static String toHexString1(byte[] b) {
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < b.length; ++i) {
buffer.append(toHexString1(b[i]));
}
return buffer.toString();
}
public static String toHexString1(byte b) {
String s = Integer.toHexString(b & 0xFF);
if (s.length() == 1) {
return "0" + s;
} else {
return s;
}
}
}
修改ServerChannelInitializer.java
@Component
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Autowired
ServerChannelHandler serverChannelHandler;
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//IdleStateHandler心跳机制,如果超时触发Handle中userEventTrigger()方法
pipeline.addLast("idleStateHandler",
new IdleStateHandler(5, 0, 0, TimeUnit.MINUTES));
//字符串编解码器
// pipeline.addLast(
// new StringDecoder(),
// new StringEncoder()
// );
//修改了这里
pipeline.addLast("decoder", new MyDecoder());
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 4, 4, -8, 0));
// pipeline.addLast("byteArrayEncoder", new ByteArrayEncoder());
//自定义Handler
pipeline.addLast("serverChannelHandler", serverChannelHandler);
}
}
至此,大工告成。
小结:在写的过程中,我在想为什么超过127后会变成负数,然后搜索了关于这块的资料,虽然大概知道怎么计算了,但是没啥用呀。果然在项目中,不断学习新的知识,填充自身很棒。这么一趟坑我就学到了很多。
老规矩写的不清晰的地方,可以指出,我会不定时更新和修改。一起加油
所有的伟大,都来自于一个勇敢的开始。
Bean 作用域
- singleton(单例):Spring容器只会创建一个bean对象;
- prototype:每次获取bean都会重新创建一个bean对象;
- request:对于每一个http请求,在同一个请求内Spring容器只会创建一个bean对象,若请求结束,bean也随之销毁;
- session:在同一个http会话里,Spring容器只会创建一个bean对象,若传话结束,也随之销毁;
- globalSession:globalSession作用域的效果与session作用域类似,但是只适用于基于portlet的web应用程序中
- application:在servlet程序中,该作用域的bean将会作为ServletContext对象的属性,被全局访问,与singleton的区别就是,singleton作用域的bean在Spring容器中只一;application作用域的bean在ServletContex中唯一;
- websocket:为每个websocket对象创建一个实例。仅在Web相关的ApplicationContext中生效。
前言
对于 Spring Bean 的作用域需要有一定了解。传送门
InitializingBean
实现InitializingBean接口的Bean,该Bean实例化后Bean中所有的属性被BeanFactory进行注入之后的,检查所有强制性属性的设置。
- afterPropertiesSet() 方法在 BeanFactory 为 Bean 填充完属性后触发
afterPropertiesSet()方法允许Bean在最终初始化完成之后,针对BeanFactory的属性注入以及整体配置的验证。afterPropertiesSet()方法可以在Bean发生错误配置(如未能设置一个基本属性)或者因为其他任何原因初始化失败而抛出异常。- InitializingBean有个功能类似的替换方案:在XML配置文件中配置init-method。
SmartInitializingSingleton
SmartInitializingSingleton接口中的afterSingletonsInstantiated()方法将会在所有的非惰性单实例Bean初始化完成之后进行回调。
- bean的afterSingletonsInstantiated()方法是在所有单例bean都初始化完成后才会调用
afterSingletonsInstantiated的。 - 此接口可以解决一些因为bean初始化太早而出现的错误和问题。
- 此接口是从Spring4.1版本开始使用的。
- 此接口可以看作是所有的Bean初始化完成后的
InitializingBean接口的一种替代方案。
区别
- SmartInitializingSingleton只作用于非懒加载单例bean,InitializingBean无此要求。但他们都不能用于懒加载的bean。
- SmartInitializingSingleton是在所有单例Bean都初始化完成后调用的,InitializingBean是每个bean初始化完成后就会调用。
触发时机
Spring 容器启动核心
源码 AbstractApplicationContext#refresh()
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
// 实例化所有剩余的(非惰性初始化)单例
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
源码 AbstractApplicationContext#finishBeanFactoryInitialization()
/**
* Finish the initialization of this context's bean factory,
* initializing all remaining singleton beans.
*/
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
// Register a default embedded value resolver if no BeanFactoryPostProcessor
// (such as a PropertySourcesPlaceholderConfigurer bean) registered any before:
// at this point, primarily for resolution in annotation attribute values.
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}
// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}
// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);
// Allow for caching all bean definition metadata, not expecting further changes.
beanFactory.freezeConfiguration();
// Instantiate all remaining (non-lazy-init) singletons.
// 实例化所有剩余的(非惰性初始化)单例
beanFactory.preInstantiateSingletons();
}
SmartInitializingSingleton 的触发
源码 DefaultListableBeanFactory#preInstantiateSingletons()
@Override
public void preInstantiateSingletons() throws BeansException {
if (logger.isTraceEnabled()) {
logger.trace("Pre-instantiating singletons in " + this);
}
// Iterate over a copy to allow for init methods which in turn register new bean definitions.
// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
// 创建一个 beanDefinitionNames 的副本用于遍历,以允许init方法注册新的bean定义。尽管这不是常规工厂启动的一部分但很好用。
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
// 触发所有非惰性(懒加载)单例bean的初始化
for (String beanName : beanNames) {
// 返回合并的RootBeanDefinition,遍历父bean定义,如果指定的bean对应于子bean定义。
// 根 Bean 定义本质上是运行时的“统一”Bean 定义视图。
// Spring 上下文包括实例化所有 Bean 使用的 AbstractBeanDefinition 是 RootBeanDefinition
// 使用 getMergedLocalBeanDefinition 方法做了一次转化, 将非 RootBeanDefinition 转换为 RootBeanDefinition 以供后续操作
// 注意, 如果当前 BeanDefinition 存在父 BeanDefinition, 会基于父 BeanDefinition 生成一个 RootBeanDefinition, 然后在将调用 OverrideFrom 子 BeanDefinition 的相关属性覆写进去
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
// 是否非抽象类 是否是单例, 是否非懒加载
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 判断是否为 FacotryBean
if (isFactoryBean(beanName)) {
// 调用 getBean方法,传入 Factory_bean的前缀 + Bean名称
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
// 判断是否是 FactoryBean方法
if (bean instanceof FactoryBean) {
FactoryBean<?> factory = (FactoryBean<?>) bean;
// 是否是需要立即初始化
boolean isEagerInit;
// 判断系统安全接口是否为空和Bean 是否实现了 SmartFactoryBean 接口, 如果实现了 SmartFactoryBean
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
// 方法判断是否希望急切的进行初始化
isEagerInit = AccessController.doPrivileged(
(PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,
getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
// 如果需要立即初始化, 则通过getBean初始化
if (isEagerInit) {
getBean(beanName);
}
}
}
else {
// 如果 beanName 对应的 Bean 不是 FactoryBean, 只是普通 Bean, 调用 getBean 方法通过 beanName 获取 Bean 实例对象
getBean(beanName);
}
}
}
// 触发所有可用的Bean的初始化回调。
for (String beanName : beanNames) {
// 获取 beanName 对应的单例Bean实例对象
Object singletonInstance = getSingleton(beanName);
// 判断获取的 SigletonInstance 是否实现了 SmartInitializingSingleton 接口
if (singletonInstance instanceof SmartInitializingSingleton) {
SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
// 判断系统安全接口是否为空,触发 afterSingletonInstantiated 初始化。
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
smartSingleton.afterSingletonsInstantiated();
return null;
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}
- 获取所有的bean,判断是否是 beanFactory,如果是检查是否需要立即初始化,否则将其初始化。
- 遍历触发所有可用的 Bean 初始化回调。获取单例对象,触发实现
SmartInitializingSingleton接口的afterSingletonsInstantiated方法。 - 对于
SmartInitializingSingleton的触发实在所有非惰性单实例Bean初始化完成后进行的。
InitializingBean的触发
源码AbstractAutowireCapableBeanFactory#invokeInitMethods()
/**
* Give a bean a chance to react now all its properties are set,
* and a chance to know about its owning bean factory (this object).
* This means checking whether the bean implements InitializingBean or defines
* a custom init method, and invoking the necessary callback(s) if it does.
* @param beanName the bean name in the factory (for debugging purposes)
* @param bean the new bean instance we may need to initialize
* @param mbd the merged bean definition that the bean was created with
* (can also be {@code null}, if given an existing bean instance)
* @throws Throwable if thrown by init methods or by the invocation process
* @see #invokeCustomInitMethod
*/
// 现在 Bean 的所有属性已经设置好了,给 Bean 一个反射的机会,以及了解他拥有的Bean Factory(这个对象)的机会
// 这意味着检查 bean 是否实现了 InitializingBean 接口或定义了自定义的初始化方法,如果有的话,将调用必要的回调
protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd)
throws Throwable {
// 判断 bean 是否实现 InitializingBean 接口
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
if (logger.isTraceEnabled()) {
logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
}
// 判断系统安全管理是否为空,然后调用 afterPropertiesSet 方法
if (System.getSecurityManager() != null) {
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
((InitializingBean) bean).afterPropertiesSet();
return null;
}, getAccessControlContext());
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
else {
((InitializingBean) bean).afterPropertiesSet();
}
}
// 自定义初始化方法
// 如果 mbd 不为空,且bean的类不等于 NullBean.class
if (mbd != null && bean.getClass() != NullBean.class) {
// 获取初始化方法名
String initMethodName = mbd.getInitMethodName();
// 判断长度、
//初始化名称是否等于“afterPropertiesSet” 和 判断是否是 isInitializingBean
// 是否指定初始化方法
if (StringUtils.hasLength(initMethodName) &&
!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
!mbd.isExternallyManagedInitMethod(initMethodName)) {
// 调用自定义方法 init-method
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}
- Bean实例如果实现了
InitializingBean接口则直接触发InitializingBean#afterPropertiesSet()。 - 如果Bean实例指定了init-method,则通过反射激活Bean指定的init-method。
- 如果Bean实例实现了
InitializingBean接口和init-method方法,则先调用afterPropertiesSet,然后调用init-method Spring对于InitializingBean的触发是在Bean所有属性已经设置好了之后。- 实现
InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率要高一点,但是init-method方式消除了对spring的依赖。 - 如果调用
afterPropertiesSet方法时出错,则不调用init-method指定的方法。
Spring 加载propertie文件方式
当应用比较大的时候,如果所有的内容都放在一个文件,会显得过分臃肿且不好理解,可以将一个文件拆分多个文件,然后在分别加载进系统。
@PropertySource
- 加载指定的属性文件(*.properties)到Spring的Environment;
- 和@Value组合使用,可以将properties中的变量注入到当前类使用。
- 和@ConfigurationProperties,可以将properties中的变量注入到当前类使用。
固定编码
@PropertySource(value = "classpath:/config/remote-ssh.properties", encoding = "utf-8", ignoreResourceNotFound = true)
@ConfigurationProperties(prefix = "ssh")
public class SshProperties {
// @Value("${ssh.enabled:false}")
private boolean enabled;
}
profile切换
# application-dev.properties
smilex.path="classpath:"
# application-prod.properties
smilex.path="/opt/user/smilex"
我们通过变量的形式注入到@PropertySource即可。
@PropertySource(value = "${smilex.path}/config/remote-ssh.properties", encoding = "utf-8", ignoreResourceNotFound = true)
@ConfigurationProperties(prefix = "ssh")
public class SshProperties {
// @Value("${ssh.enabled:false}")
private boolean enabled;
}
在SpringApplication启动时,注入环境
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.io.IOException;
import java.util.Properties;
@SpringBootApplication
public class AdminApiApplication {
public static void main(String[] args) throws IOException {
SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(AdminApiApplication.class);
Properties properties = getProperties();
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().addLast(new PropertiesPropertySource("service-properties", properties));
springApplicationBuilder.environment(environment);
springApplicationBuilder.run(args);
}
private static Properties getProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resource = resolver.getResources("classpath:*.properties");
propertiesFactoryBean.setLocations(resource);
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
}
加载包下的类
/**
* 查找包下面的类
* 规则 top.zsmile\**\*.class
*
* @param searchPath 路径,支持ANT
* @return
*/
public static Map<String, Class> getClassBySuperClass(String searchPath, Class superClass) throws IOException {
Map<String, Class> handlerMap = new HashMap<String, Class>();
//spring工具类,可以获取指定路径下的全部类
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
try {
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
searchPath;
Resource[] resources = resourcePatternResolver.getResources(pattern);
//MetadataReader 的工厂类
MetadataReaderFactory readerfactory = new CachingMetadataReaderFactory(resourcePatternResolver);
for (Resource resource : resources) {
//用于读取类信息
MetadataReader reader = readerfactory.getMetadataReader(resource);
//扫描到的class
String classname = reader.getClassMetadata().getClassName();
// 记载class类
Class<?> clazz = Class.forName(classname);
//判断是否有指定注解
if (superClass != null) {
Class<?>[] interfaces = clazz.getInterfaces();
if (interfaces.length > 0) {
if (interfaces[0].equals(superClass)) {
handlerMap.put(classname, clazz);
}
}
} else {
handlerMap.put(classname, clazz);
}
}
return handlerMap;
} catch (IOException | ClassNotFoundException e) {
throw new IOException("找不到指定Class");
}
}
前言
RequestCondition 是 Spring MVC 对一个请求匹配条件的概念模型。最终的实现类可能是针对以下情况之一:路径匹配,头部匹配,请求参数匹配,可产生MIME匹配,可消费MIME匹配,请求方法匹配,或者是以上各种情况的匹配条件的一个组合。
执行过程
大致执行过程:(个人理解)
- 接收到请求后,通过
AbstractHandlerMethodMapping.lookupHandlerMethod方法遍历接口 - 通过
RequestCondition.getMatchingCondition方法获取出匹配的RequestCondition并包装成Match - 然后对
List<Match>匹配列表进行比较,通过RequestCondition.compareTo确定最后的最匹配的 - 如果出现匹配程度一致的,抛出异常
IllegalStateException
AbstractHandlerMethodMapping
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
// 获取出匹配的路径条件
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);
// 如果匹配数量大于1
if (matches.size() > 1) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
// 进行排序,执行 RequestCondition.compareTo
matches.sort(comparator);
bestMatch = matches.get(0);
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
// 如果是CORS预检查
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
// 如果两者比较一致,抛出异常。
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
// 设置最匹配的路径
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
// 没有匹配出来
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
源代码
RequestCondition定义
package org.springframework.web.servlet.mvc.condition;
import javax.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;
public interface RequestCondition<T> {
// 将此条件和另外一个请求匹配条件合并,合并逻辑由实现类决定。
T combine(T other);
// 检查条件是否与当前请求实例相匹配,如果不匹配返回null,
// 如果匹配,生成一个新的请求匹配条件,该新的请求匹配条件是当前请求匹配条件
// 举个例子来讲,如果当前请求匹配条件是一个路径匹配条件,包含多个路径匹配模板,
// 并且其中有些模板和指定请求request匹配,那么返回的新建的请求匹配条件将仅仅
// 包含和指定请求request匹配的那些路径模板。
// 对于 Cors预检查请求,条件应与请求实例相匹配。如果不匹配,应确保返回一个示例空内容,不会导致匹配失败。
@Nullable
T getMatchingCondition(HttpServletRequest request);
// 针对指定的请求对象request比较两个请求匹配条件。
// 该方法假定被比较的两个请求匹配条件都是针对该请求对象request调用了
// #getMatchingCondition方法得到的,这样才能确保对它们的比较
// 是针对同一个请求对象request,这样的比较才有意义(最终用来确定谁是
// 更匹配的条件)。
//
int compareTo(T other, HttpServletRequest request);
}
由接口源代码可以看出,接口RequestCondition是一个泛型接口。事实上,它的泛型参数T通常也会是一个RequestCondition对象。
AbstractRequestCondition 定义
框架对接口RequestCondition有一组具体实现类。对这些具体实现类的一些通用逻辑,比如equals,hashCode和toString,是被放到抽象基类AbstractRequestCondition来实现的。同时AbstractRequestCondition还通过protected抽象方法约定了实现类其他的一些内部通用逻辑,具体如下所示:
package org.springframework.web.servlet.mvc.condition;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.lang.Nullable;
/**
* @param <T> the type of objects that this RequestCondition can be combined
* with and compared to
*/
public abstract class AbstractRequestCondition<T extends AbstractRequestCondition<T>> implements RequestCondition<T> {
/**
* 当前请求匹配条件对象是否内容为空
* @return true if empty; false otherwise
*/
public boolean isEmpty() {
return getContent().isEmpty();
}
/**
* 一个请求匹配条件可能由多个部分组成,这些组成部分被包装成一个名为 content 的集合
* 比如 :
* 对于请求路径匹配条件,可能有多个 URL pattern,
* 对于请求方法匹配条件,可能有多个 HTTP request method,
* 对于请求参数匹配条件,可能有多个 param 表达式 .
* @return a collection of objects, never null , 可能为空集合
*/
protected abstract Collection<?> getContent();
/**
* The notation to use when printing discrete items of content.
* 将该条件作为字符串展示时,各个组成部分之间的中缀标识符。比如 "||" 或者 "&&" 等。
* For example {@code " || " for URL patterns or {@code " && "} for param expressions.
*/
protected abstract String getToStringInfix();
// equlas 实现
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
return getContent().equals(((AbstractRequestCondition<?>) other).getContent());
}
// hashCode 实现
@Override
public int hashCode() {
return getContent().hashCode();
}
// toString 实现
@Override
public String toString() {
StringBuilder builder = new StringBuilder("[");
for (Iterator<?> iterator = getContent().iterator(); iterator.hasNext();) {
Object expression = iterator.next();
builder.append(expression.toString());
if (iterator.hasNext()) {
builder.append(getToStringInfix());
}
}
builder.append("]");
return builder.toString();
}
}
继承 AbstractRequestCondition 的具体实现类
| 实现类 | 简介 |
|---|---|
| PatternsRequestCondition | 路径匹配条件 |
| RequestMethodsRequestCondition | 请求方法匹配条件 |
| ParamsRequestCondition | 请求参数匹配条件 |
| HeadersRequestCondition | 头部信息匹配条件 |
| ConsumesRequestCondition | 可消费MIME匹配条件 |
| ProducesRequestCondition | 可生成MIME匹配条件 |
框架还提供了一个实现类RequestConditionHolder,这是一个匹配条件持有器,用于持有某个RequestCondition对象。如果你想持有一个RequestCondition对象,但其类型事先不可知,那么这种情况下该工具很有用。但要注意的是如果要合并或者比较两个RequestConditionHolder对象,也就是二者所持有的RequestCondition对象,那么二者所持有的RequestCondition对象必须是类型相同的,否则会抛出异常ClassCastException。
SSE介绍
SSE(Server-SentEvents,即服务器发送事件)是围绕只读Comet交互推出的API或者模式。SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是text/event-stream,而且是浏览器中的JavaScript API能解析格式输出。SSE支持短轮询、长轮询和HTTP流,而且能在断开连接时自动确定何时重新连接。
- SSE特点:实现简单、 单向通信、自动重连
- 业务场景:客户端与服务端建立连接后,只需要服务端给客户端发送数据,客户端无需要给服务端发送数据
前端DEMO
基于AXIOS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"></script>
<title>Document</title>
</head>
<body>
<div class="form-group">
<label for="clientId">clientId:</label>
<input type="text" class="form-control" id="clientId">
</div>
<div class="form-group">
<label for="content">content</label>
<input type="text" class="form-control" id="content">
</div>
<button id="subscribeBtn" class="btn btn-primary">订阅</button>
<button id="sendBtn" class="btn btn-primary">发送</button>
<button id="closeBtn" class="btn btn-primary">关闭</button>
<p id="responseText">
</p>
</body>
</html>
<script>
window.onload = function () {
let start = 0;
$("#subscribeBtn").click(() => {
const clientId = $("#clientId").val();
start = 0;
axios({
method: 'get',
url: 'http://localhost:8080/smilex/open/sse/subscribe?clientId=' + clientId,
responseType: 'stream', // 设置为流
headers: {
Accept: "text/event-stream" // 接受类型
},
onDownloadProgress: res => {
// 每次推送会在这里打印,但不一定每次都是一次传输完的。
console.log(res.event.currentTarget.response.substring(start, start + res.bytes))
start += res.bytes;
},
}).then(function (response) {
console.log("resposne", response)
const stream = response.data
});
})
$("#sendBtn").click(() => {
const clientId = $("#clientId").val();
const content = $("#content").val();
axios({
method: 'get',
url: 'http://localhost:8080/smilex/open/sse/send?clientId=' + clientId + "&content=" + content,
}).then(function (response) {
// console.log("发送", response)
});
})
$("#closeBtn").click(() => {
const clientId = $("#clientId").val();
axios({
method: 'get',
url: 'http://localhost:8080/smilex/open/sse/close?clientId=' + clientId,
}).then(function (response) {
// console.log("关闭", response)
});
})
}
</script>
基于 EventSource
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"></script>
<title>Document</title>
</head>
<body>
<div class="form-group">
<label for="clientId">clientId:</label>
<input type="text" class="form-control" id="clientId">
</div>
<div class="form-group">
<label for="content">content</label>
<input type="text" class="form-control" id="content">
</div>
<button id="subscribeBtn" class="btn btn-primary">订阅</button>
<button id="sendBtn" class="btn btn-primary">发送</button>
<button id="closeBtn" class="btn btn-primary">关闭</button>
<p id="responseText">
</p>
</body>
</html>
<script>
// EventSource
window.onload = function () {
let source = null
$("#subscribeBtn").click(() => {
const clientId = $("#clientId").val();
source = new EventSource("http://localhost:8080/smilex/open/sse/subscribe?clientId=" + clientId);
source.addEventListener('message', function (e) {
// console.log("message", e);
//do something
$("#responseText").html($("#responseText").html() + "\n" + e.data);
});
source.addEventListener('open', function (e) {
//do something
console.log("open", e)
}, false);
source.addEventListener('error', function (e) {
console.log("error", e, source.readyState)
if (source.readyState == EventSource.CLOSED) {
//do something
source.close();
} else {
//do something
}
}, false);
})
$("#sendBtn").click(() => {
const clientId = $("#clientId").val();
const content = $("#content").val();
axios({
method: 'get',
url: 'http://localhost:8080/smilex/open/sse/send?clientId=' + clientId + "&content=" + content,
}).then(function (response) {
console.log("发送", response)
});
})
$("#closeBtn").click(() => {
const clientId = $("#clientId").val();
// 需要主动关闭,如果直接调用接口关闭,会导致自动重连。
source.close();
axios({
method: 'get',
url: 'http://localhost:8080/smilex/open/sse/close?clientId=' + clientId,
}).then(function (response) {
console.log("关闭", response)
$("#responseText")[0].innerHTML = ""
});
})
}
</script>
SseEmitter
- 前端发起连接 创建并返回
SseEmitter对象 - 调用
SseEmitter对象的send方法 - 发送结束后,调用
complete方法
service
/**
* Sse服务
*/
public interface SseService {
/**
* 连接
*
* @param clientId
* @return
*/
SseEmitter connect(String clientId);
/**
* 发送
*
* @param clientId
* @param content
* @return
*/
boolean send(String clientId, String content);
/**
* 关闭
*
* @param clientId
* @return
*/
boolean close(String clientId);
}
serviceImpl
@Slf4j
@Service
public class SseServiceImpl implements SseService {
@Override
public SseEmitter connect(String clientId) {
if (SseSession.exists(clientId)) {
SseSession.remove(clientId);
}
SseEmitter sseEmitter = new SseEmitter(0L);
sseEmitter.onError((err) -> {
log.error("type: SseSession Error, msg: {} session Id : {}", err.getMessage(), clientId);
SseSession.onError(clientId, err);
});
sseEmitter.onTimeout(() -> {
log.info("type: SseSession Timeout, session Id : {}", clientId);
SseSession.remove(clientId);
});
sseEmitter.onCompletion(() -> {
log.info("type: SseSession Completion, session Id : {}", clientId);
SseSession.remove(clientId);
});
SseSession.add(clientId, sseEmitter);
return sseEmitter;
}
@Override
public boolean send(String clientId, String content) {
if (SseSession.exists(clientId)) {
try {
SseSession.send(clientId, content);
return true;
} catch (IOException exception) {
log.error("type: SseSession send Erorr:IOException, msg: {} session Id : {}", exception.getMessage(), clientId);
}
} else {
throw new SXException("User Id " + clientId + " not Found");
}
return false;
}
@Override
public boolean close(String clientId) {
log.info("type: SseSession Close, session Id : {}", clientId);
return SseSession.remove(clientId);
}
}
SseSession
利用Map 管理全局的 SseEmitter
public class SseSession {
private static Map<String, SseEmitter> sessionMap = new ConcurrentHashMap<>();
public static void add(String sessionKey, SseEmitter sseEmitter) {
if (sessionMap.get(sessionKey) != null) {
throw new SXException("client exists!");
}
sessionMap.put(sessionKey, sseEmitter);
}
public static boolean exists(String sessionKey) {
return sessionMap.get(sessionKey) != null;
}
public static boolean remove(String sessionKey) {
SseEmitter sseEmitter = sessionMap.get(sessionKey);
if (sseEmitter != null) {
sseEmitter.complete();
sessionMap.remove(sessionKey);
return true;
}
return false;
}
public static void onError(String sessionKey, Throwable throwable) {
SseEmitter sseEmitter = sessionMap.get(sessionKey);
if (sseEmitter != null) {
sseEmitter.completeWithError(throwable);
}
}
public static void send(String sessionKey, String content) throws IOException {
sessionMap.get(sessionKey).send(content);
}
}
Controller
/**
* SSE测试
*/
@RestController
@RequestMapping("/demo/sse")
public class DemoSseController {
@Resource
private SseService sseService;
@RequestMapping(value = "/subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter subscribe(String clientId) {
return sseService.connect(clientId);
}
@RequestMapping(value = "/send")
public R send(String clientId, String content) {
if (sseService.send(clientId, content)) {
return R.success();
}
return R.fail();
}
@RequestMapping(value = "/close")
public R close(String clientId) {
sseService.close(clientId);
return R.success();
}
}
注意事项
- 前端使用
EventSource时,如果后端服务先关闭了连接,那么EventSource会抛出异常并自动发起重连。所以如果不想重连,需要前端关闭后,再关闭后端连接。
参考
https://www.cnblogs.com/guogangj/p/5457959.html
https://blog.csdn.net/f641385712/article/details/88692534
https://blog.csdn.net/f641385712/article/details/88710676
什么是跨域
同源策略是由 Netscape 提出的一个著名的安全策略,它是浏览器最核心也最基本的安全功能,现在所有支持 JavaScript 的浏览器都会使用这个策略。所谓同源是指协议、域名以及端口要相同。
同源策略是基于安全方面的考虑提出来的,这个策略本身没问题,但是我们在实际开发中,由于各种原因又经常有跨域的需求,传统的跨域方案是 JSONP,JSONP 虽然能解决跨域但是有一个很大的局限性,那就是只支持 GET 请求,不支持其他类型的请求,在 RESTful 时代这几乎就没什么用。
什么是CORS
CORS,Cross-origin resource sharing是一个 W3C 标准,提供了 Web 服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,这是 JSONP 模式的现代版。
- 域,指的是一个站点,由protocal、host和port三部分组成,其中host可以是域名,也可以是 ip:port。如果没有指明,则是使用protocal的默认端口
- 同源策略, 指的是为了防止 XSS, 浏览器、客户端应该仅请求与当前页面来自同一个域的资源, 请求其他域的资源需要通过验证。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
关于CORS可以看一下这篇文章
SpringBoot下3种常见的cors方案
- @CrossOrigin。针对单个类或者方法。
@CrossOrigin(value = "http://localhost:8081")
@PostMapping("/hello")
public String hello() {
return "post hello";
}
- WebMvcConfigurer。全局配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
}
}
- 注入CorsFilter。过滤器
@Configuration
public class CorsConfig {
private CorsConfiguration corsConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(3600L);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig());
return new CorsFilter(source);
}
}
- FilterRegistrationBean。也是基于过滤器
参考
- https://www.ruanyifeng.com/blog/2016/04/cors.html
- https://blog.51cto.com/u_15080000/2593305
- https://mp.weixin.qq.com/s?__biz=MzI1NDY0MTkzNQ==&mid=2247488989&idx=1&sn=00881de1a77c2e4027ee948164644485&scene=21#wechat_redirect
前言
这边文章是通过使用注解+AOP的形式,在业务代码前后做日志增强的功能。这样在不更改业务代码的情况下,利用注解作为 Pointcut 快速实现日志记录的功能。
准备工作
Maven版本
这里是基于springboot-2.3.5版本,需要引入 SpringAop 库。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
数据库结构
我这里采用的将接口访问日志,存储到 Mysql 数据库中,其它数据库原理一致,具体根据自己项目结构进行调整。
CREATE TABLE `sys_log` (
`id` bigint(20) NOT NULL COMMENT 'ID',
`log_module` varchar(50) NOT NULL COMMENT '日志模块,sys,blog',
`log_title` varchar(50) NOT NULL COMMENT '日志标题',
`log_value` varchar(50) NOT NULL COMMENT '日志内容',
`log_type` tinyint(2) NOT NULL COMMENT '日志类型1:登录日志;2:操作日志;3:定时任务;4:异常日志;',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`operate_type` tinyint(2) NOT NULL COMMENT '操作类型',
`ip_address` varchar(100) DEFAULT NULL COMMENT 'IP地址',
`method` varchar(500) DEFAULT NULL COMMENT '请求方法',
`request_url` varchar(50) DEFAULT NULL COMMENT '请求url路径',
`request_type` varchar(50) DEFAULT NULL COMMENT '请求类型',
`request_params` text COMMENT '请求参数',
`cost_time` int(11) DEFAULT NULL COMMENT '耗费时间',
`err_msg` varchar(1000) DEFAULT NULL COMMENT '异常信息',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除,1是,0否',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志';
常量值
我将系统中使用的常量值单独分离了出来。
public interface CommonConstant {
/**************************SysLog 日志常量**************************/
//查询
public static final int SYS_LOG_OPERATE_QUERY = 1;
//添加
public static final int SYS_LOG_OPERATE_SAVE = 2;
//更新
public static final int SYS_LOG_OPERATE_UPDATE = 3;
//删除
public static final int SYS_LOG_OPERATE_REMOVE = 4;
//导入
public static final int SYS_LOG_OPERATE_IMPORT = 5;
//导出
public static final int SYS_LOG_OPERATE_EXPORT = 6;
//登录
public static final int SYS_LOG_TYPE_LOGIN = 1;
//操作
public static final int SYS_LOG_TYPE_OPERATE = 2;
//定时
public static final int SYS_LOG_TYPE_TIME = 3;
//异常
public static final int SYS_LOG_TYPE_ERROR = 4;
}
核心
这里的核心基于两个事物:
- @interface 注解类
- @Aspect 切面编程
@SysLog
这里根据我对系统的设计划分
- module:所属模块,可以划分大模块比如(Sys,系统模块),(Blog,博客模块)。
- title:子模块标题,可以简单划分大模块下的子模块。
- logType:日志类型,分为1:登录日志;2:操作日志;3:定时任务;4:异常日志。
- opreateType:操作类型,1查询,2添加,3修改,4删除,5导入,6导出。
- 日志内容:日志内容。
/**
* 系统日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
/**
* 所属模块
*
* @return ModuleType
*/
ModuleType module() default ModuleType.SYS;
/**
* 标题
* 例如:菜单管理
*
* @return
*/
String title() default "";
/**
* 日志类型
*
* @return 1:登录日志;2:操作日志;3:定时任务;4:异常日志;
*/
int logType() default CommonConstant.SYS_LOG_TYPE_OPERATE;
/**
* 操作类型
*
* @return (1查询,2添加,3修改,4删除,5导入,6导出)
*/
int operateType() default 0;
/**
* 日志内容
*
* @return
*/
String value() default "";
}
SysLogAspect
这里有几个需要注意的地方
- 需要在类上添加@Component 和 @Aspect 注解,前者是为了将SysLogAspect 作为一个Bean,注入到Spring容器中;后者是为了开启切面功能
- @Pointcut 注入点,注入点说明该切面在什么情况下生效,这里使用 "@annotation(top.zsmile.common.log.annotation.SysLog)" 表示只要是有加@SysLog注解的地方就能注入切面功能。
- @Around,括号内使用的就是对应的切点sysLogPointcut(),这时around方法就会执行。
- ProceedingJoinPoint joinPoint,指的是对应的执行点,当我们调用proceed()时,会调用对应的业务代码。
@Slf4j
@Component
@Aspect
public class SysLogAspect {
@Autowired
private SysLogService sysLogService;
@Autowired
private CommonAuthApi commonAuthApi;
@Pointcut("@annotation(top.zsmile.common.log.annotation.SysLog)")
public void sysLogPointcut() {
}
@Around("sysLogPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long beginTime = System.currentTimeMillis();
try {
//执行方法
Object result = joinPoint.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
saveSysLog(joinPoint, time, result);
return result;
} catch (Throwable throwable) {
// 异常日志
long time = System.currentTimeMillis() - beginTime;
saveErrorLog(joinPoint, time, throwable.getMessage());
throw throwable;
}
}
/**
* 保存错误日志
*
* @param joinPoint
* @param costTime
* @param errMsg
*/
private void saveErrorLog(ProceedingJoinPoint joinPoint, long costTime, String errMsg) {
SysLogEntity sysLogEntity = commonLog(joinPoint, costTime, CommonConstant.SYS_LOG_TYPE_ERROR);
sysLogEntity.setErrMsg(errMsg);
sysLogService.save(sysLogEntity);
}
/**
* 保存正常日志
*
* @param joinPoint
* @param costTime
* @param result
*/
private void saveSysLog(ProceedingJoinPoint joinPoint, long costTime, Object result) {
SysLogEntity sysLogEntity = commonLog(joinPoint, costTime);
sysLogService.save(sysLogEntity);
}
private SysLogEntity commonLog(ProceedingJoinPoint joinPoint, long costTime) {
return commonLog(joinPoint, costTime, 0);
}
private SysLogEntity commonLog(ProceedingJoinPoint joinPoint, long costTime, int logType) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLogEntity sysLogEntity = new SysLogEntity();
SysLog sysLogAnno = method.getAnnotation(SysLog.class);
if (sysLogAnno != null) {
String title = sysLogAnno.title();
ModuleType module = sysLogAnno.module();
int operateType = sysLogAnno.operateType();
String value = sysLogAnno.value();
sysLogEntity.setLogTitle(title);
sysLogEntity.setLogValue(value);
sysLogEntity.setLogType(logType > 0 ? logType : sysLogAnno.logType());
sysLogEntity.setOperateType(getOperateType(method.getName(), operateType));
sysLogEntity.setLogModule(module.name());
}
//获取request
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
sysLogEntity.setIpAddress(IPUtils.getIpAddrByRequest(request));
//请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLogEntity.setMethod(className + "." + methodName);
// 请求类型
sysLogEntity.setRequestType(request.getMethod());
// 耗时
sysLogEntity.setCostTime(costTime);
// 请求参数
sysLogEntity.setRequestParams(getParams(joinPoint, request));
// 操作用户Id
Long userId = commonAuthApi.queryUserId();
sysLogEntity.setUserId(userId);
return sysLogEntity;
}
private int getOperateType(String methodName, int operateType) {
if (operateType > 0) {
return operateType;
}
if (methodName.startsWith("query") || methodName.startsWith("list")) {
return CommonConstant.SYS_LOG_OPERATE_QUERY;
} else if (methodName.startsWith("save")) {
return CommonConstant.SYS_LOG_OPERATE_SAVE;
} else if (methodName.startsWith("update")) {
return CommonConstant.SYS_LOG_OPERATE_UPDATE;
} else if (methodName.startsWith("remove")) {
return CommonConstant.SYS_LOG_OPERATE_REMOVE;
} else if (methodName.startsWith("import")) {
return CommonConstant.SYS_LOG_OPERATE_IMPORT;
} else if (methodName.startsWith("export")) {
return CommonConstant.SYS_LOG_OPERATE_EXPORT;
}
return CommonConstant.SYS_LOG_OPERATE_QUERY;
}
/**
* 获取Request参数
*
* @param httpServletRequest
* @return
*/
private String getParams(ProceedingJoinPoint joinPoint, HttpServletRequest httpServletRequest) {
String method = httpServletRequest.getMethod();
if (method.equalsIgnoreCase("POST") || method.equalsIgnoreCase("PUT") || method.equalsIgnoreCase("DELET")) {
PropertyFilter profilter = new PropertyFilter() {
@Override
public boolean apply(Object o, String name, Object value) {
if (value != null && value.toString().length() > 200) {
return false;
}
return true;
}
};
return JSON.toJSONString(joinPoint.getArgs(), profilter);
} else {
return JSON.toJSONString(httpServletRequest.getParameterMap());
}
}
}
使用方法
@SysLog(title = "角色管理", operateType = CommonConstant.SYS_LOG_OPERATE_QUERY, value = "分页查询")
前言
最近在开发中,碰到一个问题,关于数据库Long类型查询后,返回给前端后,精度丢失。
297874820157145088 => 297874820157145100
数据库数据如下:
| id | 字典码 | 字典名称 | 备注 |
|---|---|---|---|
| 297874820157145088 | enableFlag | 启用状态 | 0未启用,1启用 |
查询语句如下:
SELECT id AS id, dict_code AS dictCode, dict_name AS dictName, remark AS remark, create_time AS createTime, create_by AS createBy, update_time AS updateTime, update_by AS updateBy, del_flag AS delFlag FROM sys_dict WHERE (del_flag=0) LIMIT 10 OFFSET 0
查询结果:(正确,此时并未出现精度丢失)
| id | 字典码 | 字典名称 | 备注 |
|---|---|---|---|
| 297874820157145088 | enableFlag | 启用状态 | 0未启用,1启用 |
再来看一下Springboot准备返回时的数据显示:

这里可以看到返回类型是Long类型且精度并未丢失。


可以看到这时前端显示出来的,就出现问题了。是不是网络传输的过程有问题呢?我们换PostMan试一下。

可以看见这里的id是正确的,那么应该时JavaScript导致的精度丢失了。
大致看了一下网上的思路,因为JavaScript 的整数类型范围有限,精度为17位 ,当接口返回的Long类型过长时,javaScript会进行截断,所以解决办法就是把Long类型转换成String类型返回,这样就不会由精度丢失的问题了。
解决方法
基于注解@JsonSerialize(不推荐)
最直接的方式就是在需要转换的Long类型字段使用注解标记。
@JsonSerialize
private Long id;
这样子好处是可以对单个的字段进行精细管理,但是需要每个字段都添加转换,很不方便,工作量大。
基于jackson全局配置(不推荐)
spring:
jackson:
generator:
write-numbers-as-strings: true

这个方法会将全局所有的数字类型包括int/long等都转换为string类型返回,不推荐使用。
使用JsonComponent 序列化配置
@JsonComponent
public class JsonConfig {
/**
* 添加Long转json精度丢失的配置
*/
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(module);
return objectMapper;
}
}
为什么这样写能生效呢?我们可以看一下 WebMvcConfigurationSupport 类。
WebMvcConfigurationSupport 分析
//初始化
static {
// etc...
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
}
// 添加默认的httpMesssage转换器
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
//etc
if (jackson2Present) {
builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
这里可以看出来Springboot提供的默认mvc配置内容:
- 初始化并查找是否有 ObjectMapper 类
- 如果没有发现 ObjectMapper Bean对象,就会提供给一个默认的 MappingJackson2HttpMessageConverter 对象。
- 如果发现 ObjectMapper Bean对象,就会将这个绑定到默认的转换器上。
WebMvcConfigurer/WebMvcConfigurationSupport
这里虽然将这两个类放一起,是因为都能基于他们去重写 configureMessageConverters方法,来实现对转换器的添加。
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加Long转json精度丢失的配置
*
* @Return: void
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
converters.add(jackson2HttpMessageConverter);
}
}
这里需要注意的是,该方法可能存在失效的情况,但是如果我们改成这样,就能有效。
converters.add(0,jackson2HttpMessageConverter);
这是为什么呢?让我们探究一下。
分析
先看一下都有什么转换器。

可以看到在我们添加转换器前,就已经有了2个 Mappingjackson2HttpMessageConverter 了。我们说过springboot 自带了 HttpMessageConverter 。
那么这时出现多个,会不会相互之间有影响?
// AbstractMessageConverterMethodProcessor.class
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
// AbstractMessageConverterMethodArgumentResolver.class
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
这里可以看到,会遍历messageConverters转换器列表,但是问题在于,如果有一个 HttpMessageConverter 类响应了读写信息,那么就会进行返回,这样导致了后面的转换器不生效。
解决方法
那么针对这种有几种解决方法
- 提升自定义的消息处理转换器优先级;
converters.add(0,jackson2HttpMessageConverter);
- 移除列表里的springboot 默认的转换器;
converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
- 使用@EnableWebMvc 注解。慎用,会清空sprinb自带的默认转换器导致某些功能失效。此时的converters转换列表为空。
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
converters.add(jackson2HttpMessageConverter);
}
}
springboot 多模块配置注意点
模块依赖且项目下的包路径不一致
当有模块A和模块B时,假设项目结构如下
- module A
- com
- aaa
- TestABean.java
- aaa
- com
- module B
- com
- bbb
- TestBBean.java
- SpringbootApplication.java
- bbb
- com
此时SpringbootApplication是无法加载到com.aaa下的TestABean。这时因为com.aaa和com.bbb不属于同一目录下,而springbootApplication的扫描,默认是当前目录作为相对根路径进行扫描组件的,那么我们应该怎么处理这个问题。 有几种解决方式:
- 利用@ComponentScan
@ComponentScan(basePackages = {"com.aaa","com.bbb"})
- 利用@ComponentScans
@ComponentScans({@ComponentScan("com.aaa"),@ComponentScan("com.bbb")})
- 利用@SpringBootApplication。
@SpringBootApplication(scanBasePackages = {"com.aaa","com.bbb"})
都能实现扫描到包的效果,那么这三者有什么异同呢?
-
@SpringBootApplication 默认会自动扫描同一包下的子包。
-
@SpringBootApplication和@ComponentScan设置后,都会覆盖@SpringBootApplication的默认扫描行为
-
@ComponentScans不会覆盖@SpringBootApplication的默认扫描行为
// 无法加载到TestBBean
@ComponentScan(basePackages = {"com.aaa"})
@SpringBootApplication(scanBasePackages = {"com.aaa"})
// 可以加载到TestBBean
@ComponentScan(basePackages = {"com.aaa","com.bbb"})
@SpringBootApplication(scanBasePackages = {"com.aaa","com.bbb"})
@ComponentScans({@ComponentScan("com.aaa")})
包扫描启动分析TODO
这里用的 Spring Boot 2.3.5.RELEASE 版本。
这里只关注重点的关键代码。
// ConfigurationClassParser
/**
* Apply processing and build a complete {@link ConfigurationClass} by reading the
* annotations, members and methods from the source class. This method can be called
* multiple times as relevant sources are discovered.
* @param configClass the configuration class being build
* @param sourceClass a source class
* @return the superclass, or {@code null} if none found or previously processed
*/
@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
// ...
// Process any @ComponentScan annotations
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
// ...
}
这里获取了ComponenetScan和ConponentScans两个注解配置,让我们看看 AnnotationConfigUtils.attributesForRepeatable 里面发生了什么。
// AnnotationConfigUtils
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
Class<?> containerClass, Class<?> annotationClass) {
return attributesForRepeatable(metadata, containerClass.getName(), annotationClass.getName());
}
@SuppressWarnings("unchecked")
static Set<AnnotationAttributes> attributesForRepeatable(
AnnotationMetadata metadata, String containerClassName, String annotationClassName) {
Set<AnnotationAttributes> result = new LinkedHashSet<>();
// Direct annotation present?
addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName, false));
// Container annotation present?
Map<String, Object> container = metadata.getAnnotationAttributes(containerClassName, false);
if (container != null && container.containsKey("value")) {
for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container.get("value")) {
addAttributesIfNotNull(result, containedAttributes);
}
}
// Return merged result
return Collections.unmodifiableSet(result);
}
参考链接
- https://my.oschina.net/icebergxty/blog/3142263
- https://my.oschina.net/icebergxty/blog/3103354
mybatis使用LocalDateTime
- 查看mybatis版本。 从 3.4.5 开始,MyBatis 默认支持 JSR-310 。如果是3.4.5以下的版本需要引入 JSR-310。
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.2</version>
</dependency>
- 给JDBC连接加上时区属性,避免时间出现偏差。
&serverTimezone=Asia/Shanghai
springboot使用LocalDateTime
maven引入
- 以下几种情况不用单独引入jackson-datatype-jsr
- 引入了 spring-boot-starter-web
- 引入了 spring-boot-starter-json
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
使用jackson 全局统一转换
- 利用ObjectMapper输入输出转换时间格式,添加时间序列化。
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
// 简单类型转换
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
/**
* localDateTime => yyyy-MM-dd HH:mm:ss
* localDate => yyyy-MM-dd
* localTime => HH:mm:ss
*/
JavaTimeModule javaTimeModule = new JavaTimeModule();
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
module.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
module.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
module.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
module.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
objectMapper.registerModules(module, javaTimeModule);
// 不序列化为null
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
- 利用 Jackson2ObjectMapperBuilderCustomizer 序列化
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
builder.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
builder.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
builder.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
};
}
局部转换
@JsonFormat 注解
利用注解 @JsonFormat 加在需要用的实体类时间字段上,就会完成格式化。
public class testVO {
@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd")
private LocalDate updateTime;
}
springboot 下mybatis-plus配置方式
- 基于yml配置
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
global-config:
banner: false
db-config:
#主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";
id-type: ASSIGN_ID
logic-delete-field: del_flag
logic-delete-value: 1
logic-not-delete-value: 0
table-underline: true
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 返回类型为Map,显示null对应的字段
call-setters-on-nulls: true
- 基于Springboot 的SqlSessionFactory Bean 注入
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setBanner(false);
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setIdType(IdType.ASSIGN_ID);
dbConfig.setLogicDeleteField("del_flag");
dbConfig.setLogicDeleteValue("1");
dbConfig.setLogicNotDeleteValue("0");
dbConfig.setTableUnderline(true);
globalConfig.setDbConfig(dbConfig);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class);
configuration.setCallSettersOnNulls(true);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**/*.xml"));
sessionFactory.setGlobalConfig(globalConfig);
sessionFactory.setDataSource(dynamicDataSource);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
问题记录
关于 Invalid bound statement 异常问题
记录20220311
发现了个异常,我的项目突然出现了Invalid bound statement,然后由于项目的Application启动类,放在了com.xxx.modules下,导致会出现组件加载异常和Invalid bound statement问题,当我将Application启动类移到com.xxx下,组件扫描正常,但是依然无法扫描到xml。
经过排查,当映射成功的时候,我发现dao层的对象是mybatis-plus.mapperProxy代理,故障时却是显示是mybatis.mapperProxy代理,那么是什么原因导致这个问题呢?
尝试的解决方式1.
@ComponentScan("com.xxx.modules.dao")
此时能够扫描到xml,但是感觉到问题不应该出现在这里。
继续查找原因,发现之前写多数据源时设置了SqlSessionFactory。
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
return sessionFactory.getObject();
}
这里的sessionFactory没有使用到yml里面的Mybatis-plus的配置。这也就意味着,我在yml里面的配置完全无效,屏蔽掉sqlSessionFactory的注释后,项目正常运行了。
knife4j
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案 。
-
访问地址都是/doc.html
前言
最近打算给自己的后台管理系统增加一个API管理,但是原生的Swagger,十分嫌弃,太丑了,所以找了个Knife4j,感觉上好看很多了。
Maven版本
这里是基于springboot-2.3.5版本,OpenApi2。特别说明一下,我这里使用的是单体应用,如果架构不同会有区分微服务版本、网关版本等。
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
Swagger2Config
因为代码篇幅较多,我将对应的说明描述下代码里面,就不在外说明了。
@Configuration
@EnableSwagger2WebMvc
public class Swagger2Config {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()).enable(true)
.select()
/***
重要的两个方法:
apis():指定要生成文档的接口包基本路径
paths():指定针对哪些请求生成接口文档
参考官方资料:http://www.baeldung.com/swagger-2-documentation-for-spring-rest-api
****/
// 1. 将 top.zsmile 设置为基础包扫描。
// 2. 只包含 RestController 注解,也可以使用 Api 注解,但是对应的类需要添加。
// 3. 针对所有路径扫描
.apis(RequestHandlerSelectors.basePackage("top.zsmile"))
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build()
.securitySchemes(securityScheme())
.securityContexts(securityContexts());
}
/**
* 安全计划
* 使用 X-ACCESS-TOKEN 作为请求头,添加在 HTTP Header 里面
*
* @return
*/
@Bean
public List<ApiKey> securityScheme() {
return Collections.singletonList(new ApiKey(CommonConstant.X_ACCESS_TOKEN, CommonConstant.X_ACCESS_TOKEN, "header"));
}
/**
* 新增 securityContexts 保持登录状态
*/
private List<SecurityContext> securityContexts() {
return Collections.singletonList(SecurityContext.builder()
.securityReferences(defaultAuth())
//设置需要登录认证的路径,非noauth开头的都需要Token认证
.forPaths(PathSelectors.regex("^(?!noauth).*$"))
.build());
}
/**
* 默认授权
*
* @return
*/
private List<SecurityReference> defaultAuth() {
// 权限范围:Scope=>全局,设置在keyName=>X-ACCESS-TOKEN
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Collections.singletonList(new SecurityReference(CommonConstant.X_ACCESS_TOKEN, authorizationScopes));
}
/**
* 文档信息
*
* @return
*/
private ApiInfo apiInfo() {
Contact smileX = new Contact("SmileX", "https://github.com/smileluck", "");
return new ApiInfoBuilder()
.title("SmileX项目接口文档")
.description("API接口文档")
.version("1.0.0")
.termsOfServiceUrl("https://github.com/smileluck")
.contact(smileX)
.build();
}
}
经过这个配置,其实已经可以使用了。
增强
接下来是为了打开knife4j的增强,可以采用注解 @EnableKnife4j 开启增强。
@EnableKnife4j
@Configuration
@EnableSwagger2WebMvc
public class Swagger2Config {}
或者通过yaml的形式进行配置。
knife4j:
# 开启增强
enable: true
# 开启生产环境保护
production: false
# 开启Swagger的Basic认证功能,默认是false
basic:
enable: false
# Basic认证用户名
username: knifeAdmin
# Basic认证密码
password: knifeAdmin
更多配置请查看官方文档。
注意事项
Shiro整合使用
需要给knife4j 的路径进行放行,否则当我们访问时会出现401的问题。
/*swagger*/
chainDefinition.addPathDefinition("/doc.html", "anon");
chainDefinition.addPathDefinition("/webjars/**", "anon");
chainDefinition.addPathDefinition("/v2/api-docs", "anon");
chainDefinition.addPathDefinition("/swagger-resources", "anon");
如果使用了权限框架,如shiro、SpringSecurity,需要WebMvcConfigurer里面添加配置:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 显示swagger-ui.html文档展示页,还必须注入swagger资源:
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
统一返回实体
@ApiModel("统一返回结果")
public class R<T> implements Serializable {
public static final long serialVersionUID = 1L;
@ApiModelProperty("状态码")
private int code;
@ApiModelProperty("响应信息")
private String msg;
@ApiModelProperty("结果")
private T data;
@ApiModelProperty("是否成功")
private boolean success;
private R(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
this.success = ResultCode.SUCCESS.getCode() == code;
}
private R(int code, String msg) {
this(code, msg, null);
}
private R(ResultCode resultCode) {
this(resultCode.getCode(), resultCode.getMessage());
}
private R(ResultCode resultCode, String msg) {
this(resultCode.getCode(), msg);
}
private R(ResultCode resultCode, T data) {
this(resultCode.getCode(), resultCode.getMessage(), data);
}
private R(ResultCode resultCode, String msg, T data) {
this(resultCode.getCode(), msg, data);
}
public static <T> R<T> success() {
return new R(ResultCode.SUCCESS);
}
public static <T> R<T> success(ResultCode resultCode) {
return new R(resultCode);
}
public static <T> R<T> success(ResultCode resultCode, String msg) {
return new R(resultCode, msg);
}
public static <T> R<T> success(String msg) {
return new R(ResultCode.SUCCESS, msg);
}
public static <T> R<T> success(T data) {
return new R(ResultCode.SUCCESS, data);
}
public static <T> R<T> success(String msg, T data) {
return new R(ResultCode.SUCCESS, msg, data);
}
public static <T> R<T> fail() {
return new R(ResultCode.FAILURE);
}
public static <T> R<T> fail(Integer code, String msg) {
return new R(code, msg);
}
public static <T> R<T> fail(ResultCode resultCode) {
return new R(resultCode);
}
public static <T> R<T> fail(ResultCode resultCode, String msg) {
return new R(resultCode, msg);
}
public static <T> R<T> fail(String msg) {
return new R(ResultCode.FAILURE, msg);
}
public static <T> R<T> fail(T data) {
return new R(ResultCode.FAILURE, data);
}
public static <T> R<T> fail(String msg, T data) {
return new R(ResultCode.FAILURE, msg, data);
}
public static boolean isSuccess(@Nullable R r) {
return ObjectUtils.nullSafeEquals(ResultCode.SUCCESS, r.getCode());
}
public static boolean isNotSuccess(@Nullable R r) {
return !isSuccess(r);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public boolean getSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
}
界面
设置全局token

只要在这里填入token,那么就会自动注入到所有请求的请求头中使用。

前言
官网文档:https://spring.io/projects/spring-cloud
构建工程
构建新的SpringCloud工程
最简单的方法是访问start.spring.io网站,选择合适的SpringBoot和SpringCloud版本,进行构建。
添加Spring Cloud到现有的 Springboot 项目
如果你想要添加 SpringCloud 到一个现有的 SpringBoot 应用,第一步是确定你需要的 SpringCloud版本,这个版本将取决于你所使用的SpringBoot的版本。
更多详细的版本对应关系,可以访问网址: https://start.spring.io/actuator/info

Dalston, Edgware, Finchley, Greenwich 都已到达终点将不再支持。
现在您已经知道了要使用哪个发布系列以及该发布系列的最新服务版本,您就可以将Spring Cloud BOM添加到应用程序中了。
<properties>
<spring-cloud.version>2022.0.1</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Spring Cloud Alibaba
各个版本适配请看alibaba:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
<!--版本依赖示例-->
<properties>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<!-- SpringCloud -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
基础组件
前言
Spring 事务
四大特性
隔离级别
七大传播机制
参考
- https://blog.csdn.net/m0_73533108/article/details/126688445
前言
我们使用 SpringBoot + Druid 完成了单机多数据源的切换,但是这时会出现一个问题,添加事务了以后,数据源切换出现了问题,那么应该怎么解决呢?(实际是解决数据一致性的问题)
抛出异常回滚时,子事务已经提交,无法回滚,会产生数据不一致的问题。
单机
实现事务内切换数据源(支持原生Spring声明式事务哟,仅此一家),并支持多数据源事务回滚(有了 它除了跨服务的事务你需要考虑分布式事务,其他都不需要,极大的减少了系统的复杂程度)
这里采用的方案是:
- 扩展
Spring Jdbc提供的抽象类AbstractRoutingDataSource,实现切换数据源 - 数据源配置主要有两种
- 基于配置文件(该文章基于这种实现,但支持动态添加)
- 基于数据库表
- 基于
AspectJ实现动态数据源切换,支持方法级、类级,优先方法级别 - 通过实现
ibatis.Transaction和TransactionFactory支持原生Spring的@Transaction的多数据源事务回滚
环境准备
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<druid.version>1.2.8</druid.version>
<mybatis-spring-boot.version>2.2.2</mybatis-spring-boot.version>
数据源切换
思路
扩展 Spring Jdbc 提供的抽象类 AbstractRoutingDataSource,实现切换数据源
- targetDataSources是目标数据源集合
- defaultTargetDataSource是默认数据源
- resolvedDataSources是解析后的数据源集合
- resolvedDefaultDataSource是解析后的默认数据源
- determineCurrentLookupKey 为抽象方法,通过扩展这个方法来实现数据源的切换,key是数据源的名称。
lookup key通常是绑定在线程上下文中,根据这个key去resolvedDataSources中取出DataSource
通过注解 + AOP + ThreadLocal 拦截方法后,添加数据源KEY到中,然后查询数据库时会获取到这个key,进而获取对应的数据源进行操作。
实现代码
- 首先我们需要使用一个注解作为AOP切面的拦截标识
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
String value() default DynamicDataSourceProperties.PRIMARY;
}
- 基于线程变量
ThreadLocal提供一个数据源切换的工具,采用 双端 队列存储,主要是为了支持嵌套数据源切换。
@Slf4j
public final class DataSourceContentHolder {
private static final ThreadLocal<Deque<String>> contentHolder = new NamedThreadLocal("dynamic-datasource") {
@Override
protected Object initialValue() {
// return new LinkedList<>();
return new ArrayDeque<>();
}
};
private DataSourceContentHolder() {
}
/**
* 给当前线程添加数据源
*
* @param dataSource
*/
public static void add(String dataSource) {
log.debug("添加数据源 => {}", dataSource);
contentHolder.get().add(dataSource);
}
/**
* 获取当前线程数据源
*
* @return
*/
public static String get() {
String ds = contentHolder.get().peek();
ds = StringUtils.isNotBlank(ds) ? ds : DynamicDataSourceProperties.PRIMARY;
log.debug("获取当前数据源 => {}", ds);
return ds;
}
/**
* 清空当前数据源
* 如果当前数据源不为空,则会只移除队列的元素
*/
public static void poll() {
Deque<String> queue = contentHolder.get();
String ds = queue.poll();
log.debug("移除数据源 => {}", ds);
if (queue.isEmpty()) {
contentHolder.remove();
}
}
/**
* 嵌套执行方法
*
* @param dataSource
* @param callback
*/
public static void call(String dataSource, Runnable callback) {
try {
add(dataSource);
callback.run();
} finally {
poll();
}
}
}
- 定义一个接口类并定义动态数据源的基本操作
/**
* 动态数据源配置
*/
public interface IDynamicDataSource {
/**
* 是否包含数据源
*
* @param key 数据源KEY
* @return
*/
boolean containKey(Object key);
/**
* 添加数据源
*
* @param key 数据源KEY
* @param dataSourceProperties 数据源配置
*/
void add(Object key, DataSourceProperties dataSourceProperties);
/**
* 添加数据源
*
* @param key 数据源KEY
* @param dataSource 数据源
*/
void add(Object key, DataSource dataSource);
/**
* 覆盖数据源Maps
*
* @param objectObjectMap
*/
void setMap(Map<Object, Object> objectObjectMap);
/**
* 删除数据源
*
* @param key
*/
void del(Object key);
/**
* 替换数据源
* <p>
* 如果数据源不存在则添加,存在则替换
* </p>
*
* @param key 被替换的数据源KEY
* @param dataSource 数据源
*/
void replace(Object key, DataSource dataSource);
/**
* 替换数据源
* <p>
* 如果数据源不存在则添加,存在则替换
* </p>
*
* @param key 被替换的数据源KEY
* @param dataSourceProperties 数据源配置
*/
void replace(Object key, DataSourceProperties dataSourceProperties);
/**
* 获取当前数据源
*
* @return 数据源
*/
Object get();
/**
* 通过数据源KEY,获取数据源
*
* @param key 数据源,不存在则为NULL
* @return
*/
Object get(String key);
/**
* 获取当前数据源的Key
*
* @return 数据源Key
*/
String getKey();
}
- 继承
Spring的AbstractRoutingDataSource并实现IDynamicDataSource接口完成数据源的管理
/**
* 实现多数数据源控制
*/
public class DynamicDataSource extends AbstractRoutingDataSource implements IDynamicDataSource {
private static volatile DynamicDataSource INSTANCE;
private static Map<Object, Object> dataSourceMap = new ConcurrentHashMap<>();
private static final ReentrantLock lock = new ReentrantLock();
public static DynamicDataSource getInstance() {
if (INSTANCE == null) {
synchronized (DynamicDataSource.class) {
if (INSTANCE == null) {
INSTANCE = new DynamicDataSource();
}
}
}
return INSTANCE;
}
public boolean containKey(Object key) {
boolean b = dataSourceMap.containsKey(key);
return b;
}
public void add(Object key, DataSourceProperties dataSourceProperties) {
DataSource dataSource = DataSourceFactory.createDataSource(dataSourceProperties);
add(key, dataSource);
}
public void add(Object key, DataSource dataSource) {
boolean hasKey = dataSourceMap.containsKey(key);
if (hasKey) {
// if (o instanceof DataSource) {
// DruidDataSource druidDataSource = (DruidDataSource) o;
// druidDataSource.close();
// }
throw new SXException("数据库连接池 KEY 重复");
}
dataSourceMap.put(key, dataSource);
setMap(dataSourceMap);
}
public void setMap(Map<Object, Object> objectObjectMap) {
lock.lock();
this.dataSourceMap = objectObjectMap;
setPrimary();
super.setTargetDataSources(dataSourceMap);
super.afterPropertiesSet();
lock.unlock();
}
/**
* 设置主数据源
*/
private void setPrimary() {
Object o = this.dataSourceMap.get(DynamicDataSourceProperties.PRIMARY);
if (o != null) {
this.setDefaultTargetDataSource(o);
}
}
public void del(Object key) {
Object o = dataSourceMap.get(key);
if (o != null) {
if (o instanceof DataSource) {
DruidDataSource dataSource = (DruidDataSource) o;
if (dataSource != null) {
dataSource.close();
dataSourceMap.remove(key);
setMap(dataSourceMap);
}
} else {
throw new SXException("数据池类型无法识别");
}
}
}
/**
* @param key 被替换的数据源KEY
* @param dataSource 数据源
*/
public void replace(Object key, DataSource dataSource) {
Object o = dataSourceMap.get(key);
dataSourceMap.put(key, dataSource);
if (DynamicDataSourceProperties.PRIMARY.equals(key)) {
this.setDefaultTargetDataSource(dataSource);
}
if (o != null) {
if (o instanceof DataSource) {
DruidDataSource dataSource1 = (DruidDataSource) o;
if (dataSource1 != null) {
dataSource1.close();
}
}
}
setMap(dataSourceMap);
}
/**
* @param key 被替换的数据源KEY
* @param dataSourceProperties 数据源配置
*/
public void replace(Object key, DataSourceProperties dataSourceProperties) {
DruidDataSource dataSource = DataSourceFactory.createDataSource(dataSourceProperties);
replace(key, dataSource);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContentHolder.get();
}
@Override
public Object get() {
return get(getKey());
}
@Override
public Object get(String key) {
return dataSourceMap.get(key);
}
@Override
public String getKey() {
return DataSourceContentHolder.get();
}
}
- 创建AOP切面,实施数据源切换拦截
@Aspect
@Component
@Slf4j
@Order(-1)
public class DataSourceAspect {
@Pointcut("@within(top.zsmile.common.datasource.annotation.DS) || @annotation(top.zsmile.common.datasource.annotation.DS)")
public void dataSourceAspect() {
}
@Before("dataSourceAspect()")
public void beforeSwitch(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DS methodDS = method.getAnnotation(DS.class);
String value = null;
if (methodDS != null) {
value = methodDS.value();
} else {
Class<?> aClass = joinPoint.getTarget().getClass();
DS annotation = aClass.getAnnotation(DS.class);
if (annotation != null) {
value = annotation.value();
}
}
if (checkValue(value)) {
DataSourceContentHolder.add(value);
}
}
/**
* 检查数据源是否存在
*
* @param value
* @return
*/
private boolean checkValue(final String value) {
if (!StringUtils.isEmpty(value)) {
boolean containKey = DynamicDataSource.getInstance().containKey(value);
if (!containKey) {
throw new SXException("数据源" + value + "未准备");
}
}
return true;
}
@After("dataSourceAspect()")
public void afterSwitchDS() {
DataSourceContentHolder.poll();
}
}
- 创建
Druid公共配置文件 (DruidProperties)、 动态数据源配置文件(DynamicProperties)、数据源配置文件(DataSourceProperties)
@Data
//@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DataSourceProperties {
private String driverClassName;
private String url;
private String username;
private String password;
/**
* Druid默认参数
*/
private int initialSize;
private int maxActive;
private int minIdle;
private long maxWait;
private long timeBetweenEvictionRunsMillis;
private long minEvictableIdleTimeMillis;
private long maxEvictableIdleTimeMillis;
private String validationQuery;
private int validationQueryTimeout;
private Boolean testOnBorrow;
private Boolean testOnReturn;
private Boolean testWhileIdle;
private Boolean poolPreparedStatements;
private int maxOpenPreparedStatements;
private Boolean sharePreparedStatements;
private String filters;
private String connectionProperties;
}
@Data
public class DruidProperties {
/**
* Druid默认参数
*/
private int initialSize = 5;
private int maxActive = 20;
private int minIdle = 5;
private long maxWait = 60 * 1000L;
private long timeBetweenEvictionRunsMillis = 60 * 1000L;
private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
private String validationQuery = "select 1 from DUAL";
private int validationQueryTimeout = -1;
private Boolean testOnBorrow = false;
private Boolean testOnReturn = false;
private Boolean testWhileIdle = true;
private Boolean poolPreparedStatements = true;
private int maxOpenPreparedStatements = -1;
private Boolean sharePreparedStatements = false;
private String filters = "stat,wall";
private String connectionProperties;
}
@Slf4j
@Getter
@Setter
@ConfigurationProperties(prefix = DynamicDataSourceProperties.PREFIX)
public class DynamicDataSourceProperties {
public static final String PREFIX = "spring.datasource.dynamic";
/**
* 默认数据源,master
*/
public static final String PRIMARY = "master";
/**
* 所有数据源配置
*/
private Map<String, DataSourceProperties> datasource = new LinkedHashMap<>();
/**
* Druid配置
*/
private DruidProperties druid = new DruidProperties();
}
- 数据源配置
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Resource
private DynamicDataSourceProperties dynamicDataSourceProperties;
/**
* 动态数据源
*
* @return
*/
@ConditionalOnMissingBean
@Bean(name = "dynamicDataSource")
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
Map<Object, Object> dataSourceMap = getDynamicDataSource();
dynamicDataSource.setMap(dataSourceMap);
return dynamicDataSource;
}
/**
* mybatis-spring start config sql-session-factory
*
* @param dataSource
* @return
*/
@Bean
public SqlSessionFactoryBeanCustomizer sqlSessionFactoryBeanCustomizer(@Qualifier("dynamicDataSource") DynamicDataSource dataSource) {
return new SqlSessionFactoryBeanCustomizer() {
@Override
public void customize(SqlSessionFactoryBean factoryBean) {
// 动态切换数据源事务必须要添加的。
//factoryBean.setTransactionFactory(new DynamicTransactionFactory());
factoryBean.setDataSource(dataSource);
}
};
}
/**
* 事务管理器
*
* @param dynamicDataSource 动态数据源
* @return
*/
@ConditionalOnMissingBean
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
/**
* 遍历数据源配置并加载
*
* @return
*/
private Map<Object, Object> getDynamicDataSource() {
DruidProperties druid = dynamicDataSourceProperties.getDruid();
Map<String, DataSourceProperties> dataSourcePropertiesMap = dynamicDataSourceProperties.getDatasource();
Map<Object, Object> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size());
dataSourcePropertiesMap.forEach((k, v) -> {
DataSourceProperties mergeProperties = DynamicDataSourceUtils.merge(v, druid);
DruidDataSource dataSource = DataSourceFactory.createDataSource(mergeProperties);
dataSourceMap.put(k, dataSource);
});
return dataSourceMap;
}
}
到目前为止,已经可以支持多数据源的切换,但是会有一个问题,那就是 添加了 Spring声明式事务注解@Transactional后就没有办法切换数据源了。
其实市面上比较成熟的Mybatis Plus提供的 多数据源也会存在这个问题,查看源代码
SpringManagedTransaction其实就可以知道原因,因为Spring 开启事务时会调getConnection()方法,其内部会缓存数据库连接。
那么问题是不是出在了数据库连接上,我们更换一下数据源的 Connection获取
多库事务问题
临时方案
如何解决切库事务问题?借助Spring的声明式事务处理,我们可以在多次切库操作时强制开启新的事务:
@DS("slave")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
但是处理当我们在主事务中抛出异常回滚的时候,子事务已经提交了,无法回滚,这时就产生数据错乱了。
实现代码
- 先在
DynamicDataSource重写getConnection方法
/**
* 处理多数据源的事务的关键
* @return
* @throws SQLException
*/
@Override
public Connection getConnection() throws SQLException {
DataSource dataSource =
super.getResolvedDataSources().get(getKey());
return dataSource.getConnection();
}
- 实现
Transaction。注意这里的Transaction是org.apache.ibatis.transaction.Transaction
@Slf4j
public class DynamicTransaction implements Transaction {
/**
* 数据源
*/
private final DataSource dataSource;
/**
* 主数据源连接
*/
private Connection connection;
/**
* 是否开启事务
*/
private Boolean isConnectionTransactional;
/**
* 是否自动提交
*/
private Boolean autoCommit;
/**
* 连接标识
*/
private String identification;
/**
* 其他连接缓存
*/
private ConcurrentMap<String, Connection> connections;
/**
* 构造器
*
* @param dataSource 数据源
*/
public DynamicTransaction(DataSource dataSource) {
Assert.notNull(dataSource, "No DataSource specified");
// 当前数据源Key
this.identification = DataSourceContentHolder.get();
this.dataSource = dataSource;
connections = new ConcurrentHashMap<>();
log.debug("init dynamic transaction, identify key => {},address => {}", this.identification, this);
}
/**
* @return
* @throws SQLException
*/
@Override
public Connection getConnection() throws SQLException {
/* 获取当前生效的数据源标识 */
String current = DataSourceContentHolder.get();
log.debug("current key => {}, identify key=> {}", current, this.identification);
// 如果当前数据源是主数据源
if (current.equals(this.identification)) {
// 如果为空则创建连接
if (this.connection == null) {
openConnection();
}
return this.connection;
} else {
/* 不是默认数据源,获取连接并设置属性 */
if (!connections.containsKey(current)) {
// 如果连接不包含该数据源KEY获取的连接
try {
Connection conn = this.dataSource.getConnection();
/* 自动提交属性和主数据源保持连接 */
conn.setAutoCommit(this.autoCommit);
connections.put(current, conn);
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("could not get jdbc connection", ex);
}
}
return connections.get(current);
}
}
/**
* 打开连接
*
* @throws SQLException
*/
private void openConnection() throws SQLException {
// 获取连接
this.connection = DataSourceUtils.getConnection(this.dataSource);
// 是否自动提交
this.autoCommit = this.getConnection().getAutoCommit();
// 确定当前连接是否是事务性的。
// 即通过 Spring 的事务管理器绑定到当前线程的
this.isConnectionTransactional =
DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
log.debug("jdbc connection [{}] will {} be managed by spring", this.connection, (this.isConnectionTransactional ? "" : "not"));
}
/**
* 提交事务
*
* @throws SQLException
*/
@Override
public void commit() throws SQLException {
// 如果主事务不为空 && 如果是 Spring 管理的事务性连接 && 不是自动提交
if (this.connection != null && this.isConnectionTransactional &&
!this.autoCommit) {
// 提交主连接的事务
log.debug("committing jdbc connection [{}]", this.connection);
this.connection.commit();
// 遍历提交子连接的事务
for (Connection conn : connections.values()) {
conn.commit();
}
}
}
/**
* 回滚事务
*
* @throws SQLException
*/
@Override
public void rollback() throws SQLException {
if (this.connection != null && this.isConnectionTransactional &&
!this.autoCommit) {
log.debug("rolling back jdbc connection [{}]", this.connection);
this.connection.rollback();
for (Connection conn : connections.values()) {
conn.rollback();
}
}
}
/**
* 关闭并释放连接
*
* @throws SQLException
*/
@Override
public void close() throws SQLException {
DataSourceUtils.releaseConnection(this.connection, this.dataSource);
for (Connection conn : connections.values()) {
DataSourceUtils.releaseConnection(conn, this.dataSource);
}
}
/**
* 获取事务超时时间
*
* @return
* @throws SQLException
*/
@Override
public Integer getTimeout() throws SQLException {
ConnectionHolder holder = (ConnectionHolder)
TransactionSynchronizationManager.getResource(dataSource);
if (holder != null && holder.hasTimeout()) {
return holder.getTimeToLiveInSeconds();
}
return null;
}
}
- 继承
SpringManagedTransactionFactory实现事务工厂DynamicTransactionFactory
public class DynamicTransactionFactory extends SpringManagedTransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new DynamicTransaction(dataSource);
}
}
- 将
DynamicTransactionFactory注入Mybaits的SqlSessionFactory
/**
* mybatis-spring start config sql-session-factory
*
* @param dataSource
* @return
*/
@Bean
public SqlSessionFactoryBeanCustomizer sqlSessionFactoryBeanCustomizer(@Qualifier("dynamicDataSource") DynamicDataSource dataSource) {
return new SqlSessionFactoryBeanCustomizer() {
@Override
public void customize(SqlSessionFactoryBean factoryBean) {
factoryBean.setTransactionFactory(new DynamicTransactionFactory()); // 添加到这里额
factoryBean.setDataSource(dataSource);
}
};
}
分布式事务
Atomikos
seata
引用
https://blog.csdn.net/qq_35789269/article/details/128125061
https://juejin.cn/post/7103913605539561509
https://blog.csdn.net/m0_73533108/article/details/126688445
https://blog.csdn.net/demohui/article/details/109659540?ops_request_misc=&request_id=&biz_id=102&utm_term=spring%20%E4%BA%8B%E5%8A%A1%E5%86%85%E5%88%87%E6%8D%A2%E6%95%B0%E6%8D%AE%E6%BA%90&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-109659540.142^v88^control,239^v2^insert_chatgpt
AbstractRoutingDataSource
我们先看一下这个类里面包含了几个变量。
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
这里我们先简单的说一下各个变量的作用:
- targetDataSources:存储未解析的目标的数据源Map
- defaultTargetDataSource: 未解析的默认的目标数据源
- lenientFallback:回退。稍后再说
- dataSourceLookup:dataSource解析器,默认是JNDI
- resolvedDataSources:将目标的数据源解析并保存成实际的数据链接后的map
- resolvedDefaultDataSource: 默认的数据源
Method afterPropertiesSet
afterPropertiesSet 是用来解析targetDataSources 来获取真正的dataSource
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
- 该方法会先判断targetDataSources是否为空,如果为空抛出异常。
- 新建一下HashMap,大小为targetDataSources的大小,然后赋值给resolvedDataSources
- 遍历targetDataSources,解析Key和DataSource,并存储到resolvedDataSources
- 如果设置了 defaultTargetDataSource【默认数据源】,则会将默认数据源解析后,存储到resolveDefaultDataSource
以上的流程可以看出,该方法必须在设置了targetDataSources后调用,才能生效。
所以我们在做多数据源时,有时会看到这样的代码:这是为了解析我们配置的数据源Bean。
AbstractRoutingDataSource.setTargetDataSources(Map(...))
AbstractRoutingDataSource.afterPropertiesSet();
Method resolveSpecifiedDataSource
- 当参数DataSource的类型为DataSource时候,则返回
- 如果是String时,因为默认时使用的JNDILookUp,所以这里会默认去根据JNDI解析。如果需要替换不同的datasourceLookUp,Spring内置了4种:
- BeanFactoryDataSourceLookup:在SpringBean容器里面查找dataSource。
- JndiDataSourceLookup:从JNDI数据源解析
- MapDataSourceLookup:从Map容器解析
- SingleDataSourceLookup:从单数据源直接返回
- 如果不是以上两种类型,则抛出异常信息。
/**
* Resolve the specified data source object into a DataSource instance.
* <p>The default implementation handles DataSource instances and data source
* names (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}).
* @param dataSource the data source value object as specified in the
* {@link #setTargetDataSources targetDataSources} map
* @return the resolved DataSource (never {@code null})
* @throws IllegalArgumentException in case of an unsupported value type
*/
protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource) dataSource;
}
else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String) dataSource);
}
else {
throw new IllegalArgumentException(
"Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
}
protected <T> T lookup(String jndiName, @Nullable Class<T> requiredType) throws NamingException {
Assert.notNull(jndiName, "'jndiName' must not be null");
String convertedName = this.convertJndiName(jndiName);
Object jndiObject;
try {
jndiObject = this.getJndiTemplate().lookup(convertedName, requiredType);
} catch (NamingException var6) {
if (convertedName.equals(jndiName)) {
throw var6;
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Converted JNDI name [" + convertedName + "] not found - trying original name [" + jndiName + "]. " + var6);
}
jndiObject = this.getJndiTemplate().lookup(jndiName, requiredType);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Located object with JNDI name [" + convertedName + "]");
}
return jndiObject;
}
springboot中使用undertow替换tomcat
引入maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除Tomcat容器 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入undertow容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
<exclusions>
<exclusion>
<!-- 排除websocket模块-->
<groupId>io.undertow</groupId>
<artifactId>undertow-websockets-jsr</artifactId>
</exclusion>
</exclusions>
</dependency>
基本配置
# === undertow 日志 ========
# 是否打开 undertow 日志,默认为 false
server.undertow.accesslog.enabled=false
# 设置访问日志所在目录
server.undertow.accesslog.dir=logs
# 日志格式
server.undertow.accesslog.pattern=common
# 日志文件前缀
server.undertow.accesslog.prefix=access_log.
# 日志文件后缀
server.undertow.accesslog.suffix=log
# 指定工作者线程的 I/0 线程数,默认为 2 或者 CPU 的个数
server.undertow.threads.io=2
# 指定工作者线程个数,默认为 I/O 线程个数的 8 倍
server.undertow.threads.worker=8
# 设置 HTTP POST 内容的最大长度,默认不做限制
server.undertow.max-http-post-size=4MB
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理;
server.undertow.buffer-size=1024
# 是否分配的直接内存(NIO直接分配的堆外内存)
server.undertow.direct-buffers=true
关于出现no sapjco3 in java.library.path错误
-
问题描述:出现
no sapjco3 in java.library.path异常时,可能是缺乏相应的文件,导致无法获取到sapjco的资源,导致异常。 -
解决办法:引入
jar和dll文件。文件在Git仓库\src\资源文件下获取。
- 将
sapjco3.dll放入jdk目录下的\jre\bin文件夹 - 将
sapjco3.jar放入jdk目录下的\jre\lib\ext文件夹
- 将
-
完整错误信息:

druid
Maven引入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<!-- 移除HikariCp数据源 -->
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>
多数据源
配置
- 设置Datasource properties
@Data
public class DataSourceProperties {
private String driverClassName;
private String url;
private String username;
private String password;
/**
* Druid默认参数
*/
private int initialSize = 5;
private int maxActive = 20;
private int minIdle = 5;
private long maxWait = 60 * 1000L;
private long timeBetweenEvictionRunsMillis = 60 * 1000L;
private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
private String validationQuery = "select 1 from DUAL";
private int validationQueryTimeout = -1;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;
private boolean testWhileIdle = true;
private boolean poolPreparedStatements = true;
private int maxOpenPreparedStatements = -1;
private boolean sharePreparedStatements = false;
private String filters = "stat,wall";
}
- yml配置
spring:
datasource:
druid:
# mysql 连接信息
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/smilex-boot?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
# 其它配置
time-between-eviction-runs-millis: 60000
keep-alive: true
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall
stat-view-servlet:
enabled: true
allow: 127.0.0.1
url-pattern: /druid/*
- 实现AbstractRoutingDataSource
/**
* 实现多数数据源控制
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static DynamicDataSource INSTANCE;
private static Map<Object, Object> dataSourceMap = new HashMap<>();
private static final ReentrantLock lock = new ReentrantLock();
public static DynamicDataSource getInstance() {
if (INSTANCE == null) {
synchronized (DynamicDataSource.class) {
if (INSTANCE == null) {
INSTANCE = new DynamicDataSource();
}
}
}
return INSTANCE;
}
public void addDataSource(Object key, DataSourceProperties dataSourceProperties) {
DataSource dataSource = DataSourceFactory.createDataSource(dataSourceProperties);
addDataSource(key, dataSource);
}
public void addDataSource(Object key, DataSource dataSource) {
Object o = dataSourceMap.get(key);
if (o != null) {
// if (o instanceof DataSource) {
// DruidDataSource druidDataSource = (DruidDataSource) o;
// druidDataSource.close();
// }
throw new SXException("数据库连接池 KEY 重复");
}
dataSourceMap.put(key, dataSource);
setDataSourceMap(dataSourceMap);
}
public void setDataSourceMap(Map<Object, Object> objectObjectMap) {
lock.lock();
this.dataSourceMap = objectObjectMap;
super.setTargetDataSources(dataSourceMap);
super.afterPropertiesSet();
lock.unlock();
}
public void delDataSource(Object key) {
Object o = dataSourceMap.get(key);
if (o != null) {
if (o instanceof DataSource) {
DruidDataSource dataSource = (DruidDataSource) o;
if (dataSource != null) {
dataSource.close();
dataSource = null;
dataSourceMap.remove(key);
setDataSourceMap(dataSourceMap);
}
} else {
throw new SXException("数据池类型无法识别");
}
}
}
/**
* Recording to the datasource of the map with the key
* @param key
* @param dataSource
*/
public void replaceDataSource(Object key, DataSource dataSource) {
dataSourceMap.put(key, dataSource);
setDataSourceMap(dataSourceMap);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContentHolder.getDataSource();
}
}
- 配置Spring DynamicDatasouce Bean
@Configuration
@Order(-1)
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
DataSource dataSource = DataSourceFactory.createDataSource(dataSourceProperties);
dynamicDataSource.addDataSource("master", dataSource);
dynamicDataSource.setDefaultTargetDataSource(dataSource);
return dynamicDataSource;
}
@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
- 设置DataSourceFactory 工厂,用于根据配置生成DataSource
public class DataSourceFactory {
public static DataSource createDataSource(DataSourceProperties properties) {
DruidDataSource dataSource = new DruidDataSource();
BeanUtils.copyProperties(properties, dataSource);
return dataSource;
}
public static DataSource createDataSource(DataSource dataSource, DataSourceProperties properties) {
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
BeanUtils.copyProperties(properties, druidDataSource);
return druidDataSource;
}
}
使用方式
注解
- 添加注解 DataSource
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default DynamicDataSourceConfig.MASTER;
}
- 添加拦截器 DataSourceAspect
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
@Pointcut("@within(top.zsmile.common.datasource.annotation.DS) || @annotation(top.zsmile.common.datasource.annotation.DS)")
public void dataSourceAspect() {
}
@Before("dataSourceAspect()")
public void beforeSwitch(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DataSource methodDS = method.getAnnotation(DataSource.class);
String value = null;
if (methodDS != null) {
value = methodDS.value();
} else {
Class<?> aClass = joinPoint.getTarget().getClass();
DataSource annotation = aClass.getAnnotation(DataSource.class);
if (annotation != null) {
value = annotation.value();
}
}
if (checkValue(value)) {
DataSourceContentHolder.setDataSource(value);
}
}
private boolean checkValue(final String value) {
if (!StringUtils.isEmpty(value)) {
boolean containKey = DynamicDataSource.getInstance().containKey(value);
if (!containKey) {
throw new SXException("数据源" + value + "未准备");
}
}
return true;
}
@After("dataSourceAspect()")
public void afterSwitchDS() {
DataSourceContentHolder.clear();
}
}
- 在需要切换数据源的地方添加注解
多次切换数据源
前面我们使用的是ThreadLocal
@DataSource("test1")
public void A(){
AMapper.queryById(1);
B();
AMapper.queryById(3);
}
@DataSource("test2")
public void B(){
BMapper.queryById(2)
}
这里我们可以预计一个样子。
- 当 AMapper.queryById(1) 时,指向数据源 test1
- 当执行 BMapper.queryById(2) 时,指向 test2
- 再执行 AMapper.queryById(1) 时,指向数据源 test1。
可是问题来了,我们用ThreadLocal
我们应该如何来解决这个问题。
我们可以通过替换ThreadLocal
@Slf4j
public class DataSourceContentHolder {
private static final ThreadLocal<Queue<String>> contentHolder = new ThreadLocal() {
@Override
protected Object initialValue() {
return new LinkedList<>();
}
};
public synchronized static void setDataSource(String dataSource) {
contentHolder.get().add(dataSource);
}
public static String getDataSource() {
String ds = contentHolder.get().peek();
log.debug("当前数据源 => {}", ds);
return ds;
}
public static void pollDataSource() {
Queue<String> queue = contentHolder.get();
String ds = queue.poll();
log.debug("移除数据源 => {}", ds);
if (queue.isEmpty()) {
contentHolder.remove();
}
}
}
引入Maven
<!--二维码生成和解析相关的jar包【生成】【解析】-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
google.zxing 解析二维码异常问题
今天发现两个内容一模一样的二维码,但是一个是使用苹果6的手机截图的,一个是苹果13截图的,但是苹果6的会抛出一个NoFoundException异常。
有问题的代码:
private static String decodeQrcode(URL file) {
BufferedImage image = ImgUtil.read(file);
LuminanceSource source = new BufferedImageLuminanceSource(image);
Binarizer binarizer = new HybridBinarizer(source);
BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
Map<DecodeHintType, Object> hints = Maps.newEnumMap(DecodeHintType.class);
hints.put(DecodeHintType.CHARACTER_SET, Charsets.UTF_8.name());
try {
com.google.zxing.Result result = new MultiFormatReader().decode(binaryBitmap, hints);
return result.getText();
} catch (NotFoundException e) {
e.printStackTrace();
System.out.println(e.toString());
return "";
}
}
这时就会出现二维码异常的问题,大部分的二维码可以正常解析,但是有些二维码会报异常。经过各种尝试,做了一下更改:
-
HybridBinarizer换成GlobalHistogramBinarizer
- zxing官方默认的HybridBinarizer,两者区别HybridBinarizer算法执行效率上要慢一些,但是更有效,专门针对黑白相间的图像设计,也更适用于有阴影和渐变的二维码图像。
- GlobalHistogramBinarizer算法适用于低端机,对手机要求不高,识别速度快,精度高,但是无法处理阴影和渐变两个情况。
-
MultiFormatReader换成QRCodeMultiReader
- 这里指定了单个类型,即QRCode。但是尝试了QrCodeReader解析发现还是会出现异常。于是使用了批量解析的方式,然后默认选取第一张。
更改完后,代码如下:
private static String decodeQrcode(URL file) {
BufferedImage image = ImgUtil.read(file);
LuminanceSource source = new BufferedImageLuminanceSource(image);
Binarizer binarizer = new GlobalHistogramBinarizer(source);
BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
Map<DecodeHintType, Object> hints = Maps.newEnumMap(DecodeHintType.class);
hints.put(DecodeHintType.POSSIBLE_FORMATS, EnumSet.allOf(BarcodeFormat.class));
hints.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE);
hints.put(DecodeHintType.TRY_HARDER, BarcodeFormat.QR_CODE);
hints.put(DecodeHintType.CHARACTER_SET, Charsets.UTF_8.name());
try {
// QRCodeReader qrCodeReader =new QRCodeReader();
// return qrCodeReader.decode(binaryBitmap,hints).getText();
com.google.zxing.Result result[] = new QRCodeMultiReader().decodeMultiple(binaryBitmap, hints);
return result[0].getText();
} catch (NotFoundException e) {
e.printStackTrace();
System.out.println(e.toString());
return "";
}
}
前言
该文章基于 springboot-2.3.5-final 和 hibernate-validator.6.1.6 版本
在日常开发中,对象的校验是一个非常重要的环节,用于校验手机邮箱,用户提交的信息是否正确。
如果我们在代码里面写入了大量的if-else不仅仅复用性极差,而且维护起来也极其麻烦,我们做项目,不单单要实现功能,还要优雅。所以求求你们别用if-else做校验了。
在这时候Validator框架应运而生,就是为了减少大量的if-else校验代码,提高开发效率。
概论
JSR303 定义了 Bean Validation 的标准 Validation-api,但是没有提供具体实现。hibernate Validator 是对这个规范的实现,并且 spring 内置的校验框架是 hibernate-validator。
PS:虽然由hibernate字眼,但不是跟hibernate ORM框架强关联。
- JSR303:JSR303是一项标准,只提供规范不提供实现。定义了校验规范即校验注解如:@Null、@NotNull、@Pattern。位于:
javax.validation.constraints包下。 - hibernate validator: 是对 JSR303 规范的实现并且进行了增强和扩展。并增加了注解:@Email、@Length、@Range等。
- Spring Validation: 是对Hibernate Validation的二次封装。在SpringMvc模块中添加了自动校验。并将校验信息封装到特定的类中。同时提供此类CDI基础结构
CDI
CDI(Contexts And Dependency Injection) 是JavaEE 6标准中一个规范,将依赖注入IOC/DI上升到容器级别, 它提供了Java EE平台上服务注入的组件管理核心,简化应该是CDI的目标,让一切都可以被注解被注入。
CDI是一种依赖注入说明:
- spring框架中隐式提供了此类CDI基础结构
- hibernate-validator-cdi。提供给 独立的Java应用程序,需要它来使用注释创建
HibernateValidator
springboot
集成
我这里用maven做例子,如果使用gradle或者其它包管理工具,替换成对应写法即可
<!--对应hibernate-validator版本6.1.6-final -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.5</version>
</dependency>
配置
快速失败
hibernate-validator 默认时会将所有参数校验后,再抛出异常。有时我们想一旦校验失败就马上抛出异常,可以配置注入进去
@Configuration
public class ValidatorConfig {
/**
* 快速返回校验器
*
* @return
*/
@Bean
@ConditionalOnMissingBean(value = Validator.class)
public Validator validator() {
//hibernate-validator 6.x没问题,7.x有问题
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
/**
* 设置快速校验,返回方法校验处理器。
* 使用MethodValidationPostProcessor注入后,会启动自定义校验器
*
* @return
*/
@Bean
@ConditionalOnMissingBean(value = MethodValidationPostProcessor.class)
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
postProcessor.setValidator(validator());
return postProcessor;
}
}
全局错误拦截器
package top.zsmile.core.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import top.zsmile.common.core.api.R;
import top.zsmile.common.core.api.ResultCode;import top.zsmile.common.core.exception.SXException;
import java.util.List;
@Slf4j
@RestControllerAdvice
public class SXExceptionHandler {
/**
* 自定义异常
*/
@ExceptionHandler(SXException.class)
public R handleException(SXException e) {
log.error(e.getMessage(), e);
return R.fail(e.getMessage());
}
private static final String SPLINT = ",";
@ExceptionHandler(BindException.class)
public R BindException(BindException e) {
log.error(e.getMessage(), e);
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
StringBuffer sb = new StringBuffer("");
for (FieldError fieldError : fieldErrors) {
sb.append(fieldError.getDefaultMessage()).append(SPLINT);
}
return R.fail(sb.substring(0, sb.length() - 1));
}
}
自定义校验器
Bean验证主要有两部分构成:
- 声明约束及其可配置属性的@Constraint注释。
- constraintvalidator接口的实现,它实现了约束的行为。
这里实现了一个手机号验证。
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
/**
* 异常消息
* @return
*/
String message() default "手机号格式不正确";
/**
* 分组
* @return
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PhoneValidator implements ConstraintValidator<Phone, String> {
@Override
public void initialize(Phone constraintAnnotation) {
// 在这里获取一些注解上的值,比如message,require,设置为局部变量即可。
}
@Override
public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isEmpty(phone)) {
return true;
}
return ValidatorUtil.isPhone(phone);
}
}
这里简单封装一个ValidatorUtil库
public class ValidatorUtil {
private static final Pattern MOBILE_PATTERN = Pattern.compile("^(13[0-9]|14[5|7|9]|15[0|1|2|3|5|6|7|8|9]|17[0|1|6|7|8]|18[0-9])\\d{8}$");
public static boolean isPhone(String src) {
if (StringUtils.isEmpty(src)) {
return false;
}
Matcher matcher = MOBILE_PATTERN.matcher(src);
return matcher.matches();
}
}
使用案例
方案1(@Valid/@Validated+Entity)
使用实体类,在实体类上加校验注解,在对应接口上添加@Valid,实现接口校验。
// 实体类
@Data
public class DatabaseConnEntity implements Serializable {
private static final long serialVersionUID = -4123125798831566630L;
/**
* 连接类型:mysql
*/
@NotBlank(message = "连接类型不能为空")
private String type;
@Phone
private String phone;
}
// 接口
@PostMapping("/connect")
public R connect(@Valid DatabaseConnEntity databaseConnEntity) {
return R.success();
}
// 接口2
@PostMapping("/connect")
public R connect(@Validated DatabaseConnEntity databaseConnEntity) {
return R.success();
}
方案2(@Validated+Params)
在接受参数上加校验注解
@Validated
@RestController
@RequestMapping("/generator")
public class GeneratorController {
@GetMapping("/info")
public R info(@NotBlank String tableName) {
Map<String, String> maps = generatorService.queryTable(tableName);
if (maps == null) {
return R.fail("查询不到该表结构");
} else {
return R.success(maps);
}
}
}
方案3(使用Validator)
public class ValidatorUtils {
private final static Validator validator;
static {
// 这里可以使用 Factory工厂,也可使用Spring容器里的Bean对象。
if (SpringContextUtils.getBean(Validator.class) != null) {
validator = SpringContextUtils.getBean(Validator.class);
} else {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
}
/**
* 校验对象
*
* @param object 待校验对象
* @param groups 待校验的组
* @throws SXException 校验不通过,则报SXException异常
*/
public static void validateEntity(Object object, Class<?>... groups)
throws SXException {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
StringBuilder msg = new StringBuilder();
for (ConstraintViolation<Object> constraint : constraintViolations) {
msg.append(constraint.getMessage()).append("<br>");
}
throw new SXException(msg.toString());
}
}
}
// 使用方法
ValidatorUtils.validator(Phone);
分组校验
组校验
- 设置组
public interface Add(){ }
public interface Update(){}
- 在实体类里设置组
public class user{
@NotBlank(message = "手机不能为空",groups = {Add.class})
@Phone(groups={Update.class})
private String phone;
}
- 校验
// 接口2
@PostMapping("/connect")
public R connect(@Validated({Add.class}) DatabaseConnEntity databaseConnEntity) {
return R.success();
}
组序列
默认情况下 不同级别的约束验证是无序的,但是在一些情况下,顺序验证却是很重要。
一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。
@GroupSequence({Add.class, Update.class})
public interface change(){}
用法和组校验一样,只需要设置成该分组即可
Hibernate-validator-cdi(TODO,非本文重点,后续研究)
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.6-final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-cdi</artifactId>
<version>6.1.6-final</version>
</dependency>
其它
@Validated 和 @Valid
-
相同:
- 在Controller中校验方法参数时,使用@Valid和@Validated并无特殊差异(不需要分组校验的情况下)
- 都支持方法和参数注解
-
不同:
- @Valid是属于javax.validation包,而@Validated属于Spring下的。
- @Validated支持分组使用,@Valid不支持
- @Valid支持嵌套校验,而@Validated不支持
- @Valid支持成员属性、构造函数注解,而@Validated不支持
参考文章
- spring-validator
- https://www.cnblogs.com/sanye613/p/15027448.html
- https://rumenz.com/java-topic/hibernate/hibernate-validator-cdi/index.html
前言
最近在做登录授权,因为需要保存用户密码到数据库,那此时引发了一个思考:
-
密码明文存入,那任何人员登录数据库后,都能轻而易举的拿到用户密码,并进行登录。也就是明文存入数据库,极其容易泄露。
-
既然密码不能明文存入数据,那只能使用密文。
- 如果只是对密码进行加密后存入会导致什么问题呢?同一个加密出来的密文是一致的。
- 密文是否可逆?通过密文是不是能反编译出明文?
-
那针对加密的基础上,是不是可以通过额外的一个变量(salt),来进行加密,这样同样的密码+salt,得出来的密文就是不一样的了。
基于以上几点思考,这篇文章会着重讲述一下随机数生成的几种方式,主要是用来生成种子。
Salt生成
Salt就是一串随机数,只是我们都知道会分为伪随机和真随机,那么用真随机和伪随机产生的随机数就会达成不同的效果。
Random
我们最常用的方式如下:
Random random = new Random();
int num = random.nextInt(10);
那现在简单的看一下 Random 的源码。
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// subclass might have overriden setSeed
this.seed = new AtomicLong();
setSeed(seed);
}
}
private static long seedUniquifier() {
// L'Ecuyer, "Tables of Linear Congruential Generators of
// Different Sizes and Good Lattice Structure", 1999
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
private static final AtomicLong seedUniquifier
= new AtomicLong(8682522807148012L);
从这里可以看出 Random类,默认使用了 当前系统时间(System.nanoTime()) 和 静态AtomicLong 两者的异或结果 作为种子。
也就是说当我们同时并发的时候,seed 是一样时,产生出来的随机数也是一样的。因为他们基于的随机数算法也是一样的,所以这是可以预测出来的。
我们再来看一下常用的方法 nextInt() 。
public int nextInt() {
return next(32);
}
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
我们看到了这里使用了 AtomicLong 作为seed,Atomic原子类虽然保证了数据的原子性,但是其底层的CAS机制,使得在多线程并发下,性能并不算乐观。
简单写了个demo 跑了下并发
Random random = new Random();
new Thread(() -> {
while (true) {
int i = random.nextInt(1000);
System.out.println(System.currentTimeMillis() + "--" + i);
}
}).start();
new Thread(() -> {
while (true) {
int i = random.nextInt(1000);
System.out.println(System.currentTimeMillis() + "--" + i);
}
}).start();
new Thread(() -> {
while (true) {
int i = random.nextInt(1000);
System.out.println(System.currentTimeMillis() + "--" + i);
}
}).start();
new Thread(() -> {
while (true) {
int i = random.nextInt(1000);
System.out.println(System.currentTimeMillis() + "--" + i);
}
}).start();
从执行结果上来看,4线程执行下,平均每1秒可以生成280个随机数。那有什么办法来解决多线程下的随机数生成呢?
ThreadLocalRandom
ThreadLocalRandom 是继承 Random 类,并在其基础上解决了并发生成的问题。
每一个线程有一个独立的随机数生成器,在并发的时候不需要跟别的线程竞争。效率更高!
new Thread(() -> {
ThreadLocalRandom current = ThreadLocalRandom.current();
while (true) {
int i = current.nextInt();
System.out.println(System.currentTimeMillis() + "--" + i);
}
}).start();
ThreadLocalRandom 通过 current 方法获取当前线程所持有的 随机数生成器实例。
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
localInit 方法是用来生成当前线程的属性。仅在 Thread.threadLocalRandomProbe为0时调用,表示需要一个线程本地seed值需要生成。注意:尽管初始化是 ThreadLocal 的,但我们需要依赖一个(静态)原子生成器来初始化值。
SecureRandom
我们知道 Random 实现的算法是伪随机,也就是有规律的随机算法。进行随机时,在随机算法的种子seed 的基础上进行一定的变换,从而产生随机数字。
当不可预测性至关重要时, 如大多数对安全性要求较高的环境可以使用密码学的 PRNG。 不管选择了哪一种 PRNG, 都要始终使用带有充足熵的数值作为该算法的种子。 (诸如当前时间之类的数值只提供很小的熵, 因此不应该使用(Random的实现)。 )
-
SecureRandom 也是继承于 Random
-
SecureRandom提供加密的强随机生成器(RNG)。
-
SecureRandom 和 Random 都是种子一样时,生成出来的随机数也是一样的。二者区别在于:
- SecureRandom 的种子必须是不可预测的。
- Random 的种子是基于系统时间的
注意:根据实现的不同,{@code generateSeed}和{@code nextBytes}方法可能会因为熵被收集而阻塞,例如,如果它们需要从各种类unix操作系统上的/dev/random读取。
SecureRandom,实现强随机的核心方法在于:SecureRandomSpi.engineNextBytes 方法
public SecureRandom() {
/*
* This call to our superclass constructor will result in a call
* to our own {@code setSeed} method, which will return
* immediately when it is passed zero.
*/
super(0);
getDefaultPRNG(false, null);
}
private void getDefaultPRNG(boolean setSeed, byte[] seed) {
String prng = getPrngAlgorithm();
if (prng == null) {
// bummer, get the SUN implementation
prng = "SHA1PRNG";
this.secureRandomSpi = new sun.security.provider.SecureRandom();
this.provider = Providers.getSunProvider();
if (setSeed) {
this.secureRandomSpi.engineSetSeed(seed);
}
} else {
try {
SecureRandom random = SecureRandom.getInstance(prng);
this.secureRandomSpi = random.getSecureRandomSpi();
this.provider = random.getProvider();
if (setSeed) {
this.secureRandomSpi.engineSetSeed(seed);
}
} catch (NoSuchAlgorithmException nsae) {
// never happens, because we made sure the algorithm exists
throw new RuntimeException(nsae);
}
}
// JDK 1.1 based implementations subclass SecureRandom instead of
// SecureRandomSpi. They will also go through this code path because
// they must call a SecureRandom constructor as it is their superclass.
// If we are dealing with such an implementation, do not set the
// algorithm value as it would be inaccurate.
if (getClass() == SecureRandom.class) {
this.algorithm = prng;
}
}
使用方法
配置 -Djava.security参数来修改调用的算法
// 没有配置时,默认与SHA1PRNG
SecureRandom secureRandom = new SecureRandom();
SecureRandom secureRandom1 = SecureRandom.getInstance("SHA1PRNG");
// 通常可以用来作为其它算法的种子
secureRandom.generateSeed(20);
SpringBoot 配置ssh连接数据库
前言
有时候我们无法直连上数据库,这时就需要通过代理的形式。
方案1
- 导入依赖包
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.50</version>
</dependency>
- 添加ssh配置
ssh.enabled: true
ssh.remote.ip: 192.168.0.1
ssh.remote.port: 22
ssh.remote.username: root
ssh.remote.password: root
ssh.remote.target_host: 192.168.0.2
ssh.remote.target_port: 3306
ssh.local.resource_host: 127.0.0.1
ssh.local.resource_port: 3307
- properties配置类
@Configuration
@Data
@PropertySource(value = "classpath:/config/remote-ssh.properties", encoding = "utf-8", ignoreResourceNotFound = true)
@ConditionalOnResource(resources = {"classpath:/config/remote-ssh.properties"})
public class SshProperties {
@Value("${ssh.enabled}")
private boolean enabled;
@Value("${ssh.remote.ip}")
private String ip;
@Value("${ssh.remote.port}")
private int port;
@Value("${ssh.remote.username}")
private String username;
@Value("${ssh.remote.password}")
private String passowrd;
@Value("${ssh.remote.target_host}")
private String targetHost;
@Value("${ssh.remote.target_port}")
private int targetPort;
@Value("${ssh.local.resource_host}")
private String resourceHost;
@Value("${ssh.local.resource_port}")
private int resourcePort;
}
// 或者这样的方式
@Configuration
@Data
@PropertySource(value = "classpath:/config/remote-ssh.properties", encoding = "utf-8", ignoreResourceNotFound = true)
@ConditionalOnResource(resources = {"classpath:/config/remote-ssh.properties"})
@ConfigurationProperties(prefix = "ssh")
public class SshProperties {
// @Value("${ssh.enabled:false}")
private boolean enabled;
@Bean
public Remote getRemote() {
return new Remote();
}
@Bean
public Local getLocal() {
return new Local();
}
@Data
@Configuration
@ConfigurationProperties("ssh.remote")
public class Remote {
// @Value("${ssh.remote.ip}")
private String ip;
// @Value("${ssh.remote.port}")
private int port;
// @Value("${ssh.remote.username}")
private String username;
// @Value("${ssh.remote.password}")
private String passowrd;
// @Value("${ssh.remote.target_host}")
private String targetHost;
// @Value("${ssh.remote.target_port}")
private int targetPort;
}
@Data
@Configuration
@ConfigurationProperties("ssh.local")
public class Local {
// @Value("${ssh.local.resource_host}")
private String resourceHost;
// @Value("${ssh.local.resource_port}")
private int resourcePort;
}
}
- 使用jsch加载SshProperties
@Component
@Slf4j
@ConditionalOnBean(SshProperties.class)
public class SshConfig implements ServletContextInitializer {
private static Session session;
@Autowired(required = false)
private SshProperties sshProperties;
public Session initSession() {
try {
//如果配置文件包含ssh.enabled属性,则使用ssh转发
if (sshProperties.isEnabled()) {
Session session = new JSch().getSession(sshProperties.getRemote().getUsername(), sshProperties.getRemote().getIp(), sshProperties.getRemote().getPort());
session.setConfig("StrictHostKeyChecking", "no");
session.setPassword(sshProperties.getRemote().getPassowrd());
session.connect();
//将本地端口的请求转发到目标地址的端口
session.setPortForwardingL(sshProperties.getLocal().getResourceHost(), sshProperties.getLocal().getResourcePort(), sshProperties.getRemote().getTargetHost(), sshProperties.getRemote().getTargetPort());
log.info("ssh forward is open.");
return session;
} else {
log.info("ssh forward is closed.");
}
} catch (JSchException e) {
log.error("ssh JSchException failed.", e);
} catch (Exception e) {
log.error("ssh settings is failed. skip!", e);
}
return null;
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
session = initSession();
}
/**
* 断开SSH连接
*/
public void destroy() {
this.session.disconnect();
}
}
到这一步就大功告成了。
参考文章
待研究
cdi
- cdi和di
- spring相关
- jsr330和jsr229
- ejb
https://www.jdon.com/38322
https://zhuanlan.zhihu.com/p/337965169
https://rumenz.com/java-topic/hibernate/hibernate-validator-cdi/index.html
hibernate-validator-cdi
https://rumenz.com/java-topic/hibernate/hibernate-validator-cdi/index.html
redis
shiro-redis
- http://alexxiyang.github.io/shiro-redis/
- https://github.com/alexxiyang/shiro-redis/tree/master/docs
springboot-redis
- https://lettuce.io/core/5.3.5.RELEASE/reference/index.html#spring.spring-data-redis
- https://docs.spring.io/spring-data/redis/docs/current/reference/html/#redis:connectors:connection
springboot-cache
- CacheResolver
- CacheErrorHandler
- 参考:https://blog.csdn.net/sz85850597/article/details/89301331
tcpip
- websocket
- sockjs
- sockio
- netty
- 长轮询
- stomp
质量测试工具
- sonarqube
- arthas
鎖
MPSC 。caffeine的寫鎖機制
Blob下载
const url = window.URL.createObjectURL(new Blob([response]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'video.mp4');
document.body.appendChild(link);
link.click();
基于CANVAS视频抽帧显示
export interface videoInfo {
blob: Blob | null;
url: string;
}
// 画视频
const drawVideo = (video: HTMLVideoElement) => {
return new Promise<videoInfo>(success => {
const cvs = document.createElement('canvas');
const ctx = cvs.getContext('2d');
cvs.width = video.videoWidth;
cvs.height = video.videoHeight;
ctx?.drawImage(video, 0, 0, cvs.width, cvs.height);
cvs.toBlob(blob => {
success({
blob,
url: URL.createObjectURL(blob as Blob)
});
});
});
};
// 视频截取工具
export function captureFrame(videoFile: File, time: number = 0) {
return new Promise<videoInfo>(succeed => {
const video = document.createElement('video');
video.currentTime = time;
video.muted = true;
video.preload = 'auto';
video.autoplay = true;
video.setAttribute('crossOrigin', 'Anonymous'); // 处理跨域
video.setAttribute('preload', 'auto'); // auto|metadata|none
video.src = URL.createObjectURL(videoFile);
video.oncanplay = async () => {
// 延时500ms防止白屏问题
setTimeout(async () => {
const res = await drawVideo(video);
succeed(res);
}, 500);
};
});
}
// blob\file转base64
export function toBase64(blob: Blob | File) {
return new Promise<string>(success => {
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = (e: any) => {
success(e.target.result);
}
})
}
颜色工具
const ColorMaker = {
// 最大支持颜色种类数
TotalColors: 300,
// 获取颜色 seed:颜色种子(任意int)
getColor: (seed = 0) => {
var ret = {
TextColor: 'FFFFFF',
Color: 'FF0000'
}
// 计算对应下标
var idx = seed % ColorMaker.TotalColors;
// 计算颜色
var colorVal = ColorMaker._CalColor(idx);
// 转成RGB 16进制字符串
ret.Color = colorVal.toString(16).padStart(6, '0');
// 计算互补色
ret.TextColor = ColorMaker._CalTextColor(ret.Color);
return ret;
},
getRgbColor: (seed: number = 0) => {
const hex: string = ColorMaker.getColor(seed).Color;
return hexToRGB(hex);
},
getRgbColorArr: (seed: number = 0): number[] => {
const hex: string = ColorMaker.getColor(seed).Color;
return hexToRGBArr(hex);
},
_CalColor: (idx = 0) => {
// 默认返回红色
var ret = 0xFF0000;
// RGB的最大值
var full = 0xFFFFFF;
// 总共需要支持多少种颜色,若传0则取255
var total = ColorMaker.TotalColors > 0 ? ColorMaker.TotalColors : 0xFF;
// 将所有颜色平均分成x份
var perVal = full / total;
if (idx >= 0 && idx <= total) {
ret = perVal * idx;
}
ret = Math.round(ret);
return ret;
},
// 计算传入颜色的互补色
_CalTextColor: (input = '') => {
var R = input.substr(0, 2);
var G = input.substr(2, 2);
var B = input.substr(4, 2);
var rVal = parseInt(R, 16);
var gVal = parseInt(G, 16);
var bVal = parseInt(B, 16);
var hsl = rgbToHsl(rVal, gVal, bVal);
hsl.L = (hsl.L + 0.5) % 1.0;
var rgb = hslToRgb(hsl.H, hsl.S, hsl.L);
var ret = (rgb.R << 16) + (rgb.G << 8) + rgb.B;
return ret.toString(16).padStart(6, '0');
}
};
/**
* RGB 颜色值转换为 HSL.
* 转换公式参考自 http://en.wikipedia.org/wiki/HSL_color_space.
* r, g, 和 b 需要在 [0, 255] 范围内
* 返回的 h, s, 和 l 在 [0, 1] 之间
*
* @param Number r 红色色值
* @param Number g 绿色色值
* @param Number b 蓝色色值
* @return Array HSL各值数组
*/
function rgbToHsl(r: number, g: number, b: number) {
r /= 255, g /= 255, b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h: number = 0, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
let d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return { H: h, S: s, L: l };
}
/**
* HSL颜色值转换为RGB.
* 换算公式改编自 http://en.wikipedia.org/wiki/HSL_color_space.
* h, s, 和 l 设定在 [0, 1] 之间
* 返回的 r, g, 和 b 在 [0, 255]之间
*
* @param Number h 色相
* @param Number s 饱和度
* @param Number l 亮度
* @return Array RGB色值数值
*/
function hslToRgb(h: number, s: number, l: number) {
var r, g, b;
if (s == 0) {
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p: number, q: number, t: number) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return { R: Math.round(r * 255), G: Math.round(g * 255), B: Math.round(b * 255) };
}
export function hexToRGB(hex: string) {
// 移除井号(如果存在)
if (hex.charAt(0) === '#') {
hex = hex.slice(1);
}
// 确保长度是6个字符(包括两个字符用于每个RGB分量)
if (hex.length !== 6) {
throw new Error('Invalid hex color. It should be 6 characters long including the hash.');
}
// 将十六进制字符串转换为RGB分量
var r = parseInt(hex.substring(0, 2), 16);
var g = parseInt(hex.substring(2, 4), 16);
var b = parseInt(hex.substring(4, 6), 16);
return `rgb(${r}, ${g}, ${b})`;
}
export function hexToRGBArr(hex: string) {
// 移除井号(如果存在)
if (hex.charAt(0) === '#') {
hex = hex.slice(1);
}
// 确保长度是6个字符(包括两个字符用于每个RGB分量)
if (hex.length !== 6) {
throw new Error('Invalid hex color. It should be 6 characters long including the hash.');
}
// 将十六进制字符串转换为RGB分量
var r = parseInt(hex.substring(0, 2), 16);
var g = parseInt(hex.substring(2, 4), 16);
var b = parseInt(hex.substring(4, 6), 16);
return [r, g, b]
}
export default ColorMaker;
Upload-Image
<script setup lang="ts">
import { ref, h, defineEmits, watch } from "vue"
import type { UploadFileInfo, UploadInst, UploadCustomRequestOptions } from 'naive-ui'
import { fetchCommonUpload } from "@/service/api";
import { $t } from "@/locales";
// import { toBase64 } from "@/utils/videoFrame";
/**
* @description 图片上传
* @param options 上传的文件
* */
interface UploadEmits {
(e: "success", value: string): void;
(e: "check-validate"): void;
}
const emit = defineEmits<UploadEmits>();
type FileTypes =
| "image/apng"
| "image/bmp"
| "image/gif"
| "image/jpeg"
| "image/pjpeg"
| "image/png"
| "image/svg+xml"
| "image/tiff"
| "image/webp"
| "image/x-icon";
interface UploadFileProps {
defaultUrl: string;
filelength: number; // 文件数
fileSize: number; // 文件大小,M
fileType: FileTypes[];
disabled: boolean;
size: string;
borderRadius: string;
}
// 接受父组件参数
const props = withDefaults(defineProps<UploadFileProps>(), {
defaultUrl: '',
disabled: false,
filelength: 1,
fileSize: 5,
fileType: () => ["image/jpeg", "image/png", "image/gif"],
size: "120px",
borderRadius: "100%",
});
const fileList = ref<UploadFileInfo[]>([])
const uploadRef = ref<UploadInst | null>()
const fileUrl = ref<string>("");
watch(() => props.defaultUrl, (newValue) => {
if (fileUrl.value === '') {
fileUrl.value = newValue;
console.log(fileUrl.value)
}
});
async function handleUpload({
file,
data,
headers,
withCredentials,
action,
onFinish,
onError,
onProgress
}: UploadCustomRequestOptions) {
// fileUrl.value = await toBase64(file.file as File)
const res = await fetchCommonUpload(file.file as File);
fileUrl.value = res.url
emit("success", fileUrl.value)
}
function handleBeforeUpload(data: {
file: UploadFileInfo
fileList: UploadFileInfo[]
}) {
const fileInfo = data.file
const size = fileInfo.file?.size || props.fileSize
// 检查文件大小
if (size > props.fileSize * 1024 * 1024) {
window.$message?.warning(`请上传不超过${props.fileSize}M的文件`);
return false;
}
if (props.fileType.indexOf(fileInfo.type as FileTypes) < 0) {
window.$message?.warning(`上传图片不符合所需的格式`);
return false;
}
return true;
}
function handleRemove() {
fileList.value = []
}
function handleView() {
window.$dialog?.info({
title: $t('common.preview'),
content: () => {
return h(
"img",
{ src: fileUrl.value, class: 'w-100% object-cover' }
)
},
maskClosable: true,
showIcon: false
});
}
</script>
<template>
<n-upload ref="uploadRef" abstract v-model:file-list="fileList" :custom-request="handleUpload"
:accept="props.fileType.join(',')" @before-upload="handleBeforeUpload" :disabled="props.disabled">
<div :style="['width:' + props.size, 'height:' + props.size]"
class="border-dotted border-1 bordre-#c0ccda border-rd-2 position-relative cursor-pointer overflow-hidden">
<n-upload-trigger #="{ handleClick }" abstract class="size-full position-relative">
<div class="size-full position-relative" @click="handleClick" v-if="fileUrl === ''">
<icon-local-plus class="text-24px position-absolute absolute top-50% left-50% translate--50%" />
</div>
<div :style="['border-radius:' + props.borderRadius]"
class="size-full mr-10px position-absolute overflow-hidden top-50% left-50% translate--50%" v-else>
<img :src="fileUrl" class="size-full" />
<div
class="position-absolute top-0 size-full z-index-10 opacity-0 hover:opacity-100 bg-#f9f9f999 transition-all-300 transition-ease-linear">
<div class="position-absolute top-50% left-50% translate--50% w-80% flex flex-row justify-around">
<icon-local-view class="text-24px" @click="handleView" />
<icon-local-upload class="text-24px" @click="() => { handleRemove(); handleClick() }" />
<!-- <icon-local-delete class="text-24px" @click="handleRemove" /> -->
</div>
</div>
</div>
</n-upload-trigger>
</div>
</n-upload>
</template>
<style scoped></style>
base64
background
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)
canvas
function renderCanvas(img: HTMLImageElement) {
const canvas = canvasRef.value;
const ctx = canvas.getContext('2d')
ctx.fillStyle = ctx.createPattern(img, 'repeat');
ctx.scale(0.5, 0.4);
ctx.fillRect(0, 0, canvas.width / 0.5, canvas.height / 0.4);
}
let img = new Image();
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'
if (img.complete) {
renderCanvas(img)
} else {
img.onload = () => {
renderCanvas(img)
}
}
计算坐标
function cropImage(imagedata: ImageData) {
let left = -1, top = -1, right = -1, bottom = -1;
const idata = imagedata.data
const count = idata.length;
const countWidth = imagedata.width * 4;
console.log(count)
for (let i = 0, y = 0; i < count; i = i + 4) {
if (i != 0 && i % countWidth == 0) {
y++;
}
// console.log([idata[i], idata[i + 1], idata[i + 2], idata[i + 3]])
if (
idata[i] !== 255
|| idata[i + 1] !== 255
|| idata[i + 2] !== 255
|| idata[i + 3] !== 255
) {
const x = (i - countWidth * y) / 4;
if (left === -1) {
left = x
} else {
if (left > x) {
left = x;
}
if (right === -1) {
right = x
} else if (right < x) {
right = x
}
}
if (top === -1) {
top = y;
} else {
if (bottom === -1) {
bottom = y
} else if (bottom < y) {
bottom = y
}
}
}
}
return [left, top, right - left, bottom - top]
}
前言
官方demo : https://github.com/microsoft/onnxruntime-inference-examples/blob/main/js/segment-anything/main.js
-
版本
- "@tensorflow/tfjs": "^4.17.0",
- "onnxruntime-web": "^1.17.0",
-
配置
-
/** * Create service config by current env * * @param env The current env */ export function createModelMap(env: Env.ImportMeta) { // const mockURL = 'https://mock.apifox.com/m1/3109515-0-default'; const modelType: App.Model.ModelConfigMap = { dev: { wasm: { 'ort-wasm.wasm': '/files/ort-wasm.wasm', 'ort-wasm-simd.wasm': '/files/ort-wasm-simd.wasm', 'ort-wasm-threaded.wasm': '/files/ort-wasm-threaded.wasm', 'ort-wasm-simd-threaded.wasm': '/files/ort-wasm-simd-threaded.wasm', }, onnx: { sam_b: ['/models/sam_vit_b_01ec64.encoder.onnx', "/models/sam_vit_b_01ec64.decoder.onnx"], mobile_sam: ['/models/mobile_sam.encoder.onnx', "/models/mobile_sam.decoder.onnx"] } }, test: { wasm: { 'ort-wasm.wasm': '/files/ort-wasm.wasm', 'ort-wasm-simd.wasm': '/files/ort-wasm-simd.wasm', 'ort-wasm-threaded.wasm': '/files/ort-wasm-threaded.wasm', 'ort-wasm-simd-threaded.wasm': '/files/ort-wasm-simd-threaded.wasm', }, onnx: { sam_b: ['/models/sam_vit_b_01ec64.encoder.onnx', "/models/sam_vit_b_01ec64.decoder.onnx"], mobile_sam: ['/models/mobile_sam.encoder.onnx', "/models/mobile_sam.decoder.onnx"] } }, prod: { wasm: { 'ort-wasm.wasm': '/files/ort-wasm.wasm', 'ort-wasm-simd.wasm': '/files/ort-wasm-simd.wasm', 'ort-wasm-threaded.wasm': '/files/ort-wasm-threaded.wasm', 'ort-wasm-simd-threaded.wasm': '/files/ort-wasm-simd-threaded.wasm', }, onnx: { sam_b: ['/models/sam_vit_b_01ec64.encoder.onnx', "/models/sam_vit_b_01ec64.decoder.onnx"], mobile_sam: ['/models/mobile_sam.encoder.onnx', "/models/mobile_sam.decoder.onnx"] } }, }; const { VITE_SERVICE_ENV = 'dev' } = env; return modelType[VITE_SERVICE_ENV]; } -
onnx配置
ort.env.wasm.numThreads = data.threads; ort.env.wasm.simd = true; // ort.env.wasm.proxy = true; ort.env.wasm.wasmPaths = globalModelMap.wasm as ort.Env.WasmPrefixOrFilePaths; -
模型大小设置。这里需要注意的是onnx转换后,模型图像的宽高固定为1024*684,如果图像什么出现问题,需要做一下转换和计算。
-
封装库
- segment.ts
import * as ort from 'onnxruntime-web';
import * as tf from '@tensorflow/tfjs'
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { captureFrame, toBase64 } from '@/utils/videoFrame'
import { BlockItem, ModelMapType, ConfigType, UploadType, FrameItem, BlockItemType, PointOperaType, SegmentOperaType } from "./types/segment.d.js"
import dayjs from 'dayjs';
import ColorMaker from '@/utils/colorUtils.js';
import { createModelMap } from '~/env.config';
const MODEL_WIDTH = 1024;
const MODEL_HEIGHT = 684;
const globalModelMap = createModelMap(import.meta.env)
const MODEL_MAP: ModelMapType = globalModelMap.onnx
const modelSess: ort.InferenceSession[] = []
function getConfig(): ConfigType {
const data: ConfigType = {
model: "mobile_sam",
provider: { name: "wasm" },//wasm,webgpu,webnn
device: "cpu",
threads: 4,
clear_cache: false
};
ort.env.wasm.numThreads = data.threads;
return data;
}
ort.env.wasm.simd = true;
// ort.env.wasm.proxy = true;
ort.env.wasm.wasmPaths = globalModelMap.wasm as ort.Env.WasmPrefixOrFilePaths;
const config = getConfig();
/**
* fetch and cache url
*/
async function fetchAndCache(url: string) {
try {
const cache = await caches.open("onnx");
if (config.clear_cache) {
cache.delete(url);
}
let cachedResponse = await cache.match(url);
if (cachedResponse == undefined) {
await cache.add(url);
cachedResponse = await cache.match(url);
// console.log(`${url} (from network)`);
} else {
// console.log(`${url} (from cache)`);
}
const data = await cachedResponse?.arrayBuffer();
return data
} catch (error) {
// console.log(`${url} (from network)`);
return await fetch(url).then(response => {
// {
// mode: "cors",
// headers: {
// "Cross-Origin-Embedder-Policy": "require-corp",
// "Cross-Origin-Opener-Policy": "same-origin"
// },
// referrerPolicy: "same-origin",
// credentials: "same-origin"
// }
// response.headers.set("Cross-Origin-Embedder-Policy", "require-corp");
// response.headers.set("Cross-Origin-Opener-Policy", "same-origin");
return response.arrayBuffer()
});
}
}
function loadModel(model: string[], idx: number) {
const provider = config.provider
switch (config.provider.name) {
case "webnn":
if (!("ml" in navigator)) {
throw new Error("webnn is NOT supported");
}
provider.deviceType = config.device;
provider.powerPreference = 'default'
break;
case "webgpu":
if (!navigator.gpu) {
throw new Error("webgpu is NOT supported");
}
break;
}
const opt = { executionProviders: [provider] };
fetchAndCache(model[idx]).then(async (data: any) => {
// sess[idx] = ort.InferenceSession.create('./models/sam_vit_b_01ec64.encoder.onnx');
// const u8data = new Uint8Array(data);
modelSess[idx] = await ort.InferenceSession.create(data, opt);
// console.log(`${model[idx]} loaded.`);
// sess[idx].then(() => {
// console.log(`${model[idx]} loaded.`);
if (idx == 0) {
loadModel(model, 1);
}
// }, (e) => {
// console.log(`${model[idx]} failed with ${e}.`);
// throw e;
// });
})
}
export default function useSegment() {
onMounted(() => {
const model: string[] = MODEL_MAP[config.model as keyof ModelMapType];
loadModel(model, 0);
})
onUnmounted(() => {
modelSess.forEach((item) => {
if (item != null) {
item.release();
}
})
})
const frameList = reactive<FrameItem[]>([]);
const frameIndex = ref(0);
const blockList = reactive<BlockItem[]>([]);
const blockIndex = ref(0);
const videoUploadRef = ref();
const videoUploadLoading = ref(false);
const fileBase64 = ref('');
const frameBase64 = ref('');
const canvasRef = ref();
const previewRef = ref();
let colorIdx = 1, MAX_WIDTH: number = 1000, MAX_HEIGHT: number = 360
const _useBlocks = useBlocks()
const _useModel = useModel()
const _useMediaUpload = useMediaUpload()
const _useFrames = useFrames();
// 当前帧的图像数据
let imageImageData: ImageData | undefined, imageEmbeddings: ort.Tensor;
function useFrames() {
function frameAddHandle(addIndex: boolean = true, obj: { sec: number, type: number, base64: string, imageData?: ImageData, imageEmbeddings?: ort.Tensor }) {
frameList.push({
id: String(dayjs().valueOf()) + Math.random() * (100 - 1) + 1,
type: obj.type,
sec: obj.sec,
base64: obj.base64,
imageData: obj.imageData,
imageEmbeddings: obj.imageEmbeddings
});
if (addIndex) { frameIndex.value += 1; }
}
function frameDelHandle() {
}
return {
frameAddHandle,
frameDelHandle
}
}
function useBlocks() {
const blockShowAll = ref(false); // true 展示全部,false 展示一个
function blockAddHandle(obj: { type?: BlockItemType }, addIndex: boolean = true) {
blockShowAll.value = false;
blockList.push({
id: String(dayjs().valueOf()) + Math.random() * (100 - 1) + 1,
type: obj.type || 'mask',
hasPoint: false,
color: ColorMaker.getRgbColorArr(colorIdx),
pointModel: [],
pointXY: [],
resource: '',
resourceType: '',
imgBase64: '',
imgData: undefined,
result: null,
zIndex: 100 - blockIndex.value,
segmentImage: '',
segmentImageBase64: '',
segmentImageBitmap: null
});
colorIdx++;
if (addIndex) { blockIndex.value += 1; blockShowHandle(blockIndex.value); }
}
function blockDelHandle(index: number) {
if (blockList.length === 1) {
window.$message?.warning('请保留一层蒙版');
} else {
blockList.splice(index, 1);
if (blockIndex.value >= blockList.length) {
blockIndex.value = blockList.length - 1;
}
}
blockShowHandle();
}
function blockShowHandle(index: number = -1) {
const canvas = canvasRef.value
const ctx = canvas.getContext('2d');
ctx.globalAlpha = 0.5;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// todo: 等后面换成多帧后,需要从对应获取
ctx.putImageData(imageImageData, 0, 0, 0, 0, canvas.width, canvas.height);
if (index >= 0) {
blockIndex.value = index;
blockShowAll.value = false;
const block = blockList[index];
ctx.drawImage(block.segmentImageBitmap, 0, 0);
} else {
blockShowAll.value = true;
for (let i = blockList.length - 1; i >= 0; i--) {
const block = blockList[i];
ctx.drawImage(block.segmentImageBitmap, 0, 0);
}
}
}
return {
blockList,
blockIndex,
blockShowAll,
blockAddHandle,
blockDelHandle,
blockShowHandle,
}
}
function useMediaUpload() {
const uploadType: UploadType = {
type: 1,
blockIndex: 0,
};
async function uploadHandle(type: number, bIndex: number = 0) {
const vur = videoUploadRef.value;
uploadType.type = type;
uploadType.blockIndex = bIndex;
switch (type) {
case 1:
vur.accept = 'video/*'
break;
case 2:
vur.accept = 'video/*,image/*'
break;
case 3:
vur.accept = 'video/*'
}
vur.onchange = uploadVideo;
vur.click();
}
async function uploadVideo() {
if (uploadType.type === 1) {
uploadMedia((img) => {
_useBlocks.blockAddHandle({ type: "mask" }, false);
_useModel.imageHandle(img, 'frame')
})
} else if (uploadType.type === 2) {
uploadResource(uploadType.blockIndex)
} else if (uploadType.type === 3) {
uploadMedia((img) => {
// _useBlocks.blockAddHandle(false);
// _useModel.imageHandle(img)
_useModel.imageRenderHandle(img);
})
}
}
async function uploadMedia(callback: (img: HTMLImageElement) => void) {
const vur = videoUploadRef.value;
const file: File = vur.files[0];
if (file) {
frameList.length = 0;
frameIndex.value = 0
blockList.length = 0;
blockIndex.value = 0;
videoUploadLoading.value = true
const img = new Image()
if (file.type.startsWith("image/")) {
const base64 = await toBase64(file)
img.src = base64
fileBase64.value = img.src
frameBase64.value = img.src
} else {
const info = await captureFrame(file, 1);
img.src = await toBase64(info.blob as Blob)
fileBase64.value = await toBase64(file);
frameBase64.value = img.src
}
_useFrames.frameAddHandle(false, {
sec: 1,
type: uploadType.type,
base64: frameBase64.value,
})
videoUploadLoading.value = false
if (img.complete) {
if (callback != null) {
callback(img)
}
} else {
img.onload = () => {
if (callback != null) {
callback(img)
}
}
}
vur.value = '';
}
}
async function uploadResource(index: number) {
if (index > blockList.length || index < 0) {
return
}
const vur = videoUploadRef.value;
const file: File = vur.files[0];
if (file) {
const item = blockList[index];
item.resource = await toBase64(file);
if (file.type.startsWith('image/')) {
item.resourceType = 'image';
} else if (file.type.startsWith('video/')) {
item.resourceType = 'video';
}
vur.value = '';
}
}
return {
videoUploadRef,
videoUploadLoading,
fileBase64,
frameBase64,
uploadMedia,
uploadResource,
uploadHandle
}
}
function useModel() {
let pointOpera: PointOperaType = 'plus';
let segmentOpera: SegmentOperaType = 'mask'
async function imageRenderHandle(img: HTMLImageElement) {
let renderLoading = videoUploadLoading.value ? 1 : 0;
if (renderLoading === 0) {
videoUploadLoading.value = true
}
let width = img.width;
let height = img.height;
if (width > MAX_WIDTH) {
height = height * (MAX_WIDTH / width);
width = MAX_WIDTH;
}
// } else {
if (height > MAX_HEIGHT) {
width = width * (MAX_HEIGHT / height);
height = MAX_HEIGHT;
}
// }
width = Math.round(width);
height = Math.round(height);
const canvas: HTMLCanvasElement = canvasRef.value;
canvas.width = width;
canvas.height = height;
let context = canvas.getContext('2d');
context?.drawImage(img, 0, 0, width, height);
if (renderLoading === 0) {
videoUploadLoading.value = false
}
}
async function imageHandle(img: HTMLImageElement, type: "block" | "frame") {
imageRenderHandle(img);
videoUploadLoading.value = true;
const frame = frameList[frameIndex.value]
const block = blockList[blockIndex.value];
if (block.imgBase64 === '') {
block.imgBase64 = frameBase64.value;
}
const canvas: HTMLCanvasElement = canvasRef.value;
let width = canvas.width
let height = canvas.height
let context = canvas.getContext('2d');
setTimeout(async () => {
imageImageData = context?.getImageData(0, 0, width, height);
if (!frame.imageData) {
frame.imageData = imageImageData
}
let resizeTensor = await ort.Tensor.fromImage(imageImageData as unknown as ImageBitmap, { resizedWidth: MODEL_WIDTH, resizedHeight: MODEL_HEIGHT })
let tfTensor = tf.tensor(resizeTensor.data, [...resizeTensor.dims]);
tfTensor = tfTensor.reshape([3, MODEL_HEIGHT, MODEL_WIDTH]);
tfTensor = tfTensor.transpose([1, 2, 0]).mul(255)
const imageDataTensor = new ort.Tensor(tfTensor.dataSync(), tfTensor.shape);
let start = Date.now();
const feed = { "input_image": imageDataTensor };
const res = await modelSess[0].run(feed);
let end = Date.now();
let time_taken = (end - start) / 1000;
console.log(`Computing image embedding took ${time_taken} seconds`);
imageEmbeddings = res.image_embeddings;
if (type === "block") {
block.imageEmbeddings = res.image_embeddings;
} else if (type === 'frame') {
frame.imageEmbeddings = res.image_embeddings;
}
videoUploadLoading.value = false;
}, 500)
}
async function clickHandle(e: MouseEvent, label: number, fn?: (imgB64: string) => void) {
// const originWidth = imageFrameRef.value.naturalWidth;
// const offseWidth = imageFrameRef.value.offsetWidth;
// const xscale = offseWidth / originWidth;
// const originHeight = imageFrameRef.value.naturalHeight;
// const offsetHeight = imageFrameRef.value.offsetHeight;
// const yscale = offsetHeight / originHeight;
// 获取x,y坐标
// const x = Math.floor(e.offsetX / xscale);
// const y = Math.floor(e.offsetY / yscale);
// console.log(image_embeddings)
// if (image_embeddings === undefined) {
// await sess[0];
// }
// const emb = await image_embeddings;
const canvas = canvasRef.value
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
const x = Math.floor(e.clientX - rect.left);
const y = Math.floor(e.clientY - rect.top);
console.log(x, y)
const frame = frameList[frameIndex.value]
const block = blockList[blockIndex.value];
if (pointOpera === 'plus') {
block.pointModel.push(label);
block.pointXY.push(x, y);
} else {
block.pointModel = [label];
block.pointXY = [x, y];
}
// canvas.width = imageImageData.width;
// canvas.height = imageImageData.height;
// console.log(canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.putImageData(imageImageData, 0, 0, 0, 0, canvas.width, canvas.height);
// ctx.fillStyle = 'blue';
// ctx.fillRect(x, y, 10, 10);
// const labels = [label]
const labels = block.pointModel;
const points = block.pointXY;
const pointCoords = new ort.Tensor(new Float32Array(points), [1, points.length / 2, 2]);
const pointLabels = new ort.Tensor(new Float32Array(labels), [1, labels.length]);
const maskInput = new ort.Tensor(new Float32Array(256 * 256), [1, 1, 256, 256]);
const hasMask = new ort.Tensor(new Float32Array([0]), [1,]);
const origianlImageSize = new ort.Tensor(new Float32Array([MODEL_HEIGHT, MODEL_WIDTH]), [2,]);
// const t = new ort.Tensor(emb.image_embeddings.type, Float32Array.from(emb.image_embeddings.data), emb.image_embeddings.dims);
// console.log("t", t)
// const t = new ort.Tensor(image_embeddings.type, Float32Array.from(image_embeddings.data), image_embeddings.dims);
const feed = {
"image_embeddings": imageEmbeddings,
"point_coords": pointCoords,
"point_labels": pointLabels,
"mask_input": maskInput,
"has_mask_input": hasMask,
"orig_im_size": origianlImageSize
}
console.log("feed", feed)
// const start = Date.now();
try {
const results = await modelSess[1].run(feed);
console.log("Generated mask:", results);
const mask = results.masks;
const maskImageData = mask.toImageData();
segmentTypeHandle(maskImageData, block)
block.hasPoint = true;
if (fn) {
fn(block.segmentImageBase64);
}
} catch (error) {
console.log(`caught error: ${error}`)
}
// const end = Date.now();
// console.log(`generating masks took ${(end - start) / 1000} seconds`);
}
function cropImage(imagedata: ImageData) {
let left = -1, top = -1, right = -1, bottom = -1;
const idata = imagedata.data
const count = idata.length;
const countWidth = imagedata.width * 4;
console.log(count)
for (let i = 0, y = 0; i < count; i = i + 4) {
if (i != 0 && i % countWidth == 0) {
y++;
}
// console.log([idata[i], idata[i + 1], idata[i + 2], idata[i + 3]])
if (
idata[i] !== 255
|| idata[i + 1] !== 255
|| idata[i + 2] !== 255
|| idata[i + 3] !== 255
) {
const x = (i - countWidth * y) / 4;
if (left === -1) {
left = x
} else {
if (left > x) {
left = x;
}
if (right === -1) {
right = x
} else if (right < x) {
right = x
}
}
if (top === -1) {
top = y;
} else {
if (bottom === -1) {
bottom = y
} else if (bottom < y) {
bottom = y
}
}
}
}
return [left, top, right - left, bottom - top]
}
async function segmentTypeHandle(maskImageData: ImageData, block: BlockItem) {
const idata = maskImageData.data
const count = idata.length;
if (segmentOpera === 'mask') {
for (let i = 0; i < count; i = i + 4) {
// console.log([idata[i], idata[i + 1], idata[i + 2], idata[i + 3]])
if (idata[i] > 0) {
idata[i] = block.color[0];
idata[i + 1] = block.color[1]
idata[i + 2] = block.color[2]
idata[i + 3] = 178; // 透明度
} else {
idata[i + 3] = 0
}
}
} else {
for (let i = 0; i < count; i = i + 4) {
// console.log([idata[i], idata[i + 1], idata[i + 2], idata[i + 3]])
if (idata[i] > 0) {
idata[i + 3] = 0; // 透明度
} else {
idata[i] = 255
idata[i + 1] = 255
idata[i + 2] = 255
idata[i + 3] = 255
}
}
}
const canvas = canvasRef.value
const ctx = canvas.getContext('2d');
// ctx.globalAlpha = 0.5;
// convert image data to image bitmap
// let imageBitmap = await createImageBitmap(maskImageData, { resizeWidth: canvas.width, resizeHeight: canvas.height, resizeQuality: "medium" });
let imageBitmap = await createImageBitmap(maskImageData);
// canvas.width = maskImageData.width;
// canvas.height = maskImageData.height;
ctx.drawImage(imageBitmap, 0, 0);
if (segmentOpera === 'crop') {
const locate = cropImage(ctx.getImageData(0, 0, canvas.width, canvas.height));
const imagedata = ctx.getImageData(locate[0], locate[1], locate[2], locate[3])
imageBitmap = await createImageBitmap(imagedata);
const tmpCanvas = document.createElement('canvas');
const tmpCtx = tmpCanvas.getContext('2d');
// 设置canvas的尺寸并将复制的ImageData绘制到canvas上
tmpCanvas.width = locate[2]
tmpCanvas.height = locate[3]
tmpCtx?.drawImage(imageBitmap, 0, 0);
block.segmentImageBase64 = tmpCanvas.toDataURL()
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.putImageData(imageImageData, 0, 0, 0, 0, canvas.width, canvas.height);
previewHandle(imagedata)
} else {
const tmpCanvas = document.createElement('canvas');
const tmpCtx = tmpCanvas.getContext('2d');
// 设置canvas的尺寸并将复制的ImageData绘制到canvas上
tmpCanvas.width = canvas.width;
tmpCanvas.height = canvas.height;
tmpCtx?.drawImage(imageBitmap, 0, 0);
block.segmentImageBase64 = tmpCanvas.toDataURL()
}
block.segmentImage = canvas.toDataURL();
block.segmentImageBitmap = imageBitmap;
}
function previewHandle(imagedata: ImageData) {
if (previewRef.value) {
const tagName = previewRef.value.tagName
if (tagName === 'CANVAS') {
const canvas = previewRef.value
const ctx = canvas.getContext('2d')
canvas.width = imagedata.width;
canvas.height = imagedata.height;
ctx.putImageData(imagedata, 0, 0, 0, 0, imagedata.width, imagedata.height);
} else if (tagName === 'IMG') {
const tmpCanvas = document.createElement('canvas');
const tmpCtx = tmpCanvas.getContext('2d');
// 设置canvas的尺寸并将复制的ImageData绘制到canvas上
tmpCanvas.width = imagedata.width;
tmpCanvas.height = imagedata.height;
tmpCtx?.putImageData(imagedata, 0, 0, 0, 0, imagedata.width, imagedata.height);
previewRef.value.src = tmpCanvas.toDataURL()
}
}
}
function setMaxSize(width: number, height: number) {
MAX_WIDTH = width;
MAX_HEIGHT = height
}
function setSegmentOpera(tempSegmentOpera: SegmentOperaType) {
segmentOpera = tempSegmentOpera
}
function setPointOpera(type: PointOperaType) {
pointOpera = type
}
return {
canvasRef,
previewRef,
imageHandle,
imageRenderHandle,
clickHandle,
setPointOpera,
setSegmentOpera,
setMaxSize,
}
}
return {
_useBlocks,
_useModel,
_useMediaUpload,
_useFrames,
}
}
- segment.d.ts
import * as ort from 'onnxruntime-web';
type xy = {
x: number;
y: number;
};
export type BlockItemType = "mask" | "panel"
export interface BlockItem {
id: string;
type: BlockItemType,// mask遮罩,panel切图
hasPoint: boolean; // 是否操作
color: number[]; // 颜色 rgba
pointModel: number[]; // 操作模式
pointXY: number[]; // 操作点
resource: string;
resourceType: string;
imgBase64: string; // 图片base64
imgData: ImageData | undefined;
result: Api.AiTools.SegmentFrame | null;
zIndex: number;
segmentImage: string;// 图片遮罩
segmentImageBase64: string;// 图片遮罩base64
segmentImageBitmap: ImageBitmap | null;// 图片遮罩位图
imageEmbeddings?: ort.Tensor | undefined;
}
export interface ModelMapType {
sam_b: string[];
mobile_sam: string[];
}
export interface ProviderType extends ort.InferenceSession.ExecutionProviderOption {
deviceType?: string;
powerPreference?: string;
}
export interface ConfigType {
model: string;
provider: ProviderType;
device: string;
threads: number;
clear_cache: boolean;
}
export interface UploadType {
type: number; // 1是主视频,2是每一帧的蒙版视频,3是普通图片
blockIndex: number;
}
export interface FrameItem {
id: string;
type: number; // UploadType.type
sec: number;// 视频第几秒
base64: string;// 图片BASE64
imageData: ImageData | undefined;
imageEmbeddings: ort.Tensor | undefined;
}
export type PointOperaType = 'plus' | 'cover';// 追加/覆盖
export type SegmentOperaType = 'mask' | 'crop';// 遮罩/截取
- 使用
<script setup lang="ts">
import { ref } from 'vue';
import useSegment from "@/hooks/ai/segment"
import { $t } from '@/locales';
const segment = useSegment();
const {
videoUploadRef, videoUploadLoading, fileBase64, frameBase64, uploadHandle } = segment._useMediaUpload
const { blockList, blockIndex, blockAddHandle, blockDelHandle, blockShowHandle, } = segment._useBlocks;
const { canvasRef, clickHandle } = segment._useModel
const MediaLabelRadioRef = ref<InstanceType<typeof MediaLabelRadio>>()
async function imageClickHandle(e: MouseEvent) {
clickHandle(e, MediaLabelRadioRef.value?.getValue() as number)
}
</script>
<template>
<div>
<n-layout>
<n-layout has-sider>
<n-layout-sider bordered content-class="p-12px" :default-collapsed="true" :collapsed-width="0"
:show-collapsed-content="false" :width="160" show-trigger="arrow-circle">
<n-spin :show="videoUploadLoading">
<n-flex vertical>
<n-button type="primary" @click="uploadHandle(1)">{{ $t('tapai.uploadVideo') }}</n-button>
<n-button v-show="frameBase64 !== ''" type="primary" @click="blockAddHandle({})">{{ $t('tapai.addMask')
}}</n-button>
<n-button v-show="frameBase64 !== ''" type="primary" @click="renderVideo">{{ $t('tapai.renderVideo')
}}</n-button>
</n-flex>
</n-spin>
</n-layout-sider>
<n-layout-content content-class="p-12px">
<input ref="videoUploadRef" type="file" class="hidden" />
<n-spin :show="videoUploadLoading">
<div class="h-360px flex flex-row flex-items-center flex-justify-center">
<n-empty v-show="frameBase64 === ''" size="large" description="请不要上传大小超过100M,时长超过10M中的视频">
<template #extra>
<n-button size="small" @click="uploadHandle(1)">{{ $t('tapai.uploadVideo') }}</n-button>
</template>
</n-empty>
<div v-show="frameBase64 != ''" class="h-100% position-relative">
<canvas @click="imageClickHandle" ref="canvasRef"></canvas>
</div>
</div>
</n-spin>
</n-layout-content>
</n-layout>
<n-layout-footer bordered>
<n-spin v-show="frameBase64 != ''" :show="videoUploadLoading">
<div class="p-y-12px flex flex-row flex-justify-center">
<div
class="flex flex-col border-solid border-0px bg-#fff dark:bg-#2d2d32 cursor-pointer h-120px w-150px mr-10px position-relative flex-items-center flex-justify-around">
<n-button text @click="blockShowHandle()">{{ $t('tapai.showAll') }}</n-button>
<n-button text @click="blockAddHandle({})">{{ $t('tapai.addMask') }}</n-button>
</div>
<n-scrollbar x-scrollable>
<div class="flex flex-row h-120px">
<div v-for="(item, index) in blockList" :key="item.id"
class="border-solid border-0px cursor-pointer h-100% w-150px mr-10px position-relative bg-#fff dark:bg-#2d2d32 flex flex-col flex-items-center flex-justify-center"
:class="{ 'border-red': blockIndex === index }" @click="blockShowHandle(index)">
<div class="h-100px w-100% position-relative flex flex-items-center flex-justify-center">
<img v-if="item.segmentImage === ''" :src="frameBase64" class="w-100% h-100% object-contain" />
<!-- <img class="w-100% position-absolute top-50% left-50% translate--50%"
:src="item.result?.segmentImage" /> -->
<img v-else class="w-100% h-100% object-contain" :src="item.segmentImage" />
</div>
<n-popconfirm :show-icon="false" :positive-text="null" :negative-text="null">
<template #trigger>
<n-button text>{{ $t('tapai.operation') }}</n-button>
</template>
<n-space vertical>
<n-button text type="primary" size="tiny" @click.stop="uploadHandle(2, index)">
{{ item.resource != '' ? $t('tapai.reUpload') : $t('tapai.uploadResource') }}
</n-button>
<n-button :loading="videoUploadLoading" text type="error" size="tiny"
@click.stop="blockDelHandle(index)">
{{ $t('tapai.delMask') }}</n-button>
</n-space>
</n-popconfirm>
</div>
</div>
</n-scrollbar>
</div>
</n-spin>
</n-layout-footer>
</n-layout>
</div>
</template>
<style scoped></style>
electron+electron forge+vite+react+ts
How to create an Electron app with React, TypeScript, and Electron Forge
环境说明
- 安装最新的node环境。这里使用 18.16.0
开始构建
- 使用
electron forge搭建vite集成的框架
npm init electron-app@latest my-new-app -- --template=vite
- 集成
react依赖
npm install --save react react-dom
npm install --save-dev @types/react @types/react-dom
npm install --save-dev @vitejs/plugin-react
-
集成
tailwind css- 安装
tailwind css
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p- 配置
tailwind.config.js。如果没有在根目录下创建
/** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], }- 在
index.css添加如下代码,或者全局引用的css文件
@tailwind base; @tailwind components; @tailwind utilities; - 安装
-
集成
react-router v6
npm install react-router-dom localforage match-sorter sort-by
electron: Running postinstall script, failed in 44.4s
-
配置镜像源
# 配置 pnpm config set electron_mirror https://npm.taobao.org/mirrors/electron/ pnpm config set electron_mirror http://npm.taobao.org/mirrors/electron/ # 删除 pnpm config delete electron_mirror -
执行安装
pnpm install
安装
-
安装
node 12+ -
配置淘宝镜像
npm config set registry https://registry.npm.taobao.org -
安装全局
gitbook-clinpm install gitbook-cli -g
问题记录
TypeError: cb.apply is not a function
找到对应的文件 D:\dev-tools\nvm-noinstall\v14.18.2\node_modules\gitbook-cli\node_modules\npm\node_modules\graceful-fs\polyfills.js 注释下面几行代码
// fs.stat = statFix(fs.stat)
// fs.fstat = statFix(fs.fstat)
// fs.lstat = statFix(fs.lstat)
常用指令
-
初始化
gitbook。- 初始化项目,按照
gitbook规范会自动创建README.md和SUMMARY.md两个文件,具体用途见下文. - 其实
SUMMARY.md是电子书的章节目录,gitbook会初始化相应的文件目录结构,所以主要是用于开发初始阶段.
gitbook init - 初始化项目,按照
-
启动
gitbook- 启动本地服务,程序无报错则可以在浏览器预览电子书效果: http://localhost:4000
- 由于能够实时预览电子书效果,并且大多数开发环境搭建在本地而不是远程服务器中,所以主要用于开发调试阶段.
gitbook serve -
构建
gitbook静态网页- 构建静态网页而不启动本地服务器,默认生成文件存放在
_book/目录,当然输出目录是可配置的,暂不涉及,见高级部分. - 输出静态网页后可打包上传到服务器,也可以上传到
github等网站进行托管,因而主要用于发布准备阶段.
gitbook build - 构建静态网页而不启动本地服务器,默认生成文件存放在
部署
nginx映射
location /book {
alias /usr/local/serve/frontend/gitbook/;
index index.html;
}
win安装及配置
- 安装包下载
下载 noinstall 版本
- 配置环境变量
- 系统->高级系统设置->环境变量->系统变量
- 增加以下配置
NVM_HOME:nvm-window安装路径。D:\dev-tools\nvm-noinstallNVM_SYMLINK:nvm-window的node存储路径。D:\repository\nodePATH配置下增加%NVM_SYMLINK%和%NVM_HOME%
- 进入nvm-window的安装路径,在根目录添加文件
settings.txt,文件内容如下:
root: D:\dev-tools\nvm-noinstall # 根目录
path: D:\repository\node # node存储路径
arch: 64 # 操作系统
proxy: none # 代理
originalpath: .
originalversion:
node_mirror: http://npm.taobao.org/mirrors/node/ # node镜像源
npm_mirror: https://npm.taobao.org/mirrors/npm/ # npm镜像源
-
配置全局安装配置并配置环境变量。
npm config set prefix "D:\dev\repositories\node_global"- 环境变量配置
NODE_PATH:D:\dev\repositories\node_global\node_modules - PATH里面添加:
D:\dev\repositories\node_global
- 环境变量配置
-
配置缓存位置
npm config set cache "D:\dev\repositories\node_cache"
安装pnpm
-
进入全局安装配置文件夹
D:\dev\repositories\node_global -
执行命令
# linux || MacOS curl -fsSL https://get.pnpm.io/install.sh | sh - # window 在PowerShell操作 Invoke-WebRequest 'https://get.pnpm.io/v6.16.js' -UseBasicParsing -o pnpm.js; node pnpm.js add --global pnpm; Remove-Item pnpm.js -
换源
# 切换淘宝 npm config set registry https://registry.npm.taobao.org # 还原 npm config set registry https://registry.npmjs.org
常用指令
nvm install 【版本】。安装指定版本。nvm uninstall 【版本】。卸载指定版本nvm list。显示已经安装的版本nvm list available。显示可用版本。nvm use [版本]。切换指定版本nvm current。查看当前版本
nuxt2
jsconfig.json is not in cwd
npm generate 报错,提示 jsconfig.json is not in cwd。
解决方法:
- 打开node_modules/@nuxt/cli/dist/cli-generate.js
- 用
cwd: upath . normalize ( rootDir )替换cwd: rootDir。
const files = await globby__default['default']('**/*.*', {
...globbyOptions,
ignore,
cwd: rootDir, // 这行替换掉
absolute: true
});
const files = await globby__default['default']('**/*.*', {
...globbyOptions,
ignore,
cwd: upath . normalize ( rootDir ),
absolute: true
});
Cannot find module './index.module.css' or its corresponding type declarations
基于vite+react+electron 使用 css module
要导入自定义文件类型,必须使用 TypeScript 的declare module语法让它知道可以导入。为此,只需在其他代码的根目录所在的位置创建一个globals.d.ts(声明文件),然后添加以下代码:
declare module '*.css';
配置配置文件 tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": [
"dom",
"dom.iterable",
"ESNext"
],
"skipLibCheck": true,
/* Bundler mode */
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node", //node环境
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
// "paths": {
// "@/*": [
// "./*"
// ]
// },
"experimentalDecorators": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src",
"globals.d.ts" //配置的.d.ts文件
]
}
关于table表格格式错乱问题
此问题 elementui 和 elementplus都有这个问题
是通过纯html+elementui cdn的形式来实现的
问题代码:
<el-table
ref="table"
v-loading="loading"
element-loading-text="Loading..."
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50"/>
<el-table-column prop="tableName" label="表名" width="200"/>
<el-table-column prop="engine" label="引擎" width="200"/>
<el-table-column prop="tableComment" label="表注释"/>
<el-table-column prop="createTime" label="创建时间" width="200"/>
</el-table>
效果如下:可以看到全部都偏向了一侧

修复方法: 将单标签换成开始标签+结束标签,即可
<el-table
ref="table"
v-loading="loading"
element-loading-text="Loading..."
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50"></el-table-column>
<el-table-column prop="tableName" label="表名" width="200"></el-table-column>
<el-table-column prop="engine" label="引擎" width="200"></el-table-column>
<el-table-column prop="tableComment" label="表注释"></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="200"></el-table-column>
</el-table>
开发记录
scss深度选择器
打包优化
问题记录
前言
因为项目中使用到了image-webpack-loader 进行图片压缩,然后运行yarn run serve,出现问题。
根据错误分析出,缺少模块gifsicle。
解决思路
- 进入node_modules
- 查找文件夹/image-webpack-loader是否存在,存在查看optionalDependencies依赖信息,发现使用imagemin-gifsicle。
- 查看文件夹/imagemin-gifsicle是否存在,发现正常存在/bin,/lib文件夹等。
- 删除node_modules重新安装,错误依然存在
- 使用yarn add单独安装
yarn add imagemin-jpegtran imagemin-svgo imagemin-gifsicle imagemin-optipng --dev
此时出现问题
getaddrinfo ENOENT raw.githubusercontent.com
- ping raw.githubusercontent.com 发现无法访问
- 使用第三方ip工具https://site.ip138.com/raw.Githubusercontent.com/后,获得可访问ip
- 打开hosts文件,将代码填入尾部。
151.101.76.133 raw.githubusercontent.com
- 再次执行yarn add单独安装,安装成功
- 执行yarn run serve,正常。问题解决
老规矩写的不清晰的地方,可以指出,我会不定时更新和修改。一起加油.
所有的伟大,都来自于一个勇敢的开始。
包装类型
首先需要说明的是,我们在项目里看到的类似这样的代码:
var i = 33;
i.toString();
//输出: 33
这样会导致一些人误以为,这是基本类型自带的方法。然而实际并不是,在程序里会自动生成一个对应的包装类型的对象,进行操作。 因为是对象,所以才能够调用对象所拥有的方法。举个反面例子。
33.toString();
// SyntaxError: identifier starts immediately after numeric literal
也许有人会说,这个与上一个例子不是一样的吗?
实际上这个是错误,原因在于,我们在调用i的时候,是调用了对应的包装类型Number(),他会将33包装成Number类型,然后在实际的调用toString时,实际上是在调用这个Number类型。而我们直接使用错误例子中的写法时,我们操作的是33这个常量,所以程序会爆错误。ES为我们提供了3种基本包装类型Number,String,Boolean。
一般不直接创建包装类型和引用类型,因为这样造成的问题是,分不清是在处理基本包装类型还是引用类型。
var booleanObject = new Boolean(false);
var compare = booleanObject && true;
console.log("result:",compare);//result true;
原因很简单,你不是在判断fales && true,而是一个Object&&true,所以结果是True了。
包装和引用类型就简单讲一讲,接下来就是关于进制转换了。首先是十进制转其他进制。
进制转换
var dec=143;
console.log("十进制数:",dec); console.log("转8进制:",dec.toString(8));
console.log("转32进制:",dec.toString(32));
console.log("转16进制:",dec.toString(16));
/*为什么不用 "143".toString("8");
这是因为“143”是字符串类型,而不是Number类型,“143”.toString(8)不用管传递什么,结果都是他本 身*/
/*为什么不用 143.toString(8),麻烦看看之前的包装类型 */
然后是其它进制转换为十进制。
//只是数字,并不代表什么进制,注意这里是字符串,需要使用ParseInt转换
var num = "10"
//parseInt(num,2) 将 “10”作为二进制数转换为十进制
console.log("二进制转十进制:",parseInt(num,2));
console.log("八进制转十进制:",parseInt(num,8));
console.log("十六进制转十进制:",parseInt(num,16));
然后是其他进制转其他进制,也是运用上面的方法(比如十六进制转八进制)
console.log("十六进制转八进制:",parseInt("FF",16).toString(8));
//这里的parseInt会自动创建包装类型。
文章分享就到这里,希望多多支持,有时间可以一起交流学习。
所有的伟大,都来自于一个勇敢的开始。
1 前言
本人不是专门从事设计的人,但是空闲时间也会关注一些设计方面的知识。这段时间做了差不多4-5个小程序,基本上小程序上通用的接口都使用过了。本文对于在开发文档上已经描写清楚的不会过多进行讲述。
2 设计
2.1 写给新手设计师
如果你不是属于对于手机端设计很有心得的大牛的话,我还建议你先上微信公众号的小程序设计指南认真观看,网站如下: 微信设计指南 在文章的最后有提供微信风格的基础控件库下载,这个可以让你快速熟悉微信上所具有的一些控件。在首次做小程序设计的时候,尽量不要说是只做一个完完全全静态的小程序界面,我觉得小程序本身应该更加注重的是UE(即用户体验)和IxD(用户交互),所以你可在不考虑美观性多强前,先尝试着将控件放入设计中。 我个人觉得你的界面可以做的一般般,但是一个好的交互和体验,对于用户的感官会更加的舒服。而且我见过几个小程序设计图。我个人感觉小程序在美观上不如APP或web APP,也许是我做过的几套里的设计都太一般了吧。但是小程序的体验交互还是不错的。
2.2 尺寸说明
对于小程序的设计,个人比较建议还是以iphone6为主,我觉得这个尺寸下的图会更接近于手机上的效果,而且更方便与开发人员的开发。按照微信小程序配套的尺寸单位计算来看:
我们会发现iphon6的屏幕下对于尺寸的计算会更加方便,为2:1。
3 开发
3.1 概述
我们生成项目目录后,基本可以分为两类,一类以app命名的文件,就是作为一个全局配置文件,一类是自定义的page文件,像pages/log下定义的页面本身的文件。 关于page文件,一个page文件需包括wxml,wxss,js,json。其中wxml对应html界面,wxss对应css界面,json文件不是必选的,但一般可以全部创建出来。
3.2 关于JSON配置
JSON配置大致上可以分为两种。一种是页面page的JSON配置,另一种是全局app的JSON配置。
3.2.1 app.json文件
App.json文件存在于项目的根目录下,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab。 有5项配置,分别是:pages、windows、tabBar、networkTimeout、debug. Pages: 用于配置页面路径信息。接受一个字符串数组,每一项都对应页面(路径+文件名)信息,数组的第一项作为首页进行访问。小程序增减页面都需要对pages进行修改,否则会出现错误。文件名不需要写后缀wxml,wxss,js,json,因为框架会自动同一个路径下的4个文件。 例如: “/pages/example/example”,其中前面的/pages/example是作为路径,而后面的example就是page文件名,框架会自动加载/pages/example/下面的所有example,且后缀为wxml,wxss,js,json的文件。
Windows: 用于设置小程序的状态栏、导航条、标题、窗口背景色。
TabBar: 如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。 tabBar 中的 list 是一个数组,只能配置最少2个、最多5个 tab,tab 按数组的顺序排序。 如果你需要动态设置tabBar的路径信息,那么这个将无法满足需求,这个配置在app.json文件中,无法快捷的在程序执行期间进行切换
3.2.2 page.json文件
每一个小程序页面也可以使用.json文件来对本页面的窗口表现进行配置。 页面的配置比app.json全局配置简单得多,只是设置 app.json 中的 window 配置项的内容,页面中配置项会覆盖 app.json 的 window 中相同的配置项。 页面的.json只能设置 window 相关的配置项,以决定本页面的窗口表现。 如果页面中需要加载自定义组件那么将会使用到一个属性usingComponents属性。这个将在后面的自定义组件中讲到。
3.3 API开发
3.3.1 蓝牙开发
3.3.1.1 概述
因为写过关于蓝牙智能家居的项目,所以会将重点放在这里。 蓝牙智能家居连接的业务逻辑如下,
- 初始化蓝牙设备
- 打开蓝牙搜索服务
- 搜索蓝牙列表
- 连接设备
- 查找、匹配服务
- 查找、匹配特征
- 打开订阅
- 请求握手,写入设备
- 监听设备返回信息进行处理
3.3.1.2 使用方法
- 从git上下载开发库GIT地址
- 将BleTool.js和Tool.js放入在小程序项目下的lib文件(否则需要修改成自己的文件路径)
- 在App.js文件中引入BleTool.js文件
const BLEManage = require('lib/BleTool.js')
- 在App.js文件中onLaunch初始化蓝牙库
this.globalData.bleManage = new BLEManage(this)
- 使用方法:
获取设备列表:
this.globalData.bleManage.getDeviceList([搜索次数],[搜索间隔],[回调方法])
查找并连接设备:
this.globalData.bleManage.getDevices([设备MAC],[回调方法],[监听响应参数])
3.3.1.3 注意事项
- 因为最好不用一直保持蓝牙设备在线,所以需要监听蓝牙连接状态,但可以根据需求选择。
- 最好将蓝牙的状态保存下来,将蓝牙类绑定到app.globaldata下面,方便全局 操控蓝牙。
- 当搜索到设备后,这时设备掉线,会出现无法连接到设备,但是从设备列表能看到设备的问题,需要重启适配器再搜索即可正常。
- 推出小程序的时候,最好将小程序的蓝牙状态保存,然后等切回小程序的时候,在进行断开重连,确保小程序在后台运行的时候,蓝牙的状态信息保持一致。否则会出现异常。
- IOS系统更新13.1以后,出现蓝牙需要开放授权。位置是:设置->隐私->蓝牙->对应的应用。
3.3.1.4 代码说明
- 首先,初始化需要保存的蓝牙状态,这个将用于获取和监听全局的蓝牙状态。
constructor() {
let that = this;
//记录自身,防止某些函数this指向问题
that.self = this;
//是否开启调试
that.debug = false;
//操作状态控制
that.onlyOne = false; //唯一控制
that.silent = false; //静默连接
that.rootChangeStop = false; //路由修改是否停止
that.canRun = false; //蓝牙库是否可以继续运行
that.rootPage = null;
//属性
that.available = false;
that.searchState = false;
that.platform = null;
that.connectState = false;
//蓝牙搜索计时器
that.getDeviceAllTime = 3;
that.getDeviceTime = that.getDeviceAllTime;
that.getDeviceTimeState = true;
that.getDeviceTimeOut = null;
that.getDeviceRSSICount = 0;
that.getCanDevice = false;
//蓝牙连接计时
that.getConnectDeviceAllTime = 2;
that.getConnectDeviceTime = that.getConnectDeviceAllTime;
that.getConnectDeviceTimeState = true;
that.connectId = null;
//显示计时器
that.showMentionTimer = null;
that.listenAdapterStateChange();
that.listenConnection();
that.openBLEAdapter(function() {
that.closeBeforeConnect();
});
- 初始化了蓝牙对象的时候,打开适配器;适配器打开成功,则开启搜索服务;打开失败则,提示用户需要打开蓝牙。
listenAdapterStateChange 则是用来监听适配器的状态改变,如果用户关闭蓝牙,则方法会返回false ,这时我们就可以将蓝牙对象的openAdapter的状态标记为false,即未开启,这里的延时1s搜索是因为说是有些设备可能存在状态先响应然后再启动的现象,然后就无法搜索到设备了。
/**
* 打开蓝牙适配器
*/
openBLEAdapter(callBack) {
let that = this
console.log("openBLEAdapter")
wx.openBluetoothAdapter({
success: function(res) {
console.log('openBLEAdapter_success')
that.checkAvailable({
toast: true
}, true)
that.showMention("开启蓝牙成功", true);
// setTimeout(function() {
// that.startSearchDevice(callBack);
// }, 1000)
callBack();
},
fail: function(res) {
console.log('openBLEAdapter_fail', res)
that.checkAvailable({
toast: true
}, false)
}
})
}
/**
* 监听适配器状态
*/
listenAdapterStateChange() {
var that = this
wx.onBluetoothAdapterStateChange(function(res) {
console.log("listenAdapterStateChange", res)
that.searchState = res.discovering
let available = that.checkAvailable({
toast: true
}, res.available);
if (!res.available) {
that.onlyOne = false; //唯一控制
that.silent = false; //静默连接
that.rootChangeStop = false; //路由修改是否停止
}
// if (res.available) {
// // if (!that.available) {
// that.available = res.available
// // that.startSearchDevice();
// // }
// } else {
// that.available = res.available
// console.log("请打开蓝牙2")
// that.showMention('请打开蓝牙2', true)
// }
})
}
/**
* 开始搜索
*/
startSearchDevice(callBack) {
let that = this;
console.log("startSearchDevice")
if (that.checkAvailable({})) {
if (!that.searchState) {
// if (that.platform == "ios") {
console.log("延时1s搜索")
that.showMention('准备搜索设备', true, 1)
setTimeout(function() {
that.wechatSearchDevice(callBack);
}, 1000)
// } else {
// console.log("立即搜索")
// that.wechatSearchDevice();
// }
}
} else {
that.showMention("请打开蓝牙", true);
}
}
/**
* 开始搜索微信方法
*/
wechatSearchDevice(callBack) {
let that = this;
wx.startBluetoothDevicesDiscovery({
// services: [MainService],
success: function(res) {
that.searchState = true;
console.log("成功,准备开始搜索: ")
console.log(res)
that.showMention('开始搜索设备', true)
if (callBack != undefined) {
callBack();
}
},
fail: function(res) {
that.available = false
that.searchState = false;
console.log("开启搜索失败: ")
that.showToast({
title: '开启搜索失败',
})
}
})
}
- 搜索蓝牙设备的2种方式
开始搜索蓝牙设备了,如果你需要从周围可能存在的蓝牙设备中,选取你想要的,那么你可以在蓝牙设备的名字上动动手脚了。比如,你可加上前缀FFFF-这样的。然后 判断获取到的名字是否包含FFFF-前缀,有就加入到显示列表里面。
//微信获取设备列表
getDeviceList(count, time, callBack) {
let that = this;
that.searchCount = count;
// that.searchState = true;
if (that.searchInterval != null) {
clearInterval(that.searchInterval);
that.searchInterval = null;
}
that.searchInterval = setInterval(function() {
if (that.checkRoutePageChange()) {
return;
}
that.showMention("第" + (count - that.searchCount + 1) + "次搜索", false)
if (that.searchCount > 0) {
wx.getBluetoothDevices({
success: function(res) {
console.log(635, res)
}
})
that.searchCount--;
return;
} else {
that.showMention("搜索成功", true, 2)
clearInterval(that.searchInterval);
wx.getBluetoothDevices({
success: function(res) {
console.log("getDeviceListSuccess", res)
callBack(res.devices);
}
})
}
}, time * 1000)
}
也许你有时候需要的是查找并连接指定的设备,那么你可以通过下面这样的形式
/**
* 获取设备
*/
getDevices(mac, callBack, listenFun, options) {
let that = this;
console.log("getDevices", options)
mac = mac.toLowerCase(); //转换小写
if (!that.checkAvailable({
toast: true
})) {
return;
}
//检查是否有蓝牙操作在运行
if (that.checkOnlyOne({
toast: true
}, true)) {
return;
}
//配置属性
// that.onlyOne = true;
console.log("typeof options", typeof options)
if (typeof options == "object") {
that.slient = options.slient || false;
that.rootChangeStop = options.rootChangeStop || false;
} else {
that.slient = false;
that.rootChangeStop = false;
}
that.callBack = function() {
that.onlyOne = false;
callBack()
}
that.listenFun = listenFun
if (that.connectState && that.mac == mac) {
that.callBack()
return;
}
that.startSearchDevice();
that.rootPage = getCurrentPages()[getCurrentPages().length - 1].route;
that.showMention("开始搜索", true, 3)
that.resetSearchDevice(mac);
}
- 连接设备
蓝牙连接的过程就是不断根据与蓝牙设备所定好的一些协议,服务值,特征值等,主要是为了保证安全性。
/**
* 创建连接
*/
connectDevice(msg) {
let that = this;
console.log("connectDevice")
if (that.checkRoutePageChange()) {
return;
}
msg = msg == undefined ? '连接中' : msg;
that.showMention(msg, false)
wx.createBLEConnection({
deviceId: that.connectId,
success: function(res) {
console.log(res)
console.log("连接成功")
that.showMention('连接成功', true)
that.connectState = true;
that.searchService();
console.log("保存", that.connectId);
tool.storage.save("connectId", that.connectId)
},
fail: function(res) {
console.log("miss", res)
that.connectState = false;
that.getConnectDeviceTime--;
if (that.checkRoutePageChange()) {
return;
}
if (!that.checkAvailable({})) {
that.showMention("蓝牙关闭", true, 2)
} else {
if (that.getConnectDeviceTime > 0) {
that.showMention("连接失败", false)
setTimeout(function() {
// that.connectDevice("第" + (that.getConnectDeviceAllTime - that.getConnectDeviceTime) + "次重连中");
that.connectDevice("重连中");
}, 1000);
} else {
that.showMention("重连失败", true, 2);
wx.showModal({
title: '温馨提示',
content: '请确认设备正常通电,即将重启蓝牙适配器',
success: function(res) {
if (res.confirm) {
console.log('用户点击确定,开始重启重连')
that.restartBleAdapter(function() {
// that.getConnectDeviceTime = that.getConnectDeviceAllTime;
// that.connectDevice()
that.resetSearchDevice(that.mac)
})
} else if (res.cancel) {
console.log('用户点击取消, 取消重启重连')
}
}
})
}
}
}
})
}
/**
* 搜索服务
*/
searchService() {
console.log("searchService")
let that = this;
that.showMention('数据处理中', true, 3)
wx.getBLEDeviceServices({
//服务uid
deviceId: that.connectId,
success: function(res) {
console.log(res)
for (let i = 0; i < res.services.length; i++) {
let rs = res.services[i]
if (rs.uuid.indexOf(MainService) > -1) {
console.log('找到匹配服务', res.services[i].uuid)
that.mainservice = res.services[i].uuid;
that.characteristic()
break;
}
}
},
fail: function() {
wx.hideLoading();
console.log("搜索服务失败");
that.showToast({
title: '搜索服务失败',
})
}
})
}
/**
* 特征值获取
*/
characteristic() {
console.log("characteristic")
let that = this
wx.getBLEDeviceCharacteristics({
deviceId: that.connectId,
serviceId: that.mainservice,
success: function(res) {
console.log("特征id:", res)
for (let i = 0; i < res.characteristics.length; i++) { //写入的特征值
let ct = res.characteristics[i]
if (ct.uuid.indexOf(UpdateCharacteristic) > -1) {
console.log('找到更新匹配特征', ct.uuid)
that.updateuuid = ct.uuid;
that.openNotify(ct.uuid);
} else if (ct.uuid.indexOf(WriteCharacteristic) > -1) {
console.log('找到写匹配特征', ct.uuid)
that.writeuuid = ct.uuid;
} else if (ct.uuid.indexOf(ReadCharacteristic) > -1) {
console.log('找到读匹配特征', ct.uuid)
that.readuuid = ct.uuid
}
}
//连接成功后,不保持静默方式
that.slient = false;
// setTimeout(function() {
that.callBack();
// }, 1000)
},
fail: function(res) {
console.log('找不到特征值')
console.log(res);
wx.hideLoading();
that.showToast({
title: '找不到特征值',
})
}
})
}
/**
* 打开订阅
*/
openNotify(uuid) {
var that = this
console.log("openNotify")
wx.notifyBLECharacteristicValueChange({
deviceId: that.connectId,
serviceId: that.mainservice,
characteristicId: uuid,
state: true,
success: function(res) {
console.log("notify打开成功", res)
that.listenNotifyValueChange()
// console.log()
// wx.onBLECharacteristicValueChange(function (res) {
// console.log("订阅的值",res)
// })
},
fail: function(res) {
console.log("notify打开失败");
console.log(res)
wx.hideLoading();
that.showToast({
title: 'notify打开失败',
})
},
})
}
- 下面是关于一些基本的操作和监听
/**
* 写入数据
*/
writeCharacteristicValue(str, func) {
console.log("writeCharacteristicValue")
let that = this;
if (that.checkRoutePageChange()) {
return;
}
// console.log(that.connectId, that.mainservice, that.writeuuid, str);
if (!that.connectState) {
tool.showMention("未连接设备")
return;
}
func = func || function() {};
let resObj = {}
wx.writeBLECharacteristicValue({
deviceId: that.connectId,
serviceId: that.mainservice,
characteristicId: that.writeuuid,
value: tool.string2Buffer(str),
success: function(res) {
that.showToast({
title: '写入成功',
})
console.log("writeDate-->", res);
resObj.state = "success"
func(resObj)
},
fail: function() {
console.log("写入失败")
resObj.state = "fail"
func(resObj)
}
})
}
/**
* 监听特征值
*/
listenNotifyValueChange() {
console.log("listenNotifyValueChange")
var that = this
console.log("监听特征变化")
wx.onBLECharacteristicValueChange(function(res) {
console.log("收到数据")
// console.log('WriteDataType = ' + WriteDataType)
let value = res.value
console.log(value);
let value2 = tool.uint8Array2Str(value)
console.log("returnstr", value2)
let dataView = new DataView(tool.string2Buffer(value2))
that.listenFun(dataView);
// return;
})
}
/**
* 监听连接状态
*/
listenConnection() {
let that = this;
console.log("listenConnection")
wx.onBLEConnectionStateChange(function(res) {
console.log("connectState", res);
that.connectState = res.connected;
if (res.connected) {
that.showToast({
title: "连接成功",
})
} else {
console.log(that.available, false)
// if (that.available) {
// that.startSearchDevice()
// }
that.showToast({
title: "连接断开",
})
}
})
}
- 一些基本的字符串和数组的转换操作
//Tool.js
/*
16进制字符串转整形数组
*/
function str2Bytes(str) {
var len = str.length;
if (len % 2 != 0) {
return null;
}
var hexA = new Array();
for (var i = 0; i < len; i += 2) {
var s = str.substr(i, 2);
var v = parseInt(s, 16);
hexA.push(v);
}
return hexA;
}
/*
整形数组转buffer
*/
function array2Buffer(arr) {
let buffer = new ArrayBuffer(arr.length)
let dataView = new DataView(buffer)
for (let i = 0; i < arr.length; i++) {
dataView.setUint8(i, arr[i])
}
return buffer
}
/*
16进制字符串转数组
*/
function string2Buffer(str) {
let arr = str2Bytes(str);
return array2Buffer(arr)
}
/*
ArrayBuffer转十六进制字符串
*/
function uint8Array2Str(buffer) {
var str = "";
let dataView = new DataView(buffer)
for (let i = 0; i < dataView.byteLength; i++) {
var tmp = dataView.getUint8(i).toString(16)
if (tmp.length == 1) {
tmp = "0" + tmp
}
str += tmp
}
return str;
}
3.3.2 camera
在编写摄像机组件的时候,我发现了关于手机设备的兼容问题,拍照后返回的临时图片路径,将他显示到界面上,会出现明显的颜色变换,像是人会变阿凡达那样。问题没找出,因为大部分的机型不会有该问题,已在酷派手机上发现,可能是系统问题.
老规矩写的不清晰的地方,可以指出,我会不定时更新和修改。一起加油.
所有的伟大,都来自于一个勇敢的开始。
理论基础
名词解释
- FCPU【TODO】
- IIC端口。 IIC(Inter-Integrated Circuit)其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I²C的正确读法为“I平方C”("I-squared-C"),而“I二C”("I-two-C")则是另一种错误但被广泛使用的读法。
RC振荡器
使用电阻和电容元件的振荡器可以获得良好的频率稳定性和波形,这种振荡器称为RC或者相移振荡器。
https://zhuanlan.zhihu.com/p/531622254
只读存储器(ROM)
-
ROM。只读存储器。
-
EEPROM。
- EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。 EEPROM 可以在电脑上或专用设备上擦除已有信息,重新编程。一般用在即插即用。
- 狭义上来说,这类rom的特点是可以随机访问和修改任何一个字节,可以往每个bit中写入0或者1, 掉电后数据不丢失, 可以保存100年,可以擦写100w次。具有较高的可靠性,但是电路复杂/成本也高。因此目前的EEPROM都是几十千字节到几百千字节的,绝少有超过512K的。
- 广义上来说,flash也属于EEPROM, 因为它也是电擦除的rom。但是为了区别于一般的按字节为单位的擦写的EEPROM,我们都叫它flash。 flash做的改进就是擦除时不再以字节为单位,而是以块为单位,一次简化了电路,数据密度更高,降低了成本。上M的rom一般都是flash。
-
flash
- nor flash。 数据线和地址线分开,可以实现ram一样的随机寻址功能,可以读取任何一个字节。但是擦除仍要按块来擦。
- nand flash。 同样是按块擦除,但是数据线和地址线复用,不能利用地址线随机寻址。读取只能按页来读取。(nandflash按块来擦除,按页来读,nor flash没有页)
- 由于nand flash引脚上复用,因此读取速度比nor flash慢一点,但是擦除和写入速度比nor flash快很多。nand flash内部电路更简单,因此数据密度大,体积小,成本也低。因此大容量的flash都是nand型的。小容量的2~12M的flash多是nor型的。
- 使用寿命上,nor flash的擦除次数是nand的数倍。而且nand flash可以标记坏块,从而使软件跳过坏块。nor flash 一旦损坏便无法再用。
- 因为nor flash可以进行字节寻址,所以程序可以在nor flash中运行。嵌入式系统多用一个小容量的nor flash存储引导代码,用一个大容量的nand flash存放文件系统和内核。
视频
码率
视频码率是指在视频编码过程中,单位时间内传输的数据位数,通常以千位每秒(kbps)为单位表示。它是衡量视频文件质量和体积的重要指标。码率的高低直接影响到视频的清晰度和体积。简单来说,码率越大,意味着单位时间内传输的数据量越多,从而使得视频画面更加清晰;相反,如果码率设置得过低,可能会导致视频画面显得模糊不清。然而,码率的提高也会使文件的体积增大,有时候可能得不偿失。因此,视频编码器通常会寻找一种平衡点,既能保持视频画面的高质量又能使文件体积适中。
常见的码率
- 超清码率: 1024kbps、 分辨率1280*720、 俗名720P
- 高清码率: 512kbps 、分辨率640*480、俗名480P
- 流畅码率: 256kbps、分辨率480*360、俗名360P
计算公式
基本的视频码率算法:
- 【码率】 (kbps) = 【文件大小】(字节)*8/【时间】(秒) * 1000
音频文件专用算法:
- 【比特率】 (kbps) = 【量化采样点】 (kHz) * 【位深】 (bit/采样点) * 【声道数量】 (一般为2)
举例,D5的碟,容量4.3G,其中考虑到音频的不同格式,所以算为600M,(故剩余容量为4.3*1000-600=3700M),所以视频文件应不大于3.7G,本例中取视频文件的容量为3.446G,视频长度100分钟(6000秒),计算结果:码率约等于4933kbps
基本原则
- 码率和质量成正比,但是文件体积也和码率成正比。
- 码率超过一定数值,对图像的质量没有多大影响。
调节手段
- 调节编码的帧率。通过调节序列在时间上的分辨率达到控制码率的目的。当码率高于信道时,通过丢帧来降低码率;当码率低于信道时,可提高帧率以提高视觉效果。
- 调节图像大小。通过调节序列在空间上的分辨率来达到控制码率的目的。当码率高于信道时,减小尺寸来降低码率;当码率低于信道时,增加图像尺寸以获得更好的效果。
- 调节量化参数。编码残差系数的量化参数会直接影响到码率。量化参数变大,则编码的比特数降低;量化参数变小,编码的比特数提高。
前言
命令示例
查看文件编码格式
- 输入
ffprobe -i file.mp4 -show_streams -select_streams v -print_format json
- 输出
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"codec_type": "video",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 706,
"height": 360,
"coded_width": 706,
"coded_height": 360,
"closed_captions": 0,
"film_grain": 0,
"has_b_frames": 2,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "353:180",
"pix_fmt": "yuv420p",
"level": 30,
"chroma_location": "left",
"field_order": "progressive",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"id": "0x1",
"r_frame_rate": "23857/1000",
"avg_frame_rate": "23857/1000",
"time_base": "1/23857",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 185000,
"duration": "7.754537",
"bit_rate": "882642",
"bits_per_raw_sample": "8",
"nb_frames": "185",
"extradata_size": 46,
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0
},
"tags": {
"language": "und",
"handler_name": "VideoHandler",
"vendor_id": "[0][0][0][0]",
"encoder": "Lavc60.31.102 libx264"
}
}
]
}
前言
选项
主要选项
-f fmt (input/output)指定输入或者输出文件格式。常规可省略而使用依据扩展名的自动指定,但一些选项需要强制明确设定。-i filename (input)指定输入文件。-y (global)默认自动覆盖输出文件,而不再询问确认。-n (global)不覆盖输出文件,如果输出文件已经存在则立即退出。-stream_loop number (input)设置输入流循环的次数。循环0表示无循环,循环-1表示无限循环。-recast_media (global)允许强制使用与解码器检测到的或指定的媒体类型不同的解码器。用于解码作为数据流混合的媒体数据。-t duration(input/output)限制输入/输出的时间。如果是在 -i 前面,就是限定从输入中读取多少时间的数据;如果是用于限定输出文件,则表示写入多少时间数据后就停止。duration可以是以秒为单位的数值或者 hh:mm:ss[.xxx] 格式的时间值。 注意 -to 和 -t 是互斥的,-t 有更高优先级。-to position (output)只写入position时间后就停止,position可以是以秒为单位的数值或者 hh:mm:ss[.xxx]格式的时间值。 注意 -to 和 -t 是互斥的,-t 有更高优先级。-ss position (input/output)当在 -i 前,表示定位输入文件到position指定的位置。注意可能一些格式是不支持精确定位的,所以ffmpeg可能是定位到最接近position(在之前)的可定位点。position可以是以秒为单位的数值或者 hh:mm:ss[.xxx] 格式的时间值。-itsoffset offset (input)设置输入文件的时间偏移。offset 必须采用时间持续的方式指定,即可以有-号的时间值(以秒为单位的数值或者 hh:mm:ss[.xxx] 格式的时间值)。偏移会附加到输入文件的时间码上,意味着所指定的流会以时间码+偏移量作为最终输出时间码。-timestamp date (output)设置在容器中记录时间戳。-codec[:stream_specifier] codec (input/output,per-stream)为特定的文件选择编/解码模式,对于输出文件就是编码器,对于输入或者某个流就是解码器。选项参数中 codec 是编解码器的名字,或者是 copy(仅对输出文件)则意味着流数据直接复制而不再编码。
视频选项
-vframes number (output)设置输出文件的帧数,是 -frames:v 的别名。-r[:stream_specifier] fps (input/output,per-stream)设置帧率(一种Hz值,缩写或者分数值)。-s[:stream_specifier] size (input/output,per-stream)设置帧的尺寸。-s 1024x720指定输入视频的分辨率为1024x720,1024是宽度,720是高度
-vn (output)禁止输出视频。-vcodec codec (output)设置视频编码器,这是 -codec:v 的一个别名。aspect[:stream_specifier] aspect (output,per-stream)指定视频的纵横比(长宽显示比例)。aspect 是一个浮点数字符串或者num:den格式字符串(其值就是num/den),例如"4:3","16:9","1.3333"以及"1.7777"都是常用参数值。-b:v 640k设置输出视频的码率为640kbps
音频选项
-aframes number (output)设置 number 音频帧输出,是 -frames:a 的别名。-ar[:stream_specifier] freq (input/output,per-stream)设置音频采样率。默认是输出同于输入。对于输入进行设置,仅仅通道是真实的设备或者raw数据分离出并映射的通道才有效。对于输出则可以强制设置音频量化的采用率。-aq q (output)设置音频品质(编码指定为VBR),它是 -q:a 的别名。-ac[:stream_specifier] channels (input/output,per-stream)设置音频通道数。默认输出会有输入相同的音频通道。对于输入进行设置,仅仅通道是真实的设备或者raw数据分离出并映射的通道才有效。-an (output)禁止输出音频。-acode codec (input/output)设置音频解码/编码的编/解码器,是 -codec:a 的别名。
字幕选项
-scodec codec(input/output)设置字幕解码器,是 -codec:s 的别名。-sn (output)禁止输出字幕。-fix_sub_duration修正字幕持续时间。-canvas_size size设置字幕渲染区域的尺寸(位置)。
转码优化
- -preset的参数主要调节编码速度和质量的平衡,有ultrafast(转码速度最快,视频往往也最模糊)、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、placebo这10个选项,从快到慢。
命令实例
转化格式
ffmpeg -i input_test.mp4 -vn -acodec copy output_test.flv
ffmpeg -i input_test.aac -vn -acodec copy output_test.mp3
抽取画面中的音频
ffmpeg -i input_test.mp4 -vn -y -acodec copy output_test.aac
ffmpeg -i input_test.mp4 -vn -y -acodec copy output_test.mp3
ffmpeg -i input_test.mp4 -acodec copy -vn output_test.mp3
抽取画面中的视频
ffmpeg -i input_test.mp4 -vcodec copy -an output_test.avi
ffmpeg -i input_test.mp4 -vcodec copy -an output_test.mp4
音频+视频合成
ffmpeg -i input_test_1.mp4 -i input_test_2.mp3 -vcodec copy -acodec copy output_test.mp4ffmpeg -i input_test_1.mp4 -itsoffset 10 -i input_test_2.mp3 -vcodec copy -acodec copy output_test.mp4ffmpeg -ss 20 -t 5 -i input_test_1.mp4 -i input_test_2.aac -vcodec copy -acodec copy output_test.mp4音乐持续播放,视频只播放5秒ffmpeg -ss 20 -t 5 -i input_test_1.mp3 -i input_test_2.mp4 -vcodec copy -acodec copy output_test.mp4视频持续播放,音乐只播放5秒
音频+音频合成
格式:ffmpeg -i INPUT1 -i INPUT2 -i INPUT3 -filter_complex amix=inputs=3:duration=first:dropout_transition=3 OUTPUT
ffmpeg -i input_test_1.mp3 -i input_test_2.mp3 -filter_complex amix=inputs=2:duration=shortest output_test.mp3ffmpeg -i input_test_1.mp3 -i input_test_2.mp3 -filter_complex amix=inputs=2:duration=longest output_test.mp3ffmpeg –i input_test_1.mp3 –i input_test_2.mp3 –vcodec copy –acodec copy output_test.mp3
视频分离成图片
-
ffmpeg -i input_test.mp4 -r 1 -f image2 output_image-%03d.jpeg -
截图图片的一帧
ffmpeg -ss [时间戳] -i [输入视频文件] -frames:v 1 -update 1 -q:v 2 [输出图像文件]-ss [时间戳]:指定要截取哪一帧的时间,格式为HH:MM:SS.sss。例如,要截取第 5 秒的帧,可以使用00:00:05。-i [输入视频文件]:要截取帧的视频文件的路径。-vframes 1/-frames:v 1:指定要截取的帧数。在这里,我们只截取一帧,所以设置为 1。-q:v 2:指定输出图像的质量。数值越低,质量越高。在这里,我们使用 2,这是一个常用的中等质量设置。
图片合成视频
ffmpeg -f image2 -i output_image-%03d.jpeg output_test.mp4
改变音量大小
ffmpeg -i input_test.mp3 -af 'volume=0.5' output_test.mp3
音效淡入淡出效果
ffmpeg -i input_test.mp3 -filter_complex afade=t=in:ss=0:d=4 output_test.mp3淡入效果:把 input_test.mp3 文件的前5s做一个淡入淡出效果,输出到 output_test.mp3 文件中ffmpeg -i input_test.mp3 -filter_complex afade=t=out:st=20:d=6 output_test.mp3淡出效果:将 input_test.mp3 文件从20s开始,做6s的淡出效果,输出到 output_test.mp3 文件中
截取音频
ffmpeg -ss 10 -i input_test.mp3 -to 20 -vcodec copy -acodec copy output_test.mp3ffmpeg -ss 10 -i input_test.mp3 -t 5 -vcodec copy -acodec copy output_test.mp3ffmpeg -i input_test.mp3 -c copy -t 10 -output_ts_offset 120 output_test.mp3
容器时长获取
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -i input_test.mp3
网络资源下载
ffmpeg -i https://xxx.xxx.xxxxxx -c copy -f mp3 output_test.mp3
播放音频视频
ffplay input_test.mp3
图片生成gif动图
ffmpeg -i input_image_%03d.png -r 5 output_test.gif
抽取PCM数据
ffmpeg -i input_test.mp4 -vn -ar 44100 -ac 2 -f s16le output_test.pcm
视频转码
参考:
- https://zhuanlan.zhihu.com/p/675879599
-
转
H.264编码ffmpeg -y -i [input.video] -c:v h264 -crf 23 -preset slow [output.video]-c:v指定编码器。(也可以使用 libx264 编码器)这是一种高效率的H.264视频编码器-pixel_format yuv420p: 指定输入视频的像素格式为 yuv420p。这是一种常见的 YUV 格式,其中 Y 分量是完全采样的,而 U 和 V 分量则以 2x2 的子样本进行采样。-crf 23: 指定编码质量因子为 23。CRF(Constant Rate Factor)是一种基于质量的编码控制方法,值越小表示更高的质量和比特率。常用的范围是 18-28。
前言
在处理视频时,经常需要获取视频的时长信息,通常会碰到这样的问题:
- 不同播放器显示的文件时长不一样
- 视频转码后,时长会发生变化
- 视频播放到后面,缺少画面等。
那我们需要先明白一个视频的时长包含那几个方面。
- 文件(容器)时长(container duration)
- 音视频流时长(stream duration)
- 解码后播放时长(get duration by decoding)
ffprobe
容器时长
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -i [video.suffix]
音视频流时长
ffprobe.exe -v error -select_streams v:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 [video.suffix]
通常媒体文件里有多个视频流,各个流的时长也未必一样,一般播放器会以video stream 的时长作为播放时长。

解码后文件播放时长
ffmpeg.exe -i [video.suffix] -f null -

一般这种未最准确的方法,但由于需要解码,耗时可能会较长。
java调用ffmpeg指令
Runtime.getRuntime()
Process process = Runtime.getRuntime().exec("ffprobe -version");
int exitCode = process.waitFor();
if (exitCode == 0) {
logger.info("获取pprobe版本成功!");
} else {
logger.error("获取pprobe版本失败!");
}
ProcessBuilder
ProcessBuilder processBuilder = new ProcessBuilder().directory(new File(ffmpegPath));
// 这里是个问题点,稍后会说
processBuilder.command("ffprobe", "-version");
// 错误写法
// processBuilder.command("ffprobe -version");
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode == 0) {
logger.info("获取pprobe版本成功!");
} else {
logger.error("获取pprobe版本失败!");
}
错误记录
java.io.IOException: error=2, No such file or directory
-
问题描述:在linux系统下,
ProcessBuilder执行ffprobe -version时,会抛出异常提示java.io.IOException: error=2, No such file or directory。但是实际在系统中执行时没有问题的,但是 Runtime.getRuntime().exec执行时又不会出现问题。// 此时会抛出异常, ProcessBuilder processBuilder = new ProcessBuilder(); processBuilder.command("ffprobe -version"); Process process = processBuilder.start(); // 此时正常执行 Runtime.getRuntime().exec("ffprobe -version"); -
详情分析:让我们分别看一下源码部分差异在哪里
-
ProcessBuilder::command方法。可以看出他是把这个字符串看作一个元素放到 List 里面。
// 相当于:["ffprobe -version"] public ProcessBuilder command(String... command) { this.command = new ArrayList<>(command.length); for (String arg : command) this.command.add(arg); return this; } -
Runtime.getRuntime().exec方法。可以看出他是做了切割,然后放到一个String数组的。它的底层也是调用了ProcessBuilder去执行命令的。
// 相当于: ["ffprobe","-version"] public Process exec(String command, String[] envp, File dir) throws IOException { if (command.length() == 0) throw new IllegalArgumentException("Empty command"); StringTokenizer st = new StringTokenizer(command); String[] cmdarray = new String[st.countTokens()]; for (int i = 0; st.hasMoreTokens(); i++) cmdarray[i] = st.nextToken(); return exec(cmdarray, envp, dir); } public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException { return new ProcessBuilder(cmdarray) .environment(envp) .directory(dir) .start(); } -
知道问题出现在关于格式这个问题,不妨大胆推断一下,是不是前者将
ffprobe -version视为一个命令,而不是调用 ffprobe 下的 -version选项。
-
-
解决办法:将
processBuilder.command("ffprobe -version");分开processBuilder.command("ffprobe","-version");processBuilder.command("ffprobe","-version");另外说一句,在window下不两种方式都可以。但有些时候也会出问题,就很奇怪。
关于waitFor卡住的问题
-
问题描述:在执行视频转码的过程中,就是如下这条指令,执行到process.waitFor时会卡住,但在cmd执行时很快完成,那么问题点出来哪里呢?
ffmpeg -y -i D:\ruoyi\uploadPath\2024\03\520373139859968000.mp4 -c:v libx264 -crf 23 -preset slow D:/ruoyi/uploadPath/2024/03/520373139859968000_new.mp4 -
详情分析:经过尝试,发现该指令会输出大量的日志。WaitFor等待进程完成时, 可能会遇到日志堵塞的问题。这是因为进程的输出缓冲区已满,导致进程无法继续执行。为了避免这个问题,您可以在等待进程完成之前,使用单独的线程读取进程的输出。
-
解决办法:
- 增加了
-loglevel error选项。 使得日志输出很少,输出区不会堆满。
ffmpeg -loglevel error -y -i D:\ruoyi\uploadPath\2024\03\520373139859968000.mp4 -c:v libx264 -crf 23 -preset slow D:/ruoyi/uploadPath/2024/03/520373139859968000_new.mp4-
通过创建线程输出。
Process process = new ProcessBuilder(command).start(); // 创建一个线程来读取进程的输出 Thread outputThread = new Thread(() -> { try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } }); // 启动线程 outputThread.start(); // 等待进程完成 int exitCode = process.waitFor(); System.out.println("Process exited with code: " + exitCode); // 等待输出线程完成 outputThread.join();
- 增加了
关系代数运算
关系代数用对关系的运算来表达查询,运算对象是关系,结果得到关系
关系可以理解为一张二维表,例如一张学生表,就是一个关系,关系代数运算就是我们写sql的一些查询操作,操作表生成新的表或者视图
关系代数的运算有两种:
- 运算符为传统的集合运算符:并、差、交、笛卡尔积
- 运算符为专门的关系运算符:选择、投影、连接、除
传统的集合运算符:从表的行的角度进行运算,所以需要运算的关系R、S具有相同的属性列的类型、属性列数目
专门的关系运算符:涉及到列、行,没有那些限制
其他:选择、投影、并、差、笛卡尔积是查询操作的基本操作,其他操作可以由这5种基本操作推出
关于这些操作的概念是很晦涩的,结合具体的SQL来理解
并(union)
并操作两个关系R、S,得到的关系由属于R或S的元组组成,就是逻辑并

对应的Sql操作就是Union
第一次查询的id=1的关系 并 第二次查询id=2的关系 的结果:

and的结果也是这样的,为什么不说and呢?
这里主要谈的是并的思想,两个关系的并运算,and的话并不是两个关系的运算
差(except)
差:关系R与关系S的差由属于R而不属于S的所有元组组成

很明显,完成差的查询操作只需要从R中排除R与S交集关系即可,用not in关键字sql查询

实际上存在except关键字,Mysql不支持
换成Sql Server试试:
select * from 学生 where 学号 < 3
except
select * from 学生 where 学号 = 1;

但是没必要记这么多关键字,了解思想就行
笛卡尔积(cartesian product)
m个属性的关系R与n个属性的关系S,笛卡尔积为(m+n)列的元组的集合,行数为(m*n)

我们常用的没有限制的表连接就是笛卡尔积

选择(Selection)
选择又称为限制,在关系R中选择满足给定条件的元组,也就是筛选行

sql语句对应的就是我们的where关键字

投影(projection)
投影:关系R上的投影是从R中选择出若干属性列组成新的关系 这不就是select语句吗?从表R中选择一些属性组成新的关系

下面得到的结果就是一个新的关系
小结
五种基本操作:并、差、笛卡尔积、选择、投影 就是这么简单
相关的概念很晦涩但是完整,我们学习时需要通俗
由这5个基本查询操作,可以组成复杂的关系运算,例如连接
连接(join)
连接:从两个关系的笛卡尔积中选取属性间满足一定条件的元组
连接可以分为:
- 内连接:两个关系R、S的笛卡尔积限制了一些条件,抛弃R、S中公共属性值不存在相等的元组;等值连接、非等值连接
- 外连接:没有抛弃R、S中公共属性值不存在相等的元组;左外连接、右外连接、全连接
- 交叉连接(笛卡尔积)
内外连接的区别在与那些被舍弃的元组是否保存到了结果中,通过后面的sql来理解
teacher、student表:

内连接:
- 等值连接
select * from student,teacher where student.id = teacher.tid;
仅取出student.id = teacher.tid的元组

- 非等值连接,也就是不是等号,而是>、<这些符号
select * from student,teacher where student.id > teacher.tid;

- 内连接不加限制就是交叉连接了,也就是笛卡尔积
外连接:R、S中舍弃的元组也保存到结果中,而在其他属性上填空值
- 左外连接:只保留左边关系R的舍弃的元组
select * from student left join teacher on student.id = teacher.tid;

- 右外连接:只保留右边关系S的舍弃的元组,因为我们teacher表比student表少,所以交换一下位置
select * from teacher
right join student on student.id = teacher.tid;

- 全连接:左右两表中的舍弃的元组都保留 MySQL不支持full join关键字,但可以通过左连接 union 右连接实现
Sql Server:
select * from 学期成绩 R full join 课程注册 S on R.课程编号 = S.课程编号;
这两个表是这样的:

可以获得查询结果:

Mysql通过左连接 union 右连接就不实现了
三大范式
为了建立冗余较小、结构合理的数据库,设计数据库时必须遵循一定的规则。在关系型数据库中,这种规则就是范式。范式是符合某一种级别的关系模式的集合。关系型数据库中的关系必须满足一定的要求,即满足不同的范式。
目前关系型数据库有六种范式,分别为:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、第四范式(4NF)、第五范式(5NF)和第六范式(6NF)。要求最低的范式是第一范式。第二范式在第一范式的基础上又进一步的添加了要求,其余范式依次类推。
一般说来,数据库只需满足第三范式就行了,而通常我们用的最多的就是第一范式、第二范式、第三范式,也就是接下来要讲的“三大范式”。
第一范式(1NF)
概念:第一范式用来确保每列(每个属性)的原子性,要求每列(每个元素)都是最小不可再分的最小数据单元->>{具有原子性}
举个🌰:
客人住宿信息表 (姓名,客人编号,地址,客房号,客房描述,客房类型,客房状态,床位数,入住人数,价格)
其中地址又可以进一步的划分为国家省、市、区等。将“地址”列拆分,使得每列都是具备原子性,则下列写法符合第一范式:
客人住宿信息表(姓名,客人编号,国家,省,市,区,门牌号,客房号,客房描述,客房类型,客房状态,床位数,入住人数,价格)
第二范式 (2NF)
概念:第二范式是在第一范式的基础上要求每列(每个属性)都与主键相关,也就是要求实体的唯一性。如果一个表满足第一范式并且除去主键额的其余属性(列)都完全依赖于该主键,那么该表就满足了第二范式。
还是以客人住宿信息表为🌰,客人住宿信息表中的数据主要用来描述客人住宿信息,所以该表主键为(客人编号,客房号),其中“姓名”、“地址”依赖于“客人编号”。“客房描述”、 “客房类型”、“客房状态”、“床位数”、“入住人数”、“价格”都依赖于“客房号”。所以使用第二范式后客人住宿信息表就会拆分为两张表:
客人信息表(客人编号,姓名,地址,客房号,入住时间,结账日期,押金,总金额),主键为“客人编号”列,其他列都全部依赖于主键列
客房信息表(客房号,客房描述,客房类型,客房状态,床位数,入住人数,价格),主键为“客房号”列,其他列都全部依赖于主键列
第三范式 (3NF)
概念:第三范式指的是在第二范式的基础上更近一层,确保每列都与主键直接相关,并不是间接相关,即限制列的冗余性。如果一个关系满足第二范式,并且除了主键以外的其他列都依赖于主键列,列和列之间不存在相互依赖关系,则满足第三范式。
以第二范式中的客房信息表为🌰,每列都和主键列“客房号”相关,再细看会发现:"床位数” 、“价格”依赖于“客房类型”,“客房类型”依赖于“客房号”,“床位数”、“价格”依赖于“客房号”。所以为了能满足第三范式客房信息表又可以分为两张表:
客房表(客房号,客房描述,客房类型编号,客房状态,入住人数)
客房类型表(客房类型编号,客房类型名称,床位数,价格)
Bcnf范式
要在 3NF 的基础上消除主属性对于码的部分与传递函数依赖。
函数依赖
函数依赖是关系模式中各个属性之间的一种依赖关系,是规范化理论中最为重要和基础的概念。
为了便于理解,直接通过以下一个例子进行讲解。
| 学号 | 姓名 | 专业名 | 性别 | 出生日期 | 本学期学分 |
|---|---|---|---|---|---|
| 42014601 | 小明 | 信管 | 男 | 2000-2-11 | 24 |
| 42014602 | 小红 | 电商 | 女 | 2001-8-7 | 24 |
| 42014603 | 二丫 | 财会 | 女 | 2001-9-4 | 24 |
| 42014604 | 狗蛋 | 物流 | 男 | 2002-10-18 | 24 |
我们观察以上的表,其中学号、姓名、性别、出生日期、本学期学分,这些我们可以看作是不同的属性,假设我们已知一个同学的学号为42014603,我们就可以得到这个人的姓名为二丫,专业为财会,性别为女等信息。
也就是说在此表中如果说已知了一个学号值,我们可以通过这个学号值查找到这个学号对应的唯一的姓名,也可以通过这个学号值得到唯一的专业名。
上面这种属性(属性组)和属性(属性组)之间一对一的推导关系便为函数依赖
我们用一个“→”来表示这种函数依赖关系。即X→Y,其所表达的含义为Y依赖于X(注意是后依赖前)
例如:学号→姓名,学号→专业名,学号→性别,学号→出生日期,学号→本学期学分
几种特别的函数依赖
为了便于理解下面内容,我们用以下这个表作为例子。
| 学号 | 姓名 | 课程号 | 个人成绩 |
|---|---|---|---|
| 011 | 小马 | 020114 | 99 |
平凡函数依赖
定义: 设一个关系为R(U),X和Y为属性集U上的子集,当X → Y时,如果Y ⊂ X(也就是Y是X的子集) 那么称X→Y是平凡的函数依赖。(这时候的X一般为属性组,Y为单个属性或者属性组)
举例:(学号,姓名)→姓名
注意:如果有Y ⊂ X ,那么X → Y 一定成立。
解释:因为Y ⊂ X ,那么Y必然是X中的一部分,因为X确定了,那么自然其子集也确定了,因为整体可以决定部分。
特殊情况:Y有可能和X是一样的,因为子集可以包含自己本身,也就是自己推导自己。
非平凡函数依赖
定义:设一个关系为R(U),X和Y为属性集U上的子集,若X→Y且X不包含Y,则称X→Y为非平凡函数依赖。
举例:
(学号、课程号)→个人成绩
相对于平凡函数依赖来说,非平凡函数依赖更为重要与常见
完全函数依赖
定义:一个关系模式R(U)中,X和Y为属性集U上的子集,如果X→Y,且对与X的任意一个真子集Z来说,Z→Y都不成立。
这个完全函数依赖我们分俩个情况理解,1.X为单个属性值,这时候X→Y,那么这个关系必定为完全依赖关系。2.X为属性组,也就是你用他的子集无法推导出Y,必须用整个X才行。
举例:1.单个属性:学号→姓名,这个依赖关系必是完全函数依赖,因为X没真子集。
2.属性组:(学号,课程号)→个人成绩,其真子集有俩:学号和课程编号,如果只知道其中一个,是推导不出来个人成绩的,必须合在一起整体使用,所以是完全函数依赖。
部分函数依赖
定义:一个关系模式R(U)中,X和Y为属性集U上的子集,如果X→Y,对与X的真子集Z来说,存在一个Z→Y,那么X→Y为部分函数依赖。
举例:(学号,课程号)→姓名,此时X为属性组(学号,课程号)其真子集有俩个:学号、课程号,我们通过学号可以直接得到学生的姓名,也就是学号→姓名,所以(学号,课程号)→姓名为部分函数依赖
传递函数依赖
定义:一个关系模式R(U)中,X、Y、Z为属性集U上的子集,如果X→Y,Y不能反推出X,且Y→Z,这时候通过X可以推出Z,即X→Z,我们把X→Z称为传递函数依赖。
| 学号 | 所在系 | 系主任 |
|---|---|---|
| 0112 | 管理系 | 王老师 |
学号→所在系,所在系→系主任,所以学号→系主任为传递函数依赖。
前言
事务是什么?
事务是由一个或多个sql语句组成一个最小的不可再分的工作单元。里面的内容要么都执行成功,要么都不成功。
事务的特性(ACID)
- 原子性(atomicity)。事务是一个不可分割的工作单元,要么全部提交,要么全部失败回滚。
- 一致性(consistency)。一致性指事务执行前后,数据从一个合法性状态变换到另一个合法性状态。例如要满足存在的约束,满足数据的一致性等、
- 隔离性(isolation)指一个事务的执行不能被其他事务干扰。
- 持久性(durability)一个事务一旦提交成功,它对数据库中数据的改变将是永久性的,接下来的其他操作或故障不应对其有任何影响。
存储引擎支持情况
可以通过如下指令查询存储引擎对事务的支持情况。
show engines;
你就可以得到类似如下的表格。可以看出在 Mysql5.7 中只有 InnoDB 是支持事务的。
| Engine | Support | Comment | Transactions | XA | Savepoints |
|---|---|---|---|---|---|
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
字符集
查看
查看数据库默认字符集
show VARIABLES like "%character%"
结果信息如下:
| MySql 5.7 | MySql 8.0 | ||
|---|---|---|---|
| Variable_name | Value | Variable_name | Value |
| character_set_client | utf8mb4 | character_set_client | utf8mb4 |
| character_set_connection | utf8mb4 | character_set_connection | utf8mb4 |
| character_set_database | latin1 | character_set_database | utf8mb4 |
| character_set_filesystem | binary | character_set_filesystem | binary |
| character_set_results | character_set_results | ||
| character_set_server | latin1 | character_set_server | utf8mb4 |
| character_set_system | utf8 | character_set_system | utf8mb3 |
| character_sets_dir | C:\Program Files\MySQL\MySQL Server 5.7\share\charsets\ | character_sets_dir | D:\dev-tools\mysql-8.0.33-winx64\share\charsets\ |
测试
在 MySQL 8.0 中执行建表语句
create database test_char;
use test_char;
create table user(id int,name varchar(20));
insert into user(id,name) value(1,"张三");
insert into user(id,name) value(2,"李四");
select * from `user`;
输出结果
id|name
--+----
1|张三
2|李四
试着在 MySQL 5.7 中执行建表语句,会出现错误。
SQL 错误 [1366] [HY000]: Incorrect string value: '\xE5\xBC\xA0\xE4\xB8\x89' for column 'name' at row 1
这是因为我们数据库和表使用的字符集是latin1,是不支持中文的。
可以在/mysql/conf/mysqld.conf或者my.ini,添加这如下配置就能将 Mysql 5.7 的字符集设置为 UTF8。重启服务后生效。需要注意的是:对于已经创建了的数据库和表字符集是不会被联动更改的,只对更改配置以后的新创建的数据库和表生效。
[mysqld]
character_set_server=utf8 # 设置为utf8,也可以设置为 utf8mb4
修改现有库的字符集
ALTER DATABASE test_char character set 'utf8';
对于现有表字符集的调整,可以通过以下指令进行操作。同样这对数据表里现有的字段不会联动更改。
-- 不会转换现有表和字段的字符集。
ALTER TABLE test_char.`user` DEFAULT CHARSET=utf8mb4;
-- 会转换现有数据表和字段的字符集为 utf8
ALTER TABLE user convert to CHARACTER set 'utf8';
对于现有数据表的字段调整。
ALTER TABLE test_char.`user` MODIFY COLUMN name varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
小结-各级别的字符集
从前面的测试例子,可以看出字符集从上到下,可以分为4类
-
服务器级别(决定默认字符集)。一般通过配置文件来配置。
[mysqld] character_set_server=utf8 -
数据库级别。未配置时采用服务器级别
-- 创建 create database [db_name] character set utf8; -- 更新 alter database [db_name] character set utf8; -
数据表级别。未配置时采用数据库级别
-- 创建 create table [tb_name] character set utf8; -- 更新 alter table [tb_name] character set utf8; -
数据列级别。未配置时采用数据表级别
-- 建表时 create table if not exists [tb_name]( id int(11) not null, name varchar(20) default null character set utf8mb4, -- 未设置则跟表级别 primary key (`id`) ) ENGINE=InnoDB Default CHARSET=utf8; -- 表级别设置 -- 已有数据表,注意字段类型,如果使用数字型会抛出异常 ALTER TABLE [tb_name] ADD COLUMN [column_name] varchar(1) NOT NULL DEFAULT 1 CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '1主要的,2次选的'; -- 修改数据列 Alter Table [tb_name] MODIFY COLUMN [column_name] varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci;
字符集比较规则
查看
通过查询字符集命令对比常用的 utf8,utf8mb3,utf8mb4有什么区别
show charset;
输出结果:
| CHARSET | Description | Default collation | maxlen |
|---|---|---|---|
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
| utf8mb3 | UTF-8 Unicode | utf8mb3_general_ci | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_0900_ai_ci | 4 |
- 这里可以看出
utf8和utf8最大字节长度都是3 - 注意:Mysql中
utf8是utf8mb3的别名 - 若有需求去存储4字节编码一个字符的情况,例如emoji表情,那就需要设置字符集为
utf8mb4. - 现在我们在设计 Web 数据库时更多的使用
utf8mb4
我们再看一下 Default collation ,代表字符集的默认比较规则。
| 后缀 | 描述 |
|---|---|
| _ai | 不区分重音 |
| _as | 区分重音 |
| _ci | 不区分大小写 |
| _cs | 区分大小写 |
| _bin | 以二进制方式比较 |
utf8_general_ci和utf8_unicode_ci对于中英文来说没有什么实质区别。utf8_general_ci相对而言速度快,但是准确度较差。utf8_unicode_ci准确度高,但是速度较慢。并且适用于多语言的比较。
查看和修改表的比较规则:
show table status from dbtest1 like '%user';
ALTER table user DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
变化过程
之前我们可以看到有几种类型的字符集,那么他们几种之间有什么区别?
- character_set_filesystem:文件系统上的存储格式,默认为binary(二进制),即不做转换;
- character_set_system :系统的存储格式,默认为utf8;
- character_sets_dir:可以使用的字符集的文件路径;
剩下的5个就是影响读写乱码的罪魁祸首:
- character_set_client:客户端请求数据的字符集;
- character_set_connection:从客户端接收到数据,然后传输的字符集;
- character_set_database:默认数据库的字符集;如果没有默认数据库,使用character_set_server字段;
- character_set_results:结果集的字符集,一般与业务代码的编码相同,否则会导致乱码;
- character_set_server:数据库服务器的默认字符集;
我们看一下客户端请求到返回的大致过程

- 客户端请求数据库数据,发送的数据采用
character_set_client字符集。客户端包括但不限于bash、jdbc和odbc等 - 数据库接收到客户端的请求后,将数据转换为
character_set_connection字符集 MySQL进行内部操作时,将数据字符集转换为内部操作字符集(使用每个数据字段的character set设定值;若没设定则使用对应数据表的default character set设定值;若没设定则使用对应数据库的default character set设定值;若没设定则使用character_set_server设定值)。- 将操作结果集从内部操作字符集转换为
character_set_result。
character_set_filesystem
character_set_filesystem文件系统配置的编码格式,把操作系统上的文件名转化为该字符集。- 即从
character_set_client转化为character_set_filesystem - 默认值
binary即不做操作。
我们来改变一下 character_set_filesystem ,看看输出结果。
- 设置成
UTF8/ascii
set character_set_filesystem = utf8
- 执行输出文件
select * from test into outfile 'D://你好.txt';
-- 这里 into outfile 可能会抛出异常,需要配置对应属性。secure-file-priv
- 查看输出文件名
character_set_system
默认就是utf8,它是元数据的编码,比如数据库的字段名、数据库名等。是个只读数据不能更改,而且也不要去更改它,因为类似于用户名密码可能会使用各种奇怪的字符,只有utf8能够容纳它们。
实验
关于collation_connection作用
其实这里为什么服务器不直接从character_set_client转化成对应character_set_results字符集?
这可以从官方文档中看到一段这样的说明解释了 character_set_client 、 character_set_server 和 character_set_connection。https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html
- What character set are statements in when they leave the client?
The server takes the
character_set_clientsystem variable to be the character set in which statements are sent by the client.
- What character set should the server translate statements to after receiving them?
To determine this, the server uses the
character_set_connectionandcollation_connectionsystem variables:
The server converts statements sent by the client from
character_set_clienttocharacter_set_connection. Exception: For string literals that have an introducer such as_utf8mb4or_latin2, the introducer determines the character set. See Section 10.3.8, “Character Set Introducers”.
collation_connectionis important for comparisons of literal strings. For comparisons of strings with column values,collation_connectiondoes not matter because columns have their own collation, which has a higher collation precedence (see Section 10.8.4, “Collation Coercibility in Expressions”).What character set should the server translate query results to before shipping them back to the client?
The
character_set_resultssystem variable indicates the character set in which the server returns query results to the client. This includes result data such as column values, result metadata such as column names, and error messages.
这里引用一下翻译。
服务器将客户端发送的语句从character_set_client转换为character_set_connection。异常:对于具有引入器(如_utf8mb4或_latin2)的字符串字面值,引入器决定字符集。
Collation_connection对于字面值字符串的比较很重要。对于字符串与列值的比较,collation_connection无关紧要,因为列有自己的排序规则,排序规则优先级更高
让我们来试验一下 关于 Collation_connection 的部分。我们用字面值来测试一下。
-
先执行查询字符集
show variables like "%character%" show variables like "%connection%" -- 查询排序字符Variable_name |Value ------------------------+------------------------------------------------------- character_set_client |utf8mb4 character_set_connection|utf8mb4 -
设置一下排序规则
set collation_connection=utf8_general_ci; # _ci不区分大小写 -
执行字面量查询
select 'A' = 'a''A' = 'a' --------- 1 -
再设置一下排序结果
set collation_connection=utf8_bin; -
执行字面量查询
select 'A' = 'a''A' = 'a' --------- 0
x可以看到两次输出结果分别是0和1,也就是说字面量直接根据配置的 collation_conntion 直接排序比较的。那表内部的是怎么样呢?
-
设置一下排序结果
set collation_connection=utf8_bin; -
创建表
CREATE TABLE `temp` ( `id` int(11) NOT NULL, `name` varchar(20) CHARACTER SET utf8mb4 DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -
执行对
name的比较规则设置utf8mb4_general_ci
ALTER TABLE test_char.temp MODIFY COLUMN name varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ```
-
执行查询比较
select (select name from temp where name = "A" ) = 'a'Name |Value| -----------------------------------------------+-----+ (select name from temp where name = "A" ) = 'a'|1 | -
执行对
name的比较规则设置utf8mb4_binALTER TABLE test_char.temp MODIFY COLUMN name varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;Name |Value| -----------------------------------------------+-----+ (select name from temp where name = "A" ) = 'a'|0 |
由这两个实验结果,我们可以看看字面量和列值比较结果是不一样的。
- 服务器在进行
字节序列处理的时候,根据character_set_connection的字符集进行编码,使用比较规则Collation_connection,进行比较。 - 另一边,对于数据表的数据列, 列的字符集和排列规则的优先级更高 ,也就是说还需要从
character_set_collection转换成对应列的字符集。 - 也就是说,对于
character_set_connection起了一个中转作用,解决了字面值无法排序处理的问题。
存在的问题
乱码
- 客户端使用的字符集和
character_set_results不一致,会导致客户端无法解码。也就是所谓的乱码现象(本质原因了)。 - 编码不一致,且是有损编码,所以转换过程中存在丢失。
处理方法:
-
错误方法
- Alter TABLE .. character set=XXX
如果数据已经产生丢失,那么再进行转换并不能起到修复作用。
- ALTER TABLE … CONVERT TO CHARACTER SET …
- 官方文档对该命令的解释:用于对一个表的数据进行编码转换,该命令只适用于当前并没有乱码,并且并不能将错进错出纠正为对进对出。
- 举例说明:假设我们有一张通过错进错出(set names latin1)存入了UTF-8的数据、编码是latin1的表,并打算把表的字符集编码改成UTF-8(同时set names utf8)并且不影响原有数据的正常显示,执行alter table test_latin1 convert to character set utf8后发现依然可以错进错出(set names latin1),然而对进对出就会显示乱码(这是由于错进错出本质上数据存储的内容就是错误的内容,我们通过正确的步骤编码、读取,得到的结果一定是错的)。
-
正确方法
-
导入导出法
- 将数据通过错进错出的方法导出到文件。 一定要确认导出的文件用文本编辑器在UTF-8编码下查看没有乱码
- 用正确的字符集创建新表
- 将之前导出的文件重新导入到新表中。
-
Convert to Binary & Convert Back
这种方法是将二进制数据作为中间数据的方法来实现修改编码的,因为MySQL在将有编码意义的数据流转换为无编码意义的二进制数据的时候并不做实际的数据转换,而从二进制数据准换为带编码的数据时又会用目标编码做一次编码转换校验,利用这两个特性就可以实现在MySQL内部模拟了一次“错出”将乱码修正了。
小结
- 如果想保证任何情况下都不出现乱码,那么我们应该保证数据库编码和set names XXX是一致的;
- 假如由于某种原因不能做到第一条,那么一定要保证character_set_client、character_set_connection、character_set_results是一致的且可以和数据库编码无损转换。
需求
有这么一条数据A,他有一个字段tag_ids,用来存储tag id列表。
1149265184814718977,26,33
需要查找的数据表T。
| id | name | tag_ids |
|---|---|---|
| 1 | 测试1 | 1149265184814718977,3333 |
| 2 | 测试2 | 123213,123333,33 |
| 3 | 测试3 | 1149265184814718977,26,1111 |
我需要将数据A的tag_ids分别放到数据表T的tag_ids里面进行查询。
我们知道find_in_set特别适合这种查询场景,我们可以轻松的用如下SQL,获取数据表T的符合条件的数据:
select * from T where find_in_set("33",tag_ids);
这样就会查询出符合条件tag_ids中包含33的数据出来了。
那我们直接将数据A的tag_ids放进去替换33呢?
select * from T where find_in_set("1149265184814718977,26,33",tag_ids);
执行结果显而易见,查询不出任何东西。那我们应该怎么解决这个问题呢?
解决方法
- 首先我们需要创建一张序列表
CREATE TABLE `sequence` (
`id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-
填入1-100的数据,具体可以根据需要增加。
-
利用substring_index和sequence切割字符串
SELECT substring_index( substring_index( t.tag_ids, ',', b.id ), ',',- 1 ) AS id,b.id FROM A AS a left JOIN sequence AS b ON b.id between 1 and ( length( a.tag_ids ) - length( REPLACE ( a.tag_ids, ',', '' )) + 1 )这会将数据表A转化成这样一下格式。
tag_ids 1149265184814718977 26 33 -
结合find_in_set查询数据表T。
SELECT b.NAME, b.tag_ids FROM ( SELECT substring_index( substring_index( a.tag_ids, ',', b.id ), ',',- 1 ) AS id FROM A AS a LEFT JOIN sequence AS b ON b.id BETWEEN 1 AND ( length( a.tag_ids ) - length( REPLACE ( a.tag_ids, ',', '' )) + 1 ) ) AS a JOIN T AS b ON FIND_IN_SET( a.id, b.tag_ids)
for update
for update 会给读操作上写锁(排他锁)。因为是写锁,如果上锁时,有另一事务对此数据加写锁,那么当前事务会等待写锁释放(提交或者回滚)后拿到锁,在执行操作。
常用场景:
- 用户金额
- 商品余量
基本用法
在查询语句里面加上 for update,例如:
select * from table where id = 1 for update;
不同数据库的扩展写法:
-
Oracle
- select * from table where id = 1 for update no wait; 不等待,获取不到则抛出异常返回
- select * from table where id = 1 for update wait 3; 等待3s,获取不到则抛出异常返回
-
Mysql
-
set session innodb_lock_wait_timeout=3; select * from kb_power_compere where user_id = #{userId} and del_flag = 0 for update;设置当前会话的锁等待时间,当超过的等待时间后,会抛出异常。
-
mybatis 下,需要这么写,借助属性timeout,单位s
<select id="selectForUpdate" timeout="20"> select * from kb_power_compere where user_id = #{userId} and del_flag = 0 for update; </select>
-
注意事项
for update是根据where的索引情况进行加锁,根据具体情况,可能会产生:行级锁、间隙锁、表级锁。
- 当查询语句走主键/唯一键索引,且数据全部命中,锁住单行。(即使是范围查询,比如 where id in (1,2,3),如果都存在,也是只锁1,3,5三行)。
- 当查询语句走主键/唯一键索引,但数据部分命中,或都不命中;或走非唯一索引:用间隙锁,锁住区间行。
- 当查询语句不走索引,会用间隙锁把整张表锁住(但其实并不是表锁),因此要尽量避免索引失效的场景。
参考
- https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-gap-locks
Mysql 5.7
查看未关闭的事务:
索引失效
- 范围查询的范围过大
- 联合索引时违背最左匹配原则
- 各种语法待试验....面经里写的那些感觉有的不太对,如:
- 有or必全有索引;
- 复合索引未用左列字段;
- like以%开头;
- 需要类型转换;
- where中索引列有运算;
- where中索引列使用了函数;
- 如果mysql觉得全表扫描更快时(数据少);
问题记录
SQL错误【1290】【HY000】
The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
引发问题SQL。引发原因是由于 into outfile 出发了mysql的安全配置权限问题。
select * from temp into outfile 'D://1.txt';
查看当前的安全配置。
show variables like "%secure%";
配置方法,打开配置文件 my.ini
[mysqld]
# 配置安全配置导出路径前缀
secure-file-priv = D:\dev-tools\mysql-8.0.33-winx64\output
重启mysql 服务即可。
SQL注入
前言
SQL注入(SQL Injection)是一种常见的Web安全漏洞,形成的主要原因是web应用程序在接收相关数据参数时未做好过滤,将其直接带入到数据库中查询,导致攻击者可以拼接执行构造的SQL语句。
注入示例
- 数字型。 这种注入发生在数字型参数上,它会把参数当做语句继续执行,从而改变SQL语句的结果。
SELECT * FROM table WHERE id = %s
SELECT * FROM table WHERE id = 1 OR 1=1
- 字符型。 这种注入发生在字符型参数上,它使用特殊字符对SQL语句进行改变,从而改变SQL语句的结果。
SELECT * FROM table WHERE name = %s
SELECT * FROM table WHERE name = 'admin' OR '1'='1'
危害
SQL注入漏洞对于数据安全的影响:
- 数据库信息泄漏:数据库中存放的用户的隐私信息的泄露。
- 网页篡改:通过操作数据库对特定网页进行篡改。
- 网站被挂马,传播恶意软件:修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。
- 数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员帐户被窜改。
- 服务器被远程控制,被安装后门:经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。
- 破坏硬盘数据,瘫痪全系统。
防范
关键是对所有可能来自用户输入的数据进行严格的检查、对数据库配置使用最小权限原则。
- 代码层
- 对用户输入进行严格转义和过滤(XSS)
- 使用预编译。将
SQL语句的构建和参数分离 - 使用存储过程
- 网络层
- 使用
WAF设备启动 防Sql Inject 注入策略 - 云端防护(阿里云盾)
- 使用
预编译的作用
-
可以优化SQL执行
预编译之后的 sql 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的sql,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。可以提升性能。
-
防止sql注入
使用预编译,而其后注入的参数将不会再进行SQL编译。也就是说其后注入进来的参数系统将不会认为它会是一条SQL语句,而默认其是一个参数,参数中的or或者and 等就不是SQL语法保留字了。
数据库预编译
1, 数据库SQL编译特性
数据库接受到sql语句之后,需要词法和语义解析,优化sql语句,制定执行计划。这需要花费一些时间。但是很多情况,我们的一条sql语句可能会反复执行,或者每次执行的时候只有个别的值不同(比如query的where子句值不同,update的set子句值不同,insert的values值不同)。
- 减少编译的方法
如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则效率就明显不行了。为了解决上面的问题,于是就有了预编译,预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化。一次编译、多次运行,省去了解析优化等过程。
例如:
select * from user where id = ? -- 可以是1,2,3...
- 缓存预编译
预编译语句被DB的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中(相当于一个涵数)就会得到执行。 并不是所有预编译语句都一定会被缓存,数据库本身会用一种策略(内部机制)。
- 预编译实现方法
预编译是通过PreparedStatement和占位符来实现的。
数据库开启预编译
-
数据库是否默认开启预编译和
Jdbc版本有关- 配置
jdbc链接时强制开启预编译和缓存:useServerPrepStmts和cachePrepStmts参数。 - 预编译和预编译缓存一定要同时开启或同时关闭。否则会影响执行效率
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true&cachePrepStmts=true"); - 配置
-
mysql的预编译- 开启了预编译缓存后,connection之间,预编译的结果是独立的,是无法共享的,一个connection无法得到另外一个connection的预编译缓存结果。
- 经过试验,
mysql的预编译功能对性能影响不大,但在jdbc中使用PreparedStatement是必要的,可以有效地防止sql注入。 - 相同
PreparedStatement的对象 ,可以不用开启预编译缓存。
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true"); PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, "aaa"); ResultSet rs1 = stmt.executeQuery();//第一次执行 s1.close(); stmt.setString(1, "ddd"); ResultSet rs2 = stmt.executeQuery();//第二次执行 rs2.close(); stmt.close(); //查看mysql日志 /*1 Prepare select * from users where name = ? 1 Execute select * from users where name = 'aaa' 1 Execute select * from users where name = 'ddd'*/
Mybatis 预编译
预编译的解析
预编译分为客户端和服务端预编译,而 Mybatis 就是一种客户端预编译。
那么我们看一下 XMLStatementBuilder 看看他是怎么实现的预编译
/**
* 解析mapper中的SQL语句
*/
public void parseStatementNode() {
// 获取sql ID
String id = context.getStringAttribute("id");
// 获取数据库ID,判断databaseId是否匹配
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取标签属性
// 获取节点名称<select> <insert>等
String nodeName = context.getNode().getNodeName();
// 转换指令类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 判断是否是查询
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 获取参数类型,转换出class
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取驱动
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// 转换SQl, 主键生成器
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 重要:解析SQL语句,封装成一个SqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
// 解析完毕,最后通过MapperBuilderAssistant创建MappedStatement对象
// 统一保存到Configuration的mappedStatements属性中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
Mybaits会对Sql标签做解析,然后转换成一个SqlSource在生成一个MappedStatement保存。langDriver.createSqlSource这个方法。这该方法会通过LanguageDriver对SQL语句进行解析,生成一个SqlSource。SqlSource封装了映射文件或者注解中定义的SQL语句,它不能直接交给数据库执行,因为里面可能包含动态SQL或者占位符等元素。- 而
MyBatis在实际执行SQL语句时,会调用SqlSource的getBoundSql()方法获取一个BoundSql对象,BoundSql是将SqlSource中的动态内容经过处理后,返回的实际可执行的SQL语句,其中包含?占位符List封装的有序的参数映射关系,此外还有一些额外信息标识每个参数的属性名称等。
-
resultSetType类型- FORWARD_ONLY:结果集的游标只能向下滚动。
- SCROLL_INSENSITIVE:结果集的游标可以上下移动,当数据库变化时,当前结果集不变。
- SCROLL_SENSITIVE:返回可滚动的结果集,当数据库变化时,当前结果集同步改变。
-
statementType类型- STATEMENT:普通语句。
- PREPARED:预处理。
- CALLABLE:存储过程。
看一下 XMLLanguageDriver.createSqlSource 这个方法
// 创建SqlSource
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
//创建XMLScriptBuilder对象
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
//通过XMLScriptBuilder解析SQL脚本
return builder.parseScriptNode();
}
XMLScriptBuilder.parseScriptNodes 进行解析
/**
* 解析SQL脚本
*/
public SqlSource parseScriptNode() {
//解析动态标签,包括动态SQL和${}。执行后动态SQL和${}已经被解析完毕。
//此时SQL语句中的#{}还没有处理,#{}会在SQL执行时动态解析
MixedSqlNode rootSqlNode = parseDynamicTags(context);
//如果是dynamic的,则创建DynamicSqlSource,否则创建RawSqlSource
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
是否为动态SQL的判断在parseDynamicTags方法中
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
// 文本节点
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 封装到 TextSqlNode
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 如果包含${},则是动态Sql
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// 除了${}外,其它的都是静态
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
如果是动态标签,创建的就是DynamicSqlSource,其获取的BoundSql就是直接进行字符串的替换。对于非动态标签,则创建RawSqlSource,对应?占位符的SQL语句
- 如果
SQL中的参数是用${}作为占位符的,那么该SQL属于动态SQL,封装为DynamicSqlSource。- 否则其他的都是非动态
SQL,封装为RawSqlSource。
- 否则其他的都是非动态
预编译的处理
我们来看看RowSqlSource 是怎么来做处理的,先看一下她的构造函数
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 重点 sqlSourceParser.parse
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
// SqlSourceBuilder.parse
public class SqlSourceBuilder extends BaseBuilder {
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 参数解析器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 以#{}的解析器
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
// 是否是收缩空白的SQL
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
// 否则是原始sql
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
}
对于非动态SQL,会生成一个以 #{ 为开头,} 为结尾的解析器。紧接着就会创建一个StaticSqlSource 类,在这里做个区分:
RawSqlSource: 存储的是只有 #{} 或者没有标签的纯文本SQL信息DynamicSqlSource: 存储的是写有 ${} 或者具有动态SQL标签的SQL信息StaticSqlSource: 是DynamicSqlSource和RawSqlSource解析为BoundSql的一个中间态对象类型。BoundSql:用于生成我们最终执行的SQL语句,属性包括参数值、映射关系、以及SQL(带问号的)
我们看一下关于这个解析器 GenericTokenParser.parse()这段代码中的使用
// 转换sql字符
public String parse(String text) {
// 空返回
if (text == null || text.isEmpty()) {
return "";
}
// search open token 查找 #{}
int start = text.indexOf(openToken);
// 查找不到#{}就返回
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
do {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 就是找到了#{}的结束标识},然后将中间的内容替换成?
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
} while (start > -1);
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
- 查找
#{是否存在,如果存在则继续查找 - 查找
}是否存在,如果存在则将中间的内容替换掉? - 返回 处理后了的
sql
解析器ParameterMappingTokenHandler 的处理,它是SqlSourceBuilder的一个静态内部类: 就是将内容转换成 ?,并将内容作为映射
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}
预编译的参数替换
那么在执行SQL的时候,则会去根据BoundSql来完成参数的赋值等操作。我们来看下RawSqlSource.getBoundSql这个函数:
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
因为只有#{}的SQL语句,在上文中可以看到最后会生成一个StaticSqlSource对象,而这个类中就重写了getBoundSql函数,里面主要构造了一个BoundSql对象。
public class StaticSqlSource implements SqlSource {
@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
主要看的是SimpleExecutor.prepareStatement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
最终会走到 DefaultParameterHandler.setParameters执行参数替换
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 获取传入参数
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// mode属性有三种:IN, OUT, INOUT。如果参数为 OUT 或 INOUT,参数对象属性的真实值将会被改变
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
}
// 如果类型处理器里面有这个类型,直接赋值即可。
else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
}
// 否则转化为元数据处理,通过反射来完成get/set赋值
else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 使用不同的类型处理器向jdbc中的PreparedStatement设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
小结
首先预编译对于Mybatis而言,相当于构建出了一条SQL的模板,将#{}对应的参数改为?而已。届时只需要更改参数的值即可,无需再对SQL进行语法解析等操作。
对于动态SQL的判断,就是在于是否包含${}占位符。如果包含了就通过DynamicSqlSource来解析。而这里则影响到SQL的解析:
DynamicSqlSource:解析包含${}的语句,其实也会解析#{}的语句。RawSqlSource:解析只包含#{}的语句。
这两种类型到最后都会转化为StaticSqlSource,然后由他创建一个BoundSql对象。包括参数值、映射关系、以及转化好的SQL。
最后是关于SQL的执行,即如何将真实的参数赋值到我们上面生成的模板SQL中。这部分逻辑发生在SQL的执行过程中,其入口SimpleExecutor.prepareStatement主要做了这么几件事。
- 根据我们上面生成的
BoundSql对象。拿到我们传入的参数。 - 对每个参数进行解析,转化成对应的类型。
- 如果转化出的参数值为
null,则直接赋值,否则,还要通过类型处理器来完成赋值操作。typeHandler.setParameter(ps, i + 1, value, jdbcType); - 每种类型处理器,则会对对应的参数进行赋值。
Mybatis支持的类型处理器可以看TypeHandlerRegistry这个类
前言
环境配置
本篇笔记基于环境如下:
- SpringBoot:2.1.3.RELEASE
- junit:4.13.2
- mockito:2.23.4
- Mybatis-Plus:3.4.2
pom.xml 文件相关内容如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<version>4.13.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
参考文档
基本使用
常用注解
-
@RunWith(MockitoJUnitRunner.class)。 告诉JUnit使用Mockito模拟框架提供的"单元测试运行器" -
@InjectMocks。 创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。 -
@Mock。对函数的调用均执行mock(即虚假函数),不执行真正部分。 是完全模拟出来的,就算项目中没有这个类的实例,也能自己mock出来一个。 -
@Spy。 对函数的调用均执行真正部分。 如果不存在需要我们创建一个。
@Mock 和 @Spy 都可用于拦截那些尚未实现或不期望被真实调用的对象和方法,并为其设置自定义行为。二者的区别在于Mock不真实调用,Spy会真实调用。
常用代码配置
Mockito.initMocks(this)。初始化当前的测试类
Mybatis-Plus
lambda的使用
@InjectMocks
MainServiceImpl mainService; // 测试的真实主类
@Mock
GoodsServiceImpl goodsService; // Mockito 自动模拟的类
@Mock
GoodsMapper goodsMapper;
@Mock
BaseMapper baseMapper;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
MapperBuilderAssistant mapperBuilderAssistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
TableInfoHelper.initTableInfo(mapperBuilderAssistant, Main.class);
TableInfoHelper.initTableInfo(mapperBuilderAssistant, Goods.class);
Mockito.when(goodsService.lambdaQuery()).thenReturn(ChainWrappers.lambdaQueryChain(baseMapper));
Mockito.when(goodsService.lambdaUpdate()).thenReturn(ChainWrappers.lambdaUpdateChain(baseMapper));
Mockito.when(baseMapper.insert(Mockito.any())).thenReturn(1);
}
- 之所以需要模拟
goodsService.lambdaQuery是因为因为Mockito的@Mock注入的模拟示例,是没有提供LambdaQueryChain示例的,需要我们手动注入一下,不然再后续执行中,会抛出异常。 baseMapper.insert(Mockito.any())使用来处理在IService抽象接口中提供的save等方法,实际调用的是BaseMapper的insert方法。此外其它的IService.delete等方法也都可以直接模拟BaseMapper.delete。TableInfoHelper.initTableInfo(mapperBuilderAssistant, Goods.class);。需要初始化,否则会提示异常can not find lambda cache for this entity
Mockito.mock和new
// new 真实对象
List<Long> realList = new ArrayList<>();
realList.add(1L);
realList.add(2L);
// 返回真实对象
Mockito.when(mainService.list(Mockito.any())).thenReturn(realList);
// Mockito.mock 模拟对象
List<Long> mockList = Mockito.mock(List.class);
mockList.add(1L);
mockList.add(2L);
// 返回模拟对象
Mockito.when(mainService.list2(Mockito.any())).thenReturn(mockList);
这里会有一个很神奇的地方,我才用 Mockito.mock 创建类的时候,执行了 set/add 方法,但是实际并没有影响到实体数据。 所以需要参与真实业务的地方需使用 new 创建类。
另外,在 Mockito 2.0.6 的中文文档中,使用的是 Mockito.mock 进行操作和校验,但是试验了后,无法满足需要,具体分析可能看实际场景吧。
Mock Mapper
Mock 主测试service的mapper时,会显示 NullPointerException 或者 ClassCastException: com.amiba.heitan.mall.admin.mapper.KbActivityAnnounceMapper$MockitoMock$332138621 cannot be cast to com.amiba.heitan.mall.admin.mapper.KbActivityAnnounceDmMapper
正确 Mock Mapper 的代码如下:
@RunWith(MockitoJUnitRunner.class)
public class KbActivityAnnounceDmServiceTest {
@InjectMocks
KbActivityAnnounceDmServiceImpl kbActivityAnnounceDmService;
@Mock(name="baseMapper")
KbActivityAnnounceDmMapper kbActivityAnnounceDmMapper;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
MapperBuilderAssistant mapperBuilderAssistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
TableInfoHelper.initTableInfo(mapperBuilderAssistant, KbActivityAnnounce.class);
}
@Test
public void selectDmListByActivityId() {
KbActivityAnnounceListCompereDTO dto = new KbActivityAnnounceListCompereDTO();
dto.setId(1L);
KbActivityAnnounce one = new KbActivityAnnounce();
kbActivityAnnounceDmService.selectDmListByActivityId(dto);
}
}
public interface KbActivityAnnounceDmMapper extends BaseMapper<KbActivityAnnounceDm> {
List<KbActivityAnnounceCompereListVO> selectDmListByActivityId(@Param("dto") KbActivityAnnounceListCompereDTO dto);
}
这样才能正确模拟实际测试类的 BaseMapper 。核心在于以下这句话
@Mock(name="baseMapper")
KbActivityAnnounceDmMapper kbActivityAnnounceDmMapper;
MockMultipartFile
- 读取真实文件进行文件模拟
File file = ResourceUtils.getFile("classpath:excel/demo.xlsx");
MultipartFile mulFile = new MockMultipartFile(
"demo.xlsx", //文件名
"demo.xlsx", //originalName 相当于上传文件在客户机上的文件名
ContentType.APPLICATION_OCTET_STREAM.toString(), //文件类型
new FileInputStream(file) //文件流
);
- 模拟文件流进行文件模拟
byte[] content = new byte[2];
content[0] = 1;
content[1] = 2;
MultipartFile mulFile = new MockMultipartFile(
"demo.xlsx", //文件名
"demo.xlsx", //originalName 相当于上传文件在客户机上的文件名
ContentType.APPLICATION_OCTET_STREAM.toString(), //文件类型
content //文件流
);
Pixably
https://pixabay.com/zh/images/search/。具有各种免版税资源和API调用
正则表达式记录
- 手机号。
/^1[3|4|5|6|7|8|9][0-9]\d{8}$/
工具清单
花生壳
花生壳是一款由国内上海贝锐信息科技股份有限公司研发的网络工具,主要功能是实现内网穿透和动态域名解析。它主要为没有公网IP或动态公网IP的用户,提供简单、高效、稳定、安全的远程访问服务。
花生壳的内网穿透功能通过云服务器快速与内网服务器建立连接,同时把内网端口映射到云端,实现各类局域网应用基于域名的互联网访问。此外,花生壳还支持反向代理应用,支持TCP、UDP、HTTPS协议,端到端的TLS加密通信,黑白名单防黑验证等,支持外网设备穿透各种复杂的路由和防火墙访问到内网的应用。
花生壳支持Windows、Linux、树莓派、iOS、安卓等操作系统,并可通过iPhone、安卓手机APP或微信进行远程管理。花生壳的应用领域非常广泛,包括OA、ERP、CRM、FTP、SVN、NAS、Web服务、视频监控、Windows RDP远程桌面、游戏联机、小程序开发调试、淘宝客采集系统、遥感测绘、工业4.0等。
总之,花生壳是一款功能强大、易于使用的网络工具,可以帮助用户轻松实现远程访问和穿透内网的功能,适用于各种应用场景。
nat 123
NAT123是一款专业的内网端口映射软件,它可以帮助用户在内网中创建服务器,并允许外网用户通过映射的端口访问这些服务器。NAT123支持多种内网地址格式,可以设置任意网络环境,包括代理上网和自动检测联网状态。
NAT123具有全端口映射、动态域名解析、域名解析管理等功能。它支持开机运行、自动登录、后台映射服务功能,可以在同一台电脑上创建多个服务器。NAT123还支持多端口映射、外网地址域名80端口映射、自定义外网访问端口、多地点登录映射和泛域名映射。
使用NAT123,经过映射的网站客户端用户访问真实IP不丢失,智能自动检测映射状态并提示。此外,NAT123还提供免费的域名、80映射、https映射、非80网站映射、非网站映射等功能。
对比
nat123 VS hxk
花生壳和NAT123都是内网穿透工具,它们各有优缺点。以下是它们的一些主要优缺点:
花生壳:
优点:
易用性:花生壳的软件客户端界面操作简单直观,操作提示清晰,用户可以快速完成映射设置。 系统兼容性:花生壳支持多种操作系统,包括Windows、Linux、安卓、MacOS、OpenWRT、嵌入式SDK和树莓派等。 支持的映射类型:花生壳支持基本的80端口HTTP和443端口HTTPS映射,还提供了自带的“壳域名”功能,可以直接映射目前主流的HTTPS访问,不需要再额外去申请SSL证书或对本地的Web服务器进行设置。 企业级方案:花生壳拥有完善的企业级方案,包括采用BGP机房线路的服务,保障跨运营商访问的稳定性。 缺点:
成本:虽然花生壳有免费版本,但免费版本的功能可能有限,如果需要更高级的功能,可能需要购买付费版本。 NAT123:
优点:
稳定性:NAT123在稳定性方面表现较好,适合需要长期稳定服务的用户。 缺点:
易用性:NAT123的界面设计相对陈旧,操作不够直观,对于新手用户可能存在一定的学习曲线。 系统兼容性:NAT123支持的系统和环境相对较少,可能不适用于所有用户。 支持的映射类型:NAT123没有场景映射,不便于快速进行映射。 企业级方案:NAT123缺少面向企业的方案,可能不适用于企业级用户。 购买方式:NAT123的购买方式相对复杂,需要先充值再购买。 总之,花生壳和NAT123各有优缺点,用户可以根据自己的需求和场景选择合适的内网穿透工具。
关于花生壳连接映射地址失败
-
问题:花生壳软件连接域名服务器失败,但是测试时实际是能通的
-
解决办法:花生壳映射失败需要根据使用的应用及本地网络环境进行排查解决。
1,检查本地hosts解析文件。看看是否有错误的内网IP地址解析到域名。
2,本地清除DNS缓存,或更换电信或阿里云公共DNS。可能是本地DNS解析未刷新原因引起。
3,本地安装目录删除本地生成配置文件PhMain.ini,然后重启应用。配置文件出错了,我也不知道这是为什么,官方就是这么说的,具体原因不详。
4,操作失误,重新做一次映射流程。
5,没有内网映射资格。需要抢资格或开通。
6,流量用完或过期。抢到的映射资格有1G流量限制,一个月有效期。
7,非默认随机二级域名,或非转入解析域名。使用自己的域名需要转入域名解析,且开通映射资格。不支持自主域名哦。
此外,还可能使端口问题。
1、 尝试:本地连接-属性-双击internet协议-使用下面的DNS服务器地址-首选:8.8.8.8备用:8.8.4.4-确定-然后通过开始-运行-cmd-ipconfig/flushdns 命令清理缓存,再重新登录看下是否正常。
2、 试下命令其他命令,看反馈什么结果,或者使用命令清理下缓存:ipconfig/flushdns 以及清理下浏览器的缓存,看是否还能访问本地。
3、 把相关缓存文件清除,重新操作下映射。
4、 若以上操作都无法解决,建议转入解析到我们,对域名增加cname选配,再进行设置。
连接mysql后,无法执行多条记录
问题描述
SQL 如下
create database test_char;
use test_char;
create table user(id int,name varchar(20));
insert into user(id,name) value(1,"张三");
insert into user(id,name) value(2,"李四");
select * from `user`;
异常信息:
[1064] You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'use test_char;
create table t_user (id int,name varchar(20));
insert into ' at line 3
解决方法
- 确保每行SQL语句后面有加
; - 在数据库连接串上加上
allowMultiQueries属性为true - 对于
Dbeaver工具连接的,设置地址为:编辑连接->连接属性->驱动属性
IntelliJ IDEA设置类注释和方法注释带作者和日期
设置路径:File->setting->Editor->File and Code Templates -> File header
/**
* @Version: 1.0.0
* @Author: ${USER}
* @Date: ${YEAR}/${MONTH}/${DAY}/${TIME}
* @ClassName: ${NAME}
* @Description: ${NAME}
*/

参数说明
| 变量 | 说明 |
|---|---|
| ${NAME} | 文件名称 |
| ${YEAR} | 年 |
| ${MONTH} | 月 |
| ${DAY} | 日 |
| ${HOUR} | 时 |
| ${MINUTE} | 分 |
| ${USER} | 登录用户 |
| ${TIME} | 时分 |
Idea-问题记录
部署github page action
关于mdbook.yml文件记录
# Sample workflow for building and deploying a mdBook site to GitHub Pages
#
# To get started with mdBook see: https://rust-lang.github.io/mdBook/index.html
#
name: Deploy mdBook site to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
env:
MDBOOK_VERSION: 0.4.21
steps:
- uses: actions/checkout@v3
- name: Install mdBook
run: |
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh
rustup update
cargo install --version ${MDBOOK_VERSION} mdbook
- name: Setup Pages
id: pages
uses: actions/configure-pages@v3
- name: Build with mdBook
run: mdbook build
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./book
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
生成目录
java
https://github.com/smileluck/utils-project.git
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicInteger;
public class MdBookApplication {
private final static String path = "D:\project\B.Smile\SmileX-Note\src";
public static void main(String[] args) throws IOException {
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
StringBuilder sb = new StringBuilder();
Files.walkFileTree(Paths.get(path), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("文件夹:" + dir);
String fileName = dir.getFileName().toString();
if (!fileName.endsWith("assets")) {
String absolutePath = dir.toAbsolutePath().toString();
String replace = absolutePath.replace(path, "");
String[] split = replace.split("\\\\");
int len = split.length - 1;
for (int i = 0; i < len; i++) {
sb.append("\t");
}
sb.append("- [" + fileName + "]()\n");
dirCount.incrementAndGet();
}
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("文件" + file);
String fileName = file.getFileName().toString();
String absolutePath = file.toAbsolutePath().toString();
if (fileName.equalsIgnoreCase("README.md") || fileName.equalsIgnoreCase("SUMMARY.md")) {
} else if (fileName.endsWith(".md") || fileName.endsWith(".md")) {
String replace = absolutePath.replace(path + "\\", "");
absolutePath.replace("\\\\", "/");
String[] split = replace.split("\\\\");
int len = split.length - 1;
for (int i = 0; i <= len; i++) {
sb.append("\t");
}
replace = replace.replace("\\", "/");
replace = replace.replace(" ", "%20");
sb.append("- [" + fileName.substring(0, fileName.length() - 3) + "](" + replace + ")\n");
fileCount.incrementAndGet();
}
return super.visitFile(file, attrs);
}
});
// System.out.println("dir count:" + dirCount);
// System.out.println("file count:" + fileCount);
System.out.println(sb.toString());
}
}
踩坑注意
子目录访问404问题
在 SUMMARY.MD 的路径中,不能使用 \,必须得使用 "/",否则部署的时候子目录访问会404
- [糯米类]()
- [糯米糍](foods/nuomici.md)
关于[toc]
-
更换
action的mdbook.md文件。增加cargo install mdbook-toc# Sample workflow for building and deploying a mdBook site to GitHub Pages # # To get started with mdBook see: https://rust-lang.github.io/mdBook/index.html # name: Deploy mdBook site to Pages on: # Runs on pushes targeting the default branch push: branches: ["main"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: "pages" cancel-in-progress: false jobs: # Build job build: runs-on: ubuntu-latest env: MDBOOK_VERSION: 0.4.36 steps: - uses: actions/checkout@v4 - name: Install mdBook run: | curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh rustup update cargo install --version ${MDBOOK_VERSION} mdbook cargo install mdbook-toc - name: Setup Pages id: pages uses: actions/configure-pages@v4 - name: Build with mdBook run: mdbook build - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ./book # Deployment job deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 -
book.toml增加 自定义指令[preprocessor.toc] command = "mdbook-toc" # 自定义指令 marker = "[toc]" # 识别标签 renderer = ["html"] # 渲染
1. 配置putty
- 打开putty
- 进入 Connect=>SSH=>Tunnels 面板。
- 将目标设置成 Dynamic+Auto,添加一个自定义代理端口给,本例中设置为1080,点击ADD。上面输入框中出现D1080。

- 进入Session
- 配置自己的代理服务器地址,配置端口(默认22),类型选择ssh,设置一个存档地址并点击保存按钮。

- 点击 Open连接。
- 弹出的终端窗口中,输入服务器账号密码,登陆成功后,SSH隧道也建立成功了。代理上网期间,终端窗口不能关闭。
设置本地浏览器代理。
- 以firefox为例,打开 FireFox->选项->常规->网络设置->设置
- 设置Socket主机
- 保存即可。

最小账户问题
我们之前用putty连接SSH时,使用的账户要考虑安全性问题,尽量避免使用root账户。大家都知道ssh在linux系统上的重要性,为了安全我们要使用最小权限的账号来做ssh代理,让其只能ssh转发却不能登录系统。
useradd -s /bin/false username #创建用户,-s参数可指定用户的shell,这里设置成了 /bin/false.这样用户就无法与系统进行交互.
passwd username #设置密码
计算机-整数和浮点数表示
大纲
https://github.com/toolgood/ToolGood.Words/blob/master/java/toolgood.words/src/main/java/toolgood/words/internals/TrieNode.java
https://github.com/toolgood/ToolGood.Words/tree/master
https://www.baidu.com/s?wd=%E6%95%8F%E6%84%9F%E8%AF%8D%E7%AE%97%E6%B3%95%20Dfa%E5%92%8CAC%E7%9A%84%E5%8C%BA%E5%88%AB&rsv_spt=1&rsv_iqid=0xd209b97d000064ba&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&rqlang=cn&tn=baiduhome_pg&rsv_enter=1&rsv_dl=tb&oq=%25E6%2595%258F%25E6%2584%259F%25E8%25AF%258D%25E7%25AE%2597%25E6%25B3%2595&rsv_btype=t&inputT=3965&rsv_t=fe0bqJLPr3G51w%2FjjQ%2FrebDbhEg5qJZVl32v%2BKkXEdUKdtr30yfT1IlqR3iD6kFuf6LT&rsv_pq=8de24b9d0003dc31&rsv_sug3=70&rsv_sug2=0&rsv_sug4=5147
https://www.jianshu.com/p/fefbc1443540/
https://www.cnblogs.com/feiquan/p/11725982.html
https://www.cnblogs.com/AlanLee/p/5329555.html
https://www.jianshu.com/p/bf881b395182
https://blog.51cto.com/u_15127545/2699650
https://blog.csdn.net/WuLex/article/details/108351525
https://www.zhihu.com/question/39767421
前言
因为我们程序中的内存都是有限的,不可能无限制的往内存里存储数据。这时就需要使用适合的淘汰算法辅助淘汰价值更低的数据,为新的数据提供存储空间。
常见的算法
FIFO(First In First Out):先进先出。
FIFO 优先淘汰掉最先缓存的数据。
缺点是如果先缓存的数据使用频率比较高的话,那么该数据就不断地从缓存中存入又被移除,因此它的缓存命中率比较低。
LRU(Least Recently Used):最近最久未使用。
LRU 优先淘汰掉最久未访问到的数据。
缺点在不能应对偶发的流量激增,当热点数据被频繁访问,但是最后几分钟几乎没有访问,此时有另一批数据突然插入并被访问,那么热点数据就会被淘汰掉。相当于一个链表,移除掉了链表最后的数据,将新的数据放入链表头。
LFU(Least Frequently Used): 最近最少使用
LFU 优先淘汰掉最不经常使用的数据,需要维护一个使用频率的字段。
主要两个缺点:
- 访问频率较高,那么频率字段会占据一定空间
- 无法分辨是最近热点还是过去热点数据,因为前期访问高的数据已经固定下来,后续偶然数据也不太可能被保留下来。
TinyLFU
它是专门为了解决 LFU 上述提到的两个问题而被设计出来的。
- 第一个缺点采用的是 Count-Min Sketch算法。
- 第二个缺陷是让数据保持相对的新鲜。
统计频率 Count-Min Stketch
如何对一个key进行统计,但又可以节约空间?如果采用 HashMap ,存储精确值,那么对于这个维护会很占用内存。说到这可能就容易联想到一个东西,布隆过滤器,都是产生hash值,存储近似值。
近似频率的统计如下图所示。
对一个 key 进行多次 hash 函数后,index 到多个数组位置后进行累加,查询时取多个值中的最小值即可。
Caffeine 对这个算法的实现在FrequencySketch类。但 Caffeine 对此有进一步的优化,例如 Count–Min Sketch 使用了二维数组,Caffeine 只是用了一个一维的数组;再者,如果是数值类型的话,这个数需要用 int 或 long 来存储,但是 Caffeine 认为缓存的访问频率不需要用到那么大,只需要 15 就足够,一般认为达到 15 次的频率算是很高的了,而且 Caffeine 还有另外一个机制来使得这个频率进行衰退减半(下面就会讲到)。如果最大是 15 的话,那么只需要 4 个 bit 就可以满足了,一个 long 有 64bit,可以存储 16 个这样的统计数,Caffeine 就是这样的设计,使得存储效率提高了 16 倍。
Caffeine 对缓存的读写(afterRead和afterWrite方法)都会调用onAccess 方法,而onAccess方法里有一句:
frequencySketch().increment(key);
我们看看 FrequencySketch。
/*
* Copyright 2015 Ben Manes. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.benmanes.caffeine.cache;
import static com.github.benmanes.caffeine.cache.Caffeine.requireArgument;
import org.checkerframework.checker.index.qual.NonNegative;
/**
* A probabilistic multiset for estimating the popularity of an element within a time window. The
* maximum frequency of an element is limited to 15 (4-bits) and an aging process periodically
* halves the popularity of all elements.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
/**
一个概率多集来估算在时间窗口内流行的元素。
一个元素最大频率是限制在15(4bit)和一个老化过程会周期性地使所有元素的流行程度减半。
*/
final class FrequencySketch<E> {
/*
* This class maintains a 4-bit CountMinSketch [1] with periodic aging to provide the popularity
* history for the TinyLfu admission policy [2]. The time and space efficiency of the sketch
* allows it to cheaply estimate the frequency of an entry in a stream of cache access events.
*
* The counter matrix is represented as a single-dimensional array holding 16 counters per slot. A
* fixed depth of four balances the accuracy and cost, resulting in a width of four times the
* length of the array. To retain an accurate estimation, the array's length equals the maximum
* number of entries in the cache, increased to the closest power-of-two to exploit more efficient
* bit masking. This configuration results in a confidence of 93.75% and an error bound of
* e / width.
*
* To improve hardware efficiency, an item's counters are constrained to a 64-byte block, which is
* the size of an L1 cache line. This differs from the theoretical ideal where counters are
* uniformly distributed to minimize collisions. In that configuration, the memory accesses are
* not predictable and lack spatial locality, which may cause the pipeline to need to wait for
* four memory loads. Instead, the items are uniformly distributed to blocks, and each counter is
* uniformly selected from a distinct 16-byte segment. While the runtime memory layout may result
* in the blocks not being cache-aligned, the L2 spatial prefetcher tries to load aligned pairs of
* cache lines, so the typical cost is only one memory access.
*
* The frequency of all entries is aged periodically using a sampling window based on the maximum
* number of entries in the cache. This is referred to as the reset operation by TinyLfu and keeps
* the sketch fresh by dividing all counters by two and subtracting based on the number of odd
* counters found. The O(n) cost of aging is amortized, ideal for hardware prefetching, and uses
* inexpensive bit manipulations per array location.
*
* [1] An Improved Data Stream Summary: The Count-Min Sketch and its Applications
* http://dimacs.rutgers.edu/~graham/pubs/papers/cm-full.pdf
* [2] TinyLFU: A Highly Efficient Cache Admission Policy
* https://dl.acm.org/citation.cfm?id=3149371
* [3] Hash Function Prospector: Three round functions
* https://github.com/skeeto/hash-prospector#three-round-functions
*/
static final long RESET_MASK = 0x7777777777777777L;
static final long ONE_MASK = 0x1111111111111111L;
int sampleSize;
int blockMask;
long[] table;
int size;
/**
* Creates a lazily initialized frequency sketch, requiring {@link #ensureCapacity} be called
* when the maximum size of the cache has been determined.
*/
@SuppressWarnings("NullAway.Init")
public FrequencySketch() {}
/**
* Initializes and increases the capacity of this <tt>FrequencySketch</tt> instance, if necessary,
* to ensure that it can accurately estimate the popularity of elements given the maximum size of
* the cache. This operation forgets all previous counts when resizing.
*
* @param maximumSize the maximum size of the cache
*/
// 配置最大数量
public void ensureCapacity(@NonNegative long maximumSize) {
requireArgument(maximumSize >= 0);
int maximum = (int) Math.min(maximumSize, Integer.MAX_VALUE >>> 1);
if ((table != null) && (table.length >= maximum)) {
return;
}
table = new long[Math.max(Caffeine.ceilingPowerOfTwo(maximum), 8)];
sampleSize = (maximumSize == 0) ? 10 : (10 * maximum);
blockMask = (table.length >>> 3) - 1;
if (sampleSize <= 0) {
sampleSize = Integer.MAX_VALUE;
}
size = 0;
}
/**
* Returns if the sketch has not yet been initialized, requiring that {@link #ensureCapacity} is
* called before it begins to track frequencies.
*/
public boolean isNotInitialized() {
return (table == null);
}
/**
* Returns the estimated number of occurrences of an element, up to the maximum (15).
*
* @param e the element to count occurrences of
* @return the estimated number of occurrences of the element; possibly zero but never negative
*/
// 返回元素出现的估计次数,不超过最大次(15)
@NonNegative
public int frequency(E e) {
if (isNotInitialized()) {
return 0;
}
int[] count = new int[4];
//重新将hashcode 均匀分散
int blockHash = spread(e.hashCode());
// 应用新一轮hash,重新打散
int counterHash = rehash(blockHash);
//这个block 就是用来定位到是哪一个等分的,,再左移3位,得到一个小于16的值
int block = (blockHash & blockMask) << 3;
for (int i = 0; i < 4; i++) {
int h = counterHash >>> (i << 3);
int index = (h >>> 1) & 15;
int offset = h & 1;
count[i] = (int) ((table[block + offset + (i << 1)] >>> (index << 2)) & 0xfL);
}
// 取4个中最小的值
return Math.min(Math.min(count[0], count[1]), Math.min(count[2], count[3]));
}
/**
* Increments the popularity of the element if it does not exceed the maximum (15). The popularity
* of all elements will be periodically down sampled when the observed events exceed a threshold.
* This process provides a frequency aging to allow expired long term entries to fade away.
*
* @param e the element to add
*/
@SuppressWarnings("ShortCircuitBoolean")
public void increment(E e) {
if (isNotInitialized()) {
return;
}
int[] index = new int[8];
int blockHash = spread(e.hashCode());
int counterHash = rehash(blockHash);
int block = (blockHash & blockMask) << 3;
for (int i = 0; i < 4; i++) {
int h = counterHash >>> (i << 3);
index[i] = (h >>> 1) & 15;
int offset = h & 1;
index[i + 4] = block + offset + (i << 1);
}
boolean added =
incrementAt(index[4], index[0])
| incrementAt(index[5], index[1])
| incrementAt(index[6], index[2])
| incrementAt(index[7], index[3]);
if (added && (++size == sampleSize)) {
reset();
}
}
/** Applies a supplemental hash functions to defends against poor quality hash. */
static int spread(int x) {
x ^= x >>> 17;
x *= 0xed5ad4bb;
x ^= x >>> 11;
x *= 0xac4c1b51;
x ^= x >>> 15;
return x;
}
/** Applies another round of hashing for additional randomization. */
static int rehash(int x) {
x *= 0x31848bab;
x ^= x >>> 14;
return x;
}
/**
* Increments the specified counter by 1 if it is not already at the maximum value (15).
*
* @param i the table index (16 counters)
* @param j the counter to increment
* @return if incremented
*/
boolean incrementAt(int i, int j) {
int offset = j << 2;
long mask = (0xfL << offset);
if ((table[i] & mask) != mask) {
table[i] += (1L << offset);
return true;
}
return false;
}
/** Reduces every counter by half of its original value. */
void reset() {
int count = 0;
for (int i = 0; i < table.length; i++) {
count += Long.bitCount(table[i] & ONE_MASK);
table[i] = (table[i] >>> 1) & RESET_MASK;
}
size = (size - (count >>> 2)) >>> 1;
}
}
保新机制
为了让缓存保新,过滤掉过往频率很高但之后不常使用的过期缓存,Caffeine 当整体的统计计数达到某一个值后,那么将记录的频率都除以2。
boolean added =
incrementAt(index[4], index[0])
| incrementAt(index[5], index[1])
| incrementAt(index[6], index[2])
| incrementAt(index[7], index[3]);
if (added && (++size == sampleSize)) {
reset();
}
/** Reduces every counter by half of its original value. */
void reset() {
int count = 0;
for (int i = 0; i < table.length; i++) {
count += Long.bitCount(table[i] & ONE_MASK);
table[i] = (table[i] >>> 1) & RESET_MASK;
}
size = (size >>> 1) - (count >>> 2);
}
为什么是除以 2,及其正确性,在 TinyLFU 论文中 3.3 章节给出了数学证明
The downside of this operation is an expensive infrequent operation that goes over all the counters
in the approximation scheme and divides them by 2.Yet, division by 2 can be implemented efficiently
in hardware using shift registers. Similarly, in software, shift and mask operations allow for performing
this operation for multiple (small) counter at once. Finally, its amortized complexity is constant making
it feasible for many applications.
# reset 误差问题
Since our reset operation uses integer division, it introduces truncation error. That is, after a reset
operation, the value of a counter can be as much as 0.5 lower than that of a floating point counter. If we
have to reset again, after the reset the truncation error of the previous reset operation is divided to 0.25,
but we accumulated a new truncation error of 0.5 resulting in a total error of 0.75. It is easy to see that
the worst case truncation error converges to at most one point lower than the accurate rate of the item.
Therefore, the truncation error affects the recorded occurrence rate of an item by as much as 2
W right
after a reset operation. This means that the larger the sample size, the smaller the truncation error.
W-TinyLFU
与最先进的缓存策略相比,在一些工作负载中,TinyLFU的性能不是很好。这种情况主要发生在包含对同一对象的“稀疏突发”的跟踪中,这在存储服务器中很常见。也就是说,在这些情况下,属于新爆发的项目在被驱逐之前没有设法建立足够的频率来保持在缓存中,从而导致重复失败。 于是 Caffeine 设计出一种新的 policy,即 Window Tiny LFU(W-TinyLFU) 由两个缓存区构成。

主缓存采用SLRU退出策略和TinyLFU准入策略,而窗口缓存采用LRU退出策略,但不采用任何准入策略。
主缓存中SLRU策略的A1和A2区域是静态划分的,因此80%的空间分配给热点项(A2),而受害者从20%的非热点项(A1)中挑选。
参考
- caffeine:https://arxiv.org/pdf/1512.00727.pdf
版本规范
很多时候我们开发的时候,都需要考虑接口的一个版本迭代问题。有时可能会需要重新设计接口。
-
调整
Controller目录结构- 直接区分 v1,v2 的方式。
- /controller/app/v1;
- /controller/app/v2;
- 直接区分 v1,v2 的方式。
-
接口设计
-
在域名上做区分。
- v1.api.com;
- v2.api.com;
-
在Path上做区分
- api.demo.com/v1/;
- api.demo.com/v2/;
-
利用Request Header区分,自定义请求头,例如 api-version;
curl "http://api.demo.com/test" ^ -H "api-version: 2" ^ --compressed- 利用Request Params区分
curl "http://api.demo.com/test?api-version=2" --compressed -
-
区分依据
-
任何接口在首次发布都属于初始版本(v1)接口
-
项目发布后,如果新增的需求和 v1 接口不冲突,属于新增的接口,仍然属于 v1 接口
-
项目发布后,如果新增的需求和 v1 接口冲突,为了兼容原有接口,属于v2接口
-
项目发布后,如果新增的需求和 v1 属于接口改动,相当于是兼容升级,则需要在原有代码逻辑耦合进去。
小结
- 接口变化非常大或者整个产品的大版本更新
- 可以采用URL更新版本的方式,创建新的
Controller或者部署新的服务 - 无版本号的走默认逻辑
- 可以采用URL更新版本的方式,创建新的
- 常规的接口升级和BUGFIX
- 一般可以通过
Header中指定版本号向下兼容 - 在代码中进行判断即可满足要求
- 无版本号的走默认逻辑
- 一般可以通过
- 两种方式同时使用
- URL自带的方式实现大版本升级
- 后续小版本迭代可以通过
Header版本号进行兼容升级
RequestCondition实现URL版本管理
实现方案
- 创建注解
@ApiVersion,默认值为 1 - 创建URL匹配规则
ApiVersionCondition继承RequestCondition - 创建
ApiRequestMappingCondition匹配对应的请求,选择合适的版本
创建注解
/**
* API 版本控制
*
* @Version: 1.0.0
* @Author: Administrator
* @Date: 2023/04/17/21:06
* @ClassName: ApiVersion
* @Description: ApiVersion
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
/**
* @return 版本
*/
int value() default 1;
}
URL匹配规则
继承 RequestCondition ,泛型为本身,作用是:
- 正则匹配版本号(v1,v2)并提取出来,与注解里面的版本做对比,返回
Condition或null - 设置优先级排序规则
- 类和方法上都有注解的时候,将进行合并(combine)
/**
* @Version: 1.0.0
* @Author: Administrator
* @Date: 2023/04/17/21:23
* @ClassName: ApiVersionCondition
* @Description: ApiVersionCondition
*/
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(".*v(\\d+).*");
private int apiVersion;
public ApiVersionCondition(int apiVersion) {
this.apiVersion = apiVersion;
}
private int getApiVersion() {
return apiVersion;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition apiVersionCondition) {
return new ApiVersionCondition(apiVersionCondition.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (m.find()) {
Integer version = Integer.valueOf(m.group(1));
if (version >= this.apiVersion) {
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
return apiVersionCondition.getApiVersion() - this.apiVersion;
}
}
匹配处理器
/**
* @Version: 1.0.0
* @Author: Administrator
* @Date: 2023/04/19/17:08
* @ClassName: ApiRequestMappingHandlerMapping
* @Description: ApiRequestMappingHandlerMapping
*/
public class ApiRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private static final String VERSION_FLAG = "{version}";
private static RequestCondition<ApiVersionCondition> createCondition(Class<?> clazz) {
RequestMapping classRequestMapping = clazz.getAnnotation(RequestMapping.class);
if (classRequestMapping == null) {
return null;
}
StringBuilder mappingUrlBuilder = new StringBuilder();
if (classRequestMapping.value().length > 0) {
mappingUrlBuilder.append(classRequestMapping.value()[0]);
}
String mappingUrl = mappingUrlBuilder.toString();
if (!mappingUrl.contains(VERSION_FLAG)) {
return null;
}
ApiVersion apiVersion = clazz.getAnnotation(ApiVersion.class);
return apiVersion == null ? new ApiVersionCondition(1) : new ApiVersionCondition(apiVersion.value());
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return createCondition(method.getClass());
}
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return createCondition(handlerType);
}
}
注册处理器
@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new ApiRequestMappingHandlerMapping();
}
}
测试接口
v1
@Slf4j
@ApiVersion
@RestController
@RequestMapping("/app/demo/{version}")
public class AppDemoV1Controller {
@RequestMapping("/test")
public R test() {
log.info("v1 test");
return R.success("v1 test");
}
@RequestMapping("/extend")
public R extend() {
log.info("v1 extend");
return R.success("v1 extend");
}
}
v2
@Slf4j
@RestController
@ApiVersion(2)
@RequestMapping("/app/demo/{version}")
public class AppDemoV2Controller {
@RequestMapping("/test")
public R test() {
log.info("v2 test");
return R.success("v2 test");
}
}
效果如下
# 调用 v1 test接口
curl http://127.0.0.1:8080/smilex/app/demo/v1/test
{"code":200,"msg":"v1 test","success":true}
# 调用 v2 test接口
curl http://127.0.0.1:8080/smilex/app/demo/v2/test
{"code":200,"msg":"v2 test","success":true}
# 调用 v1 extend接口
curl http://127.0.0.1:8080/smilex/app/demo/v1/extend
{"code":200,"msg":"v1 extend","success":true}
# 调用 V2 extend接口,实际匹配 v1 extend
curl http://127.0.0.1:8080/smilex/app/demo/v2/extend
{"code":200,"msg":"v1 extend","success":true}
# 调用 v3 test接口,不存在,向下兼容,实际调用 v2 test
curl http://127.0.0.1:8080/smilex/app/demo/v3/test
{"code":200,"msg":"v2 test","success":true}
小结
- 请求正确的版本地址时,会自动匹配版本的对应接口
- 请求版本大于当前版本时,会自动匹配当前版本
- 当请求版本接口不存在时,会匹配之前版本的接口,即版本继承
前言
目前多租户SaaS架构在市面上比较流行,俨然已经成为趋势,而多租户SaaS架构的核心在于让多用户环境下 使用同 一套程序,且保证用户间的数据隔离,其中数据隔离是重点。
SAAS 多租户技术组件
基于Web
前端请求中默认带上x-tenant-id,值为用户编号
Security 层
基于 Shiro
获取登陆用户的租户ID,检验是否有权限访问该租户
基于 Spring Security
获取登陆用户的租户ID,检验是否有权限访问该租户
分布式下认证中心
DataBase层
基于数据库的实现方式,可以分为三类:
- 独立数据库
- 同一数据库下,不同数据表
- 同一数据库下,同一数据表,基于tenant_id区分
一般我们设计 SaaS 架构时,会选择前两种,但不管是前两者中的哪一种,我们都有一个必须解决的问题——如何动态切换数据源
独立数据库
一个租户一个数据库,成本最高,安全最好
TODO 等后续扩展分布式应用后,再实现
优点:
- 为不同的租户提供了不同的数据库,有利于简化数据模型的设计和扩展,满足不同租户的独特需求;
- 如果出现故障,数据恢复方便。
- 不同租户之间的性能可以按需调配
缺点:
- 维护成本和维护成本较高
- 跨租户统计复杂
同数据库下,不同表
所有租户一个数据库,但一个租户一个表
TODO 等后续扩展分布式应用后,再实现
优点:
- 为安全性较高的租户提供了一定程度的数据结构隔离,但并没有完全隔离
- 一个数据库可以支持更多的租户
缺点:
- 如果故障,回复困难,可能存在牵扯不同租户的数据
- 跨租户统计复杂
同库同表,基于tenant_id
所有租户一个数据库,且同一个表。但在表中基于tenant_id区分租户。
这是共享程度最高,但隔离即便最低的模式
TODO 等后续扩展分布式应用后,再实现
优点:
- 共享程度高,但安全性低
- 维护和购置成本最低,允许每个数据库支持的租户数量最多。
缺点:
- 隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;
- 数据好迁移
- 数据备份和恢复最困难,需要逐表逐条备份和还原
贫血模型和充血模型
贫血模型(事务脚本模式)
概念
事务脚本
事务脚本的核心是过程,通过过程的调用来组织业务逻辑,每个过程处理来自表现层的单个请求。大部分业务应用都可以被看成一系列事务,从某种程度上来说,通过事务脚本处理业务,就像执行一条条Sql语句来实现数据库信息的处理。事务脚本把业务逻辑组织成单个过程,在过程中直接调用数据库,业务逻辑在服务(Service)层处理。
贫血模型
贫血模型是面向过程,基于数据库的建模——Database Modeling
通过数据抽象系统关系,即传统数据库分析设计。该种模式实际上就是一种典型的贫血模型,通过数据库映射的实体类只有对应的属性,成为了只有getter和setter方法的数据载体,而没有具体行为,相应的行为要通过Service层去实现,随着业务升级积累,会出现胖服务层和贫血的领域模型,维护起来会越发乏力。即便如此,该种模式仍是广泛应用在软件开发领域。
总之,以常见的MVC架构的 entity 为例,这种只包含数据(set/get)不包含业务逻辑的类就叫做贫血模型。贫血模型将数据与操作分离,破坏了面向数据的封装性,是一种典型的面向过程的编程。
场景
-
常见的MVC三层架构
- Dao + Entity
- Service + DTO (Data Transfer Object)
- Controller + VO (View Object)
graph TD 控制器-->服务层 服务层-->数据层
优缺点
- 优点
- 基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发
- 贫血模型要比充血模型更加有简单、易上手。
- 缺点
- 贫血模型的系统随着不断开发,会导致实体和 Service 层越来越冗余和复杂。
- 与
entity联系比较紧密的业务会被分离到 Service 层
代码实例
/**
* 账户业务对象
*/
public class AccountBO {
/**
* 账户ID
*/
private String accountId;
/**
* 账户余额
*/
private Long balance;
/**
* 是否冻结
*/
private boolean isFrozen;
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public Long getBalance() {
return balance;
}
public void setBalance(Long balance) {
this.balance = balance;
}
public boolean isFrozen() {
return isFrozen;
}
public void setFrozen(boolean isFrozen) {
this.isFrozen = isFrozen;
}
}
/**
* 转账业务服务实现
*/
@Service
public class TransferServiceImpl implements TransferService {
@Autowired
private AccountService accountService;
@Override
public boolean transfer(String fromAccountId, String toAccountId, Long amount) {
AccountBO fromAccount = accountService.getAccountById(fromAccountId);
AccountBO toAccount = accountService.getAccountById(toAccountId);
/** 检查转出账户 **/
if (fromAccount.isFrozen()) {
throw new Exception(ErrorCodeBiz.ACCOUNT_FROZEN);
}
if (fromAccount.getBalance() < amount) {
throw new Exception(ErrorCodeBiz.INSUFFICIENT_BALANCE);
}
fromAccount.setBalance(fromAccount.getBalance() - amount);
/** 检查转入账户 **/
if (toAccount.isFrozen()) {
throw new Exception(ErrorCodeBiz.ACCOUNT_FROZEN);
}
toAccount.setBalance(toAccount.getBalance() + amount);
/** 更新数据库 **/
accountService.updateAccount(fromAccount);
accountService.updateAccount(toAccount);
return Boolean.TRUE;
}
}
充血模型(领域模型模式)
概念
领域驱动设计
领域驱动设计(Domain Driven Design,简称 DDD)主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互,注意:做好领域驱动设计的关键是对业务的熟悉程度,如果对业务不熟悉即使再熟悉领域驱动设计的概念也无法很好的设计出合理的领域设计。
领域模型模式
领域模型的特点也比较明显, 属于面向对象设计,领域模型具备自己的属性行为状态,并与现实世界的业务对象相映射。各类具备明确的职责划分,领域对象元素之间通过聚合和引用等关系配合解决实际业务应用和规则。可复用,可维护,易扩展,可以采用合适的设计模型进行详细设计。缺点是相对复杂,要求设计人员有良好的抽象能力。
充血模型
充血模型是面向对象的建模——Object Modeling
在贫血模型中,数据和业务分割在实体和service层中。充血模型与之相反,数据和对应的实体业务封装到同一个类中。因此,这充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
通过面向对象方式抽象系统关系,也就是面向对象设计。毫无疑问,在面向对象编程环境中,面向对象无疑是领域建模最佳方式。通过面向对象构建的领域模型,因为有类的继承、封装、多态等特性显得生动许多,不仅包含自身属性状态,还包括有方法行为等,即充血的领域模型。一些Service层的行为凝练为领域服务,Service层则变薄了,领域模型则丰富了行为。
场景
- 包含各种利息计算模型、还款模型等复杂业务的金融系统。
graph TD
用户接口层-->服务层
用户接口层-->领域层
用户接口层-->基础设施层
服务层-->领域层
服务层-->基础设施层
基础设施层-->领域层
依赖倒置原则(DIP):
- 高层模块不依赖于低层模块,两者都依赖于抽象;
- 抽象不应该依赖于细节,细节应依赖抽象
-
分层作用
分层 描述 用户接口层 用户界面层,或者表现层,负责向用户显示解释用户命令 服务层 定义软件要完成的任务,并且指挥协调领域对象进行不同的操作。该层不包含业务领域知识。 领域层 或称为模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手 基础设施层 主要有2方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现;
优缺点
-
优点
- 基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。
- 面向对象,将对象关联的逻辑放入实体中,而不是放入Service层
-
缺点
- 对于开发团队要求更高,对领域模型需要熟练划分,否则会造成更大的负担。
- 如何划分业务逻辑呢?按照Rod johnson提出远测是 “case by case”,可重用度高的,和实体密切关联的放在实体中,反之,放在service中。简单来说,放在实体中的应该只和这个实体有关,而不应该涉及多个实体。当逻辑放在实体中,这个实体应该仍然独立在持久层之外,可以脱离持久层框架进行单元测试,这个实体才是一个完整的,不依赖外部实体的领域对象,这个情况下,这个逻辑才是实体逻辑。
代码示例
/**
* 账户业务对象
*/
public class AccountBO {
/**
* 账户ID
*/
private String accountId;
/**
* 账户余额
*/
private Long balance;
/**
* 是否冻结
*/
private boolean isFrozen;
/**
* 出借策略
*/
private DebitPolicy debitPolicy;
/**
* 入账策略
*/
private CreditPolicy creditPolicy;
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public Long getBalance() {
return balance;
}
public void setBalance(Long balance) {
this.balance = balance;
}
public boolean isFrozen() {
return isFrozen;
}
public void setFrozen(boolean isFrozen) {
this.isFrozen = isFrozen;
}
/**
* 出借方法
*
* @param amount 金额
*/
public void debit(Long amount) {
debitPolicy.preDebit(this, amount);
this.balance -= amount;
debitPolicy.afterDebit(this, amount);
}
/**
* 转入方法
*
* @param amount 金额
*/
public void credit(Long amount) {
creditPolicy.preCredit(this, amount);
this.balance += amount;
creditPolicy.afterCredit(this, amount);
}
/**
* BO和DO转换必须加set方法这是一种权衡
*/
public DebitPolicy getDebitPolicy() {
return debitPolicy;
}
public void setDebitPolicy(DebitPolicy debitPolicy) {
this.debitPolicy = debitPolicy;
}
public CreditPolicy getCreditPolicy() {
return creditPolicy;
}
public void setCreditPolicy(CreditPolicy creditPolicy) {
this.creditPolicy = creditPolicy;
}
}
/**
* 入账策略实现
*/
@Service
public class CreditPolicyImpl implements CreditPolicy {
@Override
public void preCredit(AccountBO account, Long amount) {
if (account.isFrozen()) {
throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
}
}
@Override
public void afterCredit(AccountBO account, Long amount) {
System.out.println("afterCredit");
}
}
/**
* 出借策略实现
*/
@Service
public class DebitPolicyImpl implements DebitPolicy {
@Override
public void preDebit(AccountBO account, Long amount) {
if (account.isFrozen()) {
throw new MyBizException(ErrorCodeBiz.ACCOUNT_FROZEN);
}
if (account.getBalance() < amount) {
throw new MyBizException(ErrorCodeBiz.INSUFFICIENT_BALANCE);
}
}
@Override
public void afterDebit(AccountBO account, Long amount) {
System.out.println("afterDebit");
}
}
/**
* 转账业务服务实现
*/
@Service
public class TransferServiceImpl implements TransferService {
@Resource
private AccountService accountService;
@Resource
private CreditPolicy creditPolicy;
@Resource
private DebitPolicy debitPolicy;
@Override
public boolean transfer(String fromAccountId, String toAccountId, Long amount) {
AccountBO fromAccount = accountService.getAccountById(fromAccountId);
AccountBO toAccount = accountService.getAccountById(toAccountId);
fromAccount.setDebitPolicy(debitPolicy);
toAccount.setCreditPolicy(creditPolicy);
fromAccount.debit(amount);
toAccount.credit(amount);
accountService.updateAccount(fromAccount);
accountService.updateAccount(toAccount);
return Boolean.TRUE;
}
}
总结
基于贫血模型的传统开发架构MVC,只是将 entity 作为数据载体,重 service 层业务代码;而基于充血模型的DDD开发模式,将高复用和实体相关的业务逻辑放到实体里面,不同实体聚合形成领域层,不同领域聚合形成应用层。
充血模型下:
- Service 层主要功能:
- 负责与持久层交流。为了保证领域实体的独立性,不与其它层的的耦合,一般都会以交由Service处理。
- 负责与跨领域实体的业务聚合功能。比如转账会涉及两个钱包的操作,所以会在service层调用。
- 负责一些非功能性与三方系统的交互工作。比如幂等,发邮件,记录日志等。
参考
前言
软件架构的演进

**第一阶段是单机架构:**采用面向过程的设计方法,系统包括客户端 UI 层和数据库两层,采用 C/S 架构模式,整个系统围绕数据库驱动设计和开发,并且总是从设计数据库和字段开始。
**第二阶段是集中式架构:**采用面向对象的设计方法,系统包括业务接入层、业务逻辑层和数据库层,采用经典的三层架构,也有部分应用采用传统的 SOA 架构。这种架构容易使系统变得臃肿,可扩展性和弹性伸缩性差。
**第三阶段是分布式微服务架构:**随着微服务架构理念的提出,集中式架构正向分布式微服务架构演进。微服务架构可以很好地实现应用之间的解耦,解决单体应用扩展性和弹性伸缩能力不足的问题。
**微服务拆分困境产生的根本原因就是不知道业务或者微服务的边界到底在什么地方。**确定了业务边界和应用边界,这个困境就迎刃而解了。
微服务设计和DDD
为什么DDD适合微服务
2004 年埃里克·埃文斯(Eric Evans)发表了《领域驱动设计》(Domain-Driven Design–Tackling Complexity in the Heart of Software)这本书,从此领域驱动设计(Domain-Driven Design,简称 DDD)诞生。DDD 核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。
DDD 是一种处理高度复杂领域的设计思想,它试图分离技术实现的复杂性,并围绕业务概 念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演进的问题。DDD 不是架构,而是一种架构设计方法论,它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现架构演进。
DDD包括战略设计和战术设计两部分

我们可以用三步来划定领域模型和微服务的边界。
- 在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
- 根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。
- 根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。
有了这两层边界,微服务的设计就不是什么难事了。
DDD 与微服务的关系
DDD 是一种架构设计方法,微服务是一种架构风格,两者从本质上都是为了追求高响应力,而从业务视角去分离应用系统建设复杂度的手段。两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构。
DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。
微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。
小结
DDD 不仅可以用于微服务设计,还可以很好地应用于企业中台的设计。
总体来说,DDD 可以给你带来以下收获:
- DDD 是一套完整而系统的设计方法,它能带给你从战略设计到战术设计的标准设计过程,使得你的设计思路能够更加清晰,设计过程更加规范。
- DDD 善于处理与领域相关的拥有高复杂度业务的产品开发,通过它可以建立一个核心而稳定的领域模型,有利于领域知识的传递与传承。
- DDD 强调团队与领域专家的合作,能够帮助你的团队建立一个沟通良好的氛围,构建一致的架构体系。
- DDD 的设计思想、原则与模式有助于提高你的架构设计能力。
- 无论是在新项目中设计微服务,还是将系统从单体架构演进到微服务,都可以遵循DDD 的架构原则。
- DDD 不仅适用于微服务,也适用于传统的单体应用。
思考
你的公司是否在实施微服务架构,你在微服务设计过程中面临的最大问题是什么?
领域、子域、核心域、通用域和支撑域
领域和子域
汉语词典中对领域的解释:“领域是从事一种专门活动或事业的范围、部类或部门。”百度百科对领域的解释:“领域具体指一种特定的范围或区域”。两个解释有一个共同点——范围。
在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。
我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
我们知道细胞核、线粒体、细胞膜等物质共同构成细胞,这些物质一起协作让细胞具有这类细胞特定的生物功能。在这里你可以把细胞理解为 DDD 的聚合,细胞内的这些物质就可以理解为聚合里面的聚合根、实体以及值对象等,在聚合内这些实体一起协作完成特定的业务功能。这个过程类似 DDD 设计时,确定微服务内功能要素和边界的过程。
领域建模和微服务建设的过程和方法基本类似,其核心思想就是将问题域逐步分解,降低业务理解和系统实现的复杂度。
核心域、通用域和支撑域
- 决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。
- 没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。
- 还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。
举例来说的话,通用域则是你需要用到的通用系统,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。而支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等系统。
小结
领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小微服务需要解决的问题域,构建合适的领域模型,而领域模型映射成系统就是微服务了。
核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。
思考
请结合你所在公司的业务情况,尝试给业务做一个领域拆分,看看哪些子域是核心域,哪些子域是通用域和支撑域?
限界上下文:定义领域边界的利器
在 DDD 领域建模和系统建设过程中,有很多的参与者,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等。对同样的领域知识,不同的参与角色可能会有不同的理解,那大家交流起来就会有障碍,怎么办呢?因此,在 DDD 中就出现了“通用语言”和“限界上下文”这两个重要的概念。
这两者相辅相成,通用语言定义上下文含义,限界上下文则定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。
思考两个问题:
- 为什么要提出限界上下文的概念(也就是说除了解决交流障碍这个广义的原因,还有更具体的吗)?
- 限界上下文在微服务设计中的作用和意义是什么?
什么是通用语言
怎么理解通用语言这个概念呢?在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。

- 在事件风暴的过程中,领域专家会和设计、开发人员一起建立领域模型,在领域建模的过程中会形成通用的业务术语和用户故事。事件风暴也是一个项目团队统一语言的过程。
- 通过用户故事分析会形成一个个的领域对象,这些领域对象对应领域模型的业务对象,每一个业务对象和领域对象都有通用的名词术语,并且一一映射。
- 微服务代码模型来源于领域模型,每个代码模型的代码对象跟领域对象一一对应。
设计过程中我们可以用一些表格,来记录事件风暴和微服务设计过程中产生的领域对象及其属性。
下面是一个微服务设计实例的部分数据,表格中的这些名词术语就是项目团队在事件风暴过程中达成一致、可用于团队内部交流的通用语言。在这个表格里面我们可以看到,DDD 分析过程中所有的领域对象以及它们的属性都被记录下来了,除了 DDD 的领域对象,我们还记录了在微服务设计过程中领域对象所对应的代码对象,并将它们一一映射。

DDD 分析和设计过程中的每一个环节都需要保证限界上下文内术语的统一,在代码模型设计的时侯就要建立领域对象和代码对象的一一映射,从而保证业务模型和代码模型的一致,实现业务语言与代码语言的统一。
什么是限界上下文
DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。
限界上下文的定义就是:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。
正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。看到这,我想你应该非常清楚了,领域边界就是通过限界上下文来定义的。
限界上下文和微服务的关系
保险领域还是很复杂的,在这里我用一个简化的保险模型来说明下限界上下文和微服务的关系。

首先,领域可以拆分为多个子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问题拆分为小问题的过程。在这个图里面保险领域被拆分为:投保、支付、保单管理和理赔四个子域
子域还可根据需要进一步拆分为子子域,比如,支付子域可继续拆分为收款和付款子子域。拆到一定程度后,有些子子域的领域边界就可能变成限界上下文的边界了。
子域可能会包含多个限界上下文,如理赔子域就包括报案、查勘和定损等多个限界上下文(限界上下文与理赔的子子域领域边界重合)。也有可能子域本身的边界就是限界上下文边界,如投保子域。
每个领域模型都有它对应的限界上下文,团队在限界上下文内用通用语言交流。领域内所有限界上下文的领域模型构成整个领域的领域模型。
理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。
可以说,限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
小结
通用语言确定了项目团队内部交流的统一语言,而这个语言所在的语义环境则是由限界上下文来限定的,以确保语义的唯一性。
领域专家、架构师和开发人员的主要工作就是通过事件风暴来划分限界上下文。限界上下文确定了微服务的设计和拆分方向,是微服务设计和拆分的主要依据。如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
我们只有理解了限界上下文的真正涵义以及它在微服务设计中的作用,才能真正发挥 DDD 的价值,这是基础也是前提。
思考
- 你能找一找自己工作中的通用语言和限界上下文吗?
- 为什么要提出限界上下文的概念(也就是说除了解决交流障碍这个广义的原因,还有更具体的吗)?
- 限界上下文在微服务设计中的作用和意义是什么?
实体和值对象:从领域模型的基础单元看系统设计
这两个概念都是领域模型中的领域对象。它们在领域模型中起什么作用,战术设计时如何将它们映射到代码和数据模型中去?就是我们这一讲重点要关注的问题。
实体
在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。
实体的业务形态
在 DDD 不同的设计过程中,实体的形态是不同的。在战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。
实体和值对象是组成领域模型的基础单元。
实体的代码形态
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
实体的运行形态
实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。
实体的数据库形态
与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。
值对象
我们先看一下《实现领域驱动设计》一书中对值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。
值对象描述了领域中的一件东西,这个东西是不可变的,它将不同的相关属性组合成了一个概念整体。当度量和描述改变时,可以用另外一个值对象予以替换。
简单来说,值对象本质上就是一个集。那这个集合里面有什么呢?若干个用于描述目的、具有整体概念和不可修改的属性。那这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。

人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎了对不对?现在,我们可以将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了。
值对象的业务形态
值对象是 DDD 领域模型中的一个基础对象,它跟实体一样都来源于事件风暴所构建的领域模型,都包含了若干个属性,它与实体一起构成聚合。
本质上,实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。
在值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的数据类微服务,比如数据字典。
值对象的代码形态
值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为 Class 类,Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。
我们看一下下面这段代码,person 这个实体有若干个单一属性的值对象,比如 Id、name等属性;同时它也包含多个属性的值对象,比如地址 address。

值对象的运行形态
值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是属性嵌入的方式和序列化大对象的方式。
如果你对这两种方式不够了解,可以看看下面的例子。
-
以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。

-
以序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象 Json 串后,嵌入人员实体中。

值对象的数据库形态
DDD 引入值对象是希望实现从“数据建模为中心”向“领域建模为中心”转变,减少数据库表的数量和表与表之间复杂的依赖关系,尽可能地简化数据库设计,提升数据库性能。
传统的数据建模大多是根据数据库范式设计的,每一个数据库表对应一个实体,每一个实体的属性值用单独的一列来存储,一个实体主表会对应 N 个实体从表。而值对象在数据库持久化方面简化了设计,它的数据库设计大多采用非数据库范式,值对象的属性值和实体对象的属性值保存在同一个数据库实体表中。
举个例子,还是基于上述人员和地址那个场景,实体和数据模型设计通常有两种解决方案:
- 把地址值对象的所有属性都放到人员实体表中,创建人员实体,创建人员数据表;
- 创建人员和地址两个实体,同时创建人员和地址两张表。
那到底应该怎样设计,才能让业务含义清楚,同时又不让数据库变得复杂呢?
我们可以综合这两个方案的优势,扬长避短。在领域建模时,我们可以把地址作为值对象,人员作为实体,这样就可以保留地址的业务涵义和概念完整性。而在数据建模时,我们可以将地址的属性值嵌入人员实体数据库表中,只创建人员数据库表。这样既可以兼顾业务含义和表达,又不增加数据库的复杂度。
在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
有 DDD 专家认为,要想发挥对象的威力,就需要优先做领域建模,弱化数据库的作用,只把数据库作为一个保存数据的仓库即可。即使违反数据库设计原则,也不用大惊小怪,只要业务能够顺利运行,就没什么关系。
值对象的优势和局限
值对象是一把双刃剑,它的优势是可以简化数据库设计,提升数据库性能。但如果值对象使用不当,它的优势就会很快变成劣势。
值对象采用序列化大对象的方法简化了数据库设计,减少了实体表的数量,可以简单、清晰地表达业务概念。这种设计方式虽然降低了数据库设计的复杂度,但却无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。
值对象采用属性嵌入的方法提升了数据库的性能,但如果实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务涵义,操作起来也不方便。
所以,你可以对照着以上这些优劣势,结合你的业务场景,好好想一想了。那如果在你的业务场景中,值对象的这些劣势都可以避免掉,那就请放心大胆地使用值对象吧。
实体和值对象的关系
实体和值对象是微服务底层的最基础的对象,一起实现实体最基本的核心领域逻辑。
值对象和实体在某些场景下可以互换,很多 DDD 专家在这些场景下,其实也很难判断到底将领域对象设计成实体还是值对象?可以说,值对象在某些场景下有很好的价值,但是并不是所有的场景都适合值对象。你需要根据团队的设计和开发习惯,以及上面的优势和局限分析,选择最适合的方法。
DDD 提倡从领域模型设计出发,而不是先设计数据模型。
小结
这个过程是从业务模型向系统模型落地的过程,比较复杂,很考验你的设计能力,很多时候我们都要结合自己的业务场景,选择合适的方法来进行微服务设计。强调一点,我们不避讳传统的设计方法,毕竟适合自己的才是最好的。
思考
请用自己的话总结下,实体和值对象的主要区别是什么?
- 实体是有唯一标识,而值对象没有。
- 实体是具有业务逻辑和属性及行为的对象,而值对象是实体下的属性集合,不具备行为。
- 实体消亡,则值对象随之消亡。
聚合和聚合根:怎样设计聚合
聚合
在 DDD 中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。
聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。
聚合根
聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。
传统数据模型中的每一个实体都是对等的,如果任由实体进行无控制地调用和数据修改,很可能会导致实体之间数据逻辑的不一致。而如果采用锁的方式则会增加软件的复杂度,也会降低系统的性能。
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
- 作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
- 作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
- 在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体
怎样设计聚合
DDD 领域建模通常采用事件风暴,它通常采用用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。
以保险的投保业务场景为例,看一下聚合的构建过程主要都包括哪些步骤。

-
采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。
-
**从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。**判断一个实体是否是聚合根,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一 ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。
-
根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出 1 个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。
-
**在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。**这里我需要说明一下:投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体.
-
多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。
聚合的设计原则
”适合自己的才是最好的。”在系统设计过程时,你一定要考虑项目的具体情况,如果面临使用的便利性、高性能要求、技术能力缺失和全局事务管理等影响因素,这些原则也并不是不能突破的,总之一切以解决实际问题为出发点。
- 在一致性边界内建模真正的不变条件。聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。
- **设计小聚合。**如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。
- 通过唯一标识引用其它聚合。 聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清,也会增加聚合之间的耦合度。
- **在边界之外使用最终一致性。 ** 聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦(相关内容我会在领域事件部分详解)
- **通过应用层实现跨聚合的服务调用。**为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。
小结
-
聚合的特点:高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位,但我不建议你对微服务过度拆分。但在对性能有极致要求的场景中,聚合可以独立作为一个微服务,以满足版本的高频发布和极致的弹性伸缩能力。
**一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。**有了这个逻辑边界,在微服务架构演进时就可以以聚合为单位进行拆分和组合了,微服务的架构演进也就不再是一件难事了。
-
聚合根的特点: 聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。
-
实体的特点: 有 ID 标识,通过 ID 判断相等性,ID 在聚合内唯一即可。状态可变,它依附于聚合根,其生命周期由聚合根管理。实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。
-
值对象的特点: 无 ID,不可变,无生命周期,用完即扔。值对象之间通过属性值判断相等性。它的核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。值对象尽量只引用值对象。
-
在不少的数据统计和计算场景中,有很多实体之间相互独立,只参与计算和统计分析,但是这类场景中业务内聚性又很高,你找不出管理这些实体的聚合根。这种业务模型是非典型领域模型。虽然有些方面(比如聚合根)不符合DDD的一些原则,但是我们也可以按照DDD方法来完成设计。
思考
请你结合公司的某个业务场景,试试能分析出哪些聚合?
DDD-进阶
https://www.jerrycoding.com/article/cxdn_down/
下载网站: http://10086.jerrycoding.com/
1、打开链接后,界面如下,输入需要下载的链接
2、点击提取资源后,稍等10秒钟左右 手动刷新界面即可获取下载地址

gitlab和服务同一台机器
无Docker服务
.gitlab-ci.yml
stages:
- test_cloud_build
- test_cloud_stop
- test_cloud_start
develop_build_cloud:
stage: test_cloud_build
only:
# 只有在tag 为develop开头生效
- /^develop-[[:digit:]].*/
tags:
# CI 进程名称。gitlab需要自己开启和安装
- ci-runner
script:
- JAVA_HOME=/usr/local/java/jdk1.8.0_261
- CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
- GRADLE_HOME=/usr/local/gradle-5.2.1
- PATH=$PATH:$HOME/.local/bin:$HOME/bin:$GRADLE_HOME/bin
- export JAVA_HOME
- export GRADLE_HOME
- export CLASSPATH
- export PATH
- gradle clean
- module1=cloud-comsumer
- package_name=cloud-test.jar
- /usr/local/gradle-5.2.1/bin/gradle bootJar -p ${module1}
- echo "$CI_COMMIT_REF_NAME"
- echo "cp ${module1}/build/libs/*.jar /home/ubuntu/serve/cloud/${package_name}"
- sudo cp ${module1}/build/libs/*.jar /home/ubuntu/serve/cloud/${package_name}
develop_stop_cloud:
stage: test_cloud_stop
only:
- /^develop-[[:digit:]].*/
tags:
- ci-runner
script:
- sudo systemctl stop cloud-test
develop_start_cloud:
stage: test_cloud_start
only:
- /^develop-[[:digit:]].*/
tags:
- ci-runner
script:
- sudo systemctl start cloud-test
service
- 进入system目录下,并编辑test.service文件
# 到system目录下
cd /etc/systemd/system
# 编辑test.service
vim test.service
- 文件内容如下
[Unit]
Description=cloud-test-service server daemon
After=network-online.target
Wants=network-online.target
[Service]
User=root
Group=root
ExecStart=/bin/sh -c 'exec java -jar /home/ubuntu/serve/cloud/cloud-test.jar --spring.profiles.active=test --logging.file=/var/log/cloud-test.log'
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failur
RestartSec=60s
[Install]
- 保存文件并注册服务。更多指令可查看systemctl.md文件。
# 重新加载所有被修改过的服务配置,否则配置不会生效
sudo systemctl daemon-reload
# 系统开机时自动启动指定unit,前提是配置文件中有相关配置
sudo systemctl enable test.service
# 启动服务
sudo systemctl start test.service
# 停止服务
sudo systemctl stop test.service
# 重启服务
sudo systemctl restart test.service
# 重新加载配置
sudo systemctl reload test.service
常用指令
# 重新加载所有被修改过的服务配置,否则配置不会生效
sudo systemctl daemon-reload
# 系统开机时自动启动指定unit,前提是配置文件中有相关配置
sudo systemctl enable test.service
# 启动服务
sudo systemctl start test.service
# 停止服务
sudo systemctl stop test.service
# 重启服务
sudo systemctl restart test.service
# 重新加载配置
sudo systemctl reload test.service
# 如果Unit无法正常停止,使用强杀
sudo systemctl kill test.service
# 显示Unit状态
sudo systemctl status test.service
# 显示Unit是否在运行
sudo systemctl is-active test.service
# 显示Unit是否开机运行
sudo systemctl is-enabled test.service
# 显示unit列表
sudo systemctl list-units
sudo systemctl list-units --all # 列出所有Unit,包括缺失配置文件或启动失败的
sudo systemctl list-units --all --state=inactive # 列出所有没有运行的Unit
永久修改主机名
在Ubuntu系统中永久修改主机名也比较简单。主机名存放在**/etc/hostname**文件中,修改主机名时,编辑hostname文件,在文件中输入新的主机名并保存该文件即可。重启系统后,参照上面介绍的快速查看主机名的办法来确认主机名有没有修改成功。
值的指出的是,在其它Linux发行版中,并非都存在/etc/hostname文件。如Fedora发行版将主机名存放在/etc/sysconfig/network文件中。所以,修改主机名时应注意区分是哪种Linux发行版。
第一步: 修改/etc/hostname
/etc/hostname中存放的是主机名,hostname文件的一个例子:
vim /etc/hostname
内容如下:

第二步:修改/etc/hosts配置文件(可选)
/etc/hosts存放的是域名与ip的对应关系,域名与主机名没有任何关系,你可以为任何一个IP指定任意一个名字。
vim /etc/hosts
内容如下:

第三步:重启系统
sudo reboot
ubuntu根目录下的文件介绍
-
/bin/。用于存储二进制可执行命令文件
-
/sbin/。许多系统命令的存储位置,/usr/sbin/中也包括了许多命令。
-
/root/。超级用户主目录
-
/home/。普通用户的默认目录。在该目录下,每个用户拥有一个以用户名命名的文件夹。
-
/boot/。 存放Ubuntu内核和系统启动文件。
- /boot/grub/。 Grub引导器相关的文件
指令说明
- dd。删除行
- u。撤销
- 查找字符串
- 命令行模式。输入:/<字符串>。
- n。查找下一个匹配的字符串
- N。查找上一个匹配的字符串
常用指令
- 查找匹配的内容
# 查找 heitan-pro-admin.out 中包含 SysLogAspect 的行。-b查看非空行号
cat -b heitan-pro-admin.out | grep "c.a.h.mall.admin.aspect.SysLogAspect"
# 查找行
grep 'c.a.h.mall.admin.aspect.SysLogAspect' -nR heitan-pro-admin.out
# 查看文件最新的200条
tail -fn 200 nohup.out
# 查看文件从第280100行开始,显示1000行。即280100-281100
cat -b heitan-pro-admin.out | tail -n +280100 | head -n 1000
- 查看进程
# 查看所有进程
ps -ef
# 过滤包含java的进程
ps -ef |grep java
- 杀死进程
kill -9 [PID]
- 查找文件中间内容
cat input_file | head -n 20958238 | tail -n +20957300
- Jar启动
# 后台启动jar
nohup java -Duser.timezone=GMT+08 -jar xxx.jar &
# 后台启动jar,并指定输出文件 xxx.txt
nohup java -jar xxx.jar >xxx.txt &
Redis
- 通配符删除key
redis-cli keys "user*" | xargs redis-cli del
window
- 端口占用
#查看所有端口
netstat -ano
# 查看指定端口,最后一列为PID
netstat -ano|findstr "8081"
# 查看应用信息,PID,
tasklist|findstr "8808"
# 结束应用,PID
taskkill /pid 8008
常用工具
git
centos
- 首先,确保你的系统已经安装了 EPEL(Extra Packages for Enterprise Linux)仓库。如果没有,你可以使用以下命令安装:
sudo yum install epel-release
- 接下来,使用以下命令安装 Git 客户端:
sudo yum install git
配置
环境配置
centos
-
临时设置
export variable=value; -
设置用户
vim ~/.bash_profile # 在文件中数据输入 export variable=value; # 退出文件,执行命令,使文件生效 source ~/.bash_profile
安装JDK
JDK通常是从 Oracle官网下载, 打开页面翻到底部,找 Java for Developers 或 者 Developers , 进入 Java 相应的页面 或者 Java SE 相应的页面, 查找 Download, 接受许可协议,下载对应的x64版本即可。
注意:从Oracle官方安装JDK需要注册和登录Oracle账号。现在流行将下载链接放 到页面底部,很多工具都这样。当前推荐下载 JDK8。 今后JDK11将会成为主流 版本,因为Java11是LTS长期支持版本(2019年10月15日发布的JDK11.0.5已经 被官方标记为LTS版本),但可能还需要一些时间才会普及,而且JDK11的文件目 录结构与之前不同, 很多工具可能不兼容其JDK文件的目录结构。
有的操作系统提供了自动安装工具,直接使用也可以,比如 yum, brew, apt 等等。例 如在MacBook上,执行:
brew cask install java8
而使用如下命令,会默认安装最新的JDK13:
brew cask install java
设置环境变量
如果找不到命令,需要设置环境变量: ==JAVA_HOME== 和 ==PATH== 。
JAVA_HOME 环境变量表示JDK的安装目录,通过修改 JAVA_HOME ,可以快速切换JDK版本。很多工具依赖此环境变量。
另外, 建议不要设置 CLASS_PATH 环境变量,新手没必要设置,容易造成一些困 扰
Windows系统, 系统属性 - 高级 - 设置系统环境变量。 如果没权限也可以只设置用户 环境变量。
Linux和MacOSX系统, 需要配置脚本。 例如:
cat ~/.bash_profile
# JAVA ENV
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home
export PATH=$PATH:$JAVA_HOME/bin
让环境配置立即生效:
source ~/.bash_profile
查看环境变量:
echo $PATH
echo $JAVA_HOME
一般来说, .bash_profile 之类的脚本只用于设置环境变量。 不设置随机器自启动的程序。
如果不知道自动安装/别人安装的JDK在哪个目录怎么办?
查找的方式很多,比如,可以使用 ==which , whereis , ls ‐l==跟踪软连接, 或者 ==find --命令全局查找(可能需要sudo权限), 例如:
jps ‐v
whereis javac
ls ‐l /usr/bin/javac
find / ‐name javac
找到满足 $JAVA_HOME/bin/javac 的路径即可
Windows系统,安装在哪就是哪,默认在 C:\Program Files (x86)\Java 下。通 过任务管理器也可以查看某个程序的路径,注意 JAVA_HOME 不可能是 C:\Windows\System32 目录。
然后我们就可以在JDK安装路径下看到很多JVM工具
验证JDK安装完成
安装完成后,Java环境一般来说就可以使用了。 验证的脚本命令为:
java ‐version
可以看到输出类似于以下内容,既证明成功完成安装:
java version "1.8.0_65" Java(TM) SE Runtime Environment (build 1.8.0_65b17) Java HotSpot(TM) 64Bit Server VM (build 25.65b01, mixed mode)
然后我们就可以写个最简单的java程序了,新建一个文本文件,输入以下内容:
public class Hello {
public static void main(String[] args){
System.out.println("Hello, JVM!");
}
}
然后把文件名改成 Hello.java ,在命令行下执行:
javac Hello.java
然后使用如下命令运行它:
java Hello
说明
支持系统
- 安装方式
- 支持 Ubuntu 18.04
- 支持 Ubuntu 16.04
- 支持 Centos 7
安装方式1
-
下载jdk,二进制压缩包.tar.gz结尾的。
-
上传jdk到服务器上,用户目录下。例如/home/ubuntu。
-
解压tar文件
cd /usr/local
sudo mkdir java
cd java
sudo tar -zxvf /home/ubuntu/jdk-8u261-linux-x64.tar.gz -C /usr/local/java
- 配置环境变量。用VIM来打开环境变量配置文件
vim /etc/profile
- 在文件末尾添加如下代码
export JAVA_HOME=/usr/local/java/jdk1.8.0_261
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
- 保存文件, 使该文件生效
source /etc/profile
- 验证java安装是否成功
java -version
踩坑记录
sudo: java: command not found
当我们需要用 sudo 来执行java命令或者 .sh文件时,会提示指令未发现。明明我们使用在环境变量里配置的,应该怎么处理呢?
实际应该修改 /etc/sudoers 文件
- vim编辑/etc/sudoers文件
vim /etc/sudoers
- 在 Defaults secure_path 的后面增加 :/usr/local/java/jdk1.8.0_261/bin
# 之前
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
# 之后
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/usr/local/java/jdk1.8.0_261/bin"
- 再试验一下,输出版本即可。
sudo java -version
msyql8.0修改密码
- 修改配置文件:添加 skip-grant-tables
cd /etc/mysql/mysql.conf.d/
sudo vim mysqld.cnf
- 重启mysql服务。
sudo service mysql restart
- 登录mysql, 此时不需要密码
mysql -u root -p
- 进入mysql数据库
use mysql
- 将authentication_string 置空
update user set authentication_string='' where user='root';
- 设置新密码
alter user 'root'@'localhost' identified by 'newpassword'
- 异常 ERROR 1290 (HY000): The MySQL server is running with the --skip-grant-tables option so it cannot execute this statement 处理
FLUSH PRIVILEGES;
- 设置新密码
alter user 'root'@'localhost' identified by 'newpassword';
- 异常 ERROR 1524 (HY000): Plugin ‘auth_socket’ is not loaded
# 查看plugin
select user,plugin from mysql.user
# 设置为5.x版本密码认证方式
update user set plugin=‘mysql_native_password’ where user=‘root’;
FLUSH PRIVILEGES;
10 . 设置新密码
alter user 'root'@'localhost' identified by 'newpassword';
FLUSH PRIVILEGES;
-
将第一步的
skip-grant-tables注释掉; -
重启mysql服务
说明
支持系统:
- ubuntu 18.04
方法一
sudo apt-get install mysql-server
sudo apt-get install mysql-client
sudo apt-get install libmysqlclient-dev
方法二(win)
这里使用版本:mysql-5.7.44-winx64
-
登录官网下载免安装版本。
-
解压并进入软件根目录。
-
新建
my.ini文件[mysqld] # 设置3306端口 port=3306 # 设置mysql的安装目录,一定要与上面的安装路径保持一致 basedir=D:\dev-tools\mysql-5.7.44-winx64 # 设置mysql数据库的数据的存放目录 datadir=D:\dev-tools\mysql-5.7.44-winx64\data # 允许最大连接数 max_connections=200 # 允许连接失败的次数。 max_connect_errors=10 # 服务端使用的字符集默认为utf8mb4 character-set-server=utf8mb4 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB [mysql] # 设置mysql客户端默认字符集 default-character-set=utf8mb4 [client] # 设置mysql客户端连接服务端时默认使用的端口 可以根据实际情况进行修改 port=3306 default-character-set=utf8mb4 -
进入 \bin 目录,执行以下指令。
- --defaults-file。指定默认文件
mysqld --defaults-file=C:\Softwares\mysql-5.7.44-winx64\my.ini --initialize --console ##使用管理员的方式打开输出结果:
2023-05-05T07:22:04.819909Z 0 [Warning] [MY-010918] [Server] 'default_authentication_plugin' is deprecated and will be removed in a future release. Please use authentication_policy instead. 2023-05-05T07:22:04.819935Z 0 [System] [MY-013169] [Server] D:\dev-tools\mysql-5.7.44-winx64\bin\mysqld.exe (mysqld 8.0.33) initializing of server in progress as process 920 2023-05-05T07:22:04.840333Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. 2023-05-05T07:22:06.123640Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. 2023-05-05T07:22:07.586539Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: b*qMh0L>2-;C最后面的就是临时密码。这里看出是:
b*qMh0L>2-;C -
执行数据库初始化。提示 Service successfully installed 表示成功。
mysqld --install mysql-5.7.44 # mysql-5.7.44为服务名称,默认为Mysql
- 开启/关闭 MySql
net start mysql-5.7.44
net stop mysql-5.7.44
- 登录mysql 修改密码
#mysql -u root -p初始化的密码 #登录mysql
mysql -u root -p'b*qMh0L>2-;C'
#ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码'; # 修改密码
ALTER USER 'root'@'localhost' IDENTIFIED BY '123456'; # 修改密码
-
启动方式
-
加入到环境变量 Path 中启动:将安装地址\bin追加到Path最后面
D:\dev-tools\mysql-5.7.44-winx64\bin; -
自定义脚本启动
@echo off rem 设置 MySQL 用户名和密码 set MYSQL_USERNAME=root set MYSQL_PASSWORD=123456 rem 设置 MySQL 端口号,默认为 3306 set MYSQL_PORT=3306 rem 设置 MySQL 根目录路径,根据实际情况调整路径 set MYSQL_HOME=%cd% #rem 设置 SQL 初始化文件的路径,用于创建初始化库等 #set SQL_INIT_FILE=%~dp0init.sql rem 初始化 数据存储目录 "%MYSQL_HOME%\bin\mysqld" --initialize-insecure rem 启动 MySQL 服务 echo Starting MySQL server... "%MYSQL_HOME%\bin\mysqld" --port=%MYSQL_PORT% --console --user=%MYSQL_USERNAME% --bind-address=127.0.0.1 --datadir="%MYSQL_HOME%\data" --shared-memory #--init-file="%SQL_INIT_FILE%" echo if %errorlevel% neq 0 ( echo Failed to start MySQL server. exit /b %errorlevel% ) echo MySQL server started successfully and new database created. exit /b 0
-
彻底删除mysql安装记录
# 首先用以下命令查看自己的mysql有哪些依赖包
dpkg --list | grep mysql
# 先依次执行以下命令
sudo apt-get remove mysql-common
sudo apt-get autoremove --purge mysql-server-5.0 # 卸载 MySQL 5.x 使用, 非5.x版本可跳过该步骤
sudo apt-get autoremove --purge mysql-server
# 然后再用 dpkg --list | grep mysql 查看一下依赖包
# 最后用下面命令清除残留数据
dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P
# 查看从MySQL APT安装的软件列表, 执行后没有显示列表, 证明MySQL服务已完全卸载
dpkg -l | grep mysql | grep i
说明
支持系统:
- ubuntu 18.04
方法一
- 切换路径,下载安装包
cd /usr/local
mkdir mysql-8.0
cd /mysql-8.0
wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.10-1_all.deb
- 安装程序
sudo dpkg -i mysql-apt-config_0.8.10-1_all.deb
- 下载最新的软件包信息
sudo apt update
- 如果出现异常 仓库 “http://repo.mysql.com/apt/ubuntu bionic InRelease” 没有数字签名 。请看踩坑记录
- 查看升级
apt list --upgradable
- 安装mysql8.0
sudo apt install mysql-server
- 验证安装
mysql -uroot -p
- 设置远程访问权限
SHOW DATABASES;
USE mysql;
SELECT 'host' FROM user WHERE user='root'; #查看user表的host值
UPDATE user SET host = '%' WHERE user ='root'; #修改host值
flush privileges; #刷新MySQL的系统权限相关表
- 修改绑定IP
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
# 更改bind-address
bind-address = 0.0.0.0
方法二(win)
这里使用版本:mysql-8.0.33-winx64
-
登录官网下载免安装版本。
-
解压并进入软件根目录。
-
新建
my.ini文件[mysqld] # 设置3307端口 port=3307 # 设置mysql的安装目录,一定要与上面的安装路径保持一致 basedir=D:\dev-tools\mysql-8.0.33-winx64 # 设置mysql数据库的数据的存放目录 datadir=D:\dev-tools\mysql-8.0.33-winx64\data # 允许最大连接数 max_connections=200 # 允许连接失败的次数。 max_connect_errors=10 # 服务端使用的字符集默认为utf8mb4 character-set-server=utf8mb4 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB # 默认使用“mysql_native_password”插件认证 #mysql_native_password default_authentication_plugin=mysql_native_password [mysql] # 设置mysql客户端默认字符集 default-character-set=utf8mb4 [client] # 设置mysql客户端连接服务端时默认使用的端口 可以根据实际情况进行修改 port=3307 default-character-set=utf8mb4 -
进入 \bin 目录,执行以下指令。
- --defaults-file。指定默认文件
mysqld --defaults-file=C:\Softwares\mysql-8.0.12-winx64\my.ini --initialize --console ##使用管理员的方式打开输出结果:
2023-05-05T07:22:04.819909Z 0 [Warning] [MY-010918] [Server] 'default_authentication_plugin' is deprecated and will be removed in a future release. Please use authentication_policy instead. 2023-05-05T07:22:04.819935Z 0 [System] [MY-013169] [Server] D:\dev-tools\mysql-8.0.33-winx64\bin\mysqld.exe (mysqld 8.0.33) initializing of server in progress as process 920 2023-05-05T07:22:04.840333Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. 2023-05-05T07:22:06.123640Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. 2023-05-05T07:22:07.586539Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: b*qMh0L>2-;C最后面的就是临时密码。这里看出是:
b*qMh0L>2-;C -
执行数据库初始化。提示 Service successfully installed 表示成功。
mysqld --install MySQL-8.0.33 # MySQL-8.0.12为服务名称,默认为Mysql
- 开启/关闭 MySql
net start MySQL-8.0.33
net stop MySQL-8.0.33
- 登录mysql 修改密码
#mysql -u root -p初始化的密码 #登录mysql
mysql -u root -p'b*qMh0L>2-;C'
#ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码'; # 修改密码
ALTER USER 'root'@'localhost' IDENTIFIED BY '123456'; # 修改密码
-
启动方式
-
加入到环境变量 Path 中启动:将安装地址\bin追加到Path最后面
D:\dev-tools\mysql-8.0.33-winx64\bin; -
自定义脚本启动
@echo off rem 设置 MySQL 用户名和密码 set MYSQL_USERNAME=root set MYSQL_PASSWORD=123456 rem 设置 MySQL 端口号,默认为 3306 set MYSQL_PORT=3307 rem 设置 MySQL 根目录路径,根据实际情况调整路径 set MYSQL_HOME=%cd% #rem 设置 SQL 初始化文件的路径,用于创建初始化库等 #set SQL_INIT_FILE=%~dp0init.sql rem 初始化 数据存储目录 "%MYSQL_HOME%\bin\mysqld" --initialize-insecure rem 启动 MySQL 服务 echo Starting MySQL server... "%MYSQL_HOME%\bin\mysqld" --port=%MYSQL_PORT% --console --user=%MYSQL_USERNAME% --bind-address=127.0.0.1 --datadir="%MYSQL_HOME%\data" --shared-memory #--init-file="%SQL_INIT_FILE%" echo if %errorlevel% neq 0 ( echo Failed to start MySQL server. exit /b %errorlevel% ) echo MySQL server started successfully and new database created. exit /b 0
-
踩坑记录
仓库 “http://repo.mysql.com/apt/ubuntu bionic InRelease” 没有数字签名
执行 apt update时,报出异常。

- 查看签名列表
apt-key list
- 删除过期签名
sudo apt-key del dsa1024
- 重新添加新的签名。这里的key为 上面红色框中的 key
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29
关于groupby 的sqlmode配置问题
- 进入mysqld配置文件目录
cd /etc/mysql/mysql.conf.d
- 增加sql_mode
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
sql_mode=NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
启动服务
- 启动服务
cd /usr/bin
sudo mysqld restart --user=root
http访问
单机部署
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /usr/local/nginx/cert/localhost.pem; # 改为自己申请得到的 crt 文件的名称
ssl_certificate_key /usr/local/nginx/cert/localhost.key; # 改为自己申请得到的 key 文件的名称
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
location ^~ /smilex/ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8080;
}
# history route
location ^~ /admin/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
alias /usr/local/serve/frontend/smilex-admin/;
index index.html index.htm;
try_files $uri $uri/ /admin/index.html;
}
# hash route
#location /admin/ {
# if ($request_filename ~* ^.*?.(html|htm)$) {
# add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
# }
# root /usr/local/serve/frontend/smilex-admin;
# index index.html index.htm;
#}
}
腾讯云ssl
server {
#SSL 默认访问端口号为 443
listen 443 ssl;
#请填写绑定证书的域名
server_name cloud.tencent.com;
#请填写证书文件的相对路径或绝对路径
ssl_certificate cloud.tencent.com_bundle.crt;
#请填写私钥文件的相对路径或绝对路径
ssl_certificate_key cloud.tencent.com.key;
ssl_session_timeout 5m;
#请按照以下协议配置
ssl_protocols TLSv1.2 TLSv1.3;
#请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
location / {
#网站主页路径。此路径仅供参考,具体请您按照实际目录操作。
#例如,您的网站主页在 Nginx 服务器的 /etc/www 目录下,则请修改 root 后面的 html 为 /etc/www。
root html;
index index.html index.htm;
}
}
前端版本更新
server {
listen 80;
server_name api.dev.com;
client_max_body_size 10m;
# 老网页 v1.1.0 配置
location ~ ^/v110 {
alias /home/ubuntu/api.dev.com/V110;
index index.html index.htm;
}
# 新网页 v1.2.0 配置
location ~ ^/v120 {
alias /home/ubuntu/api.dev.com/V120;
index index.html index.htm;
}
}
在 nginx 配置文件语法中,location 语句可以使用正则表达式,定义 set $s $1 变量,实现了通用配置
server {
listen 80;
server_name api.dev.com;
client_max_body_size 10m;
# 配置正则 localtion
location ~ ^/pageV(.*) {
set $s $1; # 定义后缀变量
alias /home/wwwroot/api.dev.com/pageV$s;
index index.html index.htm;
}
}
反向代理转发数据
- 发送 temp.com/test/1 的会转发到 test.com/8001/1。
- 注意location 的/test/ 和proxy_pass 的斜杠
location /test/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#proxy_set_header X-Nginx-Proxy true;
# 后台接口地址
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
rewrite ^/test/(.*)$ /$1 break;
proxy_pass http://test.com:8001;
proxy_redirect default;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
}
···
常用的配置说明
服务配置
-
proxy_pass 。转发转发地址
-
rewrite 。转发
#只取路径 /test/ 后的 # 例如 /test/1 ,那么就变成 /1 rewrite ^/test/(.*)$ /$1 break;
跨域
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
http重定向https
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
日志相关
- error_log。error_log [错误日志地址] [level]。以下警报等级,从高到底,一般生产不会设置warn以上的以免影响性能。
- debug -调试消息。
- info -信息性消息。
- notice -公告。
- warn -警告。
- error -处理请求时出错。
- crit -关键问题。 需要立即采取行动。
- alert -警报。 必须立即采取行动。
- emerg - 紧急情况。 系统处于无法使用的状态
说明
proxy_pass 有无斜杠问题
-
无斜杆:
http://localhost:3000 -
有斜杆:
http://localhost:3000/ -
请求部分/test/r1,请求为 http://localhost:8080/test/r1
无斜杠
server {
listen 8080;
server_name localhost;
location /test {
proxy_pass http://localhost:3000;
}
#或者
location /test/ {
proxy_pass http://localhost:3000;
}
#结果都是 将http://localhost:8080/test/r1转发去http://localhost:3000/test/r1
}
- 无斜杆location匹配到的部分也属于请求的部分。
- location无论用
/test还是用/test/只要匹配上之后都会将整个请求部分/test/r1加到proxy_pass上。
有斜杠
server {
listen 8080;
server_name localhost;
location /test {
proxy_pass http://localhost:3000/;
}
#或者
location /test/ {
proxy_pass http://localhost:3000/;
}
#结果都是 将http://localhost:8080/r1转发去http://localhost:3000/r1
}
-
proxy_pass:
http://localhost:3000/。 -
有斜杆location匹配到的部分只用于匹配,不属于请求部分,需要在请求部分将location匹配到的部分剔除。
斜杠还有字符
server {
listen 8080;
server_name localhost;
location /test {
#结果都是 将http://localhost:8080/test/r1转发去http://localhost:3000/abc/r1
proxy_pass http://localhost:3000/abc;
}
#或者
location /test/ {
#结果都是 将http://localhost:8080/test/r1转发去http://localhost:3000/abcr1
proxy_pass http://localhost:3000/abc;
}
}
Nginx 报错 Permission denied 没有权限
- 打开
nginx.conf
cd /etc/nginx
sudo vim nginx.conf
- 修改
nginx.conf第一行
user ubuntu; # 改成自己的用户名
- 保存并重载配置
sudo nginx -s reload
nginx: [error] open() "/run/nginx.pid" failed
- 查看进程列表
ps -ef |grep nginx
- 杀死进程
sudo kill -9 pid
- 重新启动 nginx
sudo nginx
说明
支持系统
- 方法1
- ubuntu 18.04
- ubuntu 16.04
- 方法2
- ubuntu 18.04
- ubuntu 16.04
方法1
安装步骤
安装并解压nginx
- 通过wget下载nginx,通过下载地址可以查看对应的版本
wget http://nginx.org/download/nginx-1.21.6.tar.gz
- 解压源码压缩包到 /usr/local/ 目录下
tar -xvf nginx-1.21.6.tar.gz -C /usr/local/
安装nginx依赖
- nginx依赖PCRE(Perl Compatible Regular Expressions)库,所以需要先安装PCRE库。
apt-get install libpcre3 libpcre3-dev
## 待尝试
apt-get install gcc
apt-get install zlib1g zlib1g-dev
apt-get install openssl openssl-dev # ssl模块需要
apt-get install openssl # ssl模块需要
apt-get install libssl-dev # ssl模块需要
- 检查pcre是否安装成功。如果打印版本,则安装完成。
pcre-config --version
编译安装nginx
- 进入nginx的解压目录
cd /usr/local/nginx-1.21.6
- 配置编译和安装选项
- --prefix=/usr/local/nginx。安装路径
- --with-http_stub_status_module --with-http_ssl_module。开启ssl模块
- --with-openssl=/usr/bin/openssl。请填写系统的openssl路径。如果使用指令安装则不需要配置,会使用系统的。
./configure
- 编译和安装。如果没有在上一步配置nginx的目录的话,默认安装在 /usr/local/nginx 下。
make && make install
启动nginx
- 查看文件目录信息
ls -lh /usr/local/nginx/sbin/
添加环境变量(可选)
- 进入 /etc/profile.d/ 目录,新建export_user.sh
cd /etc/profile.d
vim export_user.sh
- export_user.sh输入如下内容。
#!/bin/bash
export PATH=$PATH:/usr/local/nginx/sbin/
- 保存export_user.sh并添加可执行权限
chmod +x export_user.sh
- 加载文件,令变量生效。
source /etc/profile
# 或者,二者等效
/etc/profile.d/export_path.sh
- 检查nginx的环境变量是否生效。输出版本即成功
nginx -v
踩坑:腾讯云服务器
腾讯云服务器需要使用加上sudo才行,因为腾讯云给出来的账号是ubuntu,非root.
# 这里的./一定需要添加,不然会显示异常:
# sudo: nginx: command not found
# 启动nginx
sudo ./nginx
# 加载配置
sudo ./nginx -s reload
# 停止服务
sudo ./nginx -s stop
方法2
- 安装依赖
sudo apt-get update
sudo apt-get install libpcre3 libpcre3-dev
sudo apt-get install zlib1g-dev
sudo apt-get install openssl libssl-dev
sudo apt install nginx
sudo apt-get update
- 启动
启动:$ nginx
加载配置:$ nginx -s reload
重启:service nginx restart
- 卸载
# 彻底卸载nginx
sudo apt-get --purge autoremove nginx
#查看nginx的版本号
nginx -v
说明
支持系统
- 方法1
- ubuntu 18.04
- ubuntu 16.04
方法一:Apt-get
使用apt-get工具安装
安装步骤
安装软件
sudo apt-get install -y redis-server
配置密码(可选)
sudo vim /etc/redis/redis/redis.conf
找到 requirepass ,关闭注释并将后面的字符串修改成想要设置的密码。例如
requirepass 123456 #将密码设置为123456
服务操作
如果是用apt-get或者yum install安装的redis,可以直接通过下面的命令停止/启动/重启redis
/etc/init.d/redis-server stop
/etc/init.d/redis-server start
/etc/init.d/redis-server restart
方法二:源码安装
安装步骤
官网下载源码
官网:下载地址
然后在Ubuntu上操作,源码安装redis
- 下载
wget https://download.redis.io/releases/redis-6.0.9.tar.gz
- 解压到了/user/local目录下
tar -xvf redis-6.0.9.tar.gz -C /usr/local/
- 进入你移动的目录
cd /usr/local/redis-6.0.9
- 编译Redis
sudo make
- 测试编译是否成功,可略过(这一步时间会比较长,耗时5分钟左右)
sudo make test
- 安装
sudo make install
方法三:win安装(redis-window)
下载地址:https://github.com/MicrosoftArchive/redis/releases
版本:3.0.504
-
这里使用的zip安装,下载安装完后,进入安装根目录(msi安装后自动绑定6379服务)
-
控制台启动
./redis-server.exe redis.windows.conf -
日志配置。
- 在根目录创建
Logs目录 - 在该目录下创建
redis_log.txt文件。 - 重启服务
- 在根目录创建
-
配置服务
./redis-server.exe --service-install redis.windows-service.conf --loglevel verbose --service-name Redis-
注意这里对应文件是
redis.windows-service.conf -
相关指令
// 安装服务 ./redis-server.exe --service-install // 卸载服务 ./redis-server.exe --service-uninstall // 开启服务 ./redis-server.exe --service-start // 停止服务 ./redis-server.exe --service-stop // 开启 CLI 模式 ./redis-cli.exe -h 127.0.0.1 -p 6379
-
方法四:win安装(WSL安装)
指南:https://redis.io/docs/install/install-redis/install-redis-on-windows/
windows
- 进入下载网站:https://www.gyan.dev/ffmpeg/builds/
- 选择版本下载。ffmpeg-6.1.1-essentials_build,这里用的是基础版本
- 解压压缩包到本地
- 配置环境变量Path即可
- CMD 执行 ffmpeg -h 出现提示即安装成功
Centos
扩展包
libx264
H. 264视频编码器。有关更多信息和使用示例,请参阅H. 264编码指南。
ffmpeg 配置为 --enable-gpl --enable-libx264.
git clone --branch stable --depth 1 https://code.videolan.org/videolan/x264.git
cd x264
./configure --enable-shared --enable-static
make && make install
libx265
H. 265/HEVC 视频编码器。有关更多信息和使用示例,请参阅H.265编码指南。
要求将 ffmpeg 配置为 --enable-gpl --enable-libx265.
git clone --branch stable --depth 2 https://bitbucket.org/multicoreware/x265_git
cd x265
./configure --enable-shared --enable-static
make && make install
libfdk_aac
AAC 音频编码器。有关更多信息和使用示例,请参阅AAC 音频编码指南。
要求将 ffmpeg 配置为 --enable-libfdk_aac (and --enable-nonfree if you also included --enable-gpl).
git clone --depth 1 https://github.com/mstorsjo/fdk-aac
cd fdk-aac
autoreconf -fiv
./configure --disable-shared
make && make install
libmp3lame
MP3音频编码器。
要求将 ffmpeg 配置为 --enable-libmp3lame.
curl -O -L https://archive.mozilla.org/pub/opus/opus-1.3.1.tar.gz
tar xzvf opus-1.3.1.tar.gz
cd opus-1.3.1
./configure --disable-shared
make
make install
libopus
Opus音频解码器和编码器。
要求将 ffmpeg 配置为 --enable-libopus.
curl -O -L https://archive.mozilla.org/pub/opus/opus-1.3.1.tar.gz
tar xzvf opus-1.3.1.tar.gz
cd opus-1.3.1
./configure --disable-shared
make
make install
libvpx
VP8/VP9视频编码器和解码器。更多信息和使用实例,请参见VP9视频编码指南。
要求将 ffmpeg 配置为 --enable-libvpx.
git clone --depth 1 https://chromium.googlesource.com/webm/libvpx.git
cd libvpx
./configure --disable-examples --disable-unit-tests --enable-vp9-highbitdepth --as=yasm
make
make install
编译安装
-
从git上clone代码到本地
-
clone最新代码
git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg -
clone指定tag代码
git clone -b n6.1.1 --depth=1 https://git.ffmpeg.org/ffmpeg.git ffmpeg-b后面写上指定 版本标签 , 即 tag, 比如 n6.1.1--depth表示克隆深度, 1 表示只克隆最新的版本. 因为如果项目迭代的版本很多, 克隆会很慢
-
上面的办法clone下来的 ./configure 无法执行
-
-
从github上下载
wget https://codeload.github.com/FFmpeg/FFmpeg/tar.gz/refs/tags/n6.1.1 -
安装依赖项目
yum -y install gcc automake autoconf libtool make -
解压并安装
- /usr/local/lib/pkgconfig 是编译时会自动存放一个.pc文件到该目录下
tar -xzvf FFmpeg-n6.1.1.tar.gz cd FFmpeg-n6.1.1.tar.gz ./configure --prefix=/usr/local/ffmpeg --enable-shared --enable-static --disable-autodetect --enable-gpl --enable-version3 --pkg-config-flags="--static" --enable-libwebp --enable-libx264 # --pkg-config-flags="--static" make make install
异常提示
nasm/yasm not found or too old. Use --disable-x86asm for a crippled build
-
问题描述
- nasm/yasm是汇编编译器,ffmpeg为了提高效率使用了汇编指令,如MMX和SSE等。所以系统中未安装nasm/yasm时,就会报上面错误。
-
解决办法
- 安装yasm
-
官网地址:http://yasm.tortall.net/
-
下载解压并安装
wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz tar -xzvf yasm-1.3.0.tar.gz cd yasm-1.3.0 ./configure --prefix=/usr/bin make && make install
-
安装nasm
wget --no-check-certificate https://www.nasm.us/pub/nasm/releasebuilds/2.16/nasm-2.16.tar.gz tar -xvf nasm-2.16.tar.gz cd nasm-2.16/ ./configure --prefix=/usr/bin make && sudo make install
-
完整错误信息
./configure --prefix=/usr/local/ nasm/yasm not found or too old. Use --disable-x86asm for a crippled build. If you think configure made a mistake, make sure you are using the latest version from Git. If the latest version fails, report the problem to the ffmpeg-user@ffmpeg.org mailing list or IRC #ffmpeg on irc.libera.chat. Include the log file "ffbuild/config.log" produced by configure as this will help solve the problem.
ERROR: libwebp >= 0.2.0 not found using pkg-config
-
问题描述
ffmpeg ./configure --enable-libwebp 执行时抛出异常
-
解决办法
yum install libwebp # or apt-get install libwebp # 不能解决,只能编译安装了 sudo yum groupinstall "Development Tools" sudo yum install autoconf automake libtool git clone https://github.com/webmproject/libwebp.git ./autogen.sh ./configure make && sudo make install # 关键在于配置环境变量 export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/ssl/lib/pkgconfig
我的部署
# 进入目录
cd /usr/local/serve
# 创建文件夹jar,用来放Jar文件
mkdir jar
# 移动文件,从home/ubuntu移动该目录下
mv ~/test.jar /usr/local/serve/jar
# 赋予文件权限
sudo chmod u+x test-start.sh
sudo chmod u+x test.jar
sudo chmod 777 -R /usr/local/serve # 简单粗暴
# 执行test-start.sh
./test-start.sh
常用指令
- 查看进程
# 查看所有进程
ps -ef
# 查看所有java进程
ps -ef |grep java
- 杀死java进程。根据PID杀死进程
kill -9 [pid]
- 查看文件日志。假设日志文件名为nohup.out
# 查看最新的200条日志
tail -fn 200 nohup.out
# 查找文件中间内容
cat nohup.out | head -n 20958238 | tail -n +20957300
# 查找文件内容匹配的行数
grep "mac-->" -nR nohup.out
- 远程调试。记得开放端口
java -Duser.timezone=GMT+08 -Xdebug -Xrunjdwp:transport=dt_socket,address=5555,server=y,suspend=y -jar test.jar
- 启动服务。
- 常用参数
- -Dspring.profiles.active=test。指定profile为test
- -Duser.timezone=GMT+08。设置时区
- -server。服务器模式。默认为混合模式
- --logging.file=/var/log/test.log。设置日志文件位置
# 控制台前台启动
java -Duser.timezone=GMT+08 -jar test.jar
# 后台启动
nohup java -Duser.timezone=GMT+08 -jar test.jar &
# 后台启动并指定输出日志文件
java -server -Xms512m -Xmx512m -Duser.timezone=GMT+08 -jar test.jar > test.out 2>&1 &
脚本启动
通过shell脚本
- 创建test-start.sh文件,内容如下。
#!/bin/bash
PID=$(ps -ef|grep ytqms-wx-1.0-SNAPSHOT.jar| grep -v grep | grep -v tail | awk '{printf $2}')
if [ $? -eq 0 ]; then
echo "查询进程ID为 : $PID"
else
echo "脚本执行失败,退出"
exit
fi
if [ ! -n "$PID" ] || [ ! $PID ] || [ "$PID" = "" ]; then
echo "未查询到PID,直接启动项目"
java -server -Xms512m -Xmx512m -Dspring.profiles.active=prod -Duser.timezone=GMT+08 -jar ytqms-wx-1.0-SNAPSHOT.jar > ytqms-wx-1.0-SNAPSHOT.out 2>&1 &
else
kill -9 ${PID}
if [ $? -eq 0 ];then
echo "杀死进程成功,启动项目"
java -server -Xms512m -Xmx512m -Dspring.profiles.active=prod -Duser.timezone=GMT+08 -jar ytqms-wx-1.0-SNAPSHOT.jar > ytqms-wx-1.0-SNAPSHOT.out 2>&1 &
else
echo "杀死进程失败"
fi
fi
- 赋予文件权限
sudo chmod u+x test-start.sh
- 运行文件
sudo ./test-start.sh
配置service
- 进入/etc/systemd/stystem,新建cloud-test.service文件
cd /etc/systemd/system
vim cloud-test.service
- 文件内容如下
[Unit]
Description=cloud-test-service server daemon
After=network-online.target
Wants=network-online.target
[Service]
User=root
Group=root
ExecStart=/bin/sh -c 'exec java -jar /home/ubuntu/serve/jar/cloud-test.jar --logging.file=/var/log/cloud-test.log'
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failur
RestartSec=60s
[Install]
- 保存文件并注册服务。更多指令可查看systemctl.md文件。
# 重新加载所有被修改过的服务配置,否则配置不会生效
sudo systemctl daemon-reload
# 系统开机时自动启动指定unit,前提是配置文件中有相关配置
sudo systemctl enable test.service
# 启动服务
sudo systemctl start test.service
# 停止服务
sudo systemctl stop test.service
# 重启服务
sudo systemctl restart test.service
# 重新加载配置
sudo systemctl reload test.service
jar
使用application.yml启动
FROM java:8
EXPOSE 8080
ENV APP_JVM="-server -Xms2g -Xmx2g -Xmn1g -Duser.timezone=GMT+08"
ENV SPRING_YML="--spring.config.location=/application.yml"
VOLUME /tmp
ADD smilex-admin.jar /admin.jar
ADD application.yml /application.yml
RUN bash -c 'touch /admin.jar'
ENTRYPOINT java ${APP_JVM} -jar /admin.jar ${SPRING_YML}
【docker运行jar】
打包docker镜像
docker build -t my-app .
交互执行镜像(1800:8888,主机的端口:容器的端口)
docker run -it --rm --name my-running-app -p 1800:8888 my-app
后台运行
docker run -d --rm --name my-running-app -p 1800:8888 my-app
【容器管理】
所有容器信息
docker container ls
停止容器
docker container stop c97dcbc8664b(container Id)
启动容器
docker container start c97dcbc8664b(container Id)
重启容器
docker container rm c97dcbc8664b(container Id)
删除容器(必须容器已经停止)
docker container rm c97dcbc8664b(container Id)
查看运行容器
docker ps [-l 查询最后启动的容器]
查看容器端口映射
docker port c97dcbc8664b(containerid)
查看容器日志
docker logs -f c97dcbc8664b(container id)
查看容器的进程
docker top c97dcbc8664b(container Id)
查看 Docker 的底层信息
docker inspect c97dcbc8664b(container Id)
容器交互
docker exec -it c97dcbc8664b bash
【镜像管理】
所有镜像
docker images
获取镜像
docker pull mysql
查找镜像
docker search mysql
删除镜像
docker rmi [-f 强行删除] c97dcbc8664b(images id)
(删除不了时,查看一下dockers ps -a ,如果容器已经停止,但这里依然查出来,执行docker rm containerid 删除后,在执行docker rmi imageid)
【docker容器和主机互相拷贝文件】
语法
docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH
实例
将主机./RS-MapReduce目录拷贝到容器30026605dcfe的/home/cloudera目录下。
docker cp RS-MapReduce 30026605dcfe:/home/cloudera
将容器30026605dcfe的/home/cloudera/RS-MapReduce目录拷贝到主机的/tmp目录中。
docker cp 30026605dcfe:/home/cloudera/RS-MapReduce /tmp/
docker build
docker运行FROM java:8报错:manifest for java:8 not found : manifest unkown: manifest unknown解决方法
dockerfile文件中的 java:8 替换成 openjdk:8
from java:8
修改后
from openjdk:8
failed to solve with frontend dockerfile.v0: failed to create LLB definition:
windows中使用dockers build镜像,出现“failed to solve with frontend dockerfile.v0: failed to create LLB definition: failed to copy: httpReadSeeker: failed open: failed to do request:”。
解决方式:修改 Docker Desktop 的设置。(Docker桌面=>设置=>Docker Engine),将 features.buildkit 的值从 true 修改为 false
{
"builder": {
"gc": {
"defaultKeepStorage": "20GB",
"enabled": true
}
},
"experimental": false,
"features": {
"buildkit": true //this change
}
}
修改后:
{
"builder": {
"gc": {
"defaultKeepStorage": "20GB",
"enabled": true
}
},
"experimental": false,
"features": {
"buildkit": false //this change
}
}
The system cannot find the file specified.
完成错误: error during connect: This error may indicate that the docker daemon is not running.http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.24/version": open //./pipe/docker_engine: The system cannot find the file specified.
在 win10 环境下,执行 docker ps 会抛出这样的异常。根据意思是提权。所以我 换了 CMD 管理员,依然出现这个异常,网上有人说执行这个指令即可。
cd "C:\Program Files\Docker\Docker"
.\DockerCli.exe -SwitchDaemox
但依然不起作用,后来发现是我的 Hyper-V 关掉了。操作步骤
- 打开
控制面板 - 打开
程序 - 选择
启用或关闭 Window 功能 - 开启
Hyper-V,重启电脑即可。
可能还跟缓存日志有关。可以把日志清掉。
Docker failed to initialize Docker Desktop is shutting down
原因的话应该是由于长时间未登陆导致log信息过期了。所以要修改/删除一下原来的信息。
不用重装docker。将 C:\Users\YourUser\AppData\Roaming 目录下Docker目录重命名。比如改为Docker_backup(这样做其实相当于删除了原信息但还把它里面的信息拷贝到备份里)。
MyBatis-面试题
理论
零拷贝
微服务
nacos
dubbo
待整理
记录1
- 重视知识积累
- 微服务概念了解, 微服务各个组件的了解
- 什么是自旋锁
- 审阅代码过程中,发现金额在分布式锁并发下,可能会产生数据错乱的问题,随即提出重构方案,独立重构支付服务的问题。 并不是最终一致性的方式,可以通过最终一致性进行更新使用。
- 要跳出问题去看知识。
- 单机提升3k到2w,用户并发500到800,遇到什么问题。
记录2
怎么实现接口鉴权(比如不同层级,如果需要佛山,传入了广东省) 怎么优化慢sql
- 数据量过大,这时需要做分库分表
- 提升服务器物理性能
- 优化sql语句查询,清除不必要的字段查询,减少回表
- 优化索引结构
- 通过 explain 方法看是否命中索引
怎么优化查询条件比较多且联表查询的sql(分页别忘记)
- 连表查询基本优化原则,小表驱动大表的形式(exists 和 in的区别)
- 在主要匹配的字段上添加联合索引
- 利用关联子查询的方式优化,先利用select id from a.id的方式,覆盖索引的方式直接获取出需要查询的id范围
- 还有用上页的最后一个 id进行where条件,这样会做索引下沉,提高查询效率
什么情况下导致索引失效(引用字段为null的场景别忘记)
- like %前置
- 类型转黄
- 函数运算
- 组合索引中,索引列顺序不匹配,
- or查询 具体可以通过explain分析是否走索引。
给你个有英文单词的文件,分析文件中有哪些单词,统计单词数(从请求参数的设计到返回参数的设计到代码编写,怎么优化代码) 以为是要做关键词匹配dfa算法,实际是简单的切割遍历判断
设计一个黑名单实现 redis+set
记录3
- 数仓方面的技术架构,分析型数据库、商务智能
- netty的性能调优,集群架构实现
- IO模型,select/poll,epoll,三者区别
- netty的零拷贝(kafka也有用到)
- rabbitmq的性能问题
- rabbitmq的集群问题
- emqp到rabbitmq是否处理过
- 如何搭建rabbitmq的环境
- netty和mq的心跳机制做过没有,如果出现宕机怎么识别是否在线
- 一个服务内搭建多个netty服务有什么问题
- 物联网数据存储一般的选型?(我这里回复是选择使用了mysql,主要是是技术栈问题,另一方面没有业务上没有要求实时性等问题,如果有可能考虑时序数据库或者别的存储方式)
- 前面提到了时序数据库,就想问我有用过哪些,我就只是说只了解了一下,本来是想要用mongodb做mysql的替代,但是还是没有去改造。
- 物联网中遇到什么问题?我这里说的是消息积压,然后怎么做处理了
- 通信过程中,如何识别指令,以及对应指令响应的是哪条消息
- 硬件数据通过netty上报平台后,怎么处理?
- 有用dubbo做什么?
- 服务间通信,用dubbo做内部还是做外部通信?
- 有遇到过什么dubbo调用的问题吗?
- dubbo的失败策略有哪些?是如何选择的?
- duboo如何使用一个大数据量的传输?
- rpc怎么去做一个大数据量传输,比如流或者文件?或者说http
- dubbo/rpc的隐私传参怎么做?
- nacos 断了以后,服务之间还能通信吗?会有什么影响?
- nacos 心跳机制和选举方式
- nacos 配置更新后,是属于主动传输还是被动传输
- 多租户的设计方案,如何管理
- 多数据源的切换和事务问题
- 通信的加密方式
记录4
- 单点登录实现 ?
- 全国多服务机器如何实现就近服务访问?
Java基础
数据类型
基本类型和包装类型的区别?
- 成员变量包装类型不赋值就是
null,而基本类型有默认值且不是null。 - 包装类型可用于泛型,而基本类型不可以。
- 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。 - 相比于对象类型, 基本数据类型占用的空间非常小。
为什么说是几乎所有对象实例呢? 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存
⚠️ 注意 : 基本数据类型存放在栈中是一个常见的误区! 基本数据类型的成员变量如果没有被 static 修饰的话(不建议这么使用,应该要使用基本数据类型对应的包装类型),就存放在堆中。
包装类型的缓存机制了解么?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
Integer 缓存源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
Character 缓存源码:
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
Boolean 缓存源码:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
下面我们来看一下问题。下面的代码的输出结果是 true 还是 false 呢?
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);
Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。
因此,答案是 false 。你答对了吗?
记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
举例:
Integer i = 10; //装箱
int n = i; //拆箱
上面这两行代码对应的字节码为:
L1
LINENUMBER 8 L1
ALOAD 0
BIPUSH 10
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;
L2
LINENUMBER 9 L2
ALOAD 0
ALOAD 0
GETFIELD AutoBoxTest.i : Ljava/lang/Integer;
INVOKEVIRTUAL java/lang/Integer.intValue ()I
PUTFIELD AutoBoxTest.n : I
RETURN
从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。
因此,
Integer i = 10等价于Integer i = Integer.valueOf(10)int n = i等价于int n = i.intValue();
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
private static long sum() {
// 应该使用 long 而不是 Long
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
为什么浮点数运算的时候会有精度丢失的风险?
浮点数运算精度丢失代码演示:
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
为什么会出现这个问题呢?
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
就比如说十进制下的 0.2 就没办法精确转换成二进制小数:
// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...
关于浮点数的更多内容,建议看一下计算机系统基础(四)浮点数
对象
接口和抽象类有什么共同点和区别?
共同点 :
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用
default关键字在接口中定义默认方法)。
区别 :
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是
public static final类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
关于深拷贝和浅拷贝区别,我这里先给结论:
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
上面的结论没有完全理解的话也没关系,我们来看一个具体的案例!
浅拷贝
浅拷贝的示例代码如下,我们这里实现了 Cloneable 接口,并重写了 clone() 方法。
clone() 方法的实现很简单,直接调用的是父类 Object 的 clone() 方法。
public class Address implements Cloneable{
private String name;
// 省略构造函数、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address;
// 省略构造函数、Getter&Setter方法
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
测试 :
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
从输出结构就可以看出, person1 的克隆对象和 person1 使用的仍然是同一个 Address 对象。
深拷贝
这里我们简单对 Person 类的 clone() 方法进行修改,连带着要把 Person 对象内部的 Address 对象一起复制。
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
测试 :
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
从输出结构就可以看出,虽然 person1 的克隆对象和 person1 包含的 Address 对象已经是不同的了。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝:
Java常见类
Object类
== 和 equals() 的区别
== 对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==比较的是值。 - 对于引用数据类型来说,
==比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
Object 类 equals() 方法:
public boolean equals(Object obj) {
return (this == obj);
}
equals() 方法存在两种使用情况:
- 类没有重写
equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object类equals()方法。 - 类重写了
equals()方法 :一般我们都重写equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 == 换成 equals() ):
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true
String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
String类equals()方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
hashCode() 有什么用?
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode?
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
当你把对象加入
HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值作比较,如果没有相符的hashCode,HashSet会假设对象没有重复出现。但是如果发现有相同hashCode值的对象,这时会调用equals()方法来检查hashCode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
其实, hashCode() 和 equals()都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!
我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode() 方法呢?
这是因为两个对象的hashCode 值相等并不代表两个对象就相等。
那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。
总结下来就是 :
- 如果两个对象的
hashCode值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode值相等并且equals()方法也返回true,我们才认为这两个对象相等。 - 如果两个对象的
hashCode值不相等,我们就可以直接认为这两个对象不相等。
相信大家看了我前面对 hashCode() 和 equals() 的介绍之后,下面这个问题已经难不倒你们了。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
思考 :重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。
总结 :
equals方法判断两个对象是相等的,那这两个对象的hashCode值也要相等。- 两个对象有相同的
hashCode值,他们也不一定是相等的(哈希碰撞)。
更多关于 hashCode() 和 equals() 的内容可以查看:Java hashCode() 和 equals()的若干问题解答
String类
String、StringBuffer、StringBuilder 的区别?
可变性
String 是不可变的(后面会详细分析原因)。
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String - 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder - 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String 为什么是不可变的?
String 类中使用 final 关键字修饰字符数组来保存字符串,所以String 对象是不可变的。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
🐛 修正 : 我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。
String 真正不可变有下面几点原因:
- 保存字符串的数组被
final修饰且为私有的,并且String类没有提供/暴露修改这个字符串的方法。 String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变。
相关阅读:如何理解 String 类型值的不可变? - 知乎提问
补充(来自issue 675):在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串。
public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
// @Stable 注解表示变量最多被修改一次,称为“稳定的”。
@Stable
private final byte[] value;
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
byte[] value;
}
Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?
新版的 String 其实支持两个编码方案: Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。

如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,byte 和 char 所占用的空间是一样的。
这是官方的介绍:https://openjdk.java.net/jeps/254 。
字符串拼接用“+” 还是 StringBuilder?
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
上面的代码对应的字节码如下:

可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);
StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。

如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
s.append(value);
}
System.out.println(s);

如果你使用 IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。
不过,使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 makeConcatWithConstants() 来实现,而不是大量的 StringBuilder 了。这个改进是 JDK9 的 JEP 280 提出的,这也意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接了。关于这部分改进的详细介绍,推荐阅读这篇文章:还在无脑用 StringBuilder?来重温一下字符串拼接吧 。
字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
更多关于字符串常量池的介绍可以看一下 Java 内存区域详解
intern 方法有什么作用?
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
示例代码(JDK 1.8) :
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
String 类型的变量和常量做“+”运算时发生了什么?
先来看字符串不加 final 关键字拼接的情况(JDK1.8):
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
注意 :比较 String 字符串的值是否相等,可以使用
equals()方法。String中的equals方法是被重写过的。Object的equals方法是比较的对象的内存地址,而String的equals方法比较的是字符串的值是否相等。如果你使用==比较两个字符串是否相等的话,IDEA 还是提示你使用equals()方法替换。

对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到:

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string"; 。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型(
byte、boolean、short、char、int、float、long、double)以及字符串常量。 final修饰的基本数据类型和字符串变量- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
String str4 = new StringBuilder().append(str1).append(str2).toString();
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。
不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。
示例代码:
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
被 final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
示例代码(str2 在运行时才能确定其值):
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
return "ing";
}
Java异常
Java 异常类层次结构图概览 :

Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:
-
Exception:程序本身可以处理的异常,可以通过catch来进行捕获。Exception又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。 -
Error:Error属于程序无法处理的错误 ,~~我们没办法通过catch来进行捕获~~不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。
比如下面这段 IO 操作的代码:

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException...。
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
NullPointerException(空指针错误)IllegalArgumentException(参数错误比如方法入参类型错误)NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)ArrayIndexOutOfBoundsException(数组越界错误)ClassCastException(类型转换错误)ArithmeticException(算术错误)SecurityException(安全错误比如权限不够)UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)- ......

try-catch-finally 如何使用?
try块 : 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。catch块 : 用于处理 try 捕获到的异常。finally块 : 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。
代码示例:
try {
System.out.println("Try to do something");
throw new RuntimeException("RuntimeException");
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
} finally {
System.out.println("Finally");
}
输出:
Try to do something
Catch Exception -> RuntimeException
Finally
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
jvm 官方文档中有明确提到:
If the
tryclause executes a return, the compiled code does the following:
- Saves the return value (if any) in a local variable.
- Executes a jsr to the code for the
finallyclause.- Upon return from the
finallyclause, returns the value saved in the local variable.
代码示例:
public static void main(String[] args) {
System.out.println(f(2));
}
public static int f(int value) {
try {
return value * value;
} finally {
if (value == 2) {
return 0;
}
}
}
输出:
0
finally 中的代码一定会执行吗?
不一定的!在某些情况下,finally 中的代码不会被执行。
就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
try {
System.out.println("Try to do something");
throw new RuntimeException("RuntimeException");
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
// 终止当前正在运行的Java虚拟机
System.exit(1);
} finally {
System.out.println("Finally");
}
输出:
Try to do something
Catch Exception -> RuntimeException
另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:
- 程序所在的线程死亡。
- 关闭 CPU。
相关 issue: https://github.com/Snailclimb/JavaGuide/issues/190。
🧗🏻 进阶一下:从字节码角度分析try catch finally这个语法糖背后的实现原理。
如何使用 try-with-resources 代替try-catch-finally?
- 适用范围(资源的定义): 任何实现
java.lang.AutoCloseable或者java.io.Closeable的对象 - 关闭资源和 finally 块的执行顺序: 在
try-with-resources语句中,任何 catch 或 finally 块在声明的资源关闭后运行
《Effective Java》中明确指出:
面对必须要关闭的资源,我们总是应该优先使用
try-with-resources而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。
Java 中类似于InputStream、OutputStream 、Scanner 、PrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:
//读取文本文件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
使用 Java 7 之后的 try-with-resources 语句改造上面的代码:
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。
通过使用分号分隔,可以在try-with-resources块中声明多个资源。
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出
NumberFormatException而不是其父类IllegalArgumentException。 - 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。
泛型
什么是泛型?有什么作用?
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList persons = new ArrayList() 这行代码就指明了该 ArrayList 对象只能传入 Person 对象,如果传入其他类型的对象就会报错。
ArrayList<E> extends AbstractList<E>
并且,原生 List``Object
泛型的使用方式有哪几种?
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
1.泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2.泛型接口 :
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
3.泛型方法 :
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
注意:
public static < E > void printArray( E[] inputArray )一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的 ``
反射
关于反射的详细解读,请看这篇文章 Java 反射机制详解 。
何谓反射?
如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射的优缺点?
反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
相关阅读:Java Reflection: Why is it so slow? 。
反射的应用场景?
像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method 来调用指定的方法。
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
另外,像 Java 中的一大利器 注解 的实现也用到了反射。
为什么你使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
注解
何谓注解?
Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质是一个继承了Annotation 的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
JDK 提供了很多内置的注解(比如 @Override 、@Deprecated),同时,我们还可以自定义注解。
注解的解析方法有哪几种?
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - 运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的
@Value、@Component)都是通过反射来进行处理的。
SPI
关于 SPI 的详细解读,请看这篇文章 Java SPI 机制详解 。
何谓 SPI?
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

SPI 和 API 有什么区别?
那 SPI 和 API 有啥区别?
说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:

一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
SPI 的优缺点?
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个
ServiceLoader同时load时,会有并发问题。
序列化和反序列化
关于序列化和反序列化的详细解读,请看这篇文章 Java 序列化详解 ,里面涉及到的知识点和面试题更全面。
什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
下面是序列化和反序列化常见应用场景:
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
维基百科是如是介绍序列化的:
序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。
综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

https://www.corejavaguru.com/java/serialization/interview-questions-1
序列化协议对应于 TCP/IP 4 层模型的哪一层?
我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢?
- 应用层
- 传输层
- 网络层
- 网络接口层

如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?
因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 transient 还有几点注意:
transient只能修饰变量,不能修饰类和方法。transient修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int类型,那么反序列后结果就是0。static变量因为不属于任何对象(Object),所以无论有没有transient关键字修饰,均不会被序列化。
常见序列化协议有哪些?
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
为什么不推荐使用 JDK 自带的序列化?
我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:
- 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
- 性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
- 存在安全问题 :序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。相关阅读:应用安全:JAVA反序列化漏洞之殇 。
I/O
关于 I/O 的详细解读,请看下面这几篇文章,里面涉及到的知识点和面试题更全面。
- Java IO 基础知识总结
- Java IO 设计模式总结
- [Java IO 模型详解
# I/O 流为什么要分为字节流和字符流呢?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
个人认为主要有两点原因:
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时;
- 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。
# Java IO 中的设计模式有哪些?
参考答案:Java IO 设计模式总结
# BIO、NIO 和 AIO 的区别?
参考答案:Java IO 模型详解
著作权归Guide所有 原文链接:https://javaguide.cn/java/basis/java-basic-questions-03.html#i-o-%E6%B5%81%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%88%86%E4%B8%BA%E5%AD%97%E8%8A%82%E6%B5%81%E5%92%8C%E5%AD%97%E7%AC%A6%E6%B5%81%E5%91%A2
Java集合
Spring
Spring Bean管理
Spring Bean生命周期
1:设置beanName 。
2:设置BeanClassLoader。
3:设置BeanFactory 。
4:设置Environment。
5:设置EmbeddedValueResolver。
上面5个步骤在开发中不常介入
6:设置ResourceLoader 。
7:设置ApplicationEventPublisher。
8:设置MessageSource 。
9:设置ApplicationContext。
10:设置ServletContext 。
(6-10要通常需要实现对应的Aware接口,特别是第9个步骤开发中常常用到)
11:执行postProcessBeforeInitialization。
12:执行InitializingBean's afterPropertiesSet。
13:执行custom init-method definition(加了@PostConstruct注解的方法会执行由上面顺序可知这个方法在afterPropertiesSet后执行)。
14:执行postProcessAfterInitialization。
(11-14这几个步骤很重要需要掌握)
15:当Spring容器关闭的时候开始执行DisposableBean's destroy方法销毁Bean

SpringMvc
SpringMvc DispatcherServlet加载流程
dispatcherservlet详解initstrategies方法在webapplicationcontext初始化购自动执行,自动扫描上下文bean查找自定义组件,找不到就加载dispatcherservlet.properties配置文件中的默认组件,装配的组件有些只允许一个组件有的允许多个,同一类型的组件错在多个可以通过order属性确定优先级的顺序,这些组件包括各种解析器,映射器,适配器,对于处理器相关的组件会有多个实例
SpringMvc 执行流程
- 用户发送请求至前端控制器 DispatcherServlet
- DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。
- 处理器映射器根据请求 url 找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)可以一并返回给 DispatcherServlet。
- DispatcherServlet 通过 HandlerAdapter 处理器适配器调用处理器
- 执行处理器(Controller,也叫后端控制器)。
- Controller 执行完成返回 ModelAndView。
- HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet。
- DispatcherServlet 将 ModelAndView 传给 View Reslover 视图解析器。
- View Reslover 解析后返回具体 View。
- DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图 中)。
- DispatcherServlet 响应用户。
SpringMvc 核心组件
DispatcherServlet:核心的中央处理器,负责接收请求、分发,并给予客户端响应。
HandlerMapping:处理器映射器,根据 uri 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。
HandlerAdapter:处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler;
Handler:请求处理器,处理实际请求的处理器。
ViewResolver:视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端
SpringAop
aop 底层是采用动态代理机制实现的:接口+实现类
- 如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象。
- 没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用Cglib 生成一个被代理对象的子类来作为代理。
就是由代理创建出一个和 impl 实现类平级的一个对象,但是这个对象不是一个真正的对象,
只是一个代理对象,但它可以实现和 impl 相同的功能,这个就是 aop 的横向机制原理,这
样就不需要修改源代码
11. Springboot集成Zipkin分布式监控
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
#服务追踪
spring.zipkin.service.name=zipkin-test
spring.zipkin.base-url=http://127.0.0.1:9411/
spring.zipkin.discovery-client-enabled=true
spring.zipkin.sender.type=web
spring.sleuth.sampler.probability=1
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index1() throws Exception {
return "成功";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
12. Eclipse安装SpringBoot插件创建项目01. Springboot(2.0.2)中集成SpringBootAdmin和Eureka监控服务
注册中心可以是Eureka、Consul、Dubbo等
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=eurekaServer
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
#展示全部细节信息
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableAutoConfiguration
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.SpringbootAdmin服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8311
spring.application.name=adminServer
eureka.client.registryFetchIntervalSeconds=5
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
eureka.instance.leaseRenewalIntervalInSeconds=10
eureka.instance.health-check-url-path=/actuator/health
#展示全部细节信息
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
#配置邮件提醒
spring.mail.host=smtp.163.com
#spring.mainl.port=465
spring.mail.protocol=smtps
spring.mail.username=Koaliai@163.com
spring.mail.password=JGRHETDUPHQALPHH
# to和from都要配置,否则发送邮件时会报错
spring.boot.admin.notify.mail.to=838032263@qq.com
spring.boot.admin.notify.mail.from=Koaliai@163.com
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
@EnableAutoConfiguration
@EnableAdminServer
@EnableDiscoveryClient
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.SpringbootAdmin客户端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=adminClient
eureka.client.registryFetchIntervalSeconds=5
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
eureka.instance.leaseRenewalIntervalInSeconds=10
eureka.instance.health-check-url-path=/actuator/health
#展示全部细节信息
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableAutoConfiguration
@EnableDiscoveryClient
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 依次启动
Eureka注册中心、SpringbootAdmin服务器端、SpringbootAdmin客户端(2) 访问
SpringbootAdmin的Web管理页面http://localhost:8311
02. Springboot(2.4.1)中集成SpringBootAdmin监控服务
SpringbootAdmin是面向springboot的一款监控组件
1.服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-dependencies</artifactId>
<version>2.3.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=adminServer
management.security.enabled=false
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
@EnableAutoConfiguration
@EnableAdminServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
(4) 访问SpringbootAdmin的Web管理页面,并进行监控客户端运行状况
http://localhost:8310/
2.客户端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-dependencies</artifactId>
<version>2.3.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8311
spring.application.name=adminClient
#admin工程的url
spring.boot.admin.client.url=http://127.0.0.1:8310
#展示全部细节信息
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
#允许admin工程远程停止本应用
management.endpoint.shutdown.enabled=true
#admin工程的账号密码
spring.boot.admin.client.username=admin
spring.boot.admin.client.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@EnableAutoConfiguration
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
03. Springboot(2.0)中集成Zuul网关(Token验证)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eureka-server
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.Zuul路由生产者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8313
spring.application.name=five
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
@RequestMapping("/five")
public class controller {
@RequestMapping(value="/index",method = RequestMethod.GET)
public String Index() {
try {
return "支付成功";
}
catch(Exception e) {
return "支付失败";
}
}
@GetMapping("/hello/{name}")
public String ZuulTestFive(@PathVariable("name") String name) {
return "hello " + name + " this is ZuulTestFive";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.Zuul路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=startGateway
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
zuul.routes.three.path= /three/**
zuul.routes.three.service-id= three
zuul.routes.three.stripPrefix= false
zuul.routes.five.path= /five/**
zuul.routes.five.service-id= five
zuul.routes.five.stripPrefix= false
eureka.client.healthcheck.enabled = true
(3) 创建intector包,并创建MyFilter.java文件
MyFilter.java文件中内容:
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
@Component
public class MyFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run(){
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
Object accessToken = request.getParameter("token");
if(accessToken == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().setHeader("Content-Type", "text/html;charset=UTF-8");
ctx.getResponse().getWriter().write("登录信息为空!");
}catch (Exception e){}
return null;
}
return null;
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@EnableZuulProxy
@ComponentScan("intector")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**依次启动
Eureka注册中心、Zuul路由生产者、Zuul路由消费者,然后访问地址验证地址:http://localhost:8312/five/index
04. Springboot(2.0)中集成Zuul网关限流(Redis存储)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eureka-server
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.Zuul路由生产者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8313
spring.application.name=five
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
@RequestMapping("/five")
public class controller {
@RequestMapping(value="/index",method = RequestMethod.GET)
public String Index() {
try {
return "支付成功";
}
catch(Exception e) {
return "支付失败";
}
}
@GetMapping("/hello/{name}")
public String ZuulTestFive(@PathVariable("name") String name) {
return "hello " + name + " this is ZuulTestFive";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.Zuul路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--Zuul限流的相关依赖-->
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>1.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=startGateway
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
zuul.routes.three.path= /three/**
zuul.routes.three.service-id= three
zuul.routes.three.stripPrefix= false
zuul.routes.five.path= /five/**
zuul.routes.five.service-id= five
zuul.routes.five.stripPrefix= false
spring.redis.database=0
spring.redis.host=192.168.0.19
spring.redis.port=8302
#开启限流
zuul.ratelimit.enabled=true
#针对five路由限流
zuul.ratelimit.repository=redis
#60s内请求超过3次,服务端就抛出异常,60s后可以恢复正常请求
zuul.ratelimit.policies.five.limit=3
#限流类型
zuul.ratelimit.policies.five.type=url
zuul.ratelimit.policies.EURKA-CLIENT1.quota=50
zuul.ratelimit.policies.five.refresh-interval=60
#针对某个IP进行限流,不影响其他IP
zuul.ratelimit.policies.five.type=origin
eureka.client.healthcheck.enabled = true
#全局配置限流
#zuul.ratelimit.enabled=true
#zuul.ratelimit.default-policy.limit=3
#zuul.ratelimit.default-policy.refresh-interval=60
#zuul.ratelimit.default-policy.type=origin
(3) 创建intector包,并创建ExceptionHandler.java文件
ExceptionHandler.java文件中内容:
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExceptionHandler implements ErrorController {
@Override
public String getErrorPath() {
return "error";
}
@RequestMapping(value="/error")
public String error(){
return "{\"result\":\"访问太多频繁,请稍后再访问!!!\"}";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@EnableZuulProxy
@ComponentScan("intector")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**依次启动
Eureka注册中心、Zuul路由生产者、Zuul路由消费者,然后访问地址验证地址:http://localhost:8312/five/index
05. Springboot(2.0)集成SpringGetWay(Token验证)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!-- 这个依赖本来是可以不加的,但是配置没办法刷新,加上它可以手动刷新配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eurekaServer
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableAutoConfiguration
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.GetWay路由消费者1
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=client-test
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@Value("${spring.cloud.client.ip-address}:${server.port}")
private String instance_id;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return instance_id;
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.GetWay路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>0.9.4.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=startGateway
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=gateway8312
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
spring.cloud.gateway.discovery.locator.enabled=false
#开启小写验证,默认feign根据服务名查找都是用的全大写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
spring.cloud.gateway.routes[0].id=client-test
spring.cloud.gateway.routes[0].uri=lb://client-test
#spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
spring.cloud.gateway.routes[0].predicates[0]=Path=/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
(3) 创建domain包,并创建ResultCode.java文件
ResultCode.java文件中内容:
import com.fasterxml.jackson.annotation.JsonFormat;
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ResultCode {
/*
请求返回状态码和说明信息
*/
SUCCESS(200, "成功"),
BAD_REQUEST(400, "参数或者语法不对"),
UNAUTHORIZED(401, "认证失败"),
LOGIN_ERROR(401, "登陆失败,用户名或密码无效"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "请求的资源不存在"),
OPERATE_ERROR(405, "操作失败,请求操作的资源不存在"),
TIME_OUT(408, "请求超时"),
SERVER_ERROR(500, "服务器内部错误"),
;
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public void setCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
(4) 在domain包中创建ResultJson.json文件
ResultJson.java文件中内容:
public class ResultJson {
private int code;
private String msg;
private Object data;
public static ResultJson Success() {
return Success("");
}
public static ResultJson Success(Object o) {
return new ResultJson(ResultCode.SUCCESS, o);
}
public static ResultJson Failure(ResultCode code) {
return Failure(code, "");
}
public static ResultJson Failure(ResultCode code, Object o) {
return new ResultJson(code, o);
}
public ResultJson (ResultCode resultCode) {
setResultCode(resultCode);
}
public ResultJson (ResultCode resultCode,Object data) {
setResultCode(resultCode);
this.data = data;
}
public void setResultCode(ResultCode resultCode) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
}
public void setCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public ResultJson(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
@Override
public String toString() {
return "{" +
"\"code\":" + code +
", \"msg\":\"" + msg + '\"' +
", \"data\":\"" + data + '\"'+
'}';
}
}
(5) 创建Configure包,并创建AuthFilter.java文件
AuthFilter.java文件中内容:
import java.nio.charset.StandardCharsets;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.alibaba.fastjson.JSON;
import domain.*;
import reactor.core.publisher.Mono;
@Component
public class AuthFilter implements GlobalFilter{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("token");
if ("token".equals(token)) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
byte[] datas = JSON.toJSONString(ResultJson.Failure(ResultCode.UNAUTHORIZED)).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(datas);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableAutoConfiguration
@EnableEurekaClient
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 依次启动
Eureka注册中心、Getway路由生产者1、GetWay路由消费者(2) 启动完所有服务,测试
Web页面地址:http://localhost:8312/client-test/index
06. Springboot(2.0)集成SpringGetWay(限流)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!-- 这个依赖本来是可以不加的,但是配置没办法刷新,加上它可以手动刷新配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eurekaServer
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableAutoConfiguration
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.GetWay路由消费者1
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=client-test
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@Value("${spring.cloud.client.ip-address}:${server.port}")
private String instance_id;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return instance_id;
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.GetWay路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>0.9.4.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.main.allow-bean-definition-overriding=true
spring.application.name=startGateway
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=gateway8312
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
spring.redis.host=127.0.0.1
spring.redis.port=8002
spring.redis.database=0
spring.cloud.gateway.discovery.locator.enabled=false
#开启小写验证,默认feign根据服务名查找都是用的全大写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
spring.cloud.gateway.routes[0].id=client-test
spring.cloud.gateway.routes[0].uri=lb://client-test
#spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
spring.cloud.gateway.routes[0].predicates[0]=Path=/**
spring.cloud.gateway.routes[0].filters[0].name=RequestRateLimiter
spring.cloud.gateway.routes[0].filters[0].args.key-resolver=#{@ipKeyResolver}
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.replenishRate=1
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.burstCapacity=3
spring.cloud.gateway.routes[0].filters[1]=StripPrefix=1
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;
@Configuration
public class Configure {
// @Bean
// KeyResolver apiKeyResolver() {
// //按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态
// return exchange -> Mono.just(exchange.getRequest().getPath().toString());
// }
//
// @Bean
// KeyResolver userKeyResolver() {
// //按用户限流
// return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
// }
@Bean
KeyResolver ipKeyResolver() {
//按IP来限流
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("Configure")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 依次启动
Eureka注册中心、Getway路由生产者1、GetWay路由消费者(2) 启动完所有服务,测试
Web页面地址:http://localhost:8312/client-test/index
07. Springboot(2.0)集成SpringGetWay(熔断降级)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!-- 这个依赖本来是可以不加的,但是配置没办法刷新,加上它可以手动刷新配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eurekaServer
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableAutoConfiguration@EnableEurekaServerpublic class Start { public static void main(String[] args) { SpringApplication.run(Start.class, args); }}
2.GetWay路由消费者1
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=client-test
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@Value("${spring.cloud.client.ip-address}:${server.port}")
private String instance_id;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return instance_id;
}
@RequestMapping(value = "/gatewayFallback",method = RequestMethod.GET)
public String Fallback() {
return "接口熔断";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.GetWay路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>0.9.4.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.main.allow-bean-definition-overriding=true
spring.application.name=startGateway
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=gateway8312
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
spring.cloud.gateway.discovery.locator.enabled=false
#开启小写验证,默认feign根据服务名查找都是用的全大写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
spring.cloud.gateway.routes[0].id=client-test
spring.cloud.gateway.routes[0].uri=lb://client-test
#spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
spring.cloud.gateway.routes[0].predicates[0]=Path=/**
#使用内置的HystrixGatewayFilterFactory工厂类做熔断降级
spring.cloud.gateway.routes[0].filters[0].name=Hystrix
# Hystrix的bean名称
spring.cloud.gateway.routes[0].filters[0].args.name=testHystrix
spring.cloud.gateway.routes[0].filters[0].args.fallbackUri=forward:/gatewayFallback
spring.cloud.gateway.routes[0].filters[1]=StripPrefix=1
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
#接口超时时间为2秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("Configure")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 依次启动
Eureka注册中心、Getway路由生产者1、GetWay路由消费者(2) 启动完所有服务,测试
Web页面地址:http://localhost:8312/client-test/index
08. Springboot热部署
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
#热部署生效
spring.devtools.restart.enabled= true
#设置重启的目录
#spring.devtools.restart.additional-paths: src/main/java
#classpath目录下的WEB-INF文件夹内容修改不重启
spring.devtools.restart.exclude= WEB-INF/**
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String Login() {
return "成功";
}
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return "成功";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
09. Springboot(2.0)中集成Mybaits和Durid操作Mysql(普通配置)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.datasource.url=jdbc:mysql://192.168.0.19:3860/test?serverTimezone=UTC&useSSL=true
spring.datasource.username=mysql
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.initial-size=10
spring.datasource.druid.max-active=50
spring.datasource.druid.min-idle=10
spring.datasource.druid.max-wait=5000
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.validation-query-timeout=20000
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.time-between-eviction-runs-millis=60000
mybatis.type-aliases-package=Entity
mybatis.mapperLocations=classpath:mapper/*.xml
(3) 在资源文件夹resources文件夹中创建mapper文件夹,并创建userMapper.xml文件
userMapper.xml文件中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.UserDao">
<resultMap id="BaseResultMap" type="Entity.User">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="address" property="address"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
</resultMap>
<sql id="Base_Column_List">
id, name, age,address, create_time, update_time
</sql>
<select id="selectAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user order by age
</select>
</mapper>
(4) 创建Entity包,并创建User.java文件
User.java文件中内容:
import java.util.Date;
public class User {
private int id;
private String name;
private int age;
private String address;
private Date createTime;
private Date updateTime;
public User() {
}
public User(int id, String name, int age, String address) {
this.id = id;
this.name = name;
this.age = age;
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
(5) 创建Dao包,并创建UserDao.java文件
UserDao.java文件中内容:
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import Entity.*;
@Mapper
public interface UserDao {
List<User> selectAll();
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
import Entity.*;
@CrossOrigin
@Controller
public class controller {
@Autowired
private UserDao userDao;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public List<User> Index() throws Exception {
List<User> users = userDao.selectAll();
return users;
}
}
(7) 主启动目录中内容
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
@MapperScan("Dao")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
10. Springboot(2.0)集成Zuul路由Ribbon负载均衡
1.Eureka服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=eureka-server
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=startGateway
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
zuul.routes.api.path=/api/**
zuul.routes.api.service-id=serviceA
zuul.routes.api.stripPrefix=false
eureka.client.healthcheck.enabled=true
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableAutoConfiguration
@EnableEurekaClient
@EnableZuulProxy
public class Start {
@Bean
@LoadBalanced //调用方法时启用负载均衡
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.路由生产者01
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8313
spring.application.name=serviceA
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@GetMapping("/api/{name}")
public String Api(@PathVariable("name") String name) {
return "hello " + name + " this is ZuulTestFive";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
4.路由生产者02
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8314
spring.application.name=serviceA
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@GetMapping("/api/{name}")
public String Api(@PathVariable("name") String name) {
return "hello " + name + " this is ZuulTestFive";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 启动eureka服务器端服务 (2) 启动zuul生产者服务 (3) 启动zuul消费者01服务 (4) 启动zuul消费者02服务 (5) 测试访问地址:http://localhost:8312/api/jacks
11. Springboot(2.0)接口重用
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 创建service包,并创建UserService.java文件
UserService.java文件中内容:
import org.springframework.stereotype.Component;
@Component
public interface UserService {
void printShow();
}
(3) 在service包中创建UserService1Impl.java文件
UserService1Impl.java文件中内容:
import org.springframework.stereotype.Service;
@Service(value="userService1")
public class UserService1Impl implements UserService {
@Override
public void printShow() {
System.out.println("service1输出");
}
}
(4) 在service包中创建UserService2Impl.java文件
UserService2Impl.java文件中内容:
import org.springframework.stereotype.Service;
@Service(value="userService2")
public class UserService2Impl implements UserService {
@Override
public void printShow() {
System.out.println("service2输出");
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import service.*;
@CrossOrigin
@RestController
public class controller {
@Resource(name="userService1")
private UserService userService1;
@Resource(name="userService1")
private UserService userService2;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
userService1.printShow();
userService2.printShow();
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller,service")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
12. Springboot(2.0)集成Springdata操作Redis(多数据源)(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.data.redis.database = 0
spring.data.redis.host = 192.168.0.19
spring.data.redis.port = 8302
spring.data.redis.password =
spring.data.redis.timeout = 2000
spring.data.redis.max-active = 30
spring.data.redis.max-idle = 30
spring.data.redis.max-waite = 30
spring.data.redis.min-idle = 30
spring.data.redis.max-wait = 6000
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class Configure {
@Value("${spring.data.redis.database}")
private int dbIndex;
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
@Value("${spring.data.redis.timeout}")
private int timeout;
@Value("${spring.data.redis.max-active}")
private int redisPoolMaxActive;
@Value("${spring.data.redis.max-wait}")
private int redisPoolMaxWait;
@Value("${spring.data.redis.max-idle}")
private int redisPoolMaxIdle;
@Value("${spring.data.redis.min-idle}")
private int redisPoolMinIdle;
@Bean
public JedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setDatabase(dbIndex);
//redisStandaloneConfiguration.setPassword(password);
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisPoolMaxIdle);
poolConfig.setMinIdle(redisPoolMinIdle);
poolConfig.setMaxTotal(redisPoolMaxActive);
poolConfig.setMaxWaitMillis(redisPoolMaxWait);
poolConfig.setTestOnBorrow(true);
JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfigurationBuilder = JedisClientConfiguration.builder();
JedisClientConfiguration jedisClientConfiguration = jedisClientConfigurationBuilder.usePooling().poolConfig(poolConfig).build();
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisStandaloneConfiguration,jedisClientConfiguration);
return jedisConnectionFactory;
}
@Bean(name="basicRedisTemplate")
public RedisTemplate<Object,Object> defaultRedisTemplate() {
RedisTemplate<Object,Object> template = new RedisTemplate<Object,Object>();
template.setConnectionFactory(redisConnectionFactory());
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import javax.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@CrossOrigin
@Controller
public class controller {
@Resource(name="basicRedisTemplate")
RedisTemplate<String,String> template;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public String Index() {
if(!template.hasKey("key")){
//写入内容
template.opsForValue().set("key","字符串");
}
else{
//删除数据
template.delete("key");
}
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
13. Springboot(2.0)集成Springdata操作Redis(缓存)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.redis.database=0
spring.redis.host=192.168.0.19
spring.redis.port=8302
spring.redis.timeout=2000
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-active=8
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import java.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@Configuration
@EnableCaching
public class Configure {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间30秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(1800000))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
@Bean
public RedisTemplate<Object,Object> defaultRedisTemplate() {
RedisTemplate<Object,Object> template = new RedisTemplate<Object,Object>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@CrossOrigin
@Controller
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
@Cacheable(value = "test", key = "#id")
public String Index(@RequestParam("id") String id) {
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**删除缓存方法
(1) 删除指定
key值@Caching(evict = { @CacheEvict(value = "MyRedis",key="redis") })(2) 删除指定文件下
value值所有key值@Caching(evict = { @CacheEvict(value = "MyRedis",allEntries=true/*表示删除MyRedis文件下所有的缓存*/) })
14. Springboot(2.0)集成Springdata操作Redis(common-pool连接池)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.redis.database=0
spring.redis.host=192.168.0.19
spring.redis.port=8302
#Redis服务器连接密码(默认为空)
#spring.redis.password=redis123
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@Configuration
public class Configure {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<Object,Object> defaultRedisTemplate() {
RedisTemplate<Object,Object> template = new RedisTemplate<Object,Object>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@CrossOrigin
@Controller
public class controller {
@Autowired
RedisTemplate<String,String> template;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public String Index() {
//写入内容
template.opsForValue().set("key","字符串");
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
15. Springboot中集成jasypt配置文件加密(自定义加密器)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
jasypt.encryptor.bean=codeSheepEncryptorBean
spring.datasource.password=123456
jasypt.encryptor.property.prefix=CodeSheep(
jasypt.encryptor.property.suffix=)
redis.password=CodeSheep(jkKPn/BDQMyplqDQ1Nxp2JB3VlpJZGbsNe2UCTzfI38CsZ/+nV0MbSxZ/EKFr4QP)
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.jasypt.encryption.StringEncryptor;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class Configure {
@Autowired
private ApplicationContext appCtx;
@Autowired
private StringEncryptor codeSheepEncryptorBean;
@Bean(name="codeSheepEncryptorBean" )
public StringEncryptor codesheepStringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword("CodeSheep");
config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setProviderName("SunJCE");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
@Bean
public String init() {
Environment environment = appCtx.getBean(Environment.class);
String redisOriginPswd = environment.getProperty("redis.password");
System.out.println(redisOriginPswd);
//此处将原始密码加密,然后在配置文件中加ENC(<加密字符串>)
//String redisEncryptedPswd = decrypt( redisOriginPswd);
//System.out.println(redisEncryptedPswd);
return "sd";
}
private String encrypt( String originPassord ) {
String encryptStr = codeSheepEncryptorBean.encrypt( originPassord );
return encryptStr;
}
private String decrypt( String encryptedPassword ) {
String decryptStr = codeSheepEncryptorBean.decrypt( encryptedPassword );
return decryptStr;
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 启动参数启动
java -jar yourproject.jar --jasypt.encryptor.password=CodeSheep
16. Springboot(2.0)中集成mybatis(树形结构查询)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.mysql.url=jdbc:mysql://127.0.0.1:3358/test?serverTimezone=UTC&useSSL=true
spring.mysql.username=mysql
spring.mysql.password=123456
spring.mysql.driver-class=com.mysql.jdbc.Driver
(3) 在资源文件夹resources中创建mapper文件夹,并在mapper文件夹中创建personMapper.xml文件
personMapper.xml文件中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.PersonDao">
<resultMap id="BaseTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="parent_id" property="parentId"/>
<result column="name" property="name"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<resultMap id="NextTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="parent_id" property="parentId"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<sql id="Base_Column_List">
id, name,parent_id
</sql>
<select id="getNextPersonTree" resultMap="NextTreeResultMap">
SELECT id,name
FROM person
WHERE parent_id = #{id}
</select>
<select id="getNodeTree" resultMap="BaseTreeResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM person
WHERE parent_id = 23
</select>
</mapper>
(4) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@MapperScan(basePackages ="Dao")
public class Configure {
@Value("${spring.mysql.url}")
private String url;
@Value("${spring.mysql.username}")
private String username;
@Value("${spring.mysql.password}")
private String password;
@Value("${spring.mysql.driver-class}")
private String driverName;
@Bean
@Primary
public DataSource basicDataSource() throws Exception {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverName);
//连接池配置
dataSource.setMaxActive(3000);
dataSource.setMinIdle(5);
dataSource.setInitialSize(5);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setValidationQuery("SELECT 'x'");
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
@Bean(name = "testSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(basicDataSource());
PathMatchingResourcePatternResolver pathMatch = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(pathMatch.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "testSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
(5) 创建Entity包,并创建Person.java文件
Person.java文件中内容:
import java.util.List;
public class Person {
private int id;
private int parentId;
private String name;
//子节点
private List<Person> child;
public Person() {
}
public Person(int id, int parentId, String name, List<Person> child) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.child = child;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public int getParentId() {
return parentId;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setChild(List<Person> child) {
this.child = child;
}
public List<Person> getChild() {
return child;
}
}
(6) 创建Dao包,并创建PersonDao.java文件
PersonDao.java文件中内容:
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import Entity.*;
@Mapper
public interface PersonDao {
@Select("select * from person where id = #{id}")
Person getPerson(int id);
//查询所有节点
List<Person> getNodeTree();
//查询节点
List<Person> getNextPersonTree(int parentId);
}
(7) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
@CrossOrigin
@Controller
public class controller {
@Autowired
private PersonDao personDao;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public List<Person> Index() throws Exception {
List<Person> list =personDao.getNextPersonTree(23);
return list;
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
17. Springboot集成mybait plus(增强型mybaits)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.mysql.url=jdbc:mysql://127.0.0.1:3358/test?serverTimezone=UTC&useSSL=true
spring.mysql.username=mysql
spring.mysql.password=123456
spring.mysql.driver-class=com.mysql.jdbc.Driver
# 初始化大小,最小,最大
spring.mysql.initialSize=5
spring.mysql.minIdle=5
spring.mysql.maxActive=20
# 配置获取连接等待超时的时间
spring.mysql.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.mysql.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.mysql.minEvictableIdleTimeMillis=300000
# 测试连接
spring.mysql.testWhileIdle=true
spring.mysql.testOnBorrow=false
spring.mysql.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.mysql.poolPreparedStatements=true
spring.mysql.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters
spring.mysql.filters=stat
# asyncInit是1.1.4中新增加的配置,如果有initialSize数量较多时,打开会加快应用启动时间
spring.mysql.asyncInit=true
(3) 资源文件夹resources中创建mapper文件夹,并在mapper文件夹中创建personMapper.xml文件
personMapper.xml文件中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.PersonDao">
<resultMap id="BaseTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="parent_id" property="parentId"/>
<result column="name" property="name"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<resultMap id="NextTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="parent_id" property="parentId"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<sql id="Base_Column_List">
id, name,parent_id
</sql>
<select id="getNextPersonTree" resultMap="NextTreeResultMap">
SELECT id,name
FROM person
WHERE parent_id = #{id}
</select>
<select id="getNodeTree" resultMap="BaseTreeResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM person
WHERE parent_id = 23
</select>
</mapper>
(4) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@MapperScan(basePackages ="Dao")
public class Configure {
@Value("${spring.mysql.url}")
private String url;
@Value("${spring.mysql.username}")
private String username;
@Value("${spring.mysql.password}")
private String password;
@Value("${spring.mysql.driver-class}")
private String driverName;
@Bean
@Primary
public DataSource basicDataSource() throws Exception {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverName);
//连接池配置
dataSource.setMaxActive(3000);
dataSource.setMinIdle(5);
dataSource.setInitialSize(5);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setValidationQuery("SELECT 'x'");
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
@Bean(name = "testSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(basicDataSource());
PathMatchingResourcePatternResolver pathMatch = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(pathMatch.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "testSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
(5) 创建Entity包,并创建Person.java文件
Person.java文件中内容:
import java.util.List;
public class Person {
private int id;
private int parentId;
private String name;
//子节点
private List<Person> child;
public Person() {
}
public Person(int id, int parentId, String name, List<Person> child) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.child = child;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public int getParentId() {
return parentId;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setChild(List<Person> child) {
this.child = child;
}
public List<Person> getChild() {
return child;
}
}
(6) 创建Dao包,并创建PersoDao.java文件
PersonDao.java文件中内容:
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import Entity.*;
@Mapper
public interface PersonDao {
@Select("select * from person where id = #{id}")
Person getPerson(int id);
//查询所有节点
List<Person> getNodeTree();
//查询节点
List<Person> getNextPersonTree(int parentId);
}
(7) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
@CrossOrigin
@Controller
public class controller {
@Autowired
private PersonDao personDao;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public List<Person> Index() throws Exception {
List<Person> list =personDao.getNextPersonTree(23);
return list;
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
18. Springboot集成Easyexcel(操作Excel)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.4</version>
</dependency>
</dependencies>
(2) 创建Entity包,并创建Users.java文件
Users.java文件中内容:
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.BaseRowModel;
public class Users extends BaseRowModel {
@ExcelProperty(value = "姓名",index = 0)
private String name;
@ExcelProperty(value = "地址",index = 1)
private String address;
public Users() {
}
public Users(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.util.IOUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.metadata.Sheet;
import com.alibaba.excel.support.ExcelTypeEnum;
import Entity.*;
@RestController
public class controller {
@RequestMapping(value="/index",method={RequestMethod.GET,RequestMethod.POST})
public ResultJson Index(){
return ResultJson.Success();
}
@GetMapping("/download")
public void DownloadExcel(HttpServletResponse response) throws IOException {
Users u1 = new Users("张三","广东");
Users u2 = new Users("李四","四川");
List<Users> list = new ArrayList<Users>();
list.add(u1);
list.add(u2);
ServletOutputStream out = response.getOutputStream();
@SuppressWarnings("deprecation")
ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX, true);
String fileName = "测试exportExcel";
Sheet sheet = new Sheet(1, 0,Users.class);
//设置自适应宽度
sheet.setAutoWidth(Boolean.TRUE);
// 第一个 sheet 名称
sheet.setSheetName("第一个sheet");
writer.write(list, sheet);
//通知浏览器以附件的形式下载处理,设置返回头要注意文件名有中文
response.setHeader("Content-disposition", "attachment;filename=" + new String( fileName.getBytes("gb2312"), "ISO8859-1" ) + ".xlsx");
writer.finish();
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
out.flush();
IOUtils.closeQuietly(out);
}
}
(4) 主启目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
19. Springboot集成EasyPoi(操作Excel)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
或
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- easy-poi -->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
(2) 创建Entity包,并创建Users.java文件
Uers.java文件中内容:
import cn.afterturn.easypoi.excel.annotation.Excel;
public class Users {
@Excel(name = "姓名", orderNum = "0", width = 15)
private String name;
@Excel(name = "地址", orderNum = "1", width = 15)
private String address;
public Users() {
}
public Users(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(3) 创建controller包,并创建ExcelUtils.java文件
ExcelUtils.java文件中内容:
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.web.multipart.MultipartFile;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
public class ExcelUtils {
/**
* excel 导出
*
* @param list 数据
* @param title 标题
* @param sheetName sheet名称
* @param pojoClass pojo类型
* @param fileName 文件名称
* @param isCreateHeader 是否创建表头
* @param response
*/
public static void exportExcel(List<?> list, String title, String sheetName, Class<?> pojoClass, String fileName,
boolean isCreateHeader, HttpServletResponse response) throws IOException {
ExportParams exportParams = new ExportParams(title, sheetName, ExcelType.XSSF);
exportParams.setCreateHeadRows(isCreateHeader);
defaultExport(list, pojoClass, fileName, response, exportParams);
}
/**
* excel 导出
*
* @param list 数据
* @param title 标题
* @param sheetName sheet名称
* @param pojoClass pojo类型
* @param fileName 文件名称
* @param response
*/
public static void exportExcel(List<?> list, String title, String sheetName, Class<?> pojoClass, String fileName,
HttpServletResponse response) throws IOException {
defaultExport(list, pojoClass, fileName, response, new ExportParams(title, sheetName, ExcelType.XSSF));
}
/**
* excel 导出
*
* @param list 数据
* @param pojoClass pojo类型
* @param fileName 文件名称
* @param response
* @param exportParams 导出参数
*/
public static void exportExcel(List<?> list, Class<?> pojoClass, String fileName, ExportParams exportParams,
HttpServletResponse response) throws IOException {
defaultExport(list, pojoClass, fileName, response, exportParams);
}
/**
* excel 导出
*
* @param list 数据
* @param fileName 文件名称
* @param response
*/
public static void exportExcel(List<Map<String, Object>> list, String fileName, HttpServletResponse response)
throws IOException {
defaultExport(list, fileName, response);
}
/**
* 默认的 excel 导出
*
* @param list 数据
* @param pojoClass pojo类型
* @param fileName 文件名称
* @param response
* @param exportParams 导出参数
*/
private static void defaultExport(List<?> list, Class<?> pojoClass, String fileName, HttpServletResponse response,
ExportParams exportParams) throws IOException {
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, pojoClass, list);
downLoadExcel(fileName, response, workbook);
}
/**
* 默认的 excel 导出
*
* @param list 数据
* @param fileName 文件名称
* @param response
*/
private static void defaultExport(List<Map<String, Object>> list, String fileName, HttpServletResponse response)
throws IOException {
Workbook workbook = ExcelExportUtil.exportExcel(list, ExcelType.HSSF);
downLoadExcel(fileName, response, workbook);
}
/**
* 下载
*
* @param fileName 文件名称
* @param response
* @param workbook excel数据
*/
private static void downLoadExcel(String fileName, HttpServletResponse response, Workbook workbook)
throws IOException {
try {
response.setCharacterEncoding("UTF-8");
response.setHeader("content-Type", "application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode(fileName + "." + ExcelTypeEnum.XLSX.getValue(), "UTF-8"));
workbook.write(response.getOutputStream());
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
/**
* excel 导入
*
* @param filePath excel文件路径
* @param titleRows 标题行
* @param headerRows 表头行
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(String filePath, Integer titleRows, Integer headerRows, Class<T> pojoClass)
throws IOException {
if (StringUtils.isBlank(filePath)) {
return null;
}
ImportParams params = new ImportParams();
params.setTitleRows(titleRows);
params.setHeadRows(headerRows);
params.setNeedSave(true);
params.setSaveUrl("/excel/");
try {
return ExcelImportUtil.importExcel(new File(filePath), pojoClass, params);
} catch (NoSuchElementException e) {
throw new IOException("模板不能为空");
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
/**
* excel 导入
*
* @param file excel文件
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(MultipartFile file, Class<T> pojoClass) throws IOException {
return importExcel(file, 1, 1, pojoClass);
}
/**
* excel 导入
*
* @param file excel文件
* @param titleRows 标题行
* @param headerRows 表头行
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(MultipartFile file, Integer titleRows, Integer headerRows, Class<T> pojoClass)
throws IOException {
return importExcel(file, titleRows, headerRows, false, pojoClass);
}
/**
* excel 导入
*
* @param file 上传的文件
* @param titleRows 标题行
* @param headerRows 表头行
* @param needVerfiy 是否检验excel内容
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(MultipartFile file, Integer titleRows, Integer headerRows, boolean needVerfiy,
Class<T> pojoClass) throws IOException {
if (file == null) {
return null;
}
try {
return importExcel(file.getInputStream(), titleRows, headerRows, needVerfiy, pojoClass);
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
/**
* excel 导入
*
* @param inputStream 文件输入流
* @param titleRows 标题行
* @param headerRows 表头行
* @param needVerfiy 是否检验excel内容
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(InputStream inputStream, Integer titleRows, Integer headerRows,
boolean needVerify, Class<T> pojoClass) throws IOException {
if (inputStream == null) {
return null;
}
ImportParams params = new ImportParams();
params.setTitleRows(titleRows);
params.setHeadRows(headerRows);
params.setSaveUrl("/excel/");
params.setNeedSave(true);
params.setNeedVerify(needVerify);
try {
return ExcelImportUtil.importExcel(inputStream, pojoClass, params);
} catch (NoSuchElementException e) {
throw new IOException("excel文件不能为空");
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
/**
* Excel 类型枚举
*/
enum ExcelTypeEnum {
XLS("xls"), XLSX("xlsx");
private String value;
ExcelTypeEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
(4) 在controller包中创建controller.java文件
controller.java文件中内容:
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import Entity.*;
@RestController
public class controller {
@RequestMapping(value="/index",method={RequestMethod.GET,RequestMethod.POST})
public ResultJson Index(){
return ResultJson.Success();
}
@GetMapping("/export")
public void ExportExcel(HttpServletResponse response) throws IOException {
Users u1 = new Users("张三","广东");
Users u2 = new Users("李四","四川");
List<Users> list = new ArrayList<Users>();
list.add(u1);
list.add(u2);
ExcelUtils.exportExcel(list, "员工信息", "员工信息sheet", Users.class, "员工信息表", response);
}
@PostMapping("/import")
public List<Users> ImportExcel(@RequestParam("file") MultipartFile file) throws IOException {
List<Users> list = ExcelUtils.importExcel(file, Users.class);
return list;
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
20. Springboot集成Minio文件系统
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.0.2</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.minio.url = http://127.0.0.1:3580
spring.minio.accessKey = minioadmin
spring.minio.secretKey = minioadmin
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.minio.MinioClient;
@Configuration
public class Configure {
@Value("${spring.minio.url}")
private String url;
@Value("${spring.minio.accessKey}")
private String accessKey;
@Value("${spring.minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
//初始化minio客户端
MinioClient minioClient = null;
try {
minioClient = new MinioClient(url, accessKey, secretKey);
}
catch(Exception e) {
minioClient = null;
}
return minioClient;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.InputStream;
import java.net.URLEncoder;
import javax.servlet.http.HttpServletResponse;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.minio.MinioClient;
import io.minio.ObjectStat;
import io.minio.PutObjectOptions;
@RestController
public class controller {
@Autowired
private MinioClient minioClient;
@RequestMapping(value="/upload",method={RequestMethod.GET,RequestMethod.POST})
public String Upload(@RequestParam("file") MultipartFile file) throws Exception {
InputStream is = file.getInputStream();
// 文件名
final String fileName = file.getOriginalFilename();
// 把文件放到minio的test桶里面
minioClient.putObject("test", fileName, is, new PutObjectOptions(is.available(), -1));
return "成功";
}
@RequestMapping(value="/download",method={RequestMethod.GET,RequestMethod.POST})
public void Download(@RequestParam("fileName") String fileName, HttpServletResponse response) throws Exception {
InputStream in = null;
ObjectStat stat = minioClient.statObject("test", fileName);
response.setContentType(stat.contentType());
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
in = minioClient.getObject("test", fileName);
IOUtils.copy(in, response.getOutputStream());
in.close();
}
@RequestMapping(value="/delete",method={RequestMethod.GET,RequestMethod.POST})
public String Delete(@RequestParam("fileName") String fileName) throws Exception {
minioClient.removeObject("test", fileName);
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
21. Springboot集成限流访问(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
</dependencies>
(2) 创建aop包,并创建Limit.java文件
Limit.java文件中内容:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
//资源名称
String name() default "";
// 限制每秒访问次数,默认最大即不限制
double perSecond() default Double.MAX_VALUE;
}
(3) 在aop包中创建LimitAspect.java文件
LimitAspect.java文件中内容:
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.google.common.util.concurrent.RateLimiter;
@Aspect
@Component
public class LimitAspect {
RateLimiter rateLimiter = RateLimiter.create(Double.MAX_VALUE);
@Pointcut("@annotation(aop.Limit)") //包名和限制标记
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
//获取目标方法
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Limit limit = method.getAnnotation(Limit.class);
rateLimiter.setRate(limit.perSecond());
// 获取令牌桶中的一个令牌,最多等待1秒
if (rateLimiter.tryAcquire(1, 1, TimeUnit.SECONDS)) {
return point.proceed();
} else {
throw new LimitAccessException("网络异常,请稍后重试!");
}
}
}
(4) 在aop包中创建LimitAccessException.java文件
LimitAccessException.java文件中内容:
public class LimitAccessException extends RuntimeException {
private static final long serialVersionUID = -3608667856397125671L;
public LimitAccessException(String message) {
super(message);
}
}
(5) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import aop.*;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"aop"})
public class Configure {
@Bean
public LimitAspect requestLimitAop() {
return new LimitAspect();
}
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import aop.*;
@RestController
public class controller {
@RequestMapping(value="/index",method={RequestMethod.GET,RequestMethod.POST})
@Limit(name = "测试限流每秒3个请求", perSecond = 1)
public String Index() throws Exception {
return "ok";
}
}
(7) 创建Exceptions包,并创建MyExceptionHandler.java文件
MyExceptionHandler.java文件中内容:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import aop.*;
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(value = LimitAccessException.class)//指定拦截的异常
public void errorHandler(HttpServletRequest request, HttpServletResponse response,Exception e) throws Exception{
response.getWriter().write("xsfjslkjskjfsd");
response.getWriter().flush();
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller,Exceptions")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
22. Springboot(2.0)中集成Netty(服务器端)(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port = 8080
(3) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class NettyServer {
@Value("${netty.port}")
private int nettyPort;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workGroup = new NioEventLoopGroup();
/**
* 启动netty服务
* @throws InterruptedException
*/
@PostConstruct
public void start() throws InterruptedException {
ServerBootstrap b=new ServerBootstrap();
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new NettyChannel());
ChannelFuture future = b.bind(nettyPort).sync();
if (future.isSuccess()) {
System.out.println("启动 Netty 成功");
}
}
/**
* 销毁
*/
@PreDestroy
public void destroy() {
bossGroup.shutdownGracefully().syncUninterruptibly();
workGroup.shutdownGracefully().syncUninterruptibly();
System.out.println("关闭 Netty 成功");
}
}
注:
(1)
@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的inti()方法。(2)
@PreDestroy修饰的方法会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的destroy()方法。
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyHandler());
}
}
(4) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
//转十六进制
String str = ByteBufUtil.hexDump(buf).toLowerCase();
System.out.println(str);
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
23. Springboot(2.0)中集成Netty(服务器端)(3)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port = 8080
(3) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class NettyServer {
@Value("${netty.port}")
private int nettyPort;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workGroup = new NioEventLoopGroup();
public void start() {
ServerBootstrap b=new ServerBootstrap();
try {
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new NettyChannel());
ChannelFuture future = b.bind(nettyPort).sync();
/*等待端口关闭*/
future.channel().closeFuture().sync();
}
catch(Exception e) {
e.printStackTrace();
}
finally{
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import org.springframework.stereotype.Component;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
@Component
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyHandler());
}
}
(4) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
//转十六进制
String str = ByteBufUtil.hexDump(buf).toLowerCase();
System.out.println(str);
}
}
(5) 主启动目录中内容
import javax.annotation.Resource;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import netty.*;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start implements CommandLineRunner{
@Resource
private NettyServer nettyServer;
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
@Override
public void run(String... args) throws Exception {
// 开启服务
nettyServer.start();
}
}
注:
@Resource对象只初始化一次
24. Springboot(2.0)中集成Netty(消息服务器)
1.服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port = 8080
(3) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class NettyServer {
@Value("${netty.port}")
private int nettyPort;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workGroup = new NioEventLoopGroup();
public void start() {
ServerBootstrap b=new ServerBootstrap();
try {
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new NettyChannel());
ChannelFuture future = b.bind(nettyPort).sync();
System.out.println("启动");
/*等待端口关闭*/
future.channel().closeFuture().sync();
}
catch(Exception e) {
e.printStackTrace();
}
finally{
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new NettyHandler());
}
}
(5) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
System.out.println(body);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
}
}
(6) 主启动目录中内容
import javax.annotation.Resource;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import netty.*;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start implements CommandLineRunner{
@Resource
private NettyServer nettyServer;
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
@Override
public void run(String... args) throws Exception {
// 开启服务
nettyServer.start();
}
}
2.客户端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.host=127.0.0.1
netty.port = 8080
(3) 创建netty包,并创建NettyClient.java文件
NettyClient.java文件中内容:
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
@Component
public class NettyClient {
@Value("${netty.host}")
private String nettyHost;
@Value("${netty.port}")
private int nettyPort;
private EventLoopGroup workGroup = new NioEventLoopGroup();
@PostConstruct
public void start() throws InterruptedException {
Bootstrap b = new Bootstrap();
b.group(workGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.option(ChannelOption.SO_BACKLOG,128)
.handler(new NettyChannel());
ChannelFuture f = b.connect(nettyHost,nettyPort).sync();
System.out.println("启动");
/*等待端口关闭*/
f.channel().closeFuture().sync();
if (f.isSuccess()) {
System.out.println("启动 Netty 成功");
}
}
@PreDestroy
public void destroy() {
workGroup.shutdownGracefully().syncUninterruptibly();
System.out.println("关闭 Netty 成功");
}
}
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new NettyHandler());
}
}
(5) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
System.out.println(body);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
25. Springboot(2.0)中集成Netty中Websocket服务
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port = 8080
(3) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
public class NettyHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static ChannelGroup channelGroup;
static {
channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 添加到channelGroup通道组
channelGroup.add(ctx.channel());
System.out.println("与客户端建立连接,通道开启!");
}
// 客户端与服务器关闭连接的时候触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道关闭!");
channelGroup.remove(ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String message = msg.text() + " --- 你好," + ctx.channel().localAddress() + " 给固定的人发消息";
ctx.channel().writeAndFlush(new TextWebSocketFrame(message));
//群发消息
message = "我是服务器,这里发送的是群消息";
channelGroup.writeAndFlush(new TextWebSocketFrame(message));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
}
}
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(65536 * 10000))
.addLast(new WebSocketServerProtocolHandler("/ws", "WebSocket", true, 65536 * 10))
.addLast(new NettyHandler());
}
}
(5) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
public class NettyHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static ChannelGroup channelGroup;
static {
channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 添加到channelGroup通道组
channelGroup.add(ctx.channel());
System.out.println("与客户端建立连接,通道开启!");
}
// 客户端与服务器关闭连接的时候触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道关闭!");
channelGroup.remove(ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String message = msg.text() + " --- 你好," + ctx.channel().localAddress() + " 给固定的人发消息";
ctx.channel().writeAndFlush(new TextWebSocketFrame(message));
//群发消息
message = "我是服务器,这里发送的是群消息";
channelGroup.writeAndFlush(new TextWebSocketFrame(message));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
26. Springboot中集成WebSocket(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
(2) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class Configure {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
@ServerEndpoint("/websocket/{userId}")
@Component
public class controller {
private static ConcurrentHashMap<String,Session> webSocketMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session,@PathParam("userId") String userId){
System.out.println(userId);
System.out.println("websocket打开");
}
@OnMessage
public void onMessage(String message,Session session){
System.out.println("接收:" + message);
try{
session.getBasicRemote().sendText("字符串");
}
catch(Exception e){
}
}
@OnError
public void onError(Session session,Throwable error){
error.printStackTrace();
}
@OnClose
public void onClose(Session session){
System.out.print("关闭");
}
}
注:
Websocket访问地址ws://127.0.0.1:8310/websocket/<id值>
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
27. Springboot中集成线程池(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
(2) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class Configure {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(10);
// 设置队列容量
executor.setQueueCapacity(20);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("hello-");
// 设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
(3) 创建service包,并创建UserService.java文件
UserService.java文件中内容:
import org.springframework.stereotype.Component;
@Component
public interface UserService {
String sayHello();
}
(4) 在service包中创建UserServiceImpl.java文件
UserServiceImpl.java文件中内容:
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class UserServiceImpl implements UserService {
@Async
public String sayHello() {
LoggerFactory.getLogger(UserServiceImpl.class).info( ":Hello World!");
return "hello";
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import service.*;
@RestController
public class controller {
@Autowired
private UserService userService;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return userService.sayHello();
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,service,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
28. Springboot中集成Sentinel降级和限流
1.自定义异常回复
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.transport.eager= true
注:
Sentinel采用延迟加载,只有在主动发起一次请求后,才会被拦截并发送给服务端。如果想关闭这个延迟,需将eager设置为true
(3) 创建Configure包,并创建ResponseData.java文件
ResponseData.java文件中内容:
public class ResponseData {
private int code;
private String message;
public ResponseData(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
(4) 在Configure包中创建SentinelException.java文件
SentinelException.java文件中内容:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
@Component
public class SentinelException implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
response.setContentType("application/json;charset=utf-8");
ResponseData data = null;
if (e instanceof FlowException) {
data = new ResponseData(-1, "流控规则被触发......");
} else if (e instanceof DegradeException) {
data = new ResponseData(-2, "降级规则被触发...");
} else if (e instanceof AuthorityException) {
data = new ResponseData(-3, "授权规则被触发...");
} else if (e instanceof ParamFlowException) {
data = new ResponseData(-4, "热点规则被触发...");
} else if (e instanceof SystemBlockException) {
data = new ResponseData(-5, "系统规则被触发...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**需要在
Sentinel管理界面设置相应的限流和降级过后才能触发
2.全局异常回复
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.transport.eager= true
注:
Sentinel采用延迟加载,只有在主动发起一次请求后,才会被拦截并发送给服务端。如果想关闭这个延迟,需将eager设置为true
(3) 创建Configure包,并创建ResponseData.java文件
ResponseData.java文件中内容:
public class ResponseData {
private int code;
private String message;
public ResponseData(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
(4) 在Configure包中创建ExcpetionHandler.java文件
ExcpetionHandler.java文件中内容:
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
@RestControllerAdvice
public class ExcpetionHandler {
@ExceptionHandler(ParamFlowException.class)
public ResponseData test() {
return new ResponseData(-4, "热点规则被触发...");
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**需要在
Sentinel管理界面设置相应的限流和降级过后才能触发
29. Springboot中集成Mybaits和Netty服务
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid的starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port=8010
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://192.168.0.19:3860/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=mysql
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis= 60000
spring.datasource.druid.min-evictable-idle-time-millis= 30000
spring.datasource.druid.validation-query= SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle= true
spring.datasource.druid.test-on-borrow= true
spring.datasource.druid.test-on-return= false
spring.datasource.druid.pool-prepared-statements= true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=Entity
(3) 在资源文件夹resources中创建mapper文件夹,并在mapper文件夹中创建AccountMapper.xml文件
AccountMapper.xml文件中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.AccountDao">
<resultMap id="accountList" type="Entity.Account">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="balance" property="balance"/>
</resultMap>
<select id="findAll" resultMap="accountList">
select * from tbl_account
</select>
</mapper>
(4) 创建Entity包,并创建Account.java文件
Account.java文件中内容:
import org.springframework.stereotype.Component;
@Component
public class Account {
private int id;
private String name;
private float balance;
public Account() {
}
public Account(int id, String name, float balance) {
this.id = id;
this.name = name;
this.balance = balance;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getBalance() {
return balance;
}
public void setBalance(float balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account [id=" + id + ", name=" + name + ", balance=" + balance + "]";
}
}
(5) 创建Dao包,并创建AccountDao.java文件
AccountDao.java文件中内容:
import java.util.List;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface AccountDao {
public List<Account> findAll();
}
(6) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import Dao.*;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class NettyServer {
@Value("${netty.port}")
private int nettyPort;
@Autowired
private AccountDao accountDao;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workGroup = new NioEventLoopGroup();
/**
* 启动netty服务
* @throws InterruptedException
*/
@PostConstruct
public void start() throws InterruptedException {
ServerBootstrap b=new ServerBootstrap();
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new NettyChannel(accountDao));
ChannelFuture future = b.bind(nettyPort).sync();
if (future.isSuccess()) {
System.out.println("启动 Netty 成功");
}
}
/**
* 销毁
*/
@PreDestroy
public void destroy() {
bossGroup.shutdownGracefully().syncUninterruptibly();
workGroup.shutdownGracefully().syncUninterruptibly();
System.out.println("关闭 Netty 成功");
}
}
(7) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import org.springframework.stereotype.Component;
import Dao.*;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
@Component
public class NettyChannel extends ChannelInitializer<SocketChannel>{
private AccountDao accountDao;
public NettyChannel(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyHandler(accountDao));
}
}
(8) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import java.util.List;
import org.springframework.stereotype.Component;
import Dao.*;
import Entity.*;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
@Component
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
private AccountDao accountDao;
public NettyHandler(AccountDao accountDao) {
this.accountDao=accountDao;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
ByteBuf msg=(ByteBuf) buf;
System.out.println(msg.toString(CharsetUtil.UTF_8));
List<Account> users = accountDao.findAll();
users.forEach(user-> System.out.println(user.toString()));
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
cause.printStackTrace();
ctx.close();
}
}
(9) 主启动目录中内容
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@MapperScan(basePackages ="Dao")
@ComponentScan("netty")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
30. Springboot(2.4)中集成Springdata操作Mongodb(多数据源)(1)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.data.mongodb.url=mongodb://192.168.0.19:8092
spring.data.mongodb.database=test
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
@Configuration
@EnableMongoRepositories(basePackages = {"Dao"})
public class Configure {
@Value("${spring.data.mongodb.url}")
private String url;
@Value("${spring.data.mongodb.database}")
private String database;
@Bean
@Primary
public SimpleMongoClientDatabaseFactory mongoDbFactory() throws Exception{
ConnectionString connectionString = new ConnectionString(url);
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.retryWrites(true)
.build();
MongoClient mongClient = MongoClients.create(settings);
return new SimpleMongoClientDatabaseFactory(mongClient,database);
}
}
(4) 创建Entity包,并创建User.java文件
User.java文件中内容:
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "user")
public class User {
private String userName;
public User(){
}
public User(String userName){
this.userName = userName;
}
public void setUserName(String userName){
this.userName = userName;
}
public String getUserName(){
return userName;
}
@Override
public String toString() {
return "User [userName=" + userName + "]";
}
}
(5) 创建Dao包,并创建userDao.java文件
userDao.java文件中内容:
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface userDao extends MongoRepository<User,ObjectId>{
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import Dao.*;
import Entity.*;
@CrossOrigin
@RestController
public class controller {
@Autowired
userDao userRepository;
@RequestMapping(value="/index",method = {RequestMethod.GET})
public String Index() {
User user = new User("张三");
userRepository.save(user);
return "成功";
}
}
(7) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
31. Springboot(2.4)中集成Springdata操作Mongodb(多数据源)(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.data.mongodb.url=mongodb://192.168.0.19:8092
spring.data.mongodb.database=test
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
@Configuration
public class Configure {
@Value("${spring.data.mongodb.url}")
private String url;
@Value("${spring.data.mongodb.database}")
private String database;
@Primary
public SimpleMongoClientDatabaseFactory mongoDbFactory() throws Exception{
ConnectionString connectionString = new ConnectionString(url);
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.retryWrites(true)
.build();
MongoClient mongClient = MongoClients.create(settings);
return new SimpleMongoClientDatabaseFactory(mongClient,database);
}
@Bean(name = "primaryMongoTemplate")
public MongoTemplate getMongoTemplate() throws Exception {
return new MongoTemplate(mongoDbFactory());
}
}
(4) 创建Entity包,并创建User.java文件
User.java文件中内容:
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "user")
public class User {
private String userName;
public User(){
}
public User(String userName){
this.userName = userName;
}
public void setUserName(String userName){
this.userName = userName;
}
public String getUserName(){
return userName;
}
@Override
public String toString() {
return "User [userName=" + userName + "]";
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import javax.annotation.Resource;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import Entity.*;
@CrossOrigin
@RestController
public class controller {
@Resource(name = "primaryMongoTemplate")
private MongoTemplate mongoTemplate;
@RequestMapping(value="/index",method = {RequestMethod.GET})
public String Index() {
User user = new User("张三");
mongoTemplate.save(user);
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
32. Springboot(2.4)中集成Springdata操作Mongodb(多数据源)(3)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
first.uri=mongodb://192.168.0.19:8092
first.database=test1
first.host=192.168.0.19
first.port=8092
second.uri=mongodb://192.168.0.19:8092
second.database=test2
second.host=192.168.0.19
second.port=8092
(3) 创建firstMongo包,并创建FirstMongoProperties.java文件
FirstMongoProperties.java文件中内容:
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class FirstMongoProperties {
@Primary
@Bean(name="firstMongoProperties1")
@ConfigurationProperties(prefix="first")
public MongoProperties firstMongoProperties(){
return new MongoProperties();
}
}
(4) 在firstMongo创建FirstMongoTemplate.java文件
FirstMongoTemplate.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
@Configuration
@EnableMongoRepositories(basePackages="firstDao",mongoTemplateRef="firstMongo")
public class FirstMongoTemplate {
@Autowired
@Qualifier("firstMongoProperties1")
private MongoProperties mongoProperties;
@Primary
@Bean
public SimpleMongoClientDatabaseFactory firstFactory(MongoProperties mongo) throws Exception{
ConnectionString connectionString = new ConnectionString(mongo.getUri());
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.retryWrites(true)
.build();
MongoClient mongClient = MongoClients.create(settings);
return new SimpleMongoClientDatabaseFactory(mongClient,mongo.getDatabase());
}
@Primary
@Bean(name="firstMongo")
public MongoTemplate firstMongoTemplate() throws Exception {
return new MongoTemplate(firstFactory(this.mongoProperties));
}
}
(5) 创建secondMongo包,并创建SecondMongoProperties.java文件
SecondMongoProperties.java文件中内容:
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SecondMongoProperties {
@Bean(name="secondMongoProperties2")
@ConfigurationProperties(prefix="second")
public MongoProperties firstMongoProperties(){
return new MongoProperties();
}
}
(6) 在secondMongo创建SecondMongoTemplate.java文件
SecondMongoTemplate.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
@Configuration
@EnableMongoRepositories(basePackages="secondDao",mongoTemplateRef="secondMongo")
public class SecondMongoTemplate {
@Autowired
@Qualifier("secondMongoProperties2")
private MongoProperties mongoProperties;
@Bean
public SimpleMongoClientDatabaseFactory secondFactory(MongoProperties mongo) throws Exception{
ConnectionString connectionString = new ConnectionString(mongo.getUri());
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.retryWrites(true)
.build();
MongoClient mongClient = MongoClients.create(settings);
return new SimpleMongoClientDatabaseFactory(mongClient,mongo.getDatabase());
}
@Bean(name="secondMongo")
public MongoTemplate firstMongoTemplate() throws Exception {
return new MongoTemplate(secondFactory(this.mongoProperties));
}
}
(7) 创建Entity包,并创建User.java文件
User.java文件中内容:
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "user")
public class User {
private String userName;
public User(){
}
public User(String userName){
this.userName = userName;
}
public void setUserName(String userName){
this.userName = userName;
}
public String getUserName(){
return userName;
}
@Override
public String toString() {
return "User [userName=" + userName + "]";
}
}
(8) 创建firstDao包,并创建FirstUserDao.java文件
FirstUserDao.java文件中内容:
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface FirstUserDao extends MongoRepository<User,ObjectId> {
User findByUserName(String userName);
}
(9) 创建secondDao包,并创建SecondUserDao.java文件
SecondUserDao.java文件中内容:
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface SecondUserDao extends MongoRepository<User,ObjectId> {
User findByUserName(String userName);
}
(10) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import Entity.*;
import firstDao.*;
import secondDao.*;
@CrossOrigin
@RestController
public class controller {
@Autowired
FirstUserDao firstRepository;
@Autowired
SecondUserDao secondRepository;
@RequestMapping(value="/first",method = {RequestMethod.GET})
public String First() {
User user = new User("张三");
firstRepository.save(user);
return "成功";
}
@RequestMapping(value="/second",method = {RequestMethod.GET})
public String Second() {
User user = new User("张三");
secondRepository.save(user);
return "成功";
}
}
(11) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
@ComponentScan("firstMongo,secondMongo,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
33. Springboot(2.0)集成mybaits开启Ecache二级缓存(1)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
</dependencies>
(2) 主启动目录创建资源文件夹resources文件夹,并创建application.properties文件
application.properties中内容:
server.port=8310
spring.mysql.url=jdbc:mysql://Ip地址:端口/数据库名称?serverTimezone=UTC&useSSL=false
spring.mysql.username=用户名
spring.mysql.password=密码
spring.mysql.driver-class=com.mysql.jdbc.Driver
(3) 在resources文件夹中,创建maper文件夹并创建personMapper.xml文件
personMapper.xml中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.PersonDao">
<resultMap id="BaseTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="parent_id" property="parentId"/>
<result column="name" property="name"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<resultMap id="NextTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="parent_id" property="parentId"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<sql id="Base_Column_List">id, name,parent_id</sql>
<select id="getNextPersonTree" resultMap="NextTreeResultMap">
SELECT id,name FROM person WHERE parent_id = #{id}
</select>
<select id="getNodeTree" resultMap="BaseTreeResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM person
WHERE parent_id = 23
</select>
</mapper>
(4) 创建Configure包,并创建Congifure.java文件
Configure.java中内容:
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@MapperScan(basePackages ="Dao")
public class Configure {
@Value("${spring.mysql.url}")
private String url;
@Value("${spring.mysql.username}")
private String username;
@Value("${spring.mysql.password}")
private String password;
@Value("${spring.mysql.driver-class}")
private String driverName;
@Bean
@Primary
public DataSource basicDataSource() throws Exception {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverName);
//连接池配置
dataSource.setMaxActive(3000);
dataSource.setMinIdle(5);
dataSource.setInitialSize(5);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setValidationQuery("SELECT 'x'");
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
@Bean(name = "testSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(basicDataSource());
PathMatchingResourcePatternResolver pathMatch = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(pathMatch.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "testSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
(5) 在Configure包中创建MyEhcacheConfig.java文件
MyEhcacheConfig.java中内容:
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
@Configuration
@EnableCaching
public class MyEhcacheConfig {
@Bean
public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean bean) {
return new EhCacheCacheManager(bean.getObject());
}
/**
* 据shared与否的设置,
* Spring分别通过CacheManager.create()
* 或new CacheManager()方式来创建一个ehcache基地.
* 也说是说通过这个来设置cache的基地是这里的Spring独用,还是跟别的(如hibernate的Ehcache共享)
*
* @return
*/
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
cacheManagerFactoryBean.setShared(true);
return cacheManagerFactoryBean;
}
}
(6) 创建Entity包,并创建Person.java文件
Person.java中内容:
package Entity;
import java.io.Serializable;
import java.util.List;
public class Person implements Serializable{
private static final long serialVersionUID = 1L;
private int id;
private int parentId;
private String name;
//子节点
private List<Person> child;
public Person() {
}
public Person(int id, int parentId, String name, List<Person> child) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.child = child;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public int getParentId() {
return parentId;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setChild(List<Person> child) {
this.child = child;
}
public List<Person> getChild() {
return child;
}
}
(7) 创建Dao包,并创建PersonDao.java文件
PersonDao.java中内容:
import java.util.List;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import Entity.*;
//分布式环境下必然会出现脏数据;
//多表联合查询的情况下极大可能会出现脏数据;
@Mapper
//整个类开启缓存
//@CacheNamespace
public interface PersonDao {
@Select("select * from person where id = #{id}")
Person getPerson(int id);
//查询所有节点
List<Person> getNodeTree();
//查询节点
List<Person> getNextPersonTree(int parentId);
}
(8) 创建controller包,并创建controller.java文件
controller.java中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
import domain.ResultJson;
@CrossOrigin
@Controller
public class controller {
@Autowired
private PersonDao personDao;
@Cacheable(key="'user_'+#uid",value="userCache")
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public Person Index() throws Exception {
return personDao.getPerson(1);
}
}
(9) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
// TODO Auto-generated method stub
SpringApplication.run(Start.class, args);
}
}
34. Springboot集成Kaptcha图形验证码
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
kaptcha.border=yes
kaptcha.border.color=105,179,90
kaptcha.textproducer.font.color=blue
kaptcha.textproducer.font.size=30
kaptcha.textproducer.font.names=宋体
kaptcha.textproducer.char.length=4
kaptcha.image.width=120
kaptcha.image.height=40
kaptcha.session.key=code
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import java.util.Properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
@Configuration
public class Configure {
@Value("${kaptcha.border}")
private String border;
@Value("${kaptcha.border.color}")
private String borderColor;
@Value("${kaptcha.textproducer.font.color}")
private String textproducerFontColor;
@Value("${kaptcha.textproducer.font.size}")
private String textproducerFontSize;
@Value("${kaptcha.textproducer.font.names}")
private String textproducerFontNames;
//验证码长度
@Value("${kaptcha.textproducer.char.length}")
private String textproducerCharLength;
@Value("${kaptcha.image.width}")
private String imageWidth;
@Value("${kaptcha.image.height}")
private String imageHeight;
@Value("${kaptcha.session.key}")
private String sessionKey;
@Bean
public DefaultKaptcha getDefaultKapcha(){
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty("kaptcha.border", border);
properties.setProperty("kaptcha.border.color", borderColor);
properties.setProperty("kaptcha.textproducer.font.color", textproducerFontColor);
properties.setProperty("kaptcha.textproducer.font.size", textproducerFontSize);
properties.setProperty("kaptcha.textproducer.font.names", textproducerFontNames);
properties.setProperty("kaptcha.textproducer.char.length", textproducerCharLength);
properties.setProperty("kaptcha.image.width", imageWidth);
properties.setProperty("kaptcha.image.height", imageHeight);
properties.setProperty("kaptcha.session.key", sessionKey);
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import com.google.code.kaptcha.impl.DefaultKaptcha;
@CrossOrigin
@Controller
public class controller {
/* 注入Kaptcha */
@Autowired
private DefaultKaptcha defaultKaptcha;
@GetMapping("/code")
@ResponseBody
public ModelAndView getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpSession session = request.getSession();
response.setDateHeader("Expires", 0);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
// return a jpeg
response.setContentType("image/jpeg");
// create the text for the image
String capText = defaultKaptcha.createText();
//存储文字
session.setAttribute("test", capText);
// create the image with the text
BufferedImage bi = defaultKaptcha.createImage(capText);
ServletOutputStream out = response.getOutputStream();
// write the data out
ImageIO.write(bi, "jpg", out);
try {
out.flush();
} finally {
out.close();
}
return null;
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
35. Springboot(2.0)集成zookeeper分布式锁
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- zookeeper 客户端 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
##ZooKeeper地址
zk.url = 127.0.0.1:2181
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Configure {
@Value("${zk.url}")
private String zkUrl;
@Bean
public CuratorFramework getCuratorFramework() {
// 用于重连策略,1000毫秒是初始化的间隔时间,3代表尝试重连次数
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
//操作zookeeper集群zkUrl路径ip:xx,ip:xx
CuratorFramework client = CuratorFrameworkFactory.newClient(zkUrl, retryPolicy);
//必须调用start开始连接ZooKeeper
client.start();
/**
* 使用Curator,可以通过LeaderSelector来实现领导选取;
* 领导选取:选出一个领导节点来负责其他节点;如果领导节点不可用,则在剩下的机器里再选出一个领导节点
*/
//构造一个监听器
/*LeaderSelectorListenerAdapter listener = new LeaderSelectorListenerAdapter() {
@Override
public void takeLeadership(CuratorFramework curatorFramework) throws Exception {
log.info("get leadership");
// 领导节点,方法结束后退出领导。zk会再次重新选择领导
}
};
LeaderSelector selector = new LeaderSelector(client, "/schedule", listener);
selector.autoRequeue();
selector.start();
*/
return client;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.util.concurrent.TimeUnit;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class controller {
@Autowired
private CuratorFramework zkClient;
@GetMapping("/zookeeperLock1")
public ResultJson ZookeeperLock1() throws Exception {
try {
// InterProcessMutex 构建一个分布式锁该节点会在客户端链接断开时被删除
InterProcessMutex lock = new InterProcessMutex(zkClient, "/test");
try {
//锁定5秒钟
if (lock.acquire(5, TimeUnit.HOURS)) {
// 模拟业务处理耗时5秒
Thread.sleep(5*1000);
System.out.println("处理事务");
}
} finally {
// 释放该锁
//lock.release();
}
} catch (Exception e) {
// zk异常
e.printStackTrace();
}
return "zookeeper1锁定";
}
@GetMapping("/zookeeperLock2")
public String ZookeeperLock2() throws Exception {
// 获取锁
InterProcessSemaphoreMutex balanceLock = new InterProcessSemaphoreMutex(zkClient, "/zktest");
try {
// 执行加锁操作
balanceLock.acquire();
System.out.println("执行事务");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 释放锁资源
balanceLock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
return "zookeeper2锁定";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("controller,Configure")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
36. Springboot(2.0)中集成Springdata操作Mysql(自动建表)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.mysql.url=jdbc:mysql://127.0.0.1:3358/test?serverTimezone=UTC&useSSL=true
spring.mysql.username=mysql
spring.mysql.password=123456
spring.mysql.driver-class=com.mysql.cj.jdbc.Driver
spring.mysql.minPoolSize = 3
spring.mysql.maxPoolSize = 25
spring.mysql.maxLifetime = 20000
spring.mysql.borrowConnectionTimeout = 30
spring.mysql.loginTimeout = 30
spring.mysql.maintenanceInterval = 60
spring.mysql.maxIdleTime = 60
#在控制台打印出SQL语句
spring.jpa.show-sql=true
#每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
spring.jpa.hibernate.ddl-auto=update
#Hibernate生成的SQL语句为MySQL方言
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@EnableJpaRepositories(basePackages="Dao")
@EntityScan("Entity")
public class Configure {
@Value("${spring.mysql.url}")
private String url;
@Value("${spring.mysql.username}")
private String username;
@Value("${spring.mysql.password}")
private String password;
@Value("${spring.mysql.driver-class}")
private String driverName;
@Bean
public DataSource basicDataSource() throws Exception {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverName);
//连接池配置
dataSource.setMaxActive(3000);
dataSource.setMinIdle(5);
dataSource.setInitialSize(5);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setValidationQuery("SELECT 1");
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
}
(4) 创建Entity包,并创建Person.java文件
controller.java文件中内容:
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="person_info")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name="name")
private String name;
@Column(name="age")
private int age;
@Column(name="address")
private String address;
@Column(name="create_time")
private Date createTime = new Date();
@Column(name="update_time")
private Date updateTime = new Date();
public Person() {
}
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
(5) 创建Dao包,并创建PersonDao.java文件
PersonDao.java文件中内容:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface PersonDao extends JpaRepository<Person,Integer>{
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
import Entity.*;
@CrossOrigin
@Controller
public class controller {
@Autowired
private PersonDao personDao;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public List<Person> Index() throws Exception {
List<Person> persons = personDao.findAll();
return persons;
}
@RequestMapping(value = "/save",method = RequestMethod.GET)
@ResponseBody
public String Save() throws Exception {
List<Person> persons = new ArrayList<Person>();
Person person1 = new Person("李四",12,"广东");
Person person2 = new Person("张三",20,"广东");
persons.add(person1);
persons.add(person2);
personDao.saveAll(persons);
return "成功";
}
}
(7) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
#每次运行该程序,没有表格会新建表格,表内有数据会清空 spring.jpa.hibernate.ddl-auto:create #每次程序结束的时候会清空表 spring.jpa.hibernate.ddl-auto:create-drop #每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新 spring.jpa.hibernate.ddl-auto:update #运行程序会校验数据与数据库的字段类型是否相同,不同会报错 spring.jpa.hibernate.ddl-auto:validate
37. Springboot中挂载静态资源(1)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.freemarker.request-context-attribute=request
spring.mvc.view.prefix=classpath:/templates/
spring.freemarker.suffix=.html
spring.freemarker.content-type=text/html
spring.freemarker.enabled=true
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.allow-request-override=false
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
(3) 在资源文件夹resources文件夹中,创建templates文件夹,并创建index.html文件
index.html文件中内容:
<html>
<head>
<title>测试页面</title>
</head>
<body>
hello
</body>
</html>
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@CrossOrigin
@Controller
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() throws Exception {
return "index";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
(6) 访问Web页面
地址:http://127.0.0.1:8310/index
38. Springboot中挂载静态资源(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.freemarker.request-context-attribute=request
spring.mvc.view.prefix=classpath:/templates/
spring.freemarker.suffix=.html
spring.freemarker.content-type=text/html
spring.freemarker.enabled=true
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.allow-request-override=false
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
(3) 创建资源文件夹static,并创建index.html文件
index.html文件中内容:
<html>
<head>
<title>测试页面</title>
</head>
<body>
hello
</body>
</html>
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@CrossOrigin
@Controller
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() throws Exception {
return "index";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
(6) 访问Web页面
地址:http://127.0.0.1:8310/index.html
39. Springboot(2.0)集成Feign传递Oauth2-Token服务
1.Eureka服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=eureka-server
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.服务提供者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8313
spring.application.name = producer
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建Entity包,并创建User.java文件
User.java文件中内容:
public class User {
private String name;
private String address;
public User() {
}
public User(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(4) 创建controller包,并创建UserApi.java文件
UserApi.java文件中内容:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import Entity.*;
@RequestMapping("user")
public interface UserApi {
@GetMapping("hello")
public String hello();
@GetMapping("findById")
public User findById(@RequestParam(value = "id")Integer id);
}
(5) 在controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.ResponseBody;
import Entity.*;
@Service
@ResponseBody
public class controller implements UserApi {
@Override
public String hello() {
System.out.println("我进来了");
return "我来了" ;
}
@Override
public User findById(Integer id) {
User user = new User();
user.setName("张三");
user.setAddress("广东");
return user;
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
@EnableEurekaClient
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.服务消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8390
spring.application.name=consumer
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建Entity包,并创建User.java文件
User.java文件中内容:
public class User {
private String name;
private String address;
public User() {
}
public User(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(4) 创建service包,并创建UserApi.java文件
UserApi.java文件中内容:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import Entity.*;
@RequestMapping("user")
@Controller
public interface UserApi {
@GetMapping("hello")
@ResponseBody
public String hello();
@GetMapping("findById")
@ResponseBody
public User findById(@RequestParam(value = "id")Integer id);
}
(5) 在service包中创建ConsumerService.java文件
ConsumerService.java文件中内容:
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient(name = "producer")
public interface ConsumerService extends UserApi{
}
(6) 创建Interceptor包,并创建TokenFeignClientInterceptor.java文件
TokenFeignClientInterceptor.java文件中内容:
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import feign.RequestInterceptor;
import feign.RequestTemplate;
@Component
public class TokenFeignClientInterceptor implements RequestInterceptor {
/**
* token放在请求头.
* @param requestTemplate 请求参数
*/
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
if (requestAttributes != null) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//添加token
requestTemplate.header(HttpHeaders.AUTHORIZATION, request.getHeader(HttpHeaders.AUTHORIZATION));
}
}
}
(7) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.net.URI;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.discovery.EurekaClient;
import Entity.*;
import service.*;
@Service
@RestController
public class controller {
@Autowired
DiscoveryClient discoveryClient;
@Autowired
private ConsumerService consumerService;
@Autowired
EurekaClient eurekaClient;
@Autowired
LoadBalancerClient loadBalancerClient;
@GetMapping("hello")
public String getHello() {
return consumerService.hello();
}
@GetMapping("findById")
public User findById(Integer id) {
return consumerService.findById(id);
}
@GetMapping("client1")
public void getClient() {
List<String> list = discoveryClient.getServices();
for (String str : list) {
System.out.println(str);
}
}
@GetMapping("client2")
public Object getClient2() {
return discoveryClient.getInstances("producer");
}
@GetMapping("client3")
public Object getClient3() {
@SuppressWarnings("unchecked")
List<InstanceInfo> list = eurekaClient.getInstancesById("HWL-2020:producer:8313");
InstanceInfo instanceInfo = list.get(0);
if(instanceInfo.getStatus() == InstanceStatus.UP) {
String url = "http://" + instanceInfo.getHostName() + ":" + instanceInfo.getPort() + "/user/hello" ;
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
System.out.println(responseEntity.getBody());
}
return null;
}
@GetMapping("client4")
public Object getClient4() {
ServiceInstance serviceInstance = loadBalancerClient.choose("producer");
URI uri = serviceInstance.getUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri.toString()+"/user/hello", String.class);
return responseEntity.getBody();
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableFeignClients("service")
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**在客户端调用提供者时头传递
Authorization信息
40. Springboot(2.0)集成数字计算验证码
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--验证码-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.google.code.kaptcha.Producer;
@CrossOrigin
@RestController
public class controller {
@Autowired
private Producer producer;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public void Index(HttpServletResponse response) throws Exception {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
//生成文字验证码
String text = producer.createText();
//个位数字相加
String s1 = text.substring(0, 1);
String s2 = text.substring(1, 2);
int count = Integer.valueOf(s1).intValue() + Integer.valueOf(s2).intValue();
//生成图片验证码
BufferedImage image = producer.createImage(s1 + "+" + s2 + "=?");
//保存 redis key 自己设置
//stringRedisTemplate.opsForValue().set("",String.valueOf(count));
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
41. Springboot(2.0)中文件上传和下载(1)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.servlet.multipart.max-file-size=100
spring.servlet.multipart.max-request-size=100
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import javax.servlet.MultipartConfigElement;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
@Configuration
public class Configure {
@Value("${spring.servlet.multipart.max-request-size}")
private Long maxRequestSize;
@Value("${spring.servlet.multipart.max-file-size}")
private Long maxFileSize;
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 置文件大小限制 ,超出此大小页面会抛出异常信息
factory.setMaxRequestSize(DataSize.ofMegabytes(maxRequestSize)); //KB,MB
// 设置总上传数据总大小
factory.setMaxRequestSize(DataSize.ofMegabytes(maxFileSize));
// 设置文件临时文件夹路径
// factory.setLocation("E://test//");
// 如果文件大于这个值,将以文件的形式存储,如果小于这个值文件将存储在内存中,默认为0
// factory.setMaxRequestSize(0);
return factory.createMultipartConfig();
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.*;
@RestController
public class Controller {
private static String s = System.getProperty("user.dir");
@PostMapping("/upload")
public String upload(@RequestParam(value = "file", required = false) MultipartFile file) {
if (file.isEmpty()) {
return "上传失败,请选择文件";
}
String fileName = file.getOriginalFilename();
String filePath = s+"/temp/";
File dest = new File(filePath + fileName);
File filefoder = new File(filePath);
if(!filefoder.exists()) {
filefoder.mkdirs();
}
try {
file.transferTo(dest);
return "上传成功";
} catch (IOException e) {
}
return "上传失败!";
}
@PostMapping("/multiUpload")
public String multiUpload(HttpServletRequest request) {
List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("file");
String filePath = s+"/temp/";
File filefoder = new File(filePath);
if(!filefoder.exists()) {
filefoder.mkdirs();
}
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
if (file.isEmpty()) {
return "上传第" + (i++) + "个文件失败";
}
String fileName = file.getOriginalFilename();
File dest = new File(filePath + fileName);
try {
file.transferTo(dest);
} catch (IOException e) {
return "上传第" + (i++) + "个文件失败";
}
}
return "上传成功";
}
//文件下载相关代码
@RequestMapping("/download")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
String fileName = "zookeeper-3.4.13.tar.gz";// 设置文件名,根据业务需要替换成要下载的文件名
System.out.println(request.getParameter("filename"));
if (fileName != null) {
//设置文件路径
String filePath = s+"/temp/";
File file = new File(filePath , fileName);
if (file.exists()) {
response.setContentType("application/force-download");// 设置强制下载不打开
response.addHeader("Content-Disposition", "attachment;fileName=" + fileName);// 设置文件名
byte[] buffer = new byte[1024];
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(file);
bis = new BufferedInputStream(fis);
OutputStream os = response.getOutputStream();
int i = bis.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
}
catch (Exception e) {
if (bis != null) {
try {
bis.close();
} catch (IOException ex) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException ex) {
e.printStackTrace();
}
}
}
}
}
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args){
SpringApplication.run(Start.class,args);
}
}
42. Springboot(2.0)中文件上传和下载(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.*;
@RestController
public class Controller {
private static String s = System.getProperty("user.dir");
@PostMapping("/upload")
public String upload(@RequestParam(value = "file", required = false) MultipartFile file) {
if (file.isEmpty()) {
return "上传失败,请选择文件";
}
String fileName = file.getOriginalFilename();
String filePath = s+"/temp/";
File dest = new File(filePath + fileName);
File filefoder = new File(filePath);
if(!filefoder.exists()) {
filefoder.mkdirs();
}
try {
file.transferTo(dest);
return "上传成功";
} catch (IOException e) {
}
return "上传失败!";
}
@PostMapping("/multiUpload")
public String multiUpload(HttpServletRequest request) {
List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("file");
String filePath = s+"/temp/";
File filefoder = new File(filePath);
if(!filefoder.exists()) {
filefoder.mkdirs();
}
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
if (file.isEmpty()) {
return "上传第" + (i++) + "个文件失败";
}
String fileName = file.getOriginalFilename();
File dest = new File(filePath + fileName);
try {
file.transferTo(dest);
} catch (IOException e) {
return "上传第" + (i++) + "个文件失败";
}
}
return "上传成功";
}
//文件下载相关代码
@RequestMapping("/download")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
String fileName = "zookeeper-3.4.13.tar.gz";// 设置文件名,根据业务需要替换成要下载的文件名
System.out.println(request.getParameter("filename"));
if (fileName != null) {
//设置文件路径
String filePath = s+"/temp/";
File file = new File(filePath , fileName);
if (file.exists()) {
response.setContentType("application/force-download");// 设置强制下载不打开
response.addHeader("Content-Disposition", "attachment;fileName=" + fileName);// 设置文件名
byte[] buffer = new byte[1024];
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(file);
bis = new BufferedInputStream(fis);
OutputStream os = response.getOutputStream();
int i = bis.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
}
catch (Exception e) {
if (bis != null) {
try {
bis.close();
} catch (IOException ex) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException ex) {
e.printStackTrace();
}
}
}
}
}
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args){
SpringApplication.run(Start.class,args);
}
}
43. Springboot(2.0)中限制文件大小
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import javax.imageio.ImageIO;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.POST)
public String Index(@RequestParam("file") MultipartFile file) throws Exception {
if(file.isEmpty()) {
return "选择图片";
}
//检查文件大小
if(file.getSize() > 1*1024*1024) {
return "请上传1M以内的图片";
}
//检查是否是图片
BufferedImage bi = ImageIO.read(file.getInputStream());
if(bi == null){
return "上传的文件不是图片";
}
String originalFilename = file.getOriginalFilename();
String fileType = null;
if(originalFilename.contains(".")) {
fileType = originalFilename.substring(originalFilename.lastIndexOf("."));
} else {
fileType = ".jpg";
}
String filePath = "D://test//"; // 上传后的路径
String fileName = UUID.randomUUID() + fileType; // 新文件名
File dest = new File(filePath + fileName);
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
}
return "成功";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
44. Springboot(2.0)中检测上传文件名
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
(3) 创建Utils包,并创建FileTypeUtils.java文件
FileTypeUtils.java文件中内容:
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class FileTypeUtils {
// 默认判断文件头前三个字节内容
public static int default_check_length = 3;
final static HashMap<String, String> fileTypeMap = new HashMap<>();
// 初始化文件头类型,不够的自行补充
static {
fileTypeMap.put("ffd8ffe000104a464946", "jpg"); //JPEG (jpg)
fileTypeMap.put("89504e470d0a1a0a0000", "png"); //PNG (png)
fileTypeMap.put("47494638396126026f01", "gif"); //GIF (gif)
fileTypeMap.put("49492a00227105008037", "tif"); //TIFF (tif)
fileTypeMap.put("424d228c010000000000", "bmp"); //16色位图(bmp)
fileTypeMap.put("424d8240090000000000", "bmp"); //24位位图(bmp)
fileTypeMap.put("424d8e1b030000000000", "bmp"); //256色位图(bmp)
fileTypeMap.put("41433130313500000000", "dwg"); //CAD (dwg)
fileTypeMap.put("3c21444f435459504520", "html"); //HTML (html)
fileTypeMap.put("3c21646f637479706520", "htm"); //HTM (htm)
fileTypeMap.put("48544d4c207b0d0a0942", "css"); //css
fileTypeMap.put("696b2e71623d696b2e71", "js"); //js
fileTypeMap.put("7b5c727466315c616e73", "rtf"); //Rich Text Format (rtf)
fileTypeMap.put("38425053000100000000", "psd"); //Photoshop (psd)
fileTypeMap.put("46726f6d3a203d3f6762", "eml"); //Email [Outlook Express 6] (eml)
fileTypeMap.put("d0cf11e0a1b11ae10000", "doc"); //MS Excel 注意:word、msi 和 excel的文件头一样
fileTypeMap.put("d0cf11e0a1b11ae10000", "vsd"); //Visio 绘图
fileTypeMap.put("5374616E64617264204A", "mdb"); //MS Access (mdb)
fileTypeMap.put("252150532D41646F6265", "ps");
fileTypeMap.put("255044462d312e350d0a", "pdf"); //Adobe Acrobat (pdf)
fileTypeMap.put("2e524d46000000120001", "rmvb"); //rmvb/rm相同
fileTypeMap.put("464c5601050000000900", "flv"); //flv与f4v相同
fileTypeMap.put("00000020667479706d70", "mp4");
fileTypeMap.put("49443303000000002176", "mp3");
fileTypeMap.put("000001ba210001000180", "mpg"); //
fileTypeMap.put("3026b2758e66cf11a6d9", "wmv"); //wmv与asf相同
fileTypeMap.put("52494646e27807005741", "wav"); //Wave (wav)
fileTypeMap.put("52494646d07d60074156", "avi");
fileTypeMap.put("4d546864000000060001", "mid"); //MIDI (mid)
fileTypeMap.put("504b0304140000000800", "zip");
fileTypeMap.put("526172211a0700cf9073", "rar");
fileTypeMap.put("235468697320636f6e66", "ini");
fileTypeMap.put("504b03040a0000000000", "jar");
fileTypeMap.put("4d5a9000030000000400", "exe");//可执行文件
fileTypeMap.put("3c25402070616765206c", "jsp");//jsp文件
fileTypeMap.put("4d616e69666573742d56", "mf");//MF文件
fileTypeMap.put("3c3f786d6c2076657273", "xml");//xml文件
fileTypeMap.put("494e5345525420494e54", "sql");//xml文件
fileTypeMap.put("7061636b616765207765", "java");//java文件
fileTypeMap.put("406563686f206f66660d", "bat");//bat文件
fileTypeMap.put("1f8b0800000000000000", "gz");//gz文件
fileTypeMap.put("6c6f67346a2e726f6f74", "properties");//bat文件
fileTypeMap.put("cafebabe0000002e0041", "class");//bat文件
fileTypeMap.put("49545346030000006000", "chm");//bat文件
fileTypeMap.put("04000000010000001300", "mxp");//bat文件
fileTypeMap.put("504b0304140006000800", "docx");//docx文件
fileTypeMap.put("d0cf11e0a1b11ae10000", "wps");//WPS文字wps、表格et、演示dps都是一样的
fileTypeMap.put("6431303a637265617465", "torrent");
fileTypeMap.put("6D6F6F76", "mov"); //Quicktime (mov)
fileTypeMap.put("FF575043", "wpd"); //WordPerfect (wpd)
fileTypeMap.put("CFAD12FEC5FD746F", "dbx"); //Outlook Express (dbx)
fileTypeMap.put("2142444E", "pst"); //Outlook (pst)
fileTypeMap.put("AC9EBD8F", "qdf"); //Quicken (qdf)
fileTypeMap.put("E3828596", "pwl"); //Windows Password (pwl)
fileTypeMap.put("2E7261FD", "ram"); //Real Audio (ram)
}
/**
* @param fileName
* @return String
* @description 通过文件后缀名获取文件类型
*/
public static String getFileTypeBySuffix(String fileName) {
return fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
}
/**
* @param inputStream
* @return String
* @description 通过文件头魔数获取文件类型
*/
public static String getFileTypeByMagicNumber(InputStream inputStream) {
byte[] bytes = new byte[default_check_length];
try {
// 获取文件头前三位魔数的二进制
inputStream.read(bytes, 0, bytes.length);
// 文件头前三位魔数二进制转为16进制
String code = bytesToHexString(bytes);
for (Map.Entry<String, String> item : fileTypeMap.entrySet()) {
if (code.startsWith(item.getKey()) || item.getKey().startsWith(code)) {
return item.getValue();
}
}
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
public static String bytesToHexString(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import Utils.FileTypeUtils;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.POST)
public String Index(@RequestParam("file") MultipartFile file) throws Exception {
String type = FileTypeUtils.getFileTypeByMagicNumber(file.getInputStream());
System.out.println(type);
String originalFilename = file.getOriginalFilename();
String fileType = null;
if(originalFilename.contains(".")) {
fileType = originalFilename.substring(originalFilename.lastIndexOf("."));
} else {
fileType = ".jpg";
}
String filePath = "D://test//"; // 上传后的路径
String fileName = UUID.randomUUID() + fileType; // 新文件名
File dest = new File(filePath + fileName);
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
}
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
45. Springboot集成Zipkin分布式监控
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
#服务追踪
spring.zipkin.service.name=zipkin-test
spring.zipkin.base-url=http://127.0.0.1:9411/
spring.zipkin.discovery-client-enabled=true
spring.zipkin.sender.type=web
spring.sleuth.sampler.probability=1
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index1() throws Exception {
return "成功";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
46. Springboot集成Came-Ftp文件定时同步
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-ftp</artifactId>
<version>2.22.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
ftp.img.url=ftp://192.168.1.6:21/disk/D?username=root&password=123456
ftp.img.dir=file:D:/test/img
ftp.file.url=ftp://192.168.1.7:21/disk/D?username=root&password=123456
ftp.file.dir=file:D:/test/file
#后台进程
camel.springboot.main-run-controller=true
(3) 创建Configure包,并创建FtpRouteBuilder.java文件
FtpRouteBuilder.java文件中内容:
import org.apache.camel.LoggingLevel;
import org.apache.camel.builder.RouteBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class FtpRouteBuilder extends RouteBuilder {
private static Logger logger = LoggerFactory.getLogger(FtpRouteBuilder.class );
@Value("${ftp.img.url}")
private String sftpServerImg;
@Value("${ftp.img.dir}")
private String downloadLocationImg;
@Value("${ftp.file.url}")
private String sftpServerFile;
@Value("${ftp.file.dir}")
private String downloadLocationFile;
@Autowired
private DataProcessor dataProcessor;
@Override
public void configure() throws Exception {
// from 内为 URL
from(sftpServerImg)
// 本地路径
.to(downloadLocationImg)
// 数据处理器
.process(dataProcessor)
.log(LoggingLevel.INFO, logger, "Download img ${file:name} complete.");
from(sftpServerFile)
.to(downloadLocationFile)
.log(LoggingLevel.INFO, logger, "Download file ${file:name} complete.");
}
}
(4) 在Configure包中创建ImgFilter.java文件
ImgFilter.java文件中内容:
import org.apache.camel.component.file.GenericFile;
import org.apache.camel.component.file.GenericFileFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ImgFilter implements GenericFileFilter<Object> {
@Value("${ftp.img.dir}")
private String fileDir;
@Override
public boolean accept(GenericFile<Object> genericFile) {
return genericFile.getFileName().endsWith(".jpg") || genericFile.isDirectory();
}
}
(5) 在Configure包中创建fileFilter.java文件
fileFilter.java文件中内容:
import org.apache.camel.component.file.GenericFile;
import org.apache.camel.component.file.GenericFileFilter;
public class fileFilter implements GenericFileFilter {
@Override
public boolean accept(GenericFile file) {
return file.getFileName().endsWith(".zip") || file.isDirectory();
}
}
(6) 在Configure包中创建DataProcessor.java文件
DataProcessor.java文件中内容:
import java.io.RandomAccessFile;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.component.file.GenericFileMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class DataProcessor implements Processor {
@Value("${ftp.img.dir}")
private String fileDir;
@SuppressWarnings("unchecked")
@Override
public void process(Exchange exchange) throws Exception {
try {
GenericFileMessage<RandomAccessFile> inMsg = (GenericFileMessage<RandomAccessFile>) exchange.getIn();
String fileName = inMsg.getGenericFile().getFileName();
System.out.println(fileName);
String sql = "insert into file(filename) values (?)";
} catch (Exception e) {
e.printStackTrace();
}
}
}
(7) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@CrossOrigin
@Controller
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public String Index() throws Exception {
return "成功";
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
47. Springboot集成JustAuth第三方登录
支持Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么和推特等第三方平台的授权登录还在继续扩展。
配置示例:
justauth.enabled=true
justauth.type.github.client-id=101d*************8b3a
justauth.type.github.client-secret=58e*************************5edd
justauth.type.github.redirect-uri=http://127.0.0.1/demo/oauth/github/callback
justauth.type.qq.client-id=10******85
justauth.type.qq.client-secret=1f7d************************d629e
justauth.type.qq.redirect-uri=http://127.0.0.1/demo/oauth/qq/callback
justauth.type.wechat.client-id=wxdcb******4ff4
justauth.type.wechat.client-secret=b4e9dc************************a08ed6d
justauth.type.wechat.redirect-uri=http://127.0.0.1/demo/oauth/wechat/callback
justauth.type.google.client-id=716******17-6db******vh******ttj320i******userco******t.com
justauth.type.google.client-secret=9IBorn************7-E
justauth.type.google.redirect-uri=http://127.0.0.1/demo/oauth/google/callback
justauth.type.microsoft.client-id=7bdce8******************e194ad76c1b
justauth.type.microsoft.client-secret=Iu0zZ4************************tl9PWan_.
justauth.type.microsoft.redirect-uri=https://127.0.0.1/demo/oauth/microsoft/callback
justauth.type.mi.client-id=288************2994
justauth.type.mi.client-secret=nFeTt89************************==
justauth.type.mi.redirect-uri=http://127.0.0.1/demo/oauth/mi/callback
justauth.type.wechat_enterprise.client-id=ww58******f3************fbc
justauth.type.wechat_enterprise.client-secret=8G6PCr00j************************rgk************AyzaPc78
justauth.type.wechat_enterprise.redirect-uri=http://127.0.0.1/demo/oauth/wechat_enterprise/callback
justauth.type.wechat_enterprise.agent-id=1*******2
(1) 在Mysql中创建相应表
DROP TABLE IF EXISTS `justauth`.`t_ja_user`;
CREATE TABLE `justauth`.`t_ja_user` (
`uuid` varchar(64) NOT NULL COMMENT '用户第三方系统的唯一id',
`username` varchar(100) DEFAULT NULL COMMENT '用户名',
`nickname` varchar(100) DEFAULT NULL COMMENT '用户昵称',
`avatar` varchar(255) DEFAULT NULL COMMENT '用户头像',
`blog` varchar(255) DEFAULT NULL COMMENT '用户网址',
`company` varchar(50) DEFAULT NULL COMMENT '所在公司',
`location` varchar(255) DEFAULT NULL COMMENT '位置',
`email` varchar(50) DEFAULT NULL COMMENT '用户邮箱',
`gender` varchar(10) DEFAULT NULL COMMENT '性别',
`remark` varchar(500) DEFAULT NULL COMMENT '用户备注(各平台中的用户个人介绍)',
`source` varchar(20) DEFAULT NULL COMMENT '用户来源',
PRIMARY KEY (`uuid`)
);
(2) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
(3) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.1.8:3860/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
spring.datasource.username=mysql
spring.datasource.password=123456
spring.redis.host=192.168.1.8
spring.redis.port=8302
#spring.redis.password=123456
spring.redis.timeout=2000ms
spring.redis.database=0
spring.redis.lettuce.pool.maxActive=8
spring.redis.lettuce.pool.maxIdle=8
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.type-aliases-package=Entity
mybatis-plus.global-config.db-config.id-type=AUTO
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
logging.level.com.xkcoding=debug
justauth.enabled=true
justauth.type.BAIDU.client-id=xxxxxx
justauth.type.BAIDU.client-secret=xxxxxx
justauth.type.BAIDU.redirect-uri=http://127.0.0.1:8310/oauth/baidu/callback
justauth.type.GITEE.client-id=xxx
justauth.type.GITEE.client-secret=xxx
justauth.type.GITEE.redirect-uri=http://127.0.0.1:8310/oauth/gitee/callback
justauth.cache.type=redis
justauth.cache.prefix=JUATAUTH::STATE::
justauth.cache.timeout=3m
(4) 创建Configure包,并创建RedisConfig.java文件
RedisConfig.java文件中内容:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
Logger log = LoggerFactory.getLogger(RedisConfig.class);
@Bean(name = "redisTemplate")
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* 缓存错误处理
*/
@Bean
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.error("Redis occur handleCacheClearError:", e);
}
};
}
/**
* 注入Redis缓存配置类
*
* @return
*/
@Bean(name = { "redisCacheConfiguration" })
public RedisCacheConfiguration redisCacheConfiguration() {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
return configuration;
}
}
(5) 在Configure包中创建JustAuthTokenCache.java文件
JustAuthTokenCache.java文件中内容:
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import com.alibaba.fastjson.JSONObject;
import me.zhyd.oauth.model.AuthToken;
@Configuration
public class JustAuthTokenCache {
@SuppressWarnings("rawtypes")
@Autowired
private RedisTemplate redisTemplate;
private BoundHashOperations<String, String, AuthToken> valueOperations;
@SuppressWarnings("unchecked")
@PostConstruct
public void init() {
valueOperations = redisTemplate.boundHashOps("JUSTAUTH::TOKEN");
}
/**
* 保存Token
*
* @param uuid 用户uuid
* @param authUser 授权用户
* @return
*/
public AuthToken saveorUpdate(String uuid, AuthToken authToken) {
valueOperations.put(uuid, authToken);
return authToken;
}
/**
* 根据用户uuid查询Token
*
* @param uuid 用户uuid
* @return
*/
public AuthToken getByUuid(String uuid) {
Object token = valueOperations.get(uuid);
if (null == token) {
return null;
}
return JSONObject.parseObject(JSONObject.toJSONString(token), AuthToken.class);
}
/**
* 查询所有Token
*
* @return
*/
public List<AuthToken> listAll() {
return new LinkedList<>(Objects.requireNonNull(valueOperations.values()));
}
/**
* 根据用户uuid移除Token
*
* @param uuid 用户uuid
* @return
*/
public void remove(String uuid) {
valueOperations.delete(uuid);
}
}
(6) 创建Entity包,并创建JustAuthUser.java文件
JustAuthUser.java文件中内容:
import java.io.Serializable;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
@TableName(value = "t_ja_user")
public class JustAuthUser extends AuthUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户第三方系统的唯一id。在调用方集成该组件时,可以用uuid + source唯一确定一个用户
*/
@TableId(type = IdType.INPUT)
private String uuid;
/**
* 用户授权的token信息
*/
@TableField(exist = false)
private AuthToken token;
/**
* 第三方平台返回的原始用户信息
*/
@TableField(exist = false)
private JSONObject rawUserInfo;
/**
* 自定义构造函数
*
* @param authUser 授权成功后的用户信息,根据授权平台的不同,获取的数据完整性也不同
*/
public JustAuthUser(AuthUser authUser) {
super(authUser.getUuid(), authUser.getUsername(), authUser.getNickname(), authUser.getAvatar(),
authUser.getBlog(), authUser.getCompany(), authUser.getLocation(), authUser.getEmail(),
authUser.getRemark(), authUser.getGender(), authUser.getSource(), authUser.getToken(),
authUser.getRawUserInfo());
}
}
(7) 创建Dao包,并创建JustAuthUserMapper.java文件
JustAuthUserMapper.java文件中内容:
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import Entity.*;
@Mapper
public interface JustAuthUserMapper extends BaseMapper<JustAuthUser> {
}
(8) 创建service包,并创建JustAuthUserService.java文件
JustAuthUserService.java文件中内容:
import com.baomidou.mybatisplus.extension.service.IService;
import Entity.*;
public interface JustAuthUserService extends IService<JustAuthUser> {
/**
* 保存或更新授权用户
*
* @param justAuthUser 授权用户
* @return
*/
boolean saveOrUpdate(JustAuthUser justAuthUser);
/**
* 根据用户uuid查询信息
*
* @param uuid 用户uuid
* @return
*/
JustAuthUser getByUuid(String uuid);
/**
* 根据用户uuid移除信息
*
* @param uuid 用户uuid
* @return
*/
boolean removeByUuid(String uuid);
}
(9) 在service包中创建JustAuthUserServiceImpl.java文件
JustAuthUserServiceImpl.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import Configure.JustAuthTokenCache;
import Dao.*;
import Entity.*;
@Service
public class JustAuthUserServiceImpl extends ServiceImpl<JustAuthUserMapper, JustAuthUser>
implements JustAuthUserService {
@Autowired
private JustAuthTokenCache justAuthTokenCache;
/**
* 保存或更新授权用户
*
* @param justAuthUser 授权用户
* @return
*/
@Override
public boolean saveOrUpdate(JustAuthUser justAuthUser) {
justAuthTokenCache.saveorUpdate(justAuthUser.getUuid(), justAuthUser.getToken());
return super.saveOrUpdate(justAuthUser);
}
/**
* 根据用户uuid查询信息
*
* @param uuid 用户uuid
* @return
*/
@Override
public JustAuthUser getByUuid(String uuid) {
JustAuthUser justAuthUser = super.getById(uuid);
if (justAuthUser != null) {
justAuthUser.setToken(justAuthTokenCache.getByUuid(uuid));
}
return justAuthUser;
}
/**
* 根据用户uuid移除信息
*
* @param uuid 用户uuid
* @return
*/
@Override
public boolean removeByUuid(String uuid) {
justAuthTokenCache.remove(uuid);
return super.removeById(uuid);
}
}
(10) 创建domain包,并创建ResultCode.java文件
ResultCode.java文件中内容:
import com.fasterxml.jackson.annotation.JsonFormat;
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ResultCode {
/*
请求返回状态码和说明信息
*/
SUCCESS(200, "成功"),
BAD_REQUEST(400, "参数或者语法不对"),
UNAUTHORIZED(401, "认证失败"),
LOGIN_ERROR(401, "登陆失败,用户名或密码无效"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "请求的资源不存在"),
OPERATE_ERROR(405, "操作失败,请求操作的资源不存在"),
TIME_OUT(408, "请求超时"),
SERVER_ERROR(500, "服务器内部错误"),
;
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public void setCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
(11) 在domain包中创建PageResult.java文件
PageResult.java文件中内容:
public class PageResult {
private int page;
private int rows;
private int total;
private Object data;
public PageResult(int page, int rows, int total, Object data) {
this.page = page;
this.rows = rows;
this.total = total;
this.data = data;
}
public void setPage(int page) {
this.page = page;
}
public int getPage() {
return page;
}
public void setRows(int rows) {
this.rows = rows;
}
public int getRows() {
return rows;
}
public void setTotal(int total) {
this.total = total;
}
public int getTotal() {
return total;
}
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
}
(12) 在domain包中创建ResultJson.java文件
ResultJson.java文件中内容:
public class ResultJson {
private int code;
private String msg;
private Object data;
public static ResultJson Success() {
return Success("");
}
public static ResultJson Success(Object o) {
return new ResultJson(ResultCode.SUCCESS, o);
}
public static ResultJson Failure(Object o) {
return new ResultJson(ResultCode.BAD_REQUEST, o);
}
public static ResultJson Failure(ResultCode code) {
return Failure(code, "");
}
public static ResultJson Failure(ResultCode code, Object o) {
return new ResultJson(code, o);
}
public ResultJson (ResultCode resultCode) {
setResultCode(resultCode);
}
public ResultJson (ResultCode resultCode,Object data) {
setResultCode(resultCode);
this.data = data;
}
public void setResultCode(ResultCode resultCode) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
}
public void setCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public ResultJson(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
@Override
public String toString() {
return "{" +
"\"code\":" + code +
", \"msg\":\"" + msg + '\"' +
", \"data\":\"" + data + '\"'+
'}';
}
}
(13) 创建controller包,并创建AuthController.java文件
AuthController.java文件中内容:
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSON;
import com.xkcoding.justauth.AuthRequestFactory;
import Entity.JustAuthUser;
import domain.ResultCode;
import domain.ResultJson;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import service.JustAuthUserService;
@RestController
@RequestMapping("/oauth")
public class AuthController {
Logger log = LoggerFactory.getLogger(AuthController.class);
@Autowired
private AuthRequestFactory factory;
@Autowired
private JustAuthUserService justAuthUserService;
/**
* 登录
*
* @param type 第三方系统类型,例如:gitee/baidu
* @param response
* @throws IOException
*/
@GetMapping("/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}
/**
* 登录回调
*
* @param type 第三方系统类型,例如:gitee/baidu
* @param callback
* @return
*/
@SuppressWarnings("unchecked")
@RequestMapping("/{type}/callback")
public ResultJson login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse<AuthUser> response = authRequest.login(callback);
log.info("【response】= {}", JSON.toJSONString(response));
if (response.ok()) {
justAuthUserService.saveOrUpdate(new JustAuthUser(response.getData()));
return ResultJson.Success(JSON.toJSONString(response));
}
return ResultJson.Failure(response.getMsg());
}
/**
* 收回
*
* @param type 第三方系统类型,例如:gitee/baidu
* @param uuid 用户uuid
* @return
*/
@SuppressWarnings("unchecked")
@RequestMapping("/revoke/{type}/{uuid}")
public ResultJson revoke(@PathVariable String type, @PathVariable String uuid) {
AuthRequest authRequest = factory.get(type);
JustAuthUser justAuthUser = justAuthUserService.getByUuid(uuid);
if (null == justAuthUser) {
return ResultJson.Failure("用户不存在");
}
AuthResponse<AuthToken> response = null;
try {
response = authRequest.revoke(justAuthUser.getToken());
if (response.ok()) {
justAuthUserService.removeByUuid(justAuthUser.getUuid());
return ResultJson.Success("用户 [" + justAuthUser.getUsername() + "] 的 授权状态 已收回!");
}
return ResultJson.Failure("用户 [" + justAuthUser.getUsername() + "] 的 授权状态 收回失败!" + response.getMsg());
} catch (Exception e) {
return ResultJson.Failure(ResultCode.BAD_REQUEST);
}
}
/**
* 刷新
*
* @param type 第三方系统类型,例如:gitee/baidu
* @param uuid 用户uuid
* @return
*/
@SuppressWarnings("unchecked")
@RequestMapping("/refresh/{type}/{uuid}")
@ResponseBody
public ResultJson refresh(@PathVariable String type, @PathVariable String uuid) {
AuthRequest authRequest = factory.get(type);
JustAuthUser justAuthUser = justAuthUserService.getByUuid(uuid);
if (null == justAuthUser) {
return ResultJson.Failure("用户不存在");
}
AuthResponse<AuthToken> response = null;
try {
response = authRequest.refresh(justAuthUser.getToken());
if (response.ok()) {
justAuthUser.setToken(response.getData());
justAuthUserService.saveOrUpdate(justAuthUser);
return ResultJson.Success("用户 [" + justAuthUser.getUsername() + "] 的 access token 已刷新!新的 accessToken: "
+ response.getData().getAccessToken());
}
return ResultJson.Failure("用户 [" + justAuthUser.getUsername() + "] 的 access token 刷新失败!" + response.getMsg());
} catch (Exception e) {
return ResultJson.Failure(ResultCode.BAD_REQUEST);
}
}
}
(14) 主启动目录中内容
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,service,controller")
@MapperScan("Dao")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
(15) 测试验证
[登录]:
浏览器访问地址:http://127.0.0.1:8443/oauth/login/baidu,即可获取到对应信息。
请观察数据库及Redis中数据变化。
[刷新Token]:
浏览器访问地址:http://127.0.0.1:8443/oauth/refresh/baidu/[uuid],其中uuid为数据库表中的uuid。
目前插件中已实现的可刷新的第三方应用有限,请查看AuthRequest.refresh方法的实现类。
请观察Redis中数据变化。
[移除Token]:
浏览器访问地址:http://127.0.0.1:8443/oauth/revoke/baidu/[uuid],其中uuid为数据库表中的uuid。
目前插件中已实现的可刷新的第三方应用有限,请查看AuthRequest.revoke方法的实现类。
请观察数据库及Redis中数据变化。
48. Springboot集成Xjar程序加密
Xjar基于对jar包内资源的加密以及拓展ClassLoader来构建的一套程序加密启动,动态解密运行的方案,避免源码泄露或反编译。它不需要侵入代码,只需要把编译好的jar包通过工具加密即可。
(1) 依赖包
<repositories>
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.core-lib</groupId>
<artifactId>xjar</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
</dependencies>
(2) 创建test.java文件
test.java文件中内容:
import io.xjar.XCryptos;
public class test {
public static void encrypt() throws Exception {
XCryptos.encryption()
//项目生成的jar
.from("F://Java//Test01//SpringStart.jar")
// 加密的密码
.use("testaa1111122222")
.include("/**/*.class")
.include("/**/*.xml")
.include("/**/*.yml")
.include("/**/*.properties")
.to("F://Java//Test01//test.jar");
}
public static void main(String[] args) throws Exception {
encrypt();
}
}
(3) 将生成文件xjar.go放入GoLang语言环境中编译
go bulid xjar.go
注:
xjar.go在Windows生成xjar.exe文件,在Linux生成xjar文件
(4) 启动Springboot项目
Windows启动:
xjar.exe java -jar test.jar
Linux启动:
nohup ./xjar java -jar test.jar
49. Springboot集成Log4j2操作Sentry异常监控系统
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions><!-- 去掉springboot默认配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-log4j2</artifactId>
<version>1.7.23</version>
</dependency>
<dependency> <!-- 引入log4j2依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
logging.config=classpath:log4j2.xml
(3) 在resources资源文件夹中创建sentry.properties文件
sentry.properties文件中内容:
stacktrace.app.packages=
dsn=http://edb111ae20f6464baf358d1b0c2662ae@192.168.0.142:9000/17
(4) 在resources资源文件夹中创建log4j2.xml文件
log4j2.xml文件中内容:
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="warn" packages="org.apache.logging.log4j.core,io.sentry.log4j2">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<Sentry name="Sentry"/>
</appenders>
<loggers>
<root level="INFO">
<appender-ref ref="Console" />
<!-- Note that the Sentry logging threshold is overridden to the WARN level -->
<appender-ref ref="Sentry" level="WARN" />
</root>
</loggers>
</configuration>
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() throws Exception {
logger.error("测试sentry打印error日志");
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
50. Springboot集成Logback操作Sentry异常监控系统
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-logback</artifactId>
<version>1.7.23</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
logging.config=classpath:logback.xml
(3) 在resources资源文件夹中创建sentry.properties文件
sentry.properties文件中内容:
stacktrace.app.packages=
dsn=http://edb111ae20f6464baf358d1b0c2662ae@192.168.0.142:9000/17
(4) 在resources资源文件夹中创建logback.xml文件
logback.xml文件中内容:
<configuration>
<!-- Configure the Console appender -->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Configure the Sentry appender, overriding the logging threshold to the WARN level -->
<appender name="Sentry" class="io.sentry.logback.SentryAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
</appender>
<!-- Enable the Console and Sentry appenders, Console is provided as an example
of a non-Sentry logger that is set to a different logging threshold -->
<root level="INFO">
<appender-ref ref="Console" />
<appender-ref ref="Sentry" />
</root>
</configuration>
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() throws Exception {
logger.error("测试sentry打印error日志");
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
51. Springboot集成LucySheet在线协同
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
(2) 创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.freemarker.request-context-attribute=request
spring.freemarker.suffix=.html
spring.freemarker.content-type=text/html
spring.resources.static-locations=classpath:/resources/static/
spring.freemarker.enabled=true
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.allow-request-override=false
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
spring.data.mongodb.uri=mongodb://127.0.0.1:8892/wb?authSource=admin&readPreference=primary&ssl=false
(3) 在resuorces资源文件夹中创建templates文件夹,并创建websocket.html文件
websocket.html文件中内容:
<!DOCTYPE html>
<html>
<head lang='zh'>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="renderer" content="webkit"/>
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=0"/>
<title>websocket--luckysheet</title>
<link rel='stylesheet' href='/lib/plugins/css/pluginsCss.css'/>
<link rel='stylesheet' href='/lib/plugins/plugins.css'/>
<link rel='stylesheet' href='/lib/css/luckysheet.css'/>
<link rel='stylesheet' href='/lib/assets/iconfont/iconfont.css'/>
<script src="/lib/plugins/js/plugin.js"></script>
<script src="/lib/luckysheet.umd.js"></script>
</head>
<body>
<div id="${wb.option.container}"
style="margin:0px;padding:0px;position:absolute;width:100%;height:100%;left: 0px;top: 0px;"></div>
</div>
</body>
<script>
var localurl = "//" + window.location.host;
// 配置项
var options = {
container: "${wb.option.container}", // 设定DOM容器的id
title: "${wb.option.title}", // 设定表格名称
lang: "${wb.option.lang}",
allowUpdate: true,
showinfobar: true,//作用:是否显示顶部信息栏
functionButton: '<button id="" class="btn btn-primary" onclick="clicks()" style="padding:3px 6px;font-size: 12px;margin-right: 10px;">保存</button> <button id="" class="btn btn-primary btn-danger" style=" padding:3px 6px; font-size: 12px; margin-right: 85px;" onclick="downExcelData()">下载</button>',
loadUrl: window.location.protocol + localurl + "/load/${wb.id}",
loadSheetUrl: window.location.protocol + localurl + "/loadSheet/${wb.id}",
updateUrl: "ws://"+localurl + "/ws/" + Math.round(Math.random() * 100) + "/${wb.id}"
// 更多其他设置...
}
// 初始化表格
luckysheet.create(options)
function uploadExcelData() {
//console.log(luckysheet.getAllSheets());
//console.log("lll=" + JSON.stringify(luckysheet.getAllSheets()));
//上传例子,可以把这个数据保存到服务器上。下次可以从服务器直接加载luckysheet数据了。
$.post("/excel/uploadData", {
exceldatas: JSON.stringify(luckysheet.getAllSheets()),
title: options.title,
}, function (data) {
//console.log("data = " + data)
alert("保存成功!")
});
}
function downExcelData() {
//这里你要自己写个后台接口,处理上传上来的Excel数据,用post传输。我用的是Java后台处理导出!这里只是写了post请求的写法
$.post("/excel/downfile", {
exceldatas: JSON.stringify(luckysheet.getAllSheets()),
}, function (data) {
//console.log("data = " + data)
});
}
</script>
</html>
(4) 创建资源文件夹static文件夹,将LuckySheet相关资源放入
(5) 创建utils包,并创建PakoGzipUtils.java文件
PakoGzipUtils.java文件中内容:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class PakoGzipUtils {
public static String compress(String str) throws IOException {
if (str == null || str.length() == 0) {
return str;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(out);
gzip.write(str.getBytes());
gzip.close();
return out.toString("ISO-8859-1");
}
/**
* @param str:
* @return 解压字符串 生成正常字符串。
* @throws IOException
*/
public static String uncompress(String str) throws IOException {
if (str == null || str.length() == 0) {
return str;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(str.getBytes("ISO-8859-1"));
GZIPInputStream gunzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = gunzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
// toString()使用平台默认编码,也可以显式的指定如toString("GBK")
return out.toString();
}
/**
* @param jsUriStr :字符串类型为
* @return 生成正常字符串
* @throws IOException
*/
public static String unCompressURI(String jsUriStr) throws IOException {
String gzipCompress=uncompress(jsUriStr);
String result=URLDecoder.decode(gzipCompress,"UTF-8");
return result;
}
/**
* @param strData :字符串类型为: 正常字符串
* @return 生成字符串类型为:
*/
public static String compress2URI(String strData) throws IOException {
String encodeGzip=compress(strData);
String jsUriStr=URLEncoder.encode(encodeGzip,"UTF-8");
return jsUriStr;
}
}
(6) 在utils包中创建SheetUtil.java文件
SheetUtil.java文件中内容:
import java.util.ArrayList;
import java.util.List;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONObject;
public class SheetUtil {
/**
* 获取sheet的默认option
* @return
*/
public static JSONObject getDefautOption() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("container", "ecsheet");
jsonObject.put("title", "ecsheet demo");
jsonObject.put("lang", "zh");
jsonObject.put("allowUpdate", true);
jsonObject.put("loadUrl", "");
jsonObject.put("loadSheetUrl", "");
jsonObject.put("updateUrl", "");
return jsonObject;
}
/**
* 获取默认的sheetData
* @return
*/
public static List<JSONObject> getDefaultSheetData() {
List<JSONObject> list = new ArrayList<>();
for (int i = 1; i < 4; i++) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("row", 84);
jsonObject.put("column", 60);
jsonObject.put("name", "sheet" + i);
Integer index = i - 1;
jsonObject.put("index", IdUtil.simpleUUID());
jsonObject.put("order", i - 1);
if (i == 1) {
jsonObject.put("status", 1);
} else {
jsonObject.put("status", 0);
}
jsonObject.put("celldata", new ArrayList<JSONObject>() {
});
list.add(jsonObject);
}
return list;
}
/**
* 获取默认的全部sheetData
* @return
*/
public static JSONObject getDefaultAllSheetData() {
JSONObject result = new JSONObject();
for (int i = 1; i < 4; i++) {
JSONObject data = new JSONObject();
data.put("r", 0);
data.put("c", 0);
data.put("v", new JSONObject());
result.put("sheet" + i, data);
}
return result;
}
/**
* 组装异步加载sheet所需的数据
*
* @param data
* @return
*/
public static JSONObject buildSheetData(List<JSONObject> data) {
JSONObject result = new JSONObject();
data.forEach((d) -> {
result.put(d.get("index").toString(), d.get("celldata"));
});
return result;
}
}
(7) 创建service包,并创建IMessageProcess.java文件
IMessageProcess.java文件中内容:
import cn.hutool.json.JSONObject;
public interface IMessageProcess {
void process(String gridKey,JSONObject message);
}
(8) 在service包中创建MessageProcess.java文件
MessageProcess.java文件中内容:
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import entity.WorkBookEntity;
import entity.WorkSheetEntity;
import repository.WorkBookRepository;
import repository.WorkSheetRepository;
@Service
public class MessageProcess implements IMessageProcess {
@Autowired
private WorkSheetRepository workSheetRepository;
@Autowired
private WorkBookRepository workBookRepository;
@Override
public void process(String wbId, JSONObject message) {
//获取操作名
String action = message.getStr("t");
//获取sheet的index值
String index = message.getStr("i");
//如果是复制sheet,index的值需要另取
if ("shc".equals(action)) {
index = message.getJSONObject("v").getStr("copyindex");
}
//如果是删除sheet,index的值需要另取
if ("shd".equals(action)) {
index = message.getJSONObject("v").getStr("deleIndex");
}
//如果是恢复sheet,index的值需要另取
if ("shre".equals(action)) {
index = message.getJSONObject("v").getStr("reIndex");
}
WorkSheetEntity ws = workSheetRepository.findByindexAndwbId(index, wbId);
System.out.println("操作:"+action);
switch (action) {
//单个单元格刷新
case "v":
ws = singleCellRefresh(ws, message);
break;
//范围单元格刷新
case "rv":
ws = rangeCellRefresh(ws, message);
break;
//config操作
case "cg":
ws = configRefresh(ws, message);
break;
//通用保存
case "all":
ws = allRefresh(ws, message);
break;
//函数链操作
case "fc":
ws = calcChainRefresh(ws, message);
break;
//删除行或列
case "drc":
ws = drcRefresh(ws, message);
break;
//增加行或列
case "arc":
ws = arcRefresh(ws, message);
break;
//清除筛选
case "fsc":
ws = fscRefresh(ws, message);
break;
//恢复筛选
case "fsr":
ws = fscRefresh(ws, message);
break;
//新建sheet
case "sha":
ws = shaRefresh(wbId, message);
break;
//切换到指定sheet
case "shs":
shsRefresh(wbId, message);
break;
//复制sheet
case "shc":
ws = shcRefresh(ws, message);
break;
//修改工作簿名称
case "na":
naRefresh(wbId, message);
break;
//删除sheet
case "shd":
ws.setDeleteStatus(1);
break;
//删除sheet后恢复操作
case "shre":
ws.setDeleteStatus(0);
break;
//调整sheet位置
case "shr":
shrRefresh(wbId, message);
break;
//sheet属性(隐藏或显示)
case "sh":
ws = shRefresh(ws, message);
break;
default:
break;
}
if (ObjectUtil.isNull(ws)) {
return;
}
workSheetRepository.save(ws);
}
/**
* 单个单元格刷新
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity singleCellRefresh(WorkSheetEntity ws, JSONObject message) {
//对celldata进行深拷贝
JSONArray celldata = ObjectUtil.cloneByStream(ws.getData().getJSONArray("celldata"));
if (StrUtil.isBlank(message.getStr("v"))) {
celldata.forEach(c -> {
JSONObject jsonObject = JSONUtil.parseObj(c);
if (!jsonObject.isEmpty()) {
if (jsonObject.getLong("r") == message.getLong("r") && jsonObject.getLong("c") == message.getLong("c")) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
}
}
});
} else {
JSONObject collectData = JSONUtil.createObj().put("r", message.getLong("r")).put("c", message.getLong("c")).put("v", message.getJSONObject("v"));
List<String> flag = new ArrayList<>();
celldata.forEach(c -> {
JSONObject jsonObject = JSONUtil.parseObj(c);
if (!jsonObject.isEmpty()) {
if (jsonObject.getLong("r") == message.getLong("r") && jsonObject.getLong("c") == message.getLong("c")) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
ws.getData().getJSONArray("celldata").add(collectData);
flag.add("used");
}
}
});
if (flag.isEmpty()) {
ws.getData().getJSONArray("celldata").add(collectData);
}
}
return ws;
}
/**
* 范围单元格刷新
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity rangeCellRefresh(WorkSheetEntity ws, JSONObject message) {
JSONArray rowArray = message.getJSONObject("range").getJSONArray("row");
JSONArray columnArray = message.getJSONObject("range").getJSONArray("column");
JSONArray vArray = message.getJSONArray("v");
JSONArray celldata = ObjectUtil.cloneByStream(ws.getData().getJSONArray("celldata"));
int countRowIndex = 0;
//遍历行列,对符合行列的内容进行更新
for (int ri = (int) rowArray.get(0); ri <= (int) rowArray.get(1); ri++) {
int countColumnIndex = 0;
for (int ci = (int) columnArray.get(0); ci <= (int) columnArray.get(1); ci++) {
List<String> flag = new ArrayList<>();
Object newCell = JSONUtil.parseArray(vArray.get(countRowIndex)).get(countColumnIndex);
JSONObject collectData = JSONUtil.createObj().put("r", ri).put("c", ci).put("v", newCell);
int rowIndex = ri;
int columnIndex = ci;
celldata.forEach(cell -> {
JSONObject jsonObject = JSONUtil.parseObj(cell);
if (!jsonObject.isEmpty()) {
if (jsonObject.getInt("r") == rowIndex && jsonObject.getInt("c") == columnIndex) {
if ("null".equals(newCell.toString()) || JSONUtil.parseObj(newCell).isEmpty()) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
} else {
ws.getData().getJSONArray("celldata").remove(jsonObject);
ws.getData().getJSONArray("celldata").add(collectData);
}
flag.add("used");
}
}
});
if (flag.isEmpty() && !JSONUtil.parseObj(newCell).isEmpty()) {
ws.getData().getJSONArray("celldata").add(collectData);
}
countColumnIndex++;
}
countRowIndex++;
}
return ws;
}
/**
* config更新
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity configRefresh(WorkSheetEntity ws, JSONObject message) {
JSONObject v = message.getJSONObject("v");
JSONObject newConfig = JSONUtil.createObj().put(message.getStr("k"), v);
System.out.println(ws.getData());
if(ws.getData().getJSONObject("config") != null) {
if (ws.getData().getJSONObject("config").isEmpty()) {
ws.getData().put("config", newConfig);
} else {
ws.getData().getJSONObject("config").put(message.getStr("k"), v);
}
}
return ws;
}
/**
* 通用保存
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity allRefresh(WorkSheetEntity ws, JSONObject message) {
if (message.getJSONObject("v").isEmpty()) {
ws.getData().remove(message.getStr("k"));
} else {
ws.getData().put(message.getStr("k"), message.getJSONObject("v"));
}
return ws;
}
/**
* 函数链操作
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity calcChainRefresh(WorkSheetEntity ws, JSONObject message) {
JSONObject value = message.getJSONObject("v");
if (!ws.getData().containsKey("calcChain")) {
ws.getData().put("calcChain", new JSONArray());
}
JSONArray calcChain = ws.getData().getJSONArray("calcChain");
if ("add".equals(message.getStr("op"))) {
calcChain.add(value);
} else if ("update".equals(message.getStr("op"))) {
calcChain.remove(calcChain.get(message.getInt(message.getStr("pos"))));
calcChain.add(value);
} else if ("del".equals(message.getStr("op"))) {
calcChain.remove(calcChain.get(message.getInt(message.getStr("pos"))));
}
return ws;
}
/**
* 删除行或列
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity drcRefresh(WorkSheetEntity ws, JSONObject message) {
JSONArray celldata = ObjectUtil.cloneByStream(ws.getData().getJSONArray("celldata"));
int index = message.getJSONObject("v").getInt("index");
int len = message.getJSONObject("v").getInt("len");
if ("r".equals(message.getStr("rc"))) {
ws.getData().put("row", ws.getData().getInt("row") - len);
} else {
ws.getData().put("column", ws.getData().getInt("column") - len);
}
for (Object cell : celldata) {
JSONObject jsonObject = JSONUtil.parseObj(cell);
if ("r".equals(message.getStr("rc"))) {
//删除行所在区域的内容
if (jsonObject.getInt("r") >= index && jsonObject.getInt("r") < index + len) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
}
//增加大于 最大删除行的的行号
if (jsonObject.getInt("r") >= index + len) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("r", jsonObject.getInt("r") - len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
} else {
//删除列所在区域的内容
if (jsonObject.getInt("c") >= index && jsonObject.getInt("c") < index + len) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
}
//增加大于 最大删除列的的列号
if (jsonObject.getInt("c") >= index + len) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("c", jsonObject.getInt("c") - len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
}
}
return ws;
}
/**
* 增加行或列,暂未实现插入数据的情况
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity arcRefresh(WorkSheetEntity ws, JSONObject message) {
JSONArray celldata = ObjectUtil.cloneByStream(ws.getData().getJSONArray("celldata"));
int index = message.getJSONObject("v").getInt("index");
int len = message.getJSONObject("v").getInt("len");
for (Object cell : celldata) {
JSONObject jsonObject = JSONUtil.parseObj(cell);
if ("r".equals(message.getStr("rc"))) {
//如果是增加行,且是向左增加
if (jsonObject.getInt("r") >= index && "lefttop".equals(message.getJSONObject("v").getStr("direction"))) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("r", jsonObject.getInt("r") + len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
//如果是增加行,且是向右增加
if (jsonObject.getInt("r") > index && "rightbottom".equals(message.getJSONObject("v").getStr("direction"))) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("r", jsonObject.getInt("r") + len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
} else {
//如果是增加列,且是向上增加
if (jsonObject.getInt("c") >= index && "lefttop".equals(message.getJSONObject("v").getStr("direction"))) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("c", jsonObject.getInt("c") + len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
//如果是增加列,且是向下增加
if (jsonObject.getInt("c") > index && "rightbottom".equals(message.getJSONObject("v").getStr("direction"))) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("c", jsonObject.getInt("c") + len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
}
}
JSONArray vArray = message.getJSONObject("v").getJSONArray("data");
if ("r".equals(message.getStr("rc"))) {
ws.getData().put("row", ws.getData().getInt("row") + len);
for (int r = 0; r < vArray.size(); r++) {
for (int c = 0; c < JSONUtil.parseArray(vArray.get(0)).size(); c++) {
if (JSONUtil.parseArray(vArray.get(r)).get(c) == null) {
continue;
}
JSONObject newCell = JSONUtil.createObj().put("r", r + index).put("c", c).put("v", JSONUtil.parseArray(vArray.get(r)).get(c));
ws.getData().getJSONArray("celldata").add(newCell);
}
}
} else {
ws.getData().put("column", ws.getData().getInt("column") + len);
for (int r = 0; r < vArray.size(); r++) {
for (int c = 0; c < JSONUtil.parseArray(vArray.get(0)).size(); c++) {
if (JSONUtil.parseArray(vArray.get(r)).get(c) == null) {
continue;
}
JSONObject newCell = JSONUtil.createObj().put("r", r).put("c", c + index).put("v", JSONUtil.parseArray(vArray.get(r)).get(c));
ws.getData().getJSONArray("celldata").add(newCell);
}
}
}
return ws;
}
/**
* 筛选操作
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity fscRefresh(WorkSheetEntity ws, JSONObject message) {
if (message.getJSONObject("v").isEmpty()) {
ws.getData().remove("filter");
ws.getData().remove("filter_select");
} else {
ws.getData().put("filter", message.getJSONObject("v").getJSONArray("filter"));
ws.getData().put("filter_select", message.getJSONObject("v").getJSONObject("filter_select"));
}
return ws;
}
/**
* 新建sheet
*
* @param wbId
* @param message
* @return
*/
private WorkSheetEntity shaRefresh(String wbId, JSONObject message) {
WorkSheetEntity ws = new WorkSheetEntity();
ws.setWbId(wbId);
ws.setData(message.getJSONObject("v"));
return ws;
}
/**
* 复制sheet
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity shcRefresh(WorkSheetEntity ws, JSONObject message) {
String index = message.getStr("i");
ws.getData().put("index", index);
ws.getData().put("name", message.getJSONObject("v").getStr("name"));
return ws;
}
/**
* 调整sheet位置
*
* @param wbId
* @param message
*/
private void shrRefresh(String wbId, JSONObject message) {
List<WorkSheetEntity> allSheets = workSheetRepository.findAllBywbId(wbId);
allSheets.forEach(sheet -> {
sheet.getData().put("order", message.getJSONObject("v").getInt(sheet.getData().getStr("index")));
workSheetRepository.save(sheet);
});
}
/**
* 切换到指定sheet
*
* @param ws
* @param message
* @return
*/
private void shsRefresh(String wbId, JSONObject message) {
WorkSheetEntity lastWs = workSheetRepository.findBystatusAndwbId(1, wbId);
lastWs.getData().put("status", 0);
WorkSheetEntity thisWs = workSheetRepository.findByindexAndwbId(message.getStr("v"), wbId);
thisWs.getData().put("status", 1);
workSheetRepository.save(lastWs);
workSheetRepository.save(thisWs);
}
/**
* sheet属性(隐藏或显示)
*
* @param wbId
* @param message
*/
private WorkSheetEntity shRefresh(WorkSheetEntity ws, JSONObject message) {
Integer hideStatus = message.getInt("v");
ws.getData().put("hide", hideStatus);
WorkSheetEntity curWs = new WorkSheetEntity();
if ("hide".equals(message.getStr("op"))) {
ws.getData().put("status", 0);
String cur = message.getStr("cur");
curWs = workSheetRepository.findByindexAndwbId(cur, ws.getWbId());
curWs.getData().put("status", 1);
} else {
curWs = workSheetRepository.findBystatusAndwbId(1, ws.getWbId());
curWs.getData().put("status", 0);
}
workSheetRepository.save(curWs);
return ws;
}
/**
* 修改工作簿名称
*
* @param wbId
* @param message
* @return
*/
private void naRefresh(String wbId, JSONObject message) {
Optional<WorkBookEntity> wb = workBookRepository.findById(wbId);
if (wb.isPresent()) {
WorkBookEntity workBookEntity = wb.get();
workBookEntity.getOption().put("title", message.getStr("v"));
workBookRepository.save(workBookEntity);
}
}
}
(9) 创建common包,并创建ResponseDTO.java文件
ResponseDTO.java文件中内容:
public class ResponseDTO {
private Integer type;
private String id;
private String username;
private String data;
public ResponseDTO(Integer type, String id, String username, String data) {
this.type = type;
this.id = id;
this.username = username;
this.data = data;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public static ResponseDTO success(String id,String username,String data) {
return new ResponseDTO(1,id,username, data);
}
public static ResponseDTO update(String id,String username,String data) {
return new ResponseDTO(2,id,username, data);
}
public static ResponseDTO mv(String id,String username,String data) {
return new ResponseDTO(3,id,username, data);
}
public static ResponseDTO bulkUpdate(String id,String username,String data) {
return new ResponseDTO(4,id,username ,data);
}
}
(10) 创建entity包,并创建WorkBookEntity.java文件
WorkBookEntity.java文件中内容:
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import cn.hutool.json.JSONObject;
@Document(collection = "workbook")
public class WorkBookEntity {
@Id
private String id;
private String name;
private JSONObject option;
public WorkBookEntity() {
}
public WorkBookEntity(String id, String name, JSONObject option) {
this.id = id;
this.name = name;
this.option = option;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public JSONObject getOption() {
return option;
}
public void setOption(JSONObject option) {
this.option = option;
}
}
(11) 在entity包中创建WorkSheetEntity.java文件
WorkSheetEntity.java文件中内容:
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import cn.hutool.json.JSONObject;
@Document(collection = "worksheet")
public class WorkSheetEntity {
@Id
private String id;
private String wbId;
private JSONObject data;
/**
* 删除标记,0是未删除,1是删除
*/
private int deleteStatus;
public WorkSheetEntity() {
}
public WorkSheetEntity(String id, String wbId, JSONObject data, int deleteStatus) {
this.id = id;
this.wbId = wbId;
this.data = data;
this.deleteStatus = deleteStatus;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getWbId() {
return wbId;
}
public void setWbId(String wbId) {
this.wbId = wbId;
}
public JSONObject getData() {
return data;
}
public void setData(JSONObject data) {
this.data = data;
}
public int getDeleteStatus() {
return deleteStatus;
}
public void setDeleteStatus(int deleteStatus) {
this.deleteStatus = deleteStatus;
}
}
(12) 创建repository包,并创建WorkBookRepository.java文件
WorkBookRepository.java文件中内容:
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import entity.*;
@Repository
public interface WorkBookRepository extends MongoRepository<WorkBookEntity,String> {
}
(13) 在repository包中创建WorkSheetRepository.java文件
WorkSheetRepository.java文件中内容:
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;
import entity.*;
import java.util.List;
@Repository
public interface WorkSheetRepository extends MongoRepository<WorkSheetEntity,String> {
@Query(value = "{'wbId':?0,'deleteStatus':0}")
List<WorkSheetEntity> findAllBywbId(String wbId);
@Query(value = "{'data.index':?0,'wbId':?1}")
WorkSheetEntity findByindexAndwbId(String index,String wbId);
@Query(value = "{'data.status':?0,'wbId':?1}")
WorkSheetEntity findBystatusAndwbId(int status,String wbId);
}
(14) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import com.alibaba.fastjson.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import entity.*;
import repository.*;
import utils.*;
@CrossOrigin
@RestController
public class controller {
@Autowired
private WorkBookRepository workBookRepository;
@Autowired
private WorkSheetRepository workSheetRepository;
@GetMapping("index/create")
public void create(HttpServletRequest request, HttpServletResponse response) throws IOException {
WorkBookEntity wb = new WorkBookEntity();
wb.setName("default");
wb.setOption(SheetUtil.getDefautOption());
WorkBookEntity saveWb = workBookRepository.save(wb);
//生成sheet数据
generateSheet(saveWb.getId());
response.sendRedirect("/index/" + saveWb.getId());
}
@GetMapping("/index/{wbId}")
public ModelAndView index(@PathVariable(value = "wbId") String wbId) {
Optional<WorkBookEntity> Owb = workBookRepository.findById(wbId);
WorkBookEntity wb = new WorkBookEntity();
if (!Owb.isPresent()) {
wb.setId(wbId);
wb.setName("default");
wb.setOption(SheetUtil.getDefautOption());
WorkBookEntity result = workBookRepository.save(wb);
generateSheet(wbId);
} else {
wb = Owb.get();
}
return new ModelAndView("websocket", "wb", wb);
}
@PostMapping("/load/{wbId}")
public String load(@PathVariable(value = "wbId") String wbId) {
List<WorkSheetEntity> wsList = workSheetRepository.findAllBywbId(wbId);
List<JSONObject> list = new ArrayList<JSONObject>();
wsList.forEach(ws -> {
list.add(ws.getData());
});
return JSONUtil.toJsonStr(list);
}
@PostMapping("/loadSheet/{wbId}")
public String loadSheet(@PathVariable(value = "wbId") String wbId) {
List<WorkSheetEntity> wsList = workSheetRepository.findAllBywbId(wbId);
List<JSONObject> list = new ArrayList<>();
wsList.forEach(ws -> {
list.add(ws.getData());
});
if (!list.isEmpty()) {
return SheetUtil.buildSheetData(list).toString();
}
return SheetUtil.getDefaultAllSheetData().toString();
}
private void generateSheet(String wbId) {
SheetUtil.getDefaultSheetData().forEach(jsonObject -> {
WorkSheetEntity ws = new WorkSheetEntity();
ws.setWbId(wbId);
ws.setData(jsonObject);
ws.setDeleteStatus(0);
workSheetRepository.save(ws);
});
}
}
(15) 创建config包,并创建WebSocketConfig.java文件
WebSocketConfig.java文件中内容:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(16) 在controller包中创建WebSocketServer.java文件
WebSocketServer.java文件中内容:
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.PostConstruct;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import common.*;
import service.*;
import utils.*;
@ServerEndpoint("/ws/{userId}/{gridKey}")
@Component
public class WebSocketServer {
static Log log = LogFactory.get(WebSocketServer.class);
private static WebSocketServer webSocketServer;
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, Map<String, WebSocketServer>> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = "";
/**
* 表格主键
*/
private String gridKey = "";
@Autowired
private IMessageProcess messageProcess;
@PostConstruct //通过@PostConstruct实现初始化bean之前进行的操作
public void init() {
webSocketServer = this;
webSocketServer.messageProcess = this.messageProcess;
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId, @PathParam("gridKey") String gridKey) {
this.session = session;
this.userId = userId;
this.gridKey = gridKey;
if (webSocketMap.containsKey(gridKey)) {
webSocketMap.get(gridKey).put(userId, this);
} else {
Map<String, WebSocketServer> map = new HashMap<>();
map.put(userId, this);
webSocketMap.put(gridKey, map);
}
addOnlineCount();
log.info("用户连接:" + userId + ",打开的表格为:" + gridKey + ",当前在线人数为:" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(this.gridKey)) {
webSocketMap.get(this.gridKey).remove(this.userId);
if (webSocketMap.get(this.gridKey).isEmpty()) {
webSocketMap.remove(this.gridKey);
}
}
subOnlineCount();
log.info("用户退出:" + this.userId + ",打开的表格为:" + this.gridKey + ",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
//可以群发消息
//消息保存到数据库、redis
if (StrUtil.isNotBlank(message)) {
try {
if ("rub".equals(message)) {
return;
}
String unMessage = PakoGzipUtils.unCompressURI(message);
log.info("用户消息:" + userId + ",报文:" + unMessage);
JSONObject jsonObject = JSONUtil.parseObj(unMessage);
if (!"mv".equals(jsonObject.getStr("t"))) {
webSocketServer.messageProcess.process(this.gridKey, jsonObject);
}
Map<String, WebSocketServer> sessionMap = webSocketMap.get(this.gridKey);
if (StrUtil.isNotBlank(unMessage)) {
sessionMap.forEach((key, value) -> {
//广播到除了发送者外的其它连接端
if (!key.equals(this.userId)) {
try {
//如果是mv,代表发送者的表格位置信息
if ("mv".equals(jsonObject.getStr("t"))) {
value.sendMessage(JSONUtil.toJsonStr(ResponseDTO.mv(userId, userId, unMessage)));
//如果是切换sheet,则不发送信息
} else if(!"shs".equals(jsonObject.getStr("t"))) {
value.sendMessage(JSONUtil.toJsonStr(ResponseDTO.update(userId, userId, unMessage)));
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException, EncodeException {
this.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
(17) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
@EnableAutoConfiguration
@ComponentScan("config,service,controller")
@EnableMongoRepositories(basePackages = {"repository"})
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
11. Springboot集成Zipkin分布式监控
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
#服务追踪
spring.zipkin.service.name=zipkin-test
spring.zipkin.base-url=http://127.0.0.1:9411/
spring.zipkin.discovery-client-enabled=true
spring.zipkin.sender.type=web
spring.sleuth.sampler.probability=1
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index1() throws Exception {
return "成功";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
12. Eclipse安装SpringBoot插件创建项目01. Springboot(2.0.2)中集成SpringBootAdmin和Eureka监控服务
注册中心可以是Eureka、Consul、Dubbo等
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=eurekaServer
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
#展示全部细节信息
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableAutoConfiguration
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.SpringbootAdmin服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8311
spring.application.name=adminServer
eureka.client.registryFetchIntervalSeconds=5
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
eureka.instance.leaseRenewalIntervalInSeconds=10
eureka.instance.health-check-url-path=/actuator/health
#展示全部细节信息
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
#配置邮件提醒
spring.mail.host=smtp.163.com
#spring.mainl.port=465
spring.mail.protocol=smtps
spring.mail.username=Koaliai@163.com
spring.mail.password=JGRHETDUPHQALPHH
# to和from都要配置,否则发送邮件时会报错
spring.boot.admin.notify.mail.to=838032263@qq.com
spring.boot.admin.notify.mail.from=Koaliai@163.com
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
@EnableAutoConfiguration
@EnableAdminServer
@EnableDiscoveryClient
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.SpringbootAdmin客户端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=adminClient
eureka.client.registryFetchIntervalSeconds=5
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
eureka.instance.leaseRenewalIntervalInSeconds=10
eureka.instance.health-check-url-path=/actuator/health
#展示全部细节信息
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableAutoConfiguration
@EnableDiscoveryClient
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 依次启动
Eureka注册中心、SpringbootAdmin服务器端、SpringbootAdmin客户端(2) 访问
SpringbootAdmin的Web管理页面http://localhost:8311
02. Springboot(2.4.1)中集成SpringBootAdmin监控服务
SpringbootAdmin是面向springboot的一款监控组件
1.服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-dependencies</artifactId>
<version>2.3.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=adminServer
management.security.enabled=false
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
@EnableAutoConfiguration
@EnableAdminServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
(4) 访问SpringbootAdmin的Web管理页面,并进行监控客户端运行状况
http://localhost:8310/
2.客户端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-dependencies</artifactId>
<version>2.3.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8311
spring.application.name=adminClient
#admin工程的url
spring.boot.admin.client.url=http://127.0.0.1:8310
#展示全部细节信息
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
#允许admin工程远程停止本应用
management.endpoint.shutdown.enabled=true
#admin工程的账号密码
spring.boot.admin.client.username=admin
spring.boot.admin.client.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@EnableAutoConfiguration
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
03. Springboot(2.0)中集成Zuul网关(Token验证)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eureka-server
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.Zuul路由生产者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8313
spring.application.name=five
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
@RequestMapping("/five")
public class controller {
@RequestMapping(value="/index",method = RequestMethod.GET)
public String Index() {
try {
return "支付成功";
}
catch(Exception e) {
return "支付失败";
}
}
@GetMapping("/hello/{name}")
public String ZuulTestFive(@PathVariable("name") String name) {
return "hello " + name + " this is ZuulTestFive";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.Zuul路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=startGateway
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
zuul.routes.three.path= /three/**
zuul.routes.three.service-id= three
zuul.routes.three.stripPrefix= false
zuul.routes.five.path= /five/**
zuul.routes.five.service-id= five
zuul.routes.five.stripPrefix= false
eureka.client.healthcheck.enabled = true
(3) 创建intector包,并创建MyFilter.java文件
MyFilter.java文件中内容:
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
@Component
public class MyFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run(){
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
Object accessToken = request.getParameter("token");
if(accessToken == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().setHeader("Content-Type", "text/html;charset=UTF-8");
ctx.getResponse().getWriter().write("登录信息为空!");
}catch (Exception e){}
return null;
}
return null;
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@EnableZuulProxy
@ComponentScan("intector")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**依次启动
Eureka注册中心、Zuul路由生产者、Zuul路由消费者,然后访问地址验证地址:http://localhost:8312/five/index
04. Springboot(2.0)中集成Zuul网关限流(Redis存储)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eureka-server
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.Zuul路由生产者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8313
spring.application.name=five
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
@RequestMapping("/five")
public class controller {
@RequestMapping(value="/index",method = RequestMethod.GET)
public String Index() {
try {
return "支付成功";
}
catch(Exception e) {
return "支付失败";
}
}
@GetMapping("/hello/{name}")
public String ZuulTestFive(@PathVariable("name") String name) {
return "hello " + name + " this is ZuulTestFive";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.Zuul路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--Zuul限流的相关依赖-->
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>1.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=startGateway
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
zuul.routes.three.path= /three/**
zuul.routes.three.service-id= three
zuul.routes.three.stripPrefix= false
zuul.routes.five.path= /five/**
zuul.routes.five.service-id= five
zuul.routes.five.stripPrefix= false
spring.redis.database=0
spring.redis.host=192.168.0.19
spring.redis.port=8302
#开启限流
zuul.ratelimit.enabled=true
#针对five路由限流
zuul.ratelimit.repository=redis
#60s内请求超过3次,服务端就抛出异常,60s后可以恢复正常请求
zuul.ratelimit.policies.five.limit=3
#限流类型
zuul.ratelimit.policies.five.type=url
zuul.ratelimit.policies.EURKA-CLIENT1.quota=50
zuul.ratelimit.policies.five.refresh-interval=60
#针对某个IP进行限流,不影响其他IP
zuul.ratelimit.policies.five.type=origin
eureka.client.healthcheck.enabled = true
#全局配置限流
#zuul.ratelimit.enabled=true
#zuul.ratelimit.default-policy.limit=3
#zuul.ratelimit.default-policy.refresh-interval=60
#zuul.ratelimit.default-policy.type=origin
(3) 创建intector包,并创建ExceptionHandler.java文件
ExceptionHandler.java文件中内容:
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExceptionHandler implements ErrorController {
@Override
public String getErrorPath() {
return "error";
}
@RequestMapping(value="/error")
public String error(){
return "{\"result\":\"访问太多频繁,请稍后再访问!!!\"}";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@EnableZuulProxy
@ComponentScan("intector")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**依次启动
Eureka注册中心、Zuul路由生产者、Zuul路由消费者,然后访问地址验证地址:http://localhost:8312/five/index
05. Springboot(2.0)集成SpringGetWay(Token验证)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!-- 这个依赖本来是可以不加的,但是配置没办法刷新,加上它可以手动刷新配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eurekaServer
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableAutoConfiguration
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.GetWay路由消费者1
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=client-test
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@Value("${spring.cloud.client.ip-address}:${server.port}")
private String instance_id;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return instance_id;
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.GetWay路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>0.9.4.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=startGateway
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=gateway8312
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
spring.cloud.gateway.discovery.locator.enabled=false
#开启小写验证,默认feign根据服务名查找都是用的全大写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
spring.cloud.gateway.routes[0].id=client-test
spring.cloud.gateway.routes[0].uri=lb://client-test
#spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
spring.cloud.gateway.routes[0].predicates[0]=Path=/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
(3) 创建domain包,并创建ResultCode.java文件
ResultCode.java文件中内容:
import com.fasterxml.jackson.annotation.JsonFormat;
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ResultCode {
/*
请求返回状态码和说明信息
*/
SUCCESS(200, "成功"),
BAD_REQUEST(400, "参数或者语法不对"),
UNAUTHORIZED(401, "认证失败"),
LOGIN_ERROR(401, "登陆失败,用户名或密码无效"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "请求的资源不存在"),
OPERATE_ERROR(405, "操作失败,请求操作的资源不存在"),
TIME_OUT(408, "请求超时"),
SERVER_ERROR(500, "服务器内部错误"),
;
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public void setCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
(4) 在domain包中创建ResultJson.json文件
ResultJson.java文件中内容:
public class ResultJson {
private int code;
private String msg;
private Object data;
public static ResultJson Success() {
return Success("");
}
public static ResultJson Success(Object o) {
return new ResultJson(ResultCode.SUCCESS, o);
}
public static ResultJson Failure(ResultCode code) {
return Failure(code, "");
}
public static ResultJson Failure(ResultCode code, Object o) {
return new ResultJson(code, o);
}
public ResultJson (ResultCode resultCode) {
setResultCode(resultCode);
}
public ResultJson (ResultCode resultCode,Object data) {
setResultCode(resultCode);
this.data = data;
}
public void setResultCode(ResultCode resultCode) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
}
public void setCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public ResultJson(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
@Override
public String toString() {
return "{" +
"\"code\":" + code +
", \"msg\":\"" + msg + '\"' +
", \"data\":\"" + data + '\"'+
'}';
}
}
(5) 创建Configure包,并创建AuthFilter.java文件
AuthFilter.java文件中内容:
import java.nio.charset.StandardCharsets;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.alibaba.fastjson.JSON;
import domain.*;
import reactor.core.publisher.Mono;
@Component
public class AuthFilter implements GlobalFilter{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("token");
if ("token".equals(token)) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
byte[] datas = JSON.toJSONString(ResultJson.Failure(ResultCode.UNAUTHORIZED)).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(datas);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableAutoConfiguration
@EnableEurekaClient
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 依次启动
Eureka注册中心、Getway路由生产者1、GetWay路由消费者(2) 启动完所有服务,测试
Web页面地址:http://localhost:8312/client-test/index
06. Springboot(2.0)集成SpringGetWay(限流)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!-- 这个依赖本来是可以不加的,但是配置没办法刷新,加上它可以手动刷新配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eurekaServer
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableAutoConfiguration
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.GetWay路由消费者1
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=client-test
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@Value("${spring.cloud.client.ip-address}:${server.port}")
private String instance_id;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return instance_id;
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.GetWay路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>0.9.4.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.main.allow-bean-definition-overriding=true
spring.application.name=startGateway
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=gateway8312
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
spring.redis.host=127.0.0.1
spring.redis.port=8002
spring.redis.database=0
spring.cloud.gateway.discovery.locator.enabled=false
#开启小写验证,默认feign根据服务名查找都是用的全大写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
spring.cloud.gateway.routes[0].id=client-test
spring.cloud.gateway.routes[0].uri=lb://client-test
#spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
spring.cloud.gateway.routes[0].predicates[0]=Path=/**
spring.cloud.gateway.routes[0].filters[0].name=RequestRateLimiter
spring.cloud.gateway.routes[0].filters[0].args.key-resolver=#{@ipKeyResolver}
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.replenishRate=1
spring.cloud.gateway.routes[0].filters[0].args.redis-rate-limiter.burstCapacity=3
spring.cloud.gateway.routes[0].filters[1]=StripPrefix=1
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;
@Configuration
public class Configure {
// @Bean
// KeyResolver apiKeyResolver() {
// //按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态
// return exchange -> Mono.just(exchange.getRequest().getPath().toString());
// }
//
// @Bean
// KeyResolver userKeyResolver() {
// //按用户限流
// return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
// }
@Bean
KeyResolver ipKeyResolver() {
//按IP来限流
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("Configure")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 依次启动
Eureka注册中心、Getway路由生产者1、GetWay路由消费者(2) 启动完所有服务,测试
Web页面地址:http://localhost:8312/client-test/index
07. Springboot(2.0)集成SpringGetWay(熔断降级)
1.Eureka注册中心
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!-- 这个依赖本来是可以不加的,但是配置没办法刷新,加上它可以手动刷新配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8300
spring.application.name=eurekaServer
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableAutoConfiguration@EnableEurekaServerpublic class Start { public static void main(String[] args) { SpringApplication.run(Start.class, args); }}
2.GetWay路由消费者1
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=client-test
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@Value("${spring.cloud.client.ip-address}:${server.port}")
private String instance_id;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return instance_id;
}
@RequestMapping(value = "/gatewayFallback",method = RequestMethod.GET)
public String Fallback() {
return "接口熔断";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.GetWay路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>0.9.4.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
(2) 在主启动目录resources资源文件夹中创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.main.allow-bean-definition-overriding=true
spring.application.name=startGateway
eureka.client.healthcheck.enabled = true
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8300/eureka/
eureka.instance.instance-id=gateway8312
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
spring.cloud.gateway.discovery.locator.enabled=false
#开启小写验证,默认feign根据服务名查找都是用的全大写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
spring.cloud.gateway.routes[0].id=client-test
spring.cloud.gateway.routes[0].uri=lb://client-test
#spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
spring.cloud.gateway.routes[0].predicates[0]=Path=/**
#使用内置的HystrixGatewayFilterFactory工厂类做熔断降级
spring.cloud.gateway.routes[0].filters[0].name=Hystrix
# Hystrix的bean名称
spring.cloud.gateway.routes[0].filters[0].args.name=testHystrix
spring.cloud.gateway.routes[0].filters[0].args.fallbackUri=forward:/gatewayFallback
spring.cloud.gateway.routes[0].filters[1]=StripPrefix=1
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
#接口超时时间为2秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("Configure")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 依次启动
Eureka注册中心、Getway路由生产者1、GetWay路由消费者(2) 启动完所有服务,测试
Web页面地址:http://localhost:8312/client-test/index
08. Springboot热部署
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
#热部署生效
spring.devtools.restart.enabled= true
#设置重启的目录
#spring.devtools.restart.additional-paths: src/main/java
#classpath目录下的WEB-INF文件夹内容修改不重启
spring.devtools.restart.exclude= WEB-INF/**
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String Login() {
return "成功";
}
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return "成功";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
09. Springboot(2.0)中集成Mybaits和Durid操作Mysql(普通配置)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.datasource.url=jdbc:mysql://192.168.0.19:3860/test?serverTimezone=UTC&useSSL=true
spring.datasource.username=mysql
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.initial-size=10
spring.datasource.druid.max-active=50
spring.datasource.druid.min-idle=10
spring.datasource.druid.max-wait=5000
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.validation-query-timeout=20000
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.time-between-eviction-runs-millis=60000
mybatis.type-aliases-package=Entity
mybatis.mapperLocations=classpath:mapper/*.xml
(3) 在资源文件夹resources文件夹中创建mapper文件夹,并创建userMapper.xml文件
userMapper.xml文件中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.UserDao">
<resultMap id="BaseResultMap" type="Entity.User">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="address" property="address"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
</resultMap>
<sql id="Base_Column_List">
id, name, age,address, create_time, update_time
</sql>
<select id="selectAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user order by age
</select>
</mapper>
(4) 创建Entity包,并创建User.java文件
User.java文件中内容:
import java.util.Date;
public class User {
private int id;
private String name;
private int age;
private String address;
private Date createTime;
private Date updateTime;
public User() {
}
public User(int id, String name, int age, String address) {
this.id = id;
this.name = name;
this.age = age;
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
(5) 创建Dao包,并创建UserDao.java文件
UserDao.java文件中内容:
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import Entity.*;
@Mapper
public interface UserDao {
List<User> selectAll();
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
import Entity.*;
@CrossOrigin
@Controller
public class controller {
@Autowired
private UserDao userDao;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public List<User> Index() throws Exception {
List<User> users = userDao.selectAll();
return users;
}
}
(7) 主启动目录中内容
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
@MapperScan("Dao")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
10. Springboot(2.0)集成Zuul路由Ribbon负载均衡
1.Eureka服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=eureka-server
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.路由消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8312
spring.application.name=startGateway
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
zuul.routes.api.path=/api/**
zuul.routes.api.service-id=serviceA
zuul.routes.api.stripPrefix=false
eureka.client.healthcheck.enabled=true
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableAutoConfiguration
@EnableEurekaClient
@EnableZuulProxy
public class Start {
@Bean
@LoadBalanced //调用方法时启用负载均衡
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.路由生产者01
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8313
spring.application.name=serviceA
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@GetMapping("/api/{name}")
public String Api(@PathVariable("name") String name) {
return "hello " + name + " this is ZuulTestFive";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
4.路由生产者02
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8314
spring.application.name=serviceA
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@GetMapping("/api/{name}")
public String Api(@PathVariable("name") String name) {
return "hello " + name + " this is ZuulTestFive";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableEurekaClient
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 启动eureka服务器端服务 (2) 启动zuul生产者服务 (3) 启动zuul消费者01服务 (4) 启动zuul消费者02服务 (5) 测试访问地址:http://localhost:8312/api/jacks
11. Springboot(2.0)接口重用
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 创建service包,并创建UserService.java文件
UserService.java文件中内容:
import org.springframework.stereotype.Component;
@Component
public interface UserService {
void printShow();
}
(3) 在service包中创建UserService1Impl.java文件
UserService1Impl.java文件中内容:
import org.springframework.stereotype.Service;
@Service(value="userService1")
public class UserService1Impl implements UserService {
@Override
public void printShow() {
System.out.println("service1输出");
}
}
(4) 在service包中创建UserService2Impl.java文件
UserService2Impl.java文件中内容:
import org.springframework.stereotype.Service;
@Service(value="userService2")
public class UserService2Impl implements UserService {
@Override
public void printShow() {
System.out.println("service2输出");
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import service.*;
@CrossOrigin
@RestController
public class controller {
@Resource(name="userService1")
private UserService userService1;
@Resource(name="userService1")
private UserService userService2;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
userService1.printShow();
userService2.printShow();
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller,service")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
12. Springboot(2.0)集成Springdata操作Redis(多数据源)(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.data.redis.database = 0
spring.data.redis.host = 192.168.0.19
spring.data.redis.port = 8302
spring.data.redis.password =
spring.data.redis.timeout = 2000
spring.data.redis.max-active = 30
spring.data.redis.max-idle = 30
spring.data.redis.max-waite = 30
spring.data.redis.min-idle = 30
spring.data.redis.max-wait = 6000
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class Configure {
@Value("${spring.data.redis.database}")
private int dbIndex;
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
@Value("${spring.data.redis.timeout}")
private int timeout;
@Value("${spring.data.redis.max-active}")
private int redisPoolMaxActive;
@Value("${spring.data.redis.max-wait}")
private int redisPoolMaxWait;
@Value("${spring.data.redis.max-idle}")
private int redisPoolMaxIdle;
@Value("${spring.data.redis.min-idle}")
private int redisPoolMinIdle;
@Bean
public JedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setDatabase(dbIndex);
//redisStandaloneConfiguration.setPassword(password);
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisPoolMaxIdle);
poolConfig.setMinIdle(redisPoolMinIdle);
poolConfig.setMaxTotal(redisPoolMaxActive);
poolConfig.setMaxWaitMillis(redisPoolMaxWait);
poolConfig.setTestOnBorrow(true);
JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfigurationBuilder = JedisClientConfiguration.builder();
JedisClientConfiguration jedisClientConfiguration = jedisClientConfigurationBuilder.usePooling().poolConfig(poolConfig).build();
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisStandaloneConfiguration,jedisClientConfiguration);
return jedisConnectionFactory;
}
@Bean(name="basicRedisTemplate")
public RedisTemplate<Object,Object> defaultRedisTemplate() {
RedisTemplate<Object,Object> template = new RedisTemplate<Object,Object>();
template.setConnectionFactory(redisConnectionFactory());
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import javax.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@CrossOrigin
@Controller
public class controller {
@Resource(name="basicRedisTemplate")
RedisTemplate<String,String> template;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public String Index() {
if(!template.hasKey("key")){
//写入内容
template.opsForValue().set("key","字符串");
}
else{
//删除数据
template.delete("key");
}
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
13. Springboot(2.0)集成Springdata操作Redis(缓存)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.redis.database=0
spring.redis.host=192.168.0.19
spring.redis.port=8302
spring.redis.timeout=2000
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-active=8
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import java.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@Configuration
@EnableCaching
public class Configure {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间30秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(1800000))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
@Bean
public RedisTemplate<Object,Object> defaultRedisTemplate() {
RedisTemplate<Object,Object> template = new RedisTemplate<Object,Object>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@CrossOrigin
@Controller
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
@Cacheable(value = "test", key = "#id")
public String Index(@RequestParam("id") String id) {
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**删除缓存方法
(1) 删除指定
key值@Caching(evict = { @CacheEvict(value = "MyRedis",key="redis") })(2) 删除指定文件下
value值所有key值@Caching(evict = { @CacheEvict(value = "MyRedis",allEntries=true/*表示删除MyRedis文件下所有的缓存*/) })
14. Springboot(2.0)集成Springdata操作Redis(common-pool连接池)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.redis.database=0
spring.redis.host=192.168.0.19
spring.redis.port=8302
#Redis服务器连接密码(默认为空)
#spring.redis.password=redis123
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@Configuration
public class Configure {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<Object,Object> defaultRedisTemplate() {
RedisTemplate<Object,Object> template = new RedisTemplate<Object,Object>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@CrossOrigin
@Controller
public class controller {
@Autowired
RedisTemplate<String,String> template;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public String Index() {
//写入内容
template.opsForValue().set("key","字符串");
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
15. Springboot中集成jasypt配置文件加密(自定义加密器)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
jasypt.encryptor.bean=codeSheepEncryptorBean
spring.datasource.password=123456
jasypt.encryptor.property.prefix=CodeSheep(
jasypt.encryptor.property.suffix=)
redis.password=CodeSheep(jkKPn/BDQMyplqDQ1Nxp2JB3VlpJZGbsNe2UCTzfI38CsZ/+nV0MbSxZ/EKFr4QP)
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.jasypt.encryption.StringEncryptor;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class Configure {
@Autowired
private ApplicationContext appCtx;
@Autowired
private StringEncryptor codeSheepEncryptorBean;
@Bean(name="codeSheepEncryptorBean" )
public StringEncryptor codesheepStringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword("CodeSheep");
config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setProviderName("SunJCE");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
@Bean
public String init() {
Environment environment = appCtx.getBean(Environment.class);
String redisOriginPswd = environment.getProperty("redis.password");
System.out.println(redisOriginPswd);
//此处将原始密码加密,然后在配置文件中加ENC(<加密字符串>)
//String redisEncryptedPswd = decrypt( redisOriginPswd);
//System.out.println(redisEncryptedPswd);
return "sd";
}
private String encrypt( String originPassord ) {
String encryptStr = codeSheepEncryptorBean.encrypt( originPassord );
return encryptStr;
}
private String decrypt( String encryptedPassword ) {
String decryptStr = codeSheepEncryptorBean.decrypt( encryptedPassword );
return decryptStr;
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
(1) 启动参数启动
java -jar yourproject.jar --jasypt.encryptor.password=CodeSheep
16. Springboot(2.0)中集成mybatis(树形结构查询)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.mysql.url=jdbc:mysql://127.0.0.1:3358/test?serverTimezone=UTC&useSSL=true
spring.mysql.username=mysql
spring.mysql.password=123456
spring.mysql.driver-class=com.mysql.jdbc.Driver
(3) 在资源文件夹resources中创建mapper文件夹,并在mapper文件夹中创建personMapper.xml文件
personMapper.xml文件中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.PersonDao">
<resultMap id="BaseTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="parent_id" property="parentId"/>
<result column="name" property="name"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<resultMap id="NextTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="parent_id" property="parentId"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<sql id="Base_Column_List">
id, name,parent_id
</sql>
<select id="getNextPersonTree" resultMap="NextTreeResultMap">
SELECT id,name
FROM person
WHERE parent_id = #{id}
</select>
<select id="getNodeTree" resultMap="BaseTreeResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM person
WHERE parent_id = 23
</select>
</mapper>
(4) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@MapperScan(basePackages ="Dao")
public class Configure {
@Value("${spring.mysql.url}")
private String url;
@Value("${spring.mysql.username}")
private String username;
@Value("${spring.mysql.password}")
private String password;
@Value("${spring.mysql.driver-class}")
private String driverName;
@Bean
@Primary
public DataSource basicDataSource() throws Exception {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverName);
//连接池配置
dataSource.setMaxActive(3000);
dataSource.setMinIdle(5);
dataSource.setInitialSize(5);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setValidationQuery("SELECT 'x'");
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
@Bean(name = "testSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(basicDataSource());
PathMatchingResourcePatternResolver pathMatch = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(pathMatch.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "testSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
(5) 创建Entity包,并创建Person.java文件
Person.java文件中内容:
import java.util.List;
public class Person {
private int id;
private int parentId;
private String name;
//子节点
private List<Person> child;
public Person() {
}
public Person(int id, int parentId, String name, List<Person> child) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.child = child;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public int getParentId() {
return parentId;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setChild(List<Person> child) {
this.child = child;
}
public List<Person> getChild() {
return child;
}
}
(6) 创建Dao包,并创建PersonDao.java文件
PersonDao.java文件中内容:
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import Entity.*;
@Mapper
public interface PersonDao {
@Select("select * from person where id = #{id}")
Person getPerson(int id);
//查询所有节点
List<Person> getNodeTree();
//查询节点
List<Person> getNextPersonTree(int parentId);
}
(7) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
@CrossOrigin
@Controller
public class controller {
@Autowired
private PersonDao personDao;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public List<Person> Index() throws Exception {
List<Person> list =personDao.getNextPersonTree(23);
return list;
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
17. Springboot集成mybait plus(增强型mybaits)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.mysql.url=jdbc:mysql://127.0.0.1:3358/test?serverTimezone=UTC&useSSL=true
spring.mysql.username=mysql
spring.mysql.password=123456
spring.mysql.driver-class=com.mysql.jdbc.Driver
# 初始化大小,最小,最大
spring.mysql.initialSize=5
spring.mysql.minIdle=5
spring.mysql.maxActive=20
# 配置获取连接等待超时的时间
spring.mysql.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.mysql.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.mysql.minEvictableIdleTimeMillis=300000
# 测试连接
spring.mysql.testWhileIdle=true
spring.mysql.testOnBorrow=false
spring.mysql.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.mysql.poolPreparedStatements=true
spring.mysql.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters
spring.mysql.filters=stat
# asyncInit是1.1.4中新增加的配置,如果有initialSize数量较多时,打开会加快应用启动时间
spring.mysql.asyncInit=true
(3) 资源文件夹resources中创建mapper文件夹,并在mapper文件夹中创建personMapper.xml文件
personMapper.xml文件中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.PersonDao">
<resultMap id="BaseTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="parent_id" property="parentId"/>
<result column="name" property="name"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<resultMap id="NextTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="parent_id" property="parentId"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<sql id="Base_Column_List">
id, name,parent_id
</sql>
<select id="getNextPersonTree" resultMap="NextTreeResultMap">
SELECT id,name
FROM person
WHERE parent_id = #{id}
</select>
<select id="getNodeTree" resultMap="BaseTreeResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM person
WHERE parent_id = 23
</select>
</mapper>
(4) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@MapperScan(basePackages ="Dao")
public class Configure {
@Value("${spring.mysql.url}")
private String url;
@Value("${spring.mysql.username}")
private String username;
@Value("${spring.mysql.password}")
private String password;
@Value("${spring.mysql.driver-class}")
private String driverName;
@Bean
@Primary
public DataSource basicDataSource() throws Exception {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverName);
//连接池配置
dataSource.setMaxActive(3000);
dataSource.setMinIdle(5);
dataSource.setInitialSize(5);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setValidationQuery("SELECT 'x'");
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
@Bean(name = "testSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(basicDataSource());
PathMatchingResourcePatternResolver pathMatch = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(pathMatch.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "testSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
(5) 创建Entity包,并创建Person.java文件
Person.java文件中内容:
import java.util.List;
public class Person {
private int id;
private int parentId;
private String name;
//子节点
private List<Person> child;
public Person() {
}
public Person(int id, int parentId, String name, List<Person> child) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.child = child;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public int getParentId() {
return parentId;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setChild(List<Person> child) {
this.child = child;
}
public List<Person> getChild() {
return child;
}
}
(6) 创建Dao包,并创建PersoDao.java文件
PersonDao.java文件中内容:
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import Entity.*;
@Mapper
public interface PersonDao {
@Select("select * from person where id = #{id}")
Person getPerson(int id);
//查询所有节点
List<Person> getNodeTree();
//查询节点
List<Person> getNextPersonTree(int parentId);
}
(7) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
@CrossOrigin
@Controller
public class controller {
@Autowired
private PersonDao personDao;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public List<Person> Index() throws Exception {
List<Person> list =personDao.getNextPersonTree(23);
return list;
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
18. Springboot集成Easyexcel(操作Excel)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.4</version>
</dependency>
</dependencies>
(2) 创建Entity包,并创建Users.java文件
Users.java文件中内容:
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.BaseRowModel;
public class Users extends BaseRowModel {
@ExcelProperty(value = "姓名",index = 0)
private String name;
@ExcelProperty(value = "地址",index = 1)
private String address;
public Users() {
}
public Users(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.util.IOUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.metadata.Sheet;
import com.alibaba.excel.support.ExcelTypeEnum;
import Entity.*;
@RestController
public class controller {
@RequestMapping(value="/index",method={RequestMethod.GET,RequestMethod.POST})
public ResultJson Index(){
return ResultJson.Success();
}
@GetMapping("/download")
public void DownloadExcel(HttpServletResponse response) throws IOException {
Users u1 = new Users("张三","广东");
Users u2 = new Users("李四","四川");
List<Users> list = new ArrayList<Users>();
list.add(u1);
list.add(u2);
ServletOutputStream out = response.getOutputStream();
@SuppressWarnings("deprecation")
ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX, true);
String fileName = "测试exportExcel";
Sheet sheet = new Sheet(1, 0,Users.class);
//设置自适应宽度
sheet.setAutoWidth(Boolean.TRUE);
// 第一个 sheet 名称
sheet.setSheetName("第一个sheet");
writer.write(list, sheet);
//通知浏览器以附件的形式下载处理,设置返回头要注意文件名有中文
response.setHeader("Content-disposition", "attachment;filename=" + new String( fileName.getBytes("gb2312"), "ISO8859-1" ) + ".xlsx");
writer.finish();
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
out.flush();
IOUtils.closeQuietly(out);
}
}
(4) 主启目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
19. Springboot集成EasyPoi(操作Excel)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
或
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- easy-poi -->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
(2) 创建Entity包,并创建Users.java文件
Uers.java文件中内容:
import cn.afterturn.easypoi.excel.annotation.Excel;
public class Users {
@Excel(name = "姓名", orderNum = "0", width = 15)
private String name;
@Excel(name = "地址", orderNum = "1", width = 15)
private String address;
public Users() {
}
public Users(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(3) 创建controller包,并创建ExcelUtils.java文件
ExcelUtils.java文件中内容:
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.web.multipart.MultipartFile;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
public class ExcelUtils {
/**
* excel 导出
*
* @param list 数据
* @param title 标题
* @param sheetName sheet名称
* @param pojoClass pojo类型
* @param fileName 文件名称
* @param isCreateHeader 是否创建表头
* @param response
*/
public static void exportExcel(List<?> list, String title, String sheetName, Class<?> pojoClass, String fileName,
boolean isCreateHeader, HttpServletResponse response) throws IOException {
ExportParams exportParams = new ExportParams(title, sheetName, ExcelType.XSSF);
exportParams.setCreateHeadRows(isCreateHeader);
defaultExport(list, pojoClass, fileName, response, exportParams);
}
/**
* excel 导出
*
* @param list 数据
* @param title 标题
* @param sheetName sheet名称
* @param pojoClass pojo类型
* @param fileName 文件名称
* @param response
*/
public static void exportExcel(List<?> list, String title, String sheetName, Class<?> pojoClass, String fileName,
HttpServletResponse response) throws IOException {
defaultExport(list, pojoClass, fileName, response, new ExportParams(title, sheetName, ExcelType.XSSF));
}
/**
* excel 导出
*
* @param list 数据
* @param pojoClass pojo类型
* @param fileName 文件名称
* @param response
* @param exportParams 导出参数
*/
public static void exportExcel(List<?> list, Class<?> pojoClass, String fileName, ExportParams exportParams,
HttpServletResponse response) throws IOException {
defaultExport(list, pojoClass, fileName, response, exportParams);
}
/**
* excel 导出
*
* @param list 数据
* @param fileName 文件名称
* @param response
*/
public static void exportExcel(List<Map<String, Object>> list, String fileName, HttpServletResponse response)
throws IOException {
defaultExport(list, fileName, response);
}
/**
* 默认的 excel 导出
*
* @param list 数据
* @param pojoClass pojo类型
* @param fileName 文件名称
* @param response
* @param exportParams 导出参数
*/
private static void defaultExport(List<?> list, Class<?> pojoClass, String fileName, HttpServletResponse response,
ExportParams exportParams) throws IOException {
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, pojoClass, list);
downLoadExcel(fileName, response, workbook);
}
/**
* 默认的 excel 导出
*
* @param list 数据
* @param fileName 文件名称
* @param response
*/
private static void defaultExport(List<Map<String, Object>> list, String fileName, HttpServletResponse response)
throws IOException {
Workbook workbook = ExcelExportUtil.exportExcel(list, ExcelType.HSSF);
downLoadExcel(fileName, response, workbook);
}
/**
* 下载
*
* @param fileName 文件名称
* @param response
* @param workbook excel数据
*/
private static void downLoadExcel(String fileName, HttpServletResponse response, Workbook workbook)
throws IOException {
try {
response.setCharacterEncoding("UTF-8");
response.setHeader("content-Type", "application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode(fileName + "." + ExcelTypeEnum.XLSX.getValue(), "UTF-8"));
workbook.write(response.getOutputStream());
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
/**
* excel 导入
*
* @param filePath excel文件路径
* @param titleRows 标题行
* @param headerRows 表头行
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(String filePath, Integer titleRows, Integer headerRows, Class<T> pojoClass)
throws IOException {
if (StringUtils.isBlank(filePath)) {
return null;
}
ImportParams params = new ImportParams();
params.setTitleRows(titleRows);
params.setHeadRows(headerRows);
params.setNeedSave(true);
params.setSaveUrl("/excel/");
try {
return ExcelImportUtil.importExcel(new File(filePath), pojoClass, params);
} catch (NoSuchElementException e) {
throw new IOException("模板不能为空");
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
/**
* excel 导入
*
* @param file excel文件
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(MultipartFile file, Class<T> pojoClass) throws IOException {
return importExcel(file, 1, 1, pojoClass);
}
/**
* excel 导入
*
* @param file excel文件
* @param titleRows 标题行
* @param headerRows 表头行
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(MultipartFile file, Integer titleRows, Integer headerRows, Class<T> pojoClass)
throws IOException {
return importExcel(file, titleRows, headerRows, false, pojoClass);
}
/**
* excel 导入
*
* @param file 上传的文件
* @param titleRows 标题行
* @param headerRows 表头行
* @param needVerfiy 是否检验excel内容
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(MultipartFile file, Integer titleRows, Integer headerRows, boolean needVerfiy,
Class<T> pojoClass) throws IOException {
if (file == null) {
return null;
}
try {
return importExcel(file.getInputStream(), titleRows, headerRows, needVerfiy, pojoClass);
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
/**
* excel 导入
*
* @param inputStream 文件输入流
* @param titleRows 标题行
* @param headerRows 表头行
* @param needVerfiy 是否检验excel内容
* @param pojoClass pojo类型
* @param <T>
* @return
*/
public static <T> List<T> importExcel(InputStream inputStream, Integer titleRows, Integer headerRows,
boolean needVerify, Class<T> pojoClass) throws IOException {
if (inputStream == null) {
return null;
}
ImportParams params = new ImportParams();
params.setTitleRows(titleRows);
params.setHeadRows(headerRows);
params.setSaveUrl("/excel/");
params.setNeedSave(true);
params.setNeedVerify(needVerify);
try {
return ExcelImportUtil.importExcel(inputStream, pojoClass, params);
} catch (NoSuchElementException e) {
throw new IOException("excel文件不能为空");
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
/**
* Excel 类型枚举
*/
enum ExcelTypeEnum {
XLS("xls"), XLSX("xlsx");
private String value;
ExcelTypeEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
(4) 在controller包中创建controller.java文件
controller.java文件中内容:
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import Entity.*;
@RestController
public class controller {
@RequestMapping(value="/index",method={RequestMethod.GET,RequestMethod.POST})
public ResultJson Index(){
return ResultJson.Success();
}
@GetMapping("/export")
public void ExportExcel(HttpServletResponse response) throws IOException {
Users u1 = new Users("张三","广东");
Users u2 = new Users("李四","四川");
List<Users> list = new ArrayList<Users>();
list.add(u1);
list.add(u2);
ExcelUtils.exportExcel(list, "员工信息", "员工信息sheet", Users.class, "员工信息表", response);
}
@PostMapping("/import")
public List<Users> ImportExcel(@RequestParam("file") MultipartFile file) throws IOException {
List<Users> list = ExcelUtils.importExcel(file, Users.class);
return list;
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
20. Springboot集成Minio文件系统
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.0.2</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.minio.url = http://127.0.0.1:3580
spring.minio.accessKey = minioadmin
spring.minio.secretKey = minioadmin
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.minio.MinioClient;
@Configuration
public class Configure {
@Value("${spring.minio.url}")
private String url;
@Value("${spring.minio.accessKey}")
private String accessKey;
@Value("${spring.minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
//初始化minio客户端
MinioClient minioClient = null;
try {
minioClient = new MinioClient(url, accessKey, secretKey);
}
catch(Exception e) {
minioClient = null;
}
return minioClient;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.InputStream;
import java.net.URLEncoder;
import javax.servlet.http.HttpServletResponse;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.minio.MinioClient;
import io.minio.ObjectStat;
import io.minio.PutObjectOptions;
@RestController
public class controller {
@Autowired
private MinioClient minioClient;
@RequestMapping(value="/upload",method={RequestMethod.GET,RequestMethod.POST})
public String Upload(@RequestParam("file") MultipartFile file) throws Exception {
InputStream is = file.getInputStream();
// 文件名
final String fileName = file.getOriginalFilename();
// 把文件放到minio的test桶里面
minioClient.putObject("test", fileName, is, new PutObjectOptions(is.available(), -1));
return "成功";
}
@RequestMapping(value="/download",method={RequestMethod.GET,RequestMethod.POST})
public void Download(@RequestParam("fileName") String fileName, HttpServletResponse response) throws Exception {
InputStream in = null;
ObjectStat stat = minioClient.statObject("test", fileName);
response.setContentType(stat.contentType());
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
in = minioClient.getObject("test", fileName);
IOUtils.copy(in, response.getOutputStream());
in.close();
}
@RequestMapping(value="/delete",method={RequestMethod.GET,RequestMethod.POST})
public String Delete(@RequestParam("fileName") String fileName) throws Exception {
minioClient.removeObject("test", fileName);
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
21. Springboot集成限流访问(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
</dependencies>
(2) 创建aop包,并创建Limit.java文件
Limit.java文件中内容:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
//资源名称
String name() default "";
// 限制每秒访问次数,默认最大即不限制
double perSecond() default Double.MAX_VALUE;
}
(3) 在aop包中创建LimitAspect.java文件
LimitAspect.java文件中内容:
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.google.common.util.concurrent.RateLimiter;
@Aspect
@Component
public class LimitAspect {
RateLimiter rateLimiter = RateLimiter.create(Double.MAX_VALUE);
@Pointcut("@annotation(aop.Limit)") //包名和限制标记
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
//获取目标方法
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Limit limit = method.getAnnotation(Limit.class);
rateLimiter.setRate(limit.perSecond());
// 获取令牌桶中的一个令牌,最多等待1秒
if (rateLimiter.tryAcquire(1, 1, TimeUnit.SECONDS)) {
return point.proceed();
} else {
throw new LimitAccessException("网络异常,请稍后重试!");
}
}
}
(4) 在aop包中创建LimitAccessException.java文件
LimitAccessException.java文件中内容:
public class LimitAccessException extends RuntimeException {
private static final long serialVersionUID = -3608667856397125671L;
public LimitAccessException(String message) {
super(message);
}
}
(5) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import aop.*;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"aop"})
public class Configure {
@Bean
public LimitAspect requestLimitAop() {
return new LimitAspect();
}
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import aop.*;
@RestController
public class controller {
@RequestMapping(value="/index",method={RequestMethod.GET,RequestMethod.POST})
@Limit(name = "测试限流每秒3个请求", perSecond = 1)
public String Index() throws Exception {
return "ok";
}
}
(7) 创建Exceptions包,并创建MyExceptionHandler.java文件
MyExceptionHandler.java文件中内容:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import aop.*;
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(value = LimitAccessException.class)//指定拦截的异常
public void errorHandler(HttpServletRequest request, HttpServletResponse response,Exception e) throws Exception{
response.getWriter().write("xsfjslkjskjfsd");
response.getWriter().flush();
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller,Exceptions")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
22. Springboot(2.0)中集成Netty(服务器端)(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port = 8080
(3) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class NettyServer {
@Value("${netty.port}")
private int nettyPort;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workGroup = new NioEventLoopGroup();
/**
* 启动netty服务
* @throws InterruptedException
*/
@PostConstruct
public void start() throws InterruptedException {
ServerBootstrap b=new ServerBootstrap();
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new NettyChannel());
ChannelFuture future = b.bind(nettyPort).sync();
if (future.isSuccess()) {
System.out.println("启动 Netty 成功");
}
}
/**
* 销毁
*/
@PreDestroy
public void destroy() {
bossGroup.shutdownGracefully().syncUninterruptibly();
workGroup.shutdownGracefully().syncUninterruptibly();
System.out.println("关闭 Netty 成功");
}
}
注:
(1)
@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的inti()方法。(2)
@PreDestroy修饰的方法会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的destroy()方法。
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyHandler());
}
}
(4) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
//转十六进制
String str = ByteBufUtil.hexDump(buf).toLowerCase();
System.out.println(str);
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
23. Springboot(2.0)中集成Netty(服务器端)(3)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port = 8080
(3) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class NettyServer {
@Value("${netty.port}")
private int nettyPort;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workGroup = new NioEventLoopGroup();
public void start() {
ServerBootstrap b=new ServerBootstrap();
try {
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new NettyChannel());
ChannelFuture future = b.bind(nettyPort).sync();
/*等待端口关闭*/
future.channel().closeFuture().sync();
}
catch(Exception e) {
e.printStackTrace();
}
finally{
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import org.springframework.stereotype.Component;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
@Component
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyHandler());
}
}
(4) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
//转十六进制
String str = ByteBufUtil.hexDump(buf).toLowerCase();
System.out.println(str);
}
}
(5) 主启动目录中内容
import javax.annotation.Resource;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import netty.*;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start implements CommandLineRunner{
@Resource
private NettyServer nettyServer;
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
@Override
public void run(String... args) throws Exception {
// 开启服务
nettyServer.start();
}
}
注:
@Resource对象只初始化一次
24. Springboot(2.0)中集成Netty(消息服务器)
1.服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port = 8080
(3) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class NettyServer {
@Value("${netty.port}")
private int nettyPort;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workGroup = new NioEventLoopGroup();
public void start() {
ServerBootstrap b=new ServerBootstrap();
try {
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new NettyChannel());
ChannelFuture future = b.bind(nettyPort).sync();
System.out.println("启动");
/*等待端口关闭*/
future.channel().closeFuture().sync();
}
catch(Exception e) {
e.printStackTrace();
}
finally{
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new NettyHandler());
}
}
(5) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
System.out.println(body);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
}
}
(6) 主启动目录中内容
import javax.annotation.Resource;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import netty.*;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start implements CommandLineRunner{
@Resource
private NettyServer nettyServer;
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
@Override
public void run(String... args) throws Exception {
// 开启服务
nettyServer.start();
}
}
2.客户端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.host=127.0.0.1
netty.port = 8080
(3) 创建netty包,并创建NettyClient.java文件
NettyClient.java文件中内容:
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
@Component
public class NettyClient {
@Value("${netty.host}")
private String nettyHost;
@Value("${netty.port}")
private int nettyPort;
private EventLoopGroup workGroup = new NioEventLoopGroup();
@PostConstruct
public void start() throws InterruptedException {
Bootstrap b = new Bootstrap();
b.group(workGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.option(ChannelOption.SO_BACKLOG,128)
.handler(new NettyChannel());
ChannelFuture f = b.connect(nettyHost,nettyPort).sync();
System.out.println("启动");
/*等待端口关闭*/
f.channel().closeFuture().sync();
if (f.isSuccess()) {
System.out.println("启动 Netty 成功");
}
}
@PreDestroy
public void destroy() {
workGroup.shutdownGracefully().syncUninterruptibly();
System.out.println("关闭 Netty 成功");
}
}
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new StringEncoder(CharsetUtil.UTF_8))
.addLast(new NettyHandler());
}
}
(5) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
System.out.println(body);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
25. Springboot(2.0)中集成Netty中Websocket服务
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port = 8080
(3) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
public class NettyHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static ChannelGroup channelGroup;
static {
channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 添加到channelGroup通道组
channelGroup.add(ctx.channel());
System.out.println("与客户端建立连接,通道开启!");
}
// 客户端与服务器关闭连接的时候触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道关闭!");
channelGroup.remove(ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String message = msg.text() + " --- 你好," + ctx.channel().localAddress() + " 给固定的人发消息";
ctx.channel().writeAndFlush(new TextWebSocketFrame(message));
//群发消息
message = "我是服务器,这里发送的是群消息";
channelGroup.writeAndFlush(new TextWebSocketFrame(message));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
}
}
(4) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class NettyChannel extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(65536 * 10000))
.addLast(new WebSocketServerProtocolHandler("/ws", "WebSocket", true, 65536 * 10))
.addLast(new NettyHandler());
}
}
(5) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
public class NettyHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static ChannelGroup channelGroup;
static {
channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 添加到channelGroup通道组
channelGroup.add(ctx.channel());
System.out.println("与客户端建立连接,通道开启!");
}
// 客户端与服务器关闭连接的时候触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道关闭!");
channelGroup.remove(ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String message = msg.text() + " --- 你好," + ctx.channel().localAddress() + " 给固定的人发消息";
ctx.channel().writeAndFlush(new TextWebSocketFrame(message));
//群发消息
message = "我是服务器,这里发送的是群消息";
channelGroup.writeAndFlush(new TextWebSocketFrame(message));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("netty")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
26. Springboot中集成WebSocket(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
(2) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class Configure {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
@ServerEndpoint("/websocket/{userId}")
@Component
public class controller {
private static ConcurrentHashMap<String,Session> webSocketMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session,@PathParam("userId") String userId){
System.out.println(userId);
System.out.println("websocket打开");
}
@OnMessage
public void onMessage(String message,Session session){
System.out.println("接收:" + message);
try{
session.getBasicRemote().sendText("字符串");
}
catch(Exception e){
}
}
@OnError
public void onError(Session session,Throwable error){
error.printStackTrace();
}
@OnClose
public void onClose(Session session){
System.out.print("关闭");
}
}
注:
Websocket访问地址ws://127.0.0.1:8310/websocket/<id值>
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
27. Springboot中集成线程池(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
(2) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class Configure {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(10);
// 设置队列容量
executor.setQueueCapacity(20);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("hello-");
// 设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
(3) 创建service包,并创建UserService.java文件
UserService.java文件中内容:
import org.springframework.stereotype.Component;
@Component
public interface UserService {
String sayHello();
}
(4) 在service包中创建UserServiceImpl.java文件
UserServiceImpl.java文件中内容:
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class UserServiceImpl implements UserService {
@Async
public String sayHello() {
LoggerFactory.getLogger(UserServiceImpl.class).info( ":Hello World!");
return "hello";
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import service.*;
@RestController
public class controller {
@Autowired
private UserService userService;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return userService.sayHello();
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,service,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
28. Springboot中集成Sentinel降级和限流
1.自定义异常回复
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.transport.eager= true
注:
Sentinel采用延迟加载,只有在主动发起一次请求后,才会被拦截并发送给服务端。如果想关闭这个延迟,需将eager设置为true
(3) 创建Configure包,并创建ResponseData.java文件
ResponseData.java文件中内容:
public class ResponseData {
private int code;
private String message;
public ResponseData(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
(4) 在Configure包中创建SentinelException.java文件
SentinelException.java文件中内容:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
@Component
public class SentinelException implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
response.setContentType("application/json;charset=utf-8");
ResponseData data = null;
if (e instanceof FlowException) {
data = new ResponseData(-1, "流控规则被触发......");
} else if (e instanceof DegradeException) {
data = new ResponseData(-2, "降级规则被触发...");
} else if (e instanceof AuthorityException) {
data = new ResponseData(-3, "授权规则被触发...");
} else if (e instanceof ParamFlowException) {
data = new ResponseData(-4, "热点规则被触发...");
} else if (e instanceof SystemBlockException) {
data = new ResponseData(-5, "系统规则被触发...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**需要在
Sentinel管理界面设置相应的限流和降级过后才能触发
2.全局异常回复
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.transport.eager= true
注:
Sentinel采用延迟加载,只有在主动发起一次请求后,才会被拦截并发送给服务端。如果想关闭这个延迟,需将eager设置为true
(3) 创建Configure包,并创建ResponseData.java文件
ResponseData.java文件中内容:
public class ResponseData {
private int code;
private String message;
public ResponseData(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
(4) 在Configure包中创建ExcpetionHandler.java文件
ExcpetionHandler.java文件中内容:
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
@RestControllerAdvice
public class ExcpetionHandler {
@ExceptionHandler(ParamFlowException.class)
public ResponseData test() {
return new ResponseData(-4, "热点规则被触发...");
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() {
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**需要在
Sentinel管理界面设置相应的限流和降级过后才能触发
29. Springboot中集成Mybaits和Netty服务
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.30.Final</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid的starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
netty.port=8010
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://192.168.0.19:3860/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=mysql
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis= 60000
spring.datasource.druid.min-evictable-idle-time-millis= 30000
spring.datasource.druid.validation-query= SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle= true
spring.datasource.druid.test-on-borrow= true
spring.datasource.druid.test-on-return= false
spring.datasource.druid.pool-prepared-statements= true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=Entity
(3) 在资源文件夹resources中创建mapper文件夹,并在mapper文件夹中创建AccountMapper.xml文件
AccountMapper.xml文件中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.AccountDao">
<resultMap id="accountList" type="Entity.Account">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="balance" property="balance"/>
</resultMap>
<select id="findAll" resultMap="accountList">
select * from tbl_account
</select>
</mapper>
(4) 创建Entity包,并创建Account.java文件
Account.java文件中内容:
import org.springframework.stereotype.Component;
@Component
public class Account {
private int id;
private String name;
private float balance;
public Account() {
}
public Account(int id, String name, float balance) {
this.id = id;
this.name = name;
this.balance = balance;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getBalance() {
return balance;
}
public void setBalance(float balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account [id=" + id + ", name=" + name + ", balance=" + balance + "]";
}
}
(5) 创建Dao包,并创建AccountDao.java文件
AccountDao.java文件中内容:
import java.util.List;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface AccountDao {
public List<Account> findAll();
}
(6) 创建netty包,并创建NettyServer.java文件
NettyServer.java文件中内容:
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import Dao.*;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
@Component
public class NettyServer {
@Value("${netty.port}")
private int nettyPort;
@Autowired
private AccountDao accountDao;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workGroup = new NioEventLoopGroup();
/**
* 启动netty服务
* @throws InterruptedException
*/
@PostConstruct
public void start() throws InterruptedException {
ServerBootstrap b=new ServerBootstrap();
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childHandler(new NettyChannel(accountDao));
ChannelFuture future = b.bind(nettyPort).sync();
if (future.isSuccess()) {
System.out.println("启动 Netty 成功");
}
}
/**
* 销毁
*/
@PreDestroy
public void destroy() {
bossGroup.shutdownGracefully().syncUninterruptibly();
workGroup.shutdownGracefully().syncUninterruptibly();
System.out.println("关闭 Netty 成功");
}
}
(7) 在netty包中创建NettyChannel.java文件
NettyChannel.java文件中内容:
import org.springframework.stereotype.Component;
import Dao.*;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
@Component
public class NettyChannel extends ChannelInitializer<SocketChannel>{
private AccountDao accountDao;
public NettyChannel(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyHandler(accountDao));
}
}
(8) 在netty包中创建NettyHandler.java文件
NettyHandler.java文件中内容:
import java.util.List;
import org.springframework.stereotype.Component;
import Dao.*;
import Entity.*;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
@Component
public class NettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
private AccountDao accountDao;
public NettyHandler(AccountDao accountDao) {
this.accountDao=accountDao;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
ByteBuf msg=(ByteBuf) buf;
System.out.println(msg.toString(CharsetUtil.UTF_8));
List<Account> users = accountDao.findAll();
users.forEach(user-> System.out.println(user.toString()));
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
cause.printStackTrace();
ctx.close();
}
}
(9) 主启动目录中内容
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@MapperScan(basePackages ="Dao")
@ComponentScan("netty")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
30. Springboot(2.4)中集成Springdata操作Mongodb(多数据源)(1)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.data.mongodb.url=mongodb://192.168.0.19:8092
spring.data.mongodb.database=test
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
@Configuration
@EnableMongoRepositories(basePackages = {"Dao"})
public class Configure {
@Value("${spring.data.mongodb.url}")
private String url;
@Value("${spring.data.mongodb.database}")
private String database;
@Bean
@Primary
public SimpleMongoClientDatabaseFactory mongoDbFactory() throws Exception{
ConnectionString connectionString = new ConnectionString(url);
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.retryWrites(true)
.build();
MongoClient mongClient = MongoClients.create(settings);
return new SimpleMongoClientDatabaseFactory(mongClient,database);
}
}
(4) 创建Entity包,并创建User.java文件
User.java文件中内容:
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "user")
public class User {
private String userName;
public User(){
}
public User(String userName){
this.userName = userName;
}
public void setUserName(String userName){
this.userName = userName;
}
public String getUserName(){
return userName;
}
@Override
public String toString() {
return "User [userName=" + userName + "]";
}
}
(5) 创建Dao包,并创建userDao.java文件
userDao.java文件中内容:
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface userDao extends MongoRepository<User,ObjectId>{
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import Dao.*;
import Entity.*;
@CrossOrigin
@RestController
public class controller {
@Autowired
userDao userRepository;
@RequestMapping(value="/index",method = {RequestMethod.GET})
public String Index() {
User user = new User("张三");
userRepository.save(user);
return "成功";
}
}
(7) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
31. Springboot(2.4)中集成Springdata操作Mongodb(多数据源)(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.data.mongodb.url=mongodb://192.168.0.19:8092
spring.data.mongodb.database=test
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
@Configuration
public class Configure {
@Value("${spring.data.mongodb.url}")
private String url;
@Value("${spring.data.mongodb.database}")
private String database;
@Primary
public SimpleMongoClientDatabaseFactory mongoDbFactory() throws Exception{
ConnectionString connectionString = new ConnectionString(url);
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.retryWrites(true)
.build();
MongoClient mongClient = MongoClients.create(settings);
return new SimpleMongoClientDatabaseFactory(mongClient,database);
}
@Bean(name = "primaryMongoTemplate")
public MongoTemplate getMongoTemplate() throws Exception {
return new MongoTemplate(mongoDbFactory());
}
}
(4) 创建Entity包,并创建User.java文件
User.java文件中内容:
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "user")
public class User {
private String userName;
public User(){
}
public User(String userName){
this.userName = userName;
}
public void setUserName(String userName){
this.userName = userName;
}
public String getUserName(){
return userName;
}
@Override
public String toString() {
return "User [userName=" + userName + "]";
}
}
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import javax.annotation.Resource;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import Entity.*;
@CrossOrigin
@RestController
public class controller {
@Resource(name = "primaryMongoTemplate")
private MongoTemplate mongoTemplate;
@RequestMapping(value="/index",method = {RequestMethod.GET})
public String Index() {
User user = new User("张三");
mongoTemplate.save(user);
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
32. Springboot(2.4)中集成Springdata操作Mongodb(多数据源)(3)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
first.uri=mongodb://192.168.0.19:8092
first.database=test1
first.host=192.168.0.19
first.port=8092
second.uri=mongodb://192.168.0.19:8092
second.database=test2
second.host=192.168.0.19
second.port=8092
(3) 创建firstMongo包,并创建FirstMongoProperties.java文件
FirstMongoProperties.java文件中内容:
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class FirstMongoProperties {
@Primary
@Bean(name="firstMongoProperties1")
@ConfigurationProperties(prefix="first")
public MongoProperties firstMongoProperties(){
return new MongoProperties();
}
}
(4) 在firstMongo创建FirstMongoTemplate.java文件
FirstMongoTemplate.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
@Configuration
@EnableMongoRepositories(basePackages="firstDao",mongoTemplateRef="firstMongo")
public class FirstMongoTemplate {
@Autowired
@Qualifier("firstMongoProperties1")
private MongoProperties mongoProperties;
@Primary
@Bean
public SimpleMongoClientDatabaseFactory firstFactory(MongoProperties mongo) throws Exception{
ConnectionString connectionString = new ConnectionString(mongo.getUri());
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.retryWrites(true)
.build();
MongoClient mongClient = MongoClients.create(settings);
return new SimpleMongoClientDatabaseFactory(mongClient,mongo.getDatabase());
}
@Primary
@Bean(name="firstMongo")
public MongoTemplate firstMongoTemplate() throws Exception {
return new MongoTemplate(firstFactory(this.mongoProperties));
}
}
(5) 创建secondMongo包,并创建SecondMongoProperties.java文件
SecondMongoProperties.java文件中内容:
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SecondMongoProperties {
@Bean(name="secondMongoProperties2")
@ConfigurationProperties(prefix="second")
public MongoProperties firstMongoProperties(){
return new MongoProperties();
}
}
(6) 在secondMongo创建SecondMongoTemplate.java文件
SecondMongoTemplate.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
@Configuration
@EnableMongoRepositories(basePackages="secondDao",mongoTemplateRef="secondMongo")
public class SecondMongoTemplate {
@Autowired
@Qualifier("secondMongoProperties2")
private MongoProperties mongoProperties;
@Bean
public SimpleMongoClientDatabaseFactory secondFactory(MongoProperties mongo) throws Exception{
ConnectionString connectionString = new ConnectionString(mongo.getUri());
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.retryWrites(true)
.build();
MongoClient mongClient = MongoClients.create(settings);
return new SimpleMongoClientDatabaseFactory(mongClient,mongo.getDatabase());
}
@Bean(name="secondMongo")
public MongoTemplate firstMongoTemplate() throws Exception {
return new MongoTemplate(secondFactory(this.mongoProperties));
}
}
(7) 创建Entity包,并创建User.java文件
User.java文件中内容:
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "user")
public class User {
private String userName;
public User(){
}
public User(String userName){
this.userName = userName;
}
public void setUserName(String userName){
this.userName = userName;
}
public String getUserName(){
return userName;
}
@Override
public String toString() {
return "User [userName=" + userName + "]";
}
}
(8) 创建firstDao包,并创建FirstUserDao.java文件
FirstUserDao.java文件中内容:
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface FirstUserDao extends MongoRepository<User,ObjectId> {
User findByUserName(String userName);
}
(9) 创建secondDao包,并创建SecondUserDao.java文件
SecondUserDao.java文件中内容:
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface SecondUserDao extends MongoRepository<User,ObjectId> {
User findByUserName(String userName);
}
(10) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import Entity.*;
import firstDao.*;
import secondDao.*;
@CrossOrigin
@RestController
public class controller {
@Autowired
FirstUserDao firstRepository;
@Autowired
SecondUserDao secondRepository;
@RequestMapping(value="/first",method = {RequestMethod.GET})
public String First() {
User user = new User("张三");
firstRepository.save(user);
return "成功";
}
@RequestMapping(value="/second",method = {RequestMethod.GET})
public String Second() {
User user = new User("张三");
secondRepository.save(user);
return "成功";
}
}
(11) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})
@ComponentScan("firstMongo,secondMongo,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
33. Springboot(2.0)集成mybaits开启Ecache二级缓存(1)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
</dependencies>
(2) 主启动目录创建资源文件夹resources文件夹,并创建application.properties文件
application.properties中内容:
server.port=8310
spring.mysql.url=jdbc:mysql://Ip地址:端口/数据库名称?serverTimezone=UTC&useSSL=false
spring.mysql.username=用户名
spring.mysql.password=密码
spring.mysql.driver-class=com.mysql.jdbc.Driver
(3) 在resources文件夹中,创建maper文件夹并创建personMapper.xml文件
personMapper.xml中内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="Dao.PersonDao">
<resultMap id="BaseTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="parent_id" property="parentId"/>
<result column="name" property="name"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<resultMap id="NextTreeResultMap" type="Entity.Person">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="parent_id" property="parentId"/>
<collection column="id" property="child" javaType="java.util.ArrayList"
ofType="Entity.Person" select="getNextPersonTree"/>
</resultMap>
<sql id="Base_Column_List">id, name,parent_id</sql>
<select id="getNextPersonTree" resultMap="NextTreeResultMap">
SELECT id,name FROM person WHERE parent_id = #{id}
</select>
<select id="getNodeTree" resultMap="BaseTreeResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM person
WHERE parent_id = 23
</select>
</mapper>
(4) 创建Configure包,并创建Congifure.java文件
Configure.java中内容:
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@MapperScan(basePackages ="Dao")
public class Configure {
@Value("${spring.mysql.url}")
private String url;
@Value("${spring.mysql.username}")
private String username;
@Value("${spring.mysql.password}")
private String password;
@Value("${spring.mysql.driver-class}")
private String driverName;
@Bean
@Primary
public DataSource basicDataSource() throws Exception {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverName);
//连接池配置
dataSource.setMaxActive(3000);
dataSource.setMinIdle(5);
dataSource.setInitialSize(5);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setValidationQuery("SELECT 'x'");
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
@Bean(name = "testSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(basicDataSource());
PathMatchingResourcePatternResolver pathMatch = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(pathMatch.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
@Bean(name = "testSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
(5) 在Configure包中创建MyEhcacheConfig.java文件
MyEhcacheConfig.java中内容:
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
@Configuration
@EnableCaching
public class MyEhcacheConfig {
@Bean
public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean bean) {
return new EhCacheCacheManager(bean.getObject());
}
/**
* 据shared与否的设置,
* Spring分别通过CacheManager.create()
* 或new CacheManager()方式来创建一个ehcache基地.
* 也说是说通过这个来设置cache的基地是这里的Spring独用,还是跟别的(如hibernate的Ehcache共享)
*
* @return
*/
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
cacheManagerFactoryBean.setShared(true);
return cacheManagerFactoryBean;
}
}
(6) 创建Entity包,并创建Person.java文件
Person.java中内容:
package Entity;
import java.io.Serializable;
import java.util.List;
public class Person implements Serializable{
private static final long serialVersionUID = 1L;
private int id;
private int parentId;
private String name;
//子节点
private List<Person> child;
public Person() {
}
public Person(int id, int parentId, String name, List<Person> child) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.child = child;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public int getParentId() {
return parentId;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setChild(List<Person> child) {
this.child = child;
}
public List<Person> getChild() {
return child;
}
}
(7) 创建Dao包,并创建PersonDao.java文件
PersonDao.java中内容:
import java.util.List;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import Entity.*;
//分布式环境下必然会出现脏数据;
//多表联合查询的情况下极大可能会出现脏数据;
@Mapper
//整个类开启缓存
//@CacheNamespace
public interface PersonDao {
@Select("select * from person where id = #{id}")
Person getPerson(int id);
//查询所有节点
List<Person> getNodeTree();
//查询节点
List<Person> getNextPersonTree(int parentId);
}
(8) 创建controller包,并创建controller.java文件
controller.java中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
import domain.ResultJson;
@CrossOrigin
@Controller
public class controller {
@Autowired
private PersonDao personDao;
@Cacheable(key="'user_'+#uid",value="userCache")
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public Person Index() throws Exception {
return personDao.getPerson(1);
}
}
(9) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
// TODO Auto-generated method stub
SpringApplication.run(Start.class, args);
}
}
34. Springboot集成Kaptcha图形验证码
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
kaptcha.border=yes
kaptcha.border.color=105,179,90
kaptcha.textproducer.font.color=blue
kaptcha.textproducer.font.size=30
kaptcha.textproducer.font.names=宋体
kaptcha.textproducer.char.length=4
kaptcha.image.width=120
kaptcha.image.height=40
kaptcha.session.key=code
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import java.util.Properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
@Configuration
public class Configure {
@Value("${kaptcha.border}")
private String border;
@Value("${kaptcha.border.color}")
private String borderColor;
@Value("${kaptcha.textproducer.font.color}")
private String textproducerFontColor;
@Value("${kaptcha.textproducer.font.size}")
private String textproducerFontSize;
@Value("${kaptcha.textproducer.font.names}")
private String textproducerFontNames;
//验证码长度
@Value("${kaptcha.textproducer.char.length}")
private String textproducerCharLength;
@Value("${kaptcha.image.width}")
private String imageWidth;
@Value("${kaptcha.image.height}")
private String imageHeight;
@Value("${kaptcha.session.key}")
private String sessionKey;
@Bean
public DefaultKaptcha getDefaultKapcha(){
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty("kaptcha.border", border);
properties.setProperty("kaptcha.border.color", borderColor);
properties.setProperty("kaptcha.textproducer.font.color", textproducerFontColor);
properties.setProperty("kaptcha.textproducer.font.size", textproducerFontSize);
properties.setProperty("kaptcha.textproducer.font.names", textproducerFontNames);
properties.setProperty("kaptcha.textproducer.char.length", textproducerCharLength);
properties.setProperty("kaptcha.image.width", imageWidth);
properties.setProperty("kaptcha.image.height", imageHeight);
properties.setProperty("kaptcha.session.key", sessionKey);
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import com.google.code.kaptcha.impl.DefaultKaptcha;
@CrossOrigin
@Controller
public class controller {
/* 注入Kaptcha */
@Autowired
private DefaultKaptcha defaultKaptcha;
@GetMapping("/code")
@ResponseBody
public ModelAndView getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpSession session = request.getSession();
response.setDateHeader("Expires", 0);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
// return a jpeg
response.setContentType("image/jpeg");
// create the text for the image
String capText = defaultKaptcha.createText();
//存储文字
session.setAttribute("test", capText);
// create the image with the text
BufferedImage bi = defaultKaptcha.createImage(capText);
ServletOutputStream out = response.getOutputStream();
// write the data out
ImageIO.write(bi, "jpg", out);
try {
out.flush();
} finally {
out.close();
}
return null;
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
35. Springboot(2.0)集成zookeeper分布式锁
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- zookeeper 客户端 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
##ZooKeeper地址
zk.url = 127.0.0.1:2181
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Configure {
@Value("${zk.url}")
private String zkUrl;
@Bean
public CuratorFramework getCuratorFramework() {
// 用于重连策略,1000毫秒是初始化的间隔时间,3代表尝试重连次数
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
//操作zookeeper集群zkUrl路径ip:xx,ip:xx
CuratorFramework client = CuratorFrameworkFactory.newClient(zkUrl, retryPolicy);
//必须调用start开始连接ZooKeeper
client.start();
/**
* 使用Curator,可以通过LeaderSelector来实现领导选取;
* 领导选取:选出一个领导节点来负责其他节点;如果领导节点不可用,则在剩下的机器里再选出一个领导节点
*/
//构造一个监听器
/*LeaderSelectorListenerAdapter listener = new LeaderSelectorListenerAdapter() {
@Override
public void takeLeadership(CuratorFramework curatorFramework) throws Exception {
log.info("get leadership");
// 领导节点,方法结束后退出领导。zk会再次重新选择领导
}
};
LeaderSelector selector = new LeaderSelector(client, "/schedule", listener);
selector.autoRequeue();
selector.start();
*/
return client;
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.util.concurrent.TimeUnit;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class controller {
@Autowired
private CuratorFramework zkClient;
@GetMapping("/zookeeperLock1")
public ResultJson ZookeeperLock1() throws Exception {
try {
// InterProcessMutex 构建一个分布式锁该节点会在客户端链接断开时被删除
InterProcessMutex lock = new InterProcessMutex(zkClient, "/test");
try {
//锁定5秒钟
if (lock.acquire(5, TimeUnit.HOURS)) {
// 模拟业务处理耗时5秒
Thread.sleep(5*1000);
System.out.println("处理事务");
}
} finally {
// 释放该锁
//lock.release();
}
} catch (Exception e) {
// zk异常
e.printStackTrace();
}
return "zookeeper1锁定";
}
@GetMapping("/zookeeperLock2")
public String ZookeeperLock2() throws Exception {
// 获取锁
InterProcessSemaphoreMutex balanceLock = new InterProcessSemaphoreMutex(zkClient, "/zktest");
try {
// 执行加锁操作
balanceLock.acquire();
System.out.println("执行事务");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 释放锁资源
balanceLock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
return "zookeeper2锁定";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("controller,Configure")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
36. Springboot(2.0)中集成Springdata操作Mysql(自动建表)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.mysql.url=jdbc:mysql://127.0.0.1:3358/test?serverTimezone=UTC&useSSL=true
spring.mysql.username=mysql
spring.mysql.password=123456
spring.mysql.driver-class=com.mysql.cj.jdbc.Driver
spring.mysql.minPoolSize = 3
spring.mysql.maxPoolSize = 25
spring.mysql.maxLifetime = 20000
spring.mysql.borrowConnectionTimeout = 30
spring.mysql.loginTimeout = 30
spring.mysql.maintenanceInterval = 60
spring.mysql.maxIdleTime = 60
#在控制台打印出SQL语句
spring.jpa.show-sql=true
#每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
spring.jpa.hibernate.ddl-auto=update
#Hibernate生成的SQL语句为MySQL方言
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@EnableJpaRepositories(basePackages="Dao")
@EntityScan("Entity")
public class Configure {
@Value("${spring.mysql.url}")
private String url;
@Value("${spring.mysql.username}")
private String username;
@Value("${spring.mysql.password}")
private String password;
@Value("${spring.mysql.driver-class}")
private String driverName;
@Bean
public DataSource basicDataSource() throws Exception {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverName);
//连接池配置
dataSource.setMaxActive(3000);
dataSource.setMinIdle(5);
dataSource.setInitialSize(5);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setTestOnReturn(true);
dataSource.setValidationQuery("SELECT 1");
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
}
(4) 创建Entity包,并创建Person.java文件
controller.java文件中内容:
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="person_info")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name="name")
private String name;
@Column(name="age")
private int age;
@Column(name="address")
private String address;
@Column(name="create_time")
private Date createTime = new Date();
@Column(name="update_time")
private Date updateTime = new Date();
public Person() {
}
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
(5) 创建Dao包,并创建PersonDao.java文件
PersonDao.java文件中内容:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import Entity.*;
@Repository
public interface PersonDao extends JpaRepository<Person,Integer>{
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import Dao.*;
import Entity.*;
@CrossOrigin
@Controller
public class controller {
@Autowired
private PersonDao personDao;
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public List<Person> Index() throws Exception {
List<Person> persons = personDao.findAll();
return persons;
}
@RequestMapping(value = "/save",method = RequestMethod.GET)
@ResponseBody
public String Save() throws Exception {
List<Person> persons = new ArrayList<Person>();
Person person1 = new Person("李四",12,"广东");
Person person2 = new Person("张三",20,"广东");
persons.add(person1);
persons.add(person2);
personDao.saveAll(persons);
return "成功";
}
}
(7) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
#每次运行该程序,没有表格会新建表格,表内有数据会清空 spring.jpa.hibernate.ddl-auto:create #每次程序结束的时候会清空表 spring.jpa.hibernate.ddl-auto:create-drop #每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新 spring.jpa.hibernate.ddl-auto:update #运行程序会校验数据与数据库的字段类型是否相同,不同会报错 spring.jpa.hibernate.ddl-auto:validate
37. Springboot中挂载静态资源(1)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.freemarker.request-context-attribute=request
spring.mvc.view.prefix=classpath:/templates/
spring.freemarker.suffix=.html
spring.freemarker.content-type=text/html
spring.freemarker.enabled=true
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.allow-request-override=false
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
(3) 在资源文件夹resources文件夹中,创建templates文件夹,并创建index.html文件
index.html文件中内容:
<html>
<head>
<title>测试页面</title>
</head>
<body>
hello
</body>
</html>
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@CrossOrigin
@Controller
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() throws Exception {
return "index";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
(6) 访问Web页面
地址:http://127.0.0.1:8310/index
38. Springboot中挂载静态资源(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.freemarker.request-context-attribute=request
spring.mvc.view.prefix=classpath:/templates/
spring.freemarker.suffix=.html
spring.freemarker.content-type=text/html
spring.freemarker.enabled=true
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.allow-request-override=false
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
(3) 创建资源文件夹static,并创建index.html文件
index.html文件中内容:
<html>
<head>
<title>测试页面</title>
</head>
<body>
hello
</body>
</html>
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@CrossOrigin
@Controller
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() throws Exception {
return "index";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
(6) 访问Web页面
地址:http://127.0.0.1:8310/index.html
39. Springboot(2.0)集成Feign传递Oauth2-Token服务
1.Eureka服务器端
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.application.name=eureka-server
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.instance.hostname = localhost
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
spring.security.user.name=admin
spring.security.user.password=123456
(3) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
2.服务提供者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8313
spring.application.name = producer
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建Entity包,并创建User.java文件
User.java文件中内容:
public class User {
private String name;
private String address;
public User() {
}
public User(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(4) 创建controller包,并创建UserApi.java文件
UserApi.java文件中内容:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import Entity.*;
@RequestMapping("user")
public interface UserApi {
@GetMapping("hello")
public String hello();
@GetMapping("findById")
public User findById(@RequestParam(value = "id")Integer id);
}
(5) 在controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.ResponseBody;
import Entity.*;
@Service
@ResponseBody
public class controller implements UserApi {
@Override
public String hello() {
System.out.println("我进来了");
return "我来了" ;
}
@Override
public User findById(Integer id) {
User user = new User();
user.setName("张三");
user.setAddress("广东");
return user;
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
@EnableEurekaClient
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
3.服务消费者
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<!--指定项目中公有的模块-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8390
spring.application.name=consumer
eureka.client.serviceUrl.defaultZone=http://admin:123456@localhost:8310/eureka/
eureka.client.healthcheck.enabled = true
(3) 创建Entity包,并创建User.java文件
User.java文件中内容:
public class User {
private String name;
private String address;
public User() {
}
public User(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(4) 创建service包,并创建UserApi.java文件
UserApi.java文件中内容:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import Entity.*;
@RequestMapping("user")
@Controller
public interface UserApi {
@GetMapping("hello")
@ResponseBody
public String hello();
@GetMapping("findById")
@ResponseBody
public User findById(@RequestParam(value = "id")Integer id);
}
(5) 在service包中创建ConsumerService.java文件
ConsumerService.java文件中内容:
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient(name = "producer")
public interface ConsumerService extends UserApi{
}
(6) 创建Interceptor包,并创建TokenFeignClientInterceptor.java文件
TokenFeignClientInterceptor.java文件中内容:
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import feign.RequestInterceptor;
import feign.RequestTemplate;
@Component
public class TokenFeignClientInterceptor implements RequestInterceptor {
/**
* token放在请求头.
* @param requestTemplate 请求参数
*/
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
if (requestAttributes != null) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//添加token
requestTemplate.header(HttpHeaders.AUTHORIZATION, request.getHeader(HttpHeaders.AUTHORIZATION));
}
}
}
(7) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.net.URI;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.discovery.EurekaClient;
import Entity.*;
import service.*;
@Service
@RestController
public class controller {
@Autowired
DiscoveryClient discoveryClient;
@Autowired
private ConsumerService consumerService;
@Autowired
EurekaClient eurekaClient;
@Autowired
LoadBalancerClient loadBalancerClient;
@GetMapping("hello")
public String getHello() {
return consumerService.hello();
}
@GetMapping("findById")
public User findById(Integer id) {
return consumerService.findById(id);
}
@GetMapping("client1")
public void getClient() {
List<String> list = discoveryClient.getServices();
for (String str : list) {
System.out.println(str);
}
}
@GetMapping("client2")
public Object getClient2() {
return discoveryClient.getInstances("producer");
}
@GetMapping("client3")
public Object getClient3() {
@SuppressWarnings("unchecked")
List<InstanceInfo> list = eurekaClient.getInstancesById("HWL-2020:producer:8313");
InstanceInfo instanceInfo = list.get(0);
if(instanceInfo.getStatus() == InstanceStatus.UP) {
String url = "http://" + instanceInfo.getHostName() + ":" + instanceInfo.getPort() + "/user/hello" ;
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
System.out.println(responseEntity.getBody());
}
return null;
}
@GetMapping("client4")
public Object getClient4() {
ServiceInstance serviceInstance = loadBalancerClient.choose("producer");
URI uri = serviceInstance.getUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri.toString()+"/user/hello", String.class);
return responseEntity.getBody();
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@EnableFeignClients("service")
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
**注:**在客户端调用提供者时头传递
Authorization信息
40. Springboot(2.0)集成数字计算验证码
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--验证码-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.google.code.kaptcha.Producer;
@CrossOrigin
@RestController
public class controller {
@Autowired
private Producer producer;
@RequestMapping(value = "/index",method = RequestMethod.GET)
public void Index(HttpServletResponse response) throws Exception {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
//生成文字验证码
String text = producer.createText();
//个位数字相加
String s1 = text.substring(0, 1);
String s2 = text.substring(1, 2);
int count = Integer.valueOf(s1).intValue() + Integer.valueOf(s2).intValue();
//生成图片验证码
BufferedImage image = producer.createImage(s1 + "+" + s2 + "=?");
//保存 redis key 自己设置
//stringRedisTemplate.opsForValue().set("",String.valueOf(count));
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
41. Springboot(2.0)中文件上传和下载(1)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.servlet.multipart.max-file-size=100
spring.servlet.multipart.max-request-size=100
(3) 创建Configure包,并创建Configure.java文件
Configure.java文件中内容:
import javax.servlet.MultipartConfigElement;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
@Configuration
public class Configure {
@Value("${spring.servlet.multipart.max-request-size}")
private Long maxRequestSize;
@Value("${spring.servlet.multipart.max-file-size}")
private Long maxFileSize;
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 置文件大小限制 ,超出此大小页面会抛出异常信息
factory.setMaxRequestSize(DataSize.ofMegabytes(maxRequestSize)); //KB,MB
// 设置总上传数据总大小
factory.setMaxRequestSize(DataSize.ofMegabytes(maxFileSize));
// 设置文件临时文件夹路径
// factory.setLocation("E://test//");
// 如果文件大于这个值,将以文件的形式存储,如果小于这个值文件将存储在内存中,默认为0
// factory.setMaxRequestSize(0);
return factory.createMultipartConfig();
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.*;
@RestController
public class Controller {
private static String s = System.getProperty("user.dir");
@PostMapping("/upload")
public String upload(@RequestParam(value = "file", required = false) MultipartFile file) {
if (file.isEmpty()) {
return "上传失败,请选择文件";
}
String fileName = file.getOriginalFilename();
String filePath = s+"/temp/";
File dest = new File(filePath + fileName);
File filefoder = new File(filePath);
if(!filefoder.exists()) {
filefoder.mkdirs();
}
try {
file.transferTo(dest);
return "上传成功";
} catch (IOException e) {
}
return "上传失败!";
}
@PostMapping("/multiUpload")
public String multiUpload(HttpServletRequest request) {
List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("file");
String filePath = s+"/temp/";
File filefoder = new File(filePath);
if(!filefoder.exists()) {
filefoder.mkdirs();
}
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
if (file.isEmpty()) {
return "上传第" + (i++) + "个文件失败";
}
String fileName = file.getOriginalFilename();
File dest = new File(filePath + fileName);
try {
file.transferTo(dest);
} catch (IOException e) {
return "上传第" + (i++) + "个文件失败";
}
}
return "上传成功";
}
//文件下载相关代码
@RequestMapping("/download")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
String fileName = "zookeeper-3.4.13.tar.gz";// 设置文件名,根据业务需要替换成要下载的文件名
System.out.println(request.getParameter("filename"));
if (fileName != null) {
//设置文件路径
String filePath = s+"/temp/";
File file = new File(filePath , fileName);
if (file.exists()) {
response.setContentType("application/force-download");// 设置强制下载不打开
response.addHeader("Content-Disposition", "attachment;fileName=" + fileName);// 设置文件名
byte[] buffer = new byte[1024];
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(file);
bis = new BufferedInputStream(fis);
OutputStream os = response.getOutputStream();
int i = bis.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
}
catch (Exception e) {
if (bis != null) {
try {
bis.close();
} catch (IOException ex) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException ex) {
e.printStackTrace();
}
}
}
}
}
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args){
SpringApplication.run(Start.class,args);
}
}
42. Springboot(2.0)中文件上传和下载(2)
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.*;
@RestController
public class Controller {
private static String s = System.getProperty("user.dir");
@PostMapping("/upload")
public String upload(@RequestParam(value = "file", required = false) MultipartFile file) {
if (file.isEmpty()) {
return "上传失败,请选择文件";
}
String fileName = file.getOriginalFilename();
String filePath = s+"/temp/";
File dest = new File(filePath + fileName);
File filefoder = new File(filePath);
if(!filefoder.exists()) {
filefoder.mkdirs();
}
try {
file.transferTo(dest);
return "上传成功";
} catch (IOException e) {
}
return "上传失败!";
}
@PostMapping("/multiUpload")
public String multiUpload(HttpServletRequest request) {
List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("file");
String filePath = s+"/temp/";
File filefoder = new File(filePath);
if(!filefoder.exists()) {
filefoder.mkdirs();
}
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
if (file.isEmpty()) {
return "上传第" + (i++) + "个文件失败";
}
String fileName = file.getOriginalFilename();
File dest = new File(filePath + fileName);
try {
file.transferTo(dest);
} catch (IOException e) {
return "上传第" + (i++) + "个文件失败";
}
}
return "上传成功";
}
//文件下载相关代码
@RequestMapping("/download")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
String fileName = "zookeeper-3.4.13.tar.gz";// 设置文件名,根据业务需要替换成要下载的文件名
System.out.println(request.getParameter("filename"));
if (fileName != null) {
//设置文件路径
String filePath = s+"/temp/";
File file = new File(filePath , fileName);
if (file.exists()) {
response.setContentType("application/force-download");// 设置强制下载不打开
response.addHeader("Content-Disposition", "attachment;fileName=" + fileName);// 设置文件名
byte[] buffer = new byte[1024];
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(file);
bis = new BufferedInputStream(fis);
OutputStream os = response.getOutputStream();
int i = bis.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
}
catch (Exception e) {
if (bis != null) {
try {
bis.close();
} catch (IOException ex) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException ex) {
e.printStackTrace();
}
}
}
}
}
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args){
SpringApplication.run(Start.class,args);
}
}
43. Springboot(2.0)中限制文件大小
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import javax.imageio.ImageIO;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.POST)
public String Index(@RequestParam("file") MultipartFile file) throws Exception {
if(file.isEmpty()) {
return "选择图片";
}
//检查文件大小
if(file.getSize() > 1*1024*1024) {
return "请上传1M以内的图片";
}
//检查是否是图片
BufferedImage bi = ImageIO.read(file.getInputStream());
if(bi == null){
return "上传的文件不是图片";
}
String originalFilename = file.getOriginalFilename();
String fileType = null;
if(originalFilename.contains(".")) {
fileType = originalFilename.substring(originalFilename.lastIndexOf("."));
} else {
fileType = ".jpg";
}
String filePath = "D://test//"; // 上传后的路径
String fileName = UUID.randomUUID() + fileType; // 新文件名
File dest = new File(filePath + fileName);
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
}
return "成功";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
44. Springboot(2.0)中检测上传文件名
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
(3) 创建Utils包,并创建FileTypeUtils.java文件
FileTypeUtils.java文件中内容:
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class FileTypeUtils {
// 默认判断文件头前三个字节内容
public static int default_check_length = 3;
final static HashMap<String, String> fileTypeMap = new HashMap<>();
// 初始化文件头类型,不够的自行补充
static {
fileTypeMap.put("ffd8ffe000104a464946", "jpg"); //JPEG (jpg)
fileTypeMap.put("89504e470d0a1a0a0000", "png"); //PNG (png)
fileTypeMap.put("47494638396126026f01", "gif"); //GIF (gif)
fileTypeMap.put("49492a00227105008037", "tif"); //TIFF (tif)
fileTypeMap.put("424d228c010000000000", "bmp"); //16色位图(bmp)
fileTypeMap.put("424d8240090000000000", "bmp"); //24位位图(bmp)
fileTypeMap.put("424d8e1b030000000000", "bmp"); //256色位图(bmp)
fileTypeMap.put("41433130313500000000", "dwg"); //CAD (dwg)
fileTypeMap.put("3c21444f435459504520", "html"); //HTML (html)
fileTypeMap.put("3c21646f637479706520", "htm"); //HTM (htm)
fileTypeMap.put("48544d4c207b0d0a0942", "css"); //css
fileTypeMap.put("696b2e71623d696b2e71", "js"); //js
fileTypeMap.put("7b5c727466315c616e73", "rtf"); //Rich Text Format (rtf)
fileTypeMap.put("38425053000100000000", "psd"); //Photoshop (psd)
fileTypeMap.put("46726f6d3a203d3f6762", "eml"); //Email [Outlook Express 6] (eml)
fileTypeMap.put("d0cf11e0a1b11ae10000", "doc"); //MS Excel 注意:word、msi 和 excel的文件头一样
fileTypeMap.put("d0cf11e0a1b11ae10000", "vsd"); //Visio 绘图
fileTypeMap.put("5374616E64617264204A", "mdb"); //MS Access (mdb)
fileTypeMap.put("252150532D41646F6265", "ps");
fileTypeMap.put("255044462d312e350d0a", "pdf"); //Adobe Acrobat (pdf)
fileTypeMap.put("2e524d46000000120001", "rmvb"); //rmvb/rm相同
fileTypeMap.put("464c5601050000000900", "flv"); //flv与f4v相同
fileTypeMap.put("00000020667479706d70", "mp4");
fileTypeMap.put("49443303000000002176", "mp3");
fileTypeMap.put("000001ba210001000180", "mpg"); //
fileTypeMap.put("3026b2758e66cf11a6d9", "wmv"); //wmv与asf相同
fileTypeMap.put("52494646e27807005741", "wav"); //Wave (wav)
fileTypeMap.put("52494646d07d60074156", "avi");
fileTypeMap.put("4d546864000000060001", "mid"); //MIDI (mid)
fileTypeMap.put("504b0304140000000800", "zip");
fileTypeMap.put("526172211a0700cf9073", "rar");
fileTypeMap.put("235468697320636f6e66", "ini");
fileTypeMap.put("504b03040a0000000000", "jar");
fileTypeMap.put("4d5a9000030000000400", "exe");//可执行文件
fileTypeMap.put("3c25402070616765206c", "jsp");//jsp文件
fileTypeMap.put("4d616e69666573742d56", "mf");//MF文件
fileTypeMap.put("3c3f786d6c2076657273", "xml");//xml文件
fileTypeMap.put("494e5345525420494e54", "sql");//xml文件
fileTypeMap.put("7061636b616765207765", "java");//java文件
fileTypeMap.put("406563686f206f66660d", "bat");//bat文件
fileTypeMap.put("1f8b0800000000000000", "gz");//gz文件
fileTypeMap.put("6c6f67346a2e726f6f74", "properties");//bat文件
fileTypeMap.put("cafebabe0000002e0041", "class");//bat文件
fileTypeMap.put("49545346030000006000", "chm");//bat文件
fileTypeMap.put("04000000010000001300", "mxp");//bat文件
fileTypeMap.put("504b0304140006000800", "docx");//docx文件
fileTypeMap.put("d0cf11e0a1b11ae10000", "wps");//WPS文字wps、表格et、演示dps都是一样的
fileTypeMap.put("6431303a637265617465", "torrent");
fileTypeMap.put("6D6F6F76", "mov"); //Quicktime (mov)
fileTypeMap.put("FF575043", "wpd"); //WordPerfect (wpd)
fileTypeMap.put("CFAD12FEC5FD746F", "dbx"); //Outlook Express (dbx)
fileTypeMap.put("2142444E", "pst"); //Outlook (pst)
fileTypeMap.put("AC9EBD8F", "qdf"); //Quicken (qdf)
fileTypeMap.put("E3828596", "pwl"); //Windows Password (pwl)
fileTypeMap.put("2E7261FD", "ram"); //Real Audio (ram)
}
/**
* @param fileName
* @return String
* @description 通过文件后缀名获取文件类型
*/
public static String getFileTypeBySuffix(String fileName) {
return fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
}
/**
* @param inputStream
* @return String
* @description 通过文件头魔数获取文件类型
*/
public static String getFileTypeByMagicNumber(InputStream inputStream) {
byte[] bytes = new byte[default_check_length];
try {
// 获取文件头前三位魔数的二进制
inputStream.read(bytes, 0, bytes.length);
// 文件头前三位魔数二进制转为16进制
String code = bytesToHexString(bytes);
for (Map.Entry<String, String> item : fileTypeMap.entrySet()) {
if (code.startsWith(item.getKey()) || item.getKey().startsWith(code)) {
return item.getValue();
}
}
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
public static String bytesToHexString(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
}
(4) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import Utils.FileTypeUtils;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.POST)
public String Index(@RequestParam("file") MultipartFile file) throws Exception {
String type = FileTypeUtils.getFileTypeByMagicNumber(file.getInputStream());
System.out.println(type);
String originalFilename = file.getOriginalFilename();
String fileType = null;
if(originalFilename.contains(".")) {
fileType = originalFilename.substring(originalFilename.lastIndexOf("."));
} else {
fileType = ".jpg";
}
String filePath = "D://test//"; // 上传后的路径
String fileName = UUID.randomUUID() + fileType; // 新文件名
File dest = new File(filePath + fileName);
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
}
return "成功";
}
}
(5) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
45. Springboot集成Zipkin分布式监控
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
#服务追踪
spring.zipkin.service.name=zipkin-test
spring.zipkin.base-url=http://127.0.0.1:9411/
spring.zipkin.discovery-client-enabled=true
spring.zipkin.sender.type=web
spring.sleuth.sampler.probability=1
(3) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index1() throws Exception {
return "成功";
}
}
(4) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
46. Springboot集成Came-Ftp文件定时同步
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-ftp</artifactId>
<version>2.22.1</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
ftp.img.url=ftp://192.168.1.6:21/disk/D?username=root&password=123456
ftp.img.dir=file:D:/test/img
ftp.file.url=ftp://192.168.1.7:21/disk/D?username=root&password=123456
ftp.file.dir=file:D:/test/file
#后台进程
camel.springboot.main-run-controller=true
(3) 创建Configure包,并创建FtpRouteBuilder.java文件
FtpRouteBuilder.java文件中内容:
import org.apache.camel.LoggingLevel;
import org.apache.camel.builder.RouteBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class FtpRouteBuilder extends RouteBuilder {
private static Logger logger = LoggerFactory.getLogger(FtpRouteBuilder.class );
@Value("${ftp.img.url}")
private String sftpServerImg;
@Value("${ftp.img.dir}")
private String downloadLocationImg;
@Value("${ftp.file.url}")
private String sftpServerFile;
@Value("${ftp.file.dir}")
private String downloadLocationFile;
@Autowired
private DataProcessor dataProcessor;
@Override
public void configure() throws Exception {
// from 内为 URL
from(sftpServerImg)
// 本地路径
.to(downloadLocationImg)
// 数据处理器
.process(dataProcessor)
.log(LoggingLevel.INFO, logger, "Download img ${file:name} complete.");
from(sftpServerFile)
.to(downloadLocationFile)
.log(LoggingLevel.INFO, logger, "Download file ${file:name} complete.");
}
}
(4) 在Configure包中创建ImgFilter.java文件
ImgFilter.java文件中内容:
import org.apache.camel.component.file.GenericFile;
import org.apache.camel.component.file.GenericFileFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ImgFilter implements GenericFileFilter<Object> {
@Value("${ftp.img.dir}")
private String fileDir;
@Override
public boolean accept(GenericFile<Object> genericFile) {
return genericFile.getFileName().endsWith(".jpg") || genericFile.isDirectory();
}
}
(5) 在Configure包中创建fileFilter.java文件
fileFilter.java文件中内容:
import org.apache.camel.component.file.GenericFile;
import org.apache.camel.component.file.GenericFileFilter;
public class fileFilter implements GenericFileFilter {
@Override
public boolean accept(GenericFile file) {
return file.getFileName().endsWith(".zip") || file.isDirectory();
}
}
(6) 在Configure包中创建DataProcessor.java文件
DataProcessor.java文件中内容:
import java.io.RandomAccessFile;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.component.file.GenericFileMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class DataProcessor implements Processor {
@Value("${ftp.img.dir}")
private String fileDir;
@SuppressWarnings("unchecked")
@Override
public void process(Exchange exchange) throws Exception {
try {
GenericFileMessage<RandomAccessFile> inMsg = (GenericFileMessage<RandomAccessFile>) exchange.getIn();
String fileName = inMsg.getGenericFile().getFileName();
System.out.println(fileName);
String sql = "insert into file(filename) values (?)";
} catch (Exception e) {
e.printStackTrace();
}
}
}
(7) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@CrossOrigin
@Controller
public class controller {
@RequestMapping(value = "/index",method = RequestMethod.GET)
@ResponseBody
public String Index() throws Exception {
return "成功";
}
}
(8) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
47. Springboot集成JustAuth第三方登录
支持Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么和推特等第三方平台的授权登录还在继续扩展。
配置示例:
justauth.enabled=true
justauth.type.github.client-id=101d*************8b3a
justauth.type.github.client-secret=58e*************************5edd
justauth.type.github.redirect-uri=http://127.0.0.1/demo/oauth/github/callback
justauth.type.qq.client-id=10******85
justauth.type.qq.client-secret=1f7d************************d629e
justauth.type.qq.redirect-uri=http://127.0.0.1/demo/oauth/qq/callback
justauth.type.wechat.client-id=wxdcb******4ff4
justauth.type.wechat.client-secret=b4e9dc************************a08ed6d
justauth.type.wechat.redirect-uri=http://127.0.0.1/demo/oauth/wechat/callback
justauth.type.google.client-id=716******17-6db******vh******ttj320i******userco******t.com
justauth.type.google.client-secret=9IBorn************7-E
justauth.type.google.redirect-uri=http://127.0.0.1/demo/oauth/google/callback
justauth.type.microsoft.client-id=7bdce8******************e194ad76c1b
justauth.type.microsoft.client-secret=Iu0zZ4************************tl9PWan_.
justauth.type.microsoft.redirect-uri=https://127.0.0.1/demo/oauth/microsoft/callback
justauth.type.mi.client-id=288************2994
justauth.type.mi.client-secret=nFeTt89************************==
justauth.type.mi.redirect-uri=http://127.0.0.1/demo/oauth/mi/callback
justauth.type.wechat_enterprise.client-id=ww58******f3************fbc
justauth.type.wechat_enterprise.client-secret=8G6PCr00j************************rgk************AyzaPc78
justauth.type.wechat_enterprise.redirect-uri=http://127.0.0.1/demo/oauth/wechat_enterprise/callback
justauth.type.wechat_enterprise.agent-id=1*******2
(1) 在Mysql中创建相应表
DROP TABLE IF EXISTS `justauth`.`t_ja_user`;
CREATE TABLE `justauth`.`t_ja_user` (
`uuid` varchar(64) NOT NULL COMMENT '用户第三方系统的唯一id',
`username` varchar(100) DEFAULT NULL COMMENT '用户名',
`nickname` varchar(100) DEFAULT NULL COMMENT '用户昵称',
`avatar` varchar(255) DEFAULT NULL COMMENT '用户头像',
`blog` varchar(255) DEFAULT NULL COMMENT '用户网址',
`company` varchar(50) DEFAULT NULL COMMENT '所在公司',
`location` varchar(255) DEFAULT NULL COMMENT '位置',
`email` varchar(50) DEFAULT NULL COMMENT '用户邮箱',
`gender` varchar(10) DEFAULT NULL COMMENT '性别',
`remark` varchar(500) DEFAULT NULL COMMENT '用户备注(各平台中的用户个人介绍)',
`source` varchar(20) DEFAULT NULL COMMENT '用户来源',
PRIMARY KEY (`uuid`)
);
(2) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
(3) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.1.8:3860/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
spring.datasource.username=mysql
spring.datasource.password=123456
spring.redis.host=192.168.1.8
spring.redis.port=8302
#spring.redis.password=123456
spring.redis.timeout=2000ms
spring.redis.database=0
spring.redis.lettuce.pool.maxActive=8
spring.redis.lettuce.pool.maxIdle=8
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.type-aliases-package=Entity
mybatis-plus.global-config.db-config.id-type=AUTO
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
logging.level.com.xkcoding=debug
justauth.enabled=true
justauth.type.BAIDU.client-id=xxxxxx
justauth.type.BAIDU.client-secret=xxxxxx
justauth.type.BAIDU.redirect-uri=http://127.0.0.1:8310/oauth/baidu/callback
justauth.type.GITEE.client-id=xxx
justauth.type.GITEE.client-secret=xxx
justauth.type.GITEE.redirect-uri=http://127.0.0.1:8310/oauth/gitee/callback
justauth.cache.type=redis
justauth.cache.prefix=JUATAUTH::STATE::
justauth.cache.timeout=3m
(4) 创建Configure包,并创建RedisConfig.java文件
RedisConfig.java文件中内容:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
Logger log = LoggerFactory.getLogger(RedisConfig.class);
@Bean(name = "redisTemplate")
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* 缓存错误处理
*/
@Bean
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.error("Redis occur handleCacheClearError:", e);
}
};
}
/**
* 注入Redis缓存配置类
*
* @return
*/
@Bean(name = { "redisCacheConfiguration" })
public RedisCacheConfiguration redisCacheConfiguration() {
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
return configuration;
}
}
(5) 在Configure包中创建JustAuthTokenCache.java文件
JustAuthTokenCache.java文件中内容:
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import com.alibaba.fastjson.JSONObject;
import me.zhyd.oauth.model.AuthToken;
@Configuration
public class JustAuthTokenCache {
@SuppressWarnings("rawtypes")
@Autowired
private RedisTemplate redisTemplate;
private BoundHashOperations<String, String, AuthToken> valueOperations;
@SuppressWarnings("unchecked")
@PostConstruct
public void init() {
valueOperations = redisTemplate.boundHashOps("JUSTAUTH::TOKEN");
}
/**
* 保存Token
*
* @param uuid 用户uuid
* @param authUser 授权用户
* @return
*/
public AuthToken saveorUpdate(String uuid, AuthToken authToken) {
valueOperations.put(uuid, authToken);
return authToken;
}
/**
* 根据用户uuid查询Token
*
* @param uuid 用户uuid
* @return
*/
public AuthToken getByUuid(String uuid) {
Object token = valueOperations.get(uuid);
if (null == token) {
return null;
}
return JSONObject.parseObject(JSONObject.toJSONString(token), AuthToken.class);
}
/**
* 查询所有Token
*
* @return
*/
public List<AuthToken> listAll() {
return new LinkedList<>(Objects.requireNonNull(valueOperations.values()));
}
/**
* 根据用户uuid移除Token
*
* @param uuid 用户uuid
* @return
*/
public void remove(String uuid) {
valueOperations.delete(uuid);
}
}
(6) 创建Entity包,并创建JustAuthUser.java文件
JustAuthUser.java文件中内容:
import java.io.Serializable;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
@TableName(value = "t_ja_user")
public class JustAuthUser extends AuthUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户第三方系统的唯一id。在调用方集成该组件时,可以用uuid + source唯一确定一个用户
*/
@TableId(type = IdType.INPUT)
private String uuid;
/**
* 用户授权的token信息
*/
@TableField(exist = false)
private AuthToken token;
/**
* 第三方平台返回的原始用户信息
*/
@TableField(exist = false)
private JSONObject rawUserInfo;
/**
* 自定义构造函数
*
* @param authUser 授权成功后的用户信息,根据授权平台的不同,获取的数据完整性也不同
*/
public JustAuthUser(AuthUser authUser) {
super(authUser.getUuid(), authUser.getUsername(), authUser.getNickname(), authUser.getAvatar(),
authUser.getBlog(), authUser.getCompany(), authUser.getLocation(), authUser.getEmail(),
authUser.getRemark(), authUser.getGender(), authUser.getSource(), authUser.getToken(),
authUser.getRawUserInfo());
}
}
(7) 创建Dao包,并创建JustAuthUserMapper.java文件
JustAuthUserMapper.java文件中内容:
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import Entity.*;
@Mapper
public interface JustAuthUserMapper extends BaseMapper<JustAuthUser> {
}
(8) 创建service包,并创建JustAuthUserService.java文件
JustAuthUserService.java文件中内容:
import com.baomidou.mybatisplus.extension.service.IService;
import Entity.*;
public interface JustAuthUserService extends IService<JustAuthUser> {
/**
* 保存或更新授权用户
*
* @param justAuthUser 授权用户
* @return
*/
boolean saveOrUpdate(JustAuthUser justAuthUser);
/**
* 根据用户uuid查询信息
*
* @param uuid 用户uuid
* @return
*/
JustAuthUser getByUuid(String uuid);
/**
* 根据用户uuid移除信息
*
* @param uuid 用户uuid
* @return
*/
boolean removeByUuid(String uuid);
}
(9) 在service包中创建JustAuthUserServiceImpl.java文件
JustAuthUserServiceImpl.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import Configure.JustAuthTokenCache;
import Dao.*;
import Entity.*;
@Service
public class JustAuthUserServiceImpl extends ServiceImpl<JustAuthUserMapper, JustAuthUser>
implements JustAuthUserService {
@Autowired
private JustAuthTokenCache justAuthTokenCache;
/**
* 保存或更新授权用户
*
* @param justAuthUser 授权用户
* @return
*/
@Override
public boolean saveOrUpdate(JustAuthUser justAuthUser) {
justAuthTokenCache.saveorUpdate(justAuthUser.getUuid(), justAuthUser.getToken());
return super.saveOrUpdate(justAuthUser);
}
/**
* 根据用户uuid查询信息
*
* @param uuid 用户uuid
* @return
*/
@Override
public JustAuthUser getByUuid(String uuid) {
JustAuthUser justAuthUser = super.getById(uuid);
if (justAuthUser != null) {
justAuthUser.setToken(justAuthTokenCache.getByUuid(uuid));
}
return justAuthUser;
}
/**
* 根据用户uuid移除信息
*
* @param uuid 用户uuid
* @return
*/
@Override
public boolean removeByUuid(String uuid) {
justAuthTokenCache.remove(uuid);
return super.removeById(uuid);
}
}
(10) 创建domain包,并创建ResultCode.java文件
ResultCode.java文件中内容:
import com.fasterxml.jackson.annotation.JsonFormat;
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ResultCode {
/*
请求返回状态码和说明信息
*/
SUCCESS(200, "成功"),
BAD_REQUEST(400, "参数或者语法不对"),
UNAUTHORIZED(401, "认证失败"),
LOGIN_ERROR(401, "登陆失败,用户名或密码无效"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "请求的资源不存在"),
OPERATE_ERROR(405, "操作失败,请求操作的资源不存在"),
TIME_OUT(408, "请求超时"),
SERVER_ERROR(500, "服务器内部错误"),
;
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public void setCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
(11) 在domain包中创建PageResult.java文件
PageResult.java文件中内容:
public class PageResult {
private int page;
private int rows;
private int total;
private Object data;
public PageResult(int page, int rows, int total, Object data) {
this.page = page;
this.rows = rows;
this.total = total;
this.data = data;
}
public void setPage(int page) {
this.page = page;
}
public int getPage() {
return page;
}
public void setRows(int rows) {
this.rows = rows;
}
public int getRows() {
return rows;
}
public void setTotal(int total) {
this.total = total;
}
public int getTotal() {
return total;
}
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
}
(12) 在domain包中创建ResultJson.java文件
ResultJson.java文件中内容:
public class ResultJson {
private int code;
private String msg;
private Object data;
public static ResultJson Success() {
return Success("");
}
public static ResultJson Success(Object o) {
return new ResultJson(ResultCode.SUCCESS, o);
}
public static ResultJson Failure(Object o) {
return new ResultJson(ResultCode.BAD_REQUEST, o);
}
public static ResultJson Failure(ResultCode code) {
return Failure(code, "");
}
public static ResultJson Failure(ResultCode code, Object o) {
return new ResultJson(code, o);
}
public ResultJson (ResultCode resultCode) {
setResultCode(resultCode);
}
public ResultJson (ResultCode resultCode,Object data) {
setResultCode(resultCode);
this.data = data;
}
public void setResultCode(ResultCode resultCode) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
}
public void setCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public ResultJson(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
@Override
public String toString() {
return "{" +
"\"code\":" + code +
", \"msg\":\"" + msg + '\"' +
", \"data\":\"" + data + '\"'+
'}';
}
}
(13) 创建controller包,并创建AuthController.java文件
AuthController.java文件中内容:
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSON;
import com.xkcoding.justauth.AuthRequestFactory;
import Entity.JustAuthUser;
import domain.ResultCode;
import domain.ResultJson;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import service.JustAuthUserService;
@RestController
@RequestMapping("/oauth")
public class AuthController {
Logger log = LoggerFactory.getLogger(AuthController.class);
@Autowired
private AuthRequestFactory factory;
@Autowired
private JustAuthUserService justAuthUserService;
/**
* 登录
*
* @param type 第三方系统类型,例如:gitee/baidu
* @param response
* @throws IOException
*/
@GetMapping("/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}
/**
* 登录回调
*
* @param type 第三方系统类型,例如:gitee/baidu
* @param callback
* @return
*/
@SuppressWarnings("unchecked")
@RequestMapping("/{type}/callback")
public ResultJson login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse<AuthUser> response = authRequest.login(callback);
log.info("【response】= {}", JSON.toJSONString(response));
if (response.ok()) {
justAuthUserService.saveOrUpdate(new JustAuthUser(response.getData()));
return ResultJson.Success(JSON.toJSONString(response));
}
return ResultJson.Failure(response.getMsg());
}
/**
* 收回
*
* @param type 第三方系统类型,例如:gitee/baidu
* @param uuid 用户uuid
* @return
*/
@SuppressWarnings("unchecked")
@RequestMapping("/revoke/{type}/{uuid}")
public ResultJson revoke(@PathVariable String type, @PathVariable String uuid) {
AuthRequest authRequest = factory.get(type);
JustAuthUser justAuthUser = justAuthUserService.getByUuid(uuid);
if (null == justAuthUser) {
return ResultJson.Failure("用户不存在");
}
AuthResponse<AuthToken> response = null;
try {
response = authRequest.revoke(justAuthUser.getToken());
if (response.ok()) {
justAuthUserService.removeByUuid(justAuthUser.getUuid());
return ResultJson.Success("用户 [" + justAuthUser.getUsername() + "] 的 授权状态 已收回!");
}
return ResultJson.Failure("用户 [" + justAuthUser.getUsername() + "] 的 授权状态 收回失败!" + response.getMsg());
} catch (Exception e) {
return ResultJson.Failure(ResultCode.BAD_REQUEST);
}
}
/**
* 刷新
*
* @param type 第三方系统类型,例如:gitee/baidu
* @param uuid 用户uuid
* @return
*/
@SuppressWarnings("unchecked")
@RequestMapping("/refresh/{type}/{uuid}")
@ResponseBody
public ResultJson refresh(@PathVariable String type, @PathVariable String uuid) {
AuthRequest authRequest = factory.get(type);
JustAuthUser justAuthUser = justAuthUserService.getByUuid(uuid);
if (null == justAuthUser) {
return ResultJson.Failure("用户不存在");
}
AuthResponse<AuthToken> response = null;
try {
response = authRequest.refresh(justAuthUser.getToken());
if (response.ok()) {
justAuthUser.setToken(response.getData());
justAuthUserService.saveOrUpdate(justAuthUser);
return ResultJson.Success("用户 [" + justAuthUser.getUsername() + "] 的 access token 已刷新!新的 accessToken: "
+ response.getData().getAccessToken());
}
return ResultJson.Failure("用户 [" + justAuthUser.getUsername() + "] 的 access token 刷新失败!" + response.getMsg());
} catch (Exception e) {
return ResultJson.Failure(ResultCode.BAD_REQUEST);
}
}
}
(14) 主启动目录中内容
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,service,controller")
@MapperScan("Dao")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
(15) 测试验证
[登录]:
浏览器访问地址:http://127.0.0.1:8443/oauth/login/baidu,即可获取到对应信息。
请观察数据库及Redis中数据变化。
[刷新Token]:
浏览器访问地址:http://127.0.0.1:8443/oauth/refresh/baidu/[uuid],其中uuid为数据库表中的uuid。
目前插件中已实现的可刷新的第三方应用有限,请查看AuthRequest.refresh方法的实现类。
请观察Redis中数据变化。
[移除Token]:
浏览器访问地址:http://127.0.0.1:8443/oauth/revoke/baidu/[uuid],其中uuid为数据库表中的uuid。
目前插件中已实现的可刷新的第三方应用有限,请查看AuthRequest.revoke方法的实现类。
请观察数据库及Redis中数据变化。
48. Springboot集成Xjar程序加密
Xjar基于对jar包内资源的加密以及拓展ClassLoader来构建的一套程序加密启动,动态解密运行的方案,避免源码泄露或反编译。它不需要侵入代码,只需要把编译好的jar包通过工具加密即可。
(1) 依赖包
<repositories>
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.core-lib</groupId>
<artifactId>xjar</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
</dependencies>
(2) 创建test.java文件
test.java文件中内容:
import io.xjar.XCryptos;
public class test {
public static void encrypt() throws Exception {
XCryptos.encryption()
//项目生成的jar
.from("F://Java//Test01//SpringStart.jar")
// 加密的密码
.use("testaa1111122222")
.include("/**/*.class")
.include("/**/*.xml")
.include("/**/*.yml")
.include("/**/*.properties")
.to("F://Java//Test01//test.jar");
}
public static void main(String[] args) throws Exception {
encrypt();
}
}
(3) 将生成文件xjar.go放入GoLang语言环境中编译
go bulid xjar.go
注:
xjar.go在Windows生成xjar.exe文件,在Linux生成xjar文件
(4) 启动Springboot项目
Windows启动:
xjar.exe java -jar test.jar
Linux启动:
nohup ./xjar java -jar test.jar
49. Springboot集成Log4j2操作Sentry异常监控系统
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions><!-- 去掉springboot默认配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-log4j2</artifactId>
<version>1.7.23</version>
</dependency>
<dependency> <!-- 引入log4j2依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
logging.config=classpath:log4j2.xml
(3) 在resources资源文件夹中创建sentry.properties文件
sentry.properties文件中内容:
stacktrace.app.packages=
dsn=http://edb111ae20f6464baf358d1b0c2662ae@192.168.0.142:9000/17
(4) 在resources资源文件夹中创建log4j2.xml文件
log4j2.xml文件中内容:
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="warn" packages="org.apache.logging.log4j.core,io.sentry.log4j2">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<Sentry name="Sentry"/>
</appenders>
<loggers>
<root level="INFO">
<appender-ref ref="Console" />
<!-- Note that the Sentry logging threshold is overridden to the WARN level -->
<appender-ref ref="Sentry" level="WARN" />
</root>
</loggers>
</configuration>
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() throws Exception {
logger.error("测试sentry打印error日志");
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
50. Springboot集成Logback操作Sentry异常监控系统
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-logback</artifactId>
<version>1.7.23</version>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
logging.config=classpath:logback.xml
(3) 在resources资源文件夹中创建sentry.properties文件
sentry.properties文件中内容:
stacktrace.app.packages=
dsn=http://edb111ae20f6464baf358d1b0c2662ae@192.168.0.142:9000/17
(4) 在resources资源文件夹中创建logback.xml文件
logback.xml文件中内容:
<configuration>
<!-- Configure the Console appender -->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Configure the Sentry appender, overriding the logging threshold to the WARN level -->
<appender name="Sentry" class="io.sentry.logback.SentryAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
</appender>
<!-- Enable the Console and Sentry appenders, Console is provided as an example
of a non-Sentry logger that is set to a different logging threshold -->
<root level="INFO">
<appender-ref ref="Console" />
<appender-ref ref="Sentry" />
</root>
</configuration>
(5) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
@RequestMapping(value = "/index",method = RequestMethod.GET)
public String Index() throws Exception {
logger.error("测试sentry打印error日志");
return "成功";
}
}
(6) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
51. Springboot集成LucySheet在线协同
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
(2) 创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port=8310
spring.freemarker.request-context-attribute=request
spring.freemarker.suffix=.html
spring.freemarker.content-type=text/html
spring.resources.static-locations=classpath:/resources/static/
spring.freemarker.enabled=true
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.allow-request-override=false
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
spring.data.mongodb.uri=mongodb://127.0.0.1:8892/wb?authSource=admin&readPreference=primary&ssl=false
(3) 在resuorces资源文件夹中创建templates文件夹,并创建websocket.html文件
websocket.html文件中内容:
<!DOCTYPE html>
<html>
<head lang='zh'>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="renderer" content="webkit"/>
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=0"/>
<title>websocket--luckysheet</title>
<link rel='stylesheet' href='/lib/plugins/css/pluginsCss.css'/>
<link rel='stylesheet' href='/lib/plugins/plugins.css'/>
<link rel='stylesheet' href='/lib/css/luckysheet.css'/>
<link rel='stylesheet' href='/lib/assets/iconfont/iconfont.css'/>
<script src="/lib/plugins/js/plugin.js"></script>
<script src="/lib/luckysheet.umd.js"></script>
</head>
<body>
<div id="${wb.option.container}"
style="margin:0px;padding:0px;position:absolute;width:100%;height:100%;left: 0px;top: 0px;"></div>
</div>
</body>
<script>
var localurl = "//" + window.location.host;
// 配置项
var options = {
container: "${wb.option.container}", // 设定DOM容器的id
title: "${wb.option.title}", // 设定表格名称
lang: "${wb.option.lang}",
allowUpdate: true,
showinfobar: true,//作用:是否显示顶部信息栏
functionButton: '<button id="" class="btn btn-primary" onclick="clicks()" style="padding:3px 6px;font-size: 12px;margin-right: 10px;">保存</button> <button id="" class="btn btn-primary btn-danger" style=" padding:3px 6px; font-size: 12px; margin-right: 85px;" onclick="downExcelData()">下载</button>',
loadUrl: window.location.protocol + localurl + "/load/${wb.id}",
loadSheetUrl: window.location.protocol + localurl + "/loadSheet/${wb.id}",
updateUrl: "ws://"+localurl + "/ws/" + Math.round(Math.random() * 100) + "/${wb.id}"
// 更多其他设置...
}
// 初始化表格
luckysheet.create(options)
function uploadExcelData() {
//console.log(luckysheet.getAllSheets());
//console.log("lll=" + JSON.stringify(luckysheet.getAllSheets()));
//上传例子,可以把这个数据保存到服务器上。下次可以从服务器直接加载luckysheet数据了。
$.post("/excel/uploadData", {
exceldatas: JSON.stringify(luckysheet.getAllSheets()),
title: options.title,
}, function (data) {
//console.log("data = " + data)
alert("保存成功!")
});
}
function downExcelData() {
//这里你要自己写个后台接口,处理上传上来的Excel数据,用post传输。我用的是Java后台处理导出!这里只是写了post请求的写法
$.post("/excel/downfile", {
exceldatas: JSON.stringify(luckysheet.getAllSheets()),
}, function (data) {
//console.log("data = " + data)
});
}
</script>
</html>
(4) 创建资源文件夹static文件夹,将LuckySheet相关资源放入
(5) 创建utils包,并创建PakoGzipUtils.java文件
PakoGzipUtils.java文件中内容:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class PakoGzipUtils {
public static String compress(String str) throws IOException {
if (str == null || str.length() == 0) {
return str;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(out);
gzip.write(str.getBytes());
gzip.close();
return out.toString("ISO-8859-1");
}
/**
* @param str:
* @return 解压字符串 生成正常字符串。
* @throws IOException
*/
public static String uncompress(String str) throws IOException {
if (str == null || str.length() == 0) {
return str;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(str.getBytes("ISO-8859-1"));
GZIPInputStream gunzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = gunzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
// toString()使用平台默认编码,也可以显式的指定如toString("GBK")
return out.toString();
}
/**
* @param jsUriStr :字符串类型为
* @return 生成正常字符串
* @throws IOException
*/
public static String unCompressURI(String jsUriStr) throws IOException {
String gzipCompress=uncompress(jsUriStr);
String result=URLDecoder.decode(gzipCompress,"UTF-8");
return result;
}
/**
* @param strData :字符串类型为: 正常字符串
* @return 生成字符串类型为:
*/
public static String compress2URI(String strData) throws IOException {
String encodeGzip=compress(strData);
String jsUriStr=URLEncoder.encode(encodeGzip,"UTF-8");
return jsUriStr;
}
}
(6) 在utils包中创建SheetUtil.java文件
SheetUtil.java文件中内容:
import java.util.ArrayList;
import java.util.List;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONObject;
public class SheetUtil {
/**
* 获取sheet的默认option
* @return
*/
public static JSONObject getDefautOption() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("container", "ecsheet");
jsonObject.put("title", "ecsheet demo");
jsonObject.put("lang", "zh");
jsonObject.put("allowUpdate", true);
jsonObject.put("loadUrl", "");
jsonObject.put("loadSheetUrl", "");
jsonObject.put("updateUrl", "");
return jsonObject;
}
/**
* 获取默认的sheetData
* @return
*/
public static List<JSONObject> getDefaultSheetData() {
List<JSONObject> list = new ArrayList<>();
for (int i = 1; i < 4; i++) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("row", 84);
jsonObject.put("column", 60);
jsonObject.put("name", "sheet" + i);
Integer index = i - 1;
jsonObject.put("index", IdUtil.simpleUUID());
jsonObject.put("order", i - 1);
if (i == 1) {
jsonObject.put("status", 1);
} else {
jsonObject.put("status", 0);
}
jsonObject.put("celldata", new ArrayList<JSONObject>() {
});
list.add(jsonObject);
}
return list;
}
/**
* 获取默认的全部sheetData
* @return
*/
public static JSONObject getDefaultAllSheetData() {
JSONObject result = new JSONObject();
for (int i = 1; i < 4; i++) {
JSONObject data = new JSONObject();
data.put("r", 0);
data.put("c", 0);
data.put("v", new JSONObject());
result.put("sheet" + i, data);
}
return result;
}
/**
* 组装异步加载sheet所需的数据
*
* @param data
* @return
*/
public static JSONObject buildSheetData(List<JSONObject> data) {
JSONObject result = new JSONObject();
data.forEach((d) -> {
result.put(d.get("index").toString(), d.get("celldata"));
});
return result;
}
}
(7) 创建service包,并创建IMessageProcess.java文件
IMessageProcess.java文件中内容:
import cn.hutool.json.JSONObject;
public interface IMessageProcess {
void process(String gridKey,JSONObject message);
}
(8) 在service包中创建MessageProcess.java文件
MessageProcess.java文件中内容:
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import entity.WorkBookEntity;
import entity.WorkSheetEntity;
import repository.WorkBookRepository;
import repository.WorkSheetRepository;
@Service
public class MessageProcess implements IMessageProcess {
@Autowired
private WorkSheetRepository workSheetRepository;
@Autowired
private WorkBookRepository workBookRepository;
@Override
public void process(String wbId, JSONObject message) {
//获取操作名
String action = message.getStr("t");
//获取sheet的index值
String index = message.getStr("i");
//如果是复制sheet,index的值需要另取
if ("shc".equals(action)) {
index = message.getJSONObject("v").getStr("copyindex");
}
//如果是删除sheet,index的值需要另取
if ("shd".equals(action)) {
index = message.getJSONObject("v").getStr("deleIndex");
}
//如果是恢复sheet,index的值需要另取
if ("shre".equals(action)) {
index = message.getJSONObject("v").getStr("reIndex");
}
WorkSheetEntity ws = workSheetRepository.findByindexAndwbId(index, wbId);
System.out.println("操作:"+action);
switch (action) {
//单个单元格刷新
case "v":
ws = singleCellRefresh(ws, message);
break;
//范围单元格刷新
case "rv":
ws = rangeCellRefresh(ws, message);
break;
//config操作
case "cg":
ws = configRefresh(ws, message);
break;
//通用保存
case "all":
ws = allRefresh(ws, message);
break;
//函数链操作
case "fc":
ws = calcChainRefresh(ws, message);
break;
//删除行或列
case "drc":
ws = drcRefresh(ws, message);
break;
//增加行或列
case "arc":
ws = arcRefresh(ws, message);
break;
//清除筛选
case "fsc":
ws = fscRefresh(ws, message);
break;
//恢复筛选
case "fsr":
ws = fscRefresh(ws, message);
break;
//新建sheet
case "sha":
ws = shaRefresh(wbId, message);
break;
//切换到指定sheet
case "shs":
shsRefresh(wbId, message);
break;
//复制sheet
case "shc":
ws = shcRefresh(ws, message);
break;
//修改工作簿名称
case "na":
naRefresh(wbId, message);
break;
//删除sheet
case "shd":
ws.setDeleteStatus(1);
break;
//删除sheet后恢复操作
case "shre":
ws.setDeleteStatus(0);
break;
//调整sheet位置
case "shr":
shrRefresh(wbId, message);
break;
//sheet属性(隐藏或显示)
case "sh":
ws = shRefresh(ws, message);
break;
default:
break;
}
if (ObjectUtil.isNull(ws)) {
return;
}
workSheetRepository.save(ws);
}
/**
* 单个单元格刷新
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity singleCellRefresh(WorkSheetEntity ws, JSONObject message) {
//对celldata进行深拷贝
JSONArray celldata = ObjectUtil.cloneByStream(ws.getData().getJSONArray("celldata"));
if (StrUtil.isBlank(message.getStr("v"))) {
celldata.forEach(c -> {
JSONObject jsonObject = JSONUtil.parseObj(c);
if (!jsonObject.isEmpty()) {
if (jsonObject.getLong("r") == message.getLong("r") && jsonObject.getLong("c") == message.getLong("c")) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
}
}
});
} else {
JSONObject collectData = JSONUtil.createObj().put("r", message.getLong("r")).put("c", message.getLong("c")).put("v", message.getJSONObject("v"));
List<String> flag = new ArrayList<>();
celldata.forEach(c -> {
JSONObject jsonObject = JSONUtil.parseObj(c);
if (!jsonObject.isEmpty()) {
if (jsonObject.getLong("r") == message.getLong("r") && jsonObject.getLong("c") == message.getLong("c")) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
ws.getData().getJSONArray("celldata").add(collectData);
flag.add("used");
}
}
});
if (flag.isEmpty()) {
ws.getData().getJSONArray("celldata").add(collectData);
}
}
return ws;
}
/**
* 范围单元格刷新
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity rangeCellRefresh(WorkSheetEntity ws, JSONObject message) {
JSONArray rowArray = message.getJSONObject("range").getJSONArray("row");
JSONArray columnArray = message.getJSONObject("range").getJSONArray("column");
JSONArray vArray = message.getJSONArray("v");
JSONArray celldata = ObjectUtil.cloneByStream(ws.getData().getJSONArray("celldata"));
int countRowIndex = 0;
//遍历行列,对符合行列的内容进行更新
for (int ri = (int) rowArray.get(0); ri <= (int) rowArray.get(1); ri++) {
int countColumnIndex = 0;
for (int ci = (int) columnArray.get(0); ci <= (int) columnArray.get(1); ci++) {
List<String> flag = new ArrayList<>();
Object newCell = JSONUtil.parseArray(vArray.get(countRowIndex)).get(countColumnIndex);
JSONObject collectData = JSONUtil.createObj().put("r", ri).put("c", ci).put("v", newCell);
int rowIndex = ri;
int columnIndex = ci;
celldata.forEach(cell -> {
JSONObject jsonObject = JSONUtil.parseObj(cell);
if (!jsonObject.isEmpty()) {
if (jsonObject.getInt("r") == rowIndex && jsonObject.getInt("c") == columnIndex) {
if ("null".equals(newCell.toString()) || JSONUtil.parseObj(newCell).isEmpty()) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
} else {
ws.getData().getJSONArray("celldata").remove(jsonObject);
ws.getData().getJSONArray("celldata").add(collectData);
}
flag.add("used");
}
}
});
if (flag.isEmpty() && !JSONUtil.parseObj(newCell).isEmpty()) {
ws.getData().getJSONArray("celldata").add(collectData);
}
countColumnIndex++;
}
countRowIndex++;
}
return ws;
}
/**
* config更新
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity configRefresh(WorkSheetEntity ws, JSONObject message) {
JSONObject v = message.getJSONObject("v");
JSONObject newConfig = JSONUtil.createObj().put(message.getStr("k"), v);
System.out.println(ws.getData());
if(ws.getData().getJSONObject("config") != null) {
if (ws.getData().getJSONObject("config").isEmpty()) {
ws.getData().put("config", newConfig);
} else {
ws.getData().getJSONObject("config").put(message.getStr("k"), v);
}
}
return ws;
}
/**
* 通用保存
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity allRefresh(WorkSheetEntity ws, JSONObject message) {
if (message.getJSONObject("v").isEmpty()) {
ws.getData().remove(message.getStr("k"));
} else {
ws.getData().put(message.getStr("k"), message.getJSONObject("v"));
}
return ws;
}
/**
* 函数链操作
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity calcChainRefresh(WorkSheetEntity ws, JSONObject message) {
JSONObject value = message.getJSONObject("v");
if (!ws.getData().containsKey("calcChain")) {
ws.getData().put("calcChain", new JSONArray());
}
JSONArray calcChain = ws.getData().getJSONArray("calcChain");
if ("add".equals(message.getStr("op"))) {
calcChain.add(value);
} else if ("update".equals(message.getStr("op"))) {
calcChain.remove(calcChain.get(message.getInt(message.getStr("pos"))));
calcChain.add(value);
} else if ("del".equals(message.getStr("op"))) {
calcChain.remove(calcChain.get(message.getInt(message.getStr("pos"))));
}
return ws;
}
/**
* 删除行或列
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity drcRefresh(WorkSheetEntity ws, JSONObject message) {
JSONArray celldata = ObjectUtil.cloneByStream(ws.getData().getJSONArray("celldata"));
int index = message.getJSONObject("v").getInt("index");
int len = message.getJSONObject("v").getInt("len");
if ("r".equals(message.getStr("rc"))) {
ws.getData().put("row", ws.getData().getInt("row") - len);
} else {
ws.getData().put("column", ws.getData().getInt("column") - len);
}
for (Object cell : celldata) {
JSONObject jsonObject = JSONUtil.parseObj(cell);
if ("r".equals(message.getStr("rc"))) {
//删除行所在区域的内容
if (jsonObject.getInt("r") >= index && jsonObject.getInt("r") < index + len) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
}
//增加大于 最大删除行的的行号
if (jsonObject.getInt("r") >= index + len) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("r", jsonObject.getInt("r") - len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
} else {
//删除列所在区域的内容
if (jsonObject.getInt("c") >= index && jsonObject.getInt("c") < index + len) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
}
//增加大于 最大删除列的的列号
if (jsonObject.getInt("c") >= index + len) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("c", jsonObject.getInt("c") - len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
}
}
return ws;
}
/**
* 增加行或列,暂未实现插入数据的情况
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity arcRefresh(WorkSheetEntity ws, JSONObject message) {
JSONArray celldata = ObjectUtil.cloneByStream(ws.getData().getJSONArray("celldata"));
int index = message.getJSONObject("v").getInt("index");
int len = message.getJSONObject("v").getInt("len");
for (Object cell : celldata) {
JSONObject jsonObject = JSONUtil.parseObj(cell);
if ("r".equals(message.getStr("rc"))) {
//如果是增加行,且是向左增加
if (jsonObject.getInt("r") >= index && "lefttop".equals(message.getJSONObject("v").getStr("direction"))) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("r", jsonObject.getInt("r") + len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
//如果是增加行,且是向右增加
if (jsonObject.getInt("r") > index && "rightbottom".equals(message.getJSONObject("v").getStr("direction"))) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("r", jsonObject.getInt("r") + len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
} else {
//如果是增加列,且是向上增加
if (jsonObject.getInt("c") >= index && "lefttop".equals(message.getJSONObject("v").getStr("direction"))) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("c", jsonObject.getInt("c") + len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
//如果是增加列,且是向下增加
if (jsonObject.getInt("c") > index && "rightbottom".equals(message.getJSONObject("v").getStr("direction"))) {
ws.getData().getJSONArray("celldata").remove(jsonObject);
jsonObject.put("c", jsonObject.getInt("c") + len);
ws.getData().getJSONArray("celldata").add(jsonObject);
}
}
}
JSONArray vArray = message.getJSONObject("v").getJSONArray("data");
if ("r".equals(message.getStr("rc"))) {
ws.getData().put("row", ws.getData().getInt("row") + len);
for (int r = 0; r < vArray.size(); r++) {
for (int c = 0; c < JSONUtil.parseArray(vArray.get(0)).size(); c++) {
if (JSONUtil.parseArray(vArray.get(r)).get(c) == null) {
continue;
}
JSONObject newCell = JSONUtil.createObj().put("r", r + index).put("c", c).put("v", JSONUtil.parseArray(vArray.get(r)).get(c));
ws.getData().getJSONArray("celldata").add(newCell);
}
}
} else {
ws.getData().put("column", ws.getData().getInt("column") + len);
for (int r = 0; r < vArray.size(); r++) {
for (int c = 0; c < JSONUtil.parseArray(vArray.get(0)).size(); c++) {
if (JSONUtil.parseArray(vArray.get(r)).get(c) == null) {
continue;
}
JSONObject newCell = JSONUtil.createObj().put("r", r).put("c", c + index).put("v", JSONUtil.parseArray(vArray.get(r)).get(c));
ws.getData().getJSONArray("celldata").add(newCell);
}
}
}
return ws;
}
/**
* 筛选操作
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity fscRefresh(WorkSheetEntity ws, JSONObject message) {
if (message.getJSONObject("v").isEmpty()) {
ws.getData().remove("filter");
ws.getData().remove("filter_select");
} else {
ws.getData().put("filter", message.getJSONObject("v").getJSONArray("filter"));
ws.getData().put("filter_select", message.getJSONObject("v").getJSONObject("filter_select"));
}
return ws;
}
/**
* 新建sheet
*
* @param wbId
* @param message
* @return
*/
private WorkSheetEntity shaRefresh(String wbId, JSONObject message) {
WorkSheetEntity ws = new WorkSheetEntity();
ws.setWbId(wbId);
ws.setData(message.getJSONObject("v"));
return ws;
}
/**
* 复制sheet
*
* @param ws
* @param message
* @return
*/
private WorkSheetEntity shcRefresh(WorkSheetEntity ws, JSONObject message) {
String index = message.getStr("i");
ws.getData().put("index", index);
ws.getData().put("name", message.getJSONObject("v").getStr("name"));
return ws;
}
/**
* 调整sheet位置
*
* @param wbId
* @param message
*/
private void shrRefresh(String wbId, JSONObject message) {
List<WorkSheetEntity> allSheets = workSheetRepository.findAllBywbId(wbId);
allSheets.forEach(sheet -> {
sheet.getData().put("order", message.getJSONObject("v").getInt(sheet.getData().getStr("index")));
workSheetRepository.save(sheet);
});
}
/**
* 切换到指定sheet
*
* @param ws
* @param message
* @return
*/
private void shsRefresh(String wbId, JSONObject message) {
WorkSheetEntity lastWs = workSheetRepository.findBystatusAndwbId(1, wbId);
lastWs.getData().put("status", 0);
WorkSheetEntity thisWs = workSheetRepository.findByindexAndwbId(message.getStr("v"), wbId);
thisWs.getData().put("status", 1);
workSheetRepository.save(lastWs);
workSheetRepository.save(thisWs);
}
/**
* sheet属性(隐藏或显示)
*
* @param wbId
* @param message
*/
private WorkSheetEntity shRefresh(WorkSheetEntity ws, JSONObject message) {
Integer hideStatus = message.getInt("v");
ws.getData().put("hide", hideStatus);
WorkSheetEntity curWs = new WorkSheetEntity();
if ("hide".equals(message.getStr("op"))) {
ws.getData().put("status", 0);
String cur = message.getStr("cur");
curWs = workSheetRepository.findByindexAndwbId(cur, ws.getWbId());
curWs.getData().put("status", 1);
} else {
curWs = workSheetRepository.findBystatusAndwbId(1, ws.getWbId());
curWs.getData().put("status", 0);
}
workSheetRepository.save(curWs);
return ws;
}
/**
* 修改工作簿名称
*
* @param wbId
* @param message
* @return
*/
private void naRefresh(String wbId, JSONObject message) {
Optional<WorkBookEntity> wb = workBookRepository.findById(wbId);
if (wb.isPresent()) {
WorkBookEntity workBookEntity = wb.get();
workBookEntity.getOption().put("title", message.getStr("v"));
workBookRepository.save(workBookEntity);
}
}
}
(9) 创建common包,并创建ResponseDTO.java文件
ResponseDTO.java文件中内容:
public class ResponseDTO {
private Integer type;
private String id;
private String username;
private String data;
public ResponseDTO(Integer type, String id, String username, String data) {
this.type = type;
this.id = id;
this.username = username;
this.data = data;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public static ResponseDTO success(String id,String username,String data) {
return new ResponseDTO(1,id,username, data);
}
public static ResponseDTO update(String id,String username,String data) {
return new ResponseDTO(2,id,username, data);
}
public static ResponseDTO mv(String id,String username,String data) {
return new ResponseDTO(3,id,username, data);
}
public static ResponseDTO bulkUpdate(String id,String username,String data) {
return new ResponseDTO(4,id,username ,data);
}
}
(10) 创建entity包,并创建WorkBookEntity.java文件
WorkBookEntity.java文件中内容:
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import cn.hutool.json.JSONObject;
@Document(collection = "workbook")
public class WorkBookEntity {
@Id
private String id;
private String name;
private JSONObject option;
public WorkBookEntity() {
}
public WorkBookEntity(String id, String name, JSONObject option) {
this.id = id;
this.name = name;
this.option = option;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public JSONObject getOption() {
return option;
}
public void setOption(JSONObject option) {
this.option = option;
}
}
(11) 在entity包中创建WorkSheetEntity.java文件
WorkSheetEntity.java文件中内容:
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import cn.hutool.json.JSONObject;
@Document(collection = "worksheet")
public class WorkSheetEntity {
@Id
private String id;
private String wbId;
private JSONObject data;
/**
* 删除标记,0是未删除,1是删除
*/
private int deleteStatus;
public WorkSheetEntity() {
}
public WorkSheetEntity(String id, String wbId, JSONObject data, int deleteStatus) {
this.id = id;
this.wbId = wbId;
this.data = data;
this.deleteStatus = deleteStatus;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getWbId() {
return wbId;
}
public void setWbId(String wbId) {
this.wbId = wbId;
}
public JSONObject getData() {
return data;
}
public void setData(JSONObject data) {
this.data = data;
}
public int getDeleteStatus() {
return deleteStatus;
}
public void setDeleteStatus(int deleteStatus) {
this.deleteStatus = deleteStatus;
}
}
(12) 创建repository包,并创建WorkBookRepository.java文件
WorkBookRepository.java文件中内容:
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import entity.*;
@Repository
public interface WorkBookRepository extends MongoRepository<WorkBookEntity,String> {
}
(13) 在repository包中创建WorkSheetRepository.java文件
WorkSheetRepository.java文件中内容:
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;
import entity.*;
import java.util.List;
@Repository
public interface WorkSheetRepository extends MongoRepository<WorkSheetEntity,String> {
@Query(value = "{'wbId':?0,'deleteStatus':0}")
List<WorkSheetEntity> findAllBywbId(String wbId);
@Query(value = "{'data.index':?0,'wbId':?1}")
WorkSheetEntity findByindexAndwbId(String index,String wbId);
@Query(value = "{'data.status':?0,'wbId':?1}")
WorkSheetEntity findBystatusAndwbId(int status,String wbId);
}
(14) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import com.alibaba.fastjson.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import entity.*;
import repository.*;
import utils.*;
@CrossOrigin
@RestController
public class controller {
@Autowired
private WorkBookRepository workBookRepository;
@Autowired
private WorkSheetRepository workSheetRepository;
@GetMapping("index/create")
public void create(HttpServletRequest request, HttpServletResponse response) throws IOException {
WorkBookEntity wb = new WorkBookEntity();
wb.setName("default");
wb.setOption(SheetUtil.getDefautOption());
WorkBookEntity saveWb = workBookRepository.save(wb);
//生成sheet数据
generateSheet(saveWb.getId());
response.sendRedirect("/index/" + saveWb.getId());
}
@GetMapping("/index/{wbId}")
public ModelAndView index(@PathVariable(value = "wbId") String wbId) {
Optional<WorkBookEntity> Owb = workBookRepository.findById(wbId);
WorkBookEntity wb = new WorkBookEntity();
if (!Owb.isPresent()) {
wb.setId(wbId);
wb.setName("default");
wb.setOption(SheetUtil.getDefautOption());
WorkBookEntity result = workBookRepository.save(wb);
generateSheet(wbId);
} else {
wb = Owb.get();
}
return new ModelAndView("websocket", "wb", wb);
}
@PostMapping("/load/{wbId}")
public String load(@PathVariable(value = "wbId") String wbId) {
List<WorkSheetEntity> wsList = workSheetRepository.findAllBywbId(wbId);
List<JSONObject> list = new ArrayList<JSONObject>();
wsList.forEach(ws -> {
list.add(ws.getData());
});
return JSONUtil.toJsonStr(list);
}
@PostMapping("/loadSheet/{wbId}")
public String loadSheet(@PathVariable(value = "wbId") String wbId) {
List<WorkSheetEntity> wsList = workSheetRepository.findAllBywbId(wbId);
List<JSONObject> list = new ArrayList<>();
wsList.forEach(ws -> {
list.add(ws.getData());
});
if (!list.isEmpty()) {
return SheetUtil.buildSheetData(list).toString();
}
return SheetUtil.getDefaultAllSheetData().toString();
}
private void generateSheet(String wbId) {
SheetUtil.getDefaultSheetData().forEach(jsonObject -> {
WorkSheetEntity ws = new WorkSheetEntity();
ws.setWbId(wbId);
ws.setData(jsonObject);
ws.setDeleteStatus(0);
workSheetRepository.save(ws);
});
}
}
(15) 创建config包,并创建WebSocketConfig.java文件
WebSocketConfig.java文件中内容:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(16) 在controller包中创建WebSocketServer.java文件
WebSocketServer.java文件中内容:
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.PostConstruct;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import common.*;
import service.*;
import utils.*;
@ServerEndpoint("/ws/{userId}/{gridKey}")
@Component
public class WebSocketServer {
static Log log = LogFactory.get(WebSocketServer.class);
private static WebSocketServer webSocketServer;
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, Map<String, WebSocketServer>> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = "";
/**
* 表格主键
*/
private String gridKey = "";
@Autowired
private IMessageProcess messageProcess;
@PostConstruct //通过@PostConstruct实现初始化bean之前进行的操作
public void init() {
webSocketServer = this;
webSocketServer.messageProcess = this.messageProcess;
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId, @PathParam("gridKey") String gridKey) {
this.session = session;
this.userId = userId;
this.gridKey = gridKey;
if (webSocketMap.containsKey(gridKey)) {
webSocketMap.get(gridKey).put(userId, this);
} else {
Map<String, WebSocketServer> map = new HashMap<>();
map.put(userId, this);
webSocketMap.put(gridKey, map);
}
addOnlineCount();
log.info("用户连接:" + userId + ",打开的表格为:" + gridKey + ",当前在线人数为:" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(this.gridKey)) {
webSocketMap.get(this.gridKey).remove(this.userId);
if (webSocketMap.get(this.gridKey).isEmpty()) {
webSocketMap.remove(this.gridKey);
}
}
subOnlineCount();
log.info("用户退出:" + this.userId + ",打开的表格为:" + this.gridKey + ",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
//可以群发消息
//消息保存到数据库、redis
if (StrUtil.isNotBlank(message)) {
try {
if ("rub".equals(message)) {
return;
}
String unMessage = PakoGzipUtils.unCompressURI(message);
log.info("用户消息:" + userId + ",报文:" + unMessage);
JSONObject jsonObject = JSONUtil.parseObj(unMessage);
if (!"mv".equals(jsonObject.getStr("t"))) {
webSocketServer.messageProcess.process(this.gridKey, jsonObject);
}
Map<String, WebSocketServer> sessionMap = webSocketMap.get(this.gridKey);
if (StrUtil.isNotBlank(unMessage)) {
sessionMap.forEach((key, value) -> {
//广播到除了发送者外的其它连接端
if (!key.equals(this.userId)) {
try {
//如果是mv,代表发送者的表格位置信息
if ("mv".equals(jsonObject.getStr("t"))) {
value.sendMessage(JSONUtil.toJsonStr(ResponseDTO.mv(userId, userId, unMessage)));
//如果是切换sheet,则不发送信息
} else if(!"shs".equals(jsonObject.getStr("t"))) {
value.sendMessage(JSONUtil.toJsonStr(ResponseDTO.update(userId, userId, unMessage)));
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException, EncodeException {
this.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
(17) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
@EnableAutoConfiguration
@ComponentScan("config,service,controller")
@EnableMongoRepositories(basePackages = {"repository"})
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
52. Springboot集成Mqtt服务
(1) 依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
</parent>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
</dependencies>
(2) 主启动目录中创建资源文件夹resources文件夹,并创建application.properties文件
application.properties文件中内容:
server.port = 8310
spring.mqtt.username=admin
spring.mqtt.password=public
## 推送信息的连接地址,如果有多个,用逗号隔开
spring.mqtt.url=tcp://127.0.0.1:1883
spring.mqtt.client.id=${random.value}
spring.mqtt.default.topic=topic
(3) 创建Configure包,并创建customerConfigure.java文件
customerConfigure.java文件中内容:
mport org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
@Configuration
public class MqttConfigure {
@Value("${spring.mqtt.username}")
private String username;
@Value("${spring.mqtt.password}")
private String password;
@Value("${spring.mqtt.url}")
private String hostUrl;
@Value("${spring.mqtt.client.id}")
private String clientId;
@Value("${spring.mqtt.default.topic}")
private String defaultTopic;
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setUserName(username);
factory.setPassword(password);
factory.setServerURIs(new String[]{hostUrl});
factory.setKeepAliveInterval(2);
return factory;
}
//接收通道
@Bean
public MessageChannel mqttInputChannel() {
return new DirectChannel();
}
//配置client,监听的topic
@Bean
public MessageProducer inbound() {
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(clientId, mqttClientFactory(),defaultTopic);
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(1);
adapter.setOutputChannel(mqttInputChannel());
return adapter;
}
//通过通道获取数据
@Bean
@ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
return new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
System.out.println("接收消息内容 : " + message.getPayload());
}
};
}
}
(4) 在Configure包中创建providerConfigure.java文件
providerConfigure.java文件中内容:
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.MqttPersistenceException;
import org.eclipse.paho.client.mqttv3.MqttTopic;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class providerConfigure {
@Value("${spring.mqtt.username}")
private String username;
@Value("${spring.mqtt.password}")
private String password;
@Value("${spring.mqtt.url}")
private String hostUrl;
@Value("${spring.mqtt.client.id}")
private String clientId;
@Value("${spring.mqtt.default.topic}")
private String defaultTopic;
private static MqttClient client;
public static MqttClient getClient() {
return client;
}
public static void setClient(MqttClient client) {
ProviderService.client = client;
}
@Bean
public MqttConnectOptions getMqttConnectOptions(){
MqttConnectOptions mqttConnectOptions=new MqttConnectOptions();
mqttConnectOptions.setUserName(username);
mqttConnectOptions.setPassword(password.toCharArray());
mqttConnectOptions.setCleanSession(false);
mqttConnectOptions.setKeepAliveInterval(2);
return mqttConnectOptions;
}
@Bean
public MqttClient getMqttPushClient() throws MqttException{
client = new MqttClient(hostUrl, clientId,new MemoryPersistence());
client.connect(getMqttConnectOptions());
client.setCallback(new ProviderCallback());
return client;
}
public void publish(String topic,String pushMessage){
publish(0, false, topic, pushMessage);
}
public void publish(int qos,boolean retained,String topic,String pushMessage){
MqttMessage message = new MqttMessage();
message.setQos(qos);
message.setRetained(retained);
message.setPayload(pushMessage.getBytes());
MqttTopic mTopic = ProviderService.getClient().getTopic(topic);
MqttDeliveryToken token;
try {
token = mTopic.publish(message);
token.waitForCompletion();
} catch (MqttPersistenceException e) {
e.printStackTrace();
} catch (MqttException e) {
e.printStackTrace();
}
}
public void subscribe(String topic){
subscribe(topic,0);
}
/**
* 订阅某个主题
* @param topic
* @param qos
*/
public void subscribe(String topic,int qos){
try {
ProviderService.getClient().subscribe(topic, qos);
} catch (MqttException e) {
e.printStackTrace();
}
}
}
(5) 在Configure包中创建providerCallback.java文件
providerCallback.java文件中内容:
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
public class ProviderCallback implements MqttCallback {
@Override
public void connectionLost(Throwable cause) {
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
// TODO Auto-generated method stub
// TODO Auto-generated method stub
System.out.println("接收消息主题 : " + topic);
System.out.println("接收消息Qos : " + message.getQos());
System.out.println("接收消息内容 : " + new String(message.getPayload()));
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
}
}
(6) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Controller {
@Autowired
private ProviderService mqttProvider;
@RequestMapping(value="/index",method = {RequestMethod.GET,RequestMethod.POST})
public @ResponseBody String Index() {
mqttProvider.publish("topic", "字符串");
return "success";
}
}
(7) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("Configure,controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
01. Java中Netty集成Tcp服务器端(自定义编解码器)(1)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
(2) 创建CarProtocol.java文件
CarProtocol.java文件中内容:
import java.util.Arrays;
public class CarProtocol {
//消息头
private int head_data = 0x7878;
//包长度
private int packageLength;
//协议号
private int protocol;
//消息内容
private byte[] content;
//消息结束
private int end_data = 0x0d0a;
public CarProtocol() {
}
public CarProtocol(int head_data, int packageLength, int protocol, byte[] content, int end_data) {
this.head_data = head_data;
this.packageLength = packageLength;
this.protocol = protocol;
this.content = content;
this.end_data = end_data;
}
public int getHead_data() {
return head_data;
}
public void setHead_data(int head_data) {
this.head_data = head_data;
}
public int getPackageLength() {
return packageLength;
}
public void setPackageLength(int packageLength) {
this.packageLength = packageLength;
}
public int getProtocol() {
return protocol;
}
public void setProtocol(int protocol) {
this.protocol = protocol;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public int getEnd_data() {
return end_data;
}
public void setEnd_data(int end_data) {
this.end_data = end_data;
}
@Override
public String toString() {
return "CarProtocol [head_data=" + head_data + ", packageLength=" + packageLength + ", protocol=" + protocol
+ ", content=" + Arrays.toString(content) + ", end_data=" + end_data + "]";
}
}
(3) 创建MyDecoder.java文件
MyDecoder.java文件中内容:
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
public class MyDecoder extends ByteToMessageDecoder {
//协议开始的标准head_data,int类型,占据4个字节.
//表示数据的长度contentLength,int类型,占据4个字节.
//数据包基础长度
private final int base_len = 10;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//可读长度大于基本数据长度
if (in.readableBytes() >= base_len) {
// 因为,太大的数据,是不合理的
if (in.readableBytes() > 2048) {
in.skipBytes(in.readableBytes());
}
//记录包头位置
int beginIdx;
while (true) {
//获取包头开始的index
beginIdx = in.readerIndex();
//标记包头开始的index
in.markReaderIndex();
//读到了协议的开始标志,结束while循环
if (in.readShort() == 0x7878) {
break;
}
// 未读到包头,略过一个字节
// 每次略过,两个字节个字节,去读取,包头信息的开始标记
in.resetReaderIndex();
in.readByte();
// 当略过,一个字节之后,
// 数据包的长度,又变得不满足
// 此时,应该结束。等待后面的数据到达
if (in.readableBytes() < base_len) {
return;
}
}
//读取消息长度只占一位
int length = in.readByte();
//协议号
int protocol = in.readByte();
//判断请求数据包数据是否到齐
if (in.readableBytes() < length) {
//还原读指针
in.readerIndex(beginIdx);
return;
}
//读取data数据
byte[] content = new byte[length];
in.readBytes(content);
System.out.println(content);
CarProtocol carProtocol = new CarProtocol();
carProtocol.setProtocol(protocol);
carProtocol.setContent(content);
System.out.println(carProtocol);
out.add(carProtocol);
}
}
}
(4) 创建MyEncoder.java文件
MyEncoder.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MyEncoder extends MessageToByteEncoder<CarProtocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, CarProtocol carProtocol, ByteBuf out) throws Exception {
out.writeShort(carProtocol.getHead_data());
out.writeByte(carProtocol.getProtocol());
out.writeBytes(carProtocol.getContent());
out.writeShort(carProtocol.getEnd_data());
}
}
(5) 创建nettyTcp.java文件
nettyTcp.java文件中内容:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class nettyTcp {
public void TcpServer() {
//线程数
int boss_os_num = 8;
//数据接收线程组
EventLoopGroup bossLoop = new NioEventLoopGroup(boss_os_num);
//线程数
int event_os_num = 32;
//Tcp管理线程组
EventLoopGroup eventLoop = new NioEventLoopGroup(event_os_num);
try {
//配置netty中ServerBootstrap对象
ServerBootstrap serverBoot = new ServerBootstrap();
//配置Tcp参数
serverBoot.group(bossLoop,eventLoop)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024 * 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new TcpChannel());
ChannelFuture cf = serverBoot.bind(8080).sync();
//等待线程池结束
cf.channel().closeFuture().sync();
}
catch(Exception e) {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
finally {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
}
}
(6) 创建TcpChannel.java文件
TcpChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
public class TcpChannel extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sch) throws Exception {
//管理数据接收管道
sch.pipeline()
//自定义解码器
.addLast(new MyDecoder())
//自定义编码器
.addLast(new MyEncoder())
.addLast(new TcpHandler());
}
}
(7) 创建TcpHandler.java文件
TcpHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TcpHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
CarProtocol carProtocol = (CarProtocol) msg;
ctx.writeAndFlush(carProtocol);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
//释放对象
ctx.close();
ctx = null;
}
}
(8) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
new nettyTcp().TcpServer();
}
}
02. Java中Netty集成Tcp客户端(自定义编解码器)(1)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
(2) 创建CarProtocol.java文件
CarProtocol.java文件中内容:
import java.util.Arrays;
public class CarProtocol {
//消息头
private int head_data = 0x7878;
//包长度
private int packageLength;
//协议号
private int protocol;
//消息内容
private byte[] content;
//消息结束
private int end_data = 0x0d0a;
public CarProtocol() {
}
public CarProtocol(int head_data, int packageLength, int protocol, byte[] content, int end_data) {
this.head_data = head_data;
this.packageLength = packageLength;
this.protocol = protocol;
this.content = content;
this.end_data = end_data;
}
public int getHead_data() {
return head_data;
}
public void setHead_data(int head_data) {
this.head_data = head_data;
}
public int getPackageLength() {
return packageLength;
}
public void setPackageLength(int packageLength) {
this.packageLength = packageLength;
}
public int getProtocol() {
return protocol;
}
public void setProtocol(int protocol) {
this.protocol = protocol;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public int getEnd_data() {
return end_data;
}
public void setEnd_data(int end_data) {
this.end_data = end_data;
}
@Override
public String toString() {
return "CarProtocol [head_data=" + head_data + ", packageLength=" + packageLength + ", protocol=" + protocol
+ ", content=" + Arrays.toString(content) + ", end_data=" + end_data + "]";
}
}
(3) 创建MyDecoder.java文件
MyDecoder.java文件中内容:
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
public class MyDecoder extends ByteToMessageDecoder {
//协议开始的标准head_data,int类型,占据4个字节.
//表示数据的长度contentLength,int类型,占据4个字节.
//数据包基础长度
private final int base_len = 10;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//可读长度大于基本数据长度
if (in.readableBytes() >= base_len) {
// 因为,太大的数据,是不合理的
if (in.readableBytes() > 2048) {
in.skipBytes(in.readableBytes());
}
//记录包头位置
int beginIdx;
while (true) {
//获取包头开始的index
beginIdx = in.readerIndex();
//标记包头开始的index
in.markReaderIndex();
//读到了协议的开始标志,结束while循环
if (in.readShort() == 0x7878) {
break;
}
// 未读到包头,略过一个字节
// 每次略过,两个字节个字节,去读取,包头信息的开始标记
in.resetReaderIndex();
in.readByte();
// 当略过,一个字节之后,
// 数据包的长度,又变得不满足
// 此时,应该结束。等待后面的数据到达
if (in.readableBytes() < base_len) {
return;
}
}
//读取消息长度只占一位
int length = in.readByte();
//协议号
int protocol = in.readByte();
//判断请求数据包数据是否到齐
if (in.readableBytes() < length) {
//还原读指针
in.readerIndex(beginIdx);
return;
}
//读取data数据
byte[] content = new byte[length];
in.readBytes(content);
System.out.println(content);
CarProtocol carProtocol = new CarProtocol();
carProtocol.setProtocol(protocol);
carProtocol.setContent(content);
System.out.println(carProtocol);
out.add(carProtocol);
}
}
}
(4) 创建MyEncoder.java文件
MyEncoder.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MyEncoder extends MessageToByteEncoder<CarProtocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, CarProtocol carProtocol, ByteBuf out) throws Exception {
out.writeShort(carProtocol.getHead_data());
out.writeByte(carProtocol.getProtocol());
out.writeBytes(carProtocol.getContent());
out.writeShort(carProtocol.getEnd_data());
}
}
(5) 创建nettyTcp.java文件
nettyTcp.java文件中内容:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class nettyTcp {
public void TcpClient() {
//线程数
int event_os_num = 32;
//Tcp管理线程组
EventLoopGroup eventLoop = new NioEventLoopGroup(event_os_num);
try {
//配置netty中ServerBootstrap对象
Bootstrap ClientBoot = new Bootstrap();
//配置Tcp参数
ClientBoot.group(eventLoop)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new TcpChannel());
ChannelFuture cf = ClientBoot.connect("127.0.0.1",8006).sync();
}
catch(Exception e) {
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
}
}
(6) 创建TcpChannel.java文件
TcpChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
public class TcpChannel extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sch) throws Exception {
//管理数据接收管道
sch.pipeline()
//自定义解码器
.addLast(new MyDecoder())
//自定义编码器
.addLast(new MyEncoder())
.addLast(new TcpHandler());
}
}
(7) 创建TcpHandler.java文件
TcpHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TcpHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
CarProtocol carProtocol = (CarProtocol) msg;
ctx.writeAndFlush(carProtocol);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
//释放对象
ctx.close();
ctx = null;
}
}
(8) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
new nettyTcp().TcpClient();
}
}
03. Java中Netty集成Tcp服务器端(自定义编解码器)(2)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
(2) 创建MyDecoder.java文件
MyDecoder.java文件中内容:
import java.nio.charset.Charset;
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
public class MyDecoder extends ByteToMessageDecoder {
//数据包基础长度
private final int base_len = 4;
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
//基础长度不足,我们设定基础长度为4
if (in.readableBytes() < base_len) {
return;
}
int beginIdx; //记录包头位置
while (true) {
// 获取包头开始的index
beginIdx = in.readerIndex();
// 标记包头开始的index
in.markReaderIndex();
// 读到了协议的开始标志,结束while循环
if (in.readByte() == 0x02) {
break;
}
// 未读到包头,略过一个字节
// 每次略过,一个字节,去读取,包头信息的开始标记
in.resetReaderIndex();
in.readByte();
// 当略过,一个字节之后,
// 数据包的长度,又变得不满足
// 此时,应该结束。等待后面的数据到达
if (in.readableBytes() < base_len) {
return;
}
}
//剩余长度不足可读取数量[没有内容长度位]
int readableCount = in.readableBytes();
if (readableCount <= 1) {
in.readerIndex(beginIdx);
return;
}
//长度域占4字节,读取int
ByteBuf byteBuf = in.readBytes(1);
String msgLengthStr = byteBuf.toString(Charset.forName("GBK"));
int msgLength = Integer.parseInt(msgLengthStr);
//剩余长度不足可读取数量[没有结尾标识]
readableCount = in.readableBytes();
if (readableCount < msgLength + 1) {
in.readerIndex(beginIdx);
return;
}
ByteBuf msgContent = in.readBytes(msgLength);
//如果没有结尾标识,还原指针位置[其他标识结尾]
byte end = in.readByte();
if (end != 0x03) {
in.readerIndex(beginIdx);
return;
}
out.add(msgContent.toString(Charset.forName("GBK")));
}
}
(3) 创建MyEncoder.java文件
MyEncoder.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MyEncoder extends MessageToByteEncoder<Object> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object in, ByteBuf out) throws Exception {
String msg = in.toString();
byte[] bytes = msg.getBytes();
byte[] send = new byte[bytes.length + 2];
System.arraycopy(bytes, 0, send, 1, bytes.length);
send[0] = 0x02;
send[send.length - 1] = 0x03;
out.writeInt(send.length);
out.writeBytes(send);
}
}
(4) 创建nettyTcp.java文件
nettyTcp.java文件中内容:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class nettyTcp {
public void TcpServer() {
//线程数
int boss_os_num = 8;
//数据接收线程组
EventLoopGroup bossLoop = new NioEventLoopGroup(boss_os_num);
//线程数
int event_os_num = 32;
//Tcp管理线程组
EventLoopGroup eventLoop = new NioEventLoopGroup(event_os_num);
try {
//配置netty中ServerBootstrap对象
ServerBootstrap serverBoot = new ServerBootstrap();
//配置Tcp参数
serverBoot.group(bossLoop,eventLoop)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024 * 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new TcpChannel());
ChannelFuture cf = serverBoot.bind(8080).sync();
//等待线程池结束
cf.channel().closeFuture().sync();
}
catch(Exception e) {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
finally {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
}
}
(5) 创建TcpChannel.java文件
TcpChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
public class TcpChannel extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sch) throws Exception {
//管理数据接收管道
sch.pipeline()
//自定义解码器
.addLast(new MyDecoder())
//自定义编码器
.addLast(new MyEncoder())
.addLast(new TcpHandler());
}
}
(6) 创建TcpHandler.java文件
TcpHandler.java文件
TcpHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TcpHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//地址标识
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息:" + msg);
ctx.writeAndFlush("hi I'm ok");
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
//释放对象
ctx.close();
ctx = null;
}
}
(7) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
new nettyTcp().TcpServer();
}
}
04. Java中Netty集成Tcp客户端(自定义编解码器)(2)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
(2) 创建MyDecoder.java文件
MyDecoder.java文件中内容:
import java.nio.charset.Charset;
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
public class MyDecoder extends ByteToMessageDecoder {
//数据包基础长度
private final int base_len = 4;
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
//基础长度不足,我们设定基础长度为4
if (in.readableBytes() < base_len) {
return;
}
int beginIdx; //记录包头位置
while (true) {
// 获取包头开始的index
beginIdx = in.readerIndex();
// 标记包头开始的index
in.markReaderIndex();
// 读到了协议的开始标志,结束while循环
if (in.readByte() == 0x02) {
break;
}
// 未读到包头,略过一个字节
// 每次略过,一个字节,去读取,包头信息的开始标记
in.resetReaderIndex();
in.readByte();
// 当略过,一个字节之后,
// 数据包的长度,又变得不满足
// 此时,应该结束。等待后面的数据到达
if (in.readableBytes() < base_len) {
return;
}
}
//剩余长度不足可读取数量[没有内容长度位]
int readableCount = in.readableBytes();
if (readableCount <= 1) {
in.readerIndex(beginIdx);
return;
}
//长度域占4字节,读取int
ByteBuf byteBuf = in.readBytes(1);
String msgLengthStr = byteBuf.toString(Charset.forName("GBK"));
int msgLength = Integer.parseInt(msgLengthStr);
//剩余长度不足可读取数量[没有结尾标识]
readableCount = in.readableBytes();
if (readableCount < msgLength + 1) {
in.readerIndex(beginIdx);
return;
}
ByteBuf msgContent = in.readBytes(msgLength);
//如果没有结尾标识,还原指针位置[其他标识结尾]
byte end = in.readByte();
if (end != 0x03) {
in.readerIndex(beginIdx);
return;
}
out.add(msgContent.toString(Charset.forName("GBK")));
}
}
(3) 创建MyEncoder.java文件
MyEncoder.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MyEncoder extends MessageToByteEncoder<Object> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object in, ByteBuf out) throws Exception {
String msg = in.toString();
byte[] bytes = msg.getBytes();
byte[] send = new byte[bytes.length + 2];
System.arraycopy(bytes, 0, send, 1, bytes.length);
send[0] = 0x02;
send[send.length - 1] = 0x03;
out.writeInt(send.length);
out.writeBytes(send);
}
}
(4) 创建nettyTcp.java文件
nettyTcp.java文件中内容:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class nettyTcp {
public void TcpClient() {
//线程数
int event_os_num = 32;
//Tcp管理线程组
EventLoopGroup eventLoop = new NioEventLoopGroup(event_os_num);
try {
//配置netty中ServerBootstrap对象
Bootstrap ClientBoot = new Bootstrap();
//配置Tcp参数
ClientBoot.group(eventLoop)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new TcpChannel());
ChannelFuture cf = ClientBoot.connect("127.0.0.1",8006).sync();
}
catch(Exception e) {
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
}
}
(5) 创建TcpChannel.java文件
TcpChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
public class TcpChannel extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sch) throws Exception {
//管理数据接收管道
sch.pipeline()
//自定义解码器
.addLast(new MyDecoder())
//自定义编码器
.addLast(new MyEncoder())
.addLast(new TcpHandler());
}
}
(6) 创建TcpHandler.java文件
TcpHandler.java文件
TcpHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TcpHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//地址标识
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息:" + msg);
ctx.writeAndFlush("hi I'm ok");
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
//释放对象
ctx.close();
ctx = null;
}
}
(7) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
new nettyTcp().TcpClient();
}
}
05. Java中余弦推荐算法
(1) 依赖包
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
(2) Mysql数据库中创建testdb数据库
(3) testdb中创建用户信息member_user表
create table member_user
(
USER_ID int(10) auto_increment
primary key,
USER_NAME varchar(20) null
)
engine = MyISAM
charset = utf8;
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (1, '郑成功');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (2, '小红');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (7, '小李');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (19, '郑晖');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (10, '张三');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (11, '二龙湖浩哥');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (12, '张三炮');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (13, '赵四');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (14, '刘能');
INSERT INTO testdb.member_user (USER_ID, USER_NAME) VALUES (15, '刘能逗');
(4) testdb中创建订单记录product_order表
create table product_order
(
ORDER_ID int auto_increment
primary key,
USER_ID int not null,
PRODUCT_ID int not null,
GWCOUNT int null,
out_trade_no varchar(100) null
);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (1, 1, 1, 15, '202001');
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (2, 2, 3, 42, '202002');
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (3, 3, 4, 2, '202003');
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (4, 4, 4, 20, '202004');
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (5, 1, 2, 21, '202005');
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (6, 5, 1, null, null);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (7, 5, 2, null, null);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (8, 5, 3, null, null);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (9, 6, 2, null, null);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (10, 6, 5, null, null);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (11, 7, 1, null, null);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (12, 7, 2, null, null);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (13, 7, 5, null, null);
INSERT INTO testdb.product_order (ORDER_ID, USER_ID, PRODUCT_ID, GWCOUNT, out_trade_no) VALUES (14, 3, 1, null, null);
(5) testdb中创建商品信息product_table表
create table product_table
(
productID int auto_increment comment '商品ID'
primary key,
product_name varchar(200) charset utf8 null comment '商品名字',
price double null comment '商品金额',
volume int null comment '成交数量',
shopp_name varchar(100) charset utf8 null comment '商店名称',
location varchar(100) charset utf8 null comment '生产地',
evaluate int null comment '好评数量',
collect int default 0 null comment '收藏数量'
)
engine = MyISAM
collate = utf8_unicode_ci;
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (1, '杨梅新鲜现摘现发正宗仙居东魁大杨梅孕妇农家时令水果6斤装包邮', 198, 1328, '浙仙旗舰店', '浙江 台州', 240, 1397);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (2, '正宗仙居东魁杨梅新鲜现摘特级大东魁现摘现发农家时令水5斤精选', 268, 497, '浙仙旗舰店', '浙江 台州', 159, 619);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (3, '新鲜洋葱10斤紫皮红皮农家2020年葱头应季蔬菜圆头整箱批发包邮', 18.8, 64, '悠鲜源旗舰店', '云南 昆明', 7, 51);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (4, '轩农谷仙居东魁杨梅新鲜水果浙江现摘现发大杨梅礼盒预定7A6斤', 358, 222, '轩农谷旗舰店', '浙江 台州', 553, 1163);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (5, '轩农谷正宗仙居杨梅新鲜当季水果特级东魁大杨梅5A级6斤高山现摘', 258, 2939, '轩农谷旗舰店', '浙江 台州', 4270, 8737);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (6, '水果小黄瓜新鲜6斤当季山东小青瓜生吃农家蔬菜助白玉女瓜10批发5', 23.8, 4, '喜人喜食品旗舰店', '山东 潍坊', 21685, 10511);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (7, '王小二 新鲜马蹄荸荠地梨孛荠当季马蹄莲5斤饽荠农家自种蔬菜包邮', 19.9, 1780, '王小二旗舰店', '湖北 宜昌', 1428, 858);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (8, '王小二 云南新鲜蚕豆农家罗汉豆带壳生兰花豆胡豆豌豆蔬菜包邮5斤', 29.9, 54, '王小二旗舰店', '云南 昆明', 72, 68);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (9, '王小二 小米椒新鲜红辣椒蔬菜包邮红尖椒灯笼椒朝天包邮农家5斤', 29.9, 601, '王小二旗舰店', '山东 潍坊', 435, 686);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (10, '王小二 湖北土辣椒新鲜青椒农家长辣椒蔬菜包邮尖椒批发特产5斤', 29.9, 86, '王小二旗舰店', '湖北 襄阳', 33, 25);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (11, '小黄瓜水果黄瓜新鲜5斤小青瓜东北旱海阳白玉生吃10山东农家蔬菜', 13.8, 7500, '田园茂旗舰店', '山东 烟台', 80081, 79562);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (12, '圣女果千禧小番茄新鲜西红柿千禧长果5斤农家时令蔬菜包邮水果', 18.8, 10000, '时卉源旗舰店', '河南 郑州', 3014, 2682);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (13, '高端货!新疆沙瓤西红柿新鲜自然熟番茄水果普罗旺斯农家顺丰包邮', 58.8, 232, '奇迹汇食品旗舰店', '新疆 吐鲁番', 91, 63);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (14, '绿宝石甜瓜小香瓜新鲜水果应季10东北瓜果包邮5斤助农当季整箱', 24.8, 15000, '梦强旗舰店', '山东 临沂', 34173, 37618);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (15, '5斤新西兰贝贝南瓜板栗小南瓜板栗味老瓜栗子板粟10农家新鲜带箱', 14.8, 10000, '刘小牛旗舰店', '山东 日照', 18590, 6991);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (16, '【5A特大】仙居杨梅新鲜孕妇水果现摘现发正宗农家东魁杨梅6斤装', 238, 260, '巨浪食品专营店', '浙江 台州', 3775, 8498);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (17, '宜兴新鲜百合2500g包邮宜兴特产百合纯农家食用大白合5斤30个左右', 52, 975, '镓荣旗舰店', '江苏 无锡', 2767, 2348);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (18, '杨梅新鲜正宗仙居东魁孕妇现摘现发农家时令水果6斤装东魁杨梅', 95, 5000, '集果邦旗舰店', '浙江 台州', 385, 10413);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (19, '新鲜自然熟黄金籽西红柿农家水果老品种5斤非铁皮博士番茄沙瓤', 49, 2164, '黄金籽旗舰店', '山东 潍坊', 4250, 13763);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (20, '云南小土豆新鲜10斤马铃薯农产品蔬菜红皮洋芋批发迷你小黄心土豆', 19.8, 10000, '红高粱食品旗舰店', '云南 昆明', 51861, 40936);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (21, '现挖云南紫皮洋葱5斤新鲜红皮大圆葱洋葱头农家自种特产蔬菜包邮', 9.9, 249, '红高粱食品旗舰店', '云南 红河', 268, 212);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (22, '新鲜芋头10斤芋艿小芋头香芋免邮包邮农家粉糯荔浦毛芋头整箱紫', 29.8, 7500, '红高粱食品旗舰店', '山东 潍坊', 49152, 45661);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (23, '云南紫皮洋葱新鲜10斤包邮洋葱头农家自种当季蔬菜红皮大圆葱整箱', 19.8, 8500, '红高粱食品旗舰店', '云南 红河', 5733, 2604);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (24, '盒马河南焦作铁棍山药净重5斤当季农家蔬菜温县新鲜山药包邮', 39.9, 1645, '盒马鲜生旗舰店', '河南 焦作', 1185, 1476);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (25, '盒马山东大葱净重5斤新鲜长葱当季时令蔬菜去叶白香葱产地农产品', 24.9, 49, '盒马鲜生旗舰店', '上海', 36, 47);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (26, '盒马山东玉菇甜瓜2粒装单粒1kg起当季时令水果新鲜甜瓜蜜瓜', 24.9, 566, '盒马鲜生旗舰店', '山东 潍坊', 97, 69);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (27, '【芭芭农场】新疆普罗旺斯西红柿净重5斤当季番茄自然熟水果蔬菜', 39.9, 7500, '盒马鲜生旗舰店', '陕西 西安', 2262, 1697);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (28, '芋头新鲜蔬菜小芋头毛芋头香芋农家自种芋头芋艿非荔浦芋头5斤10', 18.5, 4888, '果鲜萌旗舰店', '山东 潍坊', 14571, 13733);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (29, '农家自种新鲜小米辣椒5斤红辣椒朝天椒蔬菜泡椒特辣小米椒鲜辣椒', 28.8, 4867, '果品康旗舰店', '海南 海口', 9453, 9562);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (30, '2020年新鲜现挖小洋葱带箱10斤 红皮紫皮洋葱头圆葱农家自种蔬菜', 10.8, 15000, '果恋韵旗舰店', '云南 红河', 23575, 15821);
INSERT INTO testdb.product_table (productID, product_name, price, volume, shopp_name, location, evaluate, collect) VALUES (31, '海南新鲜辣椒小米椒5斤朝天小米辣红辣椒农家土蔬菜可剁泡椒包邮', 29.9, 1936, '果绰旗舰店', '海南 海口', 2413, 2962);
(6) 创建Entity包,并创建MemberUser.java文件
MemberUser.java文件中内容:
//会员信息
public class MemberUser {
private Integer user_id;//用户id
private String user_name;//用户名
public MemberUser() {
}
public MemberUser(Integer user_id, String user_name) {
this.user_id = user_id;
this.user_name = user_name;
}
public Integer getUser_id() {
return user_id;
}
public void setUser_id(Integer user_id) {
this.user_id = user_id;
}
public String getUser_name() {
return user_name;
}
public void setUser_name(String user_name) {
this.user_name = user_name;
}
@Override
public String toString() {
return "MemberUser{" +
"user_id=" + user_id +
", user_name='" + user_name + '\'' +
'}';
}
}
(7) 在Entity包中创建ProductOrder.java文件
``ProductOrder.java`文件中内容:
//商品订单
public class ProductOrder {
private Integer order_id;//订单id
private Integer user_id;//所购买的用户id
private Integer product_id;//商品id
private Integer gwcount;//购买数量
public ProductOrder() {
}
public ProductOrder(Integer order_id, Integer user_id, Integer product_id, Integer gwcount) {
this.order_id = order_id;
this.user_id = user_id;
this.product_id = product_id;
this.gwcount = gwcount;
}
public Integer getOrder_id() {
return order_id;
}
public void setOrder_id(Integer order_id) {
this.order_id = order_id;
}
public Integer getUser_id() {
return user_id;
}
public void setUser_id(Integer user_id) {
this.user_id = user_id;
}
public Integer getProduct_id() {
return product_id;
}
public void setProduct_id(Integer product_id) {
this.product_id = product_id;
}
public Integer getGwcount() {
return gwcount;
}
public void setGwcount(Integer gwcount) {
this.gwcount = gwcount;
}
@Override
public String toString() {
return "ProductOrder{" +
"order_id=" + order_id +
", user_id=" + user_id +
", product_id=" + product_id +
", gwcount=" + gwcount +
'}';
}
}
(8) 在Entity包中创建ProductTable.java文件
``ProductTable.java`文件中内容:
/**
* @Auther: truedei
* @Date: 2020 /20-6-13 21:05
* @Description:商品记录表
*/
public class ProductTable {
private Integer productID ; //商品ID'
private String product_name; //ll comment '商品名字'
private Double price ; //商品金额'
private Integer volume ; //成交数量'
private String shopp_name ; //ll comment '商店名称'
private String location ; //ll comment '生产地'
private Integer evaluate ; //好评数量'
private Integer collect ; //收藏数量'
public ProductTable(Integer productID, String product_name, Double price, Integer volume, String shopp_name, String location, Integer evaluate, Integer collect) {
this.productID = productID;
this.product_name = product_name;
this.price = price;
this.volume = volume;
this.shopp_name = shopp_name;
this.location = location;
this.evaluate = evaluate;
this.collect = collect;
}
public ProductTable() {
}
public Integer getProductID() {
return productID;
}
public void setProductID(Integer productID) {
this.productID = productID;
}
public String getProduct_name() {
return product_name;
}
public void setProduct_name(String product_name) {
this.product_name = product_name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getVolume() {
return volume;
}
public void setVolume(Integer volume) {
this.volume = volume;
}
public String getShopp_name() {
return shopp_name;
}
public void setShopp_name(String shopp_name) {
this.shopp_name = shopp_name;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public Integer getEvaluate() {
return evaluate;
}
public void setEvaluate(Integer evaluate) {
this.evaluate = evaluate;
}
public Integer getCollect() {
return collect;
}
public void setCollect(Integer collect) {
this.collect = collect;
}
@Override
public String toString() {
return "ProductTable{" +
"productID=" + productID +
", product_name='" + product_name + '\'' +
", price=" + price +
", volume=" + volume +
", shopp_name='" + shopp_name + '\'' +
", location='" + location + '\'' +
", evaluate=" + evaluate +
", collect=" + collect +
'}';
}
}
(9) 在Entity包中创建UserR.java文件
UserR.java文件中内容:
import java.util.Arrays;
/**
* @Auther: truedei
* @Date: 2020 /20-6-13 22:53
* @Description:
*/
public class UserR {
private String userName;
private Integer userId;
private Integer[] ProductIds;
private Double cos_th;
public Double getCos_th() {
return cos_th;
}
public void setCos_th(Double cos_th) {
this.cos_th = cos_th;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Integer[] getProductIds() {
return ProductIds;
}
public void setProductIds(Integer[] productIds) {
ProductIds = productIds;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
@Override
public String toString() {
return "UserR{" +
"userName='" + userName + '\'' +
", userId=" + userId +
", ProductIds=" + Arrays.toString(ProductIds) +
", cos_th=" + cos_th +
'}';
}
}
(10) 创建Db包,并创建DBHelp.java文件
DBHelp.java文件中内容:
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import Entity.*;
public class DBHelp {
static String url = "jdbc:mysql://127.0.0.1:3358/testdb?useUnicode=true&characterEncoding=UTF-8";
static String user= "mysql";
static String password= "123456";
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() {
try {
return DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
static Connection conn = DBHelp.getConnection();
static Statement st = null;
static ResultSet rs = null;
/**
* 获取所有的商品信息
* @return
* @param sqlId
*/
public static List<ProductTable> getProductList(String sqlId){
List<ProductTable> productTables = new ArrayList<>();
try {
st = conn.createStatement();
rs = st.executeQuery("select * from product_table where productID in ("+sqlId+")");
while (rs.next()){
productTables.add(new ProductTable(
rs.getInt("productID"),
rs.getString("product_name"),
rs.getDouble("price"),
rs.getInt("volume"),
rs.getString("shopp_name"),
rs.getString("location"),
rs.getInt("evaluate"),
rs.getInt("collect")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return productTables;
}
//获取用户订单信息
public static List<ProductOrder> getProductOrderList(Integer userId){
List<ProductOrder> productTables = new ArrayList<>();
// String sql = "select * from product_order where USER_ID=(select USER_ID from member_user where USER_NAME=\""+name+"\")";
String sql = "select * from product_order "+(userId==null?"":"where USER_ID="+userId);
// System.out.println("执行的 sql: "+sql);
try {
st = conn.createStatement();
rs = st.executeQuery(sql);
while (rs.next()){
productTables.add(new ProductOrder(
rs.getInt("order_id"),
rs.getInt("user_id"),
rs.getInt("product_id"),
rs.getInt("gwcount")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return productTables;
}
//获取用户信息
public static List<MemberUser> getMemberUserList(){
List<MemberUser> productTables = new ArrayList<>();
try {
st = conn.createStatement();
rs = st.executeQuery("select * from member_user");
while (rs.next()){
productTables.add(new MemberUser(
rs.getInt("user_id"),
rs.getString("user_name")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return productTables;
}
}
(11) 创建test包,并创建ArrayUtil.java文件
ArrayUtil.java文件中内容:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ArrayUtil {
/**
* 求并集
*
* @param m
* @param n
* @return
*/
public static Integer[] getB(Integer[] m, Integer[] n)
{
// 将数组转换为set集合
Set<Integer> set1 = new HashSet<Integer>(Arrays.asList(m));
Set<Integer> set2 = new HashSet<Integer>(Arrays.asList(n));
// 合并两个集合
set1.addAll(set2);
Integer[] arr = {};
return set1.toArray(arr);
}
/**
* 求交集
*
* @param m
* @param n
* @return
*/
public static Integer[] getJ(Integer[] m, Integer[] n)
{
List<Integer> rs = new ArrayList<Integer>();
// 将较长的数组转换为set
Set<Integer> set = new HashSet<Integer>(Arrays.asList(m.length > n.length ? m : n));
// 遍历较短的数组,实现最少循环
for (Integer i : m.length > n.length ? n : m)
{
if (set.contains(i))
{
rs.add(i);
}
}
Integer[] arr = {};
return rs.toArray(arr);
}
/**
* 求差集
*
* @param m
* @param n
* @return
*/
public static Integer[] getC(Integer[] m, Integer[] n)
{
// 将较长的数组转换为set
Set<Integer> set = new HashSet<Integer>(Arrays.asList(m.length > n.length ? m : n));
// 遍历较短的数组,实现最少循环
for (Integer i : m.length > n.length ? n : m)
{
// 如果集合里有相同的就删掉,如果没有就将值添加到集合
if (set.contains(i))
{
set.remove(i);
} else
{
set.add(i);
}
}
Integer[] arr = {};
return set.toArray(arr);
}
}
(12) 在test包中创建MapSortUtil.java文件
MapSortUtil.java文件中内容:
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class MapSortUtil {
private static Comparator<Map.Entry> comparatorByKeyAsc = (Map.Entry o1, Map.Entry o2) -> {
if (o1.getKey() instanceof Comparable) {
return ((Comparable) o1.getKey()).compareTo(o2.getKey());
}
throw new UnsupportedOperationException("键的类型尚未实现Comparable接口");
};
private static Comparator<Map.Entry> comparatorByKeyDesc = (Map.Entry o1, Map.Entry o2) -> {
if (o1.getKey() instanceof Comparable) {
return ((Comparable) o2.getKey()).compareTo(o1.getKey());
}
throw new UnsupportedOperationException("键的类型尚未实现Comparable接口");
};
private static Comparator<Map.Entry> comparatorByValueAsc = (Map.Entry o1, Map.Entry o2) -> {
if (o1.getValue() instanceof Comparable) {
return ((Comparable) o1.getValue()).compareTo(o2.getValue());
}
throw new UnsupportedOperationException("值的类型尚未实现Comparable接口");
};
private static Comparator<Map.Entry> comparatorByValueDesc = (Map.Entry o1, Map.Entry o2) -> {
if (o1.getValue() instanceof Comparable) {
return ((Comparable) o2.getValue()).compareTo(o1.getValue());
}
throw new UnsupportedOperationException("值的类型尚未实现Comparable接口");
};
/**
* 按键升序排列
*/
public static <K, V> Map<K, V> sortByKeyAsc(Map<K, V> originMap) {
if (originMap == null) {
return null;
}
return sort(originMap, comparatorByKeyAsc);
}
/**
* 按键降序排列
*/
public static <K, V> Map<K, V> sortByKeyDesc(Map<K, V> originMap) {
if (originMap == null) {
return null;
}
return sort(originMap, comparatorByKeyDesc);
}
/**
* 按值升序排列
*/
public static <K, V> Map<K, V> sortByValueAsc(Map<K, V> originMap) {
if (originMap == null) {
return null;
}
return sort(originMap, comparatorByValueAsc);
}
/**
* 按值降序排列
*/
public static <K, V> Map<K, V> sortByValueDesc(Map<K, V> originMap) {
if (originMap == null) {
return null;
}
return sort(originMap, comparatorByValueDesc);
}
private static <K, V> Map<K, V> sort(Map<K, V> originMap, Comparator<Map.Entry> comparator) {
return originMap.entrySet()
.stream()
.sorted(comparator)
.collect(
Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e2,
LinkedHashMap::new));
}
}
(13) 在test包中创建RecommenderSystem.java文件
RecommenderSystem.java文件中内容:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import Db.*;
import Entity.*;
public class RecommenderSystem {
RecommenderSystem(){
login(1);
}
//推荐算法开始
/**
* 登录后推荐接口
* @param userId 模拟登录的用户ID
*/
public void login(Integer userId){
//1,使用该用户的名字获取订单信息
System.out.println("----------------");
//查询登录用户的订单信息
List<ProductOrder> productOrderList = DBHelp.getProductOrderList(userId);
//存储个人 购买的所有的商品id
Integer[] ints = new Integer[productOrderList.size()];
//存储个人信息,封装成对象,方便计算
UserR userR = new UserR();
//筛选出来个人订单中的商品的id
System.out.println("个人的:");
for (int i = 0; i < productOrderList.size(); i++) {
ints[i] = productOrderList.get(i).getProduct_id();
System.out.println(productOrderList.get(i).toString());
}
userR.setUserId(productOrderList.get(0).getUser_id());
userR.setProductIds(ints);
//2,拿到所有用户的订单信息
List<ProductOrder> productOrderLists = DBHelp.getProductOrderList(null);
//存储所有人的订单信息
List<UserR> userRS = new ArrayList<>();
//利用map的机制,计算出来其余用户的所有的购买商品的id Map<用户id,商品ID拼接的字符串(1,2,3,4)>
Map<Integer,String> map = new HashMap<>();
System.out.println("所有人的:");
//筛选出来订单中的商品的id
for (int i = 0; i < productOrderLists.size(); i++) {
System.out.println(productOrderLists.get(i).toString());
map.put(productOrderLists.get(i).getUser_id(),
map.containsKey(productOrderLists.get(i).getUser_id())?
map.get(productOrderLists.get(i).getUser_id())+","+productOrderLists.get(i).getProduct_id():
productOrderLists.get(i).getProduct_id()+"");
}
//开始封装每个人的数据
for (Integer key:map.keySet() ) {
//new出来一个新的个人的对象,后面要塞到list中
UserR userR2 = new UserR();
//把其他每个人购买的商品的id 分割成数组
String[] split = map.get(key).split(",");
//转换成int数组 进行存储,方便后期计算
Integer[] ints1 = new Integer[split.length];
for (int i = 0; i < split.length; i++) {
ints1[i] = Integer.valueOf(split[i]);
}
//用户id 就是key
userR2.setUserId(key);
//用户购买的商品id的数组
userR2.setProductIds(ints1);
//塞到list中
userRS.add(userR2);
}
//二值化 处理数据
List<UserR> userRList = jisuan(userR, userRS);
System.out.println("得出的结果:");
for (int i = 0; i < userRList.size(); i++) {
System.out.println(userRList.get(i).toString());
}
System.out.println("过滤处理数据之后:");
//过滤处理
String sqlId = chuli(userRList, userR);
System.out.println("推荐的商品:");
//通过拿到的拼接的被推荐商品的id,去查数据库
List<ProductTable> productList = DBHelp.getProductList(sqlId);
//最终拿到被推荐商品的信息
for (int i = 0; i < productList.size(); i++) {
System.out.println(productList.get(i).toString());
}
}
/**
* 过滤处理
* @param userRList 所有用户的订单数据
* @param userR 当前登录用户的订单数据
* @return
*/
private String chuli(List<UserR> userRList,UserR userR) {
//为了方便下面过滤数据,预先把登录用户的订单购物的商品的id做一个map,在过滤的时候,只需要查一下map中是否存在key就ok
Map<Integer,Integer> map1 = new HashMap<>();
for (int i = 0; i < userR.getProductIds().length; i++) {
map1.put(userR.getProductIds()[i],userR.getProductIds()[i]);
}
//盛放最终过滤出来的数据 Map<商品id,出现的次数>
Map<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < userRList.size(); i++) {
//userRList.get(i).getCos_th()>0:过滤掉相似度等于0,也就是完全不匹配的
//userRList.get(i).getUserId()!=userR.getUserId():过滤掉当前用户的订单信息
if(userRList.get(i).getCos_th()>0 && userRList.get(i).getUserId()!=userR.getUserId()){
//求当前登录用户的购买商品的id和其他用户的所购买商品的差集,例如:A=[1, 2],B=[1, 2, 3] 那么这个3就是最终想要的结果
Integer[] j = ArrayUtil.getC(userRList.get(i).getProductIds(), userR.getProductIds());
//遍历求差集之后的结果
for (int i1 = 0; i1 < j.length; i1++) {
//如果其余的用户所购买撒谎那个品的id不在当前用的所购买商品的id,那么就存起来
if(!map1.containsKey(j[i1])){
//存储时,数量每次都+1,方便后面排序,出现的次数多,说明被推荐的机会越高
map.put(j[i1],map.containsKey(j[i1])?(map.get(j[i1])+1):1);
}
}
}
}
System.out.println("处理之后的map:");
for (Integer key:map.keySet()) {
System.out.println("商品id="+key+"--用户所购数量="+map.get(key));
}
//把map进行降序排序
Map<Integer, Integer> map2 = MapSortUtil.sortByKeyDesc(map);
System.out.println("按降序" + map2);
//拼接成一个sql,方便去查数据库
String sqlId = "";
for (Integer key:map2.keySet()) {
sqlId = sqlId+key +",";
}
sqlId = sqlId.substring(0,sqlId.length()-1);
System.out.println("最终拿到的被推荐给当前用户的商品id--->"+sqlId);
return sqlId;
}
/**
* 二值化 处理数据
* @param userR 当前登录用户的订单信息
* @param userRS 其他用户的订单信息
* @return 二值化处理之后的结果
*/
private List<UserR> jisuan(UserR userR, List<UserR> userRS) {
//对个人做二值化处理,为了好计算 [0,0,0,0,0,1,1,0,1]这种
//个人的
int userErzhihua[] = new int[100];
System.out.println(userR.getProductIds().length);
for (int i = 0; i < userR.getProductIds().length; i++) {
userErzhihua[userR.getProductIds()[i]]=1;
}
//库里所有人的
int erzhihua[] = new int[100];
//对其他人,做二值化处理,为了好计算 [0,0,0,0,0,1,1,0,1]这种
for (int i = 0; i < userRS.size(); i++) {
UserR product = userRS.get(i);
for (int j = 0; j < product.getProductIds().length; j++) {
erzhihua[product.getProductIds()[j]]=1;
}
//计算当前登录用户与其余每个人的余弦值 cos_th
Double compare = compare(erzhihua,userErzhihua);
product.setCos_th(compare);
//把计算好的值,重新塞到原来的位置,替换到旧的数据
userRS.set(i,product);
//防止数组中的值重复,起到清空的作用
erzhihua = new int[100];
}
return userRS;
}
/**
* 代码核心内容
* @param o1 当前登录用户的
* @param o2 其他用户的 n1 n2 n3 n4 n....
* @return
*/
private static Double compare(int[] o1, int[] o2) {
//分子求和
Double fenzi = 0.0 ;
for (int i = 0; i < o1.length; i++) {
fenzi += o1[i]*o2[i];
}
//分母第一部分
Double fenmu1 = 0.0;
for (int i = 0; i < o1.length; i++) {
fenmu1 += o1[i] * o1[i];
}
fenmu1 = Math.sqrt(fenmu1);
//分母第二部分
Double fenmu2 = 0.0;
for (int i = 0; i < o2.length; i++) {
fenmu2 += o2[i] * o2[i];
}
fenmu2 = Math.sqrt(fenmu2);
return fenzi / (fenmu1 * fenmu2);
}
}
(14) 在test包中创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
new RecommenderSystem();
}
}
06. Java中拆解协议包
import java.util.ArrayList;
import java.util.List;
public class test {
private static int startBit = 0x7e;
private static int endBit = 0x7e;
//排除包头+包尾+协议号+长度
private static int limit = 20;
public static List<String> splitPackage(String hex_str,boolean repeat){
ArrayList<String> pac_list = null;
ArrayList<Integer> start_index = new ArrayList<Integer>();
ArrayList<Integer> end_index = new ArrayList<Integer>();
//遍历截取值
String pos_val = null;
int len = hex_str.length();
int hex_item = 0;
//遍历字符串
for(int i=0;i<len;i+=2){
pos_val = hex_str.substring(i,i+2);
hex_item = Integer.parseInt(pos_val,16);
if(startBit == hex_item) {
start_index.add(i);
}
if(endBit == hex_item) {
end_index.add(i);
}
}
//索引长度
int start_len = start_index.size();
int end_len = end_index.size();
if(!start_index.isEmpty() && !end_index.isEmpty()) {
//初始化
pac_list = new ArrayList<String>();
for(int i=0;i<start_len;i++) {
for(int j=0;j<end_len;j++) {
if(i > 0) {
int start_a = start_index.get(i-1);
int start_b = start_index.get(i);
//排除包头重复数字例如0x7878
if(repeat) {
if(start_b - start_a == 2) {
int start_pos = start_index.get(i - 1);
int end_pos = end_index.get(j);
//过滤非正常结尾数据包7e
if(end_pos - start_pos > limit * 2) {
pos_val = hex_str.substring(start_pos,end_pos + 2);
pac_list.add(pos_val);
break;
}
}
}
else {
if(start_b - start_a != 2) {
int start_pos = start_index.get(i - 1);
int end_pos = end_index.get(j);
//过滤非正常结尾数据包
if(end_pos - start_pos > limit * 2) {
pos_val = hex_str.substring(start_pos,end_pos + 2);
pac_list.add(pos_val);
break;
}
}
}
}
}
}
start_index.clear();
end_index.clear();
}
return pac_list;
}
public static void main(String[] args) throws Exception {
String hex_str = "";
List<String> arr = splitPackage(hex_str,false);
}
}
07. Java中SnowFlower自增Id(雪花算法)
(1) 创建SnowFlower.java文件
Snowflower.java文件中内容:
public class SnowFlake {
// 起始的时间戳
private final static long START_STMP = 1577808000000L; //2020-01-01
// 每一部分占用的位数,就三个
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数
private final static long DATACENTER_BIT = 5; //数据中心占用的位数
// 每一部分最大值
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
// 每一部分向左的位移
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId; //数据中心
private long machineId; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L; //上一次时间戳
public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
//产生下一个ID
public synchronized long nextId() {
long currStmp = timeGen();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
//if条件里表示当前调用和上一次调用落在了相同毫秒内,只能通过第三部分,序列号自增来判断为唯一,所以+1.
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大,只能等待下一个毫秒
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
//执行到这个分支的前提是currTimestamp > lastTimestamp,说明本次调用跟上次调用对比,已经不再同一个毫秒内了,这个时候序号可以重新回置0了。
sequence = 0L;
}
lastStmp = currStmp;
//就是用相对毫秒数、机器ID和自增序号拼接
return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}
private long getNextMill() {
long mill = timeGen();
while (mill <= lastStmp) {
mill = timeGen();
}
return mill;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
(2) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
SnowFlake idWorker = new SnowFlake(0, 0);
for (int i = 0; i < 100; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
08. Java中amr录音文件转mp3文件
1.ffmpeg格式转换
(1) 安装ffmpeg并设置成环境变量
(2) 创建test.java文件
test.java文件中内容:
import java.io.File;
public class test {
public static void ToMp3(String sourcePath){
File file = new File(sourcePath);
//转换后文件的存储地址,直接将原来的文件名后加mp3后缀名
String name = file.getName();
name = name.substring(0,name.lastIndexOf("."));
String targetPath = name +".mp3";
System.out.println(sourcePath);
Runtime run = null;
try {
run = Runtime.getRuntime();
long start=System.currentTimeMillis();
//执行ffmpeg.exe,前面是ffmpeg.exe的地址,中间是需要转换的文件地址,后面是转换后的文件地址。-i是转换方式,意思是可编码解码,mp3编码方式采用的是libmp3lame
Process p=run.exec("ffmpeg -i "+ sourcePath +" -acodec libmp3lame "+ targetPath);
//释放进程
p.getOutputStream().close();
p.getInputStream().close();
p.getErrorStream().close();
p.waitFor();
long end=System.currentTimeMillis();
System.out.println(sourcePath+" convert success, costs:"+(end-start)+"ms");
//删除原来的文件
if(file.exists()){
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}finally{
//run调用lame×××最后释放内存
run.freeMemory();
}
}
public static void main(String[] args) throws Exception {
ToMp3("test.amr");
}
}
2.依赖包转换
(1) 依赖包
<dependency>
<groupId>com.github.dadiyang</groupId>
<artifactId>jave</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.25</version>
</dependency>
(2) 创建test.java文件
test.java文件中内容:
import java.io.File;
import it.sauronsoftware.jave.AudioUtils;
public class test {
public static void main(String[] args) throws Exception {
File source = new File("test.amr");
File target = new File("testAudio.mp3");
AudioUtils.amrToMp3(source, target);
}
}
09. Java中人民币大写转换
(1) 创建MoneyUtil.java文件
MoneyUtil.java文件中内容:
public class MoneyUtil {
/** 大写数字 */
private static final String[] NUMBERS = { "零", "壹", "贰", "叁", "肆", "伍",
"陆", "柒", "捌", "玖" };
/** 整数部分的单位 */
private static final String[] IUNIT = { "元", "拾", "佰", "仟", "万", "拾", "佰",
"仟", "亿", "拾", "佰", "仟", "万", "拾", "佰", "仟" };
/** 小数部分的单位 */
private static final String[] DUNIT = { "角", "分", "厘" };
/**
* 得到大写金额。
*/
public static String toChinese(String str) {
str = str.replaceAll(",", "");// 去掉","
String integerStr;// 整数部分数字
String decimalStr;// 小数部分数字
// 初始化:分离整数部分和小数部分
if (str.indexOf(".") > 0) {
integerStr = str.substring(0, str.indexOf("."));
decimalStr = str.substring(str.indexOf(".") + 1);
} else if (str.indexOf(".") == 0) {
integerStr = "";
decimalStr = str.substring(1);
} else {
integerStr = str;
decimalStr = "";
}
// integerStr去掉首0,不必去掉decimalStr的尾0(超出部分舍去)
if (!integerStr.equals("")) {
integerStr = Long.toString(Long.parseLong(integerStr));
if (integerStr.equals("0")) {
integerStr = "";
}
}
// overflow超出处理能力,直接返回
if (integerStr.length() > IUNIT.length) {
System.out.println(str + ":超出处理能力");
return str;
}
int[] integers = toArray(integerStr);// 整数部分数字
boolean isMust5 = isMust5(integerStr);// 设置万单位
int[] decimals = toArray(decimalStr);// 小数部分数字
return getChineseInteger(integers, isMust5)
+ getChineseDecimal(decimals);
}
/**
* 整数部分和小数部分转换为数组,从高位至低位
*/
private static int[] toArray(String number) {
int[] array = new int[number.length()];
for (int i = 0; i < number.length(); i++) {
array[i] = Integer.parseInt(number.substring(i, i + 1));
}
return array;
}
/**
* 得到中文金额的整数部分。
*/
private static String getChineseInteger(int[] integers, boolean isMust5) {
StringBuffer chineseInteger = new StringBuffer("");
int length = integers.length;
for (int i = 0; i < length; i++) {
// 0出现在关键位置:1234(万)5678(亿)9012(万)3456(元)
// 特殊情况:10(拾元、壹拾元、壹拾万元、拾万元)
String key = "";
if (integers[i] == 0) {
if ((length - i) == 13)// 万(亿)(必填)
key = IUNIT[4];
else if ((length - i) == 9)// 亿(必填)
key = IUNIT[8];
else if ((length - i) == 5 && isMust5)// 万(不必填)
key = IUNIT[4];
else if ((length - i) == 1)// 元(必填)
key = IUNIT[0];
// 0遇非0时补零,不包含最后一位
if ((length - i) > 1 && integers[i + 1] != 0)
key += NUMBERS[0];
}
chineseInteger.append(integers[i] == 0 ? key
: (NUMBERS[integers[i]] + IUNIT[length - i - 1]));
}
return chineseInteger.toString();
}
/**
* 得到中文金额的小数部分。
*/
private static String getChineseDecimal(int[] decimals) {
StringBuffer chineseDecimal = new StringBuffer("");
for (int i = 0; i < decimals.length; i++) {
// 舍去3位小数之后的
if (i == 3)
break;
chineseDecimal.append(decimals[i] == 0 ? ""
: (NUMBERS[decimals[i]] + DUNIT[i]));
}
return chineseDecimal.toString();
}
/**
* 判断第5位数字的单位"万"是否应加。
*/
private static boolean isMust5(String integerStr) {
int length = integerStr.length();
if (length > 4) {
String subInteger = "";
if (length > 8) {
// 取得从低位数,第5到第8位的字串
subInteger = integerStr.substring(length - 8, length - 4);
} else {
subInteger = integerStr.substring(0, length - 4);
}
return Integer.parseInt(subInteger) > 0;
} else {
return false;
}
}
}
(2) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
System.out.println(MoneyUtil.toChinese("5000.23"));
}
}
10. Java中音频录制
(1) 创建EngineeCore.java文件
EngineeCore.java文件中内容:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.TargetDataLine;
public class EngineeCore {
private String filePath;
public EngineeCore(String filePath) {
this.filePath = filePath;
}
AudioFormat audioFormat;
TargetDataLine targetDataLine;
boolean flag = true;
private void stopRecognize() {
flag = false;
targetDataLine.stop();
targetDataLine.close();
}private AudioFormat getAudioFormat() {
float sampleRate = 16000;
// 8000,11025,16000,22050,44100
int sampleSizeInBits = 16;
// 8,16
int channels = 1;
// 1,2
boolean signed = true;
// true,false
boolean bigEndian = false;
// true,false
return new AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian);
}// end getAudioFormat
public void startRecognize() {
try {
// 获得指定的音频格式
audioFormat = getAudioFormat();
DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat);
targetDataLine = (TargetDataLine) AudioSystem.getLine(dataLineInfo);
// Create a thread to capture the microphone
// data into an audio file and start the
// thread running. It will run until the
// Stop button is clicked. This method
// will return after starting the thread.
flag = true;
new CaptureThread().start();
} catch (Exception e) {
e.printStackTrace();
} // end catch
}// end captureAudio method
class CaptureThread extends Thread {
public void run() {
AudioFileFormat.Type fileType = null;
File audioFile = new File(filePath);
fileType = AudioFileFormat.Type.WAVE;
//声音录入的权值
int weight = 2;
//判断是否停止的计数
int downSum = 0;
ByteArrayInputStream bais = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
AudioInputStream ais = null;
try {
targetDataLine.open(audioFormat);
targetDataLine.start();
byte[] fragment = new byte[1024];
ais = new AudioInputStream(targetDataLine);
while (flag) {
targetDataLine.read(fragment, 0, fragment.length);
//当数组末位大于weight时开始存储字节(有声音传入),一旦开始不再需要判断末位
if (Math.abs(fragment[fragment.length-1]) > weight || baos.size() > 0) {
baos.write(fragment);
System.out.println("守卫:"+fragment[0]+",末尾:"+fragment[fragment.length-1]+",lenght"+fragment.length);
//判断语音是否停止
if(Math.abs(fragment[fragment.length-1])<=weight){
downSum++;
}else{
System.out.println("重置奇数");
downSum=0;
}
//计数超过20说明此段时间没有声音传入(值也可更改)
if(downSum>20){
System.out.println("停止录入");
break;
}
}
}
//取得录音输入流
audioFormat = getAudioFormat();
byte audioData[] = baos.toByteArray();
bais = new ByteArrayInputStream(audioData);
ais = new AudioInputStream(bais, audioFormat, audioData.length / audioFormat.getFrameSize());
//定义最终保存的文件名
System.out.println("开始生成语音文件");
AudioSystem.write(ais, fileType, audioFile);
downSum = 0;
stopRecognize();
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭流
try {
ais.close();
bais.close();
baos.reset();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
(2) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
String filePath = "D://test.wav";
EngineeCore engineeCore = new EngineeCore(filePath);
engineeCore.startRecognize();
}
}
11. Java中播放Wav文件
(1) 创建WavUtil.java文件
WavUtil.java文件中内容:
import java.io.File;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;
public class WavUtil {
private static AudioFormat audioFormat = null;
private static SourceDataLine sourceDataLine = null;
private static DataLine.Info dataLine_info = null;
private static AudioInputStream audioInputStream = null;
public static void play(String file) throws Exception {
audioInputStream = AudioSystem.getAudioInputStream(new File(file));
//audioInputStream=AudioSystem.getAudioInputStream(new URL(file));
audioFormat = audioInputStream.getFormat();
System.out.println("每秒播放帧数:"+audioFormat.getSampleRate());
System.out.println("总帧数:"+audioInputStream.getFrameLength());
System.out.println("音频时长(秒):"+audioInputStream.getFrameLength()/audioFormat.getSampleRate());
dataLine_info = new DataLine.Info(SourceDataLine.class, audioFormat);
sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLine_info);
byte[] b = new byte[1024];
int len = 0;
sourceDataLine.open(audioFormat, 1024);
sourceDataLine.start();
while ((len = audioInputStream.read(b)) > 0) {
sourceDataLine.write(b, 0, len);
}
audioInputStream.close();
sourceDataLine.drain();
sourceDataLine.close();
}
}
(2) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
WavUtil.play("test.wav");
}
}
12. Java中读取wav音频为base64字符串
import java.io.File;
import java.io.FileInputStream;
import javax.xml.bind.DatatypeConverter;
public class test {
public static byte[] readFile(String url) throws Exception {
File file = new File(url);
byte[] buff = new byte[(int)file.length()];
FileInputStream epo = new FileInputStream(file);
epo.read(buff);
epo.close();
return buff;
}
public static String byteToBase64(byte[] buff) {
String baseStr = DatatypeConverter.printBase64Binary(buff);
return baseStr;
}
public static void main(String[] args) throws Exception {
byte[] buf = readFile("test.wav");
String str = byteToBase64(buf);
System.out.println(str.length());
}
}
13. Java中读写文件byte内容
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class test {
public static byte[] readFile(String url) throws Exception {
File file = new File(url);
byte[] buff = new byte[(int)file.length()];
FileInputStream epo = new FileInputStream(file);
epo.read(buff);
epo.close();
return buff;
}
private static void fileByteWrite(File file,byte[] file_con) {
//写入对象初始化
FileOutputStream fos = null;
try {
//写入对象初始化
fos = new FileOutputStream(file,true);
//写入文件
fos.write(file_con);
//释放对象
fos.close();
fos = null;
}
catch (IOException e) {
//释放对象
fos = null;
}
}
public static void main(String[] args) throws Exception {
byte[] buf = readFile("test.wav");
}
}
14. Java中Base64字符串转换
import javax.xml.bind.DatatypeConverter;
public class test {
public static void main(String[] args) throws Exception {
byte[] buff = "hello".getBytes();
//字符串转base64
String baseStr = DatatypeConverter.printBase64Binary(buff);
System.out.println(baseStr);
//base64转字符串
byte[] bufarray = DatatypeConverter.parseBase64Binary(baseStr);
System.out.println(new String(bufarray));
}
}
15. Java中十六进制字符串和byte转换函数
//十六进制编码数组
private final char[] hex_char = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e','f' };
public String Byte_To_Hex(byte[] bytes) {
char[] buf = new char[bytes.length * 2];
int index = 0;
byte[] arrayOfByte = bytes;
int j = bytes.length;
for (int i = 0; i < j; i++) {
byte b = arrayOfByte[i];
buf[(index++)] = hex_char[(b >>> 4 & 0xF)];
buf[(index++)] = hex_char[(b & 0xF)];
}
String hexStr = new String(buf);
//释放对象
buf = null;
bytes = null;
return hexStr;
}
public byte[] Hex_To_Byte(String hex_str) {
if ((hex_str == null) || (hex_str.trim().equals(""))) {
return new byte[0];
}
byte[] bytes = new byte[hex_str.length() / 2];
for (int i = 0; i < hex_str.length() / 2; i++) {
String subhexStr = hex_str.substring(i * 2, i * 2 + 2);
bytes[i] = ((byte) Integer.parseInt(subhexStr, 16));
//释放对象
subhexStr = null;
}
//释放对象
hex_str = null;
return bytes;
}
16. Java中Ascall码和字符串互转函数
public String Ascall_To_Str(String ascall_str) {
StringBuffer str = new StringBuffer();
for (int i = 0; i < ascall_str.length(); i += 2) {
String tem_str = ascall_str.substring(i, i + 2);
char hex = (char) Integer.parseInt(tem_str, 16);
str.append(hex);
//释放对象
tem_str = null;
}
String asc_str = str.toString();
asc_str = asc_str.replace("\u0000", "");
//释放对象
str.delete(0, str.length());
str = null;
return asc_str;
}
public String Str_To_Ascall(String hex_str) {
StringBuffer str = new StringBuffer();
char[] chars = hex_str.toCharArray();
for (int i = 0; i < chars.length; i++) {
int tem = chars[i];
String tem_hex = Integer.toHexString(tem);
str.append(tem_hex);
//释放对象
tem_hex = null;
}
String asc_str = str.toString();
//释放对象
str.delete(0, str.length());
str = null;
return asc_str;
}
17. Java中Unicode码和字符串互转函数(1)
public String Str_To_Unicode(String hex_str) {
StringBuffer sb = new StringBuffer();
char [] source_char = hex_str.toCharArray();
String unicode = null;
for (int i=0;i<source_char.length;i++) {
unicode = Integer.toHexString(source_char[i]);
if (unicode.length() <= 2) {
unicode = "00" + unicode;
}
sb.append(unicode);
}
unicode = sb.toString();
//释放对象
sb.delete(0, sb.length());
sb = null;
source_char = null;
return unicode;
}
public String Unicode_To_Str(String unicode_str) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < unicode_str.length(); i += 2) {
int hex = Integer.parseInt(unicode_str.substring(i, i + 2), 16);
char hex_char = (char) hex;
if (hex_char != 0) {
//追加数据
str.append((char) hex);
}
}
String unc_str = str.toString();
//释放对象
str.delete(0,str.length());
str = null;
return unc_str;
}
18. Java中道格拉斯普克算法
(1) 创建Gps.java文件
Gps.java文件中内容:
public class Gps {
//索引
private int index;
//经度
private double lat;
//纬度
private double lon;
public Gps(){
}
public Gps(int index, double lat, double lon) {
this.index = index;
this.lat = lat;
this.lon = lon;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public double getLat() {
return lat;
}
public void setLat(double lat) {
this.lat = lat;
}
public double getLon() {
return lon;
}
public void setLon(double lon) {
this.lon = lon;
}
@Override
public String toString() {
return "Gps [index=" + index + ", lat=" + lat + ", lon=" + lon + "]";
}
}
(2) 创建Douglas.java文件
Douglas.java文件中内容:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Douglas {
/**
* @method [calculationDistance]
* @param gps1 : 位置点1
* @param gps2 : 位置点2
* @description : 计算两点之间的距离
*/
private double calculationDistance(Gps gps1, Gps gps2){
double lat1 = gps1.getLat();
double lat2 = gps2.getLat();
double lng1 = gps1.getLon();
double lng2 = gps2.getLon();
double radLat1 = lat1 * Math.PI / 180.0;
double radLat2 = lat2 * Math.PI / 180.0;
double a = radLat1 - radLat2;
double b = (lng1 * Math.PI / 180.0) - (lng2 * Math.PI / 180.0);
double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
+ Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
return s * 6370996.81;
}
/**
* @method [distToSegment]
* @param start : 起始点
* @param end : 结束点
* @param center : 中心点
* @return dist : 距离
* @description : 计算两点之间的距离
*/
private double distToSegment(Gps start,Gps end,Gps center) {
double a = Math.abs(calculationDistance(start, end));
double b = Math.abs(calculationDistance(start, center));
double c = Math.abs(calculationDistance(end, center));
double p = (a + b + c) / 2.0;
double s = Math.sqrt(Math.abs(p * (p - a) * (p - b) * (p - c)));
double dist = (double)(s * 2.0 / a);
return dist;
}
/**
* @method [compressLine]
* @param traList : 过滤的轨迹数据
* @param result : 过滤后的轨迹数据
* @param start : 起始点
* @param end : 结束点
* @param dMax : 允许最大距离误差
* @description : 递归方式压缩轨迹
* */
private List<Gps> compressLine(List<Gps> traList,List<Gps> result,int start,int end,int dMax){
if(start < end) {
double maxDist = 0;
int currentIndex = 0;
Gps startPoint = traList.get(start);
Gps endPoint = traList.get(end);
Gps center = null;
///遍历轨迹点
for(int i = start + 1; i < end; i++) {
/**中心点*/
center = traList.get(i);
/**计算阀值*/
double currentDist = distToSegment(startPoint, endPoint, center);
if (currentDist > maxDist) {
maxDist = currentDist;
currentIndex = i;
}
}
if (maxDist >= dMax) {
//将当前点加入到过滤数组中
result.add(traList.get(currentIndex));
//将原来的线段以当前点为中心拆成两段,分别进行递归处理
compressLine(traList,result,start, currentIndex, dMax);
compressLine(traList,result,currentIndex, end, dMax);
}
}
return result;
}
/**
* @method [Peucker]
* @param traList : 过滤的轨迹数据
* @param dMax : 允许最大距离误差
* @description : 递归方式压缩轨迹
* */
public List<Gps> Peucker(List<Gps> traList,int dMax){
List<Gps> result = null;
if (traList != null && traList.size() > 2) {
result = new ArrayList<Gps>();
result = compressLine(traList, result,0, traList.size() - 1, dMax);
result.add(traList.get(0));
result.add(traList.get(traList.size() - 1));
result.sort(new Comparator<Gps>() {
@Override
public int compare(Gps gps1, Gps gps2) {
if (gps1.getIndex() < gps2.getIndex()) {
return -1;
}
if (gps1.getIndex() > gps2.getIndex()) {
return 1;
}
return 0;
}
});
}
return result;
}
}
(3) 创建test.java文件
test.java文件中内容:
import java.util.ArrayList;
import java.util.List;
public class test {
public static void main(String[] args) {
String[] list = {"117.212448,39.133785", "117.212669,39.133667", "117.213165,39.133297", "117.213203,39.13327",
"117.213554,39.133099", "117.213669,39.13295", "117.213921,39.132462", "117.214088,39.132126",
"117.214142,39.131962", "117.214188,39.13176", "117.214233,39.131397", "117.21418,39.13055",
"117.214279,39.130459", "117.214539,39.130375", "117.214874,39.130188", "117.216881,39.128716",
"117.217598,39.127995", "117.217972,39.12759", "117.218338,39.127178", "117.218407,39.127071",
"117.218567,39.126911", "117.219704,39.125702", "117.219795,39.12561", "117.220284,39.125114",
"117.220619,39.124802", "117.221046,39.124348", "117.221138,39.124245", "117.221268,39.124092",
"117.222321,39.122955", "117.222824,39.122406", "117.222916,39.122311", "117.223663,39.121544",
"117.2239,39.121452", "117.224113,39.12159", "117.224251,39.121677", "117.225136,39.122208",
"117.225281,39.122292", "117.225319,39.122311", "117.226273,39.122875", "117.226685,39.123127",
"117.227371,39.12352", "117.227806,39.123779", "117.228477,39.124134", "117.228531,39.124161",
"117.228531,39.124161", "117.228668,39.124187", "117.228897,39.124325", "117.229767,39.12479",
"117.230927,39.12545", "117.231186,39.12561", "117.231659,39.125908", "117.231834,39.126026",
"117.232018,39.126186", "117.232185,39.126362", "117.232353,39.126583", "117.232658,39.126972",
"117.232658,39.126972", "117.233124,39.12748", "117.233253,39.127609", "117.233368,39.127689",
"117.233513,39.127762", "117.233665,39.127823", "117.233734,39.127846", "117.233833,39.127865",
"117.233994,39.127888", "117.234138,39.127892", "117.234329,39.127884", "117.234612,39.127838",
"117.234955,39.127754", "117.235252,39.12767", "117.236282,39.12738", "117.237137,39.127129",
"117.237671,39.126961", "117.237953,39.126949", "117.238213,39.126865", "117.238472,39.126793",
"117.2397,39.126434", "117.242233,39.125698", "117.243538,39.12532", "117.243645,39.125298"};
List<Gps> gpsList = new ArrayList<Gps>();
double lat =0d;
double lon = 0d;
String[] str = null;
/**遍历*/
for(int i=0;i< list.length;i++) {
str = list[i].split(",");
lon = Double.parseDouble(str[0]);
lat = Double.parseDouble(str[1]);
/**追加数据*/
gpsList.add(new Gps(i,lat,lon));
}
Douglas douglas = new Douglas();
List<Gps> result = douglas.Peucker(gpsList, 10);
System.out.println("总数:" + result.size());
System.out.println(result);
}
}
19. Java中Netty集成Websocket
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
(2) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) {
new NettWebSocket().WebSocketServer();
}
}
(3) 创建NettWebSocket.java文件
NettWebSocket.java文件中内容:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettWebSocket {
public void WebSocketServer() {
//线程数
int boss_os_num = 8;
//数据接收线程组
EventLoopGroup bossLoop = new NioEventLoopGroup(boss_os_num);
//线程数
int event_os_num = 32;
//Tcp管理线程组
EventLoopGroup eventLoop = new NioEventLoopGroup(event_os_num);
try {
//配置netty中ServerBootstrap对象
ServerBootstrap serverBoot = new ServerBootstrap();
//配置Tcp参数
serverBoot.group(bossLoop,eventLoop)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024 * 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new WebSocketChannel());
ChannelFuture cf = serverBoot.bind(8080).sync();
//等待线程池结束
cf.channel().closeFuture().sync();
}
catch(Exception e) {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
finally {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
}
}
(4) 创建WebSocketChannel.java文件
WebSocketChannel.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
public class WebSocketChannel extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sch) throws Exception {
//管理数据接收管道
sch.pipeline().addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(65536 * 10000))
.addLast(new WebSocketHandler());
}
}
(5) 创建WebSocketHandler.java文件
WebSocketHandler.java文件中内容:
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;
import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class WebSocketHandler extends SimpleChannelInboundHandler<Object> {
//web握操作对象
private WebSocketServerHandshaker handshaker = null;
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object obj) throws Exception {
//检测请求状态
if (obj instanceof FullHttpRequest) {
//http请求响应
WebHttpRequest(ctx, ((FullHttpRequest) obj));
} else if (obj instanceof WebSocketFrame) {
WebSocketRequest(ctx, (WebSocketFrame) obj);
}
}
/*
* @method [WebHttpRequest]
* @param [ChannelHandlerContext] ctx : 广播数据接收通道对象
* @param [FullHttpRequest] req : http请求数据
* @description : http请求响应
* */
private void WebHttpRequest(ChannelHandlerContext ctx,FullHttpRequest req) {
if (!req.decoderResult().isSuccess()
|| (!"websocket".equals(req.headers().get("Upgrade")))) {
Htp_Res_Send(ctx,"访问失败");
return;
}
//请求地址
String web_url = WebSocketUrl(req);
WebSocketServerHandshakerFactory wsFactory = new
WebSocketServerHandshakerFactory(web_url, null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
//握手
handshaker.handshake(ctx.channel(), req);
}
}
private void WebSocketRequest(ChannelHandlerContext ctx, WebSocketFrame frame) {
// TODO Auto-generated method stub
//地址标识
Channel ch = ctx.channel();
//连接地址
String remote_addr = ch.remoteAddress().toString();
//判断是否关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame
.retain());
}
//判断是否ping消息
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(
new PongWebSocketFrame(frame.content().retain()));
return;
}
//注册消息
String web_req = ((TextWebSocketFrame) frame).text();
System.out.println(web_req);
//初始化消息
TextWebSocketFrame txt_str = new TextWebSocketFrame(web_req);
//消息推送
ctx.writeAndFlush(txt_str);
}
/*
* @method [WebSocketUrl]
* @param [FullHttpRequest] req : http请求数据
* @param [String] location : websocket请求地址
* @description : http地址
* */
private String WebSocketUrl(FullHttpRequest req) {
String location = req.headers().get(HOST).toString();
return "ws://" + location;
}
public void Htp_Res_Send(ChannelHandlerContext htp_ctx,String cmd_str) {
//检测对象
if(htp_ctx != null) {
//回复对象
DefaultFullHttpResponse htp_res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
//错误对象
ByteBuf buf = Unpooled.copiedBuffer(cmd_str,CharsetUtil.UTF_8);
//允许跨域放
htp_res.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN,"*");
htp_res.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS,"true");
htp_res.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS,"cache-control,content-type,hash-referer,x-requested-with");
//写明类型
htp_res.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
//写入内容
htp_res.content().writeBytes(buf);
//回复内容
htp_ctx.write(htp_res);
//断开连接
htp_ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
//释放对象
buf = null;
htp_res = null;
cmd_str = null;
}
}
/*
* @method [exceptionCaught]
* @param [ChannelHandlerContext] ctx : 广播协议接入通道对象
* @param [Throwable] cause : 广播异常信息
* @description : 广播异常信息处理
* */
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
//释放对象
ctx.close();
ctx = null;
}
}
20. Java中Netty集成Http服务器
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
(2) 创建test.java文件
public class test {
public static void main(String[] args) {
new nettyHttp().HttpServer();
}
}
(3) 创建nettyHttp.java文件
nettyHttp.java文件中内容:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class nettyHttp {
public void HttpServer() {
//线程数
int boss_os_num = 8;
//数据接收线程组
EventLoopGroup bossLoop = new NioEventLoopGroup(boss_os_num);
//线程数
int event_os_num = 32;
//Tcp管理线程组
EventLoopGroup eventLoop = new NioEventLoopGroup(event_os_num);
try {
//配置netty中ServerBootstrap对象
ServerBootstrap serverBoot = new ServerBootstrap();
//配置Tcp参数
serverBoot.group(bossLoop,eventLoop)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024 * 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new HttpChannel());
ChannelFuture cf = serverBoot.bind(8080).sync();
//等待线程池结束
cf.channel().closeFuture().sync();
}
catch(Exception e) {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
finally {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
}
}
(4) 创建HttpChannel.java文件
HttpChannel.java文件中内容:
import java.util.concurrent.TimeUnit;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
public class HttpChannel extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sch) throws Exception {
//管理数据接收管道
sch.pipeline().addLast(new IdleStateHandler(30,0,0,TimeUnit.SECONDS))
.addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(65536 * 10000))
.addLast(new HttpHandler());
}
}
(5) 创建HttpHandler.java文件
HttpHandler.java文件中内容:
import java.io.File;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.util.regex.Pattern;
import javax.activation.MimetypesFileTypeMap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.stream.ChunkedFile;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.SystemPropertyUtil;
public class HttpHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object obj) throws Exception {
//检测请求状态
if (obj instanceof FullHttpRequest) {
//http请求响应
WebHttpRequest(ctx, ((FullHttpRequest) obj));
}
}
/*
* @method [userEventTriggered]
* @param [ChannelHandlerContext] ctx : 广播协议接入通道对象
* @param [Object] evt : 接收到的数据信息
* @description : 超时计算
* */
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
throws Exception {
//检测到期时间
if (evt instanceof IdleStateEvent) {
//地址标识
Channel ch = ctx.channel();
//连接地址
String remote_addr = ch.remoteAddress().toString();
//超时操作
Htp_Res_Send(ctx,"访问超时");
} else {
//监听消息
super.userEventTriggered(ctx, evt);
}
}
//post
private final String post_url = "/post";
//上传
private final String upload_url = "/upload";
private void WebHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
//验证请求
if (req.decoderResult().isSuccess()) {
//检测对象
if(req.method().equals(HttpMethod.POST)) {
//地址解析
String req_url = req.uri().toString();
//解析请求数据
ByteBuf buf = req.content();
//数据解析
String req_str = buf.toString(CharsetUtil.UTF_8);
//请求地址分组
switch(req_url) {
//post访问
case post_url:
System.out.println(req_str);
Htp_Res_Send(ctx,"访问成功");
break;
case upload_url :
//异常捕获
try {
//文件解码
HttpDataFactory htp_fac = new DefaultHttpDataFactory(false);
//http请求解密
HttpPostRequestDecoder htp_dec = new HttpPostRequestDecoder(htp_fac, req);
//检测对象
if (htp_dec != null && req instanceof HttpContent) {
//读取文件
HttpContent htp_chunk = (HttpContent) req;
//读取内存
htp_dec.offer(htp_chunk);
//检测对象
if(htp_chunk instanceof LastHttpContent) {
//遍历接口
while(htp_dec.hasNext()) {
//数据内容
InterfaceHttpData htp_data = htp_dec.next();
//文件上传对象
FileUpload htp_file = (FileUpload) htp_data;
//检测对象
if(htp_file.isCompleted()) {
//文件名称
String file_name = htp_file.getFilename();
//ota文件
File file = new File("D:\\upload\\"+ file_name);
//检测父文件夹
if (!file.getParentFile().exists()) {
//创建文件夹
file.getParentFile().mkdirs();
}
//检测文件是否存在
if (!file.exists()) {
//创建文件
file.createNewFile();
}
//重新替换
htp_file.renameTo(file);
//清除上传
htp_dec.removeHttpDataFromClean(htp_file);
}
//释放对象
htp_data.release();
htp_data = null;
htp_file = null;
}
}
}
//释放对象
htp_dec.destroy();
htp_fac.cleanAllHttpData();
htp_dec = null;
htp_fac = null;
}
catch(Exception e) {
Htp_Res_Send(ctx,"上传成功");
}
break;
default :
Htp_Res_Send(ctx,"访问错误");
break;
}
//释放对象
ReferenceCountUtil.release(buf);
buf.clear();
}
//get请求
if(req.method().equals(HttpMethod.GET)) {
//异常捕获
try {
//地址解析
String req_url = req.uri();
//解析文件路径
final String file_path = Request_Url(req_url);
//检测对象
if(file_path == null) {
Htp_Res_Send(ctx,"访问错误");
}
//初始化对象
File file_url = new File(file_path);
//检测对象
if(file_url.isHidden() || !file_url.exists()) {
Htp_Res_Send(ctx,"访问错误");
}
//检测对象
if (file_url.isDirectory()) {
Htp_Res_Send(ctx,"访问错误");
}
//随机读取文件
RandomAccessFile raf = new RandomAccessFile(file_url, "r");
//文件长度
long fileLength = raf.length();
//回复头
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
//标识长度
HttpUtil.setContentLength(response, fileLength);
//设置内容
setContentTypeHeader(response, file_url);
//保持长连接
if (HttpUtil.isKeepAlive(req)) {
//允许跨域放
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN,"*");
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS,"true");
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS,"cache-control,content-type,hash-referer,x-requested-with");
//写明类型
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream; charset=UTF-8");
//设置长连接
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
//写入回复
ctx.write(response);
//通过Netty的ChunkedFile对象直接将文件写入发送到缓冲区中
ctx.write(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)),ctx.newProgressivePromise());
//通道对象
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
//检测对象
if (!HttpUtil.isKeepAlive(req)) {
//关闭连接
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
catch(Exception e) {
Htp_Res_Send(ctx,"访问错误");
}
}
}
}
/**
* @mthod [Request_Url]
* @param [String] url : 路径地址
* @description : 请求地址
* */
private String Request_Url(String url) {
//转移路径
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (Exception e) {
throw new Error(e);
}
//检测路径
if (url.isEmpty() || url.charAt(0) != '/') {
return null;
}
//文件路径分割
url = url.replace('/', File.separatorChar);
//文件分割
if (url.contains(File.separator + '.') ||
url.contains('.' + File.separator) ||
url.charAt(0) == '.' || url.charAt(url.length() - 1) == '.' ||
insecure_url.matcher(url).matches()) {
return null;
}
//转换地址
return SystemPropertyUtil.get("user.dir") + File.separator + url;
}
//安全地址正则表达式
private Pattern insecure_url = Pattern.compile(".*[<>&\"].*");
/**
* @mthod [setContentTypeHeader]
* @param [HttpResponse] response : 回复对象
* @param [File] file : 文件对象
* @description : 请求地址
* */
private void setContentTypeHeader(HttpResponse response, File file) {
//文件map对象
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
//设置通知对象
response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
}
/*
* @method [Htp_Res_Send]
* @param [ChannelHandlerContext] htp_ctx : 通道对象
* @param [String] htp_str : ota消息内容
* @description : 消息回复
* */
public void Htp_Res_Send(ChannelHandlerContext htp_ctx,String htp_str) {
//检测对象
if(htp_ctx != null) {
//回复对象
DefaultFullHttpResponse htp_res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
//错误对象
ByteBuf ota_buf = Unpooled.copiedBuffer(htp_str,CharsetUtil.UTF_8);
//允许跨域放
htp_res.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN,"*");
htp_res.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS,"true");
htp_res.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS,"cache-control,content-type,hash-referer,x-requested-with");
//写明类型
htp_res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/json; charset=UTF-8");
//写入内容
htp_res.content().writeBytes(ota_buf);
//回复内容
htp_ctx.write(htp_res);
//断开连接
htp_ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
//释放对象
ota_buf.clear();
htp_res = null;
ota_buf = null;
}
}
/*
* @method [exceptionCaught]
* @param [ChannelHandlerContext] ctx : 广播协议接入通道对象
* @param [Throwable] cause : 广播异常信息
* @description : 广播异常信息处理
* */
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
//释放对象
ctx.close();
ctx = null;
}
}
21. Java中Netty集成Tcp服务器端(避免断包)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
(2) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) {
new nettyTcp().TcpServer();
}
}
(3) 创建nettyTcp.java文件
nettyTcp.java文件中内容:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class nettyTcp {
public void TcpServer() {
//线程数
int boss_os_num = 8;
//数据接收线程组
EventLoopGroup bossLoop = new NioEventLoopGroup(boss_os_num);
//线程数
int event_os_num = 32;
//Tcp管理线程组
EventLoopGroup eventLoop = new NioEventLoopGroup(event_os_num);
try {
//配置netty中ServerBootstrap对象
ServerBootstrap serverBoot = new ServerBootstrap();
//配置Tcp参数
serverBoot.group(bossLoop,eventLoop)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024 * 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new TcpChannel());
ChannelFuture cf = serverBoot.bind(8080).sync();
//等待线程池结束
cf.channel().closeFuture().sync();
}
catch(Exception e) {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
finally {
//释放数据接收线程组
bossLoop.shutdownGracefully();
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
}
}
(4) 创建TcpChannel.java文件
TcpChannel.java文件中内容:
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
public class TcpChannel extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sch) throws Exception {
//管理数据接收管道
sch.pipeline().addLast(new ObjectEncoder())
.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(this.getClass().getClassLoader())))
.addLast(new TcpHandler());
//通道关闭事件
ChannelFuture scf = sch.closeFuture();
//添加关闭事件
scf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture cf) throws Exception {
//通道关闭事件
Channel ch = cf.channel();
//连接地址
String remote_addr = ch.remoteAddress().toString();
}
});
}
}
(5) 创建TcpHandler.java文件
TcpHandler.java文件中内容:
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TcpHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//地址标识
Channel ch = ctx.channel();
//连接地址
String remote_addr = ch.remoteAddress().toString();
//转换字节编码
String ser_str = (String) msg;
//回复广播接口
ctx.writeAndFlush(ser_str);
}
/*
* @method [exceptionCaught]
* @param [ChannelHandlerContext] ctx : 广播协议接入通道对象
* @param [Throwable] cause : 广播异常信息
* @description : 广播异常信息处理
* */
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
//释放对象
ctx.close();
ctx = null;
}
}
22. Java中Netty集成Tcp客户端(避免断包)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.39.Final</version>
</dependency>
(2) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) {
new nettyTcp().TcpClient();
}
}
(3) 创建nettyTcp.java文件
nettyTcp.java文件中内容:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class nettyTcp {
public void TcpClient() {
//线程数
int event_os_num = 32;
//Tcp管理线程组
EventLoopGroup eventLoop = new NioEventLoopGroup(event_os_num);
try {
//配置netty中ServerBootstrap对象
Bootstrap ClientBoot = new Bootstrap();
//配置Tcp参数
ClientBoot.group(eventLoop)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new TcpChannel());
ChannelFuture cf = ClientBoot.connect("120.24.248.220",8006).sync();
//管道对象
Channel ch = cf.channel();
//接入广播节点
ch.writeAndFlush("{\"status\":101}");
}
catch(Exception e) {
//释放Tcp线程组
eventLoop.shutdownGracefully();
}
}
}
(4) 创建TcpChannel.java文件
TcpChannel.java文件中内容:
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
public class TcpChannel extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sch) throws Exception {
//管理数据接收管道
sch.pipeline().addLast(new ObjectEncoder())
.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(this.getClass().getClassLoader())))
.addLast(new TcpHandler());
//通道关闭事件
ChannelFuture scf = sch.closeFuture();
//添加关闭事件
scf.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture cf) throws Exception {
//通道关闭事件
Channel ch = cf.channel();
//连接地址
String remote_addr = ch.remoteAddress().toString();
}
});
}
}
(5) 创建TcpHandler.java文件
TcpHandler.java文件中内容:
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TcpHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//地址标识
Channel ch = ctx.channel();
//连接地址
String remote_addr = ch.remoteAddress().toString();
//转换字节编码
String ser_str = (String) msg;
System.out.println(ser_str);
//回复广播接口
ctx.writeAndFlush(ser_str);
}
/*
* @method [exceptionCaught]
* @param [ChannelHandlerContext] ctx : 广播协议接入通道对象
* @param [Throwable] cause : 广播异常信息
* @description : 广播异常信息处理
* */
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
//释放对象
ctx.close();
ctx = null;
}
}
23. Java中监听Redis过期时间
(1) 依赖包
<!--redis连接-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
(2) 创建test.java文件
test.java文件中内容:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class test {
//只有修改配置文件redis.conf中的:notify-keyspace-events Ex,默认为notify-keyspace-events ""
//修改好配置文件后,redis会对设置了expire的数据进行监听,当数据过期时便会将其从redis中删除:
@SuppressWarnings("resource")
public static void main(String[] args) {
//连接池配置
JedisPoolConfig config = new JedisPoolConfig();
//设置等待时间
config.setMaxWaitMillis(5000);
//获取连接时检测有效性
config.setTestOnBorrow(false);
config.setTestOnReturn(true);
//空闲时检查连接有效性
config.setTestWhileIdle(true);
//释放连接扫描间隔
config.setTimeBetweenEvictionRunsMillis(5000);
//表示每次释放连接的最大数目
config.setNumTestsPerEvictionRun(5000);
//这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义
config.setMinEvictableIdleTimeMillis(5000);
//连接耗尽是否阻塞
config.setBlockWhenExhausted(false);
JedisPool pool = new JedisPool(config, "127.0.0.1",8002);
Jedis jedis = pool.getResource();
jedis.psubscribe(new KeyExpiredListener(), "__key*__:*");
}
}
(3) 创建KeyExpiredListener.java文件
KeyExpiredListener.java文件中内容:
import redis.clients.jedis.JedisPubSub;
public class KeyExpiredListener extends JedisPubSub {
@Override
public void onPSubscribe(String pattern, int subscribedChannels) {
System.out.println("onPSubscribe "
+ pattern + " " + subscribedChannels);
}
@Override
public void onPMessage(String pattern, String channel, String message) {
System.out.println("onPMessage pattern "
+ pattern + " " + channel + " " + message);
}
}
**注:**修改redis配置文件redis.conf
notify-keyspace-events Ex notify-keyspace-events ""修改完成后重启redis此时设置expire过期时间,当数据过期时触发
24. Java中使用Forest接口访问
(1) 依赖包
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>spring-boot-starter-forest</artifactId>
<version>1.3.8</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
(2) 创建MyClient.java文件
MyClient.java文件中内容:
import com.dtflys.forest.annotation.DataParam;
import com.dtflys.forest.annotation.Request;
public interface MyClient {
@Request(
url = "http://www.carhere.net/",
type = "get",
headers = {"Accept:text/plan"}
)
String send(@DataParam("username") String username);
}
(3) 创建test.java文件
test.java文件中内容:
import com.dtflys.forest.config.ForestConfiguration;
import com.dtflys.forest.ssl.SSLUtils;
public class test {
public static void main(String[] args) throws Exception {
ForestConfiguration configuration = ForestConfiguration.configuration();
configuration.setBackendName("okhttp3");
// 连接池最大连接数,默认值为500
configuration.setMaxConnections(123);
// 每个路由的最大连接数,默认值为500
configuration.setMaxRouteConnections(222);
// 请求超时时间,单位为毫秒, 默认值为3000
configuration.setTimeout(3000);
// 连接超时时间,单位为毫秒, 默认值为2000
configuration.setConnectTimeout(2000);
// 请求失败后重试次数,默认为0次不重试
configuration.setRetryCount(3);
// 单向验证的HTTPS的默认SSL协议,默认为SSLv3
configuration.setSslProtocol(SSLUtils.SSLv3);
// 打开或关闭日志,默认为true
configuration.setLogEnabled(false);
//初始化客户端
MyClient myClient = configuration.createInstance(MyClient.class);
String result = myClient.send("test");
System.out.println(result);
}
}
25. Java中使用自定义异常重写FillInStackTrace
(1) 创建ApiException.java文件
ApiException.java文件中内容:
public class ApiException extends Exception {
private static final long serialVersionUID = 1L;
public ApiException(String message) {
super(message);
}
/*
* 重写fillInStackTrace方法会使得这个自定义的异常不会收集线程的整个异常栈信息,会大大
* 提高减少异常开销。
*/
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
(2) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) {
try {
throw new ApiException("由于MyException重写了fillInStackTrace方法,那么它不会收集线程运行栈信息。");
} catch (ApiException e) {
e.printStackTrace();
}
}
}
26. Java中使用Tessercat图像识别
(1) 依赖包
<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>4.4.0</version>
</dependency>
(2) 创建test.java文件
test.java文件中内容:
import java.io.File;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;
public class test {
public static void main(String[] args) throws Exception {
// 识别图片的路径(修改为自己的图片路径)
String path = "test.png";
// 语言库位置(修改为跟自己语言库文件夹的路径)
String lagnguagePath = "D:\\Program Files\\Tesseract-OCR\\tessdata";
File file = new File(path);
ITesseract instance = new Tesseract();
//设置训练库的位置
instance.setDatapath(lagnguagePath);
//chi_sim :简体中文, eng 根据需求选择语言库
instance.setLanguage("chi_sim");
String result = null;
try {
long startTime = System.currentTimeMillis();
result = instance.doOCR(file);
long endTime = System.currentTimeMillis();
System.out.println("Time is:" + (endTime - startTime) + " 毫秒");
} catch (TesseractException e) {
e.printStackTrace();
}
System.out.println("result: ");
System.out.println(result);
}
}
**注:**需安装
Tessercat并设置成环境变量
27. Java中公平分配Hash一致算法
import java.util.UUID;
public class test {
public static int FnvHash(String key) {
final int p = 16777619;
long hash = (int) 2166136261L;
for (int i = 0, n = key.length(); i < n; i++) {
hash = (hash ^ key.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return ((int) hash & 0x7FFFFFFF) ;
}
public static void main( String[] args ) {
//a,b,c,d,分别记录四组分到的imei个数
int a = 0;
int b = 0;
int c = 0;
int d = 0;
for (int i = 0; i < 10000; i++) {
//模拟15位的imei码
String str = UUID.randomUUID().toString().replaceAll("-","");
//模4 将FnvHash算法得到的固定结果分成四组
int hash = FnvHash(str) % 4;
switch (hash){
case 0:a++;break;
case 1:b++;break;
case 2:c++;break;
case 3:d++;break;
}
}
System.out.println("a:" + a);
System.out.println("b:" + b);
System.out.println("c:" + c);
System.out.println("d:" + d);
System.out.println("a+b+c+d:" + (a + b + c + d));
}
}
28. Java中负载均衡算法
1.随机算法
通过系统随机函数,根据后台服务器的server的地址随机选取其中一台服务器进行访问,根据概率论的相关知识,随着调用量的增加,最终的访问趋于平均,就是达到了均衡的目的。
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
public class test {
static Map<String,Integer> ipMap=new ConcurrentHashMap<String,Integer>();
static {
ipMap.put("192.168.13.1",1);
ipMap.put("192.168.13.2",2);
ipMap.put("192.168.13.3",4);
}
public String Random() {
Set<String> ipSet=ipMap.keySet();
//定义一个list放所有server
ArrayList<String> ipArrayList=new ArrayList<String>();
ipArrayList.addAll(ipSet);
//循环随机数
Random random=new Random();
//随机数在list数量中取(1-list.size)
int pos=random.nextInt(ipArrayList.size());
String serverNameReturn= ipArrayList.get(pos);
return serverNameReturn;
}
public static void main(String[] args) {
test testRandom=new test();
for (int i =0;i<10;i++){
String server=testRandom.Random();
System.out.println(server);
}
}
}
2.加权随机算法
加权随机算法就是在上面的随机算法的基础上做的优化,比如一些性能好的Server多承担一些,请求根据权重分发到各个服务器。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
public class test {
static Map<String,Integer> ipMap=new ConcurrentHashMap<String,Integer>();
static {
ipMap.put("192.168.13.1",1);
ipMap.put("192.168.13.2",2);
ipMap.put("192.168.13.3",4);
}
public String weightRandom() {
Set<String> ipSet = ipMap.keySet();
Iterator<String> ipIterator = ipSet.iterator();
//定义一个list放所有server
ArrayList<String> ipArrayList = new ArrayList<String>();
//循环set,根据set中的可以去得知map中的value,给list中添加对应数字的server数量
while (ipIterator.hasNext()) {
String serverName = ipIterator.next();
Integer weight = ipMap.get(serverName);
for (int i = 0; i < weight; i++) {
ipArrayList.add(serverName);
}
}
//循环随机数
Random random = new Random();
//随机数在list数量中取(1-list.size)
int pos = random.nextInt(ipArrayList.size());
String serverNameReturn = ipArrayList.get(pos);
return serverNameReturn;
}
public static void main(String[] args) {
test testRandom=new test();
for (int i =0;i<10;i++){
String server=testRandom.weightRandom();
System.out.println(server);
}
}
}
3.轮询算法
轮询算法顾名思义,就是按照顺序轮流访问后台服务。
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class test {
static Map<String,Integer> ipMap=new ConcurrentHashMap<String,Integer>();
static {
ipMap.put("192.168.13.1",1);
ipMap.put("192.168.13.2",2);
ipMap.put("192.168.13.3",4);
}
Integer pos = 0;
public String RoundRobin(){
Map<String,Integer> ipServerMap=new ConcurrentHashMap<>();
ipServerMap.putAll(ipMap);
//2.取出来key,放到set中
Set<String> ipset=ipServerMap.keySet();
//3.set放到list,要循环list取出
ArrayList<String> iplist=new ArrayList<String>();
iplist.addAll(ipset);
String serverName=null;
//4.定义一个循环的值,如果大于set就从0开始
synchronized(pos){
if (pos>=ipset.size()){
pos=0;
}
serverName=iplist.get(pos);
//轮询+1
pos ++;
}
return serverName;
}
public static void main(String[] args) {
test testRandom=new test();
for (int i =0;i<12;i++){
String server=testRandom.RoundRobin();
System.out.println(server);
}
}
}
4.加权轮询算法
加权随机一样,加权轮询,就是在轮询的基础上加上权重,将服务器性能好的,权重高一些。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class test {
static Map<String,Integer> ipMap=new ConcurrentHashMap<String,Integer>();
static {
ipMap.put("192.168.13.1",1);
ipMap.put("192.168.13.2",2);
ipMap.put("192.168.13.3",4);
}
Integer pos=0;
public String weightRoundRobin(){
Map<String,Integer> ipServerMap=new ConcurrentHashMap<>();
ipServerMap.putAll(ipMap);
Set<String> ipSet=ipServerMap.keySet();
Iterator<String> ipIterator=ipSet.iterator();
//定义一个list放所有server
ArrayList<String> ipArrayList=new ArrayList<String>();
//循环set,根据set中的可以去得知map中的value,给list中添加对应数字的server数量
while (ipIterator.hasNext()){
String serverName=ipIterator.next();
Integer weight=ipServerMap.get(serverName);
for (int i = 0;i < weight ;i++){
ipArrayList.add(serverName);
}
}
String serverName=null;
if (pos>=ipArrayList.size()){
pos=0;
}
serverName=ipArrayList.get(pos);
//轮询+1
pos ++;
return serverName;
}
public static void main(String[] args) {
test testRandom=new test();
for (int i =0;i<12;i++){
String server=testRandom.weightRoundRobin();
System.out.println(server);
}
}
}
5.Ip-Hash算法
根据`hash`算法,将请求大致均分的分配到各个服务器上。
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class test {
static Map<String,Integer> ipMap=new ConcurrentHashMap<String,Integer>();
static {
ipMap.put("192.168.13.1",1);
ipMap.put("192.168.13.2",2);
ipMap.put("192.168.13.3",4);
}
public String ipHash(String clientIP) {
//2.取出来key,放到set中
Set<String> ipset = ipMap.keySet();
//3.set放到list,要循环list取出
ArrayList<String> iplist = new ArrayList<String>();
iplist.addAll(ipset);
//对ip的hashcode值取余数,每次都一样的
int hashCode = clientIP.hashCode();
int serverListsize = iplist.size();
int pos = hashCode % serverListsize;
return iplist.get(pos);
}
public static void main(String[] args) {
test testIpHash=new test();
for (int i =0;i<12;i++){
System.out.println(testIpHash.ipHash("192.168.21.2"));
System.out.println(testIpHash.ipHash("192.168.21.3"));
System.out.println(testIpHash.ipHash("192.168.21.1"));
}
}
}
6.最小连接数算法
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class test {
static Map<String,Integer> ipMap=new ConcurrentHashMap<String,Integer>();
static {
ipMap.put("192.168.13.1",0);
ipMap.put("192.168.13.2",0);
ipMap.put("192.168.13.3",0);
}
//从list中选取接受请求数最少的服务并返回
public String leastConnection() {
Iterator<String> ipListIterator = ipMap.keySet().iterator();
String serverName = null;
int times = 0;//访问次数
while (ipListIterator.hasNext()) {
String tmpServerName = ipListIterator.next();
int requestTimes = ipMap.get(tmpServerName);
//第一次需要赋值
if (times == 0) {
serverName = tmpServerName;
times = requestTimes;
} else {
//找到最小次数
if (times > requestTimes) {
serverName = tmpServerName;
times = requestTimes;
}
}
}
ipMap.put(serverName, ++times);//访问后+1
System.out.println("获取到的地址是:" + serverName + ", 访问次数:" + times);
return serverName;
}
public static void main(String[] args) {
test testLeastConnection =new test();
for (int i =0;i<10;i++){
testLeastConnection.leastConnection();
}
}
}
29. Java中视频添加水印
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class test {
public static void main(String[] args) throws Exception {
List<String> command = new ArrayList<>();
command.add("F:\\ffmpeg\\bin\\ffmpeg.exe");
command.add("-y");
command.add("-i");
command.add("E:\\xx.mp4");
command.add("-vf");
command.add("\"movie=xx.png");
command.add("[logo];[in][logo]");
command.add("overlay=x=5;y=5");
command.add("[out]\"");
command.add("E:\\xx.mp4");
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
InputStream in = process.getErrorStream();
byte[] re = new byte[1024];
while(in.read(re) != -1){
System.out.println(new String(re));
}
in.close();
if(process.isAlive()){
process.waitFor();
}
}
}
30. Java中Hbase创建表
依赖包:
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.0.4</version>
</dependency>
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder.ModifyableColumnFamilyDescription;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.TableDescriptorBuilder.ModifyableTableDescriptor;
public class test {
public static void main(String[] args) throws Exception {
Confuration cnf = HBaseConfiguration.create();
cnf.set("hbase.zookeeper.quorum","<zookeeper的ip地址>");
cnf.set("hbase.zookeeper.property.clientPort","2181");
cnf.set("zookeeper.znode.parent",'/hbase/master');
Connection connection = ConnectionFactory.createConnection(cnf);
//表名
String tableName = "user";
//列名
String columnName = "info";
Admin admin = connection.getAdmin();
TableName table = TableName.valueOf(tableName);
ModifyableTableDescriptor descriptor = new ModifyableTableDescriptor(table);
ModifyableColumnFamilyDescriptor family = new ModifyableTableDescriptor(columnName.getBytes());
descriptor.setColumnFamily(family);
admin.createTable(description);
admin.close();
connection.close();
}
}
31. Java中Hbase插入表数据
依赖包:
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.0.4</version>
</dependency>
import org.apache.hadoop.cnf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.habse.util.Bytes;
public class test {
public static void main(String[] args) throws Exception {
Confuration cnf = HBaseConfiguration.create();
cnf.set("hbase.zookeeper.quorum","<zookeeper的ip地址>");
cnf.set("hbase.zookeeper.property.clientPort","2181");
cnf.set("zookeeper.znode.parent",'/hbase/master');
Connection connection = ConnectionFactory.createConnection(cnf);
//表名
String tableName = "user";
//列族名
String columnName = "info";
String rowKey = "102";
//列名
String column = "name";
//值内容
String value = "test";
Admin admin = connection.getAdmin();
Table table = connection.getTable(TableName.valueOf(tableName));
Put p = new Put(Bytes.toBytes(rowKey));
p.addColumn(Bytes.toBytes(columnName),Bytes.toBytes(column),Bytes.toByte(value));
table.put(p);
admin.close();
connection.close();
}
}
32. Java中Hbase查询表数据
依赖包:
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.0.4</version>
</dependency>
import org.apahce.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;
public class test {
public static void main(String[] args) throws Exception {
Confuration cnf = HBaseConfiguration.create();
cnf.set("hbase.zookeeper.quorum","<zookeeper的ip地址>");
cnf.set("hbase.zookeeper.property.clientPort","2181");
cnf.set("zookeeper.znode.parent",'/hbase/master');
Connection connection = ConnectionFactory.createConnection(cnf);
//表名
String tableName = "user";
//列族名
String columnName = "info";
String rowKey = "102";
//列名
String column = "name";
Admin admin = connection.getAdmin();
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(rowKey.getBytes());
get.addColumn(columnName.getBytes(),column.getBytes());
Result res = table.get(get);
String val = Bytes.toString(res.getValue(columnName.getBytes(),column.getBytes()));
admin.close();
connection.close();
}
}
33. Java中Hbase删除数据
依赖包:
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.0.4</version>
</dependency>
import org.apache.hadoop.cnf.Configuration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Table;
public class test {
public static void main(String[] args) throws Exception {
Confuration cnf = HBaseConfiguration.create();
cnf.set("hbase.zookeeper.quorum","<zookeeper的ip地址>");
cnf.set("hbase.zookeeper.property.clientPort","2181");
cnf.set("zookeeper.znode.parent",'/hbase/master');
Connection connection = ConnectionFactory.createConnection(cnf);
//表名
String tableName = "user";
//列族名
String columnName = "info";
String rowKey = "102";
//列名
String column = "name";
Admin admin = connection.getAdmin();
Table table = connection.getTable(TableName.valueOf(tableName));
Delete delete = new Delete(rowKey.getBytes());
delete.addColumn(columnName.getBytes(),column.getBytes());
table.delete(delete);
admin.close();
connection.close();
}
}
**注:**更新key值使用put,rowKey,columnName不需要变更,更新value值即可
34. Java中Hbase删除表
依赖包:
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.0.4</version>
</dependency>
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Table;
public class test {
public static void main(String[] args) throws Exception {
Confuration cnf = HBaseConfiguration.create();
cnf.set("hbase.zookeeper.quorum","<zookeeper的ip地址>");
cnf.set("hbase.zookeeper.property.clientPort","2181");
cnf.set("zookeeper.znode.parent",'/hbase/master');
Connection connection = ConnectionFactory.createConnection(cnf);
//表名
String tableName = "user";
Admin admin = connection.getAdmin();
Table table = connection.getTable(TableName.valueOf(tableName));
admin.disableTable(table);
admin.deleteTable(table);
admin.close();
connection.close();
}
}
35. Java中Hbase全表扫描
依赖包:
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.0.4</version>
</dependency>
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;
public class test {
public static void main(String[] args) throws Exception {
Confuration cnf = HBaseConfiguration.create();
cnf.set("hbase.zookeeper.quorum","<zookeeper的ip地址>");
cnf.set("hbase.zookeeper.property.clientPort","2181");
cnf.set("zookeeper.znode.parent",'/hbase/master');
Connection connection = ConnectionFactory.createConnection(cnf);
//表名
String tableName = "user";
//列族名
String columnName = "info";
//列名
String column = "name";
Admin admin = connection.getAdmin();
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
ResultScanner scanner = table.getScanner(scan);
for(Result result : scanner){
System.out.println(Bytes.toString(result.getValue(Bytes.toBytes(columnName),Bytes.toBytes(column))));
}
admin.close();
connection.close();
}
}
36. Java中word添加水印
(1) 下载Spire.Doc.Free并引入
import java.awt.Color;
import com.spire.doc.Document;
import com.spire.doc.FileFormat;
import com.spire.doc.PictureWatermark;
import com.spire.doc.TextWatermark;
import com.spire.doc.documents.WatermarkLayout;
public class test {
public static void main(String[] args) {
Document doc = new Document();
doc.loadFromFile("D:\\xx.doc");
TextWatermark textWatermark = new TextWatermark();
//水印内容
textWatermark.setText("水印内容");
textWatermark.setFontName("宋体");
textWatermark.setFontSize(30f);
textWatermark.setColor(Color.RED);
textWatermark.setLayout(WatermarkLayout.Diagonal);
doc.setWatermark(textWatermark);
dco.saveToFile("E:\\xx.doc",FileFormat.Docx_2013);
//图片水印
Document imgdoc = new Document();
imgdoc.loadFromFile("E:\\xx.doc");
PictureWatermark imageWatermark = new PictureWatermark();
imageWatermark.setPicture("E:\\xx.jpg");
imageWatermark.isWashout(false);
imgdoc.setWatermark(imageWatermark);
imgdoc.saveToFile("E:\\xx.doc",FileFormat.Docx_2013);
}
}
37. Java中根据doc模版生成指定内容
依赖包:
<dependency>
<groupId>freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.9</version>
</dependency>
(1) 创建MDoc.java文件
MDoc.java文件中内容:
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.Map;
import freemarker.template.Configuration;
import java.template.Template;
public class MDoc {
private Configuration configuration = null;
public MDoc() {
configuration = new Configuration();
configuration.setDefaultEncoding("utf-8");
}
public createDoc(Map<String,Object> dataMap,String fileName) throws Exception {
//配置信息
configuration.setClassForTemplateLoading(this.getClass(),"/");
Template t = null;
try{
//加载模版
t = configuration.getTemplate("test.ftl")
}
catch(Exception e){
e.printStackTrace();
}
//输出文档名称
File outFile = new File(fileName);
Writer out = null;
FileOutputStream fos = null;
try{
fos = new FileOutputStream(outFile);
OutputStreamWriter oWriter = new OutputStreamWriter(fos,"utf-8");
out = new BufferedWriter(oWriter);
}
catch(Exception e){
e.printStackTrace();
}
try{
t.process(dataMap,out);
out.close();
fos.close();
}
catch(Exception e){
e.printStackTrace();
}
}
}
(2) 创建test.java文件
test.java文件中内容:
import java.util.HashMap;
import java.util.Map;
public class test{
public static void main(String[] args) throws Exception {
Map<String,Object> dataMap = new HashMap<String,Object>();
dataMap.put("name","字符串");
MDoc mdoc = new MDoc();
mdoc.createDoc(dataMap,"E:\\xx.doc");
}
}
(3) 将test.doc模版另存为test.xml文件,然后在test.xml文件中找到需要填充的地方编辑为${name}后保存成功后,改为test.ftl并在启动目录中创建resources文件夹,将test.ftl文件放入
38. Java中Pdf合并
依赖包:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.lowgie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
import com.lowagie.text.Document;
import com.lowagie.text.pdf.PdfCopy;
import com.lowagie.text.pdf.PdfImportedPage;
import com.lowagie.text.pdf.PdfReader;
public class test {
public static void mergePdfFiles(String[] files,String newfile) {
Document document = null;
try{
document = new Document(new PdfReader(files[0]).getPageSize());
PdfCopy copy = new PdfCopy(document,new FileOutputStream(newfile));
document.open();
for(int i=0;i<files.length;i++){
PdfReader reader = new PdfReader(files[i]);
int n = reader.getNumberOfPages();
for(int j=1;j<=n;j++){
document.newPage();
PdfImportedPage page = copy.getImportedPage(reader,j);
copy.addPage(page);
}
}
}
catch(Exception e){
e.printStackTrace();
}
finally{
document.close();
}
}
public static void main(String[] args) throws Exception {
String[] files = {"E:\\xx.pdf","E:\\xx.pdf"};
String savePath = "E:\\xx.pdf";//合并后生成的文件
mergePdfFiles(files,savePath);
}
}
40. Java中Coap服务器搭建
依赖包:
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>californium-core</artifactId>
<version>2.0.0-M15</version>
</dependency>
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>element-connector</artifactId>
<version>2.0.0-M15</version>
</dependency>
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>scandium</artifactId>
<version>2.0.0-M15</version>
</dependency>
(服务器端)
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.server.resources.CoapExchange;
public class test {
public static void main(String[] args) throws Exception {
//默认端口5683
CoapServer server = new CoapServer(8050);
//创建一个资源为hello格式为\hello
server.add(new CoapResource("hello")){
@Override
public void handleGET(CoapExchange exchange) {
//重写处理Get请求方法
exchange.respond(ResponseCode.CONTENT,"字符串");
}
});
server.add(new CoapResource("time")){
@Override
public void handleGET(CoapExchange exchange) {
exchange.respond(ResponseCode.CONTENT,"字符串");
}
});
//post请求
server.add(new CoapResource("test",true)) {
@Override
public void handlePOST(CoapExchange exchange){
String payload = exchange.getRequestText();
byte[] payloadBytes = exchange.getRequestPayload();
System.out.println("收到的数据:" + new String(payloadBytes));
exchange.respond("字符串");
}
@Override
public void handleGET(CoapExchange exchange) {
System.out.println("端口:" + exchange.getSourcePort());
System.out.println("编码:" + exchange.getRequestCode());
exchange.respond(ResponseCode.CONTENT,"字符串");
}
});
server.start();
}
}
(客户端)
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.Utils;
public class test {
public static void main(String[] args) throws Exception {
URI uri = new URI("coap://xxip地址:8050/hello");
CoapClient client = new CoapClient(uri);
StringBuilder sb = new StringBuilder();
sb.append("字符串");
//get请求
// CoapResponse response = client.get();
CoapResponse response = client.post(sb.toString(),MediaTypeRegistry.TEXT_DLAIN);
if(respone != null){
System.out.println(response.getPayload());
System.out.println(response.getOptions());
System.out.println(response.getResponseText());
System.out.println(Utils.prettyPrint(response));
}
}
}
41. Java中操作Nsq
依赖包:
<dependency>
<groupId>com.sproutsocial</groupId>
<artifactId>nsq-j</artifactId>
<version>0.9.4</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
(生产者)
import com.sproutsocial.nsq.Publisher;
public class test {
public static void main(String[] args) {
Publisher publisher = new Publisher("xxIp地址:4150");
byte[] data = ("消息内容").getBytes();
publisher.Publish("topic",data);
}
}
(消费者)
public class test implements MeesageHandler {
@Override
public void accept(Message paramMessage){
//接收内容
System.out.println(new Stirng(paramMessage.getData()));
paramMessage.finish();
}
public static void main(String[] args){
Subscriber subscriber = new Subscriber("xxIp地址");
Test test = new Test();
subscriber.subscribe("topic","flag",test);
}
}
42. Java中生成二维码
1.带图片二维码生成
(1) 依赖包
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
(2) 创建ImageSource.java文件
ImageSource.java文件中内容:
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import com.google.zxing.LuminanceSource;
public class ImageSource extends LuminanceSource{
private final BufferedImage image;
private final int left;
private final int top;
public ImageSource(BufferedImage image) {
this(image, 0, 0, image.getWidth(), image.getHeight());
}
public ImageSource(BufferedImage image, int left, int top, int width, int height) {
super(width, height);
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
if (left + width > sourceWidth || top + height > sourceHeight) {
throw new IllegalArgumentException("复制失败");
}
for (int y = top; y < top + height; y++) {
for (int x = left; x < left + width; x++) {
if ((image.getRGB(x, y) & 0xFF000000) == 0) {
//设置成白色
image.setRGB(x, y, 0xFFFFFFFF);
}
}
}
this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY);
this.image.getGraphics().drawImage(image, 0, 0, null);
this.left = left;
this.top = top;
}
@Override
public byte[] getRow(int y, byte[] row) {
//从底层平台的位图提取一行(only one row)的亮度数据值
if (y < 0 || y >= getHeight()) {
throw new IllegalArgumentException("需要的行超出: " + y);
}
int width = getWidth();
if (row == null || row.length < width) {
row = new byte[width];
}
image.getRaster().getDataElements(left, top + y, width, 1, row);
return row;
}
@Override
public byte[] getMatrix() {
///从底层平台的位图提取亮度数据值
int width = getWidth();
int height = getHeight();
int area = width * height;
byte[] matrix = new byte[area];
image.getRaster().getDataElements(left, top, width, height, matrix);
return matrix;
}
@Override
public boolean isCropSupported() {//是否支持裁剪
return true;
}
/**
* 返回一个新的对象与裁剪的图像数据。实现可以保存对原始数据的引用,而不是复制。
*/
@Override
public LuminanceSource crop(int left, int top, int width, int height) {
return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height);
}
@Override
public boolean isRotateSupported() {//是否支持旋转
return true;
}
@Override
public LuminanceSource rotateCounterClockwise() {//逆时针旋转图像数据的90度,返回一个新的对象。
int sourceWidth = image.getWidth();
int sourceHeight = image.getHeight();
AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth);
BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = rotatedImage.createGraphics();
g.drawImage(image, transform, null);
g.dispose();
int width = getWidth();
return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width);
}
}
(3) 创建QRCodeUtil.java文件
QRCodeUtil.java文件中内容:
import java.awt.BasicStroke;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Shape;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.OutputStream;
import java.util.Hashtable;
import javax.imageio.ImageIO;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.Result;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
public class QRCodeUtil {
private static final String CHARSET = "utf-8";
private static final String FORMAT_NAME = "JPG";
//二维码尺寸
private static final int QRCODE_SIZE = 300;
//LOGO宽度
private static final int WIDTH = 60;
//LOGO高度
private static final int HEIGHT = 60;
/**
* 生成二维码
*/
private static BufferedImage createImage(String content, String imgPath, boolean needCompress) throws Exception {
@SuppressWarnings("rawtypes")
Hashtable<EncodeHintType, Comparable> hints = new Hashtable<EncodeHintType, Comparable>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,
hints);
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
if (imgPath == null || "".equals(imgPath)) {
return image;
}
// 插入图片
QRCodeUtil.insertImage(image, imgPath, needCompress);
return image;
}
/**
* 在生成的二维码中插入图片
*/
private static void insertImage(BufferedImage source, String imgPath, boolean needCompress) throws Exception {
File file1 = new File(imgPath);
if (!file1.exists()) {
System.err.println("" + imgPath + " 该文件不存在!");
return;
}
Image src = ImageIO.read(new File(imgPath));
int width = src.getWidth(null);
int height = src.getHeight(null);
if (needCompress) { // 压缩LOGO
if (width > WIDTH) {
width = WIDTH;
}
if (height > HEIGHT) {
height = HEIGHT;
}
Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = tag.getGraphics();
g.drawImage(image, 0, 0, null); // 绘制缩小后的图
g.dispose();
src = image;
}
// 插入LOGO
Graphics2D graph = source.createGraphics();
int x = (QRCODE_SIZE - width) / 2;
int y = (QRCODE_SIZE - height) / 2;
graph.drawImage(src, x, y, width, height, null);
Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);
graph.setStroke(new BasicStroke(3f));
graph.draw(shape);
graph.dispose();
}
/**
* 生成带logo二维码,并保存到磁盘
*/
public static void encode(String content, String imgPath, String destPath, boolean needCompress,String file) throws Exception {
BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
mkdirs(destPath);
ImageIO.write(image, FORMAT_NAME, new File(destPath + "/" + file));
}
public static void mkdirs(String destPath) {
File file = new File(destPath);
// 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir。(mkdir如果父目录不存在则会抛出异常)
if (!file.exists() && !file.isDirectory()) {
file.mkdirs();
}
}
public static void encode(String content, String imgPath, String destPath,String file) throws Exception {
QRCodeUtil.encode(content, imgPath, destPath, false ,file);
}
public static void encode(String content, String destPath, boolean needCompress,String file) throws Exception {
QRCodeUtil.encode(content, null, destPath, needCompress,file);
}
public static void encode(String content, String destPath,String file) throws Exception {
QRCodeUtil.encode(content, null, destPath, false,file);
}
public static void encode(String content, String imgPath, OutputStream output, boolean needCompress)
throws Exception {
BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
ImageIO.write(image, FORMAT_NAME, output);
}
public static void encode(String content, OutputStream output) throws Exception {
QRCodeUtil.encode(content, null, output, false);
}
/**
* 从二维码中,解析数据
*/
public static String decode(File file) throws Exception {
BufferedImage image;
image = ImageIO.read(file);
if (image == null) {
return null;
}
BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Result result;
Hashtable<DecodeHintType, String> hints = new Hashtable<DecodeHintType, String>();
hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
result = new MultiFormatReader().decode(bitmap, hints);
String resultStr = result.getText();
return resultStr;
}
public static String decode(String path) throws Exception {
return QRCodeUtil.decode(new File(path));
}
}
(4) 创建test.java文件
test.java文件中内容:
public class test {
public static void main(String[] args) throws Exception {
String textt = "www.baidu.com";//二维码的内容
String logo = "E:\\storage\\logo.jpg";//logo的路径
//生成二维码
QRCodeUtil.encode(textt,logo,"E:\\",true,"test.jpg");
}
}
2.生成普通二维码
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
public class test {
private static void generateQRCodeImage(String text, int width, int height, String filePath) throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height);
Path path = FileSystems.getDefault().getPath(filePath);
MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
}
public static void main(String[] args) throws Exception {
//生成二维码
generateQRCodeImage("This is my first QR Code", 350, 350, "E:\\test.png");
}
}
43. Java中生成条形码
方法一:
(1) 依赖包
<dependency>
<groupId>net.sf.barcode4j</groupId>
<artifactId>barcode4j-light</artifactId>
<version>2.0</version>
</dependency>
(2) 创建BarcodeUtil.java文件
BarcodeUtil.java文件中内容:
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.commons.lang.StringUtils;
import org.krysalis.barcode4j.impl.code39.Code39Bean;
import org.krysails.barcode4j.output.bitmap.BitmapCanvasProvider;
import org.krysails.barcode4j.tools.UnitConv;
public class BarcodeUtil {
public static File generateFile(String msg,String path){
File file = new File(path);
try{
generate(msg,new FileOutputStream(file));
}
catch(FileNotFoundException e){
throw new RuntimeException(e);
}
return file;
}
//生成字节
public static byte[] generate(String msg){
ByteArrayOutputStream ous = new ByteArrayOutputStream();
generate(msg,ous);
return ous.toByteArray();
}
public static void generate(String msg,OutputStream ous){
if(StringUtils.isEmpty(msg) || ous == null){
return;
}
Code39Bean bean = new Code39Bean();
//精细度
final int dpi = 150;
//module宽度
final double moduleWidth = UnitConv.in2mm(1.0f/dpi);
//配置对象
bean.setModuleWidth(moduleWidth);
bean.setWideFactor(3);
bean.doQuietZone(false);
String format = "image/png";
try{
//输出到流
BitmapCanvasProvider canvas = new BitmapCanvasProvider(ous,format,dpi,BufferedImage.TYPE_BYTE_BINARY,false,0);
//生成条形码
bean.generateBarCode(canvas,msg);
//结束绘制
canvas.finish();
}
catch(IOException e){
throw new RuntimeException(e);
}
}
public static void main(String[] args){
generateFile("xx","E:\\xx.png");
}
}
方法二:
(1) 依赖包
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.Result;
import com.google.zxing.WriterException;
import com.google.zxing.client.jzse.BufferedImageLuminanceSource;
import com.google.zxing.client.jzse.MatrixToImageWriter;
import com.google.zxing.common.bitMartix;
import com.google.zxing.common.HybridBinarizer;
public class test {
//生成二维码、条形码
public static void generateCode(File file,String code,int width,int height) {
//定义位图
BitMatrix matrix = null;
try{
//使用code_128格式进行编码
MutliFormatWriter writer = new MutliFormatWriter();
matrix = writer.encode(code,BarcodeFormat.CODE_128,width,height,null);
}
catch(WriterException e){
e.printStackTrace();
}
//将位图保存成图片
try(FileOutputStream outStream = new FileOutputStream(file)){
ImageIO.write(MatrixToImageWriter.toBufferedImage(matrix),"png",outStream);
outStream.flush();
}
catch(Exception e){
e.printStackTrace();
}
}
public static void readCode(File file){
try{
BufferedImage image = ImageIO.read(file);
if(image == null){
return;
}
LuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Map<DecodeHintType,Object> hints = new HashMap<>();
hints.put(DecodeHintType.CHARACTER_SET,"GBK");
hints.put(DecodeHintType.PURE_BARCODE,Boolean.TRUE);
hints.put(DecodeHintType.TRY_HARDER,Boolean.TRUE);
Result result = new MultiFormatReader().decode(bitmap,hints);
System.out.println(result.getText());
}
catch(Exception e){
e.printStaceTrace();
}
}
public static void main(String[] args) {
//生成条形码
generateCode(new File("D:\\xx.png"),"123",500,250);
//识别二维码
readCode(new File("xx.png"));
}
}
44. Java中识别二维码
(1) 依赖包
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
import com.google.zxing.*;
import com.google.zxing.client.jzse.BufferedImageLuminanceSource;
import com.google.zxing.client.jzse.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybirdBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.file.path;
import java.util.HashMap;
import java.util.Map;
public class test {
public static void readQrCode(File file){
MultiFormatReader reader = new MultiFormatReader();
try{
BufferedImage image = ImageIO.read(file);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(image)));
Map<DecodeHintType,Object> hints = new HashMap<>();
//设置编码
hints.put(DecodeHintType.CHARACTER_SET,"utf-8");
Result result = reader.decode(binaryBitmap,hints);
System.out.println(result.toString());
System.out.println(result.getText());
}
catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args){
readQrCode(new File("xx.png"));
}
}
45. Java中netty搭建TCP服务器
1. (服务器端)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建Socket.java文件
Socket.java文件中内容:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class Socket {
public void bind(int port) throws Exception {
/*服务线程组用于网络事件处理一个用户服务器断接收另一个socketChannel处理*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
/*Nio服务器端辅助启动类*/
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workGroup)
/*类似Nio中serverSocketChannel*/
.channel(NioServerSocketChannel.class)
//配置Tcp参数
.option(ChannelOption.SO_BACKLOG,1024)
/*配置延时*/
.childOption(ChannelOption.SO_KEEPALIVE,true)
/*绑定IO处理类*/
.childHandler(new ChildChannelHandler());
/*服务启动后,绑定监听端口,同步等待成功,主要用于异步处理回调*/
ChannelFuture f= serverBootstrap.bind(port).sync();
/*等待端口关闭*/
f.channel().closeFuture().sync();
}
finally{
/*退出线程资源*/
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new Socket().binf(8010);
}
}
(3) 创建ChildChannelHandler.java文件
ChildChannelHandler.java文件中内容:
import io.netty.channel.ChannelFuture;
import io.nety.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel ch){
//下发消息
ch.pipeline().addLast(new ServerHandler());
ChannelFuture f = ch.closeFuture();
f.addListeners(new ChannelFutureListener(){
public void operationComplete(ChannelFuture cf){
/*Socket关闭操作*/
Channel ch = cf.channel();
String remoteAddress = ch.remoteAddress().toString();
System.out.println(remoteAddress);
}
});
}
}
(4) 创建ServerHandler.java文件
ServerHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.ChannelHandlerContext;
public class ServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
protected void channelRead0(ChannelHandlerContext ctx ,ByteBuf msg) throws Exception {
/*
ctx.write("内容");
ctx.flush();
*/
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
/*获取远程客户端地址*/
ctx.channel().writeAndFlush("内容");
String remoteAddress = ctx.channel().remoteAddress();
System.out.println(remoteAddress);
//转十六进制
String str = ByteBufUtil.hexDump(buf).toLowerCase();
System.out.println(str);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
ctx = null;
}
}
注:
要使用发送自定义字符串使用以下方式
String str = "字符串"; ByteBuf resp = Unpooled.copiedBuffer(str.getBytes()); ctx.writeAndFlush(resp);
2. (客户端)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建Client.java文件
Client.java文件中内容:
import io.netty.bootstrap.Bootstrap;
import io.netty.channelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.Socket.nio.NioSocketChannel;
public class Client {
public void connect(int port,String host) throws Exception {
/*配置线程组*/
EventLoopGroup group = new EventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new childChannelHandler());
ChannelFuture f = b.connect(host,port).sync();
channel ch = f.channel().closeFuture().sync().Channel();
ch.writeAndFlush(Unpooled.copiedBuffer("xx",CharsetUtil.UTF_8));
}
finally{
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new Client().connect(8023,"xxIP地址");
}
}
(3) 创建childChannelHandler.java文件
childChannelHandler.java文件中内容:
import java.util.concurrent.TimeOnit;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.socket.SocketChannel;
import io.netty.handler,timeout.IdleStateHandler;
public class childChannelHandler extends ChannelInitializer<SocketChannel>{
protected void initChannel(SocketChannel ch){
/*添加心跳检测*/
ch.pipeline().add(new IdleStateHandler(15,0,0,TimeUnit.SECONDS))
/*心跳处理*/
ch.pipeline().addLast(new HeartBeatServerHandler());
/*连接下发消息*/
ch.pipeline().addLast(new ClientHandler());
ChanelFuture f = ch.closeFuture();
f.addListeners(new ChannelFutureListener(){
public void operationComplete(ChannelFuture cf){
/*Socket关闭操作*/
Channel ch = cf.channel();
String remoteAddress = ch.remoteAddress().toString();
System.out.println(remoteAddress);
}
});
}
}
(4) 创建serverHandler.java文件
serverHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ClientHandler extends SimpleChannelInboundHandler<ByteBuf>{
protected void channelRead0(ChannelHandlerContext ctx ,ByteBuf msg) throws Exception {
/*
ctx.write("内容");
ctx.flush();
*/
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,'UTF-8');
/*获取远程客户端地址*/
ctx.channel().writeAndFlush("内容");
String remoteAddress = ctx.channel().remoteAddress();
System.out.println(remoteAddress);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
ctx.allocc();
}
}
(5) 创建HeartBeatServerHandler.java文件
HeartBeatServerHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {
public void useEventTriggered(ChannelHandlerContext ctx,Object evt) throws Exception{
if(evt instanceof IdelStateEvent){
ctx.channel().close();
}
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
ctx.alloc();
}
}
**注:**心跳检测可以加入netty中TCP和UDP机制中
46. Java中netty搭建UDP服务器
1. (服务器端)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建Server.java文件
Server.java文件中内容:
import io.netty.bootstrap.Bootstrap;
import java.net.InetSocketAddress;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
public class Server {
public void bind(int port) throws Exception {
EventLoopGroup workGroup = new NioEventLoopGroup();
try{
Bootstrap serverbootstrap = new Bootstrap();
serverbootstrap.group(workgroup)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST,true)
//设置udp读取缓冲区
.option(ChannelOption.SO_REVBUF,1024 * 1024)
//设置udp缓冲区
.option(ChannelOption.SO_SNDBUF,1024 *1024)
.handler(new UdpChannelHandler());
/*绑定监听端口*/
ChannelFuture f = serverbootstrap.bind(port).sync();
f.channel().closeFuture().sync().await();
}
finally{
workgroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new Server().bind(8320);
}
}
(3) 创建UdpChannelHandler.java文件
UdpChannelHandler.java文件中内容:
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.nio.NioDatagramChannel;
public class UdpChannelHandler extends ChannelInitializer<NioDatagramChannel> {
protected void initChannel(NioDatagramChannel ch) throws Exception {
ch.pipeline().addLast(new ServerHandler());
}
}
(4) 创建ServerHandler.java文件
ServerHandler.java文件中内容:
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
public class ServerHandler extends SimpleChannelInboundHandler<DatagramPacket>{
protected void channelRead0(ChannelHandlerContext ctx,DatagramPacket msg) throws Exception {
//文件内容
ByteBuf buf = dp.copy().content();
//数据起始位
int _protocol = buf.getByte(0);
//释放对象
ReferenceCountUtil.release(buf);
ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("字符串"),CharsetUtil.UTF_8),msg.sender());
}
}
2. (客户端)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建UdpClient.java文件
UdpClient.java文件中内容:
import java.net.InetSocketAddress;
import io.netty.bootstrap.Bootstrap;
import io.netty.util.CharsetUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
public class UdpClient {
public void bind() throws Exception {
EventLoopGroup wrokGroup = new NioEventLoopGroup();
try{
Bootstrap serverbootstrap = new Bootstrap();
serverbootstrap.group(workGroup)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST,true)
.handler(new UdpChannelHandler());
Channel ch = serverbootstrap.bind(0).sysnc().channel();
ch.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("字符串",CharsetUtil.UTF_8)),new InetSocketAddress("xxIP地址",8320)).sync();
ch.closeFuture().sync().await();
}
finally{
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new UdpClient().bind();
}
}
(2) 创建UdpChannelHandler.java文件
UdpChannelHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import io.netty.util.CharsetUtil;
public class UdpChannelHandler extends SimpleChannelInboundHandler<DatagramPacket> {
protected void channelRead0(ChannelHandlerContext ctx,DatagramPacket msg){
ctx.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer("字符串"),CharsetUtil.UTF_8),msg.sender());
}
}
47. Java中netty搭建文本服务器(普通实现)
1. (服务器端)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建Socket.java文件
Socket.java文件中内容:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class Socket {
public void bind(int port) throws Exception {
/*服务线程组用于网络事件处理一个用户服务器断接收另一个socketChannel处理*/
//线程数
int boss_os_num = 8;
int event_os_num = 32;
EventLoopGroup bossGroup = new NioEventLoopGroup(boss_os_num);
EventLoopGroup workGroup = new NioEventLoopGroup(event_os_num);
try {
/*Nio服务器端辅助启动类*/
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workGroup)
/*类似Nio中serverSocketChannel*/
.channel(NioServerSocketChannel.class)
//配置Tcp参数
.option(ChannelOption.SO_BACKLOG,1024 * 1024)
/*配置延时*/
.childOption(ChannelOption.SO_KEEPALIVE,true)
/*绑定IO处理类*/
.childHandler(new ChildChannelHandler());
/*服务启动后,绑定监听端口,同步等待成功,主要用于异步处理回调*/
ChannelFuture f= serverBootstrap.bind(port).sync();
/*等待端口关闭*/
f.channel().closeFuture().sync();
}
finally{
/*退出线程资源*/
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new Socket().binf(8010);
}
}
(3) 创建ChildChannelHandler.java文件
ChildChannelHandler.java文件中内容:
import io.netty.channel.ChannelFuture;
import io.nety.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.ChannelFuture;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel sch){
sch.pipeline().addLast(new ObjectEncoder())
.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(this.getClass().getClassLoader())))
.addLast(new ServerHandler());
ChannelFuture scf = sch.closeFuture();
scf.addListener(new ChannelFutureListener(){
public void operationComplete(ChannelFuture cf) throws Exception {
Channel ch = cf.channel();
String remote_addr = ch.remoteAddress().toString();
System.out.println(remote_addr);
}
});
}
}
(4) 创建ServerHandler.java文件
ServerHandler.java文件中内容:
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.ChannelHandlerContext;
public class ServerHandler extends SimpleChannelInboundHandler<Object> {
protected void channelRead0(ChannelHandlerContext ctx ,Object msg) throws Exception {
String str = (String) msg;
System.out.println(str);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
ctx = null;
}
}
2. (客户端)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建Client.java文件
Client.java文件中内容:
import io.netty.bootstrap.Bootstrap;
import io.netty.channelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.Socket.nio.NioSocketChannel;
public class Client {
public void connect(int port,String host) throws Exception {
/*配置线程组*/
EventLoopGroup group = new EventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_KEEPALIVE,true)
.option(ChannelOption.TCP_NODELY,true)
.handler(new childChannelHandler());
ChannelFuture f = b.connect(host,port).sync();
channel ch = f.channel().closeFuture().sync().Channel();
ch.writeAndFlush("字符串");
}
finally{
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new Client().connect(8023,"xxIP地址");
}
}
(3) 创建childChannelHandler.java文件
childChannelHandler.java文件中内容:
import java.util.concurrent.TimeOnit;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.socket.SocketChannel;
import io.netty.handler.imeout.IdleStateHandler;
public class childChannelHandler extends ChannelInitializer<SocketChannel>{
protected void initChannel(SocketChannel ch){
/*添加心跳检测*/
ch.pipeline().add(new IdleStateHandler(15,0,0,TimeUnit.SECONDS))
/*连接下发消息*/
ch.pipeline().addLast(new ObjectEncoder())
.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(this.getClass().getClassLoader())))
.addLast(new clientHandler());
ChanelFuture f = ch.closeFuture();
f.addListeners(new ChannelFutureListener(){
public void operationComplete(ChannelFuture cf){
/*Socket关闭操作*/
Channel ch = cf.channel();
if(ch.remoteAddress != null){
String remoteAddress = ch.remoteAddress().toString();
System.out.println(remoteAddress);
}
}
});
}
}
(4) 创建clientHandler.java文件
clientHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.EventLoop;
import io.netty.channel.handler.timeout.IdleStateEvent;
public class ClientHandler extends SimpleChannelInboundHandler<Object>{
protected void channelRead0(ChannelHandlerContext ctx ,Object msg) throws Exception {
String str = (String) msg;
/*获取远程客户端地址*/
ctx.writeAndFlush(str);
String remoteAddress = ctx.channel().remoteAddress();
System.out.println(remoteAddress);
}
//掉线重连
public void channelInactive(ChannelHandlerContext ctx) throw Exception {
final EventLoop eventLoop = ctx.channel.eventLoop();
eventLoop.schedule(new Runnable(){
public void run(){
eventLoop.shutdownGracefully();
try{
//重连操作
}
catch(Exception e){
}
}
},1L,TimeUnit.SECONDS);
super.channelInactive(ctx);
}
//心跳检查
public void userEventTriggered(ChannelHandlerContext ctx,Object evt) throws Exception {
if(evt instanceof IdleStateEvent){
//发送心跳数据
}
else{
super.userEventTriggered(ctx,evt);
}
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
ctx = null;
}
}
**注:**心跳检测可以加入netty中TCP和UDP机制中
48. Java中netty搭建文本服务器(连接池)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建Socket.java文件
Socket.java文件中内容:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class Socket {
public void bind(int port) throws Exception {
/*服务线程组用于网络事件处理一个用户服务器断接收另一个socketChannel处理*/
//线程数
int boss_os_num = 8;
int event_os_num = 32;
EventLoopGroup bossGroup = new NioEventLoopGroup(boss_os_num);
EventLoopGroup workGroup = new NioEventLoopGroup(event_os_num);
try {
/*Nio服务器端辅助启动类*/
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workGroup)
/*类似Nio中serverSocketChannel*/
.channel(NioServerSocketChannel.class)
//配置Tcp参数
.option(ChannelOption.SO_BACKLOG,1024 * 1024)
/*配置延时*/
.childOption(ChannelOption.SO_KEEPALIVE,true)
/*绑定IO处理类*/
.childHandler(new ChildChannelHandler());
/*服务启动后,绑定监听端口,同步等待成功,主要用于异步处理回调*/
ChannelFuture f= serverBootstrap.bind(port).sync();
/*等待端口关闭*/
f.channel().closeFuture().sync();
}
finally{
/*退出线程资源*/
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new Socket().binf(8010);
}
}
(3) 创建ChildChannelHandler.java文件
ChildChannelHandler.java文件中内容:
import io.netty.channel.ChannelFuture;
import io.nety.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.ChannelFuture;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel sch){
sch.pipeline().addLast(new ObjectEncoder())
.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(this.getClass().getClassLoader())))
.addLast(new ServerHandler());
ChannelFuture scf = sch.closeFuture();
scf.addListener(new ChannelFutureListener(){
public void operationComplete(ChannelFuture cf) throws Exception {
Channel ch = cf.channel();
String remote_addr = ch.remoteAddress().toString();
System.out.println(remote_addr);
}
});
}
}
(4) 创建ServerHandler.java文件
ServerHandler.java文件中内容:
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.ChannelHandlerContext;
public class ServerHandler extends SimpleChannelInboundHandler<Object> {
protected void channelRead0(ChannelHandlerContext ctx ,Object msg) throws Exception {
String str = (String) msg;
System.out.println(str);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
ctx = null;
}
}
2. (客户端)
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建Client.java文件
Client.java文件中内容:
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.pool.AbstractChannelPoolMap;
import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.channel.pool.FixedChannelPool;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import java.net.InetAddress;
import io.netty.bootstrap.Bootstrap;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
public class Client {
//连接池对象
public static ChannelPoolMap<String, FixedChannelPool> tra_pool = null;
public void connect(int port,String host) throws Exception {
/*配置线程组*/
EventLoopGroup group = new EventLoopGroup();
try{
//Tcp管理线程组
NioEventLoopGroup eventLoop = new NioEventLoopGroup();
//配置netty中ServerBootstrap对象
Bootstrap clientBoot = new Bootstrap();
//配置Tcp参数
clientBoot.group(eventLoop)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE , true)
.remoteAddress(host,port);
//检测对象
if(Tracker.ota_tra_pool == null) {
//初始化连接池
tra_pool = new AbstractChannelPoolMap<String, FixedChannelPool>(){
@Override
protected FixedChannelPool newPool(String key) {
//连接池操作类
ChannelPoolHandler handler = new ChannelPoolHandler() {
@Override
public void channelReleased(Channel ch) throws Exception {
}
@Override
public void channelAcquired(Channel ch) throws Exception {
}
@Override
public void channelCreated(Channel ch) throws Exception {
ch.pipeline()
.addLast(new ObjectEncoder())
.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(this.getClass().getClassLoader())))
.addLast(new clientHandler());
}
};
//单个连接池大小
return new FixedChannelPool(clientBoot, handler, 50);
}
};
}
//释放对象
_host = null;
_port = null;
}
catch(Exception e) {
}
}
public static void main(String[] args) throws Exception {
new Client().connect(8023,"xxIP地址");
//获取tcp线程池
FixedChannelPool ota_pool = Tracker.ota_tra_pool.get("xxIP地址");
//获取连接池客户端
Future<Channel> future = ota_pool.acquire();
//添加监听事件
future.addListener(new FutureListener<Channel>() {
@Override
public void operationComplete(Future<Channel> future) throws Exception {
//给服务端发送数据
Channel channel = future.getNow();
//转发数据
channel.writeAndFlush("字符串");
//回收线程
ota_pool.release(channel);
}
});
}
}
(4) 创建clientHandler.java文件
clientHandler.java文件中内容:
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.EventLoop;
import io.netty.channel.handler.timeout.IdleStateEvent;
public class ClientHandler extends SimpleChannelInboundHandler<Object>{
protected void channelRead0(ChannelHandlerContext ctx ,Object msg) throws Exception {
String str = (String) msg;
/*获取远程客户端地址*/
ctx.writeAndFlush(str);
String remoteAddress = ctx.channel().remoteAddress();
System.out.println(remoteAddress);
}
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception{
ctx.close();
ctx = null;
}
}
**注:**心跳检测可以加入netty中TCP和UDP机制中
49. Java中Mqtt使用
依赖包
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>
1.生产者
(1) 创建Producer.java文件
Producer.java文件中内容:
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence;
import org.eclipse.paho.client.mqttv3.persist.MqttMemoryPersistence;
public class Producer {
public static void main(String[] args){
//订阅id
String topic = "topic";
String content = "消息内容";
int qos = 1;
//多个地址用逗号隔开tcp://xx:1883,xx:1884,xx:1885
String broker = "tcp://xIP地址:1937";
String username = "xx用户名";
String password = "xx密码";
String clientId = "id标识";
try{
//内存存储
MemoryPersistence persitence = new MemoryPersistence();
//或
//MqttDefaultFilePersistence persitence = new MqttDefaultFilePersistence();
MqttClient sampleClient = new MqttClient(broker,clientId,persitence);
//创建连接参数
MqttConnectOptions connOps = new MqttConnectOptions();
//重新启动重连时记住状态
connOps.setCleanSession(false);
//设置用户名
connOps.setUserName(username);
//设置密码
connOps.setPassword(password.toCharArray());
//建立连接
sampleClient.connect(connOps);
//创建消息
MqttMessage message = new MqttMessage(content.getBytes());
//设置消息服务质量
message.setQos(qos);
//发布消息
sampleClient.publish(topic,message);
//断开连接
sampleClient.disconnect();
//关闭客户端
sampleClient.close();
}
catch(Exception e){
}
}
}
2.订阅端口
(1) 创建mqttHandler.java文件
mqttHandler.java文件中内容:
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
public class mqttHandler {
public void connectionLost(Throwable cause) {
//重连操作
}
public void messageArrived(String topic, MqttMessage mqtt_msg) {
//接收mqtt订阅消息
String message = new String(mqtt_msg.getPayload());
System.out.println(topic);
System.out.println(message.getQos());
System.out.println(new String(message.getPayload()));
}
public void deliveryComplete(IMqttDeliveryToken token) {
}
}
(2) 创建Consumer.java文件
Consumer.java文件中内容:
import eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.persist.MqttMemoryPersistence;
public class Consumer {
String host = "tcp:xxIP地址//:1937";
String topic = "topic值";
String qos = 1;
String username = "xx用户名";
String password = "xx密码";
String clientId = "id标识";
try{
//内存存储
MemoryPersistence mqttMemory = new MemoryPersistence();
//默认为以内存保存
MqttClient mqttClient = new MqttClient(host,cliendId,mqttMemory);
//MQTT的连接设置
MqttConnectOptions options = new MqttConnectOptions();
//这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(false);
//设置连接的用户名
options.setUserName(username);
//设置连接的密码
options.setPassword(password.toCharArray());
//设置超时时间 单位为秒
options.setConnectionTimeout(10000);
//设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
//设置回调函数
mqttClient.setCallback(new mqttHandler());
//建立mqtt连接
mqttClient.connect(options);
//订阅消息
mqttClient.subscribe(topic, qos);
}
catch(Exception e){
}
}
注:
MqttClient:同步调用客户端,使用阻塞的方式与mqtt服务器沟通
MqttAsyncClient:异步调用客户端,使用非阻塞方式与mqtt服务器通信
50. Java中netty搭建websocket服务器端(wss协议)
(1) jdk中使用keytool工具生成签名
keytool -gentkey -keysize 2048 -validity 365 -keypass netty123 -storepass netty123 -keystore wss.jks
(2) netty搭建Websocket服务器端handler类
public static SSLContext createSSLContext(String type,String path,String password) throws Exception {
keyStore ks = keyStore.getInstance("JKS");
InputStream ksInpustStream = new FileInputStream(path);
ks.load(ksInputStream,password.toCharArray());
keyManagerFactory kmf = keyManagerFactory.getInstance(keyManagerFactory.getDefaultAlgorithm());
kmf.init(ks,password.toCharArray());
//安全套接字协议
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManager(),null,null);
return sslContext;
}
public void initChannel(SocketChannel ch) throws Exception {
SSLContext sslContext = createSSLContext("JKS","wss.jks","netty123");
SSLEngine engine = sslContext.createSSLEngine();
engine.setUseClientMode(false);
ch.pipline().addLast(new SslHeader(engine));
ch.pipline().addLast(new IdleStateHandler(5,0,0,TineUint.SECONDS));
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new HttpObjectAggregator(65536));
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new AcceptorIdleStateTrigger());
ch.pipeline().addLast(new websocketHandler());
}
51. Java中Netty客户端连接池
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(2) 创建MyNettyPool.java文件
MyNettyPool.java文件中内容:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.pool.AbstractChannelPoolMap;
import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.channel.pool.ChannelPool;
import io.netty.channel.socket.nio.FixedChannelPool;
import io.netty.channel.pool.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
public class MyNettyPool {
public static ChannelPoolMap<String,FixedChannelPool> poolMap = null;
private static final Bootstrap = new Bootstrap();
static {
bootstrap.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.remoteAddress("localhost",8080);
}
public MyNettyPool(){
init();
}
public void init(){
poolMap = new AbstractChannelPoolMap<String,FixedChannelPool>(){
@Override
protected FixedChannelPool newPool(String key) {
ChannelPoolHandler handler = new ChannelPoolHandler(){
@Override
public void channelReleased(Channel ch) throws Exception {
//刷新数据
}
//添加handler
@Override
public void channelCreated(Channel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder())
.addLast(new StringDecoder())
.addLast(new NettyClientHandler());
}
@Override
public void channelAcquiredd(Channel ch) throws Exception {
//获取连接池channel
}
};
//设置连接池大小
return new FixedChannelPool(bootstrap,handler,50)
}
};
}
//发送消息
public void send(String msg){
//从连接池获取连接,远程连接域名作为key值
FixedChannelPool pool = poolMap.get("localhost");
//没有申请或网络断开返回null
Future<Channel> future = pool.acquire();
future.addListener(new FutureListener<Channel>(){
@Override
public void operationComplete(Future<Channel> future) throws Exception {
//发送数据
Channel channel = future.getNow();
channel.writeAndFlush(msg);
//回收线程池
pool.release(channel);
}
});
}
}
(3) 创建NettyClientHandler.java文件
NettyClientHandler.java文件中内容:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx,ByteBuf buf) throws Exception {
String hext_str = ByteBufUtil.hexDump(buf);
//接收服务器端消息
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//完成消息接收操作
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
ctx.close();
ctx = null;
}
}
注:
(1) 连接池大小需设定在合理范围,否则会导致服务器
cpu损耗增加(2)
SimpleChannelPool实现ChannelPool接口,简单的连接池实现(3)
FixedChannelPool继承SimpleChannelPool,有大小限制的连接池实现(4)
ChannelPoolMap管理host与连接池映射的接口(5)
AbstractChannelPoolMap抽象类,实现ChannelPoolMap接口
52. Java中Netty客户端连接
(1) 依赖包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
(1) 创建TcpClient.java文件
TcpClient.java文件中内容:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class TcpClient {
public void clientStart(){
try{
String host = "127.0.0.1";
int port = 8006;
//tcp管理线程
NioEventLoopGroup eventLoop = new NioEventLoopGroup();
//配置netty中serverBoot对象
Bootstrap clientBoot = new Bootstrap();
//配置tcp参数
clientBoot.group(eventLoop)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.option(ChannelOption.SO_KEEPALIVE,true)
.handler(new ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
ChannelFuture scf = ch.closeFuture();
p.addLast(new TcpHandler());
//添加关闭事件
scf.addListener(new ChannelFutureListener(){
@Override
public void operationComplete(ChannelFuture cf) throws Exception {
Channel ch = cf.channel();
//断开连接操作区域
}
});
}
});
//连接服务器端
ChannelFuture cf = clientBoot.connect(host,port).sync();
/*for(init i=0;i<2;i++){
//建立连接
ChannelFuture sync = clientBoot.connect(host,port);
//异步阻塞
/*sync.channel().closeFuture().addListener((r)->{
System.out.println("shutdown");
eventLoop.shutdownGracefully();
});*/
}*/
}
catch(Exception e){
e.printStackTrace();
}
}
}
(2) 创建TcpHandler.java文件
TcpHandler.java文件中内容:
import io.netty.buffer.Bytebuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TcpHandler extends SimpleChannelInboundHandler<Bytebuf> {
//字符串缓冲区
private StringBuilder buf_hex = null;
@Override
protected void channelRead0(ChannelHandler ctx,Bytebuf buf) throws Exception {
String hex_str = ByteBufUtil.hexDump(buf).toLowerCase();
if(buf_hex == null) {
buf_hex = new StringBuilder();
}
buf_hex.append(hex_str);
buf.clear();
buf = null;
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
if(buf_hex != null){
String hex_str = buf_hex.toString();
System.out.println(hex_str);
}
buf_hex.delete(0,buf_hex.length());
buf_hex = null;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
ctx.close();
ctx = null;
}
}
注:
(1) 依赖出发时建立连接
ChannelFuture cf = clientBoot.connect("127.0.0.1",8008).sync();
53. Java中图片合成
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class test {
//调整图片大小
public final static void changeSize(String srcImageFile, String descImageFile, int width, int height, boolean isPadding) {
try {
// 缩放比例
double ratio = 0.0;
File file = new File(srcImageFile);
BufferedImage bufferedImage = ImageIO.read(file);
Image image = bufferedImage.getScaledInstance(width, height, bufferedImage.SCALE_SMOOTH);
// 计算缩放比例
if (bufferedImage.getHeight() > bufferedImage.getWidth()) {
ratio = (new Integer(height)).doubleValue() / bufferedImage.getHeight();
} else {
ratio = (new Integer(width)).doubleValue() / bufferedImage.getWidth();
}
AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(ratio, ratio), null);
image = op.filter(bufferedImage, null);
// 是否需要补白
if (isPadding) {
BufferedImage tempBufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2d = tempBufferedImage.createGraphics();
graphics2d.setColor(Color.white);
graphics2d.fillRect(0, 0, width, height);
if (width == image.getWidth(null)) {
graphics2d.drawImage(image, 0, (height - image.getHeight(null)) / 2, image.getWidth(null), image.getHeight(null), Color.white, null);
} else {
graphics2d.drawImage(image, (width - image.getWidth(null)) / 2, 0, image.getWidth(null), image.getHeight(null), Color.white, null);
}
graphics2d.dispose();
image = tempBufferedImage;
}
ImageIO.write((BufferedImage) image, "png", new File(descImageFile));
} catch (IOException e) {
e.printStackTrace();
}
}
//图片缩放
public static void mergeImage(String backImage, String srcImage, String descImage) {
try {
int offset = 10;
BufferedImage backBufferedImage = ImageIO.read(new File(backImage));
BufferedImage srcBufferedImage = ImageIO.read(new File(srcImage));
// 输出图片宽度
int width = backBufferedImage.getWidth()/2; //+ offset;
// 输出图片高度
int height = backBufferedImage.getWidth()/2; //+ offset;
BufferedImage descBufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2d = (Graphics2D) descBufferedImage.getGraphics();
graphics2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 往画布上添加图片,并设置边距
graphics2d.drawImage(backBufferedImage, null, 0, 0);
graphics2d.drawImage(srcBufferedImage, 0,0, width, height, null, null);
graphics2d.dispose();
// 输出新图片
ImageIO.write(descBufferedImage, "png", new File(descImage));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
mergeImage("xx.png","xx.png","D:\\test.png");
}
}
54. Srs(Simple Rtmp Server)中配置Flv视频录制
(1) 下载srs源代码并解压
源码下载地址:https://github.com/ossrs/srs/releases/
二进制文件下载地址:http://www.ossrs.net/srs.release/releases/download.html
(2) 进入srs解压目录/xx/trunk执行
./configure --prefix=/xx/srs安装路径 --full
(3) 编译
make
(4) 安装
make install
(5) 修改srs安装目录/xx/conf/srs.conf文件
srs.conf文件中内容:
listen 1935;
max_connections 1000;
srs_log_tank file;
srs_log_file ./objs/srs.log;
daemon on;
http_api {
enabled on;
listen 1985;
}
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
}
stats {
network 0;
disk sda sdb xvda xvdb;
}
vhost __defaultVhost__ {
dvr {
enabled on;
dvr_path ./objs/nginx/html/[app]/[stream].flv;
# 录制视频的路径以及文件名称生成的格式
dvr_plan segment;
dvr_duration 30;
dvr_wait_keyframe on;
}
http_hooks {
enabled on;
#配置一个rest服务,进行保存文件信息的收集,post方式
on_dvr http://192.168.0.4:8310/index;
}
}
或
listen 1935;
max_connections 1000;
srs_log_tank file;
srs_log_file ./objs/srs.log;
daemon on;
http_api {
enabled on;
listen 1985;
}
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
}
stats {
network 0;
disk sda sdb xvda xvdb;
}
vhost __defaultVhost__ {
dvr {
enabled on;
#all表示录制所有视频流,也可以按频道录制
dvr_apply all;
dvr_path ./objs/nginx/html/[app]/[stream].[timestamp].flv;
# 录制视频的路径以及文件名称生成的格式
dvr_plan segment;
dvr_duration 30;
dvr_wait_keyframe on;
#时间戳抖动算法。full使用完全的时间戳矫正;
#zero只是保证从0开始;off不矫正时间戳。
time_jitter full;
}
http_hooks {
enabled on;
on_dvr http://192.168.0.4:8310/index;
}
}
(6) 进入srs安装目录/etc/init.d文件夹中并启动srs服务
./srs start
(7) 创建Springboot项目,并配置依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(8) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value="/index",method = RequestMethod.POST)
public String Index(@RequestBody String dvrMsg) {
try {
System.out.println(dvrMsg);
return "成功";
}
catch(Exception e) {
return "失败";
}
}
}
(9) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
on_dvr回调函数返回参数{"action":"on_dvr","client_id":509,"ip":"192.168.0.4","vhost":"__defaultVhost__","app":"live","stream":"stream","param":"","cwd":"/disk/D/Srs","file":"./objs/nginx/html/live/stream.flv"}
(10) 查看端口监听状态
netstat -antp | grep 1935
(11) 使用ffmpeg推流
ffmpeg -re -i xx.mp4 -vcodec libx264 -acodec copy -strict -2 -f flv rtmp://xxIP地址:1935/live/stream
**注:**在
srs安装根目录/objs/nginx/html/live/xx.flv生成
(12) 播放flv视频文件
地址:http://192.168.0.28:8080/live/stream.flv
55. Srs(Simple Rtmp Server)配置推流权限(Java语言)
(1) 下载srs源代码并解压
源码下载地址:https://github.com/ossrs/srs/releases/
二进制文件下载地址:http://www.ossrs.net/srs.release/releases/download.html
(2) 进入srs解压目录/xx/trunk执行
./configure --prefix=/xx/srs安装路径 --full
(3) 编译
make
(4) 安装
make install
(5) 修改srs安装目录/xx/conf/srs.conf文件
srs.conf文件中内容:
listen 1935;
max_connections 1000;
srs_log_tank file;
srs_log_file ./objs/srs.log;
http_api {
enabled on;
listen 1985;
}
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
}
stats {
network 0;
disk sda sdb xvda xvdb;
}
vhost __defaultVhost__ {
min_latency on;
mr {
enabled off;
}
mw_latency 100;
gop_cache off;
queue_length 10;
tcp_nodelay on;
http_hooks {
enabled on;
on_connect http://192.168.0.4:8310/connect;
on_close http://192.168.0.4:8310/close;
on_publish http://192.168.0.4:8310/publish;
on_unpublish http://192.168.0.4:8310/unpublish;
on_play http://192.168.0.4:8310/play;
on_stop http://192.168.0.4:8310/stop;
on_dvr http://192.168.0.4:8310/dvr;
on_hls_notify http://192.168.0.4:8310/notify;
}
}
注:
Srs要求Http服务器返回状态码200并且response内容为整数错误码(0表示成功),其他错误码会断开客户端连接。
(6) 进入srs安装目录/etc/init.d文件夹中并启动srs服务
./srs start
(7) 创建Springboot项目,并配置依赖包
<!-- springboot 父节点区域 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
</parent>
<dependencies>
<!-- springboot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(8) 创建controller包,并创建controller.java文件
controller.java文件中内容:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin
@RestController
public class controller {
@RequestMapping(value="/connect",method = RequestMethod.POST)
public int Connect(@RequestBody String dvrMsg) {
System.out.println(dvrMsg);
//0表示成功1表示失败
return 0;
}
@RequestMapping(value="/close",method = RequestMethod.POST)
public ResponseEntity<Object> Close(@RequestBody String dvrMsg) {
System.out.println(dvrMsg);
return new ResponseEntity<Object>(0,HttpStatus.OK);
}
@RequestMapping(value="/publish",method = RequestMethod.POST)
public ResponseEntity<Object> Publish(@RequestBody String dvrMsg) {
System.out.println(dvrMsg);
return new ResponseEntity<Object>(0,HttpStatus.OK);
}
@RequestMapping(value="/unpublish",method = RequestMethod.POST)
public ResponseEntity<Object> Unpublish(@RequestBody String dvrMsg) {
System.out.println(dvrMsg);
return new ResponseEntity<Object>(0,HttpStatus.OK);
}
@RequestMapping(value="/play",method = RequestMethod.POST)
public ResponseEntity<Object> Play(@RequestBody String dvrMsg) {
System.out.println(dvrMsg);
return new ResponseEntity<Object>(0,HttpStatus.OK);
}
@RequestMapping(value="/stop",method = RequestMethod.POST)
public ResponseEntity<Object> Stop(@RequestBody String dvrMsg) {
System.out.println(dvrMsg);
return new ResponseEntity<Object>(0,HttpStatus.OK);
}
@RequestMapping(value="/dvr",method = RequestMethod.POST)
public ResponseEntity<Object> Dvr(@RequestBody String dvrMsg) {
System.out.println(dvrMsg);
return new ResponseEntity<Object>(0,HttpStatus.OK);
}
@RequestMapping(value="/notify",method = RequestMethod.POST)
public ResponseEntity<Object> Notify(@RequestBody String dvrMsg) {
System.out.println(dvrMsg);
return new ResponseEntity<Object>(0,HttpStatus.OK);
}
}
(9) 主启动目录中内容
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan("controller")
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
注:
on_dvr回调函数返回参数{"action":"on_dvr","client_id":509,"ip":"192.168.0.4","vhost":"__defaultVhost__","app":"live","stream":"stream","param":"","cwd":"/disk/D/Srs","file":"./objs/nginx/html/live/stream.flv"}
(10) 查看端口监听状态
netstat -antp | grep 1935
(11) 使用ffmpeg推流
ffmpeg -re -i xx.mp4 -vcodec libx264 -acodec copy -strict -2 -f flv rtmp://xxIP地址:1935/live/stream
56. Srs(Simple Rtmp Server)中回调函数
1.回调函数
| 回调函数 | 描述 |
|---|---|
on_connect | 当客户端连接到指定的vhost和app时 |
on_close | 当客户端关闭连接,或者Srs主动关闭连接时 |
on_publish | 当客户端发布流时 |
on_unpublish | 当客户端停止发布流时 |
on_play | 当客户端开始播放流时 |
on_stp | 当客户端停止播放时 |
on_dvr | 当dvr录制关闭一个flv文件时 |
2.回调函数返回参数
(1) on_connect函数
{"action": "on_connect","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app/": "live","tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a","pageUrl": "http://www.test.com/live.html"}
(2) on_close函数
{"action": "on_close","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","send_bytes": 10240, "recv_bytes": 10240}
(3) on_publish函数
{"action": "on_publish","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","tcUrl" => "rtmp://video.test.com/live?token=xxx&salt=yyy","stream": "livestream", "param":"?token=xxx&salt=yyy"}
(4) on_unpublish函数
{"action": "on_unpublish","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream", "param":"?token=xxx&salt=yyy"}
(5) on_play函数
{"action": "on_play","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream", "param":"?token=xxx&salt=yyy","pageUrl": "http://www.test.com/live.html"}
(6) on_stop函数
{"action": "on_stop","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream", "param":"?token=xxx&salt=yyy"}
(7) on_dvr函数
{"action": "on_dvr","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream", "param":"?token=xxx&salt=yyy","cwd": "/usr/local/srs","file": "./objs/nginx/html/live/livestream.1420254068776.flv"}
前言
看了下网上关于淘宝数据爬取的,发现都是换取到token令牌后,然后去访问淘宝进行爬取的,感觉太麻烦了,换了一个比较傻瓜式的方法。
使用java+selenium+swing做的一个小桌面软件,用于爬取淘宝首页数据。
项目说明
界面说明
- 淘宝账号和淘宝密码是用来登陆账号使用的,可能中途需要输入手机验证码登录,建议第一次先输入验证码后;
- 浏览器、浏览器路径和驱动路径是以谷歌浏览器和火狐浏览器为主。注意浏览器版本要和驱动对应
- chrome浏览器使用chromedriver。
- firefox浏览器使用geckodriver
- 获取接口地址是用来获取查询搜索词汇。
- 提交信息地址是用来将查询到的商品数据上传到平台。
流程说明
sequenceDiagram
爬虫 ->> 淘宝: 访问淘宝,进行登录
淘宝 -->> 爬虫: 登录成功,进入查询准备
loop 查询
爬虫->> 平台: 给个搜索词汇吧
平台-->>爬虫: 给你个 666 吧
Note right of 平台: 没有词汇要查了,跳出循环
爬虫->> 淘宝: 查询 666 按照销量查询
淘宝-->> 爬虫: 显示结果
爬虫-x 平台: 将结果提交给平台
end
项目结构
- /entity 存放实体类。CheckHomeGoodEntity是用来记录商品信息的类
- /lib 开发库
- /selenium 爬虫库。
- SeleniumConfig 爬虫配置
- SeleniumUtil 爬虫库,主要用于启动浏览器
- TBHomeSearchS 淘宝爬取规则
- /ui 界面
代码地址
代码说明
selenium部分
登录淘宝
private void login() throws Exception {
webDriver.get("https://login.taobao.com/member/login.jhtml?f=top&redirectURL=https://www.taobao.com");
Thread.sleep(5000);
WebElement usernameElement = webDriver.findElement(By.id("fm-login-id"));
usernameElement.sendKeys(seleniumConfig.getTaobaoAccount());
WebElement passwordElement = webDriver.findElement(By.id("fm-login-password"));
passwordElement.sendKeys(seleniumConfig.getTaobaoPwd());
webDriver.findElement(By.className("fm-submit")).click();
Thread.sleep(5000);
//需要滑块
if (checkElement(webDriver, By.id("nc_1_n1z"))) {
Actions action = new Actions(webDriver);
WebElement hk = webDriver.findElement(By.id("nc_1_n1z"));
action.moveToElement(hk).clickAndHold(hk);
// action.moveByOffset(200, 0).perform();
action.dragAndDropBy(hk
, 258,
0).pause(2000).perform();
webDriver.findElement(By.className("fm-submit")).click();
}
Thread.sleep(5000);
do {
if (webDriver.getCurrentUrl().contains("login.taobao.com/member/login.jhtml")) {
Thread.sleep(10000);
} else if (webDriver.getCurrentUrl().contains("login.taobao.com/member/login_unusual.htm")) {
//需要进行验证手机验证码
Thread.sleep(5000);
if (checkElement(webDriver, By.xpath("//*[@id=\"content\"]/div/div[1]/iframe"))) {
// Thread.sleep(2000)
WebElement iframe = webDriver.findElement(By.xpath("//*[@id=\"content\"]/div/div[1]/iframe"));
webDriver.switchTo().frame(iframe);
WebElement otherValidator = webDriver.findElement(By.id("otherValidator"));
otherValidator.click();
Thread.sleep(2000);
List<WebElement> liList = webDriver.findElements(By.xpath("//*[@id=\"content\"]/div/ol/li"));
for (WebElement webElement : liList) {
String text = webElement.findElement(By.className("text")).getText();
if (text.contains("手机验证码")) {
WebElement a = webElement.findElement(By.tagName("a"));
a.click();
break;
}
}
Thread.sleep(2000);
WebElement jGetCode = webDriver.findElement(By.id("J_GetCode"));
jGetCode.click();
} else {
if (checkElement(webDriver, By.id("J_Phone_Checkcode"))) {
WebElement jPhoneCheckcode = webDriver.findElement(By.id("J_Phone_Checkcode"));
jPhoneCheckcode.click();
if (jPhoneCheckcode.getAttribute("value").length() == 6) {
WebElement submitBtn = webDriver.findElement(By.id("submitBtn"));
submitBtn.click();
}
}
}
Thread.sleep(10000);
} else if (webDriver.getCurrentUrl().contains("www.taobao.com")) {
break;
} else {
break;
}
} while (true);
}
启动后,根据输入的名称,会进行自动登录操作。有2点需要注意以下:
- 登陆后,可能需要滑块验证,所以我这里模拟了鼠标操作滑动;
- 如果是非常用机器登录,会出现一个安全中心验证,这时就要进行手机验证码获取。只需收到验证码后填写,无需进行其他操作。
查询商品并提交
private void search(JSONObject info) {
try {
webDriver.get(searchHomeUrl + "&q=" + info.getString("name"));
Thread.sleep(10000);
// 没有找到与xxxx相关的宝贝
Boolean cboolean = checkElement(webDriver, By.className("combobar-noquery"));
Boolean aBoolean = checkElement(webDriver, By.className("item-not-found"));
if (aBoolean || cboolean) {
String res = OkHttpUtil.postJson(seleniumConfig.getApiSubmitPath(), info.toJSONString());
if ("".equalsIgnoreCase(res)) {
return;
}
JSONObject jsonObject = JSON.parseObject(res);
if (jsonObject.getInteger("code") == 0) {
return;
}
return;
}
List<CheckHomeGoodEntity> checkHomeGoodEntities = new ArrayList<>();
WebElement items = webDriver.findElement(By.xpath("//div[@id=\"mainsrp-itemlist\"]/div[1]/div[1]/div[1]"));
List<WebElement> mouserOnverReq = items.findElements(By.className("J_MouserOnverReq"));
//便利商品数据
for (WebElement webElement : mouserOnverReq) {
CheckHomeGoodEntity checkHomeGoodEntity = new CheckHomeGoodEntity();
WebElement picLink = webElement.findElement(By.className("pic-link"));
String detailUrl = picLink.getAttribute("data-href");
WebElement img = picLink.findElement(By.tagName("img"));
String picUrl = img.getAttribute("src");
String title = img.getAttribute("alt");
String viewPrice = webElement.findElement(By.className("price")).findElement(By.tagName("strong")).getText();
String viewSales = webElement.findElement(By.className("deal-cnt")).getText();
if (viewSales.indexOf("+") > -1) {
String substring = viewSales.substring(0, viewSales.indexOf("+"));
// Matcher matcher = pattern.matcher(substring);
if (substring.indexOf("万") > -1) {
checkHomeGoodEntity.setViewSales2((int) (Double.parseDouble(substring.substring(0, substring.length() - 1)) * 10000));
} else {
checkHomeGoodEntity.setViewSales2(Integer.parseInt(substring.substring(0, substring.length() - 1)));
}
} else if (viewSales.equalsIgnoreCase("")) {
checkHomeGoodEntity.setViewSales2(0);
} else {
checkHomeGoodEntity.setViewSales2(Integer.parseInt(viewSales.replaceAll("\\D", "")));
}
String itemLoc = webElement.findElement(By.className("location")).getText();
// String rawTitle = webElement.findElement(By.xpath("//*[@class=\"J_ClickStat\"]")).getText();
String shopLink = webElement.findElement(By.className("shop")).findElement(By.tagName("a")).getAttribute("href");
String nick = webElement.findElement(By.className("ww-small")).getAttribute("data-nick");
checkHomeGoodEntity.setRawTitle(title);
checkHomeGoodEntity.setViewPrice(new BigDecimal(viewPrice));
checkHomeGoodEntity.setItemLoc(itemLoc);
checkHomeGoodEntity.setViewSales(viewSales);
checkHomeGoodEntity.setDetailUrl(detailUrl);
if (detailUrl.indexOf("#detail") > -1) {
detailUrl = detailUrl.replaceAll("#detail", "");
}
checkHomeGoodEntity.setCommentUrl(detailUrl + "&on_comment=1");
checkHomeGoodEntity.setNick(nick);
checkHomeGoodEntity.setTitle(title);
checkHomeGoodEntity.setShopLink(shopLink);
checkHomeGoodEntity.setPicUrl(picUrl);
checkHomeGoodEntities.add(checkHomeGoodEntity);
}
info.put("checkHomeGoodEntities", checkHomeGoodEntities);
//提交到平台
String res = OkHttpUtil.postJson(seleniumConfig.getApiSubmitPath(), info.toJSONString());
if ("".equalsIgnoreCase(res)) {
return;
}
JSONObject jsonObject = JSON.parseObject(res);
if (jsonObject.getInteger("code") == 0) {
return;
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
控制浏览器查询商品后,将显示的商品列表提交到指定的接口。
最近离职了,然后闲的发慌,就做了这个demo版本吧,虽然还有挺多问题的,但是能用。
老规矩写的不清晰的地方,可以指出,我会不定时更新和修改。一起加油.
所有的伟大,都来自于一个勇敢的开始。
记录
2024-01-29。头一次参与开源项目,激励一下自己
Pull Request 详情
proxy代理前缀冲突,导致代理后,请求一直发送默认地址。 代理会生成两个路径,一个默认的是/proxy,一个其他的/proxy-demo。 然后正则匹配路径开头并重写时,因为默认的是/proxy,所以/proxy-demo,也被默认的拦截了。 修复方式:将/proxy换成/proxy-default
版本信息
v1.0-beta
解决了哪些问题
修复代理请求,都指向默认地址的问题
是否关闭了某个 Issue
否

