GeXiangDong

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

0%

使用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

仅仅Autowired注解

@Autowired在查找构件时,会从构件库中查找所有构件,查找所有是被注解变量的类型或该类型的子类。

  1. 如果找到了且只找到了一个,那么就给被注解变量设置成此构件。
  2. 如果一个也没找到,看Autowired注解的required参数的值,如果是true(默认值)则失败,终止spring环境的启动;如果是false则给被注解变量赋值为null。
  3. 如果找到了多个:
    1. 如有在找到的多个中有且仅有一个构件被标注了@Primary,则使用这个。
    2. 如果@Primary有多个或者一个也没有,终止spring环境的启动,抛出异常。

Autowired注解和Qualifier注解一起使用

在使用@Autowired注解装载构件时,可以增加一个@Qualifier指定一个待装载构件的名字

1
@Autowired @Qualifier("thisone") MyObject myObject;

上面这种写法可以解决多个MyObject的子类被设置成component的状况
相应的,在构件上有两种写法:

1
2
3
@Service("thisone")
public MyObject{
}

也可以

1
2
3
4
@Qualifier("thisone")
@Service
public MyObject{
}

注意给构件起名不要重复,重名会抛异常的。

定制RestController中Date类型转换为JSON时的格式有两种方法:

  • 通过application.yml配置修改全局设置,对所有Date型都生效
  • 给需要指定的属性增加注解,仅对被注解的属性生效

两个方法都有,注解优先级高。

application.yml中可这样设置

1
2
3
4
5
6
spring:
jackson:
date-format: yyyy-MM-dd #用字符串表示日期时间时的字符串格式
time-zone: GMT+8 #设置时区,不设置用格林威治时间
serialization:
write-dates-as-timestamps: false #值为true表示用long型时间戳

用注解设置可以使用下面2个注解

long型的timestamp表示日期:

1
@JsonFormat(shape = JsonFormat.Shape.NUMBER)

字符串表示日期:

1
@JsonFormat(timezone="GMT+8", pattern="yyyy-MM-dd")

Mybatis写mapper时,每个表都需要INSERT UPDATE写很长的语句比较麻烦,80%的表和java bean是简单的一一对应关系,因此考虑使用mybatis的SQLProvider注解减少这部分重复劳动。
SELECT和DELETE语句由于涉及字段较少,写起来还在可接收范围内,所以没做通用的。

也可以把javabean增加写注解,类似JPA那样指定表名、字段名、主键等,但做得太多就失去了mybatis依赖SQL的灵活性了

InsertUpdateSqlProvider.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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import java.lang.reflect.Field;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.ibatis.jdbc.SQL;


/**
* 使用此类有几个默认规则: 表名和类名相对应,属性名和字段名对应
* java类内用驼峰命名法;数据库表和字段都用下划线
* 类的属性定义的变量名和get/set方法名,以及set的参数类型一致,才会映射到数据库字段
* 例如: private String name; 而且有 public void setName(String name) 和 public String getName()方法
* 如果不是严格按照此规则定义的属性,不会被影射到数据库字段上
*/
public class InsertUpdateSqlProvider {

/**
* id如果传入了值,会被insert使用;如果id为null,不会被insert的columns列出
*/
public static String insert(Object obj) {
Map<String, String> map;
try {
map = getFieldsMap(obj, true);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException(e);
}
return new SQL() {
{
INSERT_INTO(getTableName(obj));
for (String col : map.keySet()) {
VALUES(col, map.get(col));
}
}
}.toString();
}

private static String updateById(Object obj, boolean includeNullValueField) {
Map<String, String> map;
try {
map = getFieldsMap(obj, includeNullValueField);
map.remove("id");
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException(e);
}
return new SQL() {
{
UPDATE(getTableName(obj));
for (String col : map.keySet()) {
SET(col + "=" + map.get(col));
}
WHERE("id = #{id}");
}
}.toString();
}

public static String updateById(Object obj) {
return updateById(obj, true);
}

public static String updateNonNullById(Object obj) {
return updateById(obj, false);
}

private static Map<String, String> getFieldsMap(Object obj, boolean includeNullValue)
throws IllegalArgumentException, IllegalAccessException {
HashMap<String, String> result = new HashMap<>();
Class<?> cls = obj.getClass();
Field[] fields = cls.getDeclaredFields(); // getDeclaredFields
for (Field f : fields) {
String col = f.getName();
String colName = col.substring(0, 1).toUpperCase() + col.substring(1);
f.setAccessible(true);
try {
cls.getMethod("get" + colName);
cls.getMethod("set" + colName, f.getType());
} catch (NoSuchMethodException | SecurityException e) {
continue;
}
if ((!"id".equals(col) && includeNullValue) || f.get(obj) != null) {
result.put(camelCase2Underscore(col), "#{" + col + "}");
}
}
return result;
}

public static String getTableName(Object obj) {
return camelCase2Underscore(obj.getClass().getSimpleName());
}

public static String camelCase2Underscore(String s) {
StringBuffer buf = new StringBuffer();
for (String w : s.split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
buf.append("_").append(w.toLowerCase());
}
return buf.substring(1);
}
}

使用通用provider的Mapper类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.UpdateProvider;
import org.springframework.stereotype.Repository;

@Repository
public interface GenericDao {

@UpdateProvider(type=InsertUpdateSqlProvider.class, method="updateById")
public int updateById(Object bean);

@UpdateProvider(type=InsertUpdateSqlProvider.class, method="updateNonNullById")
public int updateNonNullById(Object bean);

@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
@InsertProvider(type=InsertUpdateSqlProvider.class, method="insert")
public int insert(Object bean);

@InsertProvider(type=InsertUpdateSqlProvider.class, method="insert")
public int insertWithoutGeneratedKey(Object bean);
}

POM文件中加入spring-boot-devtoools的依赖,可以在修改后自动重启,方便开发过程中测试。

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

需要IDE工具打开自动编译,因为devtools是监视class文件的修改后自动重启,不是监视src目录下的java文件。

IntelliJ IDEA可以通过Build菜单下的Build Project来编译项目。

如果需要配置devtools,可以通过在application.yml里加入

1
2
3
4
5
6
7
spring:
devtools:
restart:
enabled: true
livereload:
port: 35727 # 默认是35729,如果开发环境中运行多个项目都用devtools,这个端口会冲突,需要改成不同的
# 还有其他一些属性可配置,请参考文档

如果使用了maven的多模块功能,devtools的依赖需要加到子模块中,加入到父模块不起作用,因为optional=true,是可选依赖,子模块不会继承它。

CURL常用用法汇总。

GET 方法

调用GET方法最简单

1
curl  http://127.0.0.1:8080/tvseries/

增加 header user-agent

1
curl -H "user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0“ http://localhost:8080

增加header 授权信息

1
curl -H "Authorization: Bearer 7e161a1a-4590-4696-9d90-516c54113906" http://localhost:8080/api/exam

显示详细信息 -v

增加 -v 参数可以显示连接过程的详细信息,发送和接收到的HTTP头信息等等,如果不增加 -v,只显示 http response body部分内容

1
curl -v http://localhost:8080

POST 方法

1
curl -H "Content-Type:application/json" -X POST --data '{"name": "West World", “originRelease":"2016-10-02"}’ http://127.0.0.1:8080/tvseries/

上面的data参数是用的单引号包含起来的,内容json部分有双引号。这种写法在windows下可能会出错,需要用双引号,改成如下方式:

1
curl -H "Content-Type:application/json" -X POST --data "{\"name\": \"West World\", \“originRelease\":\"2016-10-02\"}\" http://127.0.0.1:8080/tvseries/

DELETE 方法

1
curl -X DELETE https://127.0.0.1:8080/tvseries/23/

PUT 方法

1
curl  -H “Content-Type:application/json” -X PUT —data ‘{“name”: “Person of Interest”} http://127.0.0.1:8080/tvseries/33/

PUT 方法同 POST 方法,一般需要指定传输的数据。

提交压缩格式的post body

需要服务端支持才可,并不是标准的HTTP服务。

使用临时文件,需要2个命令,先创建一个压缩文件,后发送:

1
2
3
4
echo '{ "mydummy" : "json" }' | gzip > body.gz

curl -v -i http://127.0.0.1:8080/ss -H 'Content-Encoding: gzip' -H 'Content-Type:application/json' --data-binary @body.gz

或者使用管道符合并到一行上

1
2
echo '{"type": "json", "length": 2222}' | gzip | curl -H "Content-Type: application/json" -H "Content-Type: gzip" -X POST -d @- http://127.0.0.1:8080/ss

在使用spring的时候,如果Controller中抛出异常,会被spring显示在客户端界面,而日志中一般没有记录。
客户端对异常的显示也是经spring处理后的信息,没有堆栈,这不方便找错和改错。

可以通过RestControllerAdvice注解定义一个异常处理类来解决这个问题。代码如下

注:也可以不继承ResponseEntityExceptionHandler类,此处继承只是省了一些通用异常的处理。
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
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;


/**
*
* RestController执行过程中发生异常会被此处捕获处理
*
*/
@RestControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
private static final Log log = LogFactory.getLog(ControllerExceptionHandler.class);

/**
* 通过ExceptionHandler来设置待捕获的异常,Throwable可捕获任何异常,但优先级最低,因此
* HttpRequestMethodNotSupportedException HttpMediaTypeNotSupportedException
* HttpMediaTypeNotAcceptableException MissingPathVariableException
* MissingServletRequestParameterException ServletRequestBindingException
* ConversionNotSupportedException TypeMismatchException
* HttpMessageNotReadableException HttpMessageNotWritableException
* MethodArgumentNotValidException MissingServletRequestPartException
* BindException NoHandlerFoundException AsyncRequestTimeoutException
* 等已经在父类声明捕获的异常不会被此方法处理。
*/
@ExceptionHandler(Throwable.class)
@ResponseBody
ResponseEntity<Object> handleControllerException(Throwable ex, WebRequest request) {
Map<String,String> responseBody = new HashMap<>();
// 这里控制返回给客户端的信息
responseBody.put("message","internal server error. " + ex.getMessage());

Exception e;
if(ex instanceof Exception) {
e = (Exception) ex;
}else {
e = new Exception(ex);
}
return handleExceptionInternal(e, responseBody, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}

/**
* 需要覆盖这个方法,并且在此方法里记录日志;查看ResponseEntityExceptionHandler源码可知,
* 有些异常被父类捕获,不会进入此类的handleControllerException,因此如果在handleControllerException
* 记录异常日志,会导致部分异常无日志
*/
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
HttpHeaders headers, HttpStatus status, WebRequest request) {
if(log.isErrorEnabled()) {
log.error("内部错误", ex);
}
return super.handleExceptionInternal(ex, body, headers, status, request);
}
}

查看java内存使用状况可以通过jmap命令,例如: jmap -histo:live PID

如果执行命令是出现下述错误:

jmap -histo:live 19114
19114: Unable to open socket file: target process not responding or HotSpot VM not loaded
The -F option can be used when the target process is not responding

这是因为19114号进程的属主和当前执行jmap的用户不一致,换成一致的用户即可,例如:

1
sudo -u tomcatuser jmap -histo:live 19114

可以把方法设置成返回 ResponseEntity<T> 类型,其中的泛型T取代以前想返回的类型,然后通过 ResponseEntity.status(HttpStatus.OK).body(result) 方法创建返回结果,其中第一个status就是设置返回的ResponseCode

1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping
public ResponseEntity<Map<String, Object>> login(){
HashMap<String, Object> result = new HashMap<>();
boolean isOk = false;
if( !isOk ) {
result.put("message", "invalid username or password");
// status(HttpStatus.BAD_REQUEST) 是设置返回的状态码
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}else {
return ResponseEntity.status(HttpStatus.OK).body(result);
}
}