Fork me on GitHub

JackLin的博客

当前位置:首页 > 标签

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
  • 176
  • SpringBoot
## 项目介绍 参加阿里云AI训练营的第5天,也是最后一天了。今天是创意日,想做什么就做什么,没有题目约束。 那么我打算做一个简单的识别图片中的文字的应用,并部署上线,以后也可能经常会用到。 ## 项目用到的文档地址 阿里云达摩院视觉开放平台:https://vision.aliyun.com/ 阿里云视觉开放平台 “通用识别” 地址:https://help.aliyun.com/document_detail/151896.html?spm=a2c4g.11186623.6.620.44da1ded5yuZbF ## 项目开始 ### (1)说明 经过前面几天的训练,自我感觉良好。已经对阿里云的视觉开放平台比较熟悉了,也熟悉使用提供的API的一些基础的步骤,感觉就是这些套路。 那么就不啰嗦了,直接上代码。由于只用到了一个场景,所以代码也比较简单,记得测试前要到阿里云开启 “文字识别服务” 哦! ![](http://image.linkaiblog.top/image-20200608223154211.png) ### (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 里面,我们从这里面获取数据即可。 ![](http://image.linkaiblog.top/image-20200608223740407.png) 同时,经过测试,文字识别的过程是一行一行识别的,每行识别出来的文字都成为封装在数组的一个元素里面,所以我们定义一个 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> ``` ## 测试 先进入主页面,选择图片并点击上传按钮: ![](http://image.linkaiblog.top/image-20200608224523080.png) 识别出来的结果: ![](http://image.linkaiblog.top/_2020060822581958SS.png) ## 阿里云高校计划 最后在贴一张阿里云的广告:”阿里云高校计划“,快来加入我们吧! ![](http://image.linkaiblog.top/image-20200606222043666.png)

阿里云AI训练营_Day04_车辆检测系统

  • 2020-06-08
  • 172
  • 阿里巴巴
## 项目介绍 参加阿里云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 语句区分各种损伤的具体情况。 ![](http://image.linkaiblog.top/image-20200608180242183.png) > 具体代码: ```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张图片 ![](http://image.linkaiblog.top/image-20200608185634408.png) 然后点击 upload 上传并查看结果: ![](http://image.linkaiblog.top/image-20200608190502181.png) 数据库里面的结果: ![](http://image.linkaiblog.top/image-20200608190901927.png) ## 阿里云高校计划 最后在贴一张阿里云的广告:”阿里云高校计划“,快来加入我们吧! ![](http://image.linkaiblog.top/image-20200606222043666.png)

阿里云AI训练营_Day03_电子相册

  • 2020-06-06
  • 191
  • 阿里巴巴
## 项目介绍 参加阿里云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> ``` - 记得要开通对应场景的服务,否则就会报这样的错误 ![](http://image.linkaiblog.top/image-20200606212319897.png) ### (2)创建并初始化 Client,Config 和之前的套路一样,我们需要创建对应场景的 Client 和 Config 类,关于这部分的详细解释可以参照我的上一篇博客 【阿里云AI训练营-_Day02-身份证识别】。 关于 Config 的属性配置,文档中也详细说明了,我们要根据不同的场景赋予不同的值。 ![](http://image.linkaiblog.top/image-20200606213550538.png) ----- 由于我们用到了 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 中获取数据了。 ![](http://image.linkaiblog.top/image-20200606214950120.png) 但是实际上并没有想象中的怎么美好,我看了一下示例代码,发现这个 Data 里面只有一个名字叫做 elements 的对象数组,数组里面的对象是 RecognizeExpressionResponseDataElements 类的对象;然后在这个 RecognizeExpressionResponseDataElements 类里面只能够获取够获取 3 个属性: ![](http://image.linkaiblog.top/image-20200606220000314.png) 这明显和文档中的不符合啊,我个人感觉可能是依赖版本的问题,示例代码中的版本可能太低了,而文档是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 ![](http://image.linkaiblog.top/image-20200606220712373.png) > 场景识别关键代码,传入的参数为从图片获取的输入流 ```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(); } } ``` > 本地图片 ![](http://image.linkaiblog.top/image-20200606221441905.png) 最中的识别结果: ![](http://image.linkaiblog.top/image-20200606212453730.png) ## 关于 **由于前端代码出现了Bug,所以只测试了后端调用服务API的关键代码,主要是结合代码和文档一起分析,这样才能一通百通。额,好吧,最主要的原因是时间不够,月底就期末考了,能抽出来的时间不是很多,所以不想把时间花费在解决前端的Bug上面。** ## 友情推广 贴一下阿里云的活动,有兴趣的小伙伴可以参加一下 ![](http://image.linkaiblog.top/image-20200606222043666.png)

阿里云AI训练营_Day02_文字识别_身份证识别

  • 2020-06-05
  • 151
  • 阿里巴巴
## 项目介绍 近期参加了阿里云的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中的配置) ### 开通服务 按照文档中的说明,开通服务,不出意外的话很容易就开通成功了,如下图所示: ![](http://image.linkaiblog.top/image-20200530101808285.png) ### 使用 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,具体如下: ![](http://image.linkaiblog.top/image-20200605153228838.png) 改进之后,我们关于 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 代表了不同的场景,即不同的场景使用的类和方法都有所不同,不过名字都类似。具体的场景的参数也会有略微的不同,可以参看文档中关于该场景的详细描述。 ![](http://image.linkaiblog.top/image-20200530110245328.png) #### (2)具体的使用 下面以身份证识别为例,看下文档中是如何描述的: ![](http://image.linkaiblog.top/image-20200605154824944.png) 实际上在这里还可以进行调试,快速体验一下身份证识别: ![](http://image.linkaiblog.top/image-20200605163104006.png) 我们在IDEA中查看查找相关的类 ![](http://image.linkaiblog.top/image-20200530110518906.png) 实际上这些场景对应的类都可以在我们添加Maven依赖之后导入的jar包中看到: ![](http://image.linkaiblog.top/image-20200605155108707.png) 好了,看完了文档之后,我就使用身份证识别场景的类和方法对文档中的代码进行替换,效果如下 ``` 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(); } } ``` 由于我的图片打了马赛克,所以只能识别出部分结果,结果如下所示 ![](http://image.linkaiblog.top/image-20200605155952932.png) ## 关于使用文档中的配置 我照着文档一步一步来,和文档一样的代码,依赖也是一样的,莫名其妙会报错。不知道是我自己的原因还是文档本来就有错,反正我的代码就是跑不起来,这里展示一下我的报错的代码,希望有小伙伴可以发现问题并解决。 > 初始化部分 ----> 关键就是没有将 config.endpointType="internal"; 注释掉 ![](http://image.linkaiblog.top/image-20200605161121027.png) > 我引入的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> ``` > 报错 ![](http://image.linkaiblog.top/image-20200605161231544.png)

(转载)接近8000字的Spring/SpringBoot常用注解总结!安排!

  • 2020-05-18
  • 143
  • SpringBoot
非原创 > 原文作者:SnailClimb 本文转载自:https://www.imooc.com/article/304149 ## 前言 大家好,我是 Guide 哥!这是我的 221 篇优质原创文章。如需转载,请在文首注明地址,蟹蟹! 本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:https://github.com/Snailclimb/JavaGuide 可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法,掌握搞懂,使用 SpringBoot 来开发项目基本没啥大问题了! 整个目录如下,内容有点多: ![](http://img3.sycdn.imooc.com/5eaa987f0001279810802201.jpg) 为什么要写这篇文章? 最近看到网上有一篇关于 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 类上。 ![](http://img1.sycdn.imooc.com/5eaa988200014dbf10780338.jpg) 需要注意的是:一个请求方法只可以有一个`@ 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
  • 143
  • SpringBoot
今天记录一下博客按时间归档功能的思路以及具体的实现步骤。 先上效果图: ![](http://image.linkaiblog.top/时间轴_2020051610490649SS.png) **主要的解决过程有 2 个:** - 第 1 个是通过 MySQL 的查询语句,统计不同时间段内的博客数量及内容,然后返回数据给前端。 - 第 2 个就是结合前端的 Thymeleaf 模版引擎提供的一些日期格式化工具,根据后端提供的数据,实现博客的按时间归档。 **这里仅仅提供我的一种思路。因为每个人的数据表的结构不太一样,这里只能提供一种思路供大家参考。** **只要思想不滑坡,办法总比困哪多!** ### 通过 MySQL 查询将数据归档 首先,我们存在数据库中的时间是精确到秒的,如下图所示: ![](http://image.linkaiblog.top/时间轴_2020051610431343SS.png) #### (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 ``` 查询结果如下图所示: ![](http://image.linkaiblog.top/时间轴_2020050311253025SS.png) 这样,我们就能获得每个月份下的博客总数有多少,基本实现了按照月份归档。 #### (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; ``` 查询结果如图所示: ![](http://image.linkaiblog.top/时间轴_202005031113001SS.png) 到这里我们的思路就很明确了: **第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> ```