Java 15:发布

很重要的提醒

Java 15即将于2020年9月15发布.

仅摘录部分更新特性.全部特性可以在 https://openjdk.java.net/projects/jdk/15/找到.

全部更新:

  • 爱德华兹曲线数字签名算法(EdDSA)
  • 密封课程(预览)
  • 隐藏的课程
  • 删除Nashorn JavaScript引擎
  • 重新实现旧版DatagramSocket API
  • 禁用和弃用偏置锁定
  • instanceof的模式匹配(第二预览)
  • ZGC:可扩展的低延迟垃圾收集器
  • 文字块
  • Shenandoah:低暂停时间的垃圾收集器
  • 删除Solaris和SPARC端口
  • 外部存储器访问API(第二个孵化器)
  • 记录(第二预览)
  • 弃用RMI激活以进行删除

java标志
image-3213

密封类或接口(预览)

密封类或接口可以用来限制只有特定的类或接口才能继承或实现它们.

用sealed修饰符来声明密封类.然后使用permits来指定允许扩展密封类的类.

示例:


1
2
public abstract sealed class Shape
    permits Circle, Rectangle, Square {}

同时,子类在不同的包中也是可以的:


1
2
3
4
    public abstract sealed class Shape
        permits com.example.polar.Circle,
                com.example.quad.Rectangle,
                com.example.quad.simple.Square {...}

如果都在同一个包中,则可以像下面这样声明:


1
2
3
4
5
abstract sealed class Shape { }

class Circle extends Shape { }
class Rectangle extends Shape { }
class Square extends Shape { }

密封类针对子类(由permits修饰符指定的类)有三个约束:

1. 密封类及其允许的子类必须属于同一模块,并且如果在未命名的模块中声明,则必须属于同一包。
2. 每个允许的子类都必须直接扩展密封的类。
3. 每个允许的子类都必须选择一个修饰符来描述其如何继续其父类发起的密封:
3.1 可以声明一个允许的子类,final以防止其在类层次结构中的进一步扩展。
3.2 可以声明一个允许的子类,sealed以允许其层次结构的一部分扩展到超出其密封超类所设想的范围,但以受限的方式。
3.3 可以声明一个允许的子类,non-sealed以便其层次结构的一部分恢复为未知子类可以扩展的扩展。(密封类不能阻止其允许的子类这样做。)

密封接口和类的使用方式类似:


1
2
3
4
5
6
7
    public sealed interface Expr
        permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {}

    public final class ConstantExpr implements Expr {}
    public final class PlusExpr implements Expr {}
    public final class TimesExpr implements Expr {}
    public final class NegExpr implements Expr {}

密封类与record可以配合使用:


1
2
3
4
5
6
7
    public sealed interface Expr
        permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {}

    public record ConstantExpr(int i) implements Expr {}
    public record PlusExpr(Expr a, Expr b) implements Expr {}
    public record TimesExpr(Expr a, Expr b) implements Expr {}
    public record NegExpr(Expr e) implements Expr {}

instanceof模式匹配(第二预览)

instanceof模式匹配在Java 14中新增,在Java 15中开启第二预览.功能方面没有任何更改.

文本块(Text Blocks)

文本块在Java 13中开启预览,Java 14开启第二预览.在Java 15中发布.在Java 15中文本块没有任何更改.

record(第二预览)

1. 内部声明record

Java 15中增加了在方法内部使用record:

本地记录是嵌套记录的一种特殊情况。像所有嵌套记录一样,本地记录也是隐式静态的。这意味着它们自己的方法无法访问封闭方法的任何变量;反过来,这避免了捕获立即封闭的实例,该实例将以静默方式将状态添加到记录中。本地记录是隐式静态的,这与不是隐式静态的本地类相反。实际上,局部类永远不会是静态的(隐式或显式),并且始终可以访问封闭方法中的变量。


1
2
3
4
5
6
7
8
9
10
List findTopMerchants(List merchants, int month) {
    // Local record
    record MerchantSales(Merchant merchant, double sales) {}

    return merchants.stream()
        .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
        .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
        .map(MerchantSales::merchant)
        .collect(toList());
}

2. record的注解

如下的注解示例:


1
2
3
4
5
6
7
public final class Card {
    private final @MyAnno Rank rank;
    private final @MyAnno Suit suit;
    @MyAnno Rank rank() { return this.rank; }
    @MyAnno Suit suit() { return this.suit; }

}

可以优化为:


1
public record Card(@MyAnno Rank rank, @MyAnno Suit suit) { }

返回到record上的注解,这些注解出现在适用的相应程序点处。换句话说,传播是在程序员使用@Target 元注解的控制下进行的。传播规则是系统且直观的,并且遵循所有适用的规则:

1. 如果记录组件上的注解适用于字段声明,则注解将出现在相应的private字段上。
2. 如果记录组件上的注解适用于方法声明,则该注解将出现在相应的访问器方法上。
3. 如果记录组件上的注解适用于形式参数,则如果未显式声明注解,则该注解将出现在规范构造函数的相应形式参数上;如果显式声明,则注解将出现在紧凑构造函数的相应形式参数上。
4. 如果记录组件上的注解适用于某个类型,则传播规则与声明注解的传播规则相同,不同之处在于,该注解出现在相应的类型用途上而不是声明上。

Java: 小程序获取用户开放信息(OpenId,UnionID)

关于OpenId和UnionID

OpenId是用户唯一标识.也就是用户信息的主键.

UnionID 机制说明
如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,UnionID是相同的。

获取流程

UnionID只有满足条件时才会返回.

优化后流程(小程序减少请求一次后台)

在小程序启动时,会运行app.js里面的onLaunch方法.获取信息从onLaunch方法开始:

1. 在调用 wx.login时将res.code缓存到this.globalData.privateUserCode中,在后面使用;
2. 获取用户信息的时候: wx.getSetting中调用wx.getUserInfo(需要附加参数: withCredentials: true,)时,将res.code,encryptedData,iv这三个数据发送给后台,用来换取OpenId和UnionID().

未优化流程

1. 用wx.login中的res.code换取session_key,OpenId;
2. 用session_key,iv,encryptedData解密出UnionID

微信小程序中的修改

app.js

返回结果信息格式(仅供参考):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
    "code":"0",
    "data":{
        "city":"xxxxxxxxxxxx",
        "province":"xxxxxxxxxxxx",
        "country":"China",
        "watermark":{
            "appid":"xxxxxxxxxxxx",
            "timestamp":"xxxxxxxxxxxx"
        },
        "openId":"xxxxxxxxxxxx",
        "nickName":"xxxxxxxxxxxx",
        "avatarUrl":"xxxxxxxxxxxx",
        "unionId":null
    },
    "message":""
}

修改参考:


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
App({
  onLaunch: function () {

    wx.getSystemInfo({
      success: e => {
      }
    })
    // 展示本地存储能力
    var logs = wx.getStorageSync('logs') || []
    logs.unshift(Date.now())
    wx.setStorageSync('logs', logs)

    // 登录
    wx.login({
      success: res => {
        console.log(res);
        if (res && res.code) {
          this.globalData.privateUserCode = res.code;
        }
        // 发送 res.code 到后台换取 openId, sessionKey, unionId
      }
    })
    // 获取用户信息
    wx.getSetting({
      success: res => {
        if (res.authSetting['scope.userInfo']) {
          // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
          wx.getUserInfo({
            withCredentials: true,
            success: res => {
              // 可以将 res 发送给后台解码出 unionId
              this.globalData.userInfo = res.userInfo
              if (res && res.encryptedData && res.iv && this.globalData.privateUserCode) {
                const postData = {
                  openCode: this.globalData.privateUserCode,
                  encryptedData: res.encryptedData,
                  iv: res.iv
                }
                wx.request({
                  url:'http://localhost:8080/getWeChatUnionIdAndOpenId',
                  data: postData,
                  method: "POST",
                  header: {
                    'content-type': 'application/x-www-form-urlencoded' //修改此处即可
                  },
                  success: decodeData => {
                      try {
                        const objInfo = decodeData.data;
                        if (objInfo.code == "0") {
                          this.globalData.constWxUserOpenId = objInfo.data.openId;
                          this.globalData.constWxUserUnionId=objInfo.data.unionId;
                        } else {
                          console.error(`获取微信UNIONID返回结果处理错误,错误信息: ${objInfo.message}`);
                        }
                      } catch (err) {
                        console.error(`获取微信UNIONID错误,错误信息: ${err}`);
                      }
                  }
                })
              }
              // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
              // 所以此处加入 callback 以防止这种情况
              if (this.userInfoReadyCallback) {
                this.userInfoReadyCallback(res)
              }
            }
          })
        }
      }
    })
  },
  globalData: {
    userInfo: null,
    extUserInfo: null,
    privateUserCode: null,
  }
})

java标志
image-3175

Java服务端

此处需使用Java JDK 14或高于14的版本,采用Spring Boot搭建.

在application.properties中配置小程序相关信息

application.properties文件中修改小程序appid和小程序秘钥:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
#################################### common config : ####################################
spring.application.name=wxopenid
# 应用服务web访问端口
server.port=8080
# ActuatorWeb访问端口
management.server.port=8081
management.endpoints.jmx.exposure.include=*
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
#************************************************
# 微信小程序-配置信息
cn.bckf.wechat.appid=配置你自己的小程序appid
cn.bckf.wechat.secret=配置你自己的小程序秘钥
logging.level.cn.bckf=debug

将配置文件中的信息注入到Java Bean


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 读取application.properties文件中的微信小程序相关信息.
 *
 */
@Component
@ConfigurationProperties(prefix = "cn.bckf.wechat")
@Data
@NoArgsConstructor
public class SystemInfo {

    String appId;

    String secret;
}

JSON数据处理

访问微信的接口,微信会返回json数据,在Java中,需要对json数据进行解析处理.


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
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


/**
 * 微信解密数据实体类.(字段可能会增加)
 * (参考地址: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html)
 *
 * @author prd
 * @version 2020-05-16
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class WeChatDecryptData {


    @JsonProperty("openId")
    private String openid;
    @JsonProperty("nickName")
    private String nickname;
    private String gender;
    private String city;
    private String province;
    private String country;
    @JsonProperty("avatarUrl")
    private String avatarUrl;
    @JsonProperty("unionId")
    private String unionId;
    private Watermark watermark;

    public class Watermark {
        private String appid;
        private String timestamp;

        public void setAppid(String appid) {
            this.appid = appid;
        }

        public String getAppid() {
            return appid;
        }

        public void setTimestamp(String timestamp) {
            this.timestamp = timestamp;
        }

        public String getTimestamp() {
            return timestamp;
        }
    }
}




import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeChatOpenIdRes {

    @JsonProperty("session_key")
    private String sessionKey;
    private String openid;

}

解密及调用微信接口

该类中主要定义了解密方法,请求微信接口.

关于解密,也可以参考微信官方文档: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html


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
99
100
101
102
103
104
105
106
107
108
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;

/**
 * 解密及调用微信接口工具
 *
 *
 */
@Slf4j
public class WebUtil {
    /**
     * 解密微信加密信息获取UnionId.
     */
    private static int AES_128 = 128;
    private static String ALGORITHM = "AES";
    private static String AES_CBS_PADDING = "AES/CBC/PKCS5Padding";

    public static byte[] encrypt(final byte[] key, final byte[] IV, final byte[] message) throws Exception {
        return encryptDecrypt(Cipher.ENCRYPT_MODE, key, IV, message);
    }

    public static byte[] decrypt(final byte[] key, final byte[] IV, final byte[] message) throws Exception {
        return encryptDecrypt(Cipher.DECRYPT_MODE, key, IV, message);
    }

    private static byte[] encryptDecrypt(final int mode, final byte[] key, final byte[] IV, final byte[] message)
            throws Exception {
        final Cipher cipher = Cipher.getInstance(AES_CBS_PADDING);
        final SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
        final IvParameterSpec ivSpec = new IvParameterSpec(IV);
        cipher.init(mode, keySpec, ivSpec);
        return cipher.doFinal(message);
    }

    private static String getWeChatAPI() {
        return "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
    }


    /**
     * 解密微信加密的数据(获取UnionId及其余相关信息).
     *
     * @param sessionKey
     * @param encryptedData
     * @param iv
     * @return
     */
    public static String decryptWeChatInfo(String sessionKey, String encryptedData, String iv) {
        if (StringUtils.hasText(sessionKey) && StringUtils.hasText(encryptedData) && StringUtils.hasText(iv)) {
            try {
                var decodeSessionKey = Base64.getDecoder().decode(sessionKey);
                var decodeEncryptedData = Base64.getDecoder().decode(encryptedData);
                var deCodeIV = Base64.getDecoder().decode(iv);
                var keyGenerator = KeyGenerator.getInstance(ALGORITHM);
                keyGenerator.init(AES_128);
                var decryptedString = decrypt(decodeSessionKey, deCodeIV, decodeEncryptedData);
                var resStr = new String(decryptedString);
                log.debug("解密结果 : {}",resStr);
                return resStr;
            } catch (Exception ex) {
                log.debug("________________________________decryptWeChatInfo: sessionKey:{},encryptedData: {} , iv: {}", sessionKey, encryptedData, iv);
            }
        }
        return null;
    }


    /**
     * 用微信小程序获得的code换取微信用户的openId.
     *
     * @param weChatOpenCode 微信小程序获得的code.
     * @return
     */
    public static String getWeChatOpenId(String weChatOpenCode, String appId, String secret) {
        log.debug("________________________________wxOpenCode:{}", weChatOpenCode);
        if (!StringUtils.hasLength(weChatOpenCode)) {
            return "";
        }
        try {
            var client = HttpClient.newHttpClient();
            var url = getWeChatAPI().formatted(appId, secret, weChatOpenCode);
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .build();
            var response = client.send(request, HttpResponse.BodyHandlers.ofString());
            log.info(response.body());
            return response.body();
        } catch (IOException e) {
            log.error("getWeChatOpenId:{}", e.getMessage());
        } catch (InterruptedException e) {
            log.error("getWeChatOpenId:{}", e.getMessage());
        }
        return "";
    }


}

对外提供服务

对外提供访问服务:

/appid 可以访问当前application.properties配置的小程序相关信息.
/getWeChatOpenId 微信loginCode获取微信用户的openId
/getWeChatUnionId 解密微信加密的数据(获取UnionId)(关于UnionId的限制请参考官方文档).
(推荐,只需调用一次即可同时获得UnionId和OpenId) /getWeChatUnionIdAndOpenId 解密微信加密的数据(获取UnionId和OpenId).(用loginCode换取OpenId然后换取UnionId)(关于UnionId的限制请参考官方文档).


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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;


/**
 * 微信小程序服务端解密用户的OpenId和UnionId.
 *
 * 基本参考: JDK>=14
 *
 * 参考文档:
 *
 * 服务端获取开放数据:
 *
 * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
 *
 * UnionID 机制说明:
 *
 * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html
 *
 * @author prd
 * @version 2020-05-16
 */
@SpringBootApplication
@RestController
@RequestMapping("/")
@Slf4j
public class WeChatOpenDataDecryptApplication {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SystemInfo systemInfo;

    @RequestMapping("/appid")
    public SystemInfo getAppId() {
        return systemInfo;
    }


    /**
     * 用微信loginCode获取微信用户的openId.
     *
     * @param loginCode
     * @return
     */
    @RequestMapping("getWeChatOpenId")
    public String getWeChatOpenId(String loginCode) {
        return WebUtil.getWeChatOpenId(loginCode, systemInfo.getAppId(), systemInfo.getSecret());
    }


    /**
     * 返回UnionId,通用方法.
     *
     * @param resStr
     * @return
     * @throws JsonProcessingException
     */
    private Map getUnionIdCommon(String resStr) throws JsonProcessingException {
        if (StringUtils.hasText(resStr)) {
            var resObj = objectMapper.readValue(resStr, WeChatDecryptData.class);
            var waterMarkAppId = resObj.getWatermark().getAppid();
            var currentWxAppId = systemInfo.getAppId();
            if (waterMarkAppId.equalsIgnoreCase(currentWxAppId)) {
                return Map.of("code", "0", "message", "", "data", resObj);
            } else {
                var resMessage = String.format("解密微信加密的数据(获取UnionId).发生错误,错误原因: 解密后的APPID与当前小程序的APPID不一致!解密后的APPID: %s,当前APPID: %s.", waterMarkAppId, currentWxAppId);
                log.error("解密微信加密的数据(获取UnionId).发生错误,错误原因: 解密后的APPID与当前小程序的APPID不一致!解密后的APPID: {},当前APPID: {}.", waterMarkAppId, currentWxAppId);
                return Map.of("code", "-1", "message", resMessage);
            }
        }
        return Map.of("code", "-1", "message", "返回数据为空,或处理出现错误!");
    }

    /**
     * 解密微信加密的数据(获取UnionId)(关于UnionId的限制请参考官方文档).
     *
     * @param encryptedData
     * @param sessionKey
     * @param iv
     * @return
     */
    @RequestMapping("getWeChatUnionId")
    public Map getWeChatUnionId(String sessionKey, String encryptedData, String iv) {
        try {
            var resStr = WebUtil.decryptWeChatInfo(sessionKey, encryptedData, iv);
            return getUnionIdCommon(resStr);
        } catch (Exception ex) {
            return Map.of("code", "-1", "message", "处理出现错误!错误信息: " + ex.getMessage());
        }
    }


    /**
     * 解密微信加密的数据(获取UnionId和OpenId).(用loginCode换取OpenId然后换取UnionId)(关于UnionId的限制请参考官方文档).
     *
     * @param encryptedData
     * @param iv
     * @param loginCode
     * @return
     */
    @RequestMapping("getWeChatUnionIdAndOpenId")
    public Map getWeChatUnionIdAndOpenId(String encryptedData, String iv, String loginCode) {
        try {
            if (!StringUtils.hasText(loginCode) || !StringUtils.hasText(encryptedData) || !StringUtils.hasText(iv)) {
                return Map.of("code", "-1", "message", "必填参数为空!");
            }
            var openIdRes = WebUtil.getWeChatOpenId(loginCode, systemInfo.getAppId(), systemInfo.getSecret());
            if (StringUtils.hasText(openIdRes)) {
                var wxOpenIdResObj = objectMapper.readValue(openIdRes, WeChatOpenIdRes.class);
                if (wxOpenIdResObj != null) {
                    var resStr1 = WebUtil.decryptWeChatInfo(wxOpenIdResObj.getSessionKey(), encryptedData, iv);
                    return getUnionIdCommon(resStr1);
                }
            }
        } catch (Exception ex) {
            return Map.of("code", "-1", "message", "处理出现错误!错误信息: " + ex.getMessage());
        }
        return Map.of("code", "-1", "message", "返回数据为空,或处理出现错误!");
    }


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

}

项目地址

完整代码可以访问: https://gitee.com/pruidong/SpringBootDemoProjects/tree/master/WeChatGetOpenIdDemo

IDEA: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.

问题

使用IDEA 2020.1 EAP版本时,如果使用JDK 14.在运行Spring Boot项目的时候,会提示:

Java HotSpot(TM) 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.

原因是在运行中勾选了对应方案导致的.

java标志
image-3026

解决方案

IDEA->Run->Edit Configurations-> Enable launch optimization(取消勾选).

再次运行就没有这个提示了.

AutoValue: 自动生成equals,hashCode和toString方法

背景

自己编写一个并重写equals,hashCode方法,很容易出错.而使用IDE工具生成的方法又显得十分”臃肿”.于是AutoValue应运而生.AutoValue最低支持Java 8.

官方仓库地址: https://github.com/google/auto/tree/master/value

java标志
image-2977

引入依赖

Maven

引入注解包:

<dependencies>
  <dependency>
    <groupId>com.google.auto.value</groupId>
    <artifactId>auto-value-annotations</artifactId>
    <version>${auto-value.version}</version>
  </dependency>
</dependencies>

处理注解:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <annotationProcessorPaths>
          <path>
            <groupId>com.google.auto.value</groupId>
            <artifactId>auto-value</artifactId>
            <version>${auto-value.version}</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>

上面的这个,也可以像下面这样定义(注意: 您可以将处理器本身包含在编译时的类路径中。这样做可能会将不必要的类拉到您的运行时类路径中。):

<dependencies>
  <dependency>
    <groupId>com.google.auto.value</groupId>
    <artifactId>auto-value</artifactId>
    <version>${auto-value.version}</version>
    <optional>true</optional>
  </dependency>
</dependencies>

Gradle

dependencies {
  // Use 'api' rather than 'compile' for Android or java-library projects.
  compile             "com.google.auto.value:auto-value-annotations:${autoValueVersion}"
  annotationProcessor "com.google.auto.value:auto-value:${autoValueVersion}"
}

发生了什么?

AutoValue在javac内作为标准注释处理器运行。它读取您的抽象类并推断实现类的外观。它在您的程序包中生成一个具体实现类的源代码,该类扩展了您的抽象类,具有:

  • 包装可见性(非公开)
  • 每个抽象访问器方法一个字段
  • 设置这些字段的构造函数
  • 返回相关字段值的每个访问器方法的具体实现
  • 一个以常规方式比较这些值的equals实现
  • 适当的对应hashCode
  • 返回实例的有用(但未指定)字符串表示形式的toString实现

如上所示,您的手写代码将其工厂方法调用委派给了生成的构造函数和方法!

请注意,您类的调用者不需要了解任何这些。 它们只是调用您提供的工厂方法,并返回行为良好的实例。

警告

注意不要将参数以错误的顺序意外传递给生成的构造函数。 您必须确保测试足以捕获任何现场订购问题。 在大多数情况下,这应该是测试创建此值类别的任何实际目的的自然结果! 在其他情况下,只需执行上面显示的非常简单的测试即可。 考虑切换到使用builder选项来避免此问题。

我们保留随时更改hashCode实现的权利。 永远不要保留hashCode的结果或将其用于任何其他意外目的,也要小心不要依赖于您的值在HashSet之类的无序集合中出现的顺序。

示例

pom.xml

<dependencies>
	<dependency>
		<groupId>com.google.auto.value</groupId>
		<artifactId>auto-value</artifactId>
		<version>1.7</version>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>com.google.auto.value</groupId>
		<artifactId>auto-value-annotations</artifactId>
		<version>1.7</version>
	</dependency>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>compile</scope>
	</dependency>
</dependencies>

AutoValueTest.java

Animal在IDE中可能会提示错误,忽略即可.

import com.google.auto.value.AutoValue;

import static org.junit.Assert.*;


@AutoValue
abstract class Animal {
    static Animal create(String name, int numberOfLegs) {
        return new AutoValue_Animal(name, numberOfLegs);
    }

    abstract String name();

    abstract int numberOfLegs();


}


public class AutoValueTest {

    public static void main(String[] args) {
        Animal dog = Animal.create("dog", 4);
        assertEquals("dog", dog.name());
        assertEquals(4, dog.numberOfLegs());

        // You probably don't need to write assertions like these; just illustrating.
        assertTrue(Animal.create("dog", 4).equals(dog));
        assertFalse(Animal.create("cat", 4).equals(dog));
        assertFalse(Animal.create("dog", 2).equals(dog));

        assertEquals("Animal{name=dog, numberOfLegs=4}", dog.toString());
    }
}

运行AutoValueTest.java,如果没有输出错误,则正常.

Animal抽象类,经过AutoValue生成的代码如下:

import javax.annotation.Generated;

@Generated("com.google.auto.value.processor.AutoValueProcessor")
final class AutoValue_Animal extends Animal {
  private final String name;
  private final int numberOfLegs;

  AutoValue_Animal(String name, int numberOfLegs) {
    if (name == null) {
      throw new NullPointerException("Null name");
    }
    this.name = name;
    this.numberOfLegs = numberOfLegs;
  }

  @Override
  String name() {
    return name;
  }

  @Override
  int numberOfLegs() {
    return numberOfLegs;
  }

  @Override
  public String toString() {
    return "Animal{"
        + "name=" + name + ", "
        + "numberOfLegs=" + numberOfLegs + "}";
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o instanceof Animal) {
      Animal that = (Animal) o;
      return this.name.equals(that.name())
          && this.numberOfLegs == that.numberOfLegs();
    }
    return false;
  }

  @Override
  public int hashCode() {
    int h = 1;
    h *= 1000003;
    h ^= this.name.hashCode();
    h *= 1000003;
    h ^= this.numberOfLegs;
    return h;
  }
}

更多想知道的问题

点: 我怎么?

Java: 将RTF等Word文件转换为PDF文件

起因

之前介绍过一种RTF文件读取成纯文本的方案,但是缺点太明显了: 不支持格式,文本样式也丢失了.在这里再次推荐另外一种方式: 将RTF文件转换为PDF文件,使用Aspose库.

关于Aspose

Aspose提供了很多种方案,支持Word到PDF的转换.同时还支持很多文件的转换,具体可以参考: https://www.aspose.com/官网.

这么好用的库,是收费的吗? 是的.

官方提供了免费试用,如果使用免费的版本,在转换之后的PDF上会有水印.

安装方式

有两种安装方式:

使用jar文件

访问: Aspose的Maven仓库

选择aspose-words,19.12(表示19年12月发布的版本),下载aspose-words-19.12-jdk17.jar这个jar文件.

使用Maven

更推荐使用这种方式,因为方便很多.

Maven方式需要在pom.xml文件中添加:

<dependencies>
    <dependency>
        <groupId>com.aspose</groupId>
        <artifactId>aspose-words</artifactId>
        <version>19.8</version>
        <classifier>jdk17</classifier>
    </dependency>
    <dependency>
        <groupId>com.aspose</groupId>
        <artifactId>aspose-words</artifactId>
        <version>19.8</version>
        <classifier>javadoc</classifier>
    </dependency>
</dependencies>

然后添加Maven仓库地址:

<repositories>
    <repository>
        <id>AsposeJavaAPI</id>
        <name>Aspose Java API</name>
        <url>https://repository.aspose.com/repo/</url>
    </repository>
</repositories>

之后就可以在项目中使用了.

将RTF文件转换成PDF文件

Java实现代码:

// rtf源文件
Document wpd = new Document( "H:\\test.rtf");
// 保存PDF文件.
wpd.save("H:\\output.pdf", SaveFormat.PDF);
// 保存HTML文件.
wpd.save( "H:\\output.html", SaveFormat.HTML);

效果图

RTF文件:

image-2958

转换之后的PDF文件:

image-2959

转换之后的HTML文件:

image-2960