大虾居

谈笑有鸿儒,往来无白丁。

0%

Singleton是GoF设计模式之一,其易于理解,实现简单,因此得到广泛使用。但是在OOD
面向对象设计领域也有一些批评的声音,说不应该使用singleton模式,甚至有人提出
singleton是反模式(anti-pattern)
虾哥认为技术本身是中性的,没有生来就错的技术(inherently bad),作为工程师应该
有能力掌握技术,知道该在何时何地正确的使用何种技术。

那么下面就说说singleton的性质,以及如何避免误用singleton模式。

Singleton的性质

优势:

  1. 作为静态成员,可以在程序不同位置”便捷地”调用一个共享的对象。
  2. 共享的对象有且只有一个实例。
  3. 共享的对象是一个类的实例,可以使用面向对象方法进行设计,如实现接口。

劣势:

  1. Singleton自身既包含行为的实现,又包含其自身的创建,违反了单一职责原则。
  2. Singleton作为全局对象,可能会隐藏一些依赖关系,如需要按指定顺序访问全局对象的两个
    对象实际上是有依赖的。
  3. 直接的依赖会构成紧耦合,导致很难进行模拟测试(stub or mock)

为什么需要使用singleton模式


插图:面向对象的生命周期树

基于面向对象思想编写的程序,其运行过程中实际上就是各种对象的生命周期不断新陈代谢的过程。
每个对象都有负责构建它的另一个对象,因此每个对象实例的生命周期可以看作层次结构,可以表现为树形。

有的时候我们需要在树的不同位置共享同一个对象实例,用纯面向对象的方式,我们需要找到两个对象的共同祖先,
在祖先对象上构造需要共享的对象,并逐层传递到子孙对象上。但是如果层次结构比较深,这种方式就过于繁琐了。
想象一下你需要给程序中每个对象都传递一个Logger对象。


插图:传递一个对象引用

这种情况下我们可以构造另外一棵树,他的最原始祖先是一个全局变量或者静态成员(即它的生命周期不被其他对象拥有)。
这时我们可以方便的从一棵对象生命周期树的不同节点同时访问另一棵树,即可完成对象的共享。

这种模式在框架设计上非常常见,即提供统一的静态入口点访问某一框架的具体功能,例如log4net中的LogManager对象。

如何消除或减弱singleton 的不利影响

我们假设实际的场景中类之间的关系是这样的

这其中有两个依赖关系,即 A类依赖于Singleton类,Singleton类依赖于B类。这种类和类之间的直接依赖违反了dependency inversion原则,造成了紧密的耦合。

可以使用以下一种或几种方式共用来降低负面影响。

依赖反转

A和Singleton之间的关系可以通过依赖反转来解决。

A类只依赖于ISingleton的约定而非Singleton类的具体实现。


修改前


修改后

依赖注入 Dependency Injection

让使用者(client)被注入一个其依赖的对象的实例(service),而不是让它自行构建或获取service的实例。

public class A
{
    private ISingleton singleton;
    public A(ISingleton singleton)
    {
        this.singleton = singleton;
    }

    public void Foo()
    {
        this.singleton.Bar()
    }
}

不变对象 Immutable

如果全局对象的写入和读取在顺序上有隐含条件,则该读取操作和写入操作实际上是有依赖关系的。
更糟糕的是这种依赖关系没有暴露出来造成不易察觉的设计问题。

提供一个显式或隐式的初始化过程,在其生命周期中保持状态不会变化,确保总是有
可预期的调用,以便支持单元测试。

隐式初始化应从一个指定位置,如环境变量,某个(或多个)具体的配置文件路径,加载构造内部对象所需的信息。
则单元测试时可以通过间接的方式修改构造过程注入所需的mock对象。

参见log4net config
https://logging.apache.org/log4net/release/manual/configuration.html

参考

https://en.wikipedia.org/wiki/Dependency_inversion_principle
https://en.wikipedia.org/wiki/Dependency_injection
https://fuzhe1989.github.io/2017/09/30/why-global-static-singleton-var-evil/
https://www.dre.vanderbilt.edu/~schmidt/PDF/Context-Object-Pattern.pdf

为什么要添加一个初始化超级用户的命令行入口

django提供了一系列重要而且好用的命令行工具,使用创建项目时自动生成的manage.py,
或者全局django-admin都可以运行这些程序。

python mamage.py

django-admin

其中在启用了django.contrib.authapp之后,我们可以受用createsuperuser命令来创建超级用户。

python manage.py createsuperuser

这个命令通常时我们启用django身份验证功能之后要登录到admin站点必须要使用的命令。但是这个命令的
问题在于它只能用交互方式运行,我们无法用一个非交互式脚本来创建用户,比如我们的站点需要一键
安装、自动化搭建测试环境等等场景。

在使用命令行之外我们也有其他选项,比如在migrations增加一个创建默认超级用户的脚本,在安装过程中
执行migrate从而添加默认超级用户。但这样做的问题是默认用户信息在所有环境中都是相同的,如果管理人员
忘记修改默认密码,可能会导致安全风险。

如果有一个命令行能用无交互的方式,使用参数来指定超级用户的账号和密码,就会适应更多场景的要求。

如何实现

命令行接口的设计

我们需要一个命令行工具,用如下的方式创建一个超级用户。

python manage.py initadmin --user=admin --password=YourPassword --email=abc@example.com

这个命令行还能从环境变量中读取参数信息

DJANGO_SU_NAME=admin DJANGO_SU_PASSWORD=YourPassword DJANGO_SU_EMAIL=abc@example.com python manage.py initadmin 

或者

export DJANGO_SU_NAME=admin
export DJANGO_SU_PASSWORD=YourPassword
export DJANGO_SU_EMAIL=abc@example.com
python manage.py initadmin 

使用django custom django-admin commands框架

django提供了自行扩展命令行工具的框架,添加相关代码后可以直接采用manage.py或python-admin命令行进行调用。
相关文档可以参考!()[https://docs.djangoproject.com/en/3.0/howto/custom-management-commands/]

首先需要在自己的app中创建指定文件夹,django会从指定文件夹中加载相应模块,假设你的app名字为myapp,
command模块应在myapp/management/commands文件夹下,
这两个文件夹都是package,因此别忘了在management文件夹和commands文件夹都添加__init__.py文件,

commands 包下添加initadmin模块,即initadmin.py

# myapp/management/commands/initadmin.py
import os
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User


class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument('--user')
        parser.add_argument('--password')
        parser.add_argument('--email')

    def handle(self, *args, **options):
        admin_username = options.get('user') or os.environ.get('DJANGO_SU_NAME')
        admin_email = options.get('email') or os.environ.get('DJANGO_SU_EMAIL')
        admin_password = options.get('password') or os.environ.get('DJANGO_SU_PASSWORD')

        if not admin_username:
            raise Exception("No username specified.")

        if not admin_email:
            raise Exception('No email specified.')

        if not admin_password:
            raise Exception('No password specified.')

        if User.objects.filter(username=admin_username).first():
            print(f'{admin_username} user already exists.')
            return

        admin_user = User.objects.create_superuser(username=admin_username,
                                                   email=admin_email,
                                                   password=admin_password)
        admin_user.is_active = True
        admin_user.is_admin = True
        admin_user.save()
        print(f'{admin_username} user created.')

使用

命令行使用

如前面提到的方式使用即可,可通过命令行参数指定admin用户信息,或者使用环境变量

DJANGO_SU_NAME=admin DJANGO_SU_PASSWORD=YourPassword DJANGO_SU_EMAIL=abc@example.com python manage.py initadmin 

DJANGO_SU_NAME=admin DJANGO_SU_PASSWORD=YourPassword DJANGO_SU_EMAIL=abc@example.com python manage.py initadmin 

在docker中使用

首先在entrypoint.sh 中增加检查选项,并根据选项运行命令行

# docker-entrypoint.sh
if [ "x$DJANGO_MANAGEPY_INITADMIN" = 'xon' ]; then
  $PYTHON manage.py initadmin
fi

在docker运行时指定参数

docker run -e DJANGO_MANAGEPY_INITADMIN=on -e DJANGO_SU_NAME=admin -e DJANGO_SU_PASSWORD=YourPassword xxx

或者在docker-compose中指定参数

#docker-compose.yml
version: '3'
services:
  web:
    image: xxx
    ports:
      - "8000:8000"
    environment:
      DJANGO_MANAGEPY_INITADMIN: 'on'
      DJANGO_SU_NAME: 'admin'
      DJANGO_SU_PASSWORD: 'YourPassword'

为什么要使用默认站点空证书

开启HTTPS通信可以有效防范中间人攻击,是当前web安全的最佳实践之一。但是在默认情况下
通过IP直接访问443(HTTPS默认端口),nginx会认为是在访问第一个已配置的ssl server,将
请求发送至web节点,并在TLS通讯握手过程中返回该节点对应的证书信息。

这种默认行为会带来以下安全威胁:

服务器响应随意的攻击扫描请求

在公网上充斥各各种扫描攻击程序,他们会大范围的扫描IP和常用端口,对服务器发送无差别的请求。
处理这些请求可能导致额外的安全风险,以及额外的资源损耗。

泄露隐藏于站点防护之后的源站点信息

有时为了防御来自互联网的流量攻击,如DDos攻击,我们会把源站点部署在防御节点之后,即用户请求
先发送到防御服务器,web服务器处理由防御服务器转发来的请求。

这种情况下web服务器不应该直接处理任何流量,也不应该由用户直接访问。处于运维调试等目的,我们
仍然希望https端口可以访问,同时web服务器可以根据某些规则处理请求以便维护人员可以检查服务状态。
此时源站点信息应该是对外保密的,但如果证书信息泄露,可能导致攻击者绕过防御节点直接访问源站点,
造成损失。

如何解决?

通过制定nginx在处理没有制定域名(host)的请求直接返回默认http消息,可以避免信息泄露。

在http端口上的配置如下:

server {
    listen       80  default_server;
    server_name  _;
    return       444;
}

指定server_name为空_可以指定处理所有未匹配的server_name匹配规则的请求。http://nginx.org/en/docs/http/server_names.html

配置SSL端口也是相同的思路,但是SSL配置必须指定SSL证书私钥,因此我们需要生成一个本地的证书,
不带有任何站点信息。

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/conf.d/nginx.key -out /etc/nginx/conf.d/nginx.crt

运行后需要命令行交互输入一些参数,一直Enter使用默认值即可。生成的公钥证书为nginx.crt,私钥证书为nginx.key

按需要将证书文件复制到nginx需要访问的位置。然后配置nginx ssl站点。

server {
    listen              443 ssl default_server;
    server_name         _;
    ssl_certificate     /etc/nginx/conf.d/nginx.crt;
    ssl_certificate_key /etc/nginx/conf.d/nginx.key;
    return              444;
}

可以将以上默认站点http端口和https端口配置在同一个文件中,如/etc/nginx/conf.d/default.conf

server {
    listen              80  default_server;
    server_name         _;
    return              444;
}

server {
    listen              443 ssl default_server;
    server_name         _;
    ssl_certificate     /etc/nginx/conf.d/nginx.crt;
    ssl_certificate_key /etc/nginx/conf.d/nginx.key;
    return              444;
}

新版本的NPP不再包含插件管理器,需要的话需要自行安装。

安装方法1

download and install version 7.3.3:

https://notepad-plus-plus.org/download/v7.3.3.html

and then download and upgrade it with the latest version:

https://notepad-plus-plus.org/download/v7.5.1.html

安装方法2

手动下载插件二进制包

https://github.com/bruderstein/nppPluginManager/releases

uni = 32位版本 , x64 = 64位

解压到notepad++程序文件夹,重启程序即可。

出错信息

在一台新装的Centos7虚拟机上运行Docker,提示错误

ERROR: for hello_world  Cannot start service hello_world: oci runtime error: container_linux.go:235: starting container process caused "process_linux.go:258: applying cgroup configuration for process caused \"Cannot set property TasksAccounting, or unknown property.\""

解决方法

原因是centos系统版本落后,通过yum更新后重启即可

yum update -y
shutdown -r

Google Play是Android官方应用商店,App非常齐全,但是由于某些不可抗力的原因,
我们的手机不能直接使用。

下面介绍一种方法,在PC上可以从Google Play直接下载APK。本方法需要科学上网。

1. 安装浏览器插件

插件名:Play Store APK Downloader

Chrome和Firefox上都有同名插件。

2. 在GooglePlay打开要下载的APP页面

在Google Play中搜索APP并打开APP首页,此时插件图标会提示数字1,点击插件图标即可打开下载页面

3. 在下载页面下载

下载页面在一个网站里面,需要在页面上点击下载,即可下载。

我们在使用一些公共镜像的时候,往往不操作build过程,跟自行维护的服务器设置时区还是有所区别的。
那么我们如何既能使用到公共镜像的便利,又能根据自己的需要设置容器的时区呢,下面就介绍相关的方法。

1 使用Date命令

最简单的修改容器时间的方法是在创建容器后使用date命令

docker exec -it container-name /bin/bash 
date +%T -s "10:00:00"

时区的变化一般会立即反映出来,但有些时候必须要重启才能生效。

2 使用环境变量

通过设置环境变量也可以设置容器的时区信息

docker run -e TZ=Asia/Shanghai ubuntu date

相应的在docker-compose下

environment:
  - TZ=Asia/Shanghai

需要安装时区数据包tzdata才能使用环境变量设置。通过设置NTP服务器,我们可以确保容器里的时区已经同步。

3 使用 Dockerfile

在主机环境中,或者需要非常多相同容器要运行的时候,最简单的方法是使用Dockerfile。

Dockerfile包含了每个容器最基本的配置信息,要修改Docker容器的时区,我们可以修改相应的Dockerfile.

这些配置信息在容器被重新创建(recreate)时会被反映出来,而且这些在Dockerfile中运行的命令会在root用户下运行。

比如:

RUN echo "Asia/Shanghai" > /etc/timezone
RUN apk add tzdata

安装tzdata包的方式在大部分linux系统下都相同。

我们也可以在entrypoint入口点脚本中包含设置时区的指令,这可以确保无论何时容器重新启动时时区设置都生效。

4 使用volumes卷

使用Docker容器的最大的问题就是在重新启动时,保存的容器中的数据不会被持久化。为了解决这种问题,我们使用
Docker data volumes卷。

数据卷Data volumes可以在docker服务器共享,并且包含容器里面的特定数据。在卷中的数据可以被持久化而不会在
容器重新创建时丢失。

Docker中的文件夹 /usr/share/zoneinfo 包含可以使用的时区信息,把想要的时区信息从这个文件夹拷贝到
/etc/localtime文件就可以设置时区。

我们可以在服务器上设置好时区,然后把这个时区文件通过volume共享到容器里面

volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"

这样通过在docker-compose中设置的时区信息就会跟服务器达成一致了。

5 使用镜像

有时候在容器数量太大的时候,手动修改时区并不可行,要创建更多相同时区设置的实例的时候,我们可以使用镜像Images。

在给一个容器设置好时区后,退出这个容器,然后用docker commit从这个容器创建一个新镜像。NTP服务器配置
也可以在镜像里配置好。

docker commit container-name image-name

使用这个镜像,我们就可以创建很多个基于相同时区的容器了,可以保存这个镜像已备将来使用。

编写python程序难免需要同时用到python2和python3两种环境,那么怎么才能在windows 10上同时安装两种环境呢

1 安装python2.7

1.1 在官方网站下载python2.7

https://www.python.org/downloads/windows/

1.2 运行安装程序


注意选择把python添加到path环境变量

2 安装python3.7

2.1 在官方网站下载python3.7

https://www.python.org/downloads/windows/

2.2 运行安装程序


选择自定义安装


安装选项里勾上pip


路径改为 c:\Python37,选择将路径添加到path环境变量

3 配置

右键点左下角win图标-系统- 弹出窗口里点击“系统信息” - “高级系统设置” - “环境变量”

在环境变量中确认python2和python3路径都存在。

默认情况下python2和python3的运行程序都是python.exe,所以才出现难以共存的问题,
我们可以找到python2运行目录,把python.exe改名为python2.exe

但是此时会发现python2可以运行了,但是pip2运行不了。


卸载pip2

python2 -m pip uninstall pip

下载get-pip.py

python2 get-pip.py

这样就可以了

以前要在服务器上安装python访问mysql的库是相当麻烦,既要安装mysql-devel开发包,还要安装python源码。

那么如何在在Centos7上安装MySQL-python库呢。

1 yum 安装

sudo yum install MySQL-python

再也不需要安装一大堆东西了。完成。