Android四大组件:Activity(活动)Service(服务)Broadcast Receiver(广播接收器)Content Provider(内容提供器)

  • Activity(活动)
    Activity活动是所有Android应用的界面显示,凡是应用中能看到的内容,都是放在活动中。
  • Service(服务)
    Service服务无法看到,但是它会一直在后台默默地运行。
  • Broadcast Receiver(广播接收器)
    Broadcast Receiver广播接收器运行应用接收来自各处的广播消息,比如:电话、短信等。我们应用也可以自己定义广播消息对外广播发送。
  • Content Provider(内容提供器)
    Content Provider内容提供器为应用之间共享数据提供了可能。比如访问系统电话簿中的联系人,就需要通过内容提供器来实现。

本文是对Activity(活动)的总结。

活动的创建

  1. 创建活动Activity的类。例如:

    public class MainActivity extends AppCompatActivity {
       private static final String TAG = "MainActivity";
       
       @Override
       protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_main);
       }
    }
  2. 创建活动的布局(视图)

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
    
       <TextView
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:text="This is a activity!" />
    </LinearLayout>
  3. AndroidManifest.xml中进行注册。

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.jyoryo.app.android.study">
    
       <application
           android:allowBackup="true"
           android:icon="@mipmap/ic_launcher"
           android:label="@string/app_name"
           android:roundIcon="@mipmap/ic_launcher_round"
           android:supportsRtl="true"
           android:theme="@style/AppTheme">
           <activity android:name=".MainActivity">
               <intent-filter>
                   <action android:name="android.intent.action.MAIN" />
                   <category android:name="android.intent.category.LAUNCHER" />
               </intent-filter>
           </activity>
       </application>
    
    </manifest>

其中标签activity ,就是申明注册Activity。android:name,用来指定具体注册的活动。比如上面实例中.MainActivity,标识活动的类名com.jyoryo.app.android.study.MainActivity。由于标签manifest中已经通过package申明了包名,因此注册活动时,包名这一部分就可以省略了。

活动中常见用法

活动中使用Toast

Toast是Android中非常友好的一直提醒方式,类似Html中的alert,可以将一些提示信息展示给用户。Toast的使用也是非常简单:

Toast.makeText(MainActivity.this, "Notify Message", Toast.LENGTH_SHORT).show();

活动中使用Menu

  1. 创建menu资源文件。
    main/res/menu目录下创建menu资源文件。

    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:android="http://schemas.android.com/apk/res/android">
    
       <item android:id="@+id/item_add" android:title="Add" />
       <item android:id="@+id/item_modify" android:title="Modify" />
       <item android:id="@+id/item_remove" android:title="Remove" />
    </menu>
  2. 在活动中重写onCreateOptionsMenu(Menu menu)方法,引入具体的Menu。

       @Override
       public boolean onCreateOptionsMenu(Menu menu) {
           getMenuInflater().inflate(R.menu.main, menu);
           return true;
       }

    如果方法返回true,表示允许显示创建的菜单;返回false,则表示创建的菜单不显示。

  3. 在活动中重写onOptionsItemSelected(MenuItem item)方法来定义菜单的相应事件。

       @Override
       public boolean onOptionsItemSelected(MenuItem item) {
           switch (item.getItemId()) {
               case R.id.add_item:
                   Toast.makeText(this, "Click Item Add", Toast.LENGTH_SHORT).show();
                   break;
               case R.id.remove_item:
                   Toast.makeText(this, "Click Item Remove", Toast.LENGTH_SHORT).show();
                   break;
               default:break;
           }
           return true;
       }

活动之间的调用

显示Intent打开活动

    // 显示Intent
    Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
    startActivity(intent);

其中FirstActivity.this是当前活动,SecondActivity是即将进入的活动。

隐式Intent打开活动

通过隐式Intent启动活动,并不用直接声明要启动的活动,而是通过一系列actioncategory等信息,由系统分析这个隐式的Intent并找到合适的活动来启动。

  • AndroidManifest.xml中的<activity>标签配置<intent-filter>内容,来指定可以响应的信息。

    <activity android:name=".SecondActivity">
       <intent-filter>
           <action android:name="com.jyoryo.app.android.study.ACTION_START" />
    
           <category android:name="android.intent.category.DEFAULT" />
       </intent-filter>
    </activity>

    在上面定义的活动SecondActivity可以相应com.jyoryo.app.android.study.ACTION_START这个action,而category标签用来定义附加信息,精确指明当前活动可以响应可能带有categoryIntent。只有actioncategory中的内容同时匹配Intent中指定的actioncategory,这个活动才能响应这个Intent

通过隐式Intent启动其他程序的活动

通过隐式Intent,不仅能启动自己程序中的活动,而且可以启动其他程序的活动。
比如,调用系统浏览器:

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);

调用系统拨号:

Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);

活动间数据传递

向下一个活动传递数据

通过Intent中的putExtra方法,将想要传递的数据暂存在Intent中:

String data = "HelloWorld";
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("extra_data", data);
startActivity(intent);

下一个活动通过Intent获取上个活动传递的数据:

Intent intent = getIntent();
String data = intent.getStringExtra("extra_data");
Log.d("SecondActivity", "Activity Transfer Data from Intent. Data:" + data);

返回数据给上一个活动

返回数据给上一个活动,主要是通过:

  • 在上一个活动中在启动下一个活动时调用startActivityForResult方法,期望活动销毁时返回一个结果给上一个活动;
  • 然后通过在上一个活动中重写方法onActivityResult获取下一活动即将被销毁时返回给上一活动的数据。

期待启动的下一个活动在活动销毁时返回数据给上一个活动

// -----------------FirstActivity------------------
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);

下一活动销毁前返回数据:

// -----------------SecondActivity------------------
Intent intent = new Intent();
intent.putExtra("data_return", "Return FirstActivity");
setResult(RESULT_OK, intent);
// 销毁活动
finish();

上一活动取到返回的数据

    // -----------------FirstActivity------------------
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        // super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case 1:
                if(RESULT_OK == resultCode) {
                    String returnData = data.getStringExtra("data_return");
                    Log.d("FirstActivity", "onActivityResult---ReturnData:" + returnData);
                }
                break ;
                default:
        }
    }

如果活动SecondActivity并不是通过我们定义方法销毁,而是通过Back键回到FirstActivity,这样的话,数据没有办法进行返回。解决办法:在上一个活动SecondActivity重写方法onBackPressed

    // -----------------SecondActivity------------------
    @Override
    public void onBackPressed() {
        Intent intent = new Intent();
        intent.putExtra("data_return", "Return FirstActivity");
        setResult(RESULT_OK, intent);
        // 销毁活动
        finish();
    }

活动的生命周期

活动的生命周期:

  • onCreate():每个活动都会有的方法,在活动第一次被创建的时候调用;
  • onStart():这个活动在活动由不可见变为可见的时候调用;
  • onResume():这个方法在活动准备好和用户进行交互的时候调用,此时的活动一定位于返回栈的栈顶,并且处于运行状态;
  • onPause():这个方法在系统准备去启动或者恢复另一个活动的时候调用。 我们通常会在这个方法中将一些消耗 CPU 的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用;
  • onStop():这个方法在活动完全不可见的时候调用。它和 onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么 onPause()方法会得到执行,而 onStop()方法并不会执行;
  • onDestroy():这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态;
  • onRestart():这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。

以上七个方法中除了 onRestart()方法,其他都是两两相对的,从而又可以将活动分为三种生存期。

支持伪静态

为了使typecho支持伪静态,即访问文章时浏览器路径不显示index.php,我们可以在nginx配置文件中按照下面设置方法进行设置:

location / {
    index index.html index.php;
    if (-f $request_filename/index.html) {
        rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename/index.php) {
        rewrite (.*) $1/index.php;
    }
    if (!-f $request_filename) {
        rewrite (.*) /index.php;
    }
}

支持SSL

前提先准备好自己ssl证书。(可以通过Let's Encrypt申请免费的证书)。
nginx配置文件配置

server {
    listen 80;
    server_name www.domain.com;
    ## 301 跳转到https
    return 301 https://$host$request_uri;
}

server {
    server_name www.domain.com;
    root /var/www/project;
    index index.php index.html index.htm;
    
    location ~* \.(gif|jpg|png|jpeg|ico)$ {
        expires max;
    }

    ## 支持伪静态
    location / {
    index index.html index.php;
    if (-f $request_filename/index.html) {
        rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename/index.php) {
        rewrite (.*) $1/index.php;
    }
    if (!-f $request_filename) {
        rewrite (.*) /index.php;
    }
    }

    location ~ .*\.php(\/.*)*$ {
    # fastcgi_index   index.php;
    include snippets/fastcgi-php.conf;
        fastcgi_pass 127.0.0.1:9000;
    # fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        # include fastcgi_params;
    }

    listen 443 ssl;
    ssl_certificate cert/domain.com.cert;
    ssl_certificate_key cert/domain.com.key;
    include cert/options-ssl-nginx.conf;
    ssl_dhparam cert/ssl-dhparams.pem;
}

上面的options-ssl-nginx.conf内容为:

ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS";

通过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

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

更新源中包数据库

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)

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

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

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);
    }
}

使用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);
    }
}

参考文章

Spring的TransactionEventListener
Spring Event 事件中的事务控制
Java Code Examples org.springframework.transaction.support.TransactionSynchronizationAdapter
Spring Events | Baeldung

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

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

- 阅读剩余部分 -