Flutter: 动态修改应用主题

应用主题

现在很多应用都提供了手动修改应用主题的功能,一般也就两个主题: dark和light,简单说就是: 夜间和日间模式.部分没有提供手动修改的,则选择了跟随手机系统的主题.而在Flutter中,提供了这两种主题,可以很方便的切换.前提是在应用中,需要减少定制化的部分.同时需要针对两种主题进行测试.

实现说明

主题的修改是存在于: MaterialApp上面的theme属性,而这个属性的值为ThemeData,也就是除了系统自带的.也可以自定义主题.

原理分为以下几步:

  1. 父类: MaterialApp中的theme需要使用属性动态判断,以便进行主题切换;
  2. 父类: 需要定义一个类型方法,以便在其它页面调用并修改主题
  3. 子类: 定义一个类属性,用于接收父类定义的类型方法.并在需要修改主题时,传递参数调用该方法
  4. 父类: 定义具体方法,处理修改主题操作.

说起来好复杂,但是一看代码,一下就明白了.

image-3194

一个简单的”小栗子”

下面是一个简单的例子,只在首页实现了一个切换主题的功能.

例子用了CodePen.io的在线编辑,因为CodePen.io不支持导入第三方包,因此注释了SharedPreferences相关内容.如需使用,在本地导入即可

在线预览

源码


import 'package:flutter/material.dart';
/// import 'package:shared_preferences/shared_preferences.dart';



/// Flutter 动态修改应用主题
/// 
/// 
/// [特别说明]
/// 
/// 因为CodePen不支持引入第三方包,所以此处注释了SharedPreferences相关的内容(SharedPreferences可以使应用重启后设置依然生效).
/// 安装SharedPreferences: https://pub.dev/packages/shared_preferences
/// 
/// [原理]
/// 
/// 在MyApp中传递给SettingApp一个回调方法,如果SettingApp需要更新MyApp中的任意值,直接调用此方法即可(目前通过Map传递值,因为无法动态调用setState).
/// 
/// @version 2020--05-31
/// @author prd(pruidong@gmail.com)(bckf.cn)
/// 


/// Flutter dynamically modify the application theme
///
///
/// [Special Note]
///
/// Because CodePen does not support the introduction of third-party packages, the contents related to SharedPreferences are annotated here (SharedPreferences can make the settings still effective after the application restarts).
/// Install SharedPreferences: https://pub.dev/packages/shared_preferences
///
/// [Principle]
///
/// Pass a callback method to SettingApp in MyApp. If SettingApp needs to update any value in MyApp, just call this method (currently pass the value through Map  because setState cannot be called dynamically).
///
/// @version 2020--05-31
/// @author prd (pruidong@gmail.com)(bckf.cn)



void main() {
  runApp(MyApp()  );
}

typedef MainStateUpdateCall = void Function(Map map);


class MyApp extends StatefulWidget {
  const MyApp();
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  bool _darkTheme = false;
  /// final _ThemeKey = "THEME";


  /// 外部接口调用此方法修改本类的属性.
  ///
  /// 可以通过传递在map中传递值,来修改系统属性.
  /// 使用可以参考:
  /// 
  /// The external interface calls this method to modify the properties of this class.
  ///
  /// You can modify system properties by passing values in the map.
  /// Use can refer to:
  ///
  /// [_SettingAppState]的setAndReloadTheme方法.
  void mainStateUpdateMethod(Map map) {
    map.forEach((key, value) {
      setState((){
        switch(key){
          case "_darkTheme":
            _darkTheme=value;
            break;
        }
      });
    });
  }

    @override
  void initState() {
    super.initState();
    initTheme();
  }

  /// CodePen cannot introduce third-party packages, so comment the code below.
  /// 
  /// 
  void initTheme() async {
    // final prefs = await SharedPreferences.getInstance();
    // bool tempTheme = false;
    // if (prefs.containsKey(_ThemeKey)) {
    //   tempTheme = prefs.getBool(_ThemeKey);
    // }
    // setState(() {
    //   _darkTheme = tempTheme;
    // });
  }



  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme:_darkTheme ? ThemeData.dark() : ThemeData.light(), 
      home: SettingApp(mainStateUpdateCall: mainStateUpdateMethod,),
    );
  }
}



/// Setting.
///
///
class SettingApp extends StatefulWidget {
  const SettingApp({this.mainStateUpdateCall});

  final MainStateUpdateCall mainStateUpdateCall;

  @override
  _SettingAppState createState() => _SettingAppState();
}

class _SettingAppState extends State {
  bool _darkTheme = false;
  /// final _themeKey = "THEME";

  var resData = [];

  @override
  void initState() {
    super.initState();
    initSetting();
  }

  void initSetting() async {
    /// final prefs = await SharedPreferences.getInstance();
    /// bool tempTheme = false;
    ///if (prefs.containsKey(_ThemeKey)) {
    ///  tempTheme = prefs.getBool(_ThemeKey);
    ///  }
    // setState(() {
    //   _darkTheme = tempTheme;
    // });
    var tempResData = [
      Card(
        child: ListTile(
          title: Text('dark'),
          trailing: Switch(
              value: _darkTheme,
              onChanged: (value) {
                setAndReloadTheme(value);
                /// Call this method again to generate the list-so that the displayed value is correct.
                /// 重新调用此方法生成列表-以使显示的值正确.
                initSetting();
              }),
        ),
      ),
      Text("Support: bckf.cn / pruidong@gmail.com")
    ];
    setState(() {
      resData = tempResData;
    });
  }

  /// 保存设置和重新加载主题.
  /// Save the settings and reload the theme.
  ///
  void setAndReloadTheme(bool value) async {
    /// final prefs = await SharedPreferences.getInstance();
    /// prefs.setBool(_ThemeKey, value);
    /// 
    /// 
    /// 调用全局更换.
    /// Call global replacement.
    setState(() {
       _darkTheme = value;
     });
    print(value);
    print(_darkTheme);
    if (widget.mainStateUpdateCall != null) {
      var paramMap={"_darkTheme":value};
      widget.mainStateUpdateCall(paramMap);
    }
  }

  @override
  Widget build(BuildContext context) {
    var listView = ListView.separated(
      padding: const EdgeInsets.all(2),
      itemCount: resData.length,
      itemBuilder: (BuildContext itemBuildContext, int index) {
        return Padding(
          padding: EdgeInsets.all(5.0),
          child: resData[index],
        );
      },
      separatorBuilder: (BuildContext context, int index) => Divider(
        color: Theme.of(context).dividerColor,
      ),
    );
    return Scaffold(
      appBar: AppBar(title: Text("Flutter Dynamic Update App Theme")),
      body: listView,
    );
  }
}

代码比较简单,就不详细解释了.

JavaScript:Prism实现代码高亮

代码高亮解决方案

在之前用过Crayon Syntax Highlighter,Enlighter这两个代码高亮的插件.可能是js或其余部分存在冲突,总是导致奇奇怪怪的问题.也找了一些纯js的代码高亮框架.比如: Highlight.js.但最后发现一个高亮框架比较适合: Prism.js.

Prism.js

从我的角度介绍一下Prism.js:

  • 核心文件小(约2kb);
  • 可以按语言自动加载高亮文件(有条件)
  • 代码行号,复制代码等功能作为插件提供
  • 可按照插件架构自行扩展
  • 多种主题可选

最主要的不会存在升级Wordpress插件导致样式混乱的问题了.同时,在Prism.js的下载页面可以按照自己需要的部分进行下载,最大程度减少文件体积.

image-3180

综合引用

可以在Prism官网,直接下载一个打包好的JavaScript文件和CSS文件:

参考地址:

官方打包下载地址

打包之后的文件,只需要引入一个CSS和一个JavaScript文件即可.

示例

我目前使用的body下面的第一个div添加的class和data-toolbar-order:


		<div data-toolbar-order="show-language,copy-to-clipboard" class="line-numbers match-braces rainbow-braces">
		</div>

使用方式

  1. 先引入css
  2. 然后在pre,code中包含代码,同时为code设置class,class格式为: language-css,language-html,language-java或者lang-java,lang-css,lang-html.更多支持语言的简写,参考这里的Supported languages.
  3. 最后引入prism.js文件

下面的代码只会显示代码高亮(未引用语言文件的情况下,默认只包含了css,html,js语言的高亮文件):



<!DOCTYPE html>
<html>
<head>
	<title>test</title>
	<link href="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/themes/prism-dark.min.css" rel="stylesheet">
</head>
<body>
<pre><code class="language-css">p { color: red }</code></pre>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/prism.min.js"></script>
</html>


使用插件

目前我使用了如下插件:

  • Line Numbers(显示行号)
  • Remove initial line feed(删除初始换行)
  • Normalize Whitespace(空白规范)
  • Toolbar(工具栏)
  • Copy to Clipboard Button(复制按钮)
  • Show Language(显示语言)
  • Match braces(括号高亮[选中单个括号,另外一个对应的括号同时高亮])

显示行号

引入方式:

css:


	<link href="https://cdn.bootcdn.net/ajax/libs/prism/1.20.0/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet">

JavaScript:

	
		<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.20.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
	

然后在body下面的第一个div设置:

	
		<div id="page" class="line-numbers">
			......内容
		</div>

之后直接使用即可:


<pre><code class="language-css">
p { color: red }

</code></pre>

括号高亮

引入:

css:


		<link href="https://cdn.bootcdn.net/ajax/libs/prism/1.20.0/plugins/match-braces/prism-match-braces.min.css" rel="stylesheet">

JavaScript:


<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.20.0/plugins/match-braces/prism-match-braces.min.js"></script>

参考效果: 官方文档

实现效果:

添加class: match-braces 到body下面的第一个div即可.例如:

	
		<div id="page" class="line-numbers match-braces">
			......内容
		</div>

如果要显示五颜六色的括号,则继续添加: rainbow-braces,例如:

	
		<div id="page" class="line-numbers match-braces rainbow-braces">
			......内容
		</div>

工具栏

引入:

CSS:

	
	<link href="https://cdn.bootcdn.net/ajax/libs/prism/1.20.0/plugins/toolbar/prism-toolbar.min.css" rel="stylesheet">

JavaScript:

	
	<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.20.0/plugins/toolbar/prism-toolbar.min.js"></script>

然后给body设置工具栏要显示的内容:

	
<div data-toolbar-order="show-language,copy-to-clipboard" class="line-numbers match-braces rainbow-braces">
	......
</div>

show-language,copy-to-clipboard

第一个是显示代码块中的语言,第二个是复制代码.

工具栏可以自定义.具体参考: 工具栏插件文档

显示语言

引入:

	
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.20.0/plugins/show-language/prism-show-language.min.js"></script>

复制代码

引入:

	
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.20.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js"></script>

即可完成工具栏的效果.

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

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


{
    "code":"0",
    "data":{
        "city":"xxxxxxxxxxxx",
        "province":"xxxxxxxxxxxx",
        "country":"China",
        "watermark":{
            "appid":"xxxxxxxxxxxx",
            "timestamp":"xxxxxxxxxxxx"
        },
        "openId":"xxxxxxxxxxxx",
        "nickName":"xxxxxxxxxxxx",
        "avatarUrl":"xxxxxxxxxxxx",
        "unionId":null
    },
    "message":""
}

修改参考:


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和小程序秘钥:


#################################### 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


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数据进行解析处理.


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



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的限制请参考官方文档).



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

Java 14: instanceof的模式匹配(预览)

之前的instanceof使用

在Java 14对instanceof进行了一些改动.这些改动目前是预览的(后期可能会有变化).

在Java 14之前,我们通常是这样使用instanceof的:


static void show(Object obj){
  User user=null;
  if(obj instanceof User){
    user=(User)obj;
    System.out.println(user.name());
  }
}

会发现在instanceof判断obj是User的实例之后,还需要进行一次强制转换并且还需要提前定义一个变量(user)接收强制转换后的结果,才能使用.显得多余且繁琐.这次的改进就是为了优化instanceof的使用而来.

java标志
image-3141

Java 14中对instanceof的改进(预览)

基本改进

先来一段代码:

static void show1(Object obj){
        if(obj instanceof User user){
            System.out.println(user.name());
        }
}

 

可以和上面对比一下改进,发现至少有两点改进:

  • 1. 不用提前定义局部变量user;
  • 2. 局部变量名可直接写在类型后面,也就是user,在判断为true时,可直接使用user.

模式变量的作用域

模式变量存在作用域限制: 它只能在if中使用,而不能在else if/else中使用.否则会提示编译错误.

如下:

static void show1(Object obj){
    if(obj instanceof User user){
        System.out.println(user.name());
    }else if(obj instanceof String){
        // System.out.println(user.name()); error
    }else{
        // System.out.println(user.name()); error
    }
}

 

当然你可以这样用:



    private static User user=new User("aa",12);
static void show1(Object obj){

    if(obj instanceof User user){
        // 调用instanceof模式匹配变量.
        System.out.println(user.name());
    }else if(obj instanceof String){
        // 调用的静态变量user.
        System.out.println(user.name()); 
    }else{
        // 调用的静态变量user.
        System.out.println(user.name());
    }
}

因此,记住只在if中使用instanceof模式变量.

instanceof模式变量简化if表达式

可以使用instanceof模式变量来简化if表达式:

先看原始的:


public class Person {
    String name;
    int age;

    @Override
    public boolean equals(Object o) {
        if (o instanceof Person) {
            Person other = (Person) o;
            if (name.equals(other.name) && age == other.age) {
                return true;
            }
        }
        return false;
    }
}

简化第一步,引入模式变量:


    public class Person {
        String name;
        int age;

        @Override
        public boolean equals(Object o) {
            /// 将局部变量other提入条件表达式.
            if (o instanceof Person other) {
                /// 移除此行. Person other = (Person) o;
                if (name.equals(other.name) && age == other.age) {
                    return true;
                }
            }
            return false;
        }
    }

第二步,将第二个if与第一个if合并:

public class Person {
        String name;
        int age;

        @Override
        public boolean equals(Object o) {
            /// 将局部变量other提入条件表达式.
            if (o instanceof Person other && name.equals(other.name) && age == other.age) {
                /// 移除此行. Person other = (Person) o;
                /// 与上层if合并. if (name.equals(other.name) && age == other.age) {
                    return true;
                /// 与上层if合并 }
            }
            return false;
        }
    }

第三步,终极优化,直接返回if中的条件判断:

public class Person {
        String name;
        int age;

        @Override
        public boolean equals(Object o) {
            return o instanceof Person other && name.equals(other.name) && age == other.age;
            /// 将局部变量other提入条件表达式.
            /// if (o instanceof Person other && name.equals(other.name) && age == other.age) {
                /// 移除此行. Person other = (Person) o;
                /// 与上层if合并. if (name.equals(other.name) && age == other.age) {
                /// return true;
                /// 与上层if合并 }
            /// }
            /// return false;
        }
}

仅需一行,就可以直接返回equals的结果.

完整代码

完整代码如下:

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;

record User(String name, int age) implements Serializable {
}

public class Main {



    public class Person {
        String name;
        int age;

        @Override
        public boolean equals(Object o) {
            return o instanceof Person other && name.equals(other.name) && age == other.age;
            /// 将局部变量other提入条件表达式.
            /// if (o instanceof Person other && name.equals(other.name) && age == other.age) {
                /// 移除此行. Person other = (Person) o;
                /// 与上层if合并. if (name.equals(other.name) && age == other.age) {
                /// return true;
                /// 与上层if合并 }
            /// }
            /// return false;
        }
    }



    static void show(Object obj){
        User user=null;
        if(obj instanceof User){
            user=(User)obj;
            System.out.println(user.name());
        }
    }

    private static User user=new User("aa",12);
    static void show1(Object obj){
        if(obj instanceof User user){
            System.out.println(user.name());
        }else if(obj instanceof String){
            System.out.println(user.name());
        }else{
            System.out.println(user.name());
        }
    }

    public static void main(String[] args) {
        var user = new User("测试用户", 20);
        show1(user);
        show1("str");
        show1(new StringBuffer("StringBuffer"));
    }
}

购买了阿里CDN

关于阿里CDN

一共有三种:

  1. CDN: 将源站内容分发至最接近用户的节点,使用户可就近取得所需内容,提高用户访问的响应速度和成功率。
    解决因分布、带宽、服务器性能带来的访问延迟问题,适用于站点加速、点播、直播等场景。
  2. 全站加速DCDN: 旨在提升动静态资源混合站点的访问体验,支持静态资源边缘缓存,动态内容最优路由回源传输,同时满足整体站点的全网访问速度及稳定性需求。全站加速构建于阿里云CDN平台之上,适用于动静混合型、纯动态型站点或应用的内容分发加速服务。
  3. 安全加速SCDN: 旨在为网站做加速的同时,防护DDoS,CC,Web应用攻击,恶意刷流量,恶意爬虫等危害网站的行为。它构建于阿里云CDN平台之上,在CDN边缘节点中注入了阿里云云盾十年积累的安全能力,形成一张分布式的安全加速网络。适用于所有同时要兼顾内容加速和安全的网站。

购买与配置

此处仅针对全站加速DCDN,另外两个类似.

第一步是先购买资源包,可以从: 阿里CDN首页,点击 资源套餐包 进去购买.

购买如图(可能随着时间变化套餐内容和价格有变化):

image-3136

就买个便宜的试试.

第二步就是开通,全站加速CDN了.地址在: 全站加速DCDN . 开通之后就可以看到控制台了,这时候就需要去配置了.

控制台是下面这个样子:

image-3137

添加域名:

要加速的域名就是你想给用户访问的加速过的域名,源站和加速域名不能一致,也就是: 你必须有一个域名地址能访问到你的网站,然后把另外一个地址用来加速.比如blog.bckf.cn用来作为源站,www.bckf.cn用来加速,用户访问www.bckf.cn就是访问的加速过的域名了.

image-3138

  • OSS: 阿里云的存储服务,添加之后审核会很快.
  • IP: 可以用ip作为源站,但前提是ip必须是可以访问到网站的.可以有多个.
  • 域名: 使用自己的域名作为源站.可以有多个.

端口: 一般就是如果源站访问使用的是SSL,则可以选择443端口.如果没有使用SSL,则选择80端口.

加速区域: 哪里用户访问的多就选哪里.

保存之后会短暂审核,大概10分钟左右.(OSS的审核速度可能会快一些).

审核之后,需要给加速域名的DNS配置CNAME记录(不能与A记录共存).配置方法可以参考: 配置CNAME.

开启SSL

配置了CNAME之后,可以使用阿里云的免费SSL证书服务.

配置方法参考: 全站加速配置SSL