美文网首页JAVA
SpringBoot(二) SpringBoot开发实例

SpringBoot(二) SpringBoot开发实例

作者: TiaNa_na | 来源:发表于2019-11-19 14:29 被阅读0次

本文主要是项目开发过程中常用功能的总结。
下文我提到的部分功能都可以结合Hutool来实现。所以先来了解一下Hutool文档:https://www.hutool.club/docs/#/

一 发送邮件
1 引入依赖

我们可以使用Hutool的MailUtil来发送邮箱,需要加入Hutool和MailUtil的依赖。

// hutool依赖
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.0.3</version>
</dependency>
// 发送邮件依赖
<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.7</version>
</dependency>
2 邮箱配置

要想实现发邮件功能,就得配置发件邮箱。现在我以qq邮箱和腾讯企业邮箱为例说明。

  • qq邮箱
    登录邮箱,依次点击设置-->账户-->POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务,看到如下界面:


    默认情况下,POP3/SMTP服务是关闭的,我们需要开启它,按照提示,我们最终可以得到授权码,这个我们后面需要用到。
  • 腾讯企业邮箱
    企业邮箱不需要授权码。

3 功能实现

在classpath(在标准Maven项目中为src/main/resources)目录或在classpath的config目录下新建mail.setting文件。
mail.setting之所以要放到上述目录下,是Hutool源码中所规定的。

   #收件人邮箱
    to = xxx 
    # 腾讯企业邮箱配置
    # 邮件服务器的SMTP地址
    host = smtp.exmail.qq.com
    # 邮件服务器的SMTP端口
    port = 465
    # 发件人邮箱(必须正确,否则发送失败)
    from = xxx
    # 用户名,默认为发件人邮箱前缀,我填的与from一致
    user = xxx
    # 授权码
    pass = 上一步邮箱配置里获得的授权码(QQ邮箱)或密码(腾讯企业邮箱)
    # 在使用QQ或Gmail邮箱时,需要强制开启SSL支持
    sslEnable = true

    #qq邮箱配置
    # host = smtp.qq.com
    # 邮件服务器的SMTP端口
    # port = 465
    # 发件人(必须正确,否则发送失败)
    # from = xxx@qq.com
    # 用户名,默认为发件人邮箱前缀
    # user = xxx@qq.com
    # 授权码
    # pass = 上一步邮箱配置里获得的授权码
    # 在使用QQ或Gmail邮箱时,需要强制开启SSL支持
    sslEnable = true

配置完成后,就可以使用Hutool的发送邮件功能了。

//读取classpath下的mail.setting
Setting setting = new Setting("mail.setting");
//获取收件人
String to = setting.getStr("to");
MailUtil.send(to, "测试", “测试内容”, true);

以上只是发送了普通文本邮件,参照官方文档,你也可以实现发送HTML格式的邮件并附带附件以及群发的功能。
下面我们来看看'mail.setting'配置文件如何调用的,进入MailUtil.send源码,最终可以看到:

MailUtil.java
可以看到第一行Mail.create构造了一个mail对象,构造方法如下:
Mail.java
我们可以看出也可以通过传入MailAccount的方式传入配置。这在官方文档中也给出了相应示例。如果用户没有传入一个mailAccount,系统会从GlobalMailAccount中取。
GlobalMailAccount.java
而系统取得mailAccount的路径是以下三个:
MailAccount.java
说明我们的mail.setting按照以上三种方式放都是可以的。
二 生成二维码

现在需要生成类似下图的二维码图片,包括背景图片(背景图片来源),二维码和文字。

效果图
  • 生成二维码是第一步
    Hutool的QrCodeUtil可以生成二维码,引入依赖
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.3.3</version>
</dependency>
  • 生成二维码,拼接背景图片并添加文字
 public static void main(String[] args) throws IOException {
        QrConfig config = new QrConfig(400, 380);
        // 高纠错级别
        config.setErrorCorrection(ErrorCorrectionLevel.H);
        config.setMargin(1);
        File output = new File("f:/test/output.jpg");
       //生成二维码
        File file = QrCodeUtil.generate("http://192.168.168.132:9528/faq", config, output);
        //合并背景图片和二维码
        if (file.exists()) {
            BufferedImage bi1 = null;
            BufferedImage bi2 = null;
            BufferedImage destImg = null;
            //读取背景图片
            File background = new File("f:/test/background.png");
            //读取二维码图片
            bi1 = ImageIO.read(background);
            //读取背景图片
            bi2 = ImageIO.read(file);
            destImg = PictureMerge.mergeImage(bi1, bi2, true, 110, 140);
            //为图片添加文字
            destImg = PictureMerge.drawTextInImg(destImg, new FontText("ID  :  TI1911080001", "#333333", 30, "Arial"), 180, 530);
            boolean result = PictureMerge.saveImage(destImg, "f:/test/","new.jpg", "jpg");
        }
}
  • 图片文字拼接工具类
/**
 * 图片合并工具类
 *
 * @author : TiaNa
 * @createdDate : 2019/10/21
 * @updatedDate
 */

public class PictureMerge {
    /**
     * @param fileUrl 文件绝对路径或相对路径
     * @return 读取到的缓存图像
     * @throws IOException 路径错误或者不存在该文件时抛出IO异常
     */
    public static BufferedImage getBufferedImage(String fileUrl) throws IOException {
        File f = new File(fileUrl);
        return ImageIO.read(f);
    }

    /**
     * @param savedImg 待保存的图像
     * @param saveDir  保存的目录
     * @param fileName 保存的文件名,必须带后缀,比如 "beauty.jpg"
     * @param format   文件格式:jpg、png或者bmp
     * @return
     */
    public static boolean saveImage(BufferedImage savedImg, String saveDir, String fileName, String format) {
        boolean flag = false;
        // 先检查保存的图片格式是否正确
        String[] legalFormats = {"jpg", "JPG", "png", "PNG", "bmp", "BMP"};
        int i = 0;
        for (i = 0; i < legalFormats.length; i++) {
            if (format.equals(legalFormats[i])) {
                break;
            }
        }
        if (i == legalFormats.length) { // 图片格式不支持
            System.out.println("不是保存所支持的图片格式!");
            return false;
        }

        // 再检查文件后缀和保存的格式是否一致
        String postfix = fileName.substring(fileName.lastIndexOf('.') + 1);
        if (!postfix.equalsIgnoreCase(format)) {
            System.out.println("待保存文件后缀和保存的格式不一致!");
            return false;
        }

        String fileUrl = saveDir + fileName;
        File file = new File(fileUrl);
        try {
            flag = ImageIO.write(savedImg, format, file);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return flag;
    }

    /**
     * 待合并的两张图必须满足这样的前提,如果水平方向合并,则高度必须相等;如果是垂直方向合并,宽度必须相等。
     * mergeImage方法不做判断,自己判断。
     *
     * @param img1         待合并的第一张图
     * @param img2         带合并的第二张图
     * @param isHorizontal 为true时表示水平方向合并,为false时表示垂直方向合并
     * @return 返回合并后的BufferedImage对象
     * @throws IOException
     */
    public static BufferedImage mergeImage(BufferedImage img1, BufferedImage img2, boolean isHorizontal, int startX, int startY) throws IOException {
        int w1 = img1.getWidth();
        int h1 = img1.getHeight();
        int w2 = img2.getWidth();
        int h2 = img2.getHeight();

        // 从图片中读取RGB
        int[] ImageArrayOne = new int[w1 * h1];
        ImageArrayOne = img1.getRGB(0, 0, w1, h1, ImageArrayOne, 0, w1); // 逐行扫描图像中各个像素的RGB到数组中
        int[] ImageArrayTwo = new int[w2 * h2];
        ImageArrayTwo = img2.getRGB(0, 0, w2, h2, ImageArrayTwo, 0, w2);

        // 生成新图片
        BufferedImage DestImage = null;
        if (isHorizontal) { // 水平方向合并
            DestImage = new BufferedImage(w1, h1, BufferedImage.TYPE_INT_RGB);
            DestImage.setRGB(0, 0, w1, h1, ImageArrayOne, 0, w1); // 设置上半部分或左半部分的RGB
            DestImage.setRGB(startX, startY, w2, h2, ImageArrayTwo, 0, w2); // 设置下半部分的RGB
        } else { // 垂直方向合并
            DestImage = new BufferedImage(w1, h1 + h2, BufferedImage.TYPE_INT_RGB);
            DestImage.setRGB(0, 0, w1, h1, ImageArrayOne, 0, w1); // 设置上半部分或左半部分的RGB
            DestImage.setRGB(0, h1, w2, h2, ImageArrayTwo, 0, w2); // 设置下半部分的RGB
        }
        return DestImage;
    }

    /**
     * <p>Title: getImageStream</p>
     * <p>Description: 获取图片InputStream</p>
     *
     * @param destImg
     * @return
     */
    public static InputStream getImageStream(BufferedImage destImg) {
        InputStream is = null;

        BufferedImage bi = destImg;

        ByteArrayOutputStream bs = new ByteArrayOutputStream();

        ImageOutputStream imOut;
        try {
            imOut = ImageIO.createImageOutputStream(bs);

            ImageIO.write(bi, "png", imOut);

            is = new ByteArrayInputStream(bs.toByteArray());

        } catch (IOException e) {
            e.printStackTrace();
        }
        return is;
    }

    /**
     * Description: 图片上添加文字业务需求要在图片上添加文字
     *
     * @param bimage
     * @param text
     * @param left
     */
    public static BufferedImage drawTextInImg(BufferedImage bimage, FontText text, int left, int top) {
        Graphics2D g = bimage.createGraphics();
        g.setColor(getColor(text.getColor()));
        g.setBackground(Color.white);
        Font font = new Font(text.getFont(), Font.BOLD,
                text.getSize());
        g.setFont(font);
        g.drawString(text.getText(), left, top);
        g.dispose();
        return bimage;
    }

    // color #2395439
    public static Color getColor(String color) {
        if (color.charAt(0) == '#') {
            color = color.substring(1);
        }
        if (color.length() != 6) {
            return null;
        }
        try {
            int r = Integer.parseInt(color.substring(0, 2), 16);
            int g = Integer.parseInt(color.substring(2, 4), 16);
            int b = Integer.parseInt(color.substring(4), 16);
            return new Color(r, g, b);
        } catch (NumberFormatException nfe) {
            return null;
        }
    }
  • 字体类
/**
 * 字体
 *
 * @author : TiaNa
 * @createdDate : 2019/10/21
 * @updatedDate
 */
@Data
public class FontText {

    private String text;

    private String color;

    private Integer size;

    private String font;

    public FontText(String text, String color,
                    Integer size, String font) {
        super();
        this.text = text;
        this.color = color;
        this.size = size;
        this.font = font;
    }

    public FontText() {
    }
}
三 excel表格导入导出
  • 这里也是使用的Hutool工具实现
@Slf4j
public class ExcelUtils {

    /**
     * 读取excel表格内容返回List<Bean>
     *
     * @param inputStream excel文件流
     * @param head        表头数组
     * @param headerAlias 表头别名数组
     * @param bean        返回的Bean对象
     * @return
     */
    public static <T> List<T> importExcel(InputStream inputStream, String[] head, String[] headerAlias, Class<T> bean) {
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<Object> header = reader.readRow(1);
        //替换表头关键字
        if (ArrayUtils.isEmpty(head) || ArrayUtils.isEmpty(headerAlias) || head.length != headerAlias.length) {
           log.error("导入的excel表,表头格式与设定规则不一致");
        } else {
            for (int i = 0; i < head.length; i++) {
                if (head[i].equals(header.get(i))) {
                    reader.addHeaderAlias(head[i], headerAlias[i]);
                } else {
                    log.error("导入的excel表,表头格式与设定规则不一致");
                }
            }
        }
        //读取指点行开始的表数据(以下介绍的三个参数也可以使用动态传入,根据个人业务情况修改)
        //1:表头所在行数  2:数据开始读取位置 3:映射返回的Bean对象
        List<T> read = reader.read(1, 2, bean);
        return read;
    }

    /**
     * 导出excel表格内容
     *
     * @param filename    文件名
     * @param head        表头数组
     * @param headerAlias 表头别名数组
     * @param list        导入的数据
     * @return
     */
    public static void exportExcel(String filename, String[] head, String[] headerAlias, List list) {
        ExcelWriter writer = ExcelUtil.getWriter(filename);
        List rows = CollUtil.newArrayList(list);
        if (ArrayUtils.isEmpty(head) || ArrayUtils.isEmpty(headerAlias) || head.length != headerAlias.length) {
             log.error("导入的excel表,表头属性与设定规则不一致");
        } else {
            for (int i = 0; i < head.length; i++) {
                writer.addHeaderAlias(head[i], headerAlias[i]);
            }
        }

        // 一次性写出内容,使用默认样式,强制输出标题
        writer.write(rows, true);
        // 关闭writer,释放内存
        writer.close();
    }
}
  • 测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class ExcelTest {

    @Autowired
    private InstallService installService;

    @Test
    public void exportExcel(){
        List<DeviceInstall> list = installService.list();
        String name = "测试单-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
       //list里实体字段要和excelHead 保持一致
        String[] excelHeadAlias = {"编号", "日期", "联系人", "联系人电话", "描述"};
        String[] excelHead = {"Id", "installUserPhone", "installLinkUser", "installLinkPhone", "installNotes"};
        ExcelUtils.exportExcel("F:/test/excel/".concat(name).concat(".xlsx"), excelHead, excelHeadAlias, list);
    }
四 Redis缓存用户登录信息,并实现token验证

 系统中需要缓存用户登录信息和验证码,在此之前我使用session缓存,在单服务器中这样也没太大问题,但当服务部署到集群环境,就会出现session不一致的问题,这里是# 分布式系统session一致性的问题,我最终的解决方案就是用Redis缓存用户信息。

1 实现思路:

  • 用户登录时,校验成功后,产生uuid,以uuid为key,用户信息为value存入Redis,过期时长为15分钟。此外,我还需要将uuid传给前端。
  • 用户调用其他接口时,请求头中需加入登录时返回的uuid值,后端拦截器拦截到到有效的uuid时,要相对应的给Redis中的uuid续命,延长过期时间。

2 实现过程:
首先需要学习Redis基础,如果对Springboot整合Redis不熟悉,可以参考文章:idea整合springboot+redis,下面我们看代码

  • 用户登录实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public UserVO login(String account, String pwd, HttpSession session) {
       //根据用户账户查询用户信息
        LambdaQueryWrapper<User> query = Wrappers.lambdaQuery();
        query.eq(User::getAccount, account);
        User user = getOne(query);
        if (user != null && pwd.equals(user.getPassword())) {
                //登录信息存储在redis中
                String uuid = UUID.randomUUID().toString();
                user.setToken(uuid);
                redisUtils.set(uuid, user, 900);
                return user;
            }
        }
        throw new MyException("用户名或密码错误");
    }
public class UserInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisUtils redisUtils;

    /**
     * 在请求处理之前进行调用(Controller方法调用之前)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws IOException {
        //获取token
        String token = request.getHeader("token");
        //不拦截options请求
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            return true;
        }

        //查询redis存储的用户信息
       if (!StringUtils.isBlank(token ) && redisUtils.exists(token )) {
            //更新登录有效时间
            redisUtils.expire(redisUser, 900);
            return true;
        } else {
            throw new DataValidationException("用户身份信息错误");
        }
    }

    /**
     * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView mv)
            throws Exception {

    }

    /**
     * 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行 (主要是用于进行资源清理工作)
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception ex)
            throws Exception {

    }

    public void returnErrorResponse(HttpServletResponse response, Result result)
            throws IOException, UnsupportedEncodingException {
        OutputStream out = null;
        try {
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/json");
            out = response.getOutputStream();

            out.write(JSONUtil.toJsonStr(result).getBytes("utf-8"));
            out.flush();
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }
}
  • 拦截器配置,不拦截登录请求和swagger文档,且必须配置跨域
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Bean
    public UserInterceptor getUserInterceptor() {
        return new UserInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        /**
         * 拦截器按照顺序执行
         */
        registry.addInterceptor(getUserInterceptor())
                .excludePathPatterns("/api/v010/users/login")
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");

    }

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    //支持跨域请求
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                .maxAge(3600);
        super.addCorsMappings(registry);
    }
}

3 测试:

  • 访问登录接口http://localhost:8080/users/login,获取uuid
    登录接口返回数据
  • 访问其他接口时,请求头加上uuid就能请求成功,否则抛出异常


    header
五 全局异常处理

这里写的非常详细了:# SpringBoot 全局异常处理详解

六 权限管理

在所有系统中,都有权限管理的功能,下面这篇文章可以提供很多思路
一个基于SpringBoot2+Shiro的权限管理系统

七 Jmeter性能测试

# Jmeter性能测试 入门

八 SpringBoot集成极光推送

https://blog.csdn.net/x541211190/article/details/81123829

相关文章

网友评论

    本文标题:SpringBoot(二) SpringBoot开发实例

    本文链接:https://www.haomeiwen.com/subject/zicqvctx.html