【Spring Cloud】新闻头条微服务项目:文章内容安全审核(新增DFA+OCR过滤敏感词需求)

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,118元/月
云原生网关 MSE Higress,422元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 对文章审核新增加了需求----敏感词过滤,介绍了基于NFA的文本敏感词过滤及OCR提取图片文字的原理及实现,最后将文章审核代码做了合并。

一:需求新增

1.需求分析

       前面我们的想法是调用腾讯云的内容安全对文章的文本信息及图片进行安全检测,通过则将文章发布。但是这时候有个缺点,就是审核不能过滤一些敏感词,比如针孔摄像头、高额贷款、论文代写等,我们这时候就需要自己维护一套敏感词系统用来对文章进行检测。对于这套敏感词系统,我们可以在管理端进行删除增添的操作。

image.gif编辑

       除了要对文本进行敏感词过滤之外,由于图片中可能还会存在敏感词,所以我们也需要对图片中的文字进行检测,使用到的技术是OCR。

2.技术选型

(1)文本

方案 说明
数据库模糊查询 效率太低
String.indexOf("")查找 数据库量大的话也是比较慢
全文检索 分词再匹配
DFA算法 确定有穷自动机(一种数据结构)

在文字过滤系统中,为了能够应付较高的并发,需要尽量减少计算。采用DFA算法基本没有什么计算,基本是状态转移。所以选用最后一种方案。

(2)图片

方案 说明
百度OCR 收费
Tesseract-OCR Google维护的开源OCR引擎,支持Java,Python等语言调用
Tess4J 封装了Tesseract-OCR ,支持Java调用

Tess4J封装了Tesseract-OCR,Java调用起来方便,因此选用此方案。

3.DFA

(1)原理分析

DFA全称为:Deterministic Finite Automaton,即确定有穷自动机。

其特征为:有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。但不同于不确定的有限自动机,DFA中不会有从同一状态出发的两条边标志有相同的符号。image.gif编辑

简单点说就是,它是是通过event和当前的state得到下一个state,即event+state=nextstate。理解为系统中有多个节点,通过传递进入的event,来确定走哪个路由至另一个节点,而节点是有限的。

存储:一次性的把所有的敏感词存储到了多个map中,就是下图表示这种结构

敏感词:冰毒、大麻、大坏蛋

image.gif编辑        其处理过程为先检索文章中每一个字符,然后查看该字符是否包含在敏感词库中,假如包含在敏感词库中则查看该字符在敏感词库中isEnd是否为0,假如不为0则继续检索下一个字符,然后继续查看该字符是否存在于敏感词库中,假如存在则查看isEnd是否为1,假如为1则说明是敏感词。举个例子,假如有这样一句话:“我不卖冰毒”,这时候就会先检索“我”字,发现不在敏感词库,然后继续检索下一个,直至到“冰”,这时候“冰”在敏感词库中,但是isEnd为0,继续检索,“毒”在敏感词库且isEnd为1,这时候检测到敏感词“冰毒”。

(2)代码实现

①创建敏感词表:

image.gif编辑

实体类:

package com.my.model.wemedia.pojos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
 * <p>
 * 敏感词信息表
 * </p>
 *
 * @author itheima
 */
@Data
@TableName("wm_sensitive")
public class WmSensitive implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Integer id;
    /**
     * 敏感词
     */
    @TableField("sensitives")
    private String sensitives;
    /**
     * 创建时间
     */
    @TableField("created_time")
    private Date createdTime;
}

image.gif

②创建对应Mapper接口

③导入工具类

package com.my.utils.common;
import java.util.*;
public class SensitiveWordUtil {
    public static Map<String, Object> dictionaryMap = new HashMap<>();
    /**
     * 生成关键词字典库
     * @param words
     * @return
     */
    public static void initMap(Collection<String> words) {
        if (words == null) {
            System.out.println("敏感词列表不能为空");
            return ;
        }
        // map初始长度words.size(),整个字典库的入口字数(小于words.size(),因为不同的词可能会有相同的首字)
        Map<String, Object> map = new HashMap<>(words.size());
        // 遍历过程中当前层次的数据
        Map<String, Object> curMap = null;
        Iterator<String> iterator = words.iterator();
        while (iterator.hasNext()) {
            String word = iterator.next();
            curMap = map;
            int len = word.length();
            for (int i =0; i < len; i++) {
                // 遍历每个词的字
                String key = String.valueOf(word.charAt(i));
                // 当前字在当前层是否存在, 不存在则新建, 当前层数据指向下一个节点, 继续判断是否存在数据
                Map<String, Object> wordMap = (Map<String, Object>) curMap.get(key);
                if (wordMap == null) {
                    // 每个节点存在两个数据: 下一个节点和isEnd(是否结束标志)
                    wordMap = new HashMap<>(2);
                    wordMap.put("isEnd", "0");
                    curMap.put(key, wordMap);
                }
                curMap = wordMap;
                // 如果当前字是词的最后一个字,则将isEnd标志置1
                if (i == len -1) {
                    curMap.put("isEnd", "1");
                }
            }
        }
        dictionaryMap = map;
    }
    /**
     * 搜索文本中某个文字是否匹配关键词
     * @param text
     * @param beginIndex
     * @return
     */
    private static int checkWord(String text, int beginIndex) {
        if (dictionaryMap == null) {
            throw new RuntimeException("字典不能为空");
        }
        boolean isEnd = false;
        int wordLength = 0;
        Map<String, Object> curMap = dictionaryMap;
        int len = text.length();
        // 从文本的第beginIndex开始匹配
        for (int i = beginIndex; i < len; i++) {
            String key = String.valueOf(text.charAt(i));
            // 获取当前key的下一个节点
            curMap = (Map<String, Object>) curMap.get(key);
            if (curMap == null) {
                break;
            } else {
                wordLength ++;
                if ("1".equals(curMap.get("isEnd"))) {
                    isEnd = true;
                }
            }
        }
        if (!isEnd) {
            wordLength = 0;
        }
        return wordLength;
    }
    /**
     * 获取匹配的关键词和命中次数
     * @param text
     * @return
     */
    public static Map<String, Integer> matchWords(String text) {
        Map<String, Integer> wordMap = new HashMap<>();
        int len = text.length();
        for (int i = 0; i < len; i++) {
            int wordLength = checkWord(text, i);
            if (wordLength > 0) {
                String word = text.substring(i, i + wordLength);
                // 添加关键词匹配次数
                if (wordMap.containsKey(word)) {
                    wordMap.put(word, wordMap.get(word) + 1);
                } else {
                    wordMap.put(word, 1);
                }
                i += wordLength - 1;
            }
        }
        return wordMap;
    }
/*     public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("法轮");
        list.add("法轮功");
        list.add("冰毒");
        initMap(list);
        String content="我是一个好人,并不会卖冰毒,也不操练法轮功,我真的不卖冰毒";
        Map<String, Integer> map = matchWords(content);
        System.out.println(map);
    } */
}

image.gif

4.OCR

(1)原理分析

OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程。

(2)代码实现

①在tbug-headlines-common中导入如下依赖

<dependency>
    <groupId>net.sourceforge.tess4j</groupId>
    <artifactId>tess4j</artifactId>
    <version>4.1.1</version>
</dependency>

image.gif

②封装成工具类

package com.my.common.tess4j;
import lombok.Getter;
import lombok.Setter;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.awt.image.BufferedImage;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "tess4j")
public class Tess4jClient {
    private String dataPath;
    private String language;
    public String doOCR(BufferedImage image) throws TesseractException {
        //创建Tesseract对象
        ITesseract tesseract = new Tesseract();
        //设置字体库路径
        tesseract.setDatapath(dataPath);
        //中文识别
        tesseract.setLanguage(language);
        //执行ocr识别
        String result = tesseract.doOCR(image);
        //替换回车和tal键  使结果为一行
        result = result.replaceAll("\\r|\\n", "-").replaceAll(" ", "");
        return result;
    }
}

image.gif

③在spring.factories配置中添加该类,完整如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.my.common.exception.ExceptionCatch,\
  com.my.common.swagger.SwaggerConfiguration,\
  com.my.common.swagger.Swagger2Configuration,\
  com.my.common.tencentcloud.TextDetection,\
  com.my.common.tencentcloud.ImageDetection,\
  com.my.common.tess4j.Tess4jClient,\

image.gif

④在tbug-headlines-wemedia中添加如下配置

tess4j:
  data-path: D:\headlinesPro\tessdata
  language: chi_sim

image.gif

注意文件路径要填自己的路径

二:完整审核流程

1.审核类

package com.my.wemedia.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.my.apis.article.IArticleClient;
import com.my.common.tencentcloud.ImageDetection;
import com.my.common.tencentcloud.TextDetection;
import com.my.common.tess4j.Tess4jClient;
import com.my.file.service.FileStorageService;
import com.my.model.article.dtos.ArticleDto;
import com.my.model.common.dtos.ResponseResult;
import com.my.model.wemedia.pojos.WmChannel;
import com.my.model.wemedia.pojos.WmNews;
import com.my.model.wemedia.pojos.WmSensitive;
import com.my.model.wemedia.pojos.WmUser;
import com.my.utils.common.SensitiveWordUtil;
import com.my.wemedia.mapper.WmSensitiveMapper;
import com.my.wemedia.service.WmAutoScanService;
import com.my.wemedia.service.WmChannelService;
import com.my.wemedia.service.WmNewsService;
import com.my.wemedia.service.WmUserService;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@Transactional
public class WmAutoScanServiceImpl implements WmAutoScanService {
    @Autowired
    private WmNewsService wmNewsService;
    @Autowired
    private TextDetection textDetection;
    @Autowired
    private ImageDetection imageDetection;
    /**
     * 自动审核文章文本及图片
     * @param id
     */
    @Override
    @Async //表明这是一个异步方法
    public void AutoScanTextAndImage(Integer id) throws Exception {
        log.info("开始进行文章审核...");
        // Thread.sleep(300);    //休眠300毫秒,以保证能够获取到数据库中的数据
        WmNews wmNews = wmNewsService.getById(id);
        if(wmNews == null) {
            throw new RuntimeException("WmAutoScanServiceImpl-文章信息不存在");
        }
        if(wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) {
            //1.提取文章文本及图片
            Map<String,Object> map = getTextAndImages(wmNews);
            //2.检测文本
            //2.1提取文本
            String content = ((StringBuilder) map.get("content")).toString();
            //2.2自管理敏感词过滤
            boolean SHandleResult = handleSensitiveScan(content,wmNews);
            if(!SHandleResult) return;
            //2.3调用腾讯云进行文本检测
            Boolean THandleResult = handleTextScan(content, wmNews);
            if(!THandleResult) return;
            //3.检测图片
            //3.1提取图片
            List<String> imageUrl = (List<String>) map.get("images");
            //3.2调用腾讯云对图片进行检测
            Boolean IHandleResult = handleImageScan(imageUrl, wmNews);
            if(!IHandleResult) return;
            //4,审核成功
            //4.1保存文章
            log.info("检测到文章无违规内容");
            ResponseResult responseResult = saveAppArticle(wmNews);
            if(!responseResult.getCode().equals(200)) {
                throw new RuntimeException("WmAutoScanServiceImpl-文章审核,保存文章失败");
            }
            //4.2回填article_id
            wmNews.setArticleId((Long) responseResult.getData());
            wmNews.setStatus(WmNews.Status.PUBLISHED.getCode());
            wmNews.setReason("审核成功");
            wmNewsService.updateById(wmNews);
        }
    }
    /**
     * 提取文章文本及图片信息
     * @param wmNews
     * @return
     */
    private Map<String,Object> getTextAndImages(WmNews wmNews) {
        //存储纯文本
        StringBuilder text = new StringBuilder();
        //存储图片链接
        List<String> imageUrl = new ArrayList<>();
        //1.从文章内容中提取文本及图片
        if(StringUtils.isNotBlank(wmNews.getContent())) {
            List<Map> maps = JSON.parseArray(wmNews.getContent(),Map.class);
            for (Map map : maps) {
                if(map.get("type").equals("text")) {
                    //文本
                    text.append(map.get("value"));
                }
                if(map.get("type").equals("image")) {
                    //图片
                    imageUrl.add((String) map.get("value"));
                }
            }
        }
        //2.从封面中提取图片
        if(StringUtils.isNotBlank(wmNews.getImages())) {
            for (String imageurl : wmNews.getImages().split(",")) {
                imageUrl.add(imageurl);
            }
        }
        //3.结果封装
        Map<String,Object> map = new HashMap<>();
        map.put("content",text);
        map.put("images",imageUrl);
        return map;
    }
    @Autowired
    private WmSensitiveMapper wmSensitiveMapper;
    /**
     * 自管理敏感词过滤
     * @param content
     * @param wmNews
     * @return
     */
    private boolean handleSensitiveScan(String content,WmNews wmNews) {
        boolean flag = true;
        //1.获取所有的敏感词
        List<WmSensitive> wmSensitives = wmSensitiveMapper.selectList(Wrappers.<WmSensitive>lambdaQuery().select(WmSensitive::getSensitives));
        List<String> sensitiveList = wmSensitives.stream().map(WmSensitive::getSensitives).collect(Collectors.toList());
        //2.初始化敏感词库
        SensitiveWordUtil.initMap(sensitiveList);
        //3.检测文章是否包含敏感词
        Map<String, Integer> map = SensitiveWordUtil.matchWords(content);
        if(map.size() > 0) {
            log.info("当前文章存在敏感词:{}",map.keySet());
            flag = false;
            wmNews.setStatus(WmNews.Status.FAIL.getCode());
            wmNews.setReason("当前文章存在敏感词"+map.keySet());
            wmNewsService.updateById(wmNews);
        }
        return flag;
    }
    /**
     * 文本检测
     * @param content
     * @param wmNews
     * @return
     */
    private Boolean handleTextScan(String content,WmNews wmNews) throws TencentCloudSDKException {
        log.info("开始检测文章文本内容...");
        boolean flag = true;
        //1.文本为空
        if(StringUtils.isBlank(content) && StringUtils.isBlank(wmNews.getTitle())) {
            return true;
        }
        //2.调用腾讯云对文本进行检测
        JSONObject resultJson = textDetection.greenTextDetection(content + wmNews.getTitle());
        //审核不通过
        if(resultJson.get("Suggestion").equals("Block")) {
            log.warn("文章存在违规文本,审核不成功");
            flag = false;
            wmNews.setStatus(WmNews.Status.FAIL.getCode());
            wmNews.setReason("文章存在违规文本,审核不成功");
            wmNewsService.updateById(wmNews);
        }
        //自动审核不确定
        if(resultJson.get("Suggestion").equals("Review")) {
            log.info("文章存在不确定文本");
            flag = false;
            wmNews.setStatus(WmNews.Status.ADMIN_AUTH.getCode());
            wmNews.setReason("文章存在不确定文本");
            wmNewsService.updateById(wmNews);
        }
        return flag;
    }
    @Autowired
    private FileStorageService fileStorageService;
    @Autowired
    private Tess4jClient tess4jClient;
    /**
     * 图片检测
     * @param list
     * @param wmNews
     * @return
     * @throws TencentCloudSDKException
     */
    private Boolean handleImageScan(List<String> list, WmNews wmNews) throws TencentCloudSDKException {
        log.info("开始检测文章图片内容...");
        boolean flag = true;
        //图片信息为空
        if(list.size() == 0) {
            return true;
        }
        //1.图片去重
        List<String> imageList = list.stream().distinct().collect(Collectors.toList());
        //2.调用腾讯云接口逐一检测
        for (String image : imageList) {
            try {
                //敏感词检测
                //1.下载minio图片文件
                byte[] bytes = fileStorageService.downLoadFile(image);
                //2.从byte[]转换为bufferedImage
                ByteArrayInputStream in = new ByteArrayInputStream(bytes);
                BufferedImage imageFile = ImageIO.read(in);
                //3.识别图片文字
                if(imageFile != null) {
                    String result = tess4jClient.doOCR(imageFile);
                    //4.审核图片是否包含敏感词
                    boolean isSensitive = handleSensitiveScan(result,wmNews);
                    if(!isSensitive) {
                        return false;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            //腾讯云图片安全检测
            JSONObject resultJson = imageDetection.greenImageDetection(image);
            //审核不通过
            if(resultJson.get("Suggestion").equals("Block")) {
                log.warn("文章存在违规图片,审核不成功");
                flag = false;
                wmNews.setStatus(WmNews.Status.FAIL.getCode());
                wmNews.setReason("文章存在违规图片,审核不成功");
                wmNewsService.updateById(wmNews);
            }
            //自动审核不确定
            if(resultJson.get("Suggestion").equals("Review")) {
                log.info("文章存在不确定图片");
                flag = false;
                wmNews.setStatus(WmNews.Status.ADMIN_AUTH.getCode());
                wmNews.setReason("文章存在不确定图片");
                wmNewsService.updateById(wmNews);
            }
        }
        return flag;
    }
    @Autowired
    private WmChannelService wmChannelService;
    @Autowired
    private WmUserService wmUserService;
    @Resource
    private IArticleClient iArticleClient;
    /**
     * 保存App端文章数据
     * @param wmNews
     * @return
     */
    @Override
    public ResponseResult saveAppArticle(WmNews wmNews) {
        ArticleDto articleDto = new ArticleDto();
        //1.属性拷贝
        BeanUtils.copyProperties(wmNews,articleDto);
        //2.设置文章布局
        articleDto.setLayout(wmNews.getType());
        //3.设置频道信息
        WmChannel channel = wmChannelService.getById(wmNews.getChannelId());
        if(channel != null) {
            articleDto.setChannelName(channel.getName());
        }
        //4.设置作者信息
        articleDto.setAuthorId(wmNews.getUserId().longValue());
        WmUser wmUser = wmUserService.getById(wmNews.getUserId());
        if(wmUser != null) {
            articleDto.setAuthorName(wmUser.getName());
        }
        //5.设置文章id
        if(wmNews.getArticleId() != null) {
            articleDto.setId(wmNews.getArticleId());
        }
        //6.设置创建时间
        articleDto.setCreatedTime(new Date());
        //7.保存App端文章
        log.info("异步调用保存文章至App端");
        return iArticleClient.saveArticle(articleDto);
    }
}

image.gif

2.功能调用

在文章提交实现类中添加对自动审核方法的调用(见第五步)

/**
 * 提交文章
 * @param dto
 * @return
 */
@Override
public ResponseResult submitNews(WmNewsDto dto) {
    //1.参数校验
    if(dto == null || dto.getContent().length() == 0) {
        return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
    }
    //2.保存或修改文章
    //2.1属性拷贝
    WmNews wmNews = new WmNews();
    BeanUtils.copyProperties(dto,wmNews);
    //2.2设置封面图片
    if(dto.getImages() != null && dto.getImages().size() != 0) {
        String images = StringUtils.join(dto.getImages(), ",");
        wmNews.setImages(images);
    }
    //2.3封面类型为自动
    if(dto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)) {
        wmNews.setType(null);
    }
    saveOrUpdateWmNews(wmNews);
    //3.判断是否为草稿
    if(dto.getStatus().equals(WmNews.Status.NORMAL.getCode())) {
        //直接保存结束
        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }
    //4.不是草稿
    //4.1保存文章图片素材与文章关系
    //4.1.1提取图片素材列表
    List<String> imagesList = getImagesList(dto);
    //4.1.2保存
    saveRelatedImages(imagesList,wmNews.getId(),WemediaConstants.WM_CONTENT_REFERENCE);
    //4.2保存封面图片和文章关系
    saveRelatedCover(dto,imagesList,wmNews);
    //5.审核文章(异步调用)
    wmAutoScanService.AutoScanTextAndImage(wmNews.getId());
    return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}

image.gif

下篇预告:实现文章定时发布

相关文章
|
1天前
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
93 69
利用Spring Cloud Gateway Predicate优化微服务路由策略
|
2月前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
74 2
|
17天前
|
Java Nacos Sentinel
Spring Cloud Alibaba:一站式微服务解决方案
Spring Cloud Alibaba(简称SCA) 是一个基于 Spring Cloud 构建的开源微服务框架,专为解决分布式系统中的服务治理、配置管理、服务发现、消息总线等问题而设计。
169 13
Spring Cloud Alibaba:一站式微服务解决方案
|
1天前
|
存储 JSON 前端开发
【Spring项目】表白墙,留言板项目的实现
本文主要介绍了表白墙项目的实现,包含前端和后端代码,以及测试
|
25天前
|
消息中间件 监控 Java
如何将Spring Boot + RabbitMQ应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot + RabbitMQ应用程序部署到Pivotal Cloud Foundry (PCF)
31 6
|
25天前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
83 5
|
25天前
|
Java 关系型数据库 MySQL
如何将Spring Boot + MySQL应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot + MySQL应用程序部署到Pivotal Cloud Foundry (PCF)
45 5
|
25天前
|
缓存 监控 Java
如何将Spring Boot应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot应用程序部署到Pivotal Cloud Foundry (PCF)
35 5
|
1天前
|
JSON 前端开发 Java
|
1天前
|
缓存 前端开发 Java
【Spring】——SpringBoot项目创建
SpringBoot项目创建,SpringBootApplication启动类,target文件,web服务器,tomcat,访问服务器