通过Android Studio创建的Android项目,都有固定的目录结构。

一、项目根目录结构及说明

如下图所示:
AndroidProjectStructure.jpg

  1. .gradle.idea
    这两个目录下都是Android Studio自动生成的一些文件,不用关心,当然也不要去手动编辑。
  2. app
    项目代码、资源等内容几乎都是在这个目录下,我们的开发也基本都是在这个目录下进行。我们会在本会的第二部分还会对该目录下的内容单独说明。
  3. gradle
    这个目录下包含了gradle wrapper的配置文件,使用gradle wrapper的方式不需要提前将gradle下载好,而是自动根据本地缓存情况来确定是否需要联网下载gradle。Android Studio默认没有启用gradle wrapper的方式,如果需要打开,可以点击Android Studio导航栏→File→Settings→Build,Execution,Deployment→Gradle,进行配置修改。
  4. .gitignore
    这个文件是用来指定目录或文件排除在git版本控制之外的。
  5. build.gradle
    项目全局的gradle的构建脚本,通常该文件的内容不需要修改。
  6. gradle.properties
    项目全局的gradle配置文件,在这里配置的属性将会影响到项目中所有gradle编译脚本。
  7. gradlewgradlew.bat
    这两个文件是用在命令行界面中执行gradle命令的。(gradlew:是LinuxMac系统中使用;gradlew.bat:是Windows系统中使用的。)
  8. local.properties
    用于指定本机中的Android SDK路径,通过内容都是自动生成的,我们不需要修改。除非本机中的Android SDK位置发生了变化,可以在这个文件中的路径改为新的位置即可。
  9. settings.gradle
    这个文件指定项目中所有引入的模块。默认新创建的项目只有一个模块,一次该文件仅引入了app这个模块。通常情况下模块引入都是自动完成的,不需要我们手动去修改这个文件。

二、项目模块目录结构及说明

如下图示:
AndroidModuleStructure.jpg

  1. build
    这个目录主要包含了一些在编译时自动生成的文件,一般我们不要关心。
  2. libs
    如果项目中需要使用第三方jar包,就需要将这些jar包都放在这个目录。放在这个目录下的jar包都会被自动添加到构建路径里。
  3. src/androidTest
    用来编写AndroidTest测试用例的,可以对项目进行一些自动化测试。
  4. src/main/java
    这个目录是放置我们所有Java代码的地方。
  5. src/main/res
    这个目录下的内容比较多。项目中用到的所有图片、布局、字符串等资源都放在这个目录下。这个目录下还有很多子目录,图片放在drawable目录下,布局放在layout目录下,字符串放在values目录下。
  6. src/main/AndroidManifest.xml
    整个Android项目的配置文件,程序中定义的所有四大组件都需要在这个文件里注册,另外还可以在这个文件中给应用添加权限申明。
  7. src/test
    该目录用来编写Unit Test测试用例的。
  8. gitignore
    这个文件用于将app模块内指定的目录或文件排除在版本控制之外,作用域上层目录的.gitignore类似。
  9. app.iml
    IDE自动生成的文件。
  10. build.gradle
    是app模块的gradle构建脚本,这个文件会指定很多项目构建相关的配置。
  11. proguard-rules.pro
    这个文件用于指定项目代码的混淆规则。如果不希望代码被别人破解,通常会将代码进行混淆。

一、系统安装

1、重新生成SSH host key

参考How To: Ubuntu / Debian Linux Regenerate OpenSSH Host Keys

# 删除原来的host keys
/bin/rm -v /etc/ssh/ssh_host_*
# 重新生成keys
dpkg-reconfigure openssh-server
# 重启ssh
/etc/init.d/ssh restart

在需要访问服务器端客户端上更新ssh指纹:

ssh-keygen -R <your_server_host>

2、更新包管理器及更新软件apt-get updateapt-get upgrade

设置apt源

比如我这里测试中国科技大学综合效果最好,更改sources.list中配置的源:

vim /etc/apt/sources.list
deb http://mirrors.ustc.edu.cn/debian/ stretch main
deb-src http://mirrors.ustc.edu.cn/debian/ stretch main

deb http://mirrors.ustc.edu.cn/debian-security stretch/updates main contrib non-free
deb-src http://mirrors.ustc.edu.cn/debian-security stretch/updates main contrib non-free

deb http://mirrors.ustc.edu.cn/debian/ stretch-updates main contrib non-free
deb-src http://mirrors.ustc.edu.cn/debian/ stretch-updates main contrib non-free

更新源中包数据库

  1. apt-get update出现:TypeError: 'NoneType' object is not callable

apt-get.jpg
解决:(参考Python 3.5 issues during apt-get update/upgradeopenmediavault omv3升级omv4)
打开文件:/usr/lib/python3.5/weakref.py,
109行由def remove(wr, selfref=ref(self)):改为:

def remove(wr, selfref=ref(self), _atomic_removal=_remove_dead_weakref):

117行由_remove_dead_weakref(d, wr.key)改为:

_atomic_removal(d, wr.key)
  1. apt-get update出现:Certificate verification failed: The certificate is NOT trusted. The certificate chain uses not yet valid certificate. Could not handshake: Error in the certificate verification.

出现这个问题可能是服务器本地时间不正常,导致证书验证错误,此时可以看下服务器时间:

root@aml:~# date
# 当前操作时间是:2021-03-22 09:58
Tue 26 Nov 2019 08:47:23 AM UTC

# 重新修改服务器时间
# 设置时区
timedatectl set-timezone "Asia/Shanghai"
# 设置时间
date -s "2021-03-22 09:58:00"

完成后,就可以正常执行:

apt-get update
apt-get upgrade

3、安装常用软件

  • 安装apt-get的扩展软件aptitude

    apt-get update aptitude
  • 用于替换nanovi的编辑vim

    aptitude install vim

4、设置区域、时区并同步时间

# 设置区域
dpkg-reconfigure locales
# 设置时区
dpkg-reconfigure tzdata

如果可以的话,与ntp服务器同步下时间:

apt-get install ntpdate
ntpdate ntp1.aliyun.com

5、设置bash环境变量

vim ~/.bashrc

设置内容,然后使设置生效source /root/.bashrc

6、设置vim环境变量

  • 创建vim环境变量文件touch ~/.vimrc
  • 设置环境变量内容:

    syntax on
    set fencs=utf-8,gbk
    set shiftwidth=4
    set softtabstop=4
    set tabstop=4
    set number

7、添加用户

# 添加用户
useradd -d /home/{username} -m -s /bin/bash -U {username}
# 设置新加用户密码
passwd {username}

这样用户就添加成功了,但是可能由于ssh的配置文件sshd_config限制了指定组才能通过ssh登录,比如:AllowGroups root ssh,限定只有用户属于组rootssh的用户才能登录。将我们新加的用户添加都允许登录的组:

usermod -a -G ssh {username}

8、设置ssh配置信息

ssh默认端口22,安全起见强烈建议更改为其他端口号并限制root账号直接通过ssh登录。

vim /etc/ssh/sshd_config
# 更改端口
Port xxxxx
# 禁用root账号直接登录
PermitRootLogin no
# 仅允许root和ssh组使用
AllowGroups root ssh
# 重启ssh服务
/etc/init.d/ssh restart

9、添加swap交换文件(可选)

  • 添加swap文件:(设置512M:1024 512MB = 524288;设置1G:1024 1024 = 1048576;设置2G:1024 1024 2 = 2097152)

    dd if=/dev/zero of=/swapfile bs=1024 count=1048576
  • 设置swap文件用户即权限

    chown root:root swapfile
    chmod 777 swapfile
  • 将文件转为交换文件并激活

    mkswap /swapfile
    swapon /swapfile
  • 自动挂载交换分区文件

    vim /etc/fstab
    # 新起一行添加
    /swapfile swap swap defaults 0 0
  • swap交换文件优先等级

    # 查看你的系统里面的swappiness (默认是:60)
    cat /proc/sys/vm/swappiness
    
    # 临时修改swappiness值
    sysctl vm.swappiness=90
    
    # 永久更改swappiness(如果配置文件没有,可以在配置文件最后追加)
    vim /etc/sysctl.conf
    vm.swappiness = 90
    
    # 使设置生效
    sysctl -p

10.安装omv-extras

参考:omv-extras Guides
支持deb安装和命令行安装,这里用命令行:

wget -O - http://omv-extras.org/install | bash

二、安装软件

1、安装MySQL 5.6

参考:MySQL :: A Quick Guide to Using the MySQL APT RepositoryHow To Install MySQL on Debian 9 (Stretch)

  • 添加MySQLAPT 仓库

    cd /tmp
    wget https://repo.mysql.com//mysql-apt-config_0.8.13-1_all.deb
    dpkg -i mysql-apt-config_0.8.13-1_all.deb
  • 安装MySQL

    apt-get update
    aptitude install mysql-server
  • MySQL Secure Installation

    # 重启MySQL服务
    systemctl restart mysql
    
    # 调用
    mysql_secure_installation

在项目开发时,我们用的是本地搭建的开发dev环境,开发完成打包部署到服务器时,用到的是服务器prod环境。可以借用Mavenprofilesfiltersresources,在运行或打包时指定选用的环境,实现不同环境自动使用各自环境的配置文件或配置信息。

  • profiles:定义环境变量的id;
  • filters:定义了变量配置文件的地址,其中地址中的环境变量就是上面profile中定义的值;
  • resources:定义哪些目录下的文件会被配置文件中定义的变量替换,另外可以指定目录下的文件打包到classes目录下。

定义环境变量profiles

一般环境变量分:dev开发环境、prod发布环境,当然也可以类比添加其他的环境标志。
此处详细可参看:maven profile动态选择配置文件maven profile切换正式环境和测试环境

    <profiles>
        <!-- 开发测试环境 -->
        <profile>
            <id>dev</id>
            <activation>
                <!-- 设置默认激活dev环境的配置 -->
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <profile.env>dev</profile.env>
            </properties>
        </profile>
        <!-- 产品发布环境 -->
        <profile>
            <id>prod</id>
            <properties>
                <profile.env>prod</profile.env>
            </properties>
        </profile>
    </profiles>

- 阅读剩余部分 -

MySQL的函数replace(str, search_str, replace_str),用于从str中查找匹配search_str并替换为replace_str
通过这个函数可以移除内容中的空格,比如:

update table_a set column_a = replace(column_a, ' ', '')

但是对于tab、回车换行等字符不能直接用文本,此时我们可以考虑使用ASCII码来处理:

  • 空格的ASCII码为char(21)
  • tab的ASCII码为char(9)
  • 换行符的ASCII码为char(10)

SQL替换语句为:

-- 移除空格
update table_a set column_a = replace(column_a, char(21), '');

-- 移除tab
update table_a set column_a = replace(column_a, char(9), '');

-- 移除替换换行符
update table_a set column_a = replace(column_a, char(10), '');

参考文章:
sql: 去除数据库表中tab、空格、回车符等特殊字符的解决方法 去除tab、空格、回车符等使用replace语句 按照ASCII码

阿里云ECS服务器默认都会安装阿里云盾的agent,对于轻量服务器,比如内存只有512M的,我们可以卸载阿里云盾来释放此部分占用的内存。
注意:除了安装云盾的agent外,还会安装云监控的agent。
下面的命令都要用root账号或者sudo来运行

卸载阿里云插件

首先运行如下命令检测是否安装了阿里云插件:

ps aux | grep aliyun-service

如果返回的grep命令本身以外的进程,则表示服务器安装了阿里云插件。
下载卸载aliyun-service的脚本:aliyunservice_uninstall.zip
运行下面的命令:

-- 下载脚本
-- wget http://update.aegis.aliyun.com/download/uninstall.sh
wget https://www.jyoryo.com/usr/uploads/2019/06/4036770658.zip
unzip 536308210.zip
chmod +x aliyunservice_uninstall.sh aliyunservice_quartz_uninstall.sh
./aliyunservice_uninstall.sh
./aliyunservice_quartz_uninstall.sh

清除云插件进程和文件:

pkill aliyun-service
rm -fr /etc/init.d/agentwatch /usr/sbin/aliyun-service
rm -rf /usr/local/aegis*

现在再运行ps aux | grep aliyun-service应该就只会返回grep本身这条结果了。

卸载云监控的插件

首先运行如下命令检测是否安装了云监控的插件:

ps aux | grep cloudmonitor

如果返回超过一条命令,则证明安装了云监控插件。请执行如下命令来卸载删除:

bash -c "/usr/local/cloudmonitor/wrapper/bin/cloudmonitor.sh remove
rm -rf /usr/local/cloudmonitor

frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp, udp 协议,为 http 和 https 应用协议提供了额外的能力。类似的还有:ngrok、lanproxy等(frp和ngrok都是用go实现,lanproxy是用java实现)。利用内网穿透,我们可以实现微信调试,将内网的应用对外展示等。
搭建分服务器端和客户端。

服务器端搭建

下载软件

下载软件:FRP Releases
支持多平台,需根据服务器系统和CPU架构选择下载。下载的包是同时包含服务器端和客户端软件的。下载速度可能会比较慢,请耐心等会。

## 存放下载文件目录
cd /data/source
## 下载
wget https://github.com/fatedier/frp/releases/download/v0.27.0/frp_0.27.0_linux_386.tar.gz
## 解压
tar zxf frp_0.27.0_linux_386.tar.gz
## 移至安装目录
mv frp_0.27.0_linux_386 /data/soft/frp

- 阅读剩余部分 -

背景:在业务中,经常需要在执行数据库操作后(事务提交完成),发送消息或事件来异步调用其他组件执行相应的业务操作。
比如:用户注册成功后,发送激活码或激活邮件,如果用户保存后就执行异步操作发送激活码或激活邮件,但是前面用户保存后发生异常,数据库进行回滚,用户实际没有注册成功,但用户收到激活码或激活邮件。此时,我们就迫切要求数据库事务完成后再执行异步操作。

@Autowired
private UserDao userDao;
@Autowired
private JmsProducer jmsProducer;

public User saveUser(User user) {
    // 保存用户
    userDao.save(user);

    // 发送激活码或激活邮件
    jmsProducer.sendEmail(user.getId());
}

// -------------------------------------
public void sendEmail(int userId) {
    /*
     * 获取待接收邮件的用户。(如果上面的保存用户方法还未提交事务,则实际数据还未插入到数据库中,此时会返回null)
    */
    User user = userDao.get(userId);
    // 可能抛NullPointException
    String email = user.getEmail();
    mailService.send(email);
}

解决方案

1、Spring 4.2之后,使用注解@TransactionalEventListener

/**
 * 业务Service
 */
@Service
@Transactional
public class FooService {
    @Autowired
    private  ApplicationEventPublisher applicationEventPublisher;

    public User saveUser(User user) {
        userDao.save(user);
        // 注册事件
        applicationEventPublisher.publishEvent(new SavedUserEvent(user.getId()));
    }
}

// -------------------------------------
/**
 * 保存用户事件
 */
public class SavedUserEvent {
    private int userId;
    
    public SavedUserEvent(int userId) {
        this.userId = userId;
    }
    
    // getter and setter
}

// ---------------------------------
/**
 * 事件侦听,处理对应事件
 */
@Component
public class FooEventListener() {
    @Autowired
    private UserDao userDao;
    @Autowired
    private MailService mailService;

    @TransactionalEventListener
    public sendEmail(SavedUserEvent savedUserEvent) {
        User user = userDao.get(userId);
        String email = user.getEmail();
        mailService.send(email);
    }
}

2.使用TransactionSynchronizationManager 和 TransactionSynchronizationAdapter

@Autowired
private UserDao userDao;
@Autowired
private JmsProducer jmsProducer;

public User saveUser(User user) {
    // 保存用户
    userDao.save(user);
    final int userId = user.getId();

    // 事务提交后调用
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit() {
            jmsProducer.sendEmail(userId);
        }
    });
}

注意:上面的代码将在事务提交后执行.如果在非事务context中将抛出java.lang.IllegalStateException: Transaction synchronization is not active。
改进后代码:

@Autowired
private UserDao userDao;
@Autowired
private JmsProducer jmsProducer;

public User saveUser(User user) {
    // 保存用户
    userDao.save(user);
    final int userId = user.getId();

    // 兼容无论是否有事务
    if(TransactionSynchronizationManager.isActualTransactionActive()) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                jmsProducer.sendEmail(userId);
            }
        });
    } else {
        jmsProducer.sendEmail(userId);
    }
}

3.在上面2的基础上扩展TransactionSynchronizationAdapter

@Component("afterCommitExecutor")
public class AfterCommitExecutor extends TransactionSynchronizationAdapter implements Executor {
    private static final ThreadLocal<List<Runnable>> RUNNABLES = new ThreadLocal<List<Runnable>>();
    private ThreadPoolExecutor threadPool;

    @PostConstruct
    public void init() {
        Logs.debug("初始化线程池。。。");
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        if (0 >= availableProcessors) {
            availableProcessors = 1;
        }
        int maxPoolSize = (availableProcessors > 5) ? availableProcessors * 2 : 5;
        Logs.debug("CPU Processors :%s MaxPoolSize:%s", availableProcessors, maxPoolSize);
        threadPool = new ThreadPoolExecutor(
            availableProcessors,
            maxPoolSize,
            60,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(maxPoolSize * 2),
            Executors.defaultThreadFactory(),
            new RejectedExecutionHandler() {
                @Override
                public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                    Logs.debug("Task:%s rejected", r.toString());
                    if (!executor.isShutdown()) {
                        executor.getQueue().poll();
                        executor.execute(r);
                    }
                }
            }
        );
    }

    @PreDestroy
    public void destroy() {
        Logs.debug("销毁线程池。。。");
        if (null != threadPool && !threadPool.isShutdown()) {
            threadPool.shutdown();
        }
    }

    @Override
    public void execute(@NotNull Runnable runnable) {
        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
            runnable.run();
            return;
        }
        List<Runnable> threadRunnables = RUNNABLES.get();
        if (threadRunnables == null) {
            threadRunnables = new ArrayList<Runnable>();
            RUNNABLES.set(threadRunnables);
            TransactionSynchronizationManager.registerSynchronization(this);
        }
        threadRunnables.add(runnable);
    }

    @Override
    public void afterCommit() {
        Logs.debug("事务提交完成处理 ... ");
        List<Runnable> threadRunnables = RUNNABLES.get();
        for (int i = 0; i < threadRunnables.size(); i++) {
            Runnable runnable = threadRunnables.get(i);
            try {
                threadPool.execute(runnable);
            } catch (RuntimeException e) {
                Logs.error("", e);
            }
        }
    }

    @Override
    public void afterCompletion(int status) {
        Logs.debug("事务处理完毕 .... ");
        RUNNABLES.remove();
    }
}

业务层使用:

@Autowired
private UserDao userDao;
@Autowired
private JmsProducer jmsProducer;
@Autowired
private AfterCommitExecutor afterCommitExecutor;

public User saveUser(User user) {
    // 保存用户
    userDao.save(user);
    final int userId = user.getId();

    // 使用AfterCommitExecutor
    afterCommitExecutor.execute(new Runnable() {
        @Override
        public void run() {
            jmsProducer.sendEmail(userId);
        }
    });
  
}

参考文章

Spring的TransactionEventListener
Spring Event 事件中的事务控制
Java Code Examples org.springframework.transaction.support.TransactionSynchronizationAdapter
Spring Events | Baeldung
Thinking about IT: Transaction synchronization callbacks in Spring Framework

散布于应用中的多处的 功能被称为横切关注点。
通过依赖注入(DI),可以对应用中的对象之间进行解耦;通过AOP,可以将关注点与它们所影响的对象之间进行解耦。比如场景:事务、安全、缓存等。
在面向切面编程时,我们可以在集中一个地方定义通用功能,可以通过声明的方式定义这个功能要以何种方式在何处应用,而不用修改受影响的类。

Spring只支持方法级别的连接点

- 阅读剩余部分 -

最近将之前关于Spring的书重新拿出来温故一番,并进行总结。

Bean容器

Spring的Bean容器其实不止一个,归结起来可以分为2类:

  • Bean工厂(由org.springframework.beans.factory.BeanFactory接口定义),是最简单的Bean容器,提供基本的DI支持;
  • 应用上下文(由org.springframework.context.ApplicationContext接口定义),基于BeanFactory构建,提供应用框架级别的服务。

常用应用上下文

  • AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文;
  • AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文;
  • ClassPathXmlApplicationContext:从类路径下的一个或多个Spring的XML配置文件中加载应用上下文;
  • FileSystemXmlApplicationContext:从文件系统下的一个或多个Spring的XML配置文件中加载应用上下文;
  • XmlWebApplicationContext:从Web应用下的一个或多个Spring的XML配置文件中加载应用上下文。

- 阅读剩余部分 -