当前位置:首页 > 标签
SpringBoot 14 Spring 2 SpringMVC 3 MyBatis 2 Linux 4 阿里云 13 宝塔 1 Docker 3 ElasticSearch 2 Redis 4 Shiro 0 Dubbo 0 Swagger 0 Thymeleaf 6 数据库 11 MySQL 11 外键 2 Gradle 1 Test 0 Tomcat 1 JavaWeb 7 Ajax 1 注解 3 css 2 报错 3 多数据源 1 Java基础 1 源码 2 Servlet 1 JSP 1 环境搭建 8 RabbitMQ 1 七牛云 1 Edit.md 1 图像识别 4 英语 2 Zookeeper 1阿里云AI训练营_Day05_创意日_图片识别文字
- 2020-06-08
- 220
- SpringBoot
## 项目介绍
参加阿里云AI训练营的第5天,也是最后一天了。今天是创意日,想做什么就做什么,没有题目约束。
那么我打算做一个简单的识别图片中的文字的应用,并部署上线,以后也可能经常会用到。
## 项目用到的文档地址
阿里云达摩院视觉开放平台:https://vision.aliyun.com/
阿里云视觉开放平台 “通用识别” 地址:https://help.aliyun.com/document_detail/151896.html?spm=a2c4g.11186623.6.620.44da1ded5yuZbF
## 项目开始
### (1)说明
经过前面几天的训练,自我感觉良好。已经对阿里云的视觉开放平台比较熟悉了,也熟悉使用提供的API的一些基础的步骤,感觉就是这些套路。
那么就不啰嗦了,直接上代码。由于只用到了一个场景,所以代码也比较简单,记得测试前要到阿里云开启 “文字识别服务” 哦!

### (2)导入Maven依赖
```xml
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>ocr</artifactId>
<version>1.0.3</version>
</dependency>
<!-- 阿里巴巴的 fstjson ,和 jackson 的功能类似,用来处理 json字符串-->
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
```
### (3)创建Client,Config类
> AIService.java
```java
private Client ocrClient;
private RuntimeOptions runtimeOptions;
@Value("${accessKeyId}")
private String accessKeyId;
@Value("${accessKeySecret}")
private String accessKeySecret;
@PostConstruct
private void init() throws Exception {
Config config = new Config();
config.type = "access_key";
config.regionId = "cn-shanghai";
config.accessKeyId = accessKeyId;
config.accessKeySecret = accessKeySecret;
config.endpoint = "ocr.cn-shanghai.aliyuncs.com";
ocrClient = new Client(config);
runtimeOptions = new RuntimeOptions();
}
```
### (4)关键代码,调用API函数
这里由于我们需要上传图片,所以参数选择了一个 InputStream,直接从文件中获取输入流。
看文档我们知道,从图片中识别的文字会被封装在一个 data 里面,我们从这里面获取数据即可。

同时,经过测试,文字识别的过程是一行一行识别的,每行识别出来的文字都成为封装在数组的一个元素里面,所以我们定义一个 StringBuffer 对象,用来拼接这些生成的文字。
> AiService.java
```java
public String myRecognizeCharacter(InputStream is) throws Exception {
RecognizeCharacterAdvanceRequest request = new RecognizeCharacterAdvanceRequest();
request.imageURLObject = is;
request.minHeight = 10;
request.outputProbability = true;
RecognizeCharacterResponse response = ocrClient.recognizeCharacterAdvance(request, runtimeOptions);
StringBuffer result = new StringBuffer();
for (RecognizeCharacterResponse.RecognizeCharacterResponseDataResults item:response.data.results
) {
result.append(item.text + "\n");
}
return result.toString();
}
```
### (5)文件上传逻辑
我们在 Service 层实现文件上传的逻辑,然后在 Controller 层调用,并返回上传之后的文件名。
> AiService.java
```java
public String uploadImage(MultipartFile file, HttpServletRequest request) {
//获取文件名 : file.getOriginalFilename();
String uploadFileName = file.getOriginalFilename();
System.out.println("上传文件名 : "+uploadFileName);
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
System.out.println(uuid);
String newFileName = "AI-Word-" + uuid + "-" + uploadFileName;
//上传路径保存设置 UUID
String path = request.getServletContext().getRealPath("/upload");
// String path = "src/main/resources/static/upload";
//如果路径不存在,创建一个
File realPath = new File(path);
if (!realPath.exists()){
realPath.mkdir();
}
System.out.println("上传文件保存地址:"+realPath);
InputStream is = null; //文件输入流
OutputStream os = null;
try {
is = file.getInputStream();
os = new FileOutputStream(new File(realPath, newFileName)); //文件输出流
//读取写出
int len=0;
byte[] buffer = new byte[1024];
while ((len=is.read(buffer))!=-1){
os.write(buffer,0,len);
os.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return newFileName;
}
```
### (6)Controller 层的逻辑
下面我们来看一下 Controller 层的代码,主要就是实现视图的跳转。然后调用 Service 层,获得数据并放在 Model 中并返回给前端
> AiController.jva
```java
@Controller
public class AIController {
@Autowired
private AIService aiService;
@RequestMapping("/")
public String toMain() {
return "main";
}
@RequestMapping("/fileupload")
public String fileUpload(@RequestParam("file") MultipartFile file , HttpServletRequest request, Model model) throws IOException {
// 1. 文件上传,返回上传之后新的文件名称
String newFileName = aiService.uploadImage(file, request);
// 2. 图片中文字内容识别
InputStream is = file.getInputStream();
String resultWord = null;
try {
resultWord = aiService.myRecognizeCharacter(is);
} catch (Exception e) {
e.printStackTrace();
}
model.addAttribute("result", "/upload/" + newFileName);
model.addAttribute("word", resultWord);
return "result";
}
}
```
### (7) 前端部分
最后再来看一下前端部分代码:
> Result.html
```html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
div {
width: 400px;
border: 1px solid #ff5037;
margin-top: 70px;
}
img {
height: 400px;
}
</style>
</head>
<body>
<div th:text="${word}"></div>
<img th:src="@{${result}}" alt="">
</body>
</html>
```
## 测试
先进入主页面,选择图片并点击上传按钮:

识别出来的结果:

## 阿里云高校计划
最后在贴一张阿里云的广告:”阿里云高校计划“,快来加入我们吧!

阿里云AI训练营_Day04_车辆检测系统
- 2020-06-08
- 208
- 阿里巴巴
## 项目介绍
参加阿里云AI训练营的第4天,完成一个车辆检测系统
主要思路:用户上传身份证和受损车辆图片,识别结果返回前端,同时将数据存入数据库。
## 项目用到文档地址
阿里云达摩院视觉开放平台:https://vision.aliyun.com/
阿里云视觉开放平台 “车辆损伤识别” 地址:https://help.aliyun.com/document_detail/155002.html?spm=a2c4g.11174283.6.755.77e06bdfN9iMtt
## 项目模块1-各种识别服务
### (1)说明
一开始的设想是用到身份证识别和车辆识别2个服务,但是发现同时导入 2 个依赖之后,有一些 jar 包会冲突,导致身份证识别的部分由于缺少一些 jar 包而不能正常运行。最后只用到了机动车识别和车辆损伤识别2个服务。
### (2)导入项目依赖
这里的依赖我们使用较早的版本
```xml
<!-- 内容识别对应依赖 -->
<!-- https://mvnrepository.com/artifact/com.aliyun/objectdet -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>objectdet</artifactId>
<version>0.0.5</version>
</dependency>
<!-- 图片识别 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>ocr</artifactId>
<version>1.0.3</version>
</dependency>
```
之后同样要开通 “目标检测服务” 才能正常运行。
### (3)创建 Client,Config类
虽然身份证识别部分会报错,但是这里依旧吧身份证识别部分的代码贴出来
> CraService.java
```java
@Value("${accessKeyId}")
private String accessKeyId;
@Value("${accessKeySecret}")
private String accessKeySecret;
// "内容识别" --> 导入这个类:import com.aliyun.objectdet20191230.Client;
private com.aliyun.objectdet.Client objectClient;
private com.aliyun.ocr.Client ocrClient;
private RuntimeObject runtimeObject;
private RuntimeOptions runTimeOperations;
@PostConstruct
public void initClient() throws Exception {
Config objectConfig = new Config();
objectConfig.type = "access_key";
objectConfig.regionId = "cn-shanghai";
objectConfig.accessKeyId = accessKeyId;
objectConfig.accessKeySecret = accessKeySecret;
// 注意这里的域名,我们是 “内容识别” 服务,所以开头是 objectdet=
objectConfig.endpoint = "objectdet.cn-shanghai.aliyuncs.com";
objectClient = new com.aliyun.objectdet.Client(objectConfig);
runtimeObject = new RuntimeObject();
com.aliyun.ocr.models.Config ocrConfig = new com.aliyun.ocr.models.Config();
ocrConfig.type = "access_key";
ocrConfig.regionId = "cn-shanghai";
ocrConfig.accessKeyId = accessKeyId;
ocrConfig.accessKeySecret = accessKeySecret;
// 注意这里的域名,我们是 “内容识别” 服务,所以开头是 objectdet=
ocrConfig.endpoint = "ocr.cn-shanghai.aliyuncs.com";
ocrClient = new com.aliyun.ocr.Client(ocrConfig);
runTimeOperations = new RuntimeOptions();
}
```
需要注意的是两个 Client 是不同的包下面的 Client 类,一个是 “内容识别” 的,另一个是 “图像识别” 的。
### (4)调用关键 API
这里原本调用了3个服务,分别是 “身份证识别”,“机动车识别”,“车辆受损识别”,关键代码如下:
```java
// “机动车识别”
public String myDetectVehicle(String filePath) throws Exception {
DetectVehicleAdvanceRequest request = new DetectVehicleAdvanceRequest();
request.imageURLObject = new FileInputStream(filePath);
DetectVehicleResponse response = objectClient.detectVehicleAdvance(request, runtimeObject);
String result = null;
// 发现这是一个内部类
for (DetectVehicleResponse.DetectVehicleResponseDataDetectObjectInfoList item:response.data.detectObjectInfoList
) {
System.out.println(item.type);
System.out.println(item.score);
if ("vehicle".equals(item.type)) {
System.out.println("检测成功!是机动车");
} else {
System.out.println("该图片不是机动车!");
}
result = item.type;
}
return result;
}
// “车辆损伤识别”
public String myRecognizeVehicle(String filePath) throws Exception {
// 使用 xxxAdvanceRequest,支持本地图片上传
RecognizeVehicleDamageAdvanceRequest request = new RecognizeVehicleDamageAdvanceRequest();
request.imageURLObject = new FileInputStream(filePath);
// 识别 “车辆损伤”
RecognizeVehicleDamageResponse response = objectClient.recognizeVehicleDamageAdvance(request, runtimeObject);
return getHurtResult(response.data.elements);
}
// “身份证识别” -------->
public String MyRecognizeIdCard(String filePath, String side) throws Exception {
RecognizeIdentityCardAdvanceRequest request = new RecognizeIdentityCardAdvanceRequest();
request.imageURLObject = Files.newInputStream(Paths.get(filePath));
request.side = side;
RecognizeIdentityCardResponse response = ocrClient.recognizeIdentityCardAdvance(request, runTimeOperations);
if ("face".equals(side)) {
return JSON.toJSONString(response.data.frontResult);
} else {
return "";
}
}
```
### (5) 车辆损伤补充代码
看文档我们知道,由于车辆损伤有很多种情况,所以我们需要对数据进行一定的处理,思路是通过一个 switch~case 语句区分各种损伤的具体情况。

> 具体代码:
```java
// 对 “车辆损伤” 识别出来的数据进行一定的处理,方便前台展示
public String getHurtResult(RecognizeVehicleDamageResponse.RecognizeVehicleDamageResponseDataElements[] items) {
StringBuffer type = new StringBuffer("检测到的车辆损伤为:");
for (RecognizeVehicleDamageResponse.RecognizeVehicleDamageResponseDataElements item:items
) {
switch (item.type) {
case "1": type.append("轻微刮擦 ");
break;
case "2": type.append("重度刮擦 ");
break;
case "3": type.append("轻度变形 ");
break;
case "4": type.append("中度变形 ");
break;
case "5": type.append("重度变形 ");
break;
case "6": type.append("crack破损孔洞 ");
break;
case "7": type.append("翼子板和大灯缝隙 ");
break;
case "8": type.append("翼子板保险杠缝隙 ");
break;
case "9": type.append("大灯轻微刮擦 ");
break;
case "10": type.append("大灯重度刮擦 ");
break;
case "11": type.append("大灯破损 ");
break;
case "12": type.append("后视镜轻微刮擦 ");
break;
case "13": type.append("后视镜玻璃破损 ");
break;
case "14": type.append("后视镜脱落 ");
break;
case "15": type.append("挡风玻璃破损 ");
break;
}
}
return type.toString();
}
```
## 项目模块2-图片上传服务,及数据库操作
这部分做的比较简易,没有美化样式,能看就行~就是一个简单的 from 表单
> test.html
```html
<form th:action="@{/upload}" enctype="multipart/form-data" method="post">
身份证:<input type="file" name="identityfile">
受损车辆:<input type="file" name="carfile">
<input type="submit" value="upload">
</form>
```
----
图片上传对应的 Controller,上传完成之后,同时进行识别,然后在将识别输入存入数据库中
> CarController.java
```java
@RequestMapping("/upload")
public String fileUpload(@RequestParam("carfile") MultipartFile carFile, @RequestParam("identityfile") MultipartFile identityFile, Model model) throws IOException {
// 1. 上传图片 ----》 “身份证”
String identityPath = aiCarService.fileUpload(identityFile);
// 2. 上传图片 -----》 “受损车辆”
String carPath = aiCarService.fileUpload(carFile);
// 3. 调用 Service 层分析图片
String face = null;
String carStr = null;
String carStr2 = null;
try {
// 3.1 身份证识别 ---> 只需要识别正面
// face = carService.MyRecognizeIdCard(identityPath , "face");
// 3.2 机动车检查
carStr = aiCarService.myDetectVehicle(carPath);
// 3.3 车辆损伤识别
carStr2 = aiCarService.myRecognizeVehicle(carPath);
} catch (Exception e) {
e.printStackTrace();
}
// 4. 保存数据到数据库中
Car car = new Car(null, carStr, carStr2);
carService.insert(car);
// 4. 将图片路径放入 Model 中
String carFileName = carFile.getOriginalFilename();
String identityFileName = identityFile.getOriginalFilename();
model.addAttribute("carfilename", "/upload/" + carFileName);
model.addAttribute("identityfilename", "/upload/" + identityFileName);
model.addAttribute("face", face);
if ("vehicle".equals(carStr)) {
model.addAttribute("carStr", "检测成功!是机动车");
} else {
model.addAttribute("carStr", "您上传的图片不是机动车,请重新上传!!");
}
model.addAttribute("carStr2", carStr2);
return "result";
}
```
对应的 Service
> CarService.java
```java
/**
* @Description: 图片上传服务
* @Param: [file]
* @return: void
* @Author: 林凯
* @Date: 2020/6/8
*/
public String fileUpload(MultipartFile file) {
// 1. 获取文件名
String uploadFileName = file.getOriginalFilename();
// 2. 上传路径保存设置 UUID
String path = "src/main/resources/static/upload";
// 2.1 如果路径不存在,创建一个
File realPath = new File(path);
if (!realPath.exists()){
realPath.mkdir();
}
// 3. 上传文件,并保存
InputStream is = null; //文件输入流
OutputStream os = null;
try {
is = file.getInputStream();
os = new FileOutputStream(new File(realPath, uploadFileName));
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
os.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return path + "\\" + uploadFileName;
}
```
返回结果给前端,前端的处理部分
> result.html
```html
<img th:src="${carfilename}" alt="">
<img th:src="${identityfilename}" alt="">
<p th:text="${carStr}"></p>
<p th:text="${carStr2}"></p>
```
## 测试效果
首先进入上传图片页面,并上传2张图片

然后点击 upload 上传并查看结果:

数据库里面的结果:

## 阿里云高校计划
最后在贴一张阿里云的广告:”阿里云高校计划“,快来加入我们吧!

阿里云AI训练营_Day03_电子相册
- 2020-06-06
- 274
- 阿里巴巴
## 项目介绍
参加阿里云AI训练营的第3天,完成一个电子相册。
电子相册需要用到 “人脸属性识别” 和 “场景识别” 2个服务,开通之后就开始我们的项目吧!
## 项目用到文档地址
阿里云达摩院视觉开放平台:https://vision.aliyun.com/
阿里云视觉开放平台 “人脸属性识别” 地址:https://help.aliyun.com/document_detail/151968.html?spm=a2c4g.11186623.2.20.19714c68VRbnB9
阿里云视觉开放平台 “场景识别” 地址:https://help.aliyun.com/document_detail/152007.html?spm=a2c4g.11186623.6.670.19714c68sp5KFt
github给出的示例代码地址:https://github.com/aliyun/alibabacloud-viapi-demo
## 项目开始
### (1)引入项目依赖
依赖就按照示例代码中的依赖来吧,引入之后可以支持本地图片上传。
```xml
<!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-core -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.4.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.aliyun/facebody -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>facebody</artifactId>
<version>0.0.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.aliyun/imagerecog -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>imagerecog</artifactId>
<version>0.0.5</version>
</dependency>
```
- 记得要开通对应场景的服务,否则就会报这样的错误

### (2)创建并初始化 Client,Config
和之前的套路一样,我们需要创建对应场景的 Client 和 Config 类,关于这部分的详细解释可以参照我的上一篇博客 【阿里云AI训练营-_Day02-身份证识别】。
关于 Config 的属性配置,文档中也详细说明了,我们要根据不同的场景赋予不同的值。

-----
由于我们用到了 2 个场景的服务,所以可以先定义2个关于场景的变量用来标识不同的场景
> 定义 2 个标识场景的变量
```java
// 我们自己的 accessKey 和 accessSecret,需要在配置文件中配置
@Value("${aliyun.accessKeyId}")
private String accessKey;
@Value("${aliyun.accessSecret}")
private String accessSecret;
// 人脸属性识别的场景标识
static String faceBodyEndpoint = "facebody";
// 图片场景识别的标识
static String imageRecogEndpoint = "imagerecog";
```
-----
下面分别贴出关于 2 个场景的 Client 和Config 类的配置
> Client 和 Config 配置
```java
// 创建人脸属性识别的 Client
private com.aliyun.facebody.Client getFaceBodyClient(String endpoint) throws Exception {
com.aliyun.facebody.models.Config config = new com.aliyun.facebody.models.Config();
config.accessKeyId = accessKey;
config.accessKeySecret = accessSecret;
config.type = "access_key";
config.regionId = "cn-shanghai";
config.endpointType = "internal";
// 这里的 config.enpoint 由外部传入,就是传入我们一开始定义的变量,格式化之后为: facebody.cn-shanghai.aliyuncs.com , 对应 人脸属性识别的域名
config.endpoint = String.format("%s.%s", endpoint, "cn-shanghai.aliyuncs.com");
config.protocol = "http";
return new com.aliyun.facebody.Client(config);
}
// 创建场景识别的 Client
private com.aliyun.imagerecog.Client getImageRecogClient(String endpoint) throws Exception {
com.aliyun.imagerecog.models.Config config = new com.aliyun.imagerecog.models.Config();
config.accessKeyId = accessKey;
config.accessKeySecret = accessSecret;
config.type = "access_key";
config.regionId = "cn-shanghai";
config.endpointType = "internal";
// 这里的 config.enpoint 由外部传入,就是传入我们一开始定义的变量,格式化之后为: imagerecog.cn-shanghai.aliyuncs.com , 对应场景识别的访问域名。
config.endpoint = String.format("%s.%s", endpoint, "cn-shanghai.aliyuncs.com");
config.protocol = "http";
return new com.aliyun.imagerecog.Client(config);
}
```
### (3)调用对应场景的 API
老规矩,看文档;示例代码永远只能是举了例子,关键部分的解释还是文档中更详细。
#### 1. 人脸属性识别场景
文档中说了,返回的数据有很多,List,Integer之类的,他们都封装在这个叫 Data 的属性里面,那么我们调用之后就可以直接从 xxxResponse 中获取数据了。

但是实际上并没有想象中的怎么美好,我看了一下示例代码,发现这个 Data 里面只有一个名字叫做 elements 的对象数组,数组里面的对象是 RecognizeExpressionResponseDataElements 类的对象;然后在这个 RecognizeExpressionResponseDataElements 类里面只能够获取够获取 3 个属性:

这明显和文档中的不符合啊,我个人感觉可能是依赖版本的问题,示例代码中的版本可能太低了,而文档是6月3号更新的,所以使用新版本后,应该以文档为标准;
由于临近期末,时间有限,我就不再尝试了,有兴趣的小伙伴可以测试一下新版本的SDK。
那么按照示例中的代码,这部分的代码应该这样
> 人脸属性识别代码 ,传入的参数为从图片获取的输入流
```java
// 人脸属性识别主要有哪些表情
public List<String> recognizeExpression(InputStream inputStream) throws Exception {
RecognizeExpressionAdvanceRequest request = new RecognizeExpressionAdvanceRequest();
request.imageURLObject = inputStream;
List<String> labels = new ArrayList<>();
try {
// 调用我们前面的方法,获取一个 Client 对象
Client client = getFaceBodyClient(faceBodyEndpoint);
RecognizeExpressionResponse resp = client.recognizeExpressionAdvance(request, new RuntimeObject());
// 注意这里我们遍历的是 data 里面的 elements 对象数组,然后从对象中获取 expression 并放入 labels 中
for (RecognizeExpressionResponse.RecognizeExpressionResponseDataElements element : resp.data.elements) {
labels.add(ExpressionEnum.getNameByNameEn(element.expression));
}
} catch (ClientException e) {
log.error("ErrCode:{}, ErrMsg:{}, RequestId:{}", e.getErrCode(), e.getErrMsg(), e.getRequestId());
}
return labels;
}
```
#### 2. 场景识别
文档关键部分截图,这次文档和代码的出入比较很少,可以跟着文档来走。从 Data 中获取 Tags 进一步获取 Value

> 场景识别关键代码,传入的参数为从图片获取的输入流
```java
// 场景识别部分代码
public List<String> recognizeScene(InputStream inputStream) throws Exception {
RecognizeSceneAdvanceRequest request = new RecognizeSceneAdvanceRequest();
request.imageURLObject = inputStream;
List<String> labels = new ArrayList<>();
try {
// 调用之前的方法获取一个 Client 对象
com.aliyun.imagerecog.Client client = getImageRecogClient(imageRecogEndpoint);
RecognizeSceneResponse resp = client.recognizeSceneAdvance(request, new RuntimeObject());
// 遍历 data 里面的 tags,从中获取 value 并放入 labels 中
for (RecognizeSceneResponse.RecognizeSceneResponseDataTags tag: resp.data.tags) {
labels.add(tag.value);
}
} catch (ClientException e) {
log.error("ErrCode:{}, ErrMsg:{}, RequestId:{}", e.getErrCode(), e.getErrMsg(), e.getRequestId());
}
return labels;
}
```
### (4)测试
由于示例代码中的前端部分使用了 Vue + ElementUI,我对 Vue 不是很精通,所以我运行项目后拖入图片没有反应,F12控制里面的 Console 会报错。
所以这里直接使用本地的图片,直接调用 VisionService 类里面的方法(就是我们刚才写的那些方法)来测试
> 测试代码
```java
@Autowired
VisionService visionService;
@Test
void contextLoads() {
try {
FileInputStream inputStream = new FileInputStream("D:\\Temp\\images\\5a4ca1f4ddd31.jpg");
List<String> expressions = visionService.recognizeExpression(inputStream);
for (String expersion:expressions
) {
System.out.println("人脸表情:" + expersion);
}
FileInputStream inputStream1 = new FileInputStream("D:\\Temp\\images\\5a4ca1f4ddd31.jpg");
List<String> recognizeScene = visionService.recognizeScene(inputStream1);
for (String scene:recognizeScene
) {
System.out.println("对应场景:" + scene);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
```
> 本地图片

最中的识别结果:

## 关于
**由于前端代码出现了Bug,所以只测试了后端调用服务API的关键代码,主要是结合代码和文档一起分析,这样才能一通百通。额,好吧,最主要的原因是时间不够,月底就期末考了,能抽出来的时间不是很多,所以不想把时间花费在解决前端的Bug上面。**
## 友情推广
贴一下阿里云的活动,有兴趣的小伙伴可以参加一下

阿里云AI训练营_Day02_文字识别_身份证识别
- 2020-06-05
- 185
- 阿里巴巴
## 项目介绍
近期参加了阿里云的AI训练营,按照要求完成了一个 “身份证识别” 的 Web应用,特此记录一下。
**由于之前使用过百度AI的人脸识别SDK,而且对阿里云比较熟悉,所以这篇博客侧重于对官方文档阅读理解,以及对给出的视频和项目Demo的理解及运用。**
## 项目用到文档地址
阿里云达摩院视觉开放平台:https://vision.aliyun.com/
阿里云视觉开放平台文档地址:https://help.aliyun.com/product/142958.html?spm=a2c4g.11186623.6.540.263e3c74d59JVh
Aliyun Java SDK COCR 仓库地址:https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-core
新版文字识别 Ocr20191230 仓库地址:https://mvnrepository.com/artifact/com.aliyun/ocr20191230
阿里云的2个Demo的github项目地址:https://github.com/aliyun/alibabacloud-viapi-demo
文章中的代码都是参考的这2个Demo
### 说明
**经过多次尝试并仔细的阅读,发现官方提供的Demo中的引入和依赖和官方文档中引入的依赖有点区别。而且Demo中的 Config 类的配置和文档中的配置也存在一定的区别。**
**我是2种依赖和配置都测试过了,觉得官方文档中的配置的确有问题,会导致报错,于是选择了 Demo 中的配置。(后面会有详细说明)**
这里只展示本地图片上传识别,没用到阿里云OOS对象存储服务;关于前端的一些代码逻辑也没有给出,只给出身份证识别关键部分的代码。
## 开始项目(配置参照Demo中的配置)
### 开通服务
按照文档中的说明,开通服务,不出意外的话很容易就开通成功了,如下图所示:

### 使用 Maven 导入SDK相关依赖
文档中说了提供2个版本的SDK,旧版需用阿里云OSS存储我们的图片,由于我用的是七牛云的图像存储,没用用阿里云,于是选择新版本SDK,支持本地图片上传。
这里我们选择使用官方提供的Demo中的依赖,而不是用官方文档中提供的依赖
我的导入的依赖
```
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>ocr</artifactId>
<version>1.0.3</version>
</dependency>
```
### 创建 Config 类和 Client 类
参考文档中的说明,我们需要先创建一个 Config 类,将我们的 accessKeyId 和 accessKeySecret 以及一些其他的配置保存在 Config 类中,再通过 Config 类为参数创建 Client 类。
注意:Config 类和 Client 类都是位于 com.aliyun.ocr 包下面的类,导包的时候注意一下。
遇到的坑,文档中的配置存在 Bug,具体如下:

改进之后,我们关于 Config 类的配置及初始化应该这样(和官方提供的Demo中一样)
```
private Client client;
private RuntimeOptions runtime;
@Value("${accessKeyId}")
private String accessKeyId;
@Value("${accessKeySecret}")
private String accessKeySecret;
@PostConstruct
private void init() throws Exception {
Config config = new Config();
config.type = "access_key";
config.regionId = "cn-shanghai";
config.accessKeyId = accessKeyId;
config.accessKeySecret = accessKeySecret;
config.endpoint = "ocr.cn-shanghai.aliyuncs.com";
ocrClient = new Client(config);
// 需要注意的是,这里我们创建一个 RuntimeOptions 对象,之后会用到这个对象
runtime = new RuntimeOptions();
}
```
之后继续看文档
### 调用 Client 类的方法
#### (1)调用流程概述
文档中举了一个银行卡识别的例子,说白了就是创建一个 xxxRequest 对象,然后获取本地图片的数量流并赋值给 xxxRequest 对象的 imageURLObject 属性,以及将一些其他的配置放入 xxxRequest 对象中。
之后通过调用 Client 对象的 xxxAdvance() 方法,获得一个 xxxResponse 对象;然后我们只需要从这个 xxxResponse 对象中获取 JSON 字符串即可。
------
这个 xxx 代表了不同的场景,即不同的场景使用的类和方法都有所不同,不过名字都类似。具体的场景的参数也会有略微的不同,可以参看文档中关于该场景的详细描述。

#### (2)具体的使用
下面以身份证识别为例,看下文档中是如何描述的:

实际上在这里还可以进行调试,快速体验一下身份证识别:

我们在IDEA中查看查找相关的类

实际上这些场景对应的类都可以在我们添加Maven依赖之后导入的jar包中看到:

好了,看完了文档之后,我就使用身份证识别场景的类和方法对文档中的代码进行替换,效果如下
```
public String MyRecognizeIdCard(String filePath, String side) throws Exception {
// 注意我们的类的名称和方法的名称
RecognizeIdentityCardAdvanceRequest req = new RecognizeIdentityCardAdvanceRequest();
InputStream inputStream = new FileInputStream(new File(filePath));
req.imageURLObject = Files.newInputStream(Paths.get(filePath));
req.side = side;
RecognizeIdentityCardResponse rep = client.recognizeIdentityCardAdvance(req, runtime);
// 这个 face 不是凭空产生的,是我们看完文档中,文档中给出的值
if ("face".equals(side)) {
// 正面识别,通过阿里巴巴的 fastjson 将这个 fromResult 类转换为 JSON 格式的字符串并返回
return JSON.toJSONString(rep.data.frontResult);
} else {
// 反面识别
return JSON.toJSONString(rep.data.backResult);
}
}
```
之后我们在测试类中进行测试
```
@Autowired
private OcrService ocrService;
@Test
void contextLoads() {
try {
String face = ocrService.MyRecognizeIdCard("D:\\Temp\\images\\_2020053011373037SS.png", "face");
System.out.println("face = " + face);
String backface = ocrService.MyRecognizeIdCard("D:\\Temp\\images\\IMG20200530112702.jpg", "back");
System.out.println("back = " + backface);
} catch (TeaException e) {
System.out.println(e.getData());
} catch (Exception e) {
e.printStackTrace();
}
}
```
由于我的图片打了马赛克,所以只能识别出部分结果,结果如下所示

## 关于使用文档中的配置
我照着文档一步一步来,和文档一样的代码,依赖也是一样的,莫名其妙会报错。不知道是我自己的原因还是文档本来就有错,反正我的代码就是跑不起来,这里展示一下我的报错的代码,希望有小伙伴可以发现问题并解决。
> 初始化部分 ----> 关键就是没有将 config.endpointType="internal"; 注释掉

> 我引入的Maven依赖
```
<!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-core -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.aliyun/ocr20191230 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>ocr20191230</artifactId>
<version>0.0.3</version>
</dependency>
```
> 报错

(转载)接近8000字的Spring/SpringBoot常用注解总结!安排!
- 2020-05-18
- 178
- SpringBoot
非原创
> 原文作者:SnailClimb
本文转载自:https://www.imooc.com/article/304149
## 前言
大家好,我是 Guide 哥!这是我的 221 篇优质原创文章。如需转载,请在文首注明地址,蟹蟹!
本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:https://github.com/Snailclimb/JavaGuide
可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法,掌握搞懂,使用 SpringBoot 来开发项目基本没啥大问题了!
整个目录如下,内容有点多:

为什么要写这篇文章?
最近看到网上有一篇关于 SpringBoot 常用注解的文章被转载的比较多,我看了文章内容之后属实觉得质量有点低,并且有点会误导没有太多实际使用经验的人(这些人又占据了大多数)。所以,自己索性花了大概 两天时间简单总结一下了。
因为我个人的能力和精力有限,如果有任何不对或者需要完善的地方,请帮忙指出!Guide 哥感激不尽!
## 1.`@ SpringBootApplication`
这里先单独拎出 `@ SpringBootApplication` 注解说一下,虽然我们一般不会主动去使用它。
Guide 哥:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。
```
@SpringBootApplication
public class SpringSecurityJwtGuideApplication {
public static void main(java.lang.String[] args) {
SpringApplication.run(SpringSecurityJwtGuideApplication.class, args);
}
}
```
我们可以把 `@ SpringBootApplication`看作是 `@ Configuration`、`@ EnableAutoConfiguration`、`@ ComponentScan` 注解的集合。
```
package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
......
}
package org.springframework.boot;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
```
根据 SpringBoot 官网,这三个注解的作用分别是:
- `@ EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制
- `@ ComponentScan`: 扫描被`@ Component` (`@ Service`,`@ Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。
- `@ Configuration`:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类
## 2. Spring Bean 相关
### 2.1. `@ Autowired`
自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。
```
@Service
public class UserService {
......
}
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
......
}
```
### 2.2. `Component`,`@ Repository`,`@ Service`, `@ Controller`
我们一般使用 `@ Autowired` 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 `@ Autowired` 注解自动装配的 bean 的类,可以采用以下注解实现:
- `@ Component` :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
- `@ Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。
- `@ Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
- `@ Controller` : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
### 2.3. `@ RestController`
`@ RestController`注解是`@ Controller`和`@ ResponseBody`的合集,表示这是个控制器 bean,并且是将函数的返回值直 接填入 HTTP 响应体中,是 REST 风格的控制器。
Guide 哥:现在都是前后端分离,说实话我已经很久没有用过`@ Controller`。如果你的项目太老了的话,就当我没说。
单独使用 `@ Controller` 不加 `@ ResponseBody`的话一般使用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。`@ Controller` +`@ ResponseBody` 返回 JSON 或 XML 形式数据
关于`@ RestController` 和 `@ Controller`的对比,请看这篇文章:
[@ RestController vs @ Controller](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485544&idx=1&sn=3cc95b88979e28fe3bfe539eb421c6d8&chksm=cea247a3f9d5ceb5e324ff4b8697adc3e828ecf71a3468445e70221cce768d1e722085359907&token=1725092312&lang=zh_CN#rd "66")
### 2.4. `@ Scope`
声明 Spring Bean 的作用域,使用方法:
```
@Bean
@Scope("singleton")
public Person personSingleton() {
return new Person();
}
```
四种常见的 Spring Bean 的作用域:
- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
prototype : 每次请求都会创建一个新的 bean 实例。
- request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
- session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
### 2.5. `Configuration`
一般用来声明配置类,可以使用 @Component注解替代,不过使用Configuration注解声明配置类更加语义化。
```
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
```
## 3. 处理常见的 HTTP 请求类型
**5 种常见的请求类型:
**
- GET :请求从服务器获取特定资源。举个例子:GET /users(获取所有学生)
- POST :在服务器上创建一个新的资源。举个例子:POST /users(创建学生)
- PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生)
- DELETE :从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生)
PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。
### 3.1. GET 请求
@ GetMapping("users") 等价于@ RequestMapping(value="/users",method=RequestMethod.GET)
```
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
return userRepository.findAll();
}
```
### 3.2. POST 请求
@ PostMapping("users") 等价于@ RequestMapping(value="/users",method=RequestMethod.POST)
关于`@ RequestBody`注解的使用,在下面的“前后端传值”这块会讲到。
```
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) {
return userRespository.save(user);
}
```
### 3.3. PUT 请求
@ PutMapping("/users/{userId}") 等价于@ RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)
```
@PutMapping("/users/{userId}")
public ResponseEntity<User> updateUser(@PathVariable(value = "userId") Long userId,
@Valid @RequestBody UserUpdateRequest userUpdateRequest) {
......
}
```
### 3.4. DELETE 请求
@ DeleteMapping("/users/{userId}")等价于@ RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)
```
@DeleteMapping("/users/{userId}")
public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
......
}
```
### 3.5. PATCH 请求
一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。
```
@PatchMapping("/profile")
public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) {
studentRepository.updateDetail(studentUpdateRequest);
return ResponseEntity.ok().build();
}
```
## 4. 前后端传值
**掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!
**
### 4.1. `@ PathVariable` 和 `@ RequestParam`
`@ PathVariable`用于获取路径参数,`@ RequestParam`用于获取查询参数。
举个简单的例子:
```
@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
@PathVariable("klassId") Long klassId,
@RequestParam(value = "type", required = false) String type ) {
...
}
```
如果我们请求的 url 是:`/klasses/{123456}/teachers?type=web`
那么我们服务获取到的数据就是:`klassId=123456,type=web`。
### 4.2. @RequestBody
用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter或者自定义的HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。
我用一个简单的例子来给演示一下基本使用!
我们有一个注册的接口:
```
@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) {
userService.save(userRegisterRequest);
return ResponseEntity.ok().build();
}
```
`UserRegisterRequest`对象:
```
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
@NotBlank
private String userName;
@NotBlank
private String password;
@FullName
@NotBlank
private String fullName;
}
```
我们发送 post 请求到这个接口,并且 body 携带 JSON 数据:
```
{"userName":"coder","fullName":"shuangkou","password":"123456"}
```
这样我们的后端就可以直接把 json 格式的数据映射到我们的 UserRegisterRequest 类上。

需要注意的是:一个请求方法只可以有一个`@ RequestBody`,但是可以有多个`@ RequestParam`和`@ PathVariable`。 如果你的方法必须要用两个 `@ RequestBody`来接受数据的话,大概率是你的数据库设计或者系统设计出问题了!
## 5. 读取配置信息
很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。
下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。
我们的数据源application.yml内容如下::
```
wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油!
my-profile:
name: Guide哥
email: koushuangbwcx@163.com
library:
location: 湖北武汉加油中国加油
books:
- name: 天才基本法
description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
- name: 时间的秩序
description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
- name: 了不起的我
description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?
```
### 5.1. @value(常用)
使用 `@ Value("${property}")` 读取比较简单的配置信息:
```
@Value("${wuhan2020}")
String wuhan2020;
```
### 5.2. @ ConfigurationProperties(常用)
通过`@ ConfigurationProperties`读取配置信息并与 bean 绑定。
```
@Component
@ConfigurationProperties(prefix = "library")
class LibraryProperties {
@NotEmpty
private String location;
private List<Book> books;
@Setter
@Getter
@ToString
static class Book {
String name;
String description;
}
省略getter/setter
......
}
```
你可以像使用普通的 Spring bean 一样,将其注入到类中使用。
### 5.3. PropertySource(不常用)
`@ PropertySource`读取指定 properties 文件
```
@Component
@PropertySource("classpath:website.properties")
class WebSite {
@Value("${url}")
private String url;
省略getter/setter
......
}
```
更多内容请查看我的这篇文章:
[《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486181&idx=2&sn=10db0ae64ef501f96a5b0dbc4bd78786&chksm=cea2452ef9d5cc384678e456427328600971180a77e40c13936b19369672ca3e342c26e92b50&token=816772476&lang=zh_CN#rd)
## 6. 参数校验
.... 待完善(后面的不常用)这里就不继续写了,可以看看原文。
博客时间轴_按时间分类功能实现
- 2020-05-16
- 179
- SpringBoot
今天记录一下博客按时间归档功能的思路以及具体的实现步骤。
先上效果图:

**主要的解决过程有 2 个:**
- 第 1 个是通过 MySQL 的查询语句,统计不同时间段内的博客数量及内容,然后返回数据给前端。
- 第 2 个就是结合前端的 Thymeleaf 模版引擎提供的一些日期格式化工具,根据后端提供的数据,实现博客的按时间归档。
**这里仅仅提供我的一种思路。因为每个人的数据表的结构不太一样,这里只能提供一种思路供大家参考。**
**只要思想不滑坡,办法总比困哪多!**
### 通过 MySQL 查询将数据归档
首先,我们存在数据库中的时间是精确到秒的,如下图所示:

#### (1)通过 year,month等函数将时间中的年,月分离出来
对应的 SQL 语句1:
**注意:由于这里需要统计博客的数量,所以不能实现将每条博客记录的具体时间查询出来,这得借助第 2 个查询语句。**
```
select year(javaweb_myblog.blog.createtime) as 'year',
month(javaweb_myblog.blog.createtime) as 'month',
count(*) as 'count'
from javaweb_myblog.blog
group by year(javaweb_myblog.blog.createtime) desc,
month(javaweb_myblog.blog.createtime) desc
```
查询结果如下图所示:

这样,我们就能获得每个月份下的博客总数有多少,基本实现了按照月份归档。
#### (2)MySQL 查询每条具体的博客,将时间一并查询出来
对应的 SQL 语句2:
**注意:这里实际只需要查询每条博客记录的具体时间即可,对应的年份和月份我们都可以在 Thymeleaf 模版中通过具体时间获得年份,月份。当然,我们也可以如下所示,在 SQL 层面查询出年份和月份。**
```
select year(javaweb_myblog.blog.createtime) as 'year',
month(javaweb_myblog.blog.createtime) as 'month',
javaweb_myblog.blog.createtime as 'time',
javaweb_myblog.blog.btitle as 'title',
javaweb_myblog.blog.bid as 'bid'
from javaweb_myblog.blog order by javaweb_myblog.blog.createtime desc;
```
查询结果如图所示:

到这里我们的思路就很明确了:
**第1个查询可以将博客按照月份归档,那么第2个查询的结果可以和第1个查询的结果做比较,即在 Thymeleaf 中显示时,如果月份相同,即显示该条记录,表明该条记录属于当前月份下分布的博客。**
那么下面我们给出完整的代码
### 完整代码展示
代码只给出了关键代码,service和dao层的代码没有贴出来(dao层代码可以参考前面给出的 SQL 语句执行编写);不关键的 html 代码也没有贴出来。
> Controller 层
```
@GetMapping("/Timeline")
public String toTimeline(Model model) {
List<Map<String, Object>> listWithCount = blogService.queryTimeLingWithCount();
System.out.println("listWithCount size = " + listWithCount.size()); // 对应着第1个查询语句
List<Map<String, Object>> listWithOutCount = blogService.queryTimeLingWithOutCount();
System.out.println("listWithOutCount size = " + listWithOutCount.size()); // 对应着第2个查询语句
model.addAttribute("listWithCount", listWithCount);
model.addAttribute("listWithOutCount", listWithOutCount);
return "timeline";
}
```
> 前端HTML页面
```
<div class="history-date" th:each="itemWithCount:${listWithCount}">
<ul>
<h2 class="first">
<a href="#" th:text="${itemWithCount.get('year')} + '年' + ${itemWithCount.get('month')} + '月' + '(' + ${itemWithCount.get('count')} + ')'"></a>
</h2>
<!-- 关键代码(这个 if 判断是关键):查询1和查询2比较,要求月份和年份相同,这将这条记录展示出来 -->
<li class="green" th:each="itemWithOutCount:${listWithOutCount}"
th:if="${itemWithOutCount.get('year')} == ${itemWithCount.get('year')} and ${itemWithOutCount.get('month')} == ${itemWithCount.get('month')}">
<h3>
[[ ${itemWithOutCount.get('month')} + '月' + ${#dates.day(itemWithOutCount.get('time')) + '日'} ]]
<span th:text="${#dates.hour(itemWithOutCount.get('time'))} + '时' + ${#dates.minute(itemWithOutCount.get('time')) + '分'}"></span>
</h3>
<dl>
<dt>
<a th:href="@{/article/} + ${itemWithOutCount.get('bid')}" th:text="${itemWithOutCount.get('title')}" target="_blank"></a>
</dt>
</dl>
</li>
</ul>
</div>
```