ProductController.java


package com.example.controller;


import com.example.service.IProductService;
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.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author dd
 * @since 2024-05-07
 *
 */

//测试:http://localhost:8080/es_demo/product/kw/纯棉四件套/page/1
@RestController
@RequestMapping("product")
public class ProductController {

    @Autowired
    private IProductService productService;

    // 将mysql中的product数据保存到es中
    @GetMapping("saveProductToES")
    public String saveProductToES(){
        productService.saveProductFromDBToES();
        return "ok";
    }


    //在生产环境中,直接操作Elasticsearch通常需要更复杂的错误处理和事务管理逻辑,以确保数据的一致性和完整性。
    @GetMapping("delete/{productId}")
    public String deleteProduct(@PathVariable Integer productId){
        return productService.deleteProduct(productId);
    }



    // 搜索引擎:输入关键字
    //返回 商品信息 + 页码信息
    @GetMapping("kw/{kw}/page/{pageNum}")
    public Map<String, Object> getByKeyword(@PathVariable("kw") String keyword,
                                            @PathVariable("pageNum") Integer pageNum){
        if(pageNum == null)
            pageNum = 1;

        Map<String, Object> result = productService.getByNameAndInfo(keyword, keyword, pageNum);
        return result;
    }

}



Product.java


package com.example.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * <p>
 * 
 * </p>
 *
 * @author dd
 * @since 2024-05-07
 */



//    @Field(index = false, store = true,
//            type = FieldType.Date,format = DateFormat.custom,
//            pattern = "yyyy-MM-dd HH:mm:ss")

@Document(indexName = "myproduct")
public class Product implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "product_id",type = IdType.AUTO)
    @Id
    private Integer productId;
    @Field(type = FieldType.Keyword)
    private String productName;

    private BigDecimal productPrice;

    private String productImg;

    private Integer productCount;

    @Field( type = FieldType.Date,name = "update_time",format = {},
            pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd'T'HH:mm:ss'+08:00' || strict_date_opotional_time || epoch_millis")
    private LocalDateTime createTime;


    @Field( type = FieldType.Date,name = "update_time",format = {},
            pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd'T'HH:mm:ss'+08:00' || strict_date_opotional_time || epoch_millis")

    private LocalDateTime updateTime;

    @Field(type = FieldType.Text,analyzer = "ik_smart",searchAnalyzer = "ik_max_word")
    private String productInfo;

    public Integer getProductId() {
        return productId;
    }

    public void setProductId(Integer productId) {
        this.productId = productId;
    }
    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }
    public BigDecimal getProductPrice() {
        return productPrice;
    }

    public void setProductPrice(BigDecimal productPrice) {
        this.productPrice = productPrice;
    }
    public String getProductImg() {
        return productImg;
    }

    public void setProductImg(String productImg) {
        this.productImg = productImg;
    }
    public Integer getProductCount() {
        return productCount;
    }

    public void setProductCount(Integer productCount) {
        this.productCount = productCount;
    }
    public LocalDateTime getCreateTime() {
        return createTime;
    }

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }
    public LocalDateTime getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }
    public String getProductInfo() {
        return productInfo;
    }

    public void setProductInfo(String productInfo) {
        this.productInfo = productInfo;
    }

    @Override
    public String toString() {
        return "Product{" +
            "productId=" + productId +
            ", productName=" + productName +
            ", productPrice=" + productPrice +
            ", productImg=" + productImg +
            ", productCount=" + productCount +
            ", createTime=" + createTime +
            ", updateTime=" + updateTime +
            ", productInfo=" + productInfo +
        "}";
    }
}



ElasticsearchSyncListener.java


package com.example.listener;

import com.example.service.impl.ProductDeletedEvent;
import com.example.mapper.ProductElasticSearchMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class ElasticsearchSyncListener {

    @Autowired
    private ProductElasticSearchMapper productElasticSearchMapper;

    @EventListener
    public void handleProductDeletedEvent(ProductDeletedEvent event) {
        Integer productId = event.getProductId();
        // 删除Elasticsearch中的记录
        productElasticSearchMapper.deleteById(productId);
    }
}

ProductElasticSearchMapper.java


package com.example.mapper;

import com.example.entity.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;


import java.util.List;

@Repository //父接口已经 实现对象在es中的基础CRUD操作
public interface ProductElasticSearchMapper extends ElasticsearchRepository<Product,Integer> {

    //方法名必须是:findBy + pojo类属性名(首字母大写)

    public List<Product> findByProductName(String productName);

    public List<Product> findByProductInfo(String productInfo);


}


ProductMapper.java


package com.example.mapper;

import com.example.entity.Product;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author dd
 * @since 2024-05-07
 */

public interface ProductMapper extends BaseMapper<Product> {





}


ProductDeletedEvent.java


package com.example.service.impl;


import org.springframework.context.ApplicationEvent;


//定义一个事件类来表示删除操作
public class ProductDeletedEvent extends ApplicationEvent {

    private final Integer productId;

    public ProductDeletedEvent(Object source, Integer productId) {
        super(source);
        this.productId = productId;
    }

    public Integer getProductId() {
        return productId;
    }
}


ProductServiceImpl.java



package com.example.service.impl;

import com.example.entity.Product;
import com.example.mapper.ProductElasticSearchMapper;
import com.example.mapper.ProductMapper;
import com.example.service.IProductService;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.*;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;





//空值检查:
//在处理高亮字段时,已经对highlightFields.get("productName")和highlightFields.get("productInfo")进行了空值检查,确保Product类的getProductName和getProductInfo方法也能处理可能的空值情况。
//
//字段名匹配:
//确保在Elasticsearch中定义的字段名(如productName和productInfo)与在Java代码中使用的字段名相匹配。
//
//性能考虑:
//如果productMapper.selectList(null)返回大量的产品数据,并且一次性将它们全部保存到Elasticsearch中,这可能会导致性能问题。考虑分批保存或使用Elasticsearch的批量API来提高性能。
//
//查询字符串分析:
//使用了queryStringQuery,这可能会对输入的productName进行分词查询。如果希望精确匹配整个字段值,而不是基于分词,可能需要使用termQuery或matchPhraseQuery。但是,由于处理的是全文搜索场景,queryStringQuery通常是合适的。

//安全性:
//如果productName或productInfo参数来自不受信任的源(如用户输入),请确保对它们进行适当的验证和清理,以防止SQL注入或Elasticsearch注入攻击。不过,由于是在应用层进行Elasticsearch查询,而不是直接拼接查询字符串,因此SQL注入的风险较低。但仍然需要注意Elasticsearch注入的风险。

//返回类型:
//返回了一个包含分页信息和搜索结果的Map。确保前端或调用此服务的客户端知道如何解析这个Map。考虑定义一个DTO(数据传输对象)来封装返回的数据,以提供更明确的契约。






/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author dd
 * @since 2024-05-07
 */
@Service
public class ProductServiceImpl  implements IProductService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private ProductElasticSearchMapper productElasticSearchMapper;

    @Autowired
    private ElasticsearchRestTemplate restTemplate;


    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;


    // 把mysql中的数据查询出来,保存商品数据到es中



    //当前代码逻辑中的saveProductFromDBToES方法设计为一个全量同步操作,它会从MySQL数据库中查询所有产品记录,并将它们全部保存到Elasticsearch中。因此,当从数据库中删除一条记录时,该方法不会意识到有记录被删除,因为它仅仅是再次执行了全量查询和保存操作。
    //
    //为了解决这个问题,可以采取以下几种策略之一:
    //
    //增量同步:设计一个机制来跟踪数据库中的更改(如使用数据库的binlog日志),并仅同步自上次同步以来发生的更改。这通常比较复杂,但可以实现实时或近实时的数据同步。
    //
    //定期全量同步:可以定期(如每小时、每天)运行saveProductFromDBToES方法来进行全量同步。这种方法比较简单,但可能会导致数据在一定时间窗口内不同步。
    //
    //删除操作同步:在应用程序中添加逻辑,以便在数据库记录被删除时,也在Elasticsearch中删除相应的文档。这通常需要在数据库删除操作的地方添加额外的代码或使用触发器。
    //
    //使用监听器或事件驱动:如果使用的是支持事件驱动或变更数据捕获(CDC)的数据库或框架,可以配置监听器来捕获数据库更改事件,并据此更新Elasticsearch中的数据。

    @Override
    public boolean saveProductFromDBToES() {
        //1. select  product  from mysql
        List<Product> productList =    productMapper.selectList(null);

        //2.  save to es
        Iterable<Product> products = productElasticSearchMapper.saveAll(productList);

        return true;
    }


    @Override
    public String deleteProduct(Integer productId) {
        // 这里有一个方法来删除数据库中的记录
        int rows = productMapper.deleteById(productId);




        if(rows>0){
            // 发布删除事件
            applicationEventPublisher.publishEvent(new ProductDeletedEvent(this, productId));
            return "删除成功";
        }
        return "删除失败";
    }


    //创建一个事件监听器来监听ProductDeletedEvent并执行删除Elasticsearch中的记录的操作
    //ElasticsearchSyncListener类是一个Spring组件,它使用@EventListener注解来标记handleProductDeletedEvent方法作为事件监听器。当ProductDeletedEvent事件被发布时,这个方法将被调用,并执行删除Elasticsearch中相应记录的操作。
    //
    //请注意,需要确保ApplicationEventPublisher和ProductElasticSearchMapper都被正确地注入到相应的类中。此外,可能还需要配置Spring的事件支持,但通常情况下,在Spring Boot应用程序中,事件支持是默认启用的。
    //
    //最后,当调用ProductService的deleteProduct方法时,它将删除数据库中的记录并发布一个ProductDeletedEvent事件。这个事件将被ElasticsearchSyncListener监听并处理,从而删除Elasticsearch中的相应记录。









    // 实现接口方法,基于Elasticsearch的全文搜索,包括分词和高亮功能
    @Override
    public Map<String,Object> getByNameAndInfo(String productName, String productInfo, Integer pageNum) {

        //设置分页信息,每页显示3条记录,pageNum-1是因为页码通常从1开始,而数组索引从0开始
        PageRequest page = PageRequest.of(pageNum - 1, 3);

        //创建一个布尔查询构建器,用于组合多个查询条件
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

        //如果productName不为空,则创建一个基于productName的查询,并将其添加到布尔查询中
        if(productName !=null){
            QueryBuilder queryBuilder = QueryBuilders.queryStringQuery(productName);
            boolQueryBuilder.must(queryBuilder);
        }else{
            //如果productName为空且productInfo不为空,则创建一个基于productInfo的匹配查询,并将其添加到布尔查询中
            if(productInfo !=null)
                boolQueryBuilder.must(new MatchQueryBuilder("productInfo",productInfo));
        }

        //根据商品价格降序排序
        SortBuilder sortBuilder = SortBuilders.fieldSort("productPrice").order(SortOrder.DESC);
        //FieldSortBuilder priceSort = SortBuilders.fieldSort("productPrice"); // 按产品价格排序(需要指定升序或降序)


        //构建高亮查询,设置高亮字段和高亮标签
        NativeSearchQueryBuilder builder=new NativeSearchQueryBuilder();
        NativeSearchQuery query=builder
                .withQuery(boolQueryBuilder)
                .withPageable(page)
                .withSort(sortBuilder)
                .withHighlightFields(
                        new HighlightBuilder.Field("productInfo"),
                        new HighlightBuilder.Field("productName"))
                .withHighlightBuilder(new HighlightBuilder()
                        .preTags("<span style='color:red'>")
                        .postTags("</span>"))
                .build();

        // 执行查询,获取搜索结果
        SearchHits<Product> search = restTemplate.search(query, Product.class);
        List<Product> productList= new ArrayList<>();

        //遍历搜索结果,处理高亮信息,并将处理后的产品添加到列表中
        for(SearchHit<Product> searchHit:search){
            //获取高亮字段信息
            Map<String ,List<String>> highlightFields = searchHit.getHighlightFields();

            //将高亮的内容填充到content中
            //处理产品名称和产品信息的高亮信息,并将其设置回产品对象中
            String highLightProName = highlightFields.get("productName") ==null ?searchHit.getContent().getProductName() :highlightFields.get("productName").get(0);

            String highLightProInfo = highlightFields.get("productInfo") ==null ?searchHit.getContent().getProductInfo() :highlightFields.get("productInfo").get(0);
            searchHit.getContent().setProductName(highLightProName );
            searchHit.getContent().setProductInfo(highLightProInfo);

            //将处理后的产品添加到列表中
            productList.add(searchHit.getContent());

        }
        //创建SearchPage对象,用于获取分页信息(假设SearchHitSupport是有效的)
        SearchPage<Product> searchPage= SearchHitSupport.searchPageFor(search,query.getPageable());
        //总记录数
        long totalElements=searchPage.getTotalElements();
        //总页数
        int totalPages=searchPage.getTotalPages();
        //当前页数
        int currentPageForDisplay=searchPage.getPageable().getPageNumber() + 1;// 通常前端期望页码从1开始

        System.out.println(currentPageForDisplay);


        //创建一个Map对象,用于返回查询结果和分页信息
        Map<String,Object> map=new HashMap<>();
        map.put("totalElements",totalElements);  // 总记录数
        map.put("totalPages",totalPages);  //总页数
        map.put("currentPage",currentPageForDisplay);  // 当前页码
        map.put("productList",productList);  // 商品数据信息


        //返回查询结果和分页信息的Map对象
        return map;

    }

}


SyncProductService.java


package com.example.service.impl;

import com.example.service.IProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class SyncProductService {

    @Autowired
    private IProductService productService;

    @Scheduled(fixedRate = 2000) // 单位是毫秒
    public void syncProductsFromDBToES() {
        productService.saveProductFromDBToES();
    }
}


IProductService.java



package com.example.service;

import com.example.entity.Product;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.Map;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author dd
 * @since 2024-05-07
 */
public interface IProductService  {

    public boolean saveProductFromDBToES();


    public String deleteProduct(Integer productId);


    // 搜索引擎中的关键字
    // 返回满足条件的商品的数据+分页信息
    public Map<String,Object> getByNameAndInfo(String productName, String productInfo, Integer pageNum);

}


ElasticSearchSpringDemoApplication.java



package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@MapperScan("com.example.mapper")
@EnableScheduling
public class ElasticSearchSpringDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(ElasticSearchSpringDemoApplication.class, args);
    }

}


ServletInitializer.java



package com.example;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(ElasticSearchSpringDemoApplication.class);
    }

}


product.sql


/*
 Navicat Premium Data Transfer

 Source Server         : rootWindows
 Source Server Type    : MySQL
 Source Server Version : 80036
 Source Host           : localhost:3306
 Source Schema         : cloud_product_db

 Target Server Type    : MySQL
 Target Server Version : 80036
 File Encoding         : 65001

 Date: 07/05/2024 18:49:44
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for product
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product`  (
  `product_id` int(0) NOT NULL AUTO_INCREMENT,
  `product_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `product_price` decimal(10, 2) NULL DEFAULT NULL,
  `product_img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `product_count` int(0) NULL DEFAULT NULL,
  `create_time` datetime(0) NULL DEFAULT NULL,
  `update_time` datetime(0) NULL DEFAULT NULL,
  `product_info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`product_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of product
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;




同步


增量同步:设计一个机制来跟踪数据库中的更改(如使用数据库的binlog日志),并仅同步自上次同步以来发生的更改。这通常比较复杂,但可以实现实时或近实时的数据同步。

定期全量同步:可以定期(如每小时、每天)运行saveProductFromDBToES方法来进行全量同步。这种方法比较简单,但可能会导致数据在一定时间窗口内不同步。

删除操作同步:在应用程序中添加逻辑,以便在数据库记录被删除时,也在Elasticsearch中删除相应的文档。这通常需要在数据库删除操作的地方添加额外的代码或使用触发器。

使用监听器或事件驱动:如果使用的是支持事件驱动或变更数据捕获(CDC)的数据库或框架,可以配置监听器来捕获数据库更改事件,并据此更新Elasticsearch中的数据。

ProductMapper.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="com.example.mapper.ProductMapper">

</mapper>




application.yaml


# ???
server:
  servlet:
    context-path: /es_demo

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud_product_db?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  elasticsearch:
    uris: localhost:9200
    connection-timeout: 5s
    socket-timeout: 30s





pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>elasticSearchSpringDemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>elasticSearchSpringDemo</name>
    <description>elasticSearchSpringDemo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>




        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generate -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>


        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>


        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>8.11.1</version>
        </dependency>






        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>




05-11 14:27