springboot+freemarker+email实现异常邮件通知

描述

在我们的项目中,总是有一些我们不可控制的异常,比如数据库连接不上,redis挂掉,以及一些代码上未可知的异常爆发,不能在项目上线时就可以统计出来,并且修复,所以当我们这些bug抛出异常时,或者在某些可控的严重异常需要推送邮件或者短信或者其他的通讯工具比如 钉钉或者飞书等,我们就需要这样的功能,这里提供一个邮件通知方法,当有未知异常或者被定义为严重异常的,就会给运维人员发送一个邮件进行通知,方便计时应对和问题定位。

解决方案

在springboot中的全局异常捕获处,对不可控异常拿到异常栈信息,进行异常msg的组装和通过freemarker模板进行渲染html文本,然后再把这个异常msg的html进行qq模式的email的发送

,freemarker模板可以支撑字符串模板渲染,即渲染的模板字符串可以保存到数据库,也可以直接定义好xxx.ftl模板,都行,这里需要强调的是 渲染的模板字符串可以保存到数据库

就更加灵活,可以设计一套freemarker模板的管理系统,比如,对自定义的freemarker模板配置后,保存到数据库,然后根据不同的用户或者企业或者业务,就可以从库中获取对应的freemarker模板,进行数据渲染html,再通过短信或者邮件或者钉钉,这样就实现了类似阿里或者腾讯等第三方的模板配置后进行消息推送的功能。

代码

代码组成包含有自定义的模板的工具jar包邮件的springboot-starter,以及在微服务中对异常的处理,和调用邮件的消息封装以及模板的创建

模板自定jar包

详细代码

pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>wlc-spring-boot-tools</artifactId>
        <groupId>com.wlc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>wlc-template</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
    <--freemarker依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
    <--spring-webmvc依赖,这里可以换成spring的ioc包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.9</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>

</project>
BeanUtils,处理bean转map
import org.springframework.cglib.beans.BeanMap;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Map;


/**
 * 描述 实体工具类  <br>
 * 作者:IT学习道场 <br>
 * 时间:2018/10/26 13:37
 */
public class BeanUtils extends org.springframework.beans.BeanUtils {
    public BeanUtils() {
    }

    /**
     * 实例化对象:传入类对类进行实例化对象
     *
     * @param clazz 类
     * @return 对象
     * @author Lius
     * @date 2018/10/26 13:49
     */
    public static <T> T newInstance(Class<?> clazz) {
        return (T) instantiateClass(clazz);
    }

    /**
     * 实例化对象,传入类名对该类进行实例化对象
     *
     * @param clazzStr 类名,传入是必须传入全路径,com...
     * @return 对象
     * @author Lius
     * @date 2018/10/26 13:54
     */
    public static <T> T newInstance(String clazzStr) {
        try {
            Class<?> clazz = Class.forName(clazzStr);
            return newInstance(clazz);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException();
        }
    }

    /**
     * 把对象封装成Map形式
     *
     * @param src 需要封装的实体对象
     * @author Lius
     * @date 2018/10/26 14:08
     */
    public static Map toMap(Object src) {
        return BeanMap.create(src);
    }

    /**
     * 把Map转换成bean对象
     *
     * @author Lius
     * @date 2018/10/26 14:09
     */
    public static <T> T toBean(Map<String, Object> beanMap, Class<T> valueType) {
        // 对象实例化
        T bean = BeanUtils.newInstance(valueType);
        PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(valueType);
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            String properName = propertyDescriptor.getName();
            // 过滤class属性
            if ("class".equals(properName)) {
                continue;
            }
            if (beanMap.containsKey(properName)) {
                Method writeMethod = propertyDescriptor.getWriteMethod();
                if (null == writeMethod) {
                    continue;
                }
                Object value = beanMap.get(properName);
                if (!writeMethod.isAccessible()) {
                    writeMethod.setAccessible(true);
                }
                try {
                    writeMethod.invoke(bean, value);
                } catch (Throwable throwable) {
                    throw new RuntimeException("Could not set property '" + properName + " ' to bean" + throwable);
                }
            }
        }
        return bean;
    }
}
FreemarkerUtil,来处理模板的封装

import com.wlc.template.util.BeanUtils;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import java.io.IOException;
import java.util.Map;

/**
 * 描述: FreemarkerUtil辅助类 <br>
 * 时间: 2022-07-01 9:44  <br>
 * 作者:IT学习道场
 */
@Component
public class FreemarkerUtil {
    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;


    /**
     * bean转map
     * @param bean 转换bean
     * @return Map<String, Object> - map对象
     */
    public Map<String, Object> beanToMap(Object bean){
        Map<String, Object> map = BeanUtils.toMap(bean);
        return map;
    }

    /**
     * 根据模板路径获取模板渲染数据
     * @param templatePath 模板路径 ,ex:templatePath = "notice.ftl",意思是 resources/templates/下的notice.ftl文件
     * @param data 渲染数据对象
     * @return String- 渲染后的html文本
     */
    public String freeMarkerRenderHtml(String templatePath, Map<String, Object> data ){
        //获取模板信息
        Template template = null;
        String html= "";
        try {
            template = freeMarkerConfigurer.getConfiguration().getTemplate(templatePath);
            html = FreeMarkerTemplateUtils.processTemplateIntoString(template, data);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TemplateException e) {
            e.printStackTrace();
        }
        return html;

    }

    /**
     * 字符文本渲染成html文本
     * @param templateName 模板名
     * @param templateText 模板文本
     * @param data         渲染数据map
     * @return String - 渲染后的html文本
     */
    public String textRenderFreemarkerHtml(String templateName, String templateText, Map<String, Object> data){
        String html = textRenderFreemarkerHtml(templateName, templateText, "utf-8", data);
        return html;
    }

    /**
     * 字符文本渲染成html文本
     * @param templateName 模板名
     * @param templateText 模板文本
     * @param charEncode   模板编码
     * @param data         渲染数据map
     * @return String - 渲染后的html文本
     */
    public String textRenderFreemarkerHtml(String templateName, String templateText, String charEncode, Map<String, Object> data){
        Template template = textToFreemarkerTemplate(templateName, templateText, charEncode);
        String html = freemarkerTemplateRenderHtml(template, data);
        return html;
    }

    /**
     * 根据模板对象和数据map渲染html文本
     * @param template 模板对象
     * @param data 渲染数据map
     * @return String - html文本
     */
    public String freemarkerTemplateRenderHtml(Template template, Map<String, Object> data){
        String html = "";
        try {
            //渲染data数据到模板中
            html = FreeMarkerTemplateUtils.processTemplateIntoString(template, data);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TemplateException e) {
            e.printStackTrace();
        }
        return html;
    }

    /**
     * 文本转freemarker模板
     * @param templateName 模板名字
     * @param templateText 模板文本
     * @param charEncode 模板编码
     * @return Template- 返回freemarkerTemplate对象
     */
    public Template textToFreemarkerTemplate(String templateName, String templateText, String charEncode){
        //获取配置文件
        Configuration cfg = freeMarkerConfigurer.getConfiguration();
        //创建freeMarker字符串模板加载器
        StringTemplateLoader stringLoader = new StringTemplateLoader();
        //加载模板名字和模板文本
        stringLoader.putTemplate(templateName, templateText);
        //配置文件设置模板加载器
        cfg.setTemplateLoader(stringLoader);
        freemarker.template.Template template = null;
        try {
            //从配置文件中获取模板对象
            template = cfg.getTemplate(templateName, charEncode);

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

}

这样,模板的渲染jar就封装好了

下面是邮件email的starter封装

pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>wlc-spring-boot-tools</artifactId>
        <groupId>com.wlc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>wlc-email-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!-- 邮件发送的核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <!-- 模板jar -->
        <dependency>
            <groupId>com.wlc</groupId>
            <artifactId>wlc-template</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

EmailUtil-> 邮件发送工具类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Date;

/**
 * 描述: email辅助类 <br>
 * 时间: 2022-07-01 10:20  <br>
 * 作者:IT学习道场
 */
public class EmailUtil {

    @Autowired
    private JavaMailSender javaMailSender;

    @Autowired
    private MailProperties mailProperties;

    /**
     * 发送普通邮件
     * @param subject 主题
     * @param simpleText 内容
     * @param toEamils 邮件接受邮箱数组
     */
    public void simpleEmailSend(String subject, String simpleText, String... toEamils) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setSentDate(new Date());
        message.setFrom(mailProperties.getUsername());
        message.setTo(toEamils);
        message.setSubject(subject);
        message.setText(simpleText);
        //发送邮件
        javaMailSender.send(message);
    }

    /**
     * 发送html邮件
     * @param subject 发送主题
     * @param html 发送的html
     * @param toEamils 邮件接收人数组
     */
    public void emailSendHtml(String subject, String html, String... toEamils) {
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        MimeMessageHelper message = null;
        try {
            message = new MimeMessageHelper(mimeMessage, true);
            message.setSentDate(new Date());
            message.setFrom(mailProperties.getUsername());
            message.setTo(toEamils);
            message.setSubject(subject);
            message.setText(html, true);
            //发送邮件
            javaMailSender.send(mimeMessage);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }
}
EmailService -> email服务类的辅助类
import com.wlc.email.util.EmailUtil;
import com.wlc.template.freemarker.FreemarkerUtil;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Map;

/**
 * 描述: email服务类 <br>
 * 时间: 2022-07-01 10:33  <br>
 * 作者:IT学习道场
 */
public class EmailService {

    @Autowired
    private EmailUtil emailUtil;
    @Autowired
    private FreemarkerUtil freemarkerUtil;

    /**
     * 根据模板路径给邮件发送html模板
     * @param subject       邮件主题
     * @param templatePath  html模板路径
     * @param data          渲染html模板里的数据map
     * @param toEamils      邮件接受者数组
     */
    public void emailSendHtmlByTemplatePath(String subject, String templatePath, Map<String, Object> data, String... toEamils) {
        String html = freemarkerUtil.freeMarkerRenderHtml(templatePath, data);
        emailUtil.emailSendHtml(subject, html, toEamils);
    }

    /**
     * 根据字符模板文本给邮件发送html模板
     * @param subject       邮件主题
     * @param templateName  html模板名字
     * @param templateText  html模板text文本(就是html的模板字符串,使用html模板易字符串的形式保存到数据库,
     *                      然后从数据库中读取模板字符串,在转换成模板对象,把data进行渲染成html,来发送邮件)
     * @param data          渲染html模板里的数据map
     * @param toEamils      邮件接受者数组
     */
    public void emailSendHtmlByTemplateText(String subject, String templateName, String templateText, Map<String, Object> data, String... toEamils) {
        String html = freemarkerUtil.textRenderFreemarkerHtml(templateName, templateText, data);
        emailUtil.emailSendHtml(subject, html, toEamils);
    }

    /**
     * 根据字符模板文本给邮件发送html模板
     * @param subject       邮件主题
     * @param templateName  html模板名字
     * @param templateText  html模板text文本(就是html的模板字符串,使用html模板易字符串的形式保存到数据库,
     *                      然后从数据库中读取模板字符串,在转换成模板对象,把data进行渲染成html,来发送邮件)
     * @param data          渲染html模板里的数据map
     * @param charEncode    编码格式 ex: "utf-8"
     * @param toEamils      邮件接受者数组
     */
    public void emailSendHtmlByTemplateText(String subject, String templateName, String templateText, String charEncode, Map<String, Object> data, String... toEamils) {
        String html = freemarkerUtil.textRenderFreemarkerHtml(templateName, templateText, charEncode, data);
        emailUtil.emailSendHtml(subject, html, toEamils);
    }

}
EmailAutoConfiguration --> 自定义邮件自动化配置类
import com.wlc.email.service.EmailService;
import com.wlc.email.util.EmailUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 描述: 自定义邮件自动化配置类 <br>
 * 时间: 2022-07-01 11:00  <br>
 * 作者:IT学习道场
 */
@Configuration
public class EmailAutoConfiguration {
    
    @Bean
    public EmailService emailService(){
        EmailService emailService = new EmailService();
        return emailService;
    }

    @Bean
    EmailUtil emailUtil(){
        EmailUtil emailUtil = new EmailUtil();
        return emailUtil;
    }
}

resources/META-INF下的spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wlc.email.config.EmailAutoConfiguration

这样一个邮件的自定义springboot-starter就封装好了

下面就是在各个微服务中的全局异常捕获中进行msg的freemarker的组装和邮件的发送

演示的服务代码,全局异常处理

ftl代码,有类似需求可以copy


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"></meta>
    <title>邮件内容</title>
</head>
<body>
    <p style="text-align:center "><img width="400px" height="200px" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimgsa.baidu.com%2Fexp%2Fw%3D500%2Fsign%3Da5c70c27a1efce1bea2bc8ca9f50f3e8%2Fa9d3fd1f4134970a05665ffe93cad1c8a6865dcd.jpg&refer=http%3A%2F%2Fimgsa.baidu.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659171243&t=17d3f0b344e9cb402e8ec4311207fa5a"></p>
    <h4>系统管理员</h4>
    <h4>   您好!</h4>
    <p>系统异常了!日志如下:</p>
    <p>服务名:<span style="color:red">${appName}</span></p>
    <p>服务ip:<span style="color:red">${ipAddr}</span></p>
    <p>类路径:<span style="color:red">${className}</span></p>
    <p>方法名字:<span style="color:red">${methodName}</span></p>
    <p>异常发生行号:<span style="color:red">${lineNumber}</span></p>
    <p style="color:red">${content}</p>
    <p style="width: 100%;text-align: right">IT学习道场系统</p>
</body>
</html>

EmailExtendContent -> email的异常邮件信息组装类,这里是给邮件哪个模板里需要的数据组装

import com.utils.IPUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.servlet.http.HttpServletRequest;

/**
 * 描述: email的异常邮件信息组装类 <br>
 * 时间: 2022-06-30 17:03  <br>
 * 作者:IT学习道场
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class EmailExtendContent {
    //异常
    public Exception e;
    //请求request
    public HttpServletRequest request;
    //ip地址
    public String ipAddr;
    //类命
    public String className;
    //方法名
    public String methodName;
    //发生异常的行号
    public int lineNumber;

    public EmailExtendContent(Exception e, HttpServletRequest request) {
        this.e = e;
        this.request = request;
        handler();

    }


    void handler(){
        StackTraceElement stackTraceElement = e.getStackTrace()[0];
        // 获取本机ip
        ipAddr = IPUtil.getLocalNetCardAdd();
        className = stackTraceElement.getClassName();
        methodName = stackTraceElement.getMethodName();
        lineNumber = stackTraceElement.getLineNumber();
    }
}
IPUtil
package com.utils;

import jodd.util.StringUtil;
import org.springframework.util.ObjectUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.net.*;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** 
 * 描述: 获取ip帮助类 				<br>
 * 作者: wlc 						<br>
 * 实践: 2019年12月5日 下午4:41:58 	<br>
 */
public class IPUtil {
	private static final String UNKNOWN = "unknown";

	private static final Pattern PATTERN = Pattern.compile("^((2([0-4]\\d|5[0-5])|[01]?\\d{1,2})\\.){3}(2([0-4]\\d|5[0-5])|[01]?\\d{1,2})$");

	protected IPUtil(){

	}

	/**
	 * 获取 IP地址
	 * 使用 Nginx等反向代理软件, 则不能通过 request.getRemoteAddr()获取 IP地址
	 * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,
	 * X-Forwarded-For中第一个非 unknown的有效IP字符串,则为真实IP地址
	 */
	public static String getIpAddr(HttpServletRequest request) {
		String ip = request.getHeader("x-forwarded-for");
		if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
	}



	/**
	 * ip转换成long
	 *
	 * @param ip ip
	 * @return long
	 */
	public static Long ipToLong(String ip) {
		//校验ip是否正确
		Matcher matcher = PATTERN.matcher(ip);
		if (!matcher.find()) {
			throw new RuntimeException("ip 格式不正确");
		}
		String[] split = ip.split("\\.");
		return (Long.parseLong(split[0]) << 24) + (Long.parseLong(split[1]) << 16)
				+ (Long.parseLong(split[2]) << 8) + Long.parseLong(split[3]);
	}

	/**
	 * 将long类型转换成ip
	 *
	 * @param ipLong ip的long类型
	 * @return ip
	 */
	public static String longToIp(Long ipLong) {
		StringBuilder ip = new StringBuilder();
		ip.append(ipLong >>> 24).append(".");
		ip.append((ipLong >>> 16) & 0xFF).append(".");
		ip.append((ipLong >>> 8) & 0xFF).append(".");
		ip.append(ipLong & 0xFF);
		return ip.toString();
	}

	/**
	 * 检查是否为内部IP地址
	 *
	 * @param ip IP地址
	 * @return 结果
	 */
	public static boolean internalIp(String ip){
		byte[] addr = textToNumericFormatV4(ip);
		return internalIp(addr) || "127.0.0.1".equals(ip);
	}

	/**
	 * 检查是否为内部IP地址
	 *
	 * @param addr byte地址
	 * @return 结果
	 */
	private static boolean internalIp(byte[] addr){
		if (ObjectUtils.isEmpty(addr) || addr.length < 2){
			return true;
		}
		final byte b0 = addr[0];
		final byte b1 = addr[1];
		// 10.x.x.x/8
		final byte SECTION_1 = 0x0A;
		// 172.16.x.x/12
		final byte SECTION_2 = (byte) 0xAC;
		final byte SECTION_3 = (byte) 0x10;
		final byte SECTION_4 = (byte) 0x1F;
		// 192.168.x.x/16
		final byte SECTION_5 = (byte) 0xC0;
		final byte SECTION_6 = (byte) 0xA8;
		switch (b0) {
			case SECTION_1:
				return true;
			case SECTION_2:
				if (b1 >= SECTION_3 && b1 <= SECTION_4) {
					return true;
				}
			case SECTION_5:
				switch (b1) {
					case SECTION_6:
						return true;
				}
			default:
				return false;
		}
	}

	/**
	 * 将IPv4地址转换成字节
	 *
	 * @param text IPv4地址
	 * @return byte 字节
	 */
	public static byte[] textToNumericFormatV4(String text){
		if (text.length() == 0) {
			return null;
		}

		byte[] bytes = new byte[4];
		String[] elements = text.split("\\.", -1);
		try {
			long l;
			int i;
			switch (elements.length) {
				case 1:
					l = Long.parseLong(elements[0]);
					if ((l < 0L) || (l > 4294967295L)) {
						return null;
					}
					bytes[0] = (byte) (int) (l >> 24 & 0xFF);
					bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
					bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
					bytes[3] = (byte) (int) (l & 0xFF);
					break;
				case 2:
					l = Integer.parseInt(elements[0]);
					if ((l < 0L) || (l > 255L)) {
						return null;
					}
					bytes[0] = (byte) (int) (l & 0xFF);
					l = Integer.parseInt(elements[1]);
					if ((l < 0L) || (l > 16777215L)) {
						return null;
					}
					bytes[1] = (byte) (int) (l >> 16 & 0xFF);
					bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
					bytes[3] = (byte) (int) (l & 0xFF);
					break;
				case 3:
					for (i = 0; i < 2; ++i) {
						l = Integer.parseInt(elements[i]);
						if ((l < 0L) || (l > 255L)) {
							return null;
						}
						bytes[i] = (byte) (int) (l & 0xFF);
					}
					l = Integer.parseInt(elements[2]);
					if ((l < 0L) || (l > 65535L)) {
						return null;
					}
					bytes[2] = (byte) (int) (l >> 8 & 0xFF);
					bytes[3] = (byte) (int) (l & 0xFF);
					break;
				case 4:
					for (i = 0; i < 4; ++i) {
						l = Integer.parseInt(elements[i]);
						if ((l < 0L) || (l > 255L)) {
							return null;
						}
						bytes[i] = (byte) (int) (l & 0xFF);
					}
					break;
				default:
					return null;
			}
		}
		catch (NumberFormatException e) {
			return null;
		}
		return bytes;
	}

	/**
	 * 获取本地IP地址 <br>
	 * 注意:linux中会拿host文件中的ip,大部分没有配置的话会拿出127.0.0.1 <br>
	 * 非docker容器中的程序可以采用下方 getLocalIp()方法
	 * @return 本地IP地址
	 */
	public static String getHostIp() {
		try {
			return InetAddress.getLocalHost().getHostAddress();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		return "127.0.0.1";
	}


	/**
	 * 根据网卡获取本机ip <br>
	 * 注意:若是在docker容器中跑的程序,会拿到 docker的ip <br>
	 *      若是想拿宿主机的ip,加以调用下方 getLocalNetCardAdd() 方法
	 * @return 本地IP地址
	 */
	public static String getLocalNetCardIp() {
		InetAddress inetAddress = null;
		boolean isFind = false; // 返回标识
		Enumeration<NetworkInterface> networkInterfaceLists = null;
		try {
			// 获取网络接口
			networkInterfaceLists = (Enumeration<NetworkInterface>) NetworkInterface.getNetworkInterfaces();
		} catch (SocketException e) {
			e.printStackTrace();
		}
		while (networkInterfaceLists.hasMoreElements()) {
			NetworkInterface networkInterface = (NetworkInterface) networkInterfaceLists.nextElement();
			Enumeration<InetAddress> ips = networkInterface.getInetAddresses();
			// 遍历所有ip,获取本地地址中不是回环地址的ipv4地址
			while (ips.hasMoreElements()) {
				inetAddress = (InetAddress) ips.nextElement();
				if (inetAddress instanceof Inet4Address && inetAddress.isSiteLocalAddress()
						&& !inetAddress.isLoopbackAddress()) {
					isFind = true;
					break;
				}
			}
			if (isFind) {
				break;
			}
		}
		return inetAddress == null ? "" : inetAddress.getHostAddress();
	}

	/**
	 * 根据网卡获得IP地址<br>
	 * 获取到本机网卡ip,就是在docker里跑的程序,也获取的是宿主机的ip地址
	 * @return 本地IP地址
	 */
	public static String getLocalNetCardAdd() {
		String ip="";
		try {
			for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
				NetworkInterface intf = en.nextElement();
				String name = intf.getName();
				if (!name.contains("docker") && !name.contains("lo")) {
					for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
						//获得IP
						InetAddress inetAddress = enumIpAddr.nextElement();
						if (!inetAddress.isLoopbackAddress()) {
							String ipaddress = inetAddress.getHostAddress().toString();
							if (!ipaddress.contains("::") && !ipaddress.contains("0:0:") && !ipaddress.contains("fe80")) {
								if(!"127.0.0.1".equals(ip)){
									ip = ipaddress;
								}
							}
						}
					}
				}
			}
		} catch (SocketException e) {
			e.printStackTrace();
		}
		return ip;
	}

	/**
	 * 获取主机名
	 * @return 本地主机名
	 */
	public static String getHostName() {
		try {
			return InetAddress.getLocalHost().getHostName();
		}
		catch (UnknownHostException e) {
			e.printStackTrace();
		}
		return "未知";
	}


	/**
	 * 从多级反向代理中获得第一个非unknown IP地址
	 * @param ip 获得的IP地址
	 * @return 第一个非unknown IP地址
	 */
	public static String getMultistageReverseProxyIp(String ip) {
		// 多级反向代理检测
		if (ip != null && ip.indexOf(",") > 0) {
			final String[] ips = ip.trim().split(",");
			for (String subIp : ips) {
				if (false == isUnknown(subIp)) {
					ip = subIp;
					break;
				}
			}
		}
		return ip;
	}

	/**
	 * 检测给定字符串是否为未知,多用于检测HTTP请求相关
	 * @param checkString 被检测的字符串
	 * @return 是否未知
	 */
	public static boolean isUnknown(String checkString) {
		return ObjectUtils.isEmpty(checkString) || "unknown".equalsIgnoreCase(checkString);
	}
	/**
	 * 获取客户端Mac地址
	 * @param ip
	 * @return
	 */
	public static String getMACAddress(String ip) {
		String str = "";
		String macAddress = "";
		if(StringUtil.isEmpty(ip)){
			return macAddress;
		}
		try {
			Process p = Runtime.getRuntime().exec("nbtstat -A " + ip);
			InputStreamReader ir = new InputStreamReader(p.getInputStream());
			LineNumberReader input = new LineNumberReader(ir);
			for (int i = 1; i < 100; i++) {
				str = input.readLine();
				if (str != null) {
					if (str.indexOf("MAC Address") > 1) {
						macAddress = str.substring(str.indexOf("MAC Address") + 14, str.length());
						break;
					}
				}
			}
		} catch (IOException e) {
			return "";
		}
		return macAddress;
	}


}
EmailHandler --> 微服务中email处理类定制化处理类
import com.wlc.email.service.EmailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * 描述: email处理类 <br>
 * 时间: 2022-07-01 11:11  <br>
 * 作者:IT学习道场
 */
@Component
public class EmailHandler {

    @Autowired
    private EmailService emailService; // 自定义邮件starter里的 EmailService
    @Value("${spring.application.name:no-service}") //服务应用没名字
    private String appName;
    @Value("#{'${spring.mail.toEamils:}'.split(',')}") //邮件接收者的邮箱数组
    private String[] toEamils;

    /**
     * 异步发送邮件
     * @param stackExceptionMsg 栈异常信息 str 
     * @param e                 异常对象
     * @param request           request
     */
    @Async
    public void sendExceptionEmail(String stackExceptionMsg,Exception e, HttpServletRequest request){
        //根据异常对象和request组装EmailExtendContent
        EmailExtendContent emailExtendContent = new EmailExtendContent(e, request);
        //组装渲染数据data
        Map<String, Object> data = builderData(appName, emailExtendContent, stackExceptionMsg);
        //发送邮件
        emailService.emailSendHtmlByTemplatePath("美术传媒系统异常报告", "notice.ftl", data, toEamils);
    }

    private Map builderData(String appName, EmailExtendContent extendContent, String content){
        Map<String, Object> data = new HashMap<>();
        data.put("appName", appName);
        data.put("ipAddr", extendContent.ipAddr);
        data.put("className", extendContent.className);
        data.put("methodName", extendContent.methodName);
        data.put("lineNumber", extendContent.lineNumber);
        data.put("content", content);
        return data;
    }
}

然后就可以在全局异常里调用一下 EmailHandler.sendExceptionEmail即可


import cn.hutool.core.exceptions.ExceptionUtil;
import com.email.EmailHandler;
import com.http.constant.HttpCode;
import com.http.exception.BusinessException;
import com.utils.JsonUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;

/**
 * 全局结果响应处理,全局异常处理 <br>
 * 作者:IT学习道场             <br>
 * 时间:2019-01-24 10:33
 */
@Slf4j
@RestControllerAdvice
public class ResultAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private EmailHandler emailHandler;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    /**
     * 先捕获异常 然后再把数据返回到ResponseBody中,
     * 然后在Body中要返回数据的时候调用上面的拦截方法beforeBodyWrite()
     */
    @ExceptionHandler(value = Exception.class)
    public Object handleException(Object o, Exception e, HttpServletRequest request) {
        //此处返回json数据
        //捕捉到的异常如果是自定义异常类,那么就返回自定义异常类中的错误码和错误信息
        String stackExceptionMsg = ExceptionUtil.stacktraceToString(e);
        //异常输出到日志
        log.error(stackExceptionMsg);
        //自定义基础异常
        if (e instanceof BusinessException) {
            return new ResultException(((BusinessException) e).getCode(), false, ((BusinessException) e).getMessage(), request.getRequestURL().toString());
            //非法参数异常
        } else if (e instanceof IllegalArgumentException) {
            return new ResultException(HttpCode.BAD_REQUEST.code, false, "参数异常,请稍候再试", request.getRequestURL().toString());
            //绑定异常
        } else if (e instanceof BindException) {
            return new ResultException(HttpCode.BAD_REQUEST.code, false, ((BindException) e).getBindingResult().getFieldError().getDefaultMessage(), request.getRequestURL().toString());
            //方法参数异常验证异常
        } else if (e instanceof MethodArgumentNotValidException) {
            return new ResultException(HttpCode.BAD_REQUEST.code, false, ((MethodArgumentNotValidException) e).getBindingResult().getFieldError().getDefaultMessage(), request.getRequestURL().toString());
        }

        //这里是除了自定义异常的其他异常信息
        else {
       // 这里是未知不可控异常,发送异常邮件即可
            emailHandler.sendExceptionEmail(stackExceptionMsg, e, request);
            return new ResultException(HttpCode.SERVER_ERROR.code, false, "系统异常请联系管理员", request.getRequestURL().toString());
        }
    }
}

下面是

application.yml的邮件配置

server:
  port: 8080
  #启用undertow
  undertow:
    # CPU有几核,就填写几。
    io-threads: 4
    #阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
    # 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8
    worker-threads: 32
    # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
    # 每块buffer的空间大小,越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可
    buffer-size: 1024
    # 是否分配的直接内存(NIO直接分配的堆外内存)
    direct-buffers: true
  servlet:
    context-path: /


spring:
  application:
    name: wlc
  redis:
    host: localhost
    port: 6379
    password:
    lettuce:
      pool:
        # 连接池中的最大空闲连接 默认8
        max-idle: 8
        # 连接池中的最小空闲连接 默认0
        min-idle: 0
        # 连接池最大连接数 默认8 ,负数表示没有限制
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1
        max-wait: -1

  #, 1603610130@qq.com
  mail:
    host: smtp.qq.com
  #你开通smtp的邮箱地址
    username: xxxxx@qq.com
  #你的邮箱开通smtp时的授权码
    password: xxxxxxxxxx
    port: 465
    toEamils: xxxx@qq.com,yyyy@qq.com
    properties:
      mail:
        smtp:
          auth: true
          ssl:
            enable: true
          starttls:
            enable: true
            required: true

验证,已通知找个接口,搞个异常测试,我的是 int i= 1/0,简单测试

到此结束,觉得有用就关注下,全是原创干货,你可以看下我的合集,希望能对你有所帮助

关注公众号,第一手资料在【IT学习道场】

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot是一个用于构建独立的、基于生产级别的Spring应用程序的框架。而Freemarker是一种模板引擎,用于生成动态内容,特别适合Web应用程序的开发。在Spring Boot中使用Freemarker可以通过引入相应的依赖和配置来实现。 首先,在Spring Boot工程中引入Freemarker依赖。可以通过在pom.xml文件中添加以下依赖来实现: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` 这样,在工程中就可以使用Freemarker来处理模板了。如果想要修改模板文件的位置等配置,可以在application.properties文件中进行配置。比如可以通过以下配置来指定模板文件的位置: ``` spring.freemarker.allow-request-override=false spring.freemarker.allow-session-override=false spring.freemarker.cache=false spring.freemarker.charset=UTF-8 spring.freemarker.check-template-location=true spring.freemarker.content-type=text/html spring.freemarker.expose-request-attributes=false spring.freemarker.expose-session-attributes=false spring.freemarker.suffix=.ftl spring.freemarker.template-loader-path=classpath:/templates/ ``` 这样,Spring Boot就会根据这些配置来加载并解析模板文件,生成相应的动态内容。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Spring Boot 整合 Freemarker](https://blog.csdn.net/yaxuan88521/article/details/117173289)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Springboot整合FreeMarker](https://blog.csdn.net/m0_67402096/article/details/126114796)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT学习道场

为你的进步加点油!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值