CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

为什么

搭建VSCode+Docker搭建PHP远程开发环境搭建,主要解决以下几个问题:

  • 微信开发调试问题。

  • 随时随地办公问题(具体参看code-server)。

  • 减少本地环境污染,PHP与Node版本可能冲突(dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib)。

  • 减少硬盘使用。

    阅读全文 »

现在的脑科学认为,人脑关于记忆主要分为两种功能:存储功能和提取功能。在《如何学习:成就你的终身学习力》中有一段话是这么写的:

记忆是不会“丢失”的,不会像我们以为的那样越变越淡,直至踪迹全无。准确地说,“丢失”——其实只是我们一时无法提取出来记忆而已,它的提取能力在当下很低,低到几乎为零。

作者还举了一个在老式图书馆搜集材料,因为霉味而勾起自己在1982在图书馆工作一段回忆的例子。如果换成触景生情的说法,想来也是有碰到过的。

有些记忆大师公开说过,会在记忆的过程中会将要记忆的内容,配合图像(或故事)一起编码来加速和巩固记忆。

阅读全文 »

问题

Jfinal已经集成了EhCache缓存,只要简单配置就可以使用EhCache。最近的一个字典表数据量比较大,于是开启了EhCache,页面加载速度明显变快。昨日服务突然无法正常启动,错误如下(非法完整错误日志):

1
net.sf.ehcache.CacheException: java.io.InvalidClassException: com.xxx.model.Concept; local class incompatible: stream classdesc serialVersionUID = -1234, local class serialVersionUID = -5678

看提示我们知道是serialVersionUID版本引起的问题。我们知道ehcache就会进行存盘的,如果保存的对象版本不一致就有可能出现这个问题,此时回忆起对Concept所在的表做了修改和重新生成。

解决

问题基本清晰了,那解决方法其实是相对简单的,把缓存的文件删除就可以了。

具体做法是这样的,找到ehcache.xml配置文件的的存盘位置,这里我用了java.io.tmpdir,不同操作系统类型此路径可能不一致,可以在程序中打印出来看下, 我这里是/var/folders/d0/..

1
System.out.println(System.getProperty("java.io.tmpdir"));

删除临时文件,需要根据自己的实际路径进行删除。

1
2
cd /var/folders
rm -rf d0

重新启动服务,正常启动。

更好的方式

因为系统是可以正常使用的,可以编写个清理的接口,这样可以在数据结构调整时及时清理缓存。

背景

我们需要先了解下认证和授权,因为这两个概念常常被混淆。认证是鉴定用户身份的过程,它通常使用一个标识符 (如用户名或电子邮件地址)和一个加密令牌(比如密码或者存取令牌)来 鉴别用户身份(处理我是谁的问题)。授权是指验证用户是否允许做某件事的过程(处理我能做什么的问题)。更详细描述可以参看这里

接下来说说,我们可能碰到的问题,一个产品可能会发布多个前端应用,比如Web,APP,小程序(微信,百度等),管理后台,不同的前端应用可能会有不同需求的。比如微信的授权认证和App的认证就不一致,Web和APP因为能力不同,需要的接口也可能不同,而管理后台可能要基于角色进行授权。

解决方案

面对这些问题的时候我们使用Jfinal该如何解决呢?

首先需要参看文档,配置路由Routes 级别拦截器

我们可以针对不同的前端应用进行路由拆分和模块化,同时为不同的Routes添加各自的拦截器,实现不同的级别的认证与授权。

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
public class WebRoutes extends Routes {
public void config() {
addInterceptor(new JwtInterceptor());
add("/api/web/demo", DemoWebController.class);
}
}

public class AppRoutes extends Routes {
public void config() {
addInterceptor(new JwtInterceptor());
add("/api/app/demo", DemoController.class);
}
}

public class WechatRoutes extends Routes {
public void config() {
addInterceptor(new WechatInterceptor());
addInterceptor(new JwtInterceptor());
add("/api/wechat/demo", DemoController.class);
}
}

public class AdminRoutes extends Routes {
public void config() {
addInterceptor(new AdminAuthInterceptor());
add("/admin/api/demo", DemoAdminController.class);
}
}

public class JwtInterceptor implements Interceptor {
public void intercept(Invocation inv) {
System.out.println("Verify Jwt");
inv.invoke();
}
}

public class JwtInterceptor implements Interceptor {
public void intercept(Invocation inv) {
System.out.println("Verify Wechat");
inv.invoke();
}
}

public class AdminAuthInterceptor implements Interceptor {
public void intercept(Invocation inv) {
System.out.println("Verify Admin");
inv.invoke();
}
}

public class DemoController extends Controller {
//登录接口,清理指定拦截器
@Clear({WechatInterceptor.class, JwtInterceptor.class})
public void login() {
}
}

public class DemoConfig extends JFinalConfig {
public void configRoute(Routes me) {
me.add(new WebRoutes()); // Web路由
me.add(new AppRoutes()); // App路由
me.add(new WechatRoutes()); // App路由
me.add(new AdminRoutes()); // 后端路由
}
public void configConstant(Constants me) {}
public void configEngine(Engine me) {}
public void configPlugin(Plugins me) {}
public void configInterceptor(Interceptors me) {}
public void configHandler(Handlers me) {}
}

以上代码并非完整代码,也没有实现实际的拦截器逻辑,只是为了简要的说明问题,如果你在实际的实现过程中碰到问题,可以和我联系,或许能帮上忙。通过配置不同的路由和拦截器,我们还可以尽可能多的复用代码(Controller和Service)。

参看资料

https://insights.thoughtworks.cn/api-2/

https://jfinal.com/doc

https://www.yiiframework.com/doc/guide/2.0/zh-cn

概要

对于前后端分离的项目,无论是开发还是部署,可能都会碰到CORS问题。采用Jfinal-undertow方式发布的应用来说,通常是不需要考虑CORS的,因为前端与后端是一起发布的,但在开发时或采用分离部署时,还是会碰到的这个问题,因此在这里按开发和部署两部分讲解如何解决CORS问题。

首先我们简单的了解下什么是CORS,完整的资料可以自己查看wikipedia

CORS

跨域资源共享(CORS,Cross-origin resource sharing),它是一种允许受限的访问第一个访问资源域以外的资源(如字体)的机制。一张网页通常包含了很多跨域的图片,样式,脚本,iframes,或者视频,但一些跨域请求,尤其是Ajax请求,会因为默认的同源安全策略(简单来说就是绝对路径,需要是协议,域名,端口号三者与第一个访问的资源(浏览器中输入的URL)相同,才被认为是同源)而被禁止访问。

CORS定义了浏览器和服务器如何来决定一个跨域请求是安全的。它比完全同源请求有更多的自由和功能,但不要只是简单地允许所有跨源请求。CORS规范最初是作为W3C推荐标准发布的,但该文档已经过时。 Fetch Living Standard是当前维护活跃的规范。

对于程序开发来说,可能知道与CORS相关的请求头与响应头,已经够用了。如果需要更细的控制,可以进一步的阅读文档。

请求头

  • Origin

  • Access-Control-Request-Method

  • Access-Control-Request-Headers

响应头

  • Access-Control-Allow-Origin

  • Access-Control-Allow-Credentials

  • Access-Control-Expose-Headers

  • Access-Control-Max-Age

  • Access-Control-Allow-Methods

  • Access-Control-Allow-Headers

我们通常使用的是Access-Control-Allow-Origin: *

开发阶段

前端处理

如果后端没有做任何处理的情况,前端通常使用设置Chrome跨域选项和配置vue-cli代理模式这两种方式。推荐使用配置vue-cli代理模式,因为测试人员就可以不需要修改选项了。

修改Chrome跨域选项

对于Windows用户来说,创建个Chrome的快捷方式,target后中增加--disable-web-security --user-data-dir=即可,如:

1
"...\chrome.exe" --disable-web-security --user-data-dir=

对于MacOS用户来说,需要创建好用户数据目录,否则可能无法打开。

1
open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir=/Users/work/ChromeDevUserData

vue-cli修改代理模式

参照vue-cli3的文档中的devServer.proxy配置即可。

1
2
3
4
5
6
7
8
module.exports = {
/**
* vue-cli3跨域的全配置!
*/
devServer: {
proxy: 'http://172.26.0.252:8881'
}
}

后端处理

当然我们是希望前端不需要做任何配置,此时后端需要做些开发。实现的方式有很多,这里只展示一种方式简单的实现方式,通过扩展Interceptor实现CorsInterceptor,代码如下:

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
public class CorsInterceptor implements Interceptor {
@Override
public void intercept(Invocation inv) {
inv.invoke();
addCorsHead(inv.getController().getResponse());
}

private void addCorsHead(HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
}
}

//针对性的提供Cors支持
@Before(CorsInterceptor.class)
public class IndexController extends Controller {
public void index() {
renderJson(Ret.ok());
}
}

public class DemoConfig extends JFinalConfig {
@Override
@Override
public void configInterceptor(Interceptors me) {
//此处可以加个参数判断,是否加载CorsInterceptor
me.addGlobalActionInterceptor(new CorsInterceptor());
}
}

部署阶段

Jfinal-undertow部署目录结构

jfinal-undertow推荐采用目录结构部署系统,当然也支持fatjar打包,可以参看jfinal-undertow 下部署。以下是就打包好后的目录格式。

1
2
3
4
5
6
.app
├──config
├──lib
├──webapp
| └──index.html
└──jfinal.sh

采用这种方式部署是没有跨域问题的。

Nginx部署

正式环境更多的可能会用Nginx来做反向代理,将不同域名整一起。以下是简单示例,仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80;
server_name demo.com;
client_max_body_size 50m;

location / {
proxy_pass http://172.17.0.1:80;
}

location /api {
proxy_pass http://172.17.0.1:81/api;
}
}

参考链接

CORS 跨域资源共享

概要

本指南只针对使用jfinal开发后端,vue开发前端,使用undertow方式部署的项目。对于非undertow部署项目,可能有所区别,可参考本文自行调整。对使用了Ningx的用户,可以参看Vue文档进行配置。

本文主要针对vue-router采用不同的HTML5 History 模式时的不同处理方法。此处假定你已按undertow方式正常运行后台项目。

阅读全文 »

引子

今天看到一段Python程序,打印Python的保留关键字。

1
2
import keyword
print(keyword.kwlist)

程序写了这么多年,也接触了一些语言(C++、Go、Java、Javascript(TypeScript)、Kotlin、Pyton),印象中都是说语言规范,记下来就好。突然就好奇了,有多少语言能利用自己的内置类库完成这个事情呢?于是拿自己接触的这些语言来做个对比。

能否打印关键字

其实在StackOverflow上也有人有类似的问题,比如:

做了个简单的搜索,整理出下面这张表格,表格内容不一定准确,欢迎纠正。

Programming Languages Can print keys by SelfLib Keywords
C++ No https://en.cppreference.com/w/cpp/keyword
Go No https://golang.org/ref/spec#Keywords
Java No https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html
JavaScript No https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar
TypeScript No https://github.com/Microsoft/TypeScript/issues/2536
Kotlin No https://kotlinlang.org/docs/reference/keyword-reference.html
Python Yes https://docs.python.org/3/library/keyword.html

一些补充:

  • 打印保留关键字本身并不难,维护一个列表就可以的。
  • Java有个内置函数是可以判断是否为关键字的SourceVersion.isKeyword(str)

语言关键字统计

在找不同语言关键字的时候找到这样一个统计库。A list and count of keywords in programming languages

The number of keywords in a programming language can be an indication to it’s simplicity/complexity, and that can impact the simplicity/complexity of the solutions that developers produce using it. Complex solutions can be more expensive to maintain and difficult to hire for. However, this is dependent on many factors and keyword count is only one; language idioms also play a massive part.

细节之处见真章

提供丰富的类库,在意细节,是否会是Python能这么流行的原因呢?

说明

事情大致是这样的,在此之前我基本算是零基础,有那么一两次玩是刚好看到同事在玩,顺手玩玩,甚至没完成过一个面。这次呢,因为儿子买了个魔方,玩了会就没玩了,自己就拾起来玩玩,找了视频教程来看,完成整个魔方的复原,现在处于教小孩玩魔方的阶段。在这个学习的过程中呢,有了一些杂七杂八的思考,主要和软件开发有关,于是记录了下来。

如果你和我一样是魔方初学者,本文可能对你有点用,但这篇不是教程,建议看网上的视频教程。

阅读全文 »

背景

上一篇《基于ElasticSearch的OLAP框架思路》简单介绍了ES的存储结构,本文对ES的数据建模做些整理,是对上文的补充。

数据建模

范式化设计

在关系型数据库中会强调范式化设计(Normalization),其主要目标是“减少不必要的更新”,但也就带了一个问题“查询缓慢”,因为数据库越范式化,就需要Join越多的表。

关系型数据库设计时,通常设计时做到3NF,但在必要时进行数据的冗余。以下是范式的简要描述:

  • 1NF——消除非主属性对键的部分函数依赖
  • 2NF——消除非主属性对键的传递函数依赖
  • 3NF——消除主属性对键的传递函数依赖
  • BCNF——主属性不依赖主属性

反范式化设计

范式化的另一个好处呢,就是减少了存储空间(也有可能是早些年存储成本大,才有了范式化设计)。但在存储越来越便宜和数据量越来越大,查询需求也越来越多的情况下,关系型数据库就越来越受到挑战,非关系型数据库不断的涌现出来。其实可以理解来范式化设计与反范式化设计的交锋和解决不同的应用场景问题。

反范式化设计(Denormalization),就是尽量不去使用关联关系,而是在文档中保存存冗余的数据,将数据扁平化(Flattening)。这样做的优点很明显,就是数据读取性能好,缺点也很明显,不适合在数据频繁修改的场景,一条数据改动,会引起很多数据的更新,对数据一致性提出更高要求。

ES数据库设计时通常会采用反范式化设计,因为ES并不擅长处理关联关系,一般采用以下四种方法来处理关联:

  • 对象类型(Object)
  • 嵌套对象(Nested Object)
  • 父子关系(Parent/Child)
  • 应用端关联

其实应用端关联不属于ES设计的部分,在本文中不再赘述,另外基础的数据类型对数据建模影响不大,需要时参看ES相关文档进行设计即可。

对象类型

ES采用JSON的文档化存储,而JSON文档天然就是有层级的,文档内可以包含对象(普通对象和数组对象),如果存储数组对象并需要进行查询或统计时,计算结果可能是错误的,这个时候就需要用到嵌套对象。

嵌套对象

嵌套对象是对象类型的一个特例,允许对象数组中的对象独立索引,Nested文档会被保存在两个Lucene文档中,在查询时做Join处理,因此每次更新,需要重新索引整个对象(包括根对象和嵌套对象)。

父子关系

ES提供了类似关系型数据库中Join的实现。使用Join数据类型实现,可能过维护Parent/Child的关系,从而分离两个对象。这样呢,父文档和子文档是两个独立的文档,更新父文档时无需重新索引子文档,子文档被添加,更新或者删除时也不会影响父文档和其他的子文档。需要额外的内存维护,读取性能相对差。

最佳实践

应用场景

  • 对象类型,优先考虑。
  • 嵌套对象,当数据包含多数值对象,同时有查询需求
  • 父子关系,当关联文档更新非常频繁时。

避免过多字段

  • 过多字段不容易维护,Mapping保存在Cluster State中,数据量过大时会对集群性能造成影响。删除或修改需要reindex。
  • 默认1000,可通过index.mapping.total_fields.limit来进行设置
  • Dynamic默认是打开的,只要有新的字段进入Mapping就会更新,导致文档膨胀,可能通过设置Stick,采用Nested Object和Key Value来解决这个问题。

避免正则查询

  • 正则,通配符查询,特别是前缀查询,性能不好。可以采用Netest字段方式将一个字段拆分成多个字段。

避免空值引起的聚合不准

null值无论是在关系型数据库,还是在非关系型数据库,都是被特殊对待的。在ES中可以通过设置null_value,来指定字段为null时的默认值。

为索引的Mapping加入Meta信息

Mappings设置无论从功能,还是性能方面来说都非常的重要,Mappings的每一次变更的都可能造成功能和性能的变化或者数据迁移,因此需要增加Meta(版本)信息并利用版本化控制工具(如Git)进行管理。

重建索引

可以通过update_by_query来对历史数据的新增字段进行索引的重建。因为ES不允许对已有字段修改数据类型,只能通过reindex的方式将一个索引重新索引到一个新的索引上,但可能通过指定别名的方法来使得减少应用程序的修改。

数据预处理

对于OLAP来说,数据的清洗和转换是不可避免的,而通过Ingest Node和Painless Script。相当于关系型数据库的存储过程或函数。

Ingest Node:Pipeline&Processor(可实现插件)。

Painless Script:默认缓存100个。

参考资料

https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html

《Elasticsearch核心技术与实战》

背景

我们采用开发直接上线署的方式来快速开发,但因客户已经开始正式使用我司开发的小程序了,为了减少对客户的影响,现在需要额外部署一套开发环境。这个时候首先想到的就是通过Nginx来进行反向代理,开发环境使用二级域名,(如https://dev.zai500.com),方便开发小程序时候使用。因为中间有一些部署前没有碰过的问题,记录下来,以便后期项目时注意。

证书转换

公司项目是从SpringBoot转换到jfinal的,所以之前使用的是JKS格式的证书,需要进行转换,当然如果可以重新申请也能获得不同格式的证书。

jks格式转pkcs12

1
keytool -importkeystore -srckeystore server.keystore -destkeystore server.p12 -srcalias serverkey -destalias serverkey -srcstoretype jks -deststoretype pkcs12 -srcstorepass 111111 -deststorepass 111111 -noprompt

参数说明

  • importkeystore 指示导入导出keystore文件,可用于同类型或不同类型的导入导出
  • srckeystore 指示源keystore文件
  • srcalias 指示源实体别名
  • srcstoretype 指示源store类型(jks/pkcs12..)
  • srcstorepass 指示源store密码
  • noprompt 不弹出提示

查看jks信息

如果不清楚scralias可能通过如下命令来查看

1
keytool -list -keystore server.jks

创建Nginx容器

因为我们的应用是使用Docker部署的,我们很自然的,使用Docker来部署Nginx。

拷贝配置

1
2
docker run -p 80:80 -p 443:443 --name nginx -it nginx:stable
docker container cp nginx:/etc/nginx .

以上命令用将nginx的配置信息拷贝到宿主机的当前目录,之后可以将nginx改成conf

重新创建容器

现在映射配置目录,不采用文件映射的方式,因为我们需要配置证书。

1
2
3
docker stop nginx
docker rm nginx
docker run -p 80:80 -p 443:443 --name nginx -v /data/nginx/conf:/etc/nginx -it nginx:stable

注意这里不使用文件的直接映射,因为需要配置证书。

修改配置

将证书文件放置在conf目录下

1
2
3
4
5
6
- zai500.com
--- server.crt
--- server.key
- dev.zai500.com
--- server.crt
--- server.key

修改nginx.conf

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
server {
listen 443 ssl;
server_name dev.zai500.com;
ssl_certificate dev.zai500.com/server.crt;
ssl_certificate_key dev.zai500.com/server.key;
ssl_session_timeout 5m;

location / {
proxy_pass http://172.17.0.1:9080;
proxy_set_header Host $host:$server_port;
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_hide_header Vary;
proxy_set_header Accept-Encoding '';
proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie;

proxy_connect_timeout 300s;
proxy_send_timeout 900;
proxy_read_timeout 900;
}

location /im.ws {
proxy_pass http://172.17.0.1:9080/im.ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

注意事项

  • 为减少对外暴露的端口,proxy_pass使用宿主机IP,可能通过ip addr show docker0 来查看,其中inet 172.17.0.1/16部分可获取。
  • 如查有静态资源的访问需要配置好X-Forwarded-Proto和undertow(我自己是修改来程序逻辑),否则会出现Mixed Content的错误。
  • WebSocket的代理需要额外配置。

小结

本次Nginx的调整除了方便之后开发外,也加强了自己对Ng配置的理解。中间因为Mixed Content的处理多花费了不少时间。

参考资料

https://blog.csdn.net/liuchuan_com/article/details/54376258

https://yq.aliyun.com/articles/617244

https://www.cnblogs.com/mafly/p/websocket.html