GeXiangDong

精通Java、SQL、Spring的拼写,擅长Linux、Windows的开关机

0%

通过maven可以把依赖的各种jar和自己的程序都打包成一个jar,这样在部署的时候就非常方便了,这点在做可执行的jar包时就更方便了。
这依赖一个maven的插件:shade

在pom里配置shade插件即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<!-- 下面这个改成运行jar包时需要执行的java类 -->
<mainClass>cn.devmgr.spider.Application</mainClass>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

使用 mvn package 打包时,就可以把所有依赖的jar都打包到一个jar里了。要运行只需要 java -jar xxx.jar 不用在classpath参数里写一大堆jar了,是不是很方便?

shade插件还可以配置排除/包含某些jar包。

不用spring,仅仅jms和activeMQ

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.devmgr.activemqlistener</groupId>
<artifactId>activemq-listener</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
<version>1.1</version>
</dependency>

<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-core</artifactId>
<version>5.7.0</version>
</dependency>
</dependencies>

</project>

发送端 JmsMessageSender.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package cn.devmgr.activemqlistener;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Message;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;

public class JmsMessageSender {

public static void main(String[] argvs) throws Exception {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

// 消息生产者;发送消息
Queue queue = session.createQueue("print-queue");
String payload = "需要打印了";
Message msg = session.createTextMessage(payload);
MessageProducer producer = session.createProducer(queue);
System.out.println("Sending text '" + payload + "'");
producer.send(msg);

session.close();
connection.close();
}
}

接收端 JmsMessageReceiver.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package cn.devmgr.activemqlistener;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;

public class JmsMessageReceiver {

public static void main(String[] argvs) throws Exception {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

// 消息消费者: 接收消息
Queue queue = session.createQueue("print-queue");
MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener(new ConsumerMessageListener("print-queue"));
connection.start();

//60秒后退出
Thread.sleep(60000);
session.close();

connection.close();
}


public static class ConsumerMessageListener implements javax.jms.MessageListener {
private String consumerName;

public ConsumerMessageListener(String consumerName) {
this.consumerName = consumerName;
}

public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println(consumerName + " received " + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}

}
}

pom文件增加依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>

做一个excel模板文件放到src/main/resources目录下

最好再建个子目录,这样资源更方便管理。

使用模板调格式会方便很多(宽度、字体、数字格式等等,节省代码,容易修改)

数据导出

下面的例子是写在spring controller内的,如果不是controler,需要变化的只有只有最后返回的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@ResponseBody
@GetMapping("/export")
public ResponseEntity<byte[]> exportExcel() throws Exception{
logger.trace("exportExcel");
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentDispositionFormData("attachment",new String("导出的文件名.xlsx".getBytes(), "ISO8859-1"));
responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);

//中文文件名需要用iso8859-1编码
InputStream templateIs = this.getClass().getResourceAsStream("/excel-templates/templet.xlsx");
XSSFWorkbook workbook = new XSSFWorkbook(templateIs);
XSSFSheet sheet = workbook.getSheetAt(0);

List<SampleItem> list = getDataList();

CellStyle cellStyle = workbook.createCellStyle();
CreationHelper createHelper = workbook.getCreationHelper();
cellStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy/mm/dd"));

for (int i=0; i<list.size(); i++) {
SampleItem si = list.get(i);

XSSFRow row = sheet.createRow(i + 1);

Cell cell1 = row.createCell(0);
cell1.setCellValue(si.getDate());
cell1.setCellStyle(cellStyle);

Cell cell2 = row.createCell(1);
cell2.setCellValue(si.getName());

Cell cell3 = row.createCell(2);
cell3.setCellValue(si.getScore());
}

ByteArrayOutputStream bos = new ByteArrayOutputStream();
workbook.write(bos);
workbook.close();
return new ResponseEntity<byte[]>(bos.toByteArray(), responseHeaders, HttpStatus.OK);
}

修改启动类

改成继承org.springframework.boot.web.servlet.support.SpringBootServletInitializer, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.devmgr.tutorial;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class Application extends SpringBootServletInitializer{

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

修改pom文件

改packaging方式为war

1
<packaging>war</packaging>

改依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
<!-- 排除掉内嵌的tomcat,为了减少war体积,也为了避免和tomcat服务器冲突 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- 去掉了内嵌的tomcat后,需要增加servlet-api,否则编译会出错了 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>

注意事项

spring boot 2.0项目需要在tomcat 8.5以上版本环境下运行

Spring Boot项目中,默认支持的静态资源位置

classpath:位置 项目中的目录 优先级
/META-INF/resources/ src/main/resources/META-INF/resources/ 优先级最高
/resources/ src/main/resources/resources/ 第二优先
/static/ src/main/resources/static/ 第三优先
/public/ src/main/resources/public/ 第四优先

@EnableWebMvc 注解

如果项目中没有@EnableWebMvc注解,那么上述静态资源是可以直接被浏览器访问的,如果项目中有@EnableWEbMvc注解,则无法访问了。

这是因为SpringBoot默认启动了自动配置,自动配置配置上述静态资源的访问,而使用@EnableWebMvc注解后,会禁止掉org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration 的自动配置,于是这些不能被访问了。

如果需要@EnableWebMvc注解,又需要静态资源,增加一个自定义的配置:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MvcConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// URL访问所有的/css/开头的url,都映射到src/main/resources/statics/css目录下;
// 注意需要classpath:开头
registry.addResourceHandler("/css/**")
.addResourceLocations("classpath:/static/css/");
}

}

logback是spring boot中的默认日志记录框架,spring推荐使用logback-spring.xml来做配置,loback-spring.xml可以放置到src\main\resources目录下。

通过在logback-spring.xml中配置不同的pfofile,可以实现不同的profile不同的日志记录方式,下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 下面这段是定义了一个appender,这个appender定义在了springProfile之外,
无论那个profile,都会先创建这个appender,即便是对应的profile没有使用这个appender,也要先创建它
如果创建失败(例如文件类型的appender,对应目录无权限),则spring boot启动失败
-->
<appender name="out" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>

<!-- 下面是这些定义会在development这个profile激活时才有效 -->
<springProfile name="development">
<appender name="target-file" class="ch.qos.logback.core.FileAppender">
<file>target/tutorial.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36}
- %msg%n</pattern>
</encoder>
</appender>
<!-- 可以设置不同的package/class记录不同级别的日志 -->
<logger name="cn.devmgr" level="TRACE" />
<root level="WARN">
<!-- 使用哪些appender记录日志,下面这2行是把日志输出到主控台和文件里 -->
<appender-ref ref="target-file" />
<appender-ref ref="out" />
</root>
</springProfile>

<!-- 下面是这些定义会在dev这个production激活时才有效 -->
<springProfile name="production">
<appender name="logfile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/web/logs/tutorial.%d{yyyy-MM-dd}.log
</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="cn.devmgr" level="INFO" />
<logger name="org.springframework" level="WARN" />
<root level="WARN">
<appender-ref ref="logfile" />
<appender-ref ref="out" />
</root>
</springProfile>
</configuration>

需要注意:有logback配置文件后,application.yml中的日志配置部分就失效了。

设置NGINX,把真实IP转发过来

1
2
3
4
5
6
7
8
location / {
proxy_pass http://127.0.0.1:8008/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}

配置spring boot中内嵌的tomcat,使用header中的IP

修改application.yml

1
2
3
4
5
server:
use-forward-headers: true
tomcat:
remote-ip-header: X-Real-IP
protocol-header: X-Forwarded-Proto

之后在程序中获取的IP (request.getRemoteAddr()) 就是真实的IP地址了。

遇到的问题

在使用Spring REST时,有一个需要返回xml格式数据的方法:

1
2
3
@PostMapping(value = "/payment", consumes = {MediaType.TEXT_XML_VALUE}, produces = {MediaType.ALL_VALUE})
public Map<String, String> wxPayCallback(@RequestBody Map<String, String> paraMap) throws Exception {
}

被调用时”org.springframework.web.HttpMediaTypeNotSupportedException: Content type ‘text/xml;charset=UTF-8′ not supported”异常。

[http-nio-8085-exec-7] ERROR c.s.m.r.e.ControllerExceptionHandler - 异常:415
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/xml;charset=UTF-8' not supported
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:226)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:157)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:130)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:131)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:870)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:776)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:978)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:881)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)

原因和解决办法

乍一看似乎是consumes指定的content-type不对导致,实际上是缺少依赖导致,spring rest默认只包含json格式的转换,不包含xml格式的转换,因此pom内增加依赖来解决。

1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

增加上述依赖后,上面的方法会正常访问,但如果RestController上没有指明produces,增加上述依赖后,会默认输出xml,而不再是json。
可以通过

1
2
3
4
5
6
7
8
9
@Configuration
public class WebAppConfigurer extends WebMvcConfigurationSupport {

@Override
protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}

}

来修改默认的produces。

使用jackson来实现json和object之间转换。
使用的时候在resultMap中,对应的列配置,例如:

1
2
<result column="specs" property="specs" 
typeHandler="cn.devmgr.tutorial.typehandler.JsonTypeHandler" />
1
2
3
update table_xxx 
set colA=#{beanA.xxx, typeHandler=cn.devmgr.tutorial.typehandler.JsonTypeHandler}
where id=xx

JsonTypeHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package cn.devmgr.tutorial.typehandler;

import java.io.IOException;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
private final static Log log = LogFactory.getLog(JsonTypeHandler.class);

private static ObjectMapper objectMapper;
private Class<T> type;
static {
objectMapper = new ObjectMapper();
}

public JsonTypeHandler(Class<T> type) {
if(log.isTraceEnabled()) {
log.trace("JsonTypeHandler(" + type + ")");
}
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
}

private T parse(String json) {
try {
if(json == null || json.length() == 0) {
return null;
}
return objectMapper.readValue(json, type);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private String toJsonString(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return (T) parse(rs.getString(columnName));
}

@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return (T) parse(rs.getString(columnIndex));
}

@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return (T) parse(cs.getString(columnIndex));
}

@Override
public void setNonNullParameter(PreparedStatement ps, int columnIndex, T parameter, JdbcType jdbcType) throws SQLException {
ps.setString(columnIndex, toJsonString(parameter));

}

}

遇到的问题

在调用https的外部接口时,遇到一个异常:

Caused by: java.lang.RuntimeException: Could not generate DH keypair
    at sun.security.ssl.ECDHCrypt.<init>(ECDHCrypt.java:82)
    at sun.security.ssl.ClientHandshaker.serverKeyExchange(ClientHandshaker.java:724)
    at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:281)
    at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1026)
    at sun.security.ssl.Handshaker.process_record(Handshaker.java:961)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1072)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1385)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1413)
    ... 103 common frames omitted
Caused by: java.security.InvalidAlgorithmParameterException: parameter object not a ECParameterSpec
    at org.bouncycastle.jce.provider.JDKKeyPairGenerator$EC.initialize(Unknown Source)
    at sun.security.ssl.ECDHCrypt.<init>(ECDHCrypt.java:77)
    ... 110 common frames omitted

出现的原因和解决办法

发现maven打包依赖时加入org.bouncycastle:bcprov-jdk14导致,老版本的bcp只支持1024位加密,遇到2048位加密就出这个异常了。

有两个办法解决,推荐第一个,彻底解决:

* 找到依赖org.bouncycastle:bcprov-jdk14的配置,用exclusion排除它,并加入最新的bcprov。
* 在java命令行加参数: -Dcom.sun.net.ssl.enableECC=false

我的项目出错是由于itextpdf-5.0依赖了bcprov-jdk14,升级到itextpdf-5.5.13并增加了对bcprov-jdk15的依赖。 由于itextpdf-5.5.13中对bcprov-jdk15的依赖是optional,需要另外增加bcprov-jdk15

1
2
3
4
5
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.59</version>
</dependency>

参考

https://stackoverflow.com/questions/42238313/java-security-invalidalgorithmparameterexception-parameter-object-not-a-ecparam