spring-boot连接自制证书的exchange服务器发送邮件


一、pom管理依赖

<groupId>xxx</groupId>
<artifactId>xxx</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>

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

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

二、创建启动类

@SpringBootApplication
public class SpringBootMailApplication {

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

三、创建邮件发送类

@Component
public class SenderMail {
private Logger logger = LogManager.getLogger(SenderMail.class);

@Autowired
private JavaMailSender javaMailSender;

private static String MAIL_SENDER = "xieshaohu@xxx.xxx";

@PostConstruct
public void sendHtmlMail() {
MimeMessage mimeMailMessage = null;
try {
mimeMailMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true);
mimeMessageHelper.setFrom(MAIL_SENDER);
mimeMessageHelper.setTo("xxx@xxx.cn");
mimeMessageHelper.setSubject("测试邮件!");
StringBuilder sb = new StringBuilder();
sb.append("<h1>SpirngBoot测试邮件HTML</h1>")
.append("<p style='color:#F00'>你是真的太棒了!</p>")
.append("<p style='text-align:right'>右对齐</p>");
mimeMessageHelper.setText(sb.toString(), true);
javaMailSender.send(mimeMailMessage);
} catch (Exception e) {
logger.error("邮件发送失败", e);
}
}
}

四、编写application.yml配置文件

spring:
mail:
host: mail.xxx.com
port: 587 # exchange服务器时,必须设置此端口
username: xieshaohu
password: xxxxxx
default-encoding: UTF-8
jndi-name: mail/Session
properties:
mail:
smtp:
auth: true
starttls:
enable: false # 禁止TLS连接
ssl:
enable: false # 禁止ssl连接
socketFactory:
port: 587 # 设置端口
class: javax.net.ssl.SSLSocketFactory
fallback: true # 设置为true
debug: true
test-connection: false

五、配置文件特别说明:

  • 如果服务器支持不使用tls和ssl,则properties里的mail.smtp.starttls.enable就设置为false;mail.smtp.starttls.enable也设置为false
  • properties里的mail.smtp.socketFactory.fallback必须设置为true,否则会提示“javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?”
  • 指定spring.mail.port为587,否则会提示:javax.mail.AuthenticationFailedException: 535 5.7.3 Authentication unsuccessful

六、最精简的连接成功的配置

spring:
mail:
host: mail.xxx.com
port: 587 # exchange服务器时,必须设置此端口
username: xieshaohu
password: xxxxxx
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
debug: true
test-connection: false

总结:

由于最初没有配置spring.mail.port,导致研究了一圈properties的配置,发送成功后,最后通过测试发现,设置好port端口为587,一切都可以正常执行了。

Advertisements

解决Elasticsearch中文搜索只能搜索单个汉字,不能搜索词语的问题


问题原因:ES默认分词规则不能支持中文,通过安装IK Analysis for Elasticsearch支持中文分词。

重要提示:不能再已经导入数据的index中修改mapping,所以建立index之后立即配置分词器字段,然后再导入数据。

  1. 首先从https://github.com/medcl/elasticsearch-analysis-ik/tree/master下载ES对应版本的IK插件进行安装,安装方法参考链接中的文档。
  2. 使用以下命令重新创建一个Index

    curl -XPUT http://localhost:9200/cdh_es

  3. 设置需要使用IK分词器的字段,其中cdh_es是index,testdata是type,desc是需要进行IK分词的字段。

    curl -XPOST http://localhost:9200/cdh_es/testdata/_mapping -d ‘{“properties”: {“desc”: {“type”: “text”, “analyzer”: “ik_smart”, “search_analyzer”: “ik_smart”}}}’

  4. 其中分词有两种设置方法,ik_max_word和ik_smart,他们的区别如下,可以根据自己项目的情况进行选择:

    ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合;

    ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。

  5. 添加数据后就能进行查询了。

 

Android端RSA加密数据送Java服务端解密时出现BadPaddingException


原因:Android系统使用的虚拟机(dalvik)跟SUN标准JDK是有所区别的,其中他们默认的RSA实现就不同。即Android端用Cipher.getInstance(“RSA”)方法进行加密时,使用的provider是Bouncycastle Security provider,Bouncycastle Security provider默认实现的是“RSA/None/NoPadding”算法,而服务器(PC)端用Cipher.getInstance(“RSA”)进行解密时,使用的是Sun的security provider,实现的是“RSA/None/PKCS1Padding”算法,所以,解密时会失败。

正确的设置方法:Java服务端代码

Cipher cipher = Cipher.getInstance(“RSA”);

Android端代码

Cipher cipher = Cipher.getInstance(“RSA/None/PKCS1Padding”);

Servlet, Struts2 Action, SpringMVC Control是否单例测试结果


同事咨询Servlet, Struts2 Action, SpringMVC Control的应用情况,针对是否单例,运行工程实际测试了一下。

测试方法:

  • 在Servlet.service方法中打印对象的hashcode值
  • 在Action.excute方法中打印对象的hashcode值
  • 在Control.XXX方法中打印对象的hashcode值

测试结果:

  • Servlet是单例的,多个请求共用一个对象。
  • Struts2 Action是多例的,每次请求都新生成一个实例对象。
  • SpringMVC Control是单例的,多个请求共用一个对象。

结论:

对于高并发的Web应用,建议不使用Struts,每个请求新生成一个对象,是个比较大的开销,同时Struts在开发上限制太多和直接写Servlet无太大区别,缺乏灵活性。

 

使用POI输出超过65536行大Excel(SXSSF技术)


一直以来都用POI做Excel导出,之前导出的Excel格式都是2003版的——XLS,最近客户要求导出超过65536条数据,超出了XLS的上限,所以研究起XSSFWorkbook的用法。

原本以为只是把HSSFWorkbook替换成XSSFWorkbook就能导出数据,无奈总是报告内存溢出。放狗查找资料,最终锁定SXSSFWorkbook类。

首先参考Upgrading to POI 3.5, including converting existing HSSF Usermodel code to SS Usermodel (for XSSF and HSSF)完成HSSFWorkbook转换为XSSFWorkkbook的转换,注意使用CreationHelper类。

再参考SXSSF (Streaming Usermodel API)完成大文件的输出

import junit.framework.Assert;
import org.apache.poi.ss.usermodel.Cell;
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.util.CellReference;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

public static void main(String[] args) throws Throwable {
    SXSSFWorkbook wb = new SXSSFWorkbook(100); // keep 100 rows in memory, exceeding rows will be flushed to disk
    Sheet sh = wb.createSheet();
    for(int rownum = 0; rownum < 1000; rownum++){
        Row row = sh.createRow(rownum);
        for(int cellnum = 0; cellnum < 10; cellnum++){
            Cell cell = row.createCell(cellnum);
            String address = new CellReference(cell).formatAsString();
            cell.setCellValue(address);
        }

    }

    // Rows with rownum < 900 are flushed and not accessible
    for(int rownum = 0; rownum < 900; rownum++){
      Assert.assertNull(sh.getRow(rownum));
    }

    // ther last 100 rows are still in memory
    for(int rownum = 900; rownum < 1000; rownum++){
        Assert.assertNotNull(sh.getRow(rownum));
    }

    FileOutputStream out = new FileOutputStream("/temp/sxssf.xlsx");
    wb.write(out);
    out.close();    // dispose of temporary files backing this workbook on disk
    wb.dispose();
}

写在最后

  • SXSSF通过把数据缓存到磁盘临时文件,完成超大Excel的输出。
  • 一个Sheet对应一个临时文件
  • 通过SXSSFWorkbook构造函数指定多少条写入临时文件,实际测试证明,维持默认100条能够获得较好的性能。
  • 最后不要忘记使用SXSSFWorkbook.dispose()方法清除临时文件,该方法返回true,则表示所有的临时文件都已经清除。
    该方法实际最终执行SheetDataWriter.dispose(),此方法内部执行java.io.Writer.close()方法释放资源,同时执行java.io.File.delete()方法删除临时文件。
  • 如果碰到临时文件空间的问题,可以尝试启动GZIP,用CPU来换空间了。

GZIP启用方法:

SXSSFWorkbook wb = new SXSSFWorkbook(); 
  wb.setCompressTempFiles(true); // temp files will be gzipped

XML创建/解析/验证


最近用到XML结合JMS TextMessage传输数据的技术。采用TextMessage+XML格式传输对象是考虑到这个平台不仅仅使用Java语言开发,同时还需要能够支持其他语言接入:比如C, Ruby, Perl, Python, PHP, ActionScript/Flash, Smalltalk,借助于ActiveMQ提供的Stomp,这些非Java语言能够很方便的就接入平台。

总结下来整个传输过程如下(Java to Java):

DTO对象->XML对象->XML文本->JMS TextMessage->XML文本->XML对象->DTO

对象

涉及技术点:

  1. JavaDTO对象如何转变为XML对象
  2. 如何从XML对象中获得XML文本
  3. 如何通过XML文本构造XML对象
  4. 遍历XML对象创建JavaDTO对象

先说第一点,JavaDTO对象如何转变为XML对象

// 创建DocumentBuilderFactory,创建和解析XML都需要
DocumentBuilderFactory domfac = DocumentBuilderFactory.newInstance();
// 创建DocumentBuilder
DocumentBuilder builder = domfac.newDocumentBuilder();
// 新建文档
Document doc = builder.newDocument();
doc.setXmlVersion("1.0");
// 创建文档根节点
Element root = doc.createElement("root");
// 循环创建叶子节点
Element leaf = doc.createElement("leaf");
// 添加叶子节点属性
leaf.setAttribute("name", "从Java对象中获取");
// 增加叶子节点至根节点
root.appendChild(leaf);

至此,XML对象已经创建完成,现在需要从XML对象中获取XML文本。

Continue reading “XML创建/解析/验证”

Log4j中添加自定义的日志级别


Log4j自带了trace,debug,info.warn,error,fatal几个日志级别。

由于项目需要拆分业务和系统日志,而我又想偷懒,所以决定在log4j日志框架上增加自定义的日志级别专门用于业务日志输出。需要增加的日志级别定为audit,在log4j所有自带日志级别之上。

Log4j官方推荐的增加自定义的日志级别的方法是“继承org.apache.log4j.Level类”,具体的可以参考log4j中的示例XLevel。由于系统开发时不直接使用Log4j,而是通过slf4j调用log4j,官方推荐的方法行不通,所以决定修改log4j和slf4j相关源代码。

Log4j源代码修改点如下:

org.apache.log4j.Logger添加audit方法

public void audit(Object message) {
	if (repository.isDisabled(Level.AUDIT_INT)) {
		return;
	}

	if (Level.AUDIT.isGreaterOrEqual(this.getEffectiveLevel())) {
		forcedLog(FQCN, Level.AUDIT, message, null);
	}
}

org.apache.log4j.Priority类添加AUDIT_INT属性

public final static int AUDIT_INT = 60000; //日志级别大于log4j包含的所有日志级别,便于后期通过Threshold属性过滤日志

org.apache.log4j.Level类添加AUDIT相关属性和方法

public static final Level AUDIT = new Level(AUDIT_INT, "AUDIT", 0);public static Level toLevel(int val, Level defaultLevel) {
	switch(val) {
		case ALL_INT: return ALL;
		case AUDIT_INT: return Level.AUDIT;
		case DEBUG_INT: return Level.DEBUG;
		case INFO_INT: return Level.INFO;
		case WARN_INT: return Level.WARN;
		case ERROR_INT: return Level.ERROR;
		case FATAL_INT: return Level.FATAL;
		case OFF_INT: return OFF;
		case TRACE_INT: return Level.TRACE;
		default: return defaultLevel;
	}
}

至此Log4j修改完成,下面修改slf4j相关源代码。slf4j需要修改slf4j-api和slf4j-log4j12两个包 Continue reading “Log4j中添加自定义的日志级别”