纯后台生成Echarts图片,并将图片生成到PDF文档

一、如何后台生成Echarts图片?

1.PhantomJS

后台生成Echarts图,需要使用到PhantomJS:一个自带JavaScript API的无头WebKit脚本,简单理解就是:它能干浏览器能干的几乎所有事情,能解析js、能渲染等等,但是没有页面让你看。

 

2.PhantomJS的下载

https://phantomjs.org/api/fs/

 

http://wenku.kuryun.com/docs/phantomjs/index.html

 

也可以直接用我下载的文件:我的文件链接  提取码:es4e

 

打开我的文件:根据你使用的操作系统,选择一个进行下载并解压。

打开我的文件,除phantomjs文件外,可见还有一个文件夹echarts-convert,有2个文件:

 

1.echarts:里面是echarts和jquery,可以用你们自己本地或项目中的。

 

2.echarts-converts.js:是自己写的js文件,用来渲染生成echarts图片,可根据自己需求改写。

注意:若使用自己本地的echarts和jquery文件,echarts-converts.js里面的文件路径需要改写,指向你的文件所在的位置。

3.用phantomjs调用echarts-converts.js生成图片

以下以windows环境下举例:

 

1.从echarts官网随便选择一个图,将option复制本地某个文件中(例:G:\test\testOption.txt)

 

2.手动创建一个空的png文件(例:G:\test\111.png)

3.cmd调用phantomjs进程,让它去解析echarts-converts.js,并传入一些参数(如图):

 

G:\test\phantomjs-2.1.1-windows\bin\phantomjs.exe G:\test\echarts-convert\echarts-convert.js -txtPath G:\test\testOption.txt -picTmpPath G:\test\111.png -picPath G:\test\222.png

4.执行完成,完成后,111.png图片有内容,且生成了一张222.png。

二、Java如何将Echarts图生成到PDF

前面已经可以通过手动调用的方式生成Echarts图片。

 

接下来需要做的:

 

1、将手动生成图片的过程通过代码实现。

 

2、将生成的图片生成出PDF

 

3、下载PDF

 

1.生成PDF依赖

<dependency>

<groupId>com.itextpdf</groupId>

<artifactId>itextpdf</artifactId>

<version>5.5.13.3</version>

</dependency>

2.Java代码测试例子:

public static void main(String[] args)

{

try

{

// 测试同时生成两张图

// 注意:要先手动创建两个空文件G:\\test\\1.png和G:\\test\\2.png;要提前将echarts的option数据写到G:\\test\\testOption.txt和G:\\test\\testOption2.txt中

doExecPhantomJS_deleteFileAfterException(“G:\\test\\testOption.txt,G:\\test\\testOption2.txt”, “G:\\test\\1.png,G:\\test\\2.png”, “G:\\test\\111.png,G:\\test\\222.png”);

}

catch (Exception e)

{

e.printStackTrace();

}

 

Paragraph ph1 = ReportFormUtil.createImageParagraphByPath(“1、段落标题111”, “G:\\test\\111.png”, 35, 35,”这是一段前缀描述信息111″, “这是一段后缀描述信息111”);

Paragraph ph2 = ReportFormUtil.createImageParagraphByPath(“2、段落标题222”, “G:\\test\\222.png”, 35, 35, “这是一段前缀描述信息222”, “这是一段后缀描述信息222”);

 

List<Paragraph> phs = new ArrayList<Paragraph>();

phs.add(ph1);

phs.add(ph2);

 

ReportFormUtil.createPDFDocumentToDisk(“封面名称”, “小标题”, “”, phs, “G:\\test\\document.pdf”);

}

 

Tips:

 

一、ReportFormUtil文件放在文章结尾。

 

二、关于如何自动生成默认样式的option.txt文件,不过多赘述:

 

我的主要做法是:option配置可以看成一个json格式的字符串,然后:

 

1、提供可配置的默认的option样式;

 

2、将option中的数据部分(想自定义的部分)用符号占位;

 

3、在需要生成图片时,结合业务计算出自定义数据并转成json,替换掉占位符填到option字符串中生成完整的带数据的option字符串。

 

4、将3中生成的完整option字符串写到指定临时txt文件中(流程结束后删除临时文件)

 

3.测试结果

测试结果如图所示:成功生成pdf文件,且生成了两个图片段落

三、下载生成的PDF

@Override
    public void download(HttpServletResponse response, HttpServletRequest request) throws BusinessException
    {
 
        // 选中的要导出的文档id
        String id = request.getParameter("id");
        if (StringUtils.isBlank(id))
        {
            return;
        }
 
        // 1.根据id找出文档信息
        DocumentTaskModel taskModel = documentTaskDao.findById(Integer.valueOf(id));
 
        // 2.下载
        File file = new File(taskModel.getUrl());// taskModel.getUrl():该PDF文件的存放路径
        try (FileInputStream in = new FileInputStream(file); ServletOutputStream out = response.getOutputStream();)
        {
            // 若未定义文件名,则使用文档配置中的默认文档名
            String fileName = StringUtils.isNotBlank(taskModel.getName()) ? taskModel.getName()
                    : ReportFormEngine.getDocumentByKey(taskModel.getDocumentKey()).getName();
 
            response.reset();
            response.setContentType("application/x-msdownload");
            response.setHeader("Content-Length", "" + file.length());
            response.addHeader("Content-Disposition",
                    "attachment; filename=" + new String((fileName + ".pdf").getBytes("utf-8"), "iso8859-1"));
 
            byte[] buff = new byte[CommonConstants.BYTE_BUFFER_LENGTH_5120];
            int hasRead = -1;
            while ((hasRead = in.read(buff, 0, buff.length)) > 0)
            {
                out.write(buff, 0, hasRead);
            }
        }
        catch (FileNotFoundException e)
        {
            logger.error(e);
        }
        catch (IOException e)
        {
            logger.error(e);
        }
        catch (Exception e)
        {
            logger.error(taskModel.getUrl() + "下载文件失败!", e);
        }
    }

ReportFormUtil
package com.smartsecuri.bp.cbb.reportform;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
 
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
 
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
 
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;
import com.smartsecuri.bp.cbb.exception.BusinessException;
import com.smartsecuri.bp.cbb.utils.file.FileUtils;
import com.smartsecuri.bp.cbb.utils.other.CommonUtils;
import com.smartsecuri.bp.cbb.utils.system.IoUtils;
 
/**
 * ClassName : ReportFormUtil <br/>
 * Function : (PDF文档相关-主要用于生成报表) <br/>
 * date : 2022年11月29日 下午5:32:18 <br/>
 *
 * @author 
 * @version 
 * @since JDK 1.8
 */
public class ReportFormUtil
{
    private static final Logger logger = LogManager.getLogger(ReportFormUtil.class);
 
    /**
     * DEFAULT_LEADING : (默认行间距).
     */
    public static final float DEFAULT_LEADING = 16f;
    /**
     * fontSize_normal : (正文字体大小11号).
     */
    public static final float FONTSIZE_NORMAL = 11f;
    /**
     * fontSize_titile : (标题字体大小14号).
     */
    public static final float FONTSIZE_TITILE = 14f;
    /**
     * FONTSIZE_COVER : (封面字体大小32号).
     */
    public static final float FONTSIZE_COVER = 32f;
 
    /**
     * normalFont : (通用字体样式:宋体、11号).
     */
    private static Font normalFont = null;
    /**
     * titleFont : (通用标题字体样式:宋体、14号、加粗).
     */
    private static Font titleFont = null;
    /**
     * coverFont : (通用封面字体样式:宋体、28号、加粗).
     */
    private static Font coverFont = null;
 
    // windows 测试环境
    private static final String phantomPath = "G:\\test\\phantomjs-2.1.1-windows\\bin\\phantomjs.exe";
    private static final String JSpath = "G:\\test\\echarts-convert\\echarts-convert.js";
 
    // linux 环境
    // private static String phantomPath = "/usr/local/phantomjs/bin/phantomjs";
    // private static String JSpath = PathUtils.getProjectPath() + "common/reportform/echarts-convert.js";
 
    /**
     * getBaseFont : (获取可以解析中文的字体:使用宋体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static BaseFont getBaseFontChinese()
    {
        try
        {
            // 宋体资源文件路径
            URL path = ReportFormUtil.class.getResource("/config/fonts/simsun.ttc");
            return BaseFont.createFont(path + ",0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
 
            // 本地main方法测试:使用windows自带的宋体文件
            //return BaseFont.createFont("C://Windows//Fonts//simsun.ttc,0", BaseFont.IDENTITY_H, false);
        }
        catch (Exception e)
        {
            logger.error("设置字体样式失败", e);
            return null;
        }
    }
 
    /**
     * getNormalFont : (获取普通正文字体样式). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getNormalFont()
    {
        if (normalFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            normalFont = new Font(bfChinese, FONTSIZE_NORMAL, Font.NORMAL);
        }
        return normalFont;
    }
 
    /**
     * getTitleFont : (获取标题通用字体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getTitleFont()
    {
        if (titleFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            titleFont = new Font(bfChinese, FONTSIZE_TITILE, Font.BOLD);
        }
        return titleFont;
    }
 
    /**
     * getTitleFont : (获取封面通用字体). <br/>
     *
     * @author 
     * @return
     * @since JDK 1.8
     */
    public static Font getCoverFontFont()
    {
        if (coverFont == null)
        {
            BaseFont bfChinese = getBaseFontChinese();
            coverFont = new Font(bfChinese, FONTSIZE_COVER, Font.BOLD);
        }
        return coverFont;
    }
 
 
    /**
     * genFrontCover : (构建封面的文字和样式). <br/>
     *
     * @author 
     * @param coverName 封面标题
     * @param subtitle 小标题——封面标题下一行文字,可以为null或空串,表示不填
     * @param subscript 下标,可以为null或空串,表示不填
     * @return
     * @since JDK 1.8
     */
    public static Paragraph genFrontCover(String coverName, String subtitle, String subscript)
    {
        if (StringUtils.isBlank(coverName))
        {
            return null;
        }
        // 生成封面
        Paragraph frontCover = new Paragraph();
        frontCover.setAlignment(Element.ALIGN_CENTER);
        // 空10行
        ReportFormUtil.addBlankLine(frontCover, Integer.parseInt("10"));
 
        // 封面标题
        frontCover.add(new Chunk(coverName, ReportFormUtil.getCoverFontFont()));
 
        if (StringUtils.isNotBlank(subtitle))
        {
            ReportFormUtil.addBlankLine(frontCover, Integer.parseInt("2"));// 换行
            // 小标题
            frontCover.add(new Chunk(subtitle, ReportFormUtil.getTitleFont()));
        }
 
        if (StringUtils.isNotBlank(subscript))
        {
            // 换行
            ReportFormUtil.addBlankLine(frontCover, Integer.parseInt("25"));
            // companyName公司签名如:"慧盾信息安全科技(苏州)股份有限公司"
            frontCover.add(new Chunk(subscript, ReportFormUtil.getNormalFont()));
        }
        return frontCover;
    }
 
    /**
     * addBlankLine : (添加换行). <br/>
     *
     * @author 
     * @param paragraph 需要添加空行的段落
     * @param lineNum 需要添加空行的个数
     * @since JDK 1.8
     */
    public static void addBlankLine(Paragraph paragraph, int lineNum)
    {
        if (paragraph == null)
        {
            return;
        }
 
        for (int i = 0; i < lineNum; i++)
        {
            paragraph.add(Chunk.NEWLINE);
        }
    }
 
    /**
     * createTable : (创建table段落). <br/>
     *
     * @author 
     * @param <T>
     * @param list 构建table的数据
     * @param title 该段落取的名字,标题默认居左
     * @param methodNames 需要调用的方法名,用来获取单元格数据。通常是某个属性的get方法
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static <T> Paragraph createTable(List<T> list, String title, String[] tableHead, String[] methodNames,
            String prefixDescribe, String suffixDescribe)
    {
        return createTable(list, FONTSIZE_NORMAL, FONTSIZE_TITILE, title, tableHead, methodNames,
                prefixDescribe, suffixDescribe);
    }
 
    /**
     * createTableByList : (创建table段落). <br/>
     *
     * @author 
     * @param <T>
     * @param listData 
     * @param normalFontSize 正文字体大小
     * @param titleFontSize 标题字体大小
     * @param title 段落名称
     * @param methodNames 获取表格属性的方法名
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static <T> Paragraph createTable(List<T> listData, float normalFontSize, float titleFontSize, String title,
            String[] tableHead, String[] methodNames, String prefixDescribe, String suffixDescribe)
    {
 
        // 1.创建一个表格
        PdfPTable table = new PdfPTable(methodNames.length);// 列数
 
        // 2.构造表头
        for (String head : tableHead)
        {
            head = StringUtils.isBlank(head) ? "" : head;
            PdfPCell cell = new PdfPCell(new Phrase(head, getNormalFont()));
            cell.setBackgroundColor(
                    new BaseColor(Integer.parseInt("124"), Integer.parseInt("185"), Integer.parseInt("252")));// 背景色
            cell.setMinimumHeight(Float.parseFloat("15"));// 单元格最小高度
            cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 水平居中
            cell.setVerticalAlignment(Element.ALIGN_CENTER);// 垂直居中
            table.addCell(cell);
        }
 
        // 3.构造table数据
        if (CollectionUtils.isEmpty(listData))
        {
            // 没有数据,添加一行空单元格,并返回
            for (int i = 0; i < methodNames.length; i++)
            {
                table.addCell(new Phrase(" "));// 有一个空格,否则添加不了
            }
        }
        else
        {
            // 有数据:构造table数据
            for (T li : listData)
            {
                for (String name : methodNames)
                {
                    Object obj = CommonUtils.invokeMethod(li, name);
                    String valueStr = obj == null ? " " : StringUtils.isEmpty(obj.toString()) ? " " : obj.toString();
                    PdfPCell cell = new PdfPCell(new Phrase(valueStr, getNormalFont()));
                    cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 水平居中
                    table.addCell(cell);
                }
            }
        }
 
        // 4.返回段落
        return createParagraph(table, title, prefixDescribe, suffixDescribe);
    }
    
    /**
     * addDataToTable : (从段落中找到table元素,向该table中追加数据). <br/>
     *
     * @author 
     * @param <T>
     * @param paragraph table表格段落
     * @param listData 需要追加的数据
     * @param methodNames 每个数据获取的方法名
     * @since JDK 1.8
     */
    public static <T> void addDataToTable(Paragraph paragraph, List<T> listData, List<String> methodNames)
    {
        for (Element ele : paragraph)
        {
            if (!(ele instanceof PdfPTable))
            {
                // 不是table元素,直接跳过
                continue;
            }
 
            // 找到第一个table元素
            PdfPTable table = (PdfPTable) ele;
            for (T data : listData)
            {
                for (String name : methodNames)
                {
                    String valueStr = CommonUtils.invokeMethod(data, name).toString();
                    PdfPCell cell = new PdfPCell(new Phrase(valueStr, getNormalFont()));
                    cell.setHorizontalAlignment(Element.ALIGN_CENTER);// 单元格文字水平居中
                    table.addCell(cell);
                }
            }
            break;
        }
    }
 
    /**
     * createImage : (根据图片的base64加密文件创建pdf图片段落). <br/>
     *
     * @author 
     * @param picBase64Info 传入图片的base64信息,或传入前台echart通过调用getDataURL()方法获取的图片信息都可以
     * @param title 段落标题
     * @param percentX 图片缩放比例X轴
     * @param percentY 图片缩放比例Y轴
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * 
     * @return 返回图片段落
     * @since JDK 1.8
     */
    public static Paragraph createImageFromEncodeBase64(String picBase64Info, String title, float percentX,
            float percentY, String prefixDescribe, String suffixDescribe)
    {
        // 1.获取图片
        Element element = analysisPicBase64Info(picBase64Info);
        // 2.创建段落,并添加标题,设置缩放
        return createImageParagraph(element, title, percentX, percentY, prefixDescribe, suffixDescribe);
    }
 
    /**
     * createImageFromEncodeBase64_batch : (批量创建图片段落). <br/>
     *
     * @author 
     * @param picBase64Infos 传入图片的base64信息,或传入前台echart通过调用getDataURL()方法获取的图片信息都可以
     * @param titles 每个段落的标题
     * @param percentXs X轴缩放比例
     * @param percentYs Y轴缩放比例
     * @param titleCenter 标题是否居中,true-居中、false-默认居左
     * @return 返回由多个图片段落组合后的整个段落
     * @since JDK 1.8
     */
    public static Paragraph createImageFromEncodeBase64_batch(List<String> picBase64Infos, List<String> titles,
            List<Float> percentXs, List<Float> percentYs)
    {
        Paragraph paragraphs = new Paragraph(DEFAULT_LEADING);
        for (int i = 0; i <= picBase64Infos.size(); i++)
        {
            Paragraph imagePara = createImageFromEncodeBase64(picBase64Infos.get(i), titles.get(i), percentXs.get(i),
                    percentYs.get(i), null, null);
            if (!imagePara.isEmpty())
            {
                paragraphs.add(imagePara);
                // 换行
                paragraphs.add(Chunk.NEWLINE);
            }
        }
        return paragraphs;
    }
 
    /**
     * createPicParagraphByPath : (根据图片位置生成图片段落段落). <br/>
     *
     * @author 
     * @param title 段落标题
     * @param picPath 图片所在磁盘路径
     * @param percentX 图片缩放比例X轴
     * @param percentY 图片缩放比例Y轴
     * @param titleCenter 标题是否居中,true-居中、false-默认居左
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createImageParagraphByPath(String title, String picPath, float percentX, float percentY,
            String prefixDescribe, String suffixDescribe)
    {
        // 1.获取图片
        Element element = analysisPicByPath(picPath);
        // 2.创建段落,并添加标题,设置缩放
        return createImageParagraph(element, title, percentX, percentY, prefixDescribe, suffixDescribe);
    }
 
    /**
     * createPicParagraphByPath : (根据图片位置生成图片段落段落). <br/>
     *
     * @author 
     * @param picElement 图片元素
     * @param title 段落标题
     * @param percentX 图片缩放比例X轴
     * @param percentY 图片缩放比例Y轴
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createImageParagraph(Element picElement, String title, float percentX, float percentY,
            String prefixDescribe, String suffixDescribe)
    {
        if (picElement == null)
        {
            return new Paragraph();
        }
 
        try
        {
            if (!(picElement instanceof Image))
            {
                // 1. 图片解析失败
                logger.error(title + ":picElement is not instanceof Image");
                return new Paragraph();
            }
 
            // 2.设置图片缩放比例
            Image image = (Image) picElement;
            image.scalePercent(percentX, percentY);
            image.setAlignment(Element.ALIGN_CENTER);
 
            // 3.创建并返回图片段落
            return createParagraph(image, title, prefixDescribe, suffixDescribe);
        }
        catch (Exception e)
        {
            logger.error(e);
            // 空段落
            return new Paragraph();
        }
    }
 
    /**
     * createTxtParagraph : (创建文本段落). <br/>
     *
     * @author 
     * @param strings 多行句子
     * @param title 段落标题
     * @param titleCenter 标题居中
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createTxtParagraph(List<String> strings, String title, String prefixDescribe,
            String suffixDescribe)
    {
        Phrase phrase = new Phrase();
        for (String li : strings)
        {
            // 多行句子拼装
            phrase.add(new Chunk(li, getNormalFont()));
            phrase.add(Chunk.NEWLINE);
        }
 
        return createParagraph(phrase, title, prefixDescribe, suffixDescribe);
    }
 
    /**
     * createParagraph : (根据元素创建段落). <br/>
     *
     * @author 
     * @param element
     * @param title 段落标题
     * @param prefixDescribe 前缀附加文字描述
     * @param suffixDescribe 后缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createParagraph(Element element, String title, String prefixDescribe,
            String suffixDescribe)
    {
        title = StringUtils.isEmpty(title) ? "" : title;
        try
        {
            // 1.创建段落,并添加标题,添加前缀描述
            Paragraph paragraph = createParagraph(title, prefixDescribe);
            paragraph.add(element);
 
            // 2.后缀描述
            if (StringUtils.isNotBlank(suffixDescribe))
            {
                addBlankLine(paragraph, 1);// 换行符
                paragraph.add(new Paragraph(DEFAULT_LEADING, suffixDescribe, getNormalFont()));
            }
            return paragraph;
        }
        catch (Exception e)
        {
            logger.error(e);
            // 空段落
            return new Paragraph();
        }
    }
 
    /**
     * createParagraph : (创建段落). <br/>
     *
     * @author 
     * @param title
     * @param prefixDescribe 前缀附加文字描述
     * @return
     * @since JDK 1.8
     */
    public static Paragraph createParagraph(String title, String prefixDescribe)
    {
        Paragraph paragraph = new Paragraph(DEFAULT_LEADING);
 
        if (StringUtils.isNotEmpty(title))
        {
            paragraph.add(new Phrase(DEFAULT_LEADING, title, getTitleFont()));
            addBlankLine(paragraph, 2);
        }
 
        // 2.前缀描述
        if (StringUtils.isNotBlank(prefixDescribe))
        {
            paragraph.add(new Paragraph(prefixDescribe, getNormalFont()));
            ReportFormUtil.addBlankLine(paragraph, Integer.parseInt("1"));// 换行
        }
 
        return paragraph;
    }
 
    /**
     * createPDFDocument : (创建文档,默认纸张大小A4). <br/>
     *
     * @author WuTingTing
     * @return
     * @since JDK 1.8
     */
    public static Document createPDFDocument()
    {
        return new Document(PageSize.A4, Float.parseFloat("36"), Float.parseFloat("36"), Float.parseFloat("36"),
                Float.parseFloat("36"));
    }
 
    /**
     * createPDFDocumentToDisk : (生成PDF文档保存到磁盘). <br/>
     *
     * @author WuTingTing
     * @param coverName 封面标题
     * @param subtitle 小标题——封面标题下一行文字,可以为null或空串,表示不填
     * @param subscript 下标,可以为null或空串,表示不填
     * @param paragraphs 段落
     * @param documentPath 文件保存全路径
     * @return
     * @since JDK 1.8
     */
    public static boolean createPDFDocumentToDisk(String coverName, String subtitle, String subscript,
            List<Paragraph> paragraphs, String documentPath)
    {
        // 1.创建文档,设置文档页面大小,页边距
        Document document = createPDFDocument();
        // 2.封面
        Paragraph cover = genFrontCover(coverName, subtitle, subscript);
 
        // 3.生成文档并保存到指定路径
        return downloadDocument(document, cover, paragraphs, documentPath);
    }
 
    /**
     * exportDocument : (生成并下载PDF文档-通过response响应实现下载). <br/>
     *
     * @author
     * @param document 文档对象
     * @param cover 封面:若不是null,则会先添加封面,并另起新页面添加段落;若是null表示没有封面。
     * @param paragraphs 需要组成PDF文件的各个段落
     * @param response 请求的响应对象(提示:前台无法通过ajax请求触发浏览器的下载,可以通过表单提交的方式)
     * @param fileName 生成的文件名称,不需要加pdf后缀
     * @since JDK 1.8
     */
    public static void exportDocument(Document document, Paragraph cover, List<Paragraph> paragraphs,
            HttpServletResponse response, String fileName)
    {
        try (ServletOutputStream out = response.getOutputStream())
        {
            response.setContentType("application/binary;charset=UTF-8");
            response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(fileName + ".pdf", "UTF-8"));
 
            PdfWriter writer = PdfWriter.getInstance(document, out);
            writer.setStrictImageSequence(true);// 设置图片位置精确放置
            // 1.打开文档
            document.open();
 
            if (cover != null)
            {
                // 2.有封面:添加封面,并另起一页,用来塞后面的段落
                document.add(cover);
                document.newPage(); // 另起一页
            }
 
            StringBuilder errorMsg = new StringBuilder();
            for (int i = 0; i < paragraphs.size(); i++)
            {
                try
                {
                    // 将段落添加到文档
                    document.add(paragraphs.get(i));
                    // 换行
                    document.add(Chunk.NEWLINE);
                }
                catch (DocumentException e)
                {
                    errorMsg.append("PDF文件生成出错,请检查第:").append(i).append("个段落");
                }
            }
 
            if (!StringUtils.isEmpty(errorMsg.toString()))
            {
                logger.error(errorMsg);
            }
 
            // 关闭文档
            document.close();
            // 将数据输出
            out.flush();
            out.close();
        }
        catch (IOException e)
        {
            logger.error("生成PDF文档并下载,IOException:", e);
        }
        catch (DocumentException e)
        {
            logger.error("生成PDF文档并下载,DocumentException:", e);
        }
        finally
        {
            document.close();
        }
    }
 
    /**
     * downloadDocument : (生成PDF文档并保存到磁盘). <br/>
     *
     * @author 
     * @param document 文档对象
     * @param cover 封面:若不是null,则会先添加封面,并另起新页面添加段落
     * @param paragraphs 需要组成PDF文件的段落
     * @param response 请求的响应对象
     * @param fileName 生成的文件名称,不需要加pdf后缀
     * @return true成功、false失败
     * @since JDK 1.8
     */
    public static boolean downloadDocument(Document document, Paragraph cover, List<Paragraph> paragraphs,
            String documentPath)
    {
        FileOutputStream out = null;
        try
        {
            File file = new File(documentPath);
            if (!FileUtils.createFile(file))
            {
                return false;
            }
 
            out = new FileOutputStream(file);
            PdfWriter writer = PdfWriter.getInstance(document, out);
            writer.setStrictImageSequence(true);// 设置图片位置精确放置
 
            // 打开文档
            document.open();
 
            if (cover != null)
            {
                document.add(cover);
                // 起新页面
                document.newPage();
            }
 
            StringBuilder errorMsg = new StringBuilder();
            for (int i = 0; i < paragraphs.size(); i++)
            {
                try
                {
                    // 将段落添加到文档
                    document.add(paragraphs.get(i));
                    // 换行
                    document.add(Chunk.NEWLINE);
                }
                catch (DocumentException e)
                {
                    errorMsg.append("PDF文件生成出错,请检查第:").append(i).append("个段落");
                }
            }
 
            if (!StringUtils.isBlank(errorMsg.toString()))
            {
                logger.error(errorMsg);
            }
 
            // 关闭文档
            document.close();
            out.flush();
            IoUtils.close(out);
        }
        catch (Exception e)
        {
            logger.error("生成PDF文档并下载,出错:", e);
            return false;
        }
        finally
        {
            // 关闭文档
            document.close();
            IoUtils.close(out);
        }
        return true;
    }
 
    /**
     * analysisPicBase64Info : (解析base64图片信息). <br/>
     *
     * @author 
     * @param picBase64Info 传入图片的base64信息,或传入前台echart通过调用getDataURL()方法获取的图片信息都可以
     * @return 图片经过base64解码后的信息
     * @since JDK 1.8
     */
    public static Element analysisPicBase64Info(String picBase64Info)
    {
        if (StringUtils.isEmpty(picBase64Info))
        {
            // 空段落
            return new Paragraph();
        }
 
        // 1.获取图片base64字符串信息:若入参是通过前台echarts调用getDataURL()方法获取的,则该字符串包含逗号,且则逗号后面的内容才是图片的信息
        String pictureInfoStr = picBase64Info.indexOf(",") == -1 ? picBase64Info : picBase64Info.split(",")[1];
        // 2.将图片信息进行base64解密
        byte[] imgByte = Base64.decodeBase64(pictureInfoStr);
 
        // 对异常的数据进行处理
        /**
         * .图片的原始表达ascii码范围是0-255,
         * .这里面有一些不可见的编码。然后为了图片正确传输才转成编码base64的0-63,
         * .当从base64转成byte时,byte的范围是[-128,127],那么此时就会可能产生负数,而负数不是在ascii的范围里,所以需要转换一下
         */
        for (int i = 0; i < imgByte.length; i++)
        {
            if (imgByte[i] < 0)
            {
                imgByte[i] += 256;
            }
        }
 
        try
        {
            return Image.getInstance(imgByte);
        }
        catch (Exception e)
        {
            logger.error("analysisPicBase64Info error", e);
            return new Paragraph();
        }
    }
 
    /**
     * analysisPicBase64Info : (根据图片地址解析并生成图片段落). <br/>
     *
     * @author 
     * @param picPath 图片路径
     * @return 
     * @since JDK 1.8
     */
    public static Element analysisPicByPath(String picPath)
    {
        if (StringUtils.isEmpty(picPath))
        {
            return null;// 空段落
        }
 
        File file = new File(picPath);
        if (!file.exists())
        {
            // 图片文件不存在
            return null;// 空段落
        }
 
        try (FileInputStream in = new FileInputStream(file))
        {
            byte[] imgByte = new byte[(int) file.length()];
            in.read(imgByte);
            /**
             * .图片的原始表达ascii码范围是0-255,
             * .这里面有一些不可见的编码。然后为了图片正确传输才转成编码base64的0-63,
             * .当从base64转成byte时,byte的范围是[-128,127],那么此时就会可能产生负数,而负数不是在ascii的范围里,所以需要转换一下
             */
            for (int i = 0; i < imgByte.length; i++)
            {
                if (imgByte[i] < 0)
                {
                    imgByte[i] += 256;
                }
            }
 
            return Image.getInstance(imgByte);
        }
        catch (Exception e)
        {
            logger.error("analysisPicBase64Info error", e);
            return null;
        }
    }
 
    /**
     * analysisPicBase64Info_batch : (批量解析base64加密的图片信息,生成Image对象). <br/>
     *
     * @author 
     * @param picBase64Infos 传入图片的base64信息,或传入前台echart通过调用getDataURL()方法获取的图片信息都可以
     * @return
     * @since JDK 1.8
     */
    public static List<Element> analysisPicBase64Info_batch(List<String> picBase64Infos)
    {
        List<Element> images = new ArrayList<Element>();
        for (String li : picBase64Infos)
        {
            Element image = analysisPicBase64Info(li);
            images.add(image);
        }
        return images;
    }
 
    /**
     * doExecPhantomJS_deleteFileAfterException : (执行:浏览器加载option,渲染并截图保存到制定文件,若过程中出现错误则删除picTmpPath、picPath中生成的文件). <br/>
     * .【注意】支持一次多个,多个时路径以逗号隔开,且jsonPath和picPath的个数要一致。
     */
    public static void doExecPhantomJS_deleteFileAfterException(String txtPath, String picTmpPath, String picPath)
            throws BusinessException, Exception
    {
        doExecPhantomJS_deleteFileAfterException(txtPath, picTmpPath, picPath, null, null);
    }
    
    /**
     * doExecPhantomJS_deleteFileAfterException : (执行:浏览器加载option,渲染并截图保存到制定文件,若过程中出现错误则删除picTmpPath、picPath中生成的文件). <br/>
     * .【注意】支持一次多个,多个时路径以逗号隔开,且jsonPath和picPath的个数要一致。
     * .单个例子:
     * txtPath="/a/a.json"
     * picTmpPath="/a/tmp/a.png"
     * picPath="/a/a.png"
     * width=1000
     * height=600
     * 
     * .多个例子:
     * txtPath="/a/a.txt,/b/b.json,/c/c.txt"
     * picTmpPath="/a/tmp/a.png,/b/tmp/b.png,/c/tmp/c.png"
     * picPath="/a/a.png,/b/b.png,/c/c.png"
     * width=1000,1000,1000
     * height=600,600,600,600
     *
     * @author WuTingTing
     * @param txtPath 图表option信息的txt文件全路径
     * @param picTmpPath 图片临时存放路径,生成后将转移到picPath路径下
     * @param picPath 图片文件全路径
     * @param width 自定义截图宽度
     * @param height 自定义截图高度
     * @throws Exception 
     * @since JDK 1.8
     */
    public static void doExecPhantomJS_deleteFileAfterException(String txtPath, String picTmpPath, String picPath,
            String width, String height) throws BusinessException, Exception
    {
        try
        {
            doExecPhantomJS(txtPath, picTmpPath, picPath, width, height);
        }
        catch (Exception e)
        {
            // 执行过程中出错,删除本次执行中可能生成的文件,不删除txtPath是防止该文件为默认内置文件
            String[] picTmpPaths = picTmpPath.split(",");
            String[] picPaths = picPath.split(",");
 
            for (String li : picTmpPaths)
            {
                FileUtils.delFile(li);
            }
 
            for (String li : picPaths)
            {
                FileUtils.delFile(li);
            }
 
            if (e instanceof BusinessException)
            {
                logger.error(((BusinessException) e).getMsg());
                throw new BusinessException().setMsg(((BusinessException) e).getMsg());
            }
            else
            {
                throw new Exception(e);
            }
        }
    }
 
    /**
     * doExecPhantomJS : (执行:浏览器加载option,渲染并截图保存到制定文件). <br/>
     * .【注意】支持一次多个,多个时路径以逗号隔开,且jsonPath和picPath的个数要一致。
     * .单个例子:
     * txtPath="/a/a.json"
     * picTmpPath="/a/tmp/a.png"
     * picPath="/a/a.png"
     * 
     * .多个例子:
     * txtPath="/a/a.txt,/b/b.json,/c/c.txt"
     * picTmpPath="/a/tmp/a.png,/b/tmp/b.png,/c/tmp/c.png"
     * picPath="/a/a.png,/b/b.png,/c/c.png"
     *
     * @author WuTingTing
     * @param txtPath 图表option信息的txt文件全路径
     * @param picTmpPath 图片临时存放路径,生成后将转移到picPath路径下
     * @param picPath 图片文件全路径
     * @throws Exception 
     * @since JDK 1.8
     */
    private static void doExecPhantomJS(String txtPath, String picTmpPath, String picPath, String width, String height)
            throws BusinessException, Exception
    {
        String cmd = getCMD(txtPath, picTmpPath, picPath, width, height);
        logger.info("图片生成命令:" + cmd);
 
        BufferedReader processInput = null;
 
        // 检查文件是否存在
        boolean existP = FileUtils.fileExists(phantomPath);
        boolean existJ = FileUtils.fileExists(JSpath);
        if (!existP || !existJ)
        {
            throw new BusinessException()
                    .setMsg("生成图片必要文件缺失:" + (!existP ? "phantomjs " : "") + (!existJ ? "echarts-convert.js、" : ""));
        }
 
        try
        {
            Process process = Runtime.getRuntime().exec(cmd);
            processInput = new BufferedReader(
                    new InputStreamReader(process.getInputStream(), Charset.forName("UTF-8")));
            String line = "";
            while ((line = processInput.readLine()) != null)
            {
                // 执行信息打印
                logger.info(line);
            }
            int waitFor = process.waitFor();
            if (waitFor != 0)
            {
                logger.info("图片生成过程,非正常退出,请检查是否存在异常");
            }
        }
        finally
        {
            if (processInput != null)
            {
                try
                {
                    processInput.close();
                }
                catch (IOException e)
                {
                    logger.error("io close fail");
                }
            }
        }
    }
 
    /**
     * getCMD : (拼接命令). <br/>
     *
     * @author 
     * @param txtPath 图表option信息的txt文件全路径
     * @param picTmpPath 图片临时存放路径,生成后将转移到picPath路径下
     * @param picPath 图片文件全路径
     * @param width 自定义截图宽度
     * @param height 自定义截图高度
     * @return
     * @throws BusinessException
     * @since JDK 1.8
     */
    private static String getCMD(String txtPath, String picTmpPath, String picPath, String width, String height)
            throws BusinessException
    {
        if (StringUtils.isBlank(txtPath) || StringUtils.isBlank(picTmpPath) || StringUtils.isBlank(picPath))
        {
            logger.error("txtPath or picTmpPath or picPath is blank");
            throw new BusinessException().setMsg("doExecPhantomJS 入参错误");
        }
 
        if (StringUtils.isNotBlank(width) && StringUtils.isNotBlank(height))
        {
            return phantomPath + " " + JSpath + " -txtPath " + txtPath + " -picTmpPath " + picTmpPath + " -picPath "
                    + picPath + " -width " + width + " -height " + height;
        }
        else
        {
            // 未自定义截图宽度和高度,会使用默认宽、高
            return phantomPath + " " + JSpath + " -txtPath " + txtPath + " -picTmpPath " + picTmpPath + " -picPath "
                    + picPath;
        }
    }
 
    public static void main(String[] args)
    {
        try
        {
            // 测试同时生成两张图
            // 注意:要先手动创建两个空文件G:\\test\\1.png和G:\\test\\2.png;要提前将echarts的option数据写到G:\\test\\testOption.txt和G:\\test\\testOption2.txt中
            doExecPhantomJS_deleteFileAfterException("G:\\test\\testOption.txt,G:\\test\\testOption2.txt", "G:\\test\\1.png,G:\\test\\2.png", "G:\\test\\111.png,G:\\test\\222.png");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
 
        Paragraph ph1 = ReportFormUtil.createImageParagraphByPath("1、段落标题111", "G:\\test\\111.png", 35, 35,"这是一段前缀描述信息111", "这是一段后缀描述信息111");
        Paragraph ph2 = ReportFormUtil.createImageParagraphByPath("2、段落标题222", "G:\\test\\222.png", 35, 35, "这是一段前缀描述信息222", "这是一段后缀描述信息222");
 
        List<Paragraph> phs = new ArrayList<Paragraph>();
        phs.add(ph1);
        phs.add(ph2);
 
        ReportFormUtil.createPDFDocumentToDisk("封面名称", "小标题", "", phs, "G:\\test\\document.pdf");
    }
}

 

 

本文出自:https://blog.csdn.net/qq_41773784/article/details/131592864

七种常用的设计模式

常用的七种设计模式:单例模式、工厂方法模式、抽象工厂模式、代理模式、装饰器模式、观察者模式和责任链模式。

设计模式分类

设计模式根据工作的目的,分为创建型模式、结构型模式和行为型模式三类。

创建型模式:单例模式、工厂方法模式、抽象工厂模式、创建者模式、原型模式。

结构型模式:适配器模式、代理模式、装饰器模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

软件设计七大原则(OOP原则)

开闭原则:对扩展开放,对修改关闭。
里氏替换原则:不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义。
依赖倒置原则:要面向接口编程,不要面向实现编程。
单一职责原则:控制类的粒度大小、将对象解耦、提高其内聚性。
接口隔离原则:要为各个类建立它们需要的专用接口。
迪米特法则:一个类应该保持对其它对象最少的了解,降低耦合度。
合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

实际上,七大原则的目的只有一个:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。

1、单例模式:一个类只有一个实例,且该类能自行创建这个实例的一种模式
①单例类只有一个实例对象

②该单例对象必须由单例类自行创建

③单例类对外提供一个访问该单例的全局访问点

④、优点
单例模式可以保证内存里只有一个实例,减少了内存的开销。
可以避免对资源的多重占用。
单例模式设置全局访问点,可以优化和共享资源的访问。
⑤、缺点
单例模式一般没有接口,扩展困难。
单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则

饿汉式单例:类一旦加载就创建一个单例,保证在调用getInstance方法之前单例已经存在,这种饿汉式单例会造成空间浪费。

public class Hungry {
    private Hungry(){}
    private final static Hungry HUNGRY = new Hungry();
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

懒汉式单例:为了避免内存空间浪费,采用懒汉式单例,即用到该单例对象的时候再创建。

public class LazyMan {
    private LazyMan(){};
 
    public static LazyMan lazyMan;
 
    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

这是最简单的懒汉式,但是,存在很大问题,单线程下这段代码没有问题,但是在多线程下有很大问题。

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"");
    }
 
    public static LazyMan lazyMan;
 
    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
 
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(()->{
                lazyMan.getInstance();
            }).start();
        }
    }
}

会发现结果都不一样,因此,并发情况下,这段代码是有问题的。我们需要进行两端检测,进行“加锁”:synchronized (Singleton.class)。

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"");
    }
 
    public static LazyMan lazyMan;
 
    public static LazyMan getInstance(){
        if (lazyMan==null){        //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
            synchronized (LazyMan.class){    //第一层锁,保证只有一个线程进入
                //双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
                //当某一线程获得锁创建一个LazyMan对象时,即已有引用指向对象,lazyMan不为空,从而保证只会创建一个对象
                //假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
                if(lazyMan==null){    //第二层检查
                    //synchronized关键字作用为禁止指令重排,保证返回Singleton对象一定在创建对象后
                    lazyMan = new LazyMan();        //这行代码存在的问题,不能保证原子性
实际上会执行以下内容:
                    //(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象
                    //假设以上三个内容为三条单独指令,因指令重排可能会导致执行顺序为1->3->2(正常为1->2->3),当单例模式中存在普通变量需要在构造方法中进行初始化操作时,单线程情况下,顺序重排没有影响;但在多线程情况下,假如线程1执行lazyMan = new LazyMan()语句时先1再3,由于系统调度线程2的原因没来得及执行步骤2,但此时已有引用指向对象也就是lazyMan!=null,故线程2在第一次检查时不满足条件直接返回lazyMan,此时lazyMan为null
                    //synchronized关键字可保证lazyMan = new LazyMan()语句执行顺序为123,因其为非原子性依旧可能存在系统调度问题(即执行步骤时被打断),但能确保的是只要lazyMan!=0,就表明一定执行了属性初始化操作;而若在步骤3之前被打断,此时lazyMan依旧为null,其他线程可进入第一层检查向下执行创建对象
                }
            }
        }
        return lazyMan;
    }
 
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(()->{
                lazyMan.getInstance();
            }).start();
        }
    }
}

可以看到结果都是只有一个,按理来说是没有问题,实际上不是,上述标注行代码存在问题的,不是一个原子性操作。

静态内部类单例:是不安全,存在问题的,是不安全,存在问题的(修改过的懒汉式单例也是如此)。

public class Inner {
    private Inner(){}
    public static Inner getInstance(){
        return InnerClass.INNER;
    }
    public static class InnerClass{
        private static final Inner INNER = new Inner();
    }
}

2、工厂方法模式:实例化对象不是用new,用工厂方法替代。将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。

简单工厂模式:用来生产同一等级架构中的任意产品(对于增加新的产品,需要修改已有代码)
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例。

接下来创建一个接口,两个实现类,一个工厂,一个测试类

//创建手机接口
public interface Phone {
    void name();
}
//创建华为实现类
public class HuaWei implements Phone{
    @Override
    public void name() {
        System.out.println("华为手机");
    }
}
//创建小米实现类
public class XiaoMi implements Phone{
    @Override
    public void name() {
        System.out.println("小米手机");
    }
}
//创建工厂
public class PhoneFactory {
    public static Phone getPhone(String phone){
        if(phone.equals("华为")){
            return new HuaWei();
        }else if(phone.equals("小米")){
            return  new XiaoMi();
        }else {
            return null;
        }
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Phone p1= PhoneFactory.getPhone("华为");
        Phone p2= PhoneFactory.getPhone("小米");
        p1.name();
        p2.name();
    }
}

得到测试结果

        华为手机
        小米手机

我们通过创建一个PhoneFactory类,成功的完成工厂的创建。我们在创建对象时,也就不需要直接创建对象,而是可以通过创建工厂,这样大大的降低了代码的耦合性。但是,静态工厂模式是不能添加数据的。比如说,我们想添加一个“Oppo”手机类,你不直接修改PhoneFactory工厂代码,是不能实现的。所以,就有了第二种的工厂方法模式。

工厂方法模式:用来生产同一等级架构中的固定产品,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。(支持增加任意产品)

//创建手机接口
public interface Phone {
    void name();
}
//创建华为实现类
public class HuaWei implements Phone{
    @Override
    public void name() {
        System.out.println("华为手机");
    }
}
//创建手机工厂接口
public interface PhoneFactory {
    Phone getPhone();
}
//创建华为工厂
public class HuaWeiFactory implements PhoneFactory{
    @Override
    public Phone getPhone() {
        return new HuaWei();
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Phone phone = new HuaWeiFactory().getPhone();
        phone.name();
    }
}

得到测试结果

        华为手机

我们创建了手机工厂接口PhoneFactory,再创建华为工厂HuaWeiFactory实现工厂,这样就可以通过HuaWeiFactory创建对象。增加新的具体工厂和产品族很方便,比如说,我们想要增加小米,只需要创建一个小米工厂XiaoMiFactory实现手机工厂接口PhoneFactory,合理的解决的简单工厂模式不能修改代码的缺点。但是,在现实使用中,简单工厂模式占绝大多数。

简单工厂模式与工厂方法模式比较:

结构的复杂度:简单工厂模式占优。
代码的复杂度:简单工厂模式占优。
编程的复杂度:简单工厂模式占优。
管理的复杂的:简单工厂模式占优。
因此,虽然简单工厂模式不符合设计模式,但是实际使用远大于工厂方法模式。

3、抽象工厂模式:抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类(围绕一个超级工厂创建其他工厂,该超级工厂称为工厂的工厂)

抽象工厂模式得主要角色:

1、抽象工厂(Abstract Factory)提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
2具体工厂(Concrete Factory)主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
3抽象产品(Product)定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
4具体产品(ConcreteProduct)实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

//电脑接口
public interface Computer {
    void play();
    void watch();
}
//创建华为电脑对象
public class HuaWeiComputer implements Computer{
    @Override
    public void play() {
        System.out.println("HuaWei's play!");
    }
 
    @Override
    public void watch() {
        System.out.println("HuaWei's watch!");
    }
}
//手机接口
public interface Phone {
    void send();
    void call();
}
//创建华为手机对象
public class HuaWeiPhone implements Phone{
    @Override
    public void send() {
        System.out.println("HuaWei's send");
    }
 
    @Override
    public void call() {
        System.out.println("HuaWei's call");
    }
}
//抽象工厂
public interface IProductFactory {
    //生产手机
    Phone phone();
    //生产电脑
    Computer computer();
}
//创建华为工厂
public class HuaWeiFactory implements IProductFactory{
    @Override
    public Phone phone() {
        return new HuaWeiPhone();
    }
 
    @Override
    public Computer computer() {
        return new HuaWeiComputer();
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        HuaWeiFactory huaWeiFactory = new HuaWeiFactory();
        Phone phone = huaWeiFactory.phone();
        phone.call();
        phone.send();
        Computer computer = huaWeiFactory.computer();
        computer.play();
        computer.watch();
    }
}

得到测试结果

        HuaWei's call
        HuaWei's send
        HuaWei's play!
        HuaWei's watch!

我们通过创建一个抽象工厂完成了对具体工厂的创建,只需要传入参数就可以实例化对象。具体产品在应用层的代码隔离,无需关心创建的细节将一个系列的产品统一到一起创建。将一系列产品规划到一起创建。但是,抽象工厂模式也存在着缺点。规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难,不可以增加产品,只能增加品牌。

4、代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

静态代理模式:

角色分析:

1、抽象角色:一般会使用接口或抽象类来解决

2、真实角色:被代理的角色

3、代理角色:代理真实角色,代理真实角色后我们会进行一些附属操作

4、访问角色:访问代理对象的人

//租房
public interface Rent {
    void rent();
}
//房东
public class Master implements Rent{
    @Override
    public void rent() {
        System.out.println("Master rent");
    }
}
//中介
public class Proxy implements Rent{
    private Master master;
 
    public Proxy() {
    }
 
    public Proxy(Master master) {
        this.master = master;
    }
 
    @Override
    public void rent() {
        see();
        master.rent();
        fare();
    }
    //看房
    public void see(){
        System.out.println("see");
    }
    //收费
    public void fare(){
        System.out.println("fare");
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Master master = new Master();
        //进行代理
        Proxy proxy = new Proxy(master);
        //不需要通过对象,直接通过代理完成响应业务
        proxy.rent();
    }
}

得到测试结果

        see
        Master rent
        fare

可以从上述代码看出,我们通过创建中介这一代理完成了租房,并且还有看房、收费的附属操作。我们不需要使用房东对象,通过使用代理中介就可以完成操作。

代理模式优点:可以使真实角色的操作更加纯粹!不用去关注一些公共的业务,公共也就可以交给代理角色,实现了业务的分工,公共业务发生扩展的时候,方便集中管理!
代理模式缺点:一个真实角色就会产生一个代理角色;代码量会翻倍开发效率会变低,也许,这样无法理解到代理模式的好处。举个例子也许能更好理解,比如说我们想要在原有固定功能上新增业务,按照开闭原则我们是不能对原有代码进行修改的。但是我们可以通过代理模式,增加代理,在实现原有功能的情况下写入新的功能,创建对象时也就可以使用代理,完成操作。

动态代理模式:虽然静态代理模式可以很好的解决开闭原则,但是每有一个真实角色就会产生一个代理,代码量翻倍过于臃肿,开发效率较低。因此,我们就使用动态代理模式进行设计。

//接口
public interface IUserService {
    void add();
    void delete();
    void update();
    void query();
 
}
//实现类
public class UserServiceImpl implements IUserService {
    @Override
    public void add() {
        System.out.println("add");
    }
 
    @Override
    public void delete() {
        System.out.println("delete");
    }
 
    @Override
    public void update() {
        System.out.println("update");
    }
 
    @Override
    public void query() {
        System.out.println("query");
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//自动生成动态代理类模板
public class ProxyInvocationHandler implements InvocationHandler {
    //被代理接口
    private Object target;
   
    public void setTarget(Object target) {
        this.target = target;
    }
     //得到代理类
    public Object getProxy() {
        return Proxy.newProxyInstance(getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    public void log(String s) {
        System.out.println("[debug]:" + s);
    }
    //得到代理类
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log(method.getName());
        Object result = method.invoke(target, args);
        return result;
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        ProxyInvocationHandler handler = new ProxyInvocationHandler();
        //设置代理对象
        handler.setTarget(userService);
        //生成代理类
        IUserService proxy = (IUserService)handler.getProxy();
        proxy.add();
        proxy.query();
    }
}

得到测试结果

        [debug]:add
        add
        [debug]:query
        query

通过测试我们可以顺利的使用动态代理模式完成一系列操作,当我们想要添加附属操作时,我们只需要在模板中进行添加。优点:①可以使真实角色的操作更加纯粹!不用去关注一些公共的业务。②公共也就可以交给代理角色!实现了业务的分工。③公共业务发生扩展的时候,方便集中管理。④一个动态代理类代理的是一个接口,一般就是对应的一类业务。⑤一个动态代理类可以代理多个类,只要是实现了同一个接口即可!

5、装饰者模式:动态的将新功能附加到对象上。在对象功能的拓展方面,比继承更有弹性。同时装饰者模式也体现了开闭原则。

角色分析:

1、抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
2、具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
3、抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
4、具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

//定义抽象类
public abstract class Drink {
    public abstract double cost();
}
//定义两个抽象类的实现类
public class Juice extends Drink{
    @Override
    public double cost() {
        System.out.println("juice: "+16);
        return 16;
    }
}
public class Milk extends Drink{
    @Override
    public double cost() {
        System.out.println("milk: "+12);
        return 12;
    }
}
//定义装饰抽象类
public abstract class Decorator extends Drink {
    public abstract double cost();
}
//定义两个装饰具体实现类
public class Chocolate extends Decorator{
    private final static double COST = 4;
    private Drink drink;
 
    public Chocolate(Drink drink) {
        this.drink = drink;
    }
 
    @Override
    public double cost() {
        System.out.println("chocolate:"+4);
        return COST+drink.cost();
    }
}
public class Pudding extends Decorator{
    private final static double COST = 5;
    private Drink drink;
 
    public Pudding(Drink drink) {
        this.drink = drink;
    }
 
    @Override
    public double cost() {
        System.out.println("pudding:"+5);
        return COST+drink.cost();
    }
}
//测试类
public class Test {
    public static void main(String[] args) {
        Drink milk = new Milk();
        milk = new Pudding(milk);
        milk = new Chocolate(milk);
        System.out.println(milk.cost());
    }
}

得到测试结果

        chocolate:4
        pudding:5
        milk: 12
        21.0

可以看到非常简单的就能够完成具体构件和具体装饰的组合。也可以看到结构也非常简洁明,具体构件在自己的package中,具体装饰也在自己的package中。我们不管是想要增加具体构件还是具体配饰,都可以在各自的package中添加。对已有的代码不需要进行任何操作,就算新加的有bug也不会影响原有代码的操作。

6、观察者模式:对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

观察者模式有点:

1、降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。

2、目标与观察者之间建立了一套触发机制。

//定义观察者接口
public interface Observer {
    void response();
}
//具体化观察者1
public class Observer1 implements Observer{
    @Override
    public void response() {
        System.out.println("Observer1 action");
    }
}
//具体化观察者2
public class Observer2 implements Observer{
    @Override
    public void response() {
        System.out.println("Observer2 action");
    }
}
//抽象目标
public abstract class Subject {
    //创建观察者集合
    protected ArrayList<Observer> array = new ArrayList<Observer>();
    //增加观察者
    public void add(Observer observer){
        array.add(observer);
    }
    //删除观察者
    public void remove(Observer observer){
        array.remove(observer);
    }
    //通知观察者方法
    public abstract void notifyObserver();
}
//具体化目标
public class Subject1 extends Subject{
    @Override
    public void notifyObserver() {
        for (Observer observer :array){
            observer.response();
        }
    }
}
//测试类
public class Test {
    public static void main(String[] args) {
        Subject subject = new Subject1();
        Observer obs1 = new Observer1();
        Observer obs2 = new Observer2();
        subject.add(obs1);
        subject.add(obs2);
        subject.notifyObserver();
    }
}

得到测试结果:

        Observer1 action
        Observer2 action

通过测试我们可以看到,我们不需要建立太多观察者和具体目标之间的联系,大大降低了目标与观察者之间的耦合关系。并且结构也十分简单明了,观察者和目标分别在各自的package包下。当我们想要添加观察者时,只需要在观察者包下进行添加,实现Observer接口就可以了。

7、责任链模式:一种处理请求的模式,它让多个处理器都有机会处理该诘求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。
责任链模式的主要角色
抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
责任链模式优点
降低了对象之间的耦合度。处理者不需要知道客户的任何信息,客户也不要知道处理者是如何实现方法的。
提高了系统的灵活性。当我们想要新增处理器到整个链条中时,所付出的代价是非常小的
责任链模式缺点
降低了系统的性能。对比较长的职责链,请求的处理可能涉及多个处理对象
不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。

//抽象处理者
public abstract class Handler {
    private Handler next;
    public void setNext(Handler next) { this.next = next; }
    public Handler getNext() { return next; }
 
    //处理请求
    public abstract void handleRequest(int info);
}
//具体处理者1
public class Handler1 extends Handler{
    @Override
    public void handleRequest(int info) {
        if (info <10){
            System.out.println("Handler1完成处理");
        }else {
            if (getNext()!=null){
                getNext().handleRequest(info);
            }else {
                System.out.println("没有处理者进行处理");
            }
        }
    }
}
//具体处理者2
public class Handler2 extends Handler{
    @Override
    public void handleRequest(int info) {
        if (info <20&&info>10){
            System.out.println("Handler2完成处理");
        }else {
            if (getNext()!=null){
                getNext().handleRequest(info);
            }else {
                System.out.println("没有处理者进行处理");
            }
        }
    }
}
测试类
public class Test {
    public static void main(String[] args) {
        Handler handler1 = new Handler1();
        Handler handler2 = new Handler2();
        handler1.setNext(handler2);
        handler1.handleRequest(5);
        handler1.handleRequest(15);
        handler1.handleRequest(25);
    }
}

得到测试结果:

Handler1完成处理
Handler2完成处理
没有处理者进行处理

通过测试结果我们看到,5交给了Handler1处理,15交给了Handler2处理,而25则没有处理者处理。请求者根本不需要参与处理,只需要提交数据就可以完成功能的处理,完全不需要管是哪个处理者进行处理的。当我们想要继续添加处理者时,这只需要再次添加就可以了,也不会对之前的代码造成影响。

本文出自:https://blog.csdn.net/a154555/article/details/125612512

Java I/O流概述

Java I/O流概述
引言
在Java编程中,I/O流是一种用于在程序和外部数据源之间传输数据的机制。这些外部数据源可以是文件、网络连接、内存中的数据等。I/O流以字节或字符的形式操作数据,具体取决于您的需求。

I/O流的类型
Java中有两种主要类型的I/O流:

字节流(Byte Streams):字节流用于以字节的形式处理数据,适用于二进制文件和字节数据的读写。主要使用InputStream和OutputStream类来操作字节流。
字符流(Character Streams):字符流用于以字符的形式处理文本数据,适用于文本文件和字符串的读写。主要使用Reader和Writer类来操作字符流。
字节流(Byte Streams)
字节流的基本概念
字节流是处理二进制数据的一种方式。InputStream和OutputStream类是字节流的主要代表。通过字节流,您可以读取和写入字节数组,适用于处理图像、音频等二进制数据。

字节流的应用
字节流的常见应用场景包括:

读取和写入二进制文件。
处理网络数据流。
操作图像和音频文件。

// 示例代码:使用字节流读取和写入文件
try (InputStream inputStream = new FileInputStream("input.txt");
     OutputStream outputStream = new FileOutputStream("output.txt")) {
    int byteData;
    while ((byteData = inputStream.read()) != -1) {
        outputStream.write(byteData);
    }
} catch (IOException e) {
    e.printStackTrace();
}

字符流(Character Streams)
字符流的基本概念
字符流是处理文本数据的一种方式。Reader和Writer类是字符流的主要代表。通过字符流,您可以以字符的形式读取和写入文本数据,适用于处理文本文件。

字符流的应用
字符流的常见应用场景包括:

读取和写入文本文件。
处理CSV、XML等文本格式的数据。
处理字符编码和国际化文本。

// 示例代码:使用字符流读取和写入文本文件
try (Reader reader = new FileReader("input.txt");
     Writer writer = new FileWriter("output.txt")) {
    int charData;
    while ((charData = reader.read()) != -1) {
        writer.write(charData);
    }
} catch (IOException e) {
    e.printStackTrace();
}

缓冲流(Buffered Streams)
缓冲流的介绍
缓冲流用于提高I/O性能。它们在内部维护了一个缓冲区,可以一次性读取或写入多个字节或字符,减少了磁盘或网络访问的次数,从而提高了效率。

示例代码

// 示例代码:使用缓冲流提高文件读取性能
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
     BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line);
        writer.newLine(); // 添加换行符
    }
} catch (IOException e) {
    e.printStackTrace();
}

异常处理和资源管理
异常处理
在I/O操作中,可能会发生异常,如IOException。使用try-catch块来捕获和处理这些异常,确保程序能够优雅地处理错误情况。

资源管理(try-with-resources)
Java 7引入了自动资源管理(try-with-resources)机制,可自动关闭打开的流。这可以通过try块中的资源声明来实现,确保资源在退出try块时自动关闭。

// 示例代码:使用try-with-resources自动关闭资源
try (InputStream inputStream = new FileInputStream("input.txt");
     OutputStream outputStream = new FileOutputStream("output.txt")) {
    // 处理流操作
} catch (IOException e) {
    e.printStackTrace();
}

文件和目录操作
文件和目录操作
使用File类,您可以进行各种文件和目录操作,如创建、删除、重命名文件和目录。

// 示例代码:文件和目录操作
File file = new File("myFile.txt");
if (file.exists()) {
    // 文件存在,执行操作
    if (file.delete()) {
        System.out.println("文件删除成功");
    } else {
        System.out.println("文件删除失败");
    }
} else {
    System.out.println("文件不存在");
}

总结
通过本文,我们深入研究了Java中的I/O流。我们介绍了字节流和字符流的基本概念,讨论了缓冲流的性能提升,以及如何处理异常和管理资源。此外,我们还研究了文件和目录操作,使您能够更好地理解如何在Java程序中有效地处理输入和输出。希望这篇文章对您在Java编程中使用I/O流时有所帮助。

本文出自:https://blog.csdn.net/qq_43546721/article/details/133688858

arm 点灯实验代码以及现象

.text 
.global _start
_start: 
@1.设置GPIOE寄存器的时钟使能  RCC_MP_AHB4ENSETR[4]->1     0x50000a28
LDR R0,=0x50000A28
LDR R1,[R0]
ORR R1,R1,#(0x1<<4)  @第4位置1
STR R1,[R0]
@1.设置GPIOF寄存器的时钟使能  RCC_MP_AHB4ENSETR[4]->1     0x50000a28
LDR R0,=0x50000A28
LDR R1,[R0]
ORR R1,R1,#(0x1<<5)  @第5位置1
STR R1,[R0]
 
@2.设置PE10管脚为输出模式  GPIOE_MODER[21:20]->01   0x50006000
LDR R0,=0x50006000
LDR R1,[R0]
BIC R1,R1,#(0x3<<20)
ORR R1,R1,#(0x1<<20)
STR R1,[R0]
@3.设置PE10管脚为推挽输出  GPIOE_OTYPER[10]->0   0X50006004
LDR R0,=0x50006004
LDR R1,[R0]
BIC R1,R1,#(0x1<<20)
STR R1,[R0]
@4.设置PE10管脚为低速输出  GPIOE_OTYPER[21:20]->00   0X50006008
LDR R0,=0x50006008
LDR R1,[R0]
BIC R1,R1,#(0x3<<20)
STR R1,[R0]
@5.设置PE10管脚输出时没有上拉下拉电阻  GPIOE_PUPDR[21:20]->00  0X5000600C
LDR R0,=0x5000600c
LDR R1,[R0]
BIC R1,R1,#(0x3<<20)
STR R1,[R0]
 
@LED2
@2.设置PF10管脚为输出模式  GPIOE_MODER[21:20]->01   0x50007000
LDR R0,=0x50007000
LDR R1,[R0]
BIC R1,R1,#(0x3<<20)
ORR R1,R1,#(0x1<<20)
STR R1,[R0]
@3.设置PF10管脚为推挽输出  GPIOE_OTYPER[10]->0   0X50007004
LDR R0,=0x50007004
LDR R1,[R0]
BIC R1,R1,#(0x1<<20)
STR R1,[R0]
@4.设置PF10管脚为低速输出  GPIOE_OTYPER[21:20]->00   0X50007008
LDR R0,=0x50007008
LDR R1,[R0]
BIC R1,R1,#(0x3<<20)
STR R1,[R0]
@5.设置PF10管脚输出时没有上拉下拉电阻  GPIOE_PUPDR[21:20]->00  0X5000700C
LDR R0,=0x5000700c
LDR R1,[R0]
BIC R1,R1,#(0x3<<20)
STR R1,[R0]
 
 
@LED3
@1.设置PE8管脚为输出模式  GPIOE_MODER[21:20]->01   0x50006000
LDR R0,=0x50006000
LDR R1,[R0]
BIC R1,R1,#(0x3<<16)
ORR R1,R1,#(0x1<<16)
STR R1,[R0]
@2.设置PE8管脚为推挽输出  GPIOE_OTYPER[10]->0   0X50006004
LDR R0,=0x50006004
LDR R1,[R0]
BIC R1,R1,#(0x1<<16)
STR R1,[R0]
@4.设置PE8管脚为低速输出  GPIOE_OTYPER[21:20]->00   0X50006008
LDR R0,=0x50006008
LDR R1,[R0]
BIC R1,R1,#(0x3<<16)
STR R1,[R0]
@5.设置PE8管脚输出时没有上拉下拉电阻  GPIOE_PUPDR[21:20]->00  0X5000600C
LDR R0,=0x5000600c
LDR R1,[R0]
BIC R1,R1,#(0x3<<16)
STR R1,[R0]
 
 
@  6.PE10输出高低电平      GPIOE_ODR[10]->1(高电平)  0(低电平)    0X50006014
 
@LED1亮灭交替
loop:
    @亮
    LDR R0,=0X50006014
    LDR R1,[R0]
    ORR R1,R1,#(0x1<<10)
    STR R1,[R0]
    BL delay
    @灭
    LDR R0,=0X50006014
    LDR R1,[R0]
    BIC R1,R1,#(0x1<<10)
    STR R1,[R0]
    BL delay
    
 
@LED2亮灭交替
 
    @亮
    LDR R0,=0X50007014
    LDR R1,[R0]
    ORR R1,R1,#(0x1<<10)
    STR R1,[R0]
    BL delay
    @灭
    LDR R0,=0X50007014
    LDR R1,[R0]
    BIC R1,R1,#(0x1<<10)
    STR R1,[R0]
    BL delay
 
 
@LED3亮灭交替
 
    @亮
    LDR R0,=0X50006014
    LDR R1,[R0]
    ORR R1,R1,#(0x1<<8)
    STR R1,[R0]
    BL delay
    @灭
    LDR R0,=0X50006014
    LDR R1,[R0]
    BIC R1,R1,#(0x1<<8)
    STR R1,[R0]
    BL delay
    b loop
 
 
 
@封装延时函数
 
delay:
    LDR R4,=0X10000000
mm:
    cmp r4,#0
    subne r4,r4,#1
    bne mm
    mov pc,lr  @程序返回
 
 
.end

————————————————

 

本文出自:https://blog.csdn.net/YSTXDY/article/details/133689998

docker-compose一键启动neo4j

下载镜像

docker pull neo4j:3.5.22-community

编写配置文件
参考文档

编写docker-compose.yml文件

version: "3"

services:
    neo4j:
        image: neo4j:3.5.22-community
        container_name: neo4j 
        restart: always
        ports:
            - 7474:7474
            - 7687:7687
        environment:
            - NEO4J_AUTH:neo4j/neofj
            - TZ:Asia/Shanghai
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /root/neo4j/data:/data
            - /root/neo4j/logs:/logs
            - /root/neo4j/conf:/var/lib/neo4j/conf
            - /root/neo4j/import:/var/lib/neo4j/import
        logging:
            driver: "json-file"
            options:
                max-size: "10m"

networks:
    default:
        external:
            name: my-docker-compose-network

一键启动
docker-compose up -d

本文出自:https://blog.csdn.net/weixin_42289273/article/details/133514499

【JAVA】为什么要使用封装以及如何封装

前言

Java的封装指的是在一个类中将数据和方法进行封装,使其可以保护起来,只能在该类内部访问,而不允许外部直接访问和修改。这是Java面向对象编程的三个基本特性之一,另外两个是继承和多态。在此之前我们已经学习过关于继承的概念,今天我们来学习有关封装的内容。

 

 

封装的含义

封装是面向对象编程的一种重要概念,是将数据和对数据的操作封装在一个类中,使得数据对外部的访问受到限制,只能通过类中的公共方法来访问或操作。封装的目的是隐藏类的实现细节,并且保护数据不被随意修改,从而增强了代码的安全性和可维护性。另外,封装也使得代码的扩展和修改更加方便,只需在类内部进行修改而不需要修改其他代码。

 

通过封装,可以提高类的安全性和可维护性,使得类的实现细节被隐藏,只暴露出有限的接口和功能给外部使用,从而减少了类之间的耦合性。同时,封装还可以实现数据隐藏和数据保护,提高程序的可靠性和可扩展性。

 

四种访问控制符

Java中,采用访问控制符来控制类中数据成员和方法的访问权限,主要有四种访问控制符:public、private、protected和默认访问控制符(即不写访问控制符)。

 

public:表示该数据成员或方法可以被其他任何类访问。

private:表示该数据成员或方法只能在当前类内部访问。

protected:表示该数据成员或方法可以在当前类和其子类中访问。

默认访问控制符:表示该数据成员或方法只能在同一个包内的其他类中访问。

封装的两种经典实例

银行账户类

该类的主要属性包括账户名、账号、账户余额等。其中账户余额是一个私有属性,外部无法直接访问。类中提供了一系列操作账户的方法,例如存款、取款、查询余额等。这些方法都能够在保证账户余额正确的情况下,修改账户余额。

public class BankAccount {
    private String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }

    public String getAccountNumber() {
        return accountNumber;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        balance += amount;
    }

    public void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
        } else {
            System.out.println("Insufficient balance.");
        }
    }
}

accountNumber: 表示账户号码的字符串。

balance: 表示账户余额的双精度浮点数。

BankAccount(String accountNumber, double balance): 构造函数,用于创建一个新的银行账户对象。

getAccountNumber(): 返回账户号码的方法。

getBalance(): 返回账户余额的方法。

deposit(double amount): 存款方法,用于向账户中添加资金。

withdraw(double amount): 取款方法,用于从账户中扣除资金。如果账户余额不足,将输出一条错误信息。

学生类

public class Student {
    // 属性
    private String name;
    private int age;
    private String gender;
    private String id;

    // 构造方法
    public Student(String name, int age, String gender, String id) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.id = id;
    }

    // 方法
    public void study() {
        System.out.println("学生正在学习");
    }

    public void showInfo() {
        System.out.println("姓名:" + name);
        System.out.println("年龄:" + age);
        System.out.println("性别:" + gender);
        System.out.println("学号:" + id);
    }

    // Getter和Setter方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

属性:学生类具有姓名、年龄、性别和学号这些属性。

构造方法:学生类具有一个带四个参数的构造方法,用于初始化学生对象。

方法:学生类具有学习方法和展示学生信息的方法。

Getter和Setter方法:学生类具有获取和设置属性值的Getter和Setter方法,用于保护属性的私有性。

文章知识点与官方知识档案匹配,可进一步学习相关知识

本文出自:https://blog.csdn.net/weixin_73602725/article/details/133498865

网络编程和套接字操作

网络编程是计算机编程中重要的领域之一,它使程序能够在网络上进行数据传输和通信。C语言是一种强大的编程语言,也可以用于网络编程。网络编程通常涉及套接字(Socket)操作,套接字是一种用于网络通信的抽象接口。本文将详细讨论如何进行C语言网络编程和套接字操作。

 

第一部分:网络编程基础

在开始学习C语言网络编程之前,首先需要理解一些基本的概念和术语。

 

1.1 IP地址和端口号

IP地址:IP地址是用于标识计算机或设备在网络上的唯一地址。IPv4和IPv6是两种常用的IP地址协议,它们用于不同的网络环境。IPv4地址通常表示为四个十进制数,如192.168.1.1,而IPv6地址更长,以冒号分隔,如2001:0db8:85a3:0000:0000:8a2e:0370:7334。

 

端口号:端口号用于标识正在运行的应用程序或服务。它是一个16位的整数,范围从0到65535。常见的端口号已被分配给特定的应用程序,例如HTTP通常使用端口80,HTTPS使用端口443。

 

1.2 客户端和服务器

在网络编程中,有两个主要的角色:客户端和服务器。

 

客户端:客户端是发送请求并接收服务器响应的程序或设备。它通常主动发起连接,并向服务器请求数据或服务。

 

服务器:服务器是接受客户端请求并提供相应服务的程序或设备。它通常监听一个特定的端口,并等待客户端连接。

 

1.3 套接字(Socket)

套接字是网络编程中的核心概念。套接字允许程序通过网络进行通信,可以是TCP套接字或UDP套接字。

 

TCP套接字:TCP(传输控制协议)提供可靠的、面向连接的通信。TCP套接字用于建立可靠的双向通信通道,数据按顺序传输,且不丢失。

 

UDP套接字:UDP(用户数据报协议)提供不可靠的、面向数据包的通信。UDP套接字用于快速数据传输,但不能保证数据的顺序和可靠性。

 

第二部分:套接字编程步骤

进行网络编程和套接字操作通常涉及以下步骤:

 

2.1 包含头文件

要进行套接字编程,需要包含C语言的相关头文件。最常用的头文件是<sys/socket.h>、<netinet/in.h>和<arpa/inet.h>。这些头文件包含了套接字函数和网络地址结构的定义。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

2.2 创建套接字

创建套接字是网络编程的第一步。使用socket函数来创建套接字,该函数的原型如下

 

int socket(int domain, int type, int protocol);

domain参数指定套接字的协议族,常用的是AF_INET(IPv4)和AF_INET6(IPv6)。

 

type参数指定套接字的类型,常用的有SOCK_STREAM(用于TCP)和SOCK_DGRAM(用于UDP)。

 

protocol参数通常设置为0,以便根据domain和type自动选择协议。

 

以下是创建TCP套接字的示例:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("Socket creation failed");
    // 处理错误
}

2.3 定义服务器和客户端地址

在网络通信中,服务器和客户端都需要定义自己的地址信息。这些信息包括IP地址和端口号。

定义服务器地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;            // 使用IPv4
server_addr.sin_port = htons(PORT_NUMBER);   // 设置端口号(需要使用htons函数进行字节序转换)
server_addr.sin_addr.s_addr = INADDR_ANY;    // 监听所有可用的网络接口
定义客户端地址
struct sockaddr_in client_addr;
client_addr.sin_family = AF_INET;            // 使用IPv4
client_addr.sin_port = htons(PORT_NUMBER);   // 设置服务器端口号
client_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 设置服务器IP地址

2.4 绑定套接字(服务器端)

在服务器端,需要将套接字绑定到一个特定的IP地址和端口号上,以便监听客户端连接。使用bind函数来执行绑定操作:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd是套接字描述符,即要绑定的套接字。

addr是一个指向sockaddr结构的指针,其中包含要绑定的地址信息。

addrlen是addr结构的长度。

以下是服务器端绑定套接字的示例:

if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    perror("Bind failed");
    // 处理错误
}

2.5 监听连接请求(服务器端)

在服务器端,需要使用listen函数开始监听客户端的连接请求:

 

int listen(int sockfd, int backlog);

sockfd是套接字描述符,即要监听的套接字。

 

backlog是等待连接队列的最大长度,通常设置为5或更大。

 

以下是服务器端监听连接请求的示例:

if (listen(sockfd, 5) == -1) {
    perror("Listen failed");
    // 处理错误
}

2.6 接受连接(服务器端)

当客户端尝试连接到服务器时,服务器需要使用accept函数接受连接请求并创建新的套接字用于通信:

 

int new_socket = accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd是监听套接字的描述符。

 

addr是一个指向sockaddr结构的指针,用于存储客户端的地址信息。

 

addrlen是addr结构的长度。

 

accept函数将返回一个新的套接字描述符,用于与客户端通信。服务器可以使用此套接字与客户端进行数据交换。

 

以下是服务器端接受连接的示例:

int new_socket;
socklen_t addr_len = sizeof(client_addr);
new_socket = accept(sockfd, (struct sockaddr *)&client_addr, &addr_len);
if (new_socket == -1) {
    perror("Accept failed");
    // 处理错误
}

2.7 连接到服务器(客户端)

在客户端,需要使用connect函数连接到服务器:

 

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd是客户端套接字的描述符。

 

addr是一个指向服务器地址的指针。

 

addrlen是服务器地址结构的长度。

 

以下是客户端连接到服务器的示例:

if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    perror("Connection failed");
    // 处理错误
}

2.8 发送和接收数据

一旦连接建立,服务器和客户端可以使用send和recv函数来发送和接收数据。

 

发送数据

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

sockfd是套接字描述符。

 

buf是包含要发送数据的缓冲区的指针。

 

len是要发送的数据的字节数。

 

flags通常设置为0。

 

以下是发送数据的示例:

char message[] = "Hello, Server!";
send(new_socket, message, strlen(message), 0);
接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd是套接字描述符。

buf是用于存储接收数据的缓冲区的指针。

 

len是缓冲区的大小,表示最多可以接收多少字节的数据。

 

flags通常设置为0。

 

以下是接收数据的示例:

char buffer[1024];
ssize_t bytes_received = recv(new_socket, buffer, sizeof(buffer), 0);
if (bytes_received == -1) {
    perror("Receive failed");
    // 处理错误
} else {
    buffer[bytes_received] = '\0'; // 添加字符串结束符
    printf("Received: %s\n", buffer);
}

2.9 关闭套接字

在完成通信后,服务器和客户端应使用close函数关闭套接字以释放资源:

 

int close(int sockfd);

sockfd是要关闭的套接字描述符。

以下是关闭套接字的示例:

 

close(new_socket); // 关闭与客户端的套接字

close(sockfd);     // 关闭服务器监听套接字

第三部分:完整的示例

以下是一个简单的C语言网络编程示例,其中包括了创建服务器和客户端的代码。

服务器端示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define PORT_NUMBER 8080
 
int main() {
    int sockfd, new_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
 
    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        exit(1);
    }
 
    // 定义服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT_NUMBER);
    server_addr.sin_addr.s_addr = INADDR_ANY;
 
    // 绑定套接字到地址
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        exit(1);
    }
 
    // 监听连接请求
    if (listen(sockfd, 5) == -1) {
        perror("Listen failed");
        exit(1);
    }
 
    // 接受连接
    new_socket = accept(sockfd, (struct sockaddr *)&client_addr, &addr_len);
    if (new_socket == -1) {
        perror("Accept failed");
        exit(1);
    }
 
    // 接收数据
    char buffer[1024];
    ssize_t bytes_received = recv(new_socket, buffer, sizeof(buffer), 0);
    if (bytes_received == -1) {
        perror("Receive failed");
        exit(1);
    }
    buffer[bytes_received] = '\0'; // 添加字符串结束符
    printf("Received: %s\n", buffer);
 
    // 发送响应
    const char *response = "Hello, Client!";
    send(new_socket, response, strlen(response), 0);
 
    // 关闭套接字
    close(new_socket);
    close(sockfd);
 
    return 0;
}

客户端示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define SERVER_IP "127.0.0.1"
#define PORT_NUMBER 8080
 
int main() {
    int sockfd;
    struct sockaddr_in server_addr;
 
    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        exit(1);
    }
 
    // 定义服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT_NUMBER);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
 
    // 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Connection failed");
        exit(1);
    }
 
    // 发送数据
    const char *message = "Hello, Server!";
    send(sockfd, message, strlen(message), 0);
 
    // 接收响应
    char buffer[1024];
    ssize_t bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
    if (bytes_received == -1) {
        perror("Receive failed");
        exit(1);
    }
    buffer[bytes_received] = '\0'; // 添加字符串结束符
    printf("Received: %s\n", buffer);
 
    // 关闭套接字
    close(sockfd);
 
    return 0;
}

第四部分:网络编程进阶

以上示例介绍了基本的套接字编程步骤,但网络编程还涉及到更多高级概念和技术,例如多线程服务器、非阻塞套接字、套接字选项、数据序列化和安全性等。要深入学习网络编程,建议查阅相关文档和书籍,并尝试构建更复杂的网络应用程序。

 

此外,网络编程也需要考虑安全性和性能方面的问题。在实际应用中,您需要谨慎处理输入数据,防止网络攻击,并优化网络通信以提高性能。

 

总之,C语言网络编程是一个广阔而有趣的领域,它允许您构建各种网络应用程序,从简单的聊天客户端到复杂的服务器应用程序。通过深入学习套接字编程和网络协议,您将能够更好地理解和掌握网络编程的原理和实践。

本文出自:https://blog.csdn.net/weixin_68551689/article/details/133443429

如何使用 Java 灵活读取 Excel 内容?

在日常工作和学习中,我们经常需要从 Excel 文件中读取数据。对于少量数据,手动复制和粘贴可能是可行的,但是对于大量的数据,这种方法会变得非常低效。在这种情况下,使用 Java 程序自动读取 Excel 文件中的数据将是更好的选择。

 

本教程将介绍如何使用 Java 程序读取 Excel 文件中的数据,包括以下几个方面:

 

了解 Excel 文件格式

选择 Java 的 Excel 文件读取库

读取 Excel 文件中的数据

处理 Excel 文件中的不同数据类型

使用 Java 程序导出 Excel 文件

让我们开始吧!

 

1、了解 Excel 文件格式

在开始编写 Java 程序读取 Excel 文件之前,我们需要了解 Excel 文件格式。Excel 文件是基于 Microsoft 的 OLE2 格式的,这个格式将文件组织成一系列的 “容器” 和 “对象”。Excel 文件的主要容器是 Workbook,它包含一个或多个 Sheet。每个 Sheet 包含一组行和列,这些行和列组成了一个二维的单元格数组。每个单元格可以包含文本、数字、日期、布尔值等不同的数据类型。

 

2、选择 Java 的 Excel 文件读取库

在 Java 中,有多个库可以用于读取 Excel 文件。这些库包括 Apache POI、JExcelApi、EasyXLS、JXL 等。在本教程中,我们将使用 Apache POI 库。

 

Apache POI 是一个 Java API,用于操作 Microsoft 文档格式,包括 Word、Excel 和 PowerPoint。它提供了一组 Java 类,可以用于创建、读取和修改 Microsoft 文档。

 

3、读取 Excel 文件中的数据

在本节中,我们将演示如何使用 Apache POI 库读取 Excel 文件中的数据。

 

在开始之前,我们需要将 Apache POI 库添加到项目的依赖中。您可以通过 Maven 或 Gradle 等构建工具将它们添加到项目中。

 

以下是 Maven 项目的 pom.xml 文件中添加 Apache POI 库的示例代码:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>

以下是 Gradle 项目的 build.gradle 文件中添加 Apache POI 库的示例代码:

dependencies {
    implementation 'org.apache.poi:poi:4.1.2'
}

接下来,我们将演示如何使用 Apache POI 库读取 Excel 文件中的数据。

 

首先,我们需要创建一个 Workbook 对象,该对象表示整个 Excel 文件。在 Apache POI 中,Workbook 对象有三种不同的类型:HSSFWorkbook 表示一个 .xls 文件,XSSFWorkbook 表示一个 .xlsx 文件,SXSSFWorkbook 表示一个大型 .xlsx 文件。我们可以使用 WorkbookFactory.create() 方法根据文件的类型创建一个 Workbook 对象。以下是创建一个 XSSFWorkbook 对象的示例代码:

 

FileInputStream file = new FileInputStream(new File(“path/to/excel/file.xlsx”));

Workbook workbook = WorkbookFactory.create(file);

接下来,我们可以获取一个 Sheet 对象,该对象表示 Excel 文件中的一个工作表。在 Apache POI 中,Sheet 对象由 Workbook 对象的 getSheet() 方法返回。以下是获取名为 “Sheet1” 的 Sheet 对象的示例代码:

 

Sheet sheet = workbook.getSheet(“Sheet1”);

一旦我们获取了 Sheet 对象,我们就可以遍历其中的每一行和每一列,以读取其中的单元格数据。以下是遍历 Sheet 对象中的所有单元格并输出单元格值的示例代码:

Iterator<Row> rowIterator = sheet.iterator();
while (rowIterator.hasNext()) {
    Row row = rowIterator.next();
    Iterator<Cell> cellIterator = row.cellIterator();
    while (cellIterator.hasNext()) {
        Cell cell = cellIterator.next();
        switch (cell.getCellType()) {
            case STRING:
                System.out.print(cell.getStringCellValue() + "\t");
                break;
            case NUMERIC:
                System.out.print(cell.getNumericCellValue() + "\t");
                break;
            case BOOLEAN:
                System.out.print(cell.getBooleanCellValue() + "\t");
                break;
            default:
                System.out.print("\t");
        }
    }
    System.out.println();
}

上述代码中,我们使用了一个嵌套的迭代器来遍历每一行和每一列。在每个单元格中,我们使用 switch 语句根据不同的数据类型执行不同的操作。

 

4、处理 Excel 文件中的不同数据类型

在 Excel 文件中,单元格可以包含不同的数据类型,包括字符串、数字、日期和布尔值等。当我们读取单元格中的数据时,我们需要知道单元格中的数据类型,以正确地处理它们。

 

在 Apache POI 中,我们可以使用 Cell.getCellType() 方法获取单元格的数据类型。该方法返回 CellType 枚举类型的值,可以使用 switch 语句根据不同的枚举值执行不同的操作。

 

以下是使用 switch 语句处理不同数据类型的示例代码:

switch (cell.getCellType()) {
    case STRING:
        System.out.print(cell.getStringCellValue() + "\t");
        break;
    case NUMERIC:
        if (DateUtil.isCellDateFormatted(cell)) {
            System.out.print(cell.getDateCellValue() + "\t");
        } else {
            System.out.print(cell.getNumericCellValue() + "\t");
        }
        break;
    case BOOLEAN:
        System.out.print(cell.getBooleanCellValue() + "\t");
        break;
    default:
        System.out.print("\t");
}

在上述示例代码中,我们首先检查单元格的数据类型是否为字符串。如果是字符串,我们使用 getStringCellValue() 方法获取字符串值。如果单元格的数据类型为数字,我们还需要检查单元格是否包含日期。如果是日期,我们使用 getDateCellValue() 方法获取日期值,否则我们使用 getNumericCellValue() 方法获取数字值。

 

5、使用 Java 程序导出 Excel 文件

除了读取 Excel 文件中的数据外,我们还可以使用 Java 程序创建和导出 Excel 文件。Apache POI 提供了 HSSFWorkbook 和 XSSFWorkbook 两个类,用于创建和操作 Excel 文件。

 

HSSFWorkbook 类用于操作旧版的 .xls 格式的 Excel 文件,而 XSSFWorkbook 类用于操作新版的 .xlsx 格式的 Excel 文件。我们可以根据需要选择合适的类来创建和导出 Excel 文件。

 

以下是创建和导出 Excel 文件的一般步骤:

 

创建一个 Workbook 对象,表示整个 Excel 文件。

创建一个 Sheet 对象,表示 Excel 文件中的一个工作表。

创建 Row 和 Cell 对象,表示 Excel 文件中的行和列。

为每个单元格设置数据和样式。

将 Workbook 对象写入文件或输出流中。

下面,让我们来看一些示例代码,演示如何使用 Java 程序创建和导出 Excel 文件。

 

创建一个 XSSFWorkbook 对象

以下是创建一个 XSSFWorkbook 对象的示例代码:

Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Sheet1");

创建行和单元格

以下是创建行和单元格的示例代码:

Row row = sheet.createRow(0);
Cell cell = row.createCell(0);
cell.setCellValue("Hello, World!");

设置单元格样式

在 Excel 文件中,我们可以为单元格设置不同的样式,包括字体、颜色、对齐方式等。以下是设置单元格样式的示例代码:

CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
cell.setCellStyle(style);

在上述示例代码中,我们首先使用 createCellStyle() 方法创建一个单元格样式对象。然后,我们使用 createFont() 方法创建一个字体对象,并设置其属性。最后,我们将样式应用于单元格。

 

导出 Excel 文件

导出 Excel 文件时,我们可以将 Workbook 对象写入文件或输出流中。以下是将 Workbook 对象写入文件的示例代码:

FileOutputStream fileOut = new FileOutputStream("path/to/excel/file.xlsx");
workbook.write(fileOut);
fileOut.close();

在上述示例代码中,我们首先使用 FileOutputStream 类创建一个文件输出流对象。然后,我们使用 write() 方法将 Workbook 对象写入文件中。最后,我们关闭文件输出流。

 

除了写入文件,我们还可以将 Workbook 对象输出到网络流中,以便在浏览器中下载 Excel 文件。以下是将 Workbook 对象输出到网络流中的示例代码:

response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=\"excel_file.xlsx\"");
OutputStream outputStream = response.getOutputStream();
workbook.write(outputStream);
outputStream.close();

在上述示例代码中,我们首先设置 HTTP 响应的内容类型和文件名。然后,我们使用 getOutputStream() 方法获取网络输出流对象。最后,我们使用 write() 方法将 Workbook 对象写入网络流中,并关闭输出流。

 

完整示例代码

 

下面是一个完整的示例代码,演示如何使用 Java 程序读取和导出 Excel 文件:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
 
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 
public class ExcelUtils {
 
    public static void readExcel(String fileName) throws IOException {
        // 创建文件输入流
        FileInputStream fileInputStream = new FileInputStream(new File(fileName));
 
        // 创建工作簿对象
        Workbook workbook = WorkbookFactory.create(fileInputStream);
 
        // 获取第一个工作表
        Sheet sheet = workbook.getSheetAt(0);
 
        // 遍历工作表中的所有行和单元格
        for (Row row : sheet) {
            for (Cell cell : row) {
                // 根据单元格的类型读取数据
                switch (cell.getCellType()) {
                    case STRING:
                        System.out.print(cell.getStringCellValue() + "\t");
                        break;
                    case NUMERIC:
                        System.out.print(cell.getNumericCellValue() + "\t");
                        break;
                    case BOOLEAN:
                        System.out.print(cell.getBooleanCellValue() + "\t");
                        break;
                    case FORMULA:
                        System.out.print(cell.getCellFormula() + "\t");
                        break;
                    case BLANK:
                        System.out.print("\t");
                        break;
                    default:
                        System.out.print("\t");
                }
            }
            System.out.println();
        }
 
        // 关闭文件输入流
        fileInputStream.close();
    }
 
    public static void writeExcel(String fileName) throws IOException {
        // 创建工作簿对象
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("Sheet1");
 
        // 创建行和单元格
        Row row = sheet.createRow(0);
        Cell cell = row.createCell(0);
        cell.setCellValue("Hello, World!");
 
        // 设置单元格样式
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        cell.setCellStyle(style);
 
        // 导出 Excel 文件
        FileOutputStream fileOut = new FileOutputStream(fileName);
        workbook.write(fileOut);
        fileOut.close();
    }
}
使用该类,我们可以方便地读取和导出 Excel 文件,如下所示:

public static void main(String[] args) throws IOException {
    String fileName = "path/to/excel/file.xlsx";
 
    // 读取 Excel 文件
    ExcelUtils.readExcel(fileName);
 
    // 导出 Excel 文件
    ExcelUtils.writeExcel(fileName);
}

6、结论

本文介绍了如何使用 Java 程序灵活读取和导出 Excel 文件。我们首先介绍了 Apache POI 库,它是 Java 操作 Office 文档的一个强大的工具。然后,我们演示了如何使用 POI 库读取 Excel 文件,并解释了如何根据单元如何设置单元格样式。最后,我们提供了一个完整的示例代码,演示了如何使用 ExcelUtils 类来读取和导出 Excel 文件。

 

Java 程序读取和导出 Excel 文件是一项非常实用的技能,尤其是在需要处理大量数据的项目中。使用 Apache POI 库,我们可以方便地读取和写入 Excel 文件,这可以大大提高我们的工作效率。

 

在实际应用中,我们需要根据具体的需求来选择读取和写入 Excel 文件的方法,以达到最好的效果。

本文出自:https://blog.csdn.net/bairo007/article/details/131818662

JAVA 泛型、序列化和复制

泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。比如我们要写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,我们就可以使用 Java 泛型。

 

1 泛型方法(<E>)

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
}
  1. <? extends T>表示该通配符所代表的类型是 T 类型的子类。
  2. <? super T>表示该通配符所代表的类型是 T 类型的父类。

 

2 泛型类<T>

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}

3 类型通配符?

类 型 通 配 符 一 般 是 使 用 ? 代 替 具 体 的 类 型 参 数 。 例 如 List<?> 在 逻 辑 上 是

List<String>,List<Integer> 等所有 List<具体类型实参>的父类。

 

4 类型擦除

Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个

过程就称为类型擦除。如在代码中定义的 List<Object>和 List<String>等类型,在编译之后

都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。

类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般

是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换

成具体的类。

 

5 JAVA 序列化( 创建可复用的 Java 对象)

保存 ( 持久化 ) 对象 及其状态到内存或者磁盘

Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java 对象序列化就能够帮助我们实现该功能。

序列化对象以字节数组保持 – 静态成员不保存

使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对

象序列化不会关注类中的静态变量。

序列化用户远程对象传输

除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。

Serializable 实现序列化

在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。

ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化

通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。

writeObject 和 readObject 自定义序列化策略

在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。

序列化 ID

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个

类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

 

序列化并不保存静态变量

序列化子父类说明

要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。

Transient 关键字 阻止该变量被序列化到文件中

 

在变量声明前加上 Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

6 JAVA 复制

将一个对象的引用复制给另外一个对象,一共有三种方式。第一种方式是直接赋值,第二种方式是浅拷贝,第三种是深拷贝。所以大家知道了哈,这三种概念实际上都是为了拷贝对象。

 

6.1 直接赋值复制

直接赋值。在 Java 中,A a1 = a2,我们需要理解的是这实际上复制的是引用,也就是说 a1 和 a2 指向的是同一个对象。因此,当 a1 变化的时候,a2 里面的成员变量也会跟着变化。

 

6.2 浅复制(复制引用但不复制引用的对象)

创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

class Resume implements Cloneable{
public Object clone() {
try {
return (Resume)super.clone();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

6.3 深复制(复制对象和其应用对象)

深拷贝不仅复制对象本身,而且复制对象包含的引用指向的所有对象。

 

class Student implements Cloneable {

String name;

int age;

Professor p;

Student(String name, int age, Professor p) {

this.name = name;

this.age = age;

this.p = p;

}

public Object clone() {

Student o = null;

try {

o = (Student) super.clone();

} catch (CloneNotSupportedException e) {

System.out.println(e.toString());

}

o.p = (Professor) p.clone();

return o;

}

}

 

6.4 序列化(深 clone 一中实现)

在 Java 语言里深复制一个对象,常常可以先使对象实现 Serializable 接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建象。

本文出自:https://blog.csdn.net/Lj_chuxuezhe/article/details/133469762

Docker启动安装nacos

前言

安装之前你需要准备一个mysql,当前安装方式是将数据持久化到数据库中的,这里的部署是单机模式

1、Docker 拉取镜像

docker pull nacos/nacos-server

PS:这是拉取最新的nacos版本,如果需要拉取别的版本可以加:版本号(如:docker pull nacos/nacos-server:v2.2.0)

2、挂载目录

mkdir -p /mydata/nacos/logs/                    #新建logs目录
mkdir -p /mydata/nacos/conf/			#新建conf目录

PS:这一步是添加映射文件夹,将宿主机的文件映射到nacos容器中

3、启动nacos并复制文件到宿主机,关闭容器

启动容器

docker run -p 8848:8848 --name nacos -d nacos/nacos-server

复制文件

docker cp nacos:/home/nacos/logs/ /mydata/nacos/
docker cp nacos:/home/nacos/conf/ /mydata/nacos/

关闭容器

docker rm -f nacos

PS:这一步启动nacos是为了将nacos里面的文件拷贝出到挂载目录中,这样我们就可以直接修改挂载目录中文件来映射到容器里面去了

4、mysql中创建nacos所需的表

mysql中新建一个库,名字可自定义,这里就用nacos-config

从github中找到创建表的文件,在nacos-config库中执行,创建所需的表

5、再次启动nacos

docker run -d
--name nacos															 
-p 8848:8848  -p 9848:9848 -p 9849:9849
--privileged=true
-e JVM_XMS=256m
-e JVM_XMX=256m
-e MODE=standalone
-v /mydata/nacos/logs/:/home/nacos/logs
-v /mydata/nacos/conf/:/home/nacos/conf/
--restart=always
nacos/nacos-server

PS : 复制上面的语句执行失败,可以将上面的执行语句变成一行,如下,可直接复制执行

docker run -d --name nacos -p 8848:8848  -p 9848:9848 -p 9849:9849 --privileged=true -e JVM_XMS=256m -e JVM_XMX=256m -e MODE=standalone -v /mydata/nacos/logs/:/home/nacos/logs -v /mydata/nacos/conf/:/home/nacos/conf/ --restart=always nacos/nacos-server

语句讲解

docker run -d : 启动容器 -d是后台启动并返回容器id的意思

–name nacos :为容器指定一个名称

-p 8848:8848 -p 9848:9848 -p 9849:9849 : 指定端口映射,注意这里的p不能大写,大写是随机端口映射

–privileged=true : 扩大容器内的权限,将容器内的权限变为root权限,不加的话就是普通用户权限,可能会出现cannot open directory

-e JVM_XMS=256m : 为jvm启动时分配的内存

-e JVM_XMX=256m : 为jvm运行过程中分配的最大内存

-e MODE=standalone : 使用 standalone模式(单机模式),MODE值有cluster(集群)模式/standalone模式两种,MODE必须大写

-v /mydata/nacos/logs/:/home/nacos/logs : 将容器的/home/nacos/logs目录挂载到 /mydata/nacos/logs

-v /mydata/nacos/conf/:/home/nacos/conf/: 将容器的/home/nacos/conf目录挂载到 /mydata/nacos/conf

–restart=always :重启docker时,自动启动相关容器

注意事项

需要在防火墙开放相关端口,如果你是云服务器,开放安全组,下面提供相关语句

## 开放端口8848 9848 9849
firewall-cmd --zone=public --add-port=8848/tcp --permanent
firewall-cmd --zone=public --add-port=9848/tcp --permanent
firewall-cmd --zone=public --add-port=9849/tcp --permanent

## 重启防火墙
firewall-cmd --reload

## 查看所有开启的端口
firewall-cmd --zone=public --list-ports

PS:这里有点小问题,重启完防火墙之后,需要重启docker

## 重启docker
systemctl restart docker

这里最容易犯错的就是挂载目录对应不上,可以看下自己语句中的-v 后面的目录是否映射正确,博主第一次安装的时候logs里面还有一个logs文件夹,conf里面还有个conf文件夹,导致出错

6、修改配置文件

主要修改的是application.properties文件

## 在宿主机中修改application.properties文件
vim /mydata/nacos/conf/application.properties

文件修改的地方(修改为你对应的mysql)

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=30000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user=root
db.password=root

PS :因为我们在第三步已经复制了logs和conf到了我们的宿主机里面,那么我们可以直接修改application.properties文件映射到容器里,可以通过下面的语句来查看

## 进入到nacos容器里
docker exec -it nacos /bin/bash

## 查看application.properties文件
cat /home/nacos/conf/application.properties

## 退出容器
exit

PS:如果文件没有修改成功的话,说明挂载目录映射的不对,仔细查看一下,可以通过下面的方法来查看是否正确

docker inspect --format="{{json .Mounts}}" nacos

7、访问页面

http://ip:8848/nacos/index.html

 

本文出自:https://blog.csdn.net/ilvjiale/article/details/129417768