宝塔—python管理器使用教程-长沙SEO霜天

mikel阅读(4800)

来源: 宝塔—python管理器使用教程-长沙SEO霜天

python管理器使用教程

1、请确认项目根目录下已经存在 requirements.txt 文件,用于安装依赖包。

2、选择gunicorn或uwsgi模式启动时会先在根目录创建默认配置,如果需要修改配置,请在创建完成后点击配置修改。

宝塔—python管理器使用教程

3、在版本管理可以安装你想使用的版本

宝塔—python管理器使用教程
4、每个项目都使用独立的虚拟环境,所以创建时需要稍等,虚拟环境的目录会在你项目的根目录下以项目名_venv的名字呈现
宝塔—python管理器使用教程
5、如果需要在现有项目添加模块请点击对应的模块按钮来安装
宝塔—python管理器使用教程
6、管理器gunicorn,uwsgi启动方式为

django

gunicorn -c 项目路径/gunicorn.conf hellodj.wsgi:application,其中hellodj为项目运行文件夹或文件

uwsgi -d –ini 项目路径/uwsgi.ini -w hellodj.wsgi.application

flask

gunicorn -c 项目路径/gunicorn.conf hellodj:app,其中hellodj为项目运行文件夹或文件

uwsgi -d –ini 项目路径/uwsgi.ini -w hellodj:app

宝塔—python管理器使用教程

宝塔—python管理器使用教程

4、如果需要使用域名或80端口访问项目,点击映射按钮即可,操作完成后会在网站处显示

宝塔—python管理器使用教程

宝塔—python管理器使用教程

原创文章,作者:霜天,如若转载,请注明出处:https://www.i5SEO.com/python-manager-use-tutorial.html

阿里云ECS建立微信公众号----以itchatmp为例【python】 - 简书

mikel阅读(1138)

来源: 阿里云ECS建立微信公众号—-以itchatmp为例【python】 – 简书

阿里云ECS建立微信公众号—-以itchatmp为例#

1、由于近期发现itchat开源项目要升级到itchatmp , 所以考虑下把自己的阿里云ECS利用起来,不然有点浪费了。

2、首先熟悉下flask的hello world实现,把这段代码放到ECS上:

#encoding:utf8
from flask import Flask, render_template

app = Flask(__name__)
@app.route('/')
def hello_world():
    content = 'Hello World!'
    return render_template('hello.html', content = content)

if __name__ == '__main__':
    app.run('0.0.0.0')

其中:@app.route(‘/’) 是定义的路由,表示对应网址的根目录。
然后在本机输入ECS的ip加上端口5000,应该就能看到hello world了。

3、

flask的 中文文档资料: http://dormousehole.readthedocs.io/en/latest/

flask的基础视频参考: http://www.jikexueyuan.com/course/943.html

4、 以itchatmp为例:

首先根据itchatmp的github网址: https://github.com/littlecodersh/itchatmp

下载到本地,然后

pip install itchatmp

在这个过程中,很可能安装失败,是因为墙的因素,建议开启翻墙工具

在安装好之后,把对应的示范脚本,根据你的微信订阅号的相关参数进行修改:

import itchatmp

itchatmp.update_config(itchatmp.WechatConfig(
    token='xxxxxxxxxx',
    appId = 'xxxxxxxxxx',
    appSecret = 'xxxxxxxxxx'))

@itchatmp.msg_register(itchatmp.content.TEXT)
def text_reply(msg):
    return msg['content']

itchatmp.run()

5、然后到微信订阅号的后台,把url部分,修改成你的ECS的ip。

6、运行一个形如weixinchat.py的脚本,把上述代码复制,并运行。一切顺利的话,你的订阅号就工作了,此时,他的默认功能是,当你输入信息后,订阅号自动回复相同内容。

7、本文案例是在Win10,Win NT 2008下都测试通过,

8、如果你需要一些Linux相关的参考资料,如下:

作者:vansnowpea
链接:https://www.jianshu.com/p/6f386659f5aa
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

基于itchat实现微信群消息同步机器人 - 简书

mikel阅读(998)

来源: 基于itchat实现微信群消息同步机器人 – 简书

最近 全栈数据工程师养成攻略 的微信群已经将近500人,开了二群之后为了打通不同微信群之间的消息,花了点时间做了个消息同步机器人,在任意群收到消息时同步到其他群,并且将聊天内容上传至数据库,以供进一步分析、统计和展示。

基本思路是,用 Python 模拟微信网页版登陆,接收到群里消息后,对文本、图片、分享等各类消息类型分别处理,并转发至其他群。

前期准备

首先得有一个微信号,用于代码模拟登陆。由于我的微信号得自己留着用,现阶段注册微信又必须要手机号,于是只好特意办了个电信号,用来申请了一个新的微信,微信号是 honlanbot。虽说似乎可以用阿里小号来注册微信,不过听说存在反复回收和安全隐患问题,故不采用。

其次,需要用到一个Python库 itchat,这个库已经做好了用代码调用微信的大多数功能,非常好用,官方文档在这里,安装的时候使用 pip 即可。

pip install itchat

我的手机支持双卡双待,于是把两张卡都装手机里,再双开微信,同时保持两个微信号手机在线,差不多就可以开始写代码了。用 itchat 调用微信主要是模拟微信网页版登陆,所以必须保持微信号手机在线,因为手机端微信一旦退出,其在网页、PC、MAC、IPAD等相应终端认证的账号也会随之退出。

初步尝试

itchat 提供了一些官方代码,让我们在自己的本本或电脑上新建一个 py 文件,初步尝试一下。

运行以下代码,会出现出现一张二维码,扫码登陆之后将会给“文件传输助手”发送一条消息。

# 加载包
import itchat
# 登陆
itchat.auto_login()
# 发送文本消息,发送目标是“文件传输助手”
itchat.send('Hello, filehelper', toUserName='filehelper')

以下代码则注册了一个消息响应事件,用来定义接收到文本消息后如何处理。在 itchat 里可以定义文本、图片、名片、位置、通知、分享、文件等多种消息类型,从而分别执行不同的处理。

import itchat
# 注册消息响应事件,消息类型为itchat.content.TEXT,即文本消息
@itchat.msg_register(itchat.content.TEXT)
def text_reply(msg):
    # 返回同样的文本消息
    return msg['Text']

itchat.auto_login()
# 绑定消息响应事件后,让itchat运行起来,监听消息
itchat.run()

再来看看如何处理其他类型消息,可以把在消息响应事件里把 msg 打印出来,是一个字典,看看有哪些感兴趣的字段。

import itchat
# import全部消息类型
from itchat.content import *

# 处理文本类消息
# 包括文本、位置、名片、通知、分享
@itchat.msg_register([TEXT, MAP, CARD, NOTE, SHARING])
def text_reply(msg):
    # 微信里,每个用户和群聊,都使用很长的ID来区分
    # msg['FromUserName']就是发送者的ID
    # 将消息的类型和文本内容返回给发送者
    itchat.send('%s: %s' % (msg['Type'], msg['Text']), msg['FromUserName'])

# 处理多媒体类消息
# 包括图片、录音、文件、视频
@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO])
def download_files(msg):
    # msg['Text']是一个文件下载函数
    # 传入文件名,将文件下载下来
    msg['Text'](msg['FileName'])
    # 把下载好的文件再发回给发送者
    return '@%s@%s' % ({'Picture': 'img', 'Video': 'vid'}.get(msg['Type'], 'fil'), msg['FileName'])

# 处理好友添加请求
@itchat.msg_register(FRIENDS)
def add_friend(msg):
    # 该操作会自动将新好友的消息录入,不需要重载通讯录
    itchat.add_friend(**msg['Text']) 
    # 加完好友后,给好友打个招呼
    itchat.send_msg('Nice to meet you!', msg['RecommendInfo']['UserName'])

# 处理群聊消息
@itchat.msg_register(TEXT, isGroupChat=True)
def text_reply(msg):
    if msg['isAt']:
        itchat.send(u'@%s\u2005I received: %s' % (msg['ActualNickName'], msg['Content']), msg['FromUserName'])

# 在auto_login()里面提供一个True,即hotReload=True
# 即可保留登陆状态
# 即使程序关闭,一定时间内重新开启也可以不用重新扫码
itchat.auto_login(True)
itchat.run()

开发消息同步机器人

经过以上示例代码,可以总结出消息同步机器人的开发思路:

  • 登陆后使用get_chatrooms()获取全部群聊的数据,包括每个群聊的ID和昵称,可以将需要同步消息的群聊保存至通讯录;
  • 接收到群聊消息时,如果消息来自于需要同步消息的群聊,就根据消息类型进行处理,同时转发到其他需要同步的群聊。

直接上代码好了,首先定义一个消息响应函数,文本类消息我感兴趣的是 TEXTSHARING 两类,使用 isGroupChat=True 指定消息来自于群聊,这个参数默认为 False

# 自动回复文本等类别的群聊消息
# isGroupChat=True表示为群聊消息
@itchat.msg_register([TEXT, SHARING], isGroupChat=True)
def group_reply_text(msg):
    # 消息来自于哪个群聊
    chatroom_id = msg['FromUserName']
    # 发送者的昵称
    username = msg['ActualNickName']

    # 消息并不是来自于需要同步的群
    if not chatroom_id in chatroom_ids:
        return

    if msg['Type'] == TEXT:
        content = msg['Content']
    elif msg['Type'] == SHARING:
        content = msg['Text']

    # 根据消息类型转发至其他群
    if msg['Type'] == TEXT:
        for item in chatrooms:
            if not item['UserName'] == chatroom_id:
                itchat.send('%s\n%s' % (username, msg['Content']), item['UserName'])
    elif msg['Type'] == SHARING:
        for item in chatrooms:
            if not item['UserName'] == chatroom_id:
                itchat.send('%s\n%s\n%s' % (username, msg['Text'], msg['Url']), item['UserName'])

再来处理下图片等多媒体类消息。

# 自动回复图片等类别的群聊消息
# isGroupChat=True表示为群聊消息          
@itchat.msg_register([PICTURE, ATTACHMENT, VIDEO], isGroupChat=True)
def group_reply_media(msg):
    # 消息来自于哪个群聊
    chatroom_id = msg['FromUserName']
    # 发送者的昵称
    username = msg['ActualNickName']

    # 消息并不是来自于需要同步的群
    if not chatroom_id in chatroom_ids:
        return

    # 如果为gif图片则不转发
    if msg['FileName'][-4:] == '.gif':
        return

    # 下载图片等文件
    msg['Text'](msg['FileName'])
    # 转发至其他需要同步消息的群聊
    for item in chatrooms:
        if not item['UserName'] == chatroom_id:
            itchat.send('@%s@%s' % ({'Picture': 'img', 'Video': 'vid'}.get(msg['Type'], 'fil'), msg['FileName']), item['UserName'])

以上代码实现了对文本、分享、图片、视频四类消息的处理,如果对其他类型的消息也感兴趣,进行相应的处理即可。在前面补上 import 的代码,在后面补上登陆、获取群聊数据和开始监测的代码,就大功告成了。

完整代码在这里:https://pan.baidu.com/s/1bpAJk0B

成果展示

目前两个群之间可以进行消息同步了,一群和二群的小伙伴终于可以畅快地聊了起来(当群主不容易,经常要发很多红包 = =)。

进一步工作

当然,我不可能一直在笔记本上运行这么个 py 代码,所以把它部署到服务器上运行就好了,开个 screen 或者用 IPython 都可以。如果账号偶尔下线了,再运行一下就好。

另外,我还写了个 API,响应消息的时候会把相应的数据 POST 到我的服务器并存到数据库,以供进一步的分析、统计和展示,这也是我身为一个群主应尽的职责~

作者:宏伦工作室
链接:https://www.jianshu.com/p/7aeadca0c9bd#
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

微信群信息采集_大数据_w_t_y_y的博客-CSDN博客

mikel阅读(1566)

来源: 微信群信息采集_大数据_w_t_y_y的博客-CSDN博客

一、数据库:

1、微信账号表

CREATE TABLE `t_wechat_robot` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT ‘自增长id’,
`robot_id` varchar(255) DEFAULT NULL COMMENT ‘机器Id’,
`robot_name` varchar(255) DEFAULT NULL COMMENT ‘机器名称’,
`robot_nick_name` varchar(255) DEFAULT NULL COMMENT ‘机器微信昵称’,
`status` tinyint(1) DEFAULT NULL COMMENT ‘状态,0在线,1离线’,
`is_delete` tinyint(1) DEFAULT NULL COMMENT ‘删除标志,1 删除,0未删除’,
`create_time` timestamp NULL DEFAULT NULL COMMENT ‘创建时间’,
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新时间’,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT=’微信群聊机器表’;
并手动新增一条我的微信号记录:

INSERT INTO `t_wechat_robot` VALUES (‘1’, ‘1’, ‘机器1’, MYWXNICKNAME, ‘0’, ‘0’, null, ‘2019-04-24 14:57:00’);
2、微信群表

CREATE TABLE `t_wechat_group` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT ‘自增长id’,
`robot_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘机器id’,
`group_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘群Id’,
`group_user_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘群名’,
`group_nick_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`head_img_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘群头像’,
`member_count` int(11) DEFAULT ‘0’ COMMENT ‘群成员数’,
`owner_nick_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘群主’,
`chat_room_id` int(11) DEFAULT NULL COMMENT ‘聊天室id’,
`create_time` timestamp NULL DEFAULT NULL COMMENT ‘创建时间’,
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新时间’,
`is_delete` tinyint(1) DEFAULT NULL COMMENT ‘删除标志’,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=861 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT=’微信群’;
3、微信群成员表

CREATE TABLE `t_wechat_group_member` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT ‘自增长id’,
`group_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘群id’,
`user_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘用户名’,
`nick_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mem_status` int(11) DEFAULT NULL COMMENT ‘状态’,
`uin` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT ‘uin’,
`create_time` timestamp NULL DEFAULT NULL COMMENT ‘创建时间’,
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新时间’,
`is_delete` tinyint(4) DEFAULT NULL COMMENT ‘删除标志,1 删除,0未删除’,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
mongo群消息表:

项目:

 

二、api

此处省略部分get和set方法:

1、PageParam:

package com.wtyy.common;
import java.io.Serializable;
/**
* 传统的分页参数
*/
public class PageParam implements Serializable {

private static final long serialVersionUID = -5552159160388121108L;
private static final int MIN_PAGE_SIZE = 1;
private static final int MAX_PAGE_SIZE = 1000;
private static final int DEFAULT_PAGE_SIZE = 10;
/**
* 当前页码,从1开始。
*/
private int pageIndex;

/**
* 每页记录数。
*/
private int pageSize = DEFAULT_PAGE_SIZE;

public PageParam(int pageIndex, int pageSize) {
setPageIndex(pageIndex);
setPageSize(pageSize);
}

/**
* 得到开始记录index。
*/
public int getStartIndex() {
return (pageIndex – 1) * pageSize;
}

public int getPageIndex() {
return pageIndex;
}

public void setPageIndex(int pageIndex) {
if (pageIndex < 1){
throw new RuntimeException(“page index should > 0”);
}
this.pageIndex = pageIndex;
}

public int getPageSize() {
return pageSize;
}

public void setPageSize(int pageSize) {
if (pageSize >= MIN_PAGE_SIZE && pageSize <= MAX_PAGE_SIZE){
this.pageSize = pageSize;
}else {
throw new RuntimeException(String.format(“page size should be ranged in [%s, %s]”,MIN_PAGE_SIZE,MAX_PAGE_SIZE));
}
}

}
2、Pager:

 

package com.wtyy.common;
import java.io.Serializable;
import java.util.List;

/**
* 传统分页
*/
public class Pager<T> implements Serializable {

private static final long serialVersionUID = -9134108412928477507L;

/**
* 总记录数。
*/
private int totalCount;

/**
* 当前页的记录列表。
*/
private List<T> list;

public Pager(int totalCount, List<T> list){
this.totalCount = totalCount;
this.list = list;
}
}
3、WechatMsgType微信群消息类型(整型):

package com.wtyy.constant;

/**
* @Description: 微信群消息类型
*/
public enum WechatMsgType {

TEXT(“文本”,1),

PICTURE(“图片”,3),

RECORDING(“语音”,34),

Video(“视频”,43);

/**
* @param typeName
* @param typeCode
*/
private WechatMsgType(String typeName, Integer typeCode) {
this.typeName = typeName;
this.typeCode = typeCode;
}

private String typeName;

private Integer typeCode;

public String getTypeName() {
return typeName;
}

public void setTypeName(String typeName) {
this.typeName = typeName;
}

public Integer getTypeCode() {
return typeCode;
}

public void setTypeCode(Integer typeCode) {
this.typeCode = typeCode;
}

}
4、WechatType微信群消息类型(字符型):

package com.wtyy.constant;

/**
* 微信群消息类型
*/
public enum WechatType {
//文本消息
Text,

//图片消息
Picture,

//语音消息
Recording,

//视频消息
Video
}
5、WechatRobot登录机器:

package com.wtyy.module;

import java.io.Serializable;
import java.util.Date;

public class WechatRobot implements Serializable{
/**
*
*/
private static final long serialVersionUID = -8066995470069020705L;

private Integer id;

private String robotId;

private String robotName;

private String robotNickName;

/**
* 状态,0在线,1离线
*/
private Short status;

private Short isDelete;

private Date createTime;

private Date updateTime;

}
6、WechatGroup微信群:

package com.wtyy.module;

import java.io.Serializable;
import java.util.Date;

public class WechatGroup implements Serializable{
/**
*
*/
private static final long serialVersionUID = -7954472287669464909L;

private Integer id;

private String robotId;

private String groupId;

private String groupUserName;

private String groupNickName;

private String headImgUrl;

private Integer memberCount;

private String ownerNickName;

private Integer chatRoomId;

private Date createTime;

private Date updateTime;

private Short isDelete;

}
7、WechatGroupMember微信群成员:

package com.wtyy.module;

import java.io.Serializable;
import java.util.Date;

public class WechatGroupMember implements Serializable{
/**
*
*/
private static final long serialVersionUID = 3937137164822326424L;

private Integer id;

private String groupId;

private String userName;

private String nickName;

private Integer memStatus;

private Long uin;

private Date createTime;

private Date updateTime;

private Short isDelete;

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((nickName == null) ? 0 : nickName.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
WechatGroupMember other = (WechatGroupMember) obj;
if (nickName == null) {
if (other.nickName != null)
return false;
} else if (!nickName.equals(other.nickName))
return false;
return true;
}

}
8、WechatGroupMsg微信群消息:

package com.wtyy.module;

import java.io.Serializable;
import java.util.Date;
public class WechatGroupMsg implements Serializable{

private static final long serialVersionUID = -8707880890289759728L;

private String robotId;

private String groupId;

private String msgId;

private String fromUserName;

private String toUserName;

/**消息类型
* @see
* com.iflytek.edu.zx.table.admin.constant.WechatMsgType
*/
private Integer msgType;

/**
* 消息类型
* @see
* com.iflytek.edu.zx.table.admin.constant.WechatType
*/
private String type;

/**
* 文件路径
*/
private String url;

/**
* 消息内容
*/
private String content;

/**
* 消息内容
*/
private String text;

/**
* 文件消息 文件名称
*/
private String fileName;

private Integer status;

private Integer imgStatus;

private Date createTime;

/**
* 发送者昵称
*/
private String actualNickName;

private String actualUserName;

private Integer statusNotifyCode;

private String statusNotifyUserName;

RecommendInfo recommendInfo;

}
9、RecommendInfo:

package com.wtyy.module;
import java.io.Serializable;

public class RecommendInfo implements Serializable{
private static final long serialVersionUID = 6421951838838130816L;

private String userName;

private String nickName;

private String qQNum;

private String content;
}
10、WechatService微信群信息服务:

package com.wtyy.service;
import java.util.List;
import com.wtyy.common.PageParam;
import com.wtyy.common.Pager;
import com.wtyy.module.WechatGroup;
import com.wtyy.module.WechatGroupMember;
import com.wtyy.module.WechatRobot;

public interface WechatService {

/**
* 新增机器
*/
public void addWechatRobot(WechatRobot wechatRobot);

/**
* 修改机器
*/
public void updateWechatRobot(WechatRobot wechatRobot);

/**
* 删除机器
*/
public void deleteWechatRobot(Integer id);

/**
* 根据昵称获取robotId
*/
public String getRobotIdByNickName(String nickName);

/**
* 分页获取机器
*/
public Pager<WechatRobot> getPagerWechatRobot(PageParam pageParam);

/**
* 添加群
*/
public void addWechatGroup(WechatGroup wechatGroup);

/**
* 根据群昵称获取groupId
*/
public String getGroupIdByGroupNickName(String groupNickName);

/**
* 修改群
*/
public void updateWechatGroup(WechatGroup wechatGroup);

/**
* 分页获取群
*/
public Pager<WechatGroup> getPagerWechatGroup(PageParam pageParam,
String robotId);

/**
* 添加群成员
*/
public void batchInsertWechatGroupMember(List<WechatGroupMember> groupMembers,
String groupId);

/**
* 获取群成员列表,为null返回空集合[]
*/
public List<WechatGroupMember> getMembersByGroupId(String groupId);

/**
* 批量删除群成员
*/
public void batchDeleteWechatGroupMember(List<Integer> ids, String groupId);

/**
* 更新群成员数
*/
public void updateWechatGroupSize(String groupId, int size);

}
11、WechatGroupMsgService微信群消息服务:

package com.wtyy.service;
import com.wtyy.common.PageParam;
import com.wtyy.common.Pager;
import com.wtyy.module.WechatGroupMsg;

public interface WechatGroupMsgService {
/**
* 添加群消息
*/
public void addGroupMsg(WechatGroupMsg wechatGroupMsg);

/**
* 分页获取群消息
*/
public Pager<WechatGroupMsg> getPagerWechatGroupMsg(PageParam pageParam,
String robotId,String groupId);
}
三、service:

配置文件:

spring.datasource.driver-class-name=com.mySQL.jdbc.Driver
spring.datasource.url=jdbc:mySQL://localhost:3307/itchat?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=wtyy

mybatis.mapper-locations=classpath*:Mapper/*Mapper.xml
server.port=8081

1、Dao:

看下ConfigDAO:这是因为存在特殊符号和一些表情,utf-8存不了(会报错\xF0\x9F\x91\x8D05…),需要utf8mb4。具体可见我的博客 mybatis分类下的异常处理。

package com.wtyy.dao;

import org.apache.ibatis.annotations.Update;

public interface ConfigDAO {

@Update(“set names utf8mb4”)
public void setCharsetToUtf8mb4();
}
其他的dao都是对mySQL的增删改查,就不粘了。

2、impl:

1、看下mongo的dao,其他的都是对mysql表的增删改查,不粘了。

package com.wtyy.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.wtyy.module.WechatGroupMsg;
import com.wtyy.util.MongodbManager;

/**
* @Description: mongo存储微信群消息
*/
@Repository(“wechatGroupMsgDao”)
public class WechatGroupMsgDao {

///collection名
private String wechatMsgMongoCollection =”wechatmsg”;

public void addGroupMsg(WechatGroupMsg wechatGroupMsg) {
DBCollection dbCollection = MongodbManager.DB_TL.getCollection(wechatMsgMongoCollection);
DBObject documents = new BasicDBObject();
documents.put(“robot_id”, wechatGroupMsg.getRobotId());
documents.put(“group_id”, wechatGroupMsg.getGroupId());
documents.put(“msg_id”, wechatGroupMsg.getMsgId());
documents.put(“from_user_name”, wechatGroupMsg.getFromUserName());
documents.put(“to_user_name”, wechatGroupMsg.getToUserName());
documents.put(“msg_type”, wechatGroupMsg.getMsgType());
documents.put(“type”, wechatGroupMsg.getType());
documents.put(“content”, wechatGroupMsg.getContent());
documents.put(“text”, wechatGroupMsg.getText());
documents.put(“status”, wechatGroupMsg.getStatus());
documents.put(“img_status”, wechatGroupMsg.getImgStatus());
documents.put(“create_time”, wechatGroupMsg.getCreateTime());
documents.put(“file_name”, wechatGroupMsg.getFileName());
documents.put(“actual_nick_name”, wechatGroupMsg.getActualNickName());
documents.put(“actual_user_name”, wechatGroupMsg.getActualUserName());
documents.put(“status_notify_code”, wechatGroupMsg.getStatusNotifyCode());
documents.put(“status_notify_user_name”, wechatGroupMsg.getStatusNotifyUserName());
documents.put(“url”, wechatGroupMsg.getUrl());
dbCollection.insert(documents );
}

}
3、util:

package com.wtyy.util;

import java.net.UnknownHostException;
import java.util.ArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.mongodb.DB;
import com.mongodb.MongoClient;
import com.mongodb.ReadPreference;
import com.mongodb.ServerAddress;
import com.mongodb.WriteConcern;

/**
* mongodb管理器,提供DB对象。
*/
public class MongodbManager {

private static final Logger LOGGER = LoggerFactory.getLogger(“mongodbManager”);

private static MongoClient client;

//数据库名
private static String wechatMsgMongoDB = “wechatMsg”;

static {
String adressList = “localhost:27017”;

try {

String[] addresses = adressList.split(“,”);
ArrayList<ServerAddress> serverAddressList = new ArrayList<ServerAddress>();
for(int j=0; j < addresses.length; j++){
String[] address = addresses[j].split(“:”);
ServerAddress mongoAddress = new ServerAddress(address[0], Integer.parseInt(address[1]));
serverAddressList.add(mongoAddress);
}
client = new MongoClient(serverAddressList);
client.setReadPreference(ReadPreference.primaryPreferred());
LOGGER.info(“读写分离未启动”);

client.setWriteConcern(WriteConcern.SAFE);
LOGGER.info(“写入安全模式启动”);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}

public static final DB DB_TL = client.getDB(wechatMsgMongoDB);
}
4、启动类:

package com.wtyy;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

import com.alibaba.dubbo.config.spring.context.annotation.EnableDubboConfig;
import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;

@SpringBootApplication
@MapperScan(“com.wtyy.dao”)
@ImportResource(“classpath:provider.xml”)
public class ServiceStart {
public static void main(String[] args) {
SpringApplication.run(ServiceStart.class, args);
}

}
四、rest:

1、配置文件:

application.properties:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3307/itchat?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=wtyy

server.port=8082

spring.redis.host=localhost
spring.redis.port=6379
#spring.redis.password=
spring.redis.database=1
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=500
spring.redis.pool.min-idle=0
spring.redis.timeout=0

#kafka
# 指定kafka 代理地址,可以多个
spring.kafka.bootstrap-servers=localhost:9092
# 指定默认消费者group id
spring.kafka.consumer.group-id=myGroup
# 指定默认topic id
spring.kafka.template.default-topic= my-replicated-topic
# 指定listener 容器中的线程数,用于提高并发量
spring.kafka.listener.concurrency= 3
# 每次批量发送消息的数量
spring.kafka.producer.batch-size= 1000
#key-value序列化反序列化
#spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
#spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
#spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
#spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.buffer-memory=524288
#群消息
wechat.msg = wechatmsg
#群列表
wechat.group = wechatgroup
pom:

<project xmlns=”http://maven.apache.org/POM/4.0.0″ xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd”>
<modelVersion>4.0.0</modelVersion>
<groupId>com.wtyy</groupId>
<artifactId>itchat-rest</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath /> <!– lookup parent from repository –>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>
<!– api –>
<dependency>
<groupId>com.wtyy</groupId>
<artifactId>itchat-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

<!– mybatis –>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>

<!– dubbo –>
<dependency>
<groupId>com.alibaba.spring.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>

<!– redis –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>

</project>
2、dto:是微信消息的原格式,这里封装成对象,后面可以直接用json转成dto,由于转成的dto首字母全部是大写,所以存储的时候又按照阿里的开发规范转换了一次。至于dto的封装,看消息的原格式就很清晰了:

{
“MsgId”: “5689968435146263788”,
“FromUserName”: “@863e84f7210f6ca0da17b018aedf81bcf8a173fee1d811d2769f101c4267e020”,
“ToUserName”: “@@c25f17fed47b6946321965da58fab144f674975dafd780a9e2e173de307e60b6”,
“MsgType”: 1,
“Content”: “我是消息”,
“Status”: 3,
“ImgStatus”: 1,
“CreateTime”: 1555677369,
“VoiceLength”: 0,
“PlayLength”: 0,
“FileName”: “”,
“FileSize”: “”,
“MediaId”: “”,
“Url”: “”,
“AppMsgType”: 0,
“StatusNotifyCode”: 0,
“StatusNotifyUserName”: “”,
“RecommendInfo”: {
“UserName”: “”,
“NickName”: “”,
“QQNum”: 0,
“Province”: “”,
“City”: “”,
“Content”: “”,
“Signature”: “”,
“Alias”: “”,
“Scene”: 0,
“VerifyFlag”: 0,
“AttrStatus”: 0,
“Sex”: 0,
“Ticket”: “”,
“OpCode”: 0
},
“ForwardFlag”: 0,
“AppInfo”: {
“AppID”: “”,
“Type”: 0
},
“HasProductId”: 0,
“Ticket”: “”,
“ImgHeight”: 0,
“ImgWidth”: 0,
“SubMsgType”: 0,
“NewMsgId”: 5689968435146263788,
“OriContent”: “”,
“EncryFileName”: “”,
“ActualNickName”: “发送人”,
“IsAt”: false,
“ActualUserName”: “@863e84f7210f6ca0da17b018aedf81bcf8a173fee1d811d2769f101c4267e020”,
“User”: {
“MemberList”: [
{
“MemberList”: [],
“Uin”: 0,
“UserName”: “@45161ba956dbd3d622499558e649c4beddee71fcd9277b4c8274c10f61295821”,
“NickName”: “成员1”,
“AttrStatus”: 102501,
“PYInitial”: “”,
“PYQuanPin”: “”,
“RemarkPYInitial”: “”,
“RemarkPYQuanPin”: “”,
“MemberStatus”: 0,
“DisplayName”: “”,
“KeyWord”: “”
}, {
“MemberList”: [],
“Uin”: 0,
“UserName”: “@d24ebe966bca4fbb6f08a886d3f2960b1cdfb5fa65113f7081e40b1b1bebbfcb”,
“NickName”: “成员2”,
“AttrStatus”: 233509,
“PYInitial”: “”,
“PYQuanPin”: “”,
“RemarkPYInitial”: “”,
“RemarkPYQuanPin”: “”,
“MemberStatus”: 0,
“DisplayName”: “”,
“KeyWord”: “”
}, {
“MemberList”: [],
“Uin”: 0,
“UserName”: “@863e84f7210f6ca0da17b018aedf81bcf8a173fee1d811d2769f101c4267e020”,
“NickName”: “成员3”,
“AttrStatus”: 16912485,
“PYInitial”: “”,
“PYQuanPin”: “”,
“RemarkPYInitial”: “”,
“RemarkPYQuanPin”: “”,
“MemberStatus”: 0,
“DisplayName”: “”,
“KeyWord”: “”
}
],
“Uin”: 0,
“UserName”: “@@c25f17fed47b6946321965da58fab144f674975dafd780a9e2e173de307e60b6”,
“NickName”: “我是群名称”,
“HeadImgUrl”: “/cgi-bin/mmwebwx-bin/webwxgetheadimg?seq=0&username=@@c25f17fed47b6946321965da58fab144f674975dafd780a9e2e173de307e60b6&skey=@crypt_15c50994_8b9ef531e394f9e342c232850ba31c0f”,
“ContactFlag”: 2,
“MemberCount”: 3,
“RemarkName”: “”,
“HideInputBarFlag”: 0,
“Sex”: 0,
“Signature”: “”,
“VerifyFlag”: 0,
“OwnerUin”: 0,
“PYInitial”: “”,
“PYQuanPin”: “”,
“RemarkPYInitial”: “”,
“RemarkPYQuanPin”: “”,
“StarFriend”: 0,
“AppAccountFlag”: 0,
“Statues”: 1,
“AttrStatus”: 0,
“Province”: “”,
“City”: “”,
“Alias”: “”,
“SnsFlag”: 0,
“UniFriend”: 0,
“DisplayName”: “”,
“ChatRoomId”: 0,
“KeyWord”: “”,
“EncryChatRoomId”: “”,
“IsOwner”: 1,
“IsAdmin”: null,
“Self”: {
“MemberList”: [],
“Uin”: 0,
“UserName”: “@863e84f7210f6ca0da17b018aedf81bcf8a173fee1d811d2769f101c4267e020”,
“NickName”: “成员3”,
“AttrStatus”: 16912485,
“PYInitial”: “”,
“PYQuanPin”: “”,
“RemarkPYInitial”: “”,
“RemarkPYQuanPin”: “”,
“MemberStatus”: 0,
“DisplayName”: “”,
“KeyWord”: “”
},
“HeadImgUpdateFlag”: 1,
“ContactType”: 0,
“ChatRoomOwner”: “@863e84f7210f6ca0da17b018aedf81bcf8a173fee1d811d2769f101c4267e020”
},
“Type”: “Text”,
“Text”: “我是消息”
}
(1) BaseMsg群消息的原格式:

package com.wtyy.dto;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import com.wtyy.module.WechatGroup;
import com.wtyy.module.WechatGroupMember;
import com.wtyy.module.WechatGroupMsg;

public class BaseMsg implements Serializable{

protected static Logger Logger=LoggerFactory.getLogger(BaseMsg.class);

private static final long serialVersionUID = -948732742619659228L;

private String MsgId;

private String FromUserName;

private String ToUserName;

private Integer MsgType;

private String Content;

private Integer Status;

private Integer ImgStatus;

private Date CreateTime;

private Integer VoiceLength;

private Integer PlayLength;

private String FileName;

private String FileSize;

private String MediaId;

private String Url;

private Integer AppMsgType;

private Integer StatusNotifyCode;

private String StatusNotifyUserName;

private Integer ForwardFlag;

private String Ticket;

private Integer SubMsgType;

private String OriContent;

private Boolean IsAt;

//发送者
private String ActualNickName;

private String ActualUserName;

private String Type;

//同content
private String Text;

/** 推荐消息报文 **/
private RecommendInfo RecommendInfo;

/**
* 成员
*/
private WechatGroupDTO User;

/*省略所有get和set方法*/

/**
* WechatGroupDTO 转化成WechatGroup
*/
public static WechatGroup toWehatGroup(WechatGroupDTO wechatGroupDTO) {

List<Member> memberList = wechatGroupDTO.getMemberList();
WechatGroup wechatGroup = new WechatGroup();
wechatGroup.setChatRoomId(wechatGroupDTO.getChatRoomId());
wechatGroup.setGroupNickName(wechatGroupDTO.getNickName());
wechatGroup.setGroupUserName(wechatGroupDTO.getUserName());
wechatGroup.setHeadImgUrl(wechatGroupDTO.getHeadImgUrl());
wechatGroup.setMemberCount(wechatGroupDTO.getMemberCount());
String chatRoomOwner = wechatGroupDTO.getChatRoomOwner();
if(!StringUtils.isEmpty(chatRoomOwner)){
for(Member member:memberList){
if(chatRoomOwner.equals(member.getUserName())){
wechatGroup.setOwnerNickName(member.getNickName());
break;
}
}

}

return wechatGroup;
}

/**
* WechatGroupDTO 转化成 List<WechatGroupMember> 群成员
*/
public static List<WechatGroupMember> toWechatGroupMembers(WechatGroupDTO wechatGroupDTO) {
List<WechatGroupMember> members = new ArrayList<>();
List<Member> membersObj = wechatGroupDTO.getMemberList();
for(Member member : membersObj){
WechatGroupMember wechatGroupMember = new WechatGroupMember();
wechatGroupMember.setNickName(member.getNickName());
/*try {
//wechatGroupMember.setNickName(URLDecoder.decode(member.getNickName(), “utf-8”));
} catch (UnsupportedEncodingException e) {
Logger.error(“解码异常”+member.getNickName());
}*/
//wechatGroupMember.setUin(member.getUin());
wechatGroupMember.setUserName(member.getUserName());
members.add(wechatGroupMember);
}
return members;
}

/**
* 原生消息转化成WechatGroupMsg 消息实体
*/
public static WechatGroupMsg toWechatGroupMsg(BaseMsg msgObj, String groupId,String robotId) {
WechatGroupMsg wechatGroupMsg = new WechatGroupMsg();
wechatGroupMsg.setActualNickName(msgObj.getActualNickName());
wechatGroupMsg.setActualUserName(msgObj.getActualUserName());
wechatGroupMsg.setContent(msgObj.getContent());
wechatGroupMsg.setFileName(msgObj.getFileName());
wechatGroupMsg.setFromUserName(msgObj.getFromUserName());
wechatGroupMsg.setGroupId(groupId);
wechatGroupMsg.setImgStatus(msgObj.getImgStatus());
wechatGroupMsg.setMsgId(msgObj.getMsgId());
wechatGroupMsg.setMsgType(msgObj.getMsgType());
wechatGroupMsg.setRobotId(robotId);
wechatGroupMsg.setStatus(msgObj.getStatus());
wechatGroupMsg.setStatusNotifyCode(msgObj.getStatusNotifyCode());
wechatGroupMsg.setStatusNotifyUserName(msgObj.getStatusNotifyUserName());
wechatGroupMsg.setText(msgObj.getText());
wechatGroupMsg.setType(msgObj.getType());
wechatGroupMsg.setCreateTime(msgObj.getCreateTime());
wechatGroupMsg.setUrl(msgObj.getUrl());
return wechatGroupMsg;
}

}
(2)微信消息体中的User对象:

package com.wtyy.dto;

import java.io.Serializable;
import java.util.List;

public class WechatGroupDTO implements Serializable{

/**
*
*/
private static final long serialVersionUID = 3919060806498433575L;

private List<Member> MemberList;

private Long Uin;

/**
* 群名
*/
private String UserName;

/**
* 群昵称
*/
private String NickName;

/**
* 群头像
*/
private String HeadImgUrl;

private Integer ChatRoomId;

/**
* 群成员数
*/
private Integer MemberCount;

/**
* 群主(用户名)
*/
private String ChatRoomOwner;

/**
* 当前登录用户
*/
private Member Self;

/*省略所有get和set方法*/
}
(3)、微信成员对象:

 

package com.wtyy.dto;
import java.io.Serializable;
public class Member implements Serializable{

private static final long serialVersionUID = 4300987291874273446L;

//private Integer Uin;

private String UserName;

private String NickName;

}
(4)RecommendInfo:

package com.wtyy.dto;

import java.io.Serializable;
public class RecommendInfo implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;

private String Province;
private String Ticket;
private String UserName;
private int Sex;
private int AttrStatus;
private String City;
private String NickName;
private int Scene;
private String Content;
private String Alias;
private String Signature;
private int OpCode;
private int QQNum;
private int VerifyFlag;
}
3、kafka监听:(一般是写成一个单独的job,这里暂时和controller放在一个工程里面)

package com.wtyy.consume;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.spec.MGF1ParameterSpec;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import ch.qos.logback.core.db.dialect.MsSQLDialect;

import com.alibaba.fastjson.JSON;
import com.wtyy.dto.BaseMsg;
import com.wtyy.dto.WechatGroupDTO;
import com.wtyy.module.WechatGroup;
import com.wtyy.module.WechatGroupMember;
import com.wtyy.module.WechatGroupMsg;
import com.wtyy.service.WechatGroupMsgService;
import com.wtyy.service.WechatService;
import com.wtyy.util.RedisUtil;

/**
*
* @Description: kafka消息监听
*/
@Service
public class WechatGroupConsumer {

protected static Logger logger=LoggerFactory.getLogger(WechatGroupConsumer.class);

@Autowired
private WechatService wechatService;

@Autowired
private WechatGroupMsgService wechatGroupMsgService;

@Autowired
private RedisUtil redisClient;

//redis缓存过期时间,一天
private static final int EXPIRE_TIME = 60*60*24;

//机器昵称-机器id
private static final String WECHAT_ROBOT_NICKNAME_ID = “wechat:robot:{nickName}”;

//群昵称-群id
private static final String WECHAT_GROUP_NICKNAME_ID = “wechatGroup:{nickName}:{robotId}”;

/**
* 1、监听群和群成员
*/
@KafkaListener(topics = {“${wechat.group}”})
public void consumeGroup(ConsumerRecord<?, ?> consumerRecord) throws UnsupportedEncodingException{
String group = (String) consumerRecord.value();
/*List<WechatGroupDTO> groupList = JSONUtils.parseList(groups, WechatGroupDTO.class);

for(WechatGroupDTO wechatGroupDTO : groupList){
handleWechatGroup(wechatGroupDTO);
}*/
WechatGroupDTO wechatGroupDTO = null;
try{
wechatGroupDTO = JSON.parSEObject(group, WechatGroupDTO.class);
}catch (Exception e) {
logger.error(“群成员信息获取异常”);
}
handleWechatGroup(wechatGroupDTO);
}

/**
* 2、监听群消息
*/
@KafkaListener(topics = {“${wechat.msg}”})
public void consumeMsg(ConsumerRecord<?, ?> consumerRecord) throws UnsupportedEncodingException{

try{
String message = (String) consumerRecord.value();
System.err.println(message);
BaseMsg msgObj = JSON.parSEObject(message, BaseMsg.class);
//System.err.println(“时间”+msgObj.getCreateTime());
WechatGroupDTO wechatGroupDTO = msgObj.getUser();
//群昵称
String groupNickName = wechatGroupDTO.getNickName();
//当前登录用户昵称
String currentRobotNickName = wechatGroupDTO.getSelf().getNickName();

String robotId = redisClient.get(WECHAT_ROBOT_NICKNAME_ID, currentRobotNickName);
if(StringUtils.isEmpty(robotId)){
robotId = wechatService.getRobotIdByNickName(currentRobotNickName);
if(!StringUtils.isEmpty(robotId)){
redisClient.setex(WECHAT_ROBOT_NICKNAME_ID, EXPIRE_TIME, robotId, currentRobotNickName);
}else{
logger.error(“robotId为空”);
}
}

if(!StringUtils.isEmpty(robotId)){

//根据群昵称查询群是否存在
String groupId = redisClient.get(WECHAT_GROUP_NICKNAME_ID, groupNickName,robotId);
if(StringUtils.isEmpty(groupId)){
groupId = wechatService.getGroupIdByGroupNickName(groupNickName);
if(!StringUtils.isEmpty(groupId)){
redisClient.setex(WECHAT_GROUP_NICKNAME_ID, EXPIRE_TIME, groupId, groupNickName,robotId);
}
}

if(groupId == null){
//群不存在,插入群与群成员
groupId = addWechatGroupAndMembers(msgObj.getUser(),robotId,groupId);
redisClient.setex(WECHAT_GROUP_NICKNAME_ID, EXPIRE_TIME, groupId, groupNickName,robotId);

}

//插入群消息
WechatGroupMsg wechatGroupMsg = BaseMsg.toWechatGroupMsg(msgObj,groupId,robotId);
wechatGroupMsgService.addGroupMsg(wechatGroupMsg);
}

}catch (Exception e) {
logger.error(e+e.getMessage());
}
}

/**
* @author: tingzhang7
* @param msgObj 原生消息DTO
* @param robotId 机器id
* @param groupId groupId,为空则新增群
* @Description: 新增群
* @param 返回群id
*/
private String addWechatGroupAndMembers(WechatGroupDTO wechatGroupDTO, String robotId,String groupId) {

if(groupId == null){
//新增群
groupId = UUID.randomUUID().toString();
WechatGroup wechatGroup = BaseMsg.toWehatGroup(wechatGroupDTO);
wechatGroup.setGroupId(groupId);
wechatGroup.setRobotId(robotId);
wechatService.addWechatGroup(wechatGroup);
}

//新增群成员
List<WechatGroupMember> groupMembers = BaseMsg.toWechatGroupMembers(wechatGroupDTO);
wechatService.batchInsertWechatGroupMember(groupMembers, groupId);
return groupId;
}

/**
* @author: tingzhang7
* @Description: 处理群
*/
private void handleWechatGroup(WechatGroupDTO wechatGroupDTO) {

if(wechatGroupDTO == null){
logger.error(“群解析后为空”);
return ;
}
String groupNickName = wechatGroupDTO.getNickName();
if(StringUtils.isEmpty(groupNickName)){
logger.error(“群名称为空”);
return;
}
//机器昵称
String robotNickName = wechatGroupDTO.getSelf().getNickName();

//获取机器id
String robotId = redisClient.get(WECHAT_ROBOT_NICKNAME_ID, robotNickName);
if(StringUtils.isEmpty(robotId)){
robotId = wechatService.getRobotIdByNickName(robotNickName);
if(!StringUtils.isEmpty(robotId)){
redisClient.setex(WECHAT_ROBOT_NICKNAME_ID, EXPIRE_TIME, robotId, robotNickName);
}else{
logger.error(“获取不到机器robotId “+robotNickName);
return ;
}
}

//判断该群有没有记录过
String groupId = redisClient.get(WECHAT_GROUP_NICKNAME_ID, groupNickName,robotId);
if(StringUtils.isEmpty(groupId)){
groupId = wechatService.getGroupIdByGroupNickName(groupNickName);
if(!StringUtils.isEmpty(groupId)){
redisClient.setex(WECHAT_GROUP_NICKNAME_ID, EXPIRE_TIME, groupId, groupNickName,robotId);
}
}
//新增群
if(groupId == null){
//群不存在,插入群与群成员
groupId = addWechatGroupAndMembers(wechatGroupDTO,robotId,groupId);
redisClient.setex(WECHAT_GROUP_NICKNAME_ID, EXPIRE_TIME, groupId, groupNickName,robotId);
}else{
//群存在,更新群成员(按照nickName比较)
List<WechatGroupMember> oldMembers = wechatService.getMembersByGroupId(groupId);
List<WechatGroupMember> newMembers = BaseMsg.toWechatGroupMembers(wechatGroupDTO);
if(newMembers.size() != oldMembers.size()){
wechatService.updateWechatGroupSize(groupId,newMembers.size());
}
//newMembers备份
List<WechatGroupMember> copyOfNewMembers = BaseMsg.toWechatGroupMembers(wechatGroupDTO);
//新增的 = newMembers-old,newMembers为新增的差集
newMembers.removeAll(oldMembers);

List<WechatGroupMember> members = new ArrayList<>();
for(WechatGroupMember member : newMembers){
members.add(member);
if(members.size() == 200){
wechatService.batchInsertWechatGroupMember(members, groupId);
members.clear();
}
}
if(!CollectionUtils.isEmpty(members)){
wechatService.batchInsertWechatGroupMember(members, groupId);
}

//减少的 = old-copyOfNewMembers,oldMembers为减少的差集
oldMembers.removeAll(copyOfNewMembers);
List<Integer> ids = new ArrayList<>();
for(WechatGroupMember groupMember : oldMembers){
ids.add(groupMember.getId());
if(ids.size() == 200){
wechatService.batchDeleteWechatGroupMember(ids,groupId);
ids.clear();
}
}
if(!CollectionUtils.isEmpty(ids)){
wechatService.batchDeleteWechatGroupMember(ids,groupId);
}
}
}

}
4、启动类:

package com.wtyy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
import org.springframework.scheduling.annotation.EnableScheduling;

import com.alibaba.dubbo.config.spring.context.annotation.EnableDubboConfig;
import com.alibaba.dubbo.spring.boot.annotation.EnableDubboConfiguration;

@SpringBootApplication
@EnableDubboConfiguration
@ImportResource(“classpath:consumer.xml”)
@EnableScheduling
public class RestStart {
public static void main(String[] args) {
SpringApplication.run(RestStart.class, args);
}

}
五、最后看下Python脚本:

import itchat, time
from itchat.content import *
from kafka import KafkaProducer
import json
import threading
import oss2
import os
import uuid
producer = KafkaProducer(
value_serializer=lambda v: json.dumps(v).encode(‘utf-8’),
bootstrap_servers=[‘localhost:9092’]
)

#获取群列表
def getChatRooms() :
list = itchat.get_chatrooms(update=True)
itchat.dump_login_status()
for i in list:
# if i[‘MemberCount’] == 0:
# memberList = itchat.update_chatroom(i[‘UserName’], detailedMember=True)
# producer.send(‘wechatgroup’, memberList)
# print(i[‘NickName’],”查询”)
# else:
# producer.send(‘wechatgroup’, i)
# print(i[‘NickName’],”不查询”)
memberList = itchat.update_chatroom(i[‘UserName’], detailedMember=True)
producer.send(‘wechatgroup’, memberList)
timer = threading.Timer(10.5, getChatRooms)
timer.start()

#接收群-文本消息
@itchat.msg_register(TEXT, isGroupChat=True)
def text_reply(msg):
print(“文本消息”)
producer.send(‘wechatmsg’, msg)
# if msg.isAt:
# msg.user.send(u’@%s\u2005I received: %s’ % (
# msg.actualNickName, msg.text))

#接收群 分享消息
@itchat.msg_register([SHARING],isGroupChat=True)
def recieve_sharing(msg):
print(“分享消息”)
producer.send(‘wechatmsg’,msg)

#接收群 文件消息
@itchat.msg_register([PICTURE, RECORDING, ATTACHMENT, VIDEO],isGroupChat=True)
def download_files(msg):
msg.download(msg.fileName)
url = uploadFile(msg.fileName)
msg[‘Text’] = “”
msg[‘Url’] = url
producer.send(‘wechatmsg’,msg)

#上传文件到阿里云并返回url
def uploadFile(fileName):
url = ”
auth = oss2.Auth(xxx, xxx)
endpoint = ‘https://oss-cn-hangzhou.aliyuncs.com’
bucket = oss2.Bucket(auth, endpoint, xxx, connect_timeout=30)
fileUrl = os.getcwd()+os.path.sep+fileName
result = bucket.put_object_from_file(fileName, fileUrl)

if result.status == 200:
# 阿里返回一个关于fileName的url地址 ,最后一个参数是以秒为单位的过期时间
url = bucket.sign_url(‘GET’, fileName, 60 * 60 * 24)
print(‘上传成功’)
#上传成功后删除文件
os.remove(fileUrl)
return url

@itchat.msg_register([TEXT, MAP, CARD, NOTE, SHARING])
def text_reply(msg):
msg.user.send(‘%s: %s’ % (msg.type, msg.text))

@itchat.msg_register(FRIENDS)
def add_friend(msg):
msg.user.verify()
msg.user.send(‘Nice to meet you!’)

# 通过群昵称获得群聊userName
def get_group_userName(group_name):
group_list = itchat.search_chatrooms(name=group_name)
return group_list[0][‘UserName’]

# 通过好友昵称获得好友userName
def get_friend_userName(friend_name):
friend = itchat.search_friends(name=friend_name)
return friend[0][‘UserName’]

#自动拉取好友进群,传入昵称
def addFriendToChatRoom(group_name,friend_name):
groupUserName = get_group_userName(group_name)
print(“群”+groupUserName)
friendUserName = get_friend_userName(friend_name)
print(“好友”+friendUserName)
status = itchat.add_member_into_chatroom(groupUserName,
[{‘UserName’: friendUserName}],
useInvitation=False)
print(“状态”)
print(status)

#多用户登录
instanceDir = ‘%s.pkl’ % uuid.uuid4()
itchat.auto_login(hotReload=True,enableCmdQR =True,statusStorageDir=instanceDir)
#定时获取群列表
timer = threading.Timer(2, getChatRooms)
timer.start()
itchat.run(True)

注:将用户加入群聊有直接加入和发送邀请两种,通过 useInvitation设置,True为发送邀请False为直接加入,超过40人的群只能使用邀请的方式。

********************************************************分割线********************************************************************************

好了,现在启动本地redis、kafka后,再分别启动service项目和rest项目。

准备扫码登录:

登录成功后可以看到会自动生成一个.pkl的文:

就是这个文件缓存了登录状态,所以这个文件的名称我生成了一个随机数,否则

itchat.auto_login(hotReload=True,enableCmdQR =True)
这样的话只允许一个用户登录。登录后就可以定时的更新群成员、获取各类群消息了:

 

————————————————
版权声明:本文为CSDN博主「w_t_y_y」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/w_t_y_y/article/details/89512517

sql server对并发的处理-乐观锁和悲观锁 - Tracy.Net - 博客园

mikel阅读(974)

来源: sql server对并发的处理-乐观锁和悲观锁 – Tracy.Net – 博客园

假如两个线程同时修改数据库同一条记录,就会导致后一条记录覆盖前一条,从而引发一些问题。

例如:

一个售票系统有一个余票数,客户端每调用一次出票方法,余票数就减一。

情景:

总共300张票,假设两个售票点,恰好在同一时间出票,它们做的操作都是先查询余票数,然后减一。

一般的SQL语句:

 

1
2
3
4
5
6
7
8
9
declare @count as int
begin tran
    select @count=count from ttt
    WAITFOR DELAY '00:00:05' --模拟并发,故意延迟5秒
    update ttt set count=@count-1
commit TRAN
SELECT FROM ttt

 

问题就在于,同一时间获取的余票都为300,每个售票点都做了一次更新为299的操作,导致余票少了1,而实际出了两张票。

打开两个查询窗口,分别快速运行以上代码即可看到效果。

 

定义解释:

悲观锁:相信并发是绝大部分的,并且每一个线程都必须要达到目的的。

乐观锁:相信并发是极少数的,假设运气不好遇到了,就放弃并返回信息告诉它再次尝试。因为它是极少数发生的。

 

悲观锁解决方案:

 

1
2
3
4
5
6
7
declare @count as int
begin tran
    select @count=count from tb WITH(UPDLOCK)
   WAITFOR DELAY '00:00:05' --模拟并发,故意延迟5秒
    update tb set count=@count-1
commit tran

 

在查询的时候加了一个更新锁,保证自查询起直到事务结束不会被其他事务读取修改,避免产生脏数据。

从而可以解决上述问题。

 

乐观锁解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--首先给表加一列timestamp
ALTER TABLE ttt ADD timesFlag TIMESTAMP NOT null
然后更新时判断这个值是否被修改
declare @count as int
DECLARE @flag AS TIMESTAMP
DECLARE @rowCount AS int
begin tran
    select @count=COUNT,@flag=timesflag from ttt
    WAITFOR DELAY '00:00:05'
    update ttt set count=@count-1 WHERE timesflag=@flag --这里加了条件
    SET @rowcount=@@ROWCOUNT  --获取被修改的行数
commit TRAN
--对行数进行判断即可
IF @rowCount=1
    PRINT '更新成功'
ELSE
    PRINT '更新失败'

这便是乐观锁的解决方案,可以解决并发带来的数据错误问题,但不保证每一次调用更新都成功,可能会返回’更新失败’

 

悲观锁和乐观锁

悲观锁一定成功,但在并发量特别大的时候会造成很长堵塞甚至超时,仅适合小并发的情况。

乐观锁不一定每次都修改成功,但能充分利用系统的并发处理机制,在大并发量的时候效率要高很多。

SQL Server 锁机制 悲观锁 乐观锁 实测解析 - taiyonghai - 博客园

mikel阅读(646)

来源: SQL Server 锁机制 悲观锁 乐观锁 实测解析 – taiyonghai – 博客园

先引入一些概念,直接Copy其他Blogs中的,我就不单独写了。

一、为什么会有锁

多个用户同时对数据库的并发操作时会带来以下数据不一致的问题:

1.丢失更新

A,B两个用户读同一数据并进行修改,其中一个用户的修改结果破坏了另一个修改的结果,比如订票系统

2.脏读

A用户修改了数据,随后B用户又读出该数据,但A用户因为某些原因取消了对数据的修改,数据恢复原值,此时B得到的数据就与数据库内的数据产生了不一致

3.不可重复读

A用户读取数据,随后B用户读出该数据并修改,此时A用户再读取数据时发现前后两次的值4.不一致

并发控制的主要方法是封锁,锁就是在一段时间内禁止用户做某些操作以避免产生数据不一致

 

二、锁的种类

共享 (S) 用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。

更新 (U) 用于可更新的资源中。防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁。

排它 (X) 用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。

意向锁 用于建立锁的层次结构。意向锁的类型为:意向共享 (IS)、意向排它 (IX) 以及与意向排它共享 (SIX)。

架构锁 在执行依赖于表架构的操作时使用。架构锁的类型为:架构修改 (Sch-M) 和架构稳定性 (Sch-S)。

大容量更新 (BU) 向表中大容量复制数据并指定了 TABLOCK 提示时使用。

共享锁

共享 (S) 锁允许并发事务读取 (SELECT) 一个资源。资源上存在共享 (S) 锁时,任何其它事务都不能修改数据。一旦已经读取数据,便立即释放资源上的共享 (S) 锁,除非将事务隔离级别设置为可重复读或更高级别,或者在事务生存周期内用锁定提示保留共享 (S) 锁。

更新锁

更新 (U) 锁可以防止通常形式的死锁。一般更新模式由一个事务组成,此事务读取记录,获取资源(页或行)的共享 (S) 锁,然后修改行,此操作要求锁转换为排它 (X) 锁。如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则一个事务尝试将锁转换为排它 (X) 锁。共享模式到排它锁的转换必须等待一段时间,因为一个事务的排它锁与其它事务的共享模式锁不兼容;发生锁等待。第二个事务试图获取排它 (X) 锁以进行更新。由于两个事务都要转换为排它 (X) 锁,并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。

若要避免这种潜在的死锁问题,请使用更新 (U) 锁。一次只有一个事务可以获得资源的更新 (U) 锁。如果事务修改资源,则更新 (U) 锁转换为排它 (X) 锁。否则,锁转换为共享锁。

排它锁

排它 (X) 锁可以防止并发事务对资源进行访问。其它事务不能读取或修改排它 (X) 锁锁定的数据。

意向锁

意向锁表示 SQL Server 需要在层次结构中的某些底层资源上获取共享 (S) 锁或排它 (X) 锁。例如,放置在表级的共享意向锁表示事务打算在表中的页或行上放置共享 (S) 锁。在表级设置意向锁可防止另一个事务随后在包含那一页的表上获取排它 (X) 锁。意向锁可以提高性能,因为 SQL Server 仅在表级检查意向锁来确定事务是否可以安全地获取该表上的锁。而无须检查表中的每行或每页上的锁以确定事务是否可以锁定整个表。

意向锁包括意向共享 (IS)、意向排它 (IX) 以及与意向排它共享 (SIX)。

 

两种由程序员定义的锁

乐观锁:依靠表中数据行内的版本戳或时间戳字段来人工管理锁的工作。

悲观锁:使用数据库或对象上提供的锁机制来处理。

死锁:死锁的意思就是A用户查找表1并获得了S锁,B用户查找表1也获得了S锁,当A用户找到要更新的行申请X锁时被告知B已经有S锁需要等待B解锁,B用户也找到要更新的行申请X锁时被告知A已经有了S锁需要等待A解锁,然后A与B就相互无休止的等待造成死锁。

 

三、锁的粒度也就是范围

锁粒度是被封锁目标的大小,封锁粒度小则并发性高,但开销大,封锁粒度大则并发性低但开销小

SQL Server支持的锁粒度可以分为为行、页、键、键范围、索引、表或数据库获取锁

RID      行标识符。用于单独锁定表中的一行。

KEY       索引中的行锁。用于保护可串行事务中的键范围。

PAGE 8    千字节 (KB) 的数据页或索引页。

EXTENT     相邻的八个数据页或索引页构成的一组。

TABLE     包括所有数据和索引在内的整个表。

DATABASE  数据库。

 

锁的粒度和锁的类型都是由SQL Server进行控制的(当然你也可以使用锁提示,但不推荐)。锁会给数据库带来阻塞,因此越大粒度的锁造成更多的阻塞,但由于大粒度的锁需要更少的锁,因此会提升性能。而小粒度的锁由于锁定更少资源,会减少阻塞,因此提高了并发,但同时大量的锁也会造成性能的下降。

 

四、锁的应用

在使用SQL时,大都会遇到这样的问题,你Update一条记录时,需要通过Select来检索出其值或条件,然后在通过这个值来执行修改操作。

但当以上操作放到多线程中并发处理时会出现问题:某线程select了一条记录但还没来得及update时,另一个线程仍然可能会进来select到同一条记录。

一般解决办法就是使用锁和事物的联合机制:

1. 把select放在事务中, 否则select完成, 锁就释放了
2. 要阻止另一个select , 则要手工加锁, select 默认是共享锁, select之间的共享锁是不冲突的, 所以, 如果只是共享锁, 即使锁没有释放, 另一个select一样可以下共享锁, 从而select出数据

BEGIN TRAN
SELECT * FROM Table WITH(UPDLOCK) 
--或者 SELECT * FROM Table WITH(TABLOCKX, READPAST) 具体情况而定。
UPDATE ....
COMMIT TRAN

所有Select加 With (NoLock)解决阻塞死锁,在查询语句中使用 NOLOCK 和 READPAST
处理一个数据库死锁的异常时候,其中一个建议就是使用 NOLOCK 或者 READPAST 。有关 NOLOCK 和 READPAST的一些技术知识点:
对于非银行等严格要求事务的行业,搜索记录中出现或者不出现某条记录,都是在可容忍范围内,所以碰到死锁,应该首先考虑,我们业务逻辑是否能容忍出现或者不出现某些记录,而不是寻求对双方都加锁条件下如何解锁的问题。
NOLOCK 和 READPAST 都是处理查询、插入、删除等操作时候,如何应对锁住的数据记录。但是这时候一定要注意NOLOCK 和 READPAST的局限性,确认你的业务逻辑可以容忍这些记录的出现或者不出现:
简单来说:

1.NOLOCK 可能把没有提交事务的数据也显示出来
2.READPAST 会把被锁住的行不显示出来

不使用 NOLOCK 和 READPAST ,在 Select 操作时候则有可能报错误:事务(进程 ID **)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。

SELECT * FROM Table WITH(NOLOCK)
SELECT * FROM Table WITH(READPAST)

锁描述:

HOLDLOCK:将共享锁保留到事务完成,而不是在相应的表、行或数据页不再需要时就立即释放锁。HOLDLOCK等同于 SERIALIZABLE。
NOLOCK 不要发出共享锁,并且不要提供排它锁。当此选项生效时,可能会读取未提交的事务或一组在读取中间回滚的页面。有可能发生脏读。仅应用于 SELECT 语句。
PAGLOCK:在通常使用单个表锁的地方采用页锁。
READCOMMITTED:用与运行在提交读隔离级别的事务相同的锁语义执行扫描。默认情况下,SQL Server 2000在此隔离级别上操作。
READPAST:跳过锁定行。此选项导致事务跳过由其它事务锁定的行(这些行平常会显示在结果集内),而不是阻塞该事务,使其等待其它事务释放在这些行上的锁。 READPAST 锁提示仅适用于运行在提交读隔离级别的事务,并且只在行级锁之后读取。仅适用于 SELECT 语句。
READUNCOMMITTED:等同于 NOLOCK。
REPEATABLEREAD:用与运行在可重复读隔离级别的事务相同的锁语义执行扫描。
ROWLOCK:使用行级锁,而不使用粒度更粗的页级锁和表级锁。
SERIALIZABLE:用与运行在可串行读隔离级别的事务相同的锁语义执行扫描。等同于 HOLDLOCK。
TABLOCK:使用表锁代替粒度更细的行级锁或页级锁。在语句结束前,SQL Server 一直持有该锁。但是,如果同时指定 HOLDLOCK,那么在事务结束之前,锁将被一直持有。
TABLOCKX 使用表的排它锁。该锁可以防止其它事务读取或更新表,并在语句或事务结束前一直持有。
UPDLOCK:读取表时使用更新锁,而不使用共享锁,并将锁一直保留到语句或事务的结束。UPDLOCK:的优点是允许您读取数据(不阻塞其它事务)并在以后更新数据,同时确保自从上次读取数据后数据没有被更改。
XLOCK:使用排它锁并一直保持到由语句处理的所有数据上的事务结束时。可以使用 PAGLOCK 或 TABLOCK 指定该锁,这种情况下排它锁适用于适当级别的粒度。

 

实际开始动手用代码说话吧!

SQLServer2012在查询分析器里面开两个连接

插入锁:

结论:“表锁”锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select,所以Select如果不想等待就要在Select后加With(Nolock),但这样会产生脏数据就是其他事务已更新但并没有提交的数据,如果该事务进行了RollBack则取出的数据就是错误的,所以好自己权衡利弊,一般情况下90%以上的Select都允许脏读,只有账户金额相关的不允许。

复制代码
------------------A连接 Insert Lock-------------------
BEGIN TRAN
INSERT INTO dbo.UserInfo
        ( Name, Age, Mobile, AddTime, Type )
VALUES  ( 'eee', -- Name - varchar(50)
          2, -- Age - int
          '555', -- Mobile - char(11)
          GETDATE(), -- AddTime - datetime
          0  -- Type - int
          )
          
SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode
--ROLLBACK TRAN

------------------------B连接 Insert Lock------------------------
INSERT INTO dbo.UserInfo
        ( Name, Age, Mobile, AddTime, Type )
VALUES  ( 'fff', -- Name - varchar(50)
          2, -- Age - int
          '123', -- Mobile - char(11)
          GETDATE(), -- AddTime - datetime
          1  -- Type - int
          ) --可以执行插入
          
SELECT * FROM dbo.UserInfo --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Age=1 --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Id=3 --可以执行查询(根据主键可以)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Age=1 --可以执行查询
UPDATE dbo.UserInfo SET Type=5 WHERE Name='fff'  --需要等待解锁
DELETE FROM dbo.UserInfo WHERE Name='fff' --需要等待解锁
复制代码

更新锁:

结论:“表锁”锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select

复制代码
-----------------------A连接 Update Lock-----------------------
BEGIN TRAN
UPDATE dbo.UserInfo SET Name = 'eee' WHERE Age = 2

SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode

--ROLLBACK TRAN

------------------------B连接 Update Lock------------------------
INSERT INTO dbo.UserInfo
        ( Name, Age, Mobile, AddTime, Type )
VALUES  ( 'ppp', -- Name - varchar(50)
          15, -- Age - int
          '666', -- Mobile - char(11)
          GETDATE(), -- AddTime - datetime
          9  -- Type - int
          ) --可以执行插入
SELECT * FROM dbo.UserInfo --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Name='ppp' --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Id=3 --可以执行查询(根据主键可以)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'ppp' --可以执行查询
UPDATE dbo.UserInfo SET Age=8 WHERE Name='ccc' --需要等待解锁
DELETE dbo.UserInfo WHERE Age = 5 --需要等待解锁
复制代码

主键锁:

结论:“行锁+表锁” 锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select、Update、Delete

复制代码
------------------------A连接 Key Lock--------------------
BEGIN TRAN
UPDATE dbo.UserInfo SET Name='hhh' WHERE Id=3 --以主键为条件

SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode

--ROLLBACK TRAN

------------------------B连接 Key Lock----------------------
INSERT INTO dbo.UserInfo
        ( Name, Age, Mobile, AddTime, Type )
VALUES  ( 'kkk', -- Name - varchar(50)
          18, -- Age - int
          '234', -- Mobile - char(11)
          GETDATE(), -- AddTime - datetime
          7  -- Type - int
          ) --可以执行插入
SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以执行查询

-----//全表查询及操作正在处理的行
SELECT * FROM dbo.UserInfo --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
DELETE dbo.UserInfo WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
-----//使用非主键为条件的操作
SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --需要等待解锁(非主键不可)
DELETE dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
-----//使用主键为条件的操作
SELECT * FROM dbo.UserInfo WHERE id=1 --可以执行查询(根据主键可以)
UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以执行更新(根据主键可以)
DELETE dbo.UserInfo WHERE Id=1 --可以执行删除(根据主键可以)
复制代码

索引锁:

结论:“行锁+表锁” 锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select、Update、Delete,也不影响以索引列Name为条件的Update、Delete但不可以Select

复制代码
------------------------A连接 Index Lock--------------------
DROP INDEX dbo.UserInfo.Index_UserInfo_Name
CREATE INDEX Index_UserInfo_Name ON dbo.UserInfo(Name)

BEGIN TRAN
UPDATE dbo.UserInfo SET age=66 WHERE Name='ddd' --使用name索引列为条件

SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode

--ROLLBACK TRAN

----------------------B连接 Index Lock-------------------
INSERT INTO dbo.UserInfo
        ( Name, Age, Mobile, AddTime, Type )
VALUES  ( 'iii', -- Name - varchar(50)
          20, -- Age - int
          '235235235', -- Mobile - char(11)
          GETDATE(), -- AddTime - datetime
          12  -- Type - int
          ) --可以执行插入
SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事物中,有更新字段但还没有提交,此时就会查处脏数据)
SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以执行查询

-----//全表查询及操作正在处理的行
SELECT * FROM dbo.UserInfo --需要等待解锁
SELECT * FROM dbo.UserInfo WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
DELETE dbo.UserInfo WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
-----//使用非主键非索引为条件的操作
SELECT * FROM dbo.UserInfo WHERE Age=5 --需要等待解锁(非主键不可)
UPDATE dbo.UserInfo SET Name='ooo' WHERE Age=5 --需要等待解锁(非主键不可)
DELETE dbo.UserInfo WHERE Age=5 --需要等待解锁(非主键不可)
-----//使用主键为条件的操作
SELECT * FROM dbo.UserInfo WHERE Id=1 --可以执行更新(根据主键可以)
UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以执行更新(根据主键可以)
DELETE dbo.UserInfo WHERE Id=1 --可以执行删除(根据主键可以)
-----//使用索引为条件的操作
SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --可以执行更新(根据索引可以)
DELETE dbo.UserInfo WHERE Name='aaa' --可以执行删除(根据索引可以)
复制代码

悲观锁(更新锁-人工手动设置上锁):

结论:可以理解为在使用版本控制软件的时候A迁出了一个文件,并且将这个文件锁定,B就无法再迁出该文件了,直到A迁入解锁后才能被其他人迁出。

复制代码
------------------------A连接 Update Lock(悲观锁)---------------------
BEGIN TRAN
SELECT * FROM dbo.UserInfo WITH(UPDLOCK) WHERE Id=2

SELECT resource_type, request_mode,COUNT(*)  FROM sys.dm_tran_locks
WHERE request_session_id=@@SPID
GROUP BY resource_type,request_mode

--COMMIT TRAN
--ROLLBACK TRAN

---------------------------B连接 Update Lock(悲观锁)-------------------------
SELECT * FROM dbo.UserInfo --可以执行查询
SELECT * FROM dbo.UserInfo WHERE id=2 --可以执行查询
SELECT * FROM dbo.UserInfo WHERE Name='ooo' --可以执行查询

UPDATE dbo.UserInfo SET Age=3 WHERE id=1 --可以执行更新(根据主键可以)
UPDATE dbo.UserInfo SET Age=3 WHERE Name='ccc' --需要等待解锁(非主键不可)

DELETE dbo.UserInfo WHERE id=1 --可以执行更新(根据主键可以)
DELETE dbo.UserInfo WHERE name='ccc' --需要等待解锁(非主键不可)
复制代码

乐观锁(人工通过逻辑在数据库中模拟锁)

结论:可以理解为同样在使用版本控制软件的时候A迁出了一个文件,B也可以迁出该文件,两个人都可以对此文件进行修改,其中一个人先进行提交的时候,版本并没有变化所以可以正常提交,另一个后提交的时候,发现版本增加不对称了,就提示冲突由用户来选择如何进行合并再重新进行提交。

复制代码
--------------------------A客户端连接 Lock(乐观锁)------------------------
--DROP TABLE Coupon
-----------------创建优惠券表-----------------
CREATE TABLE Coupon
(
    Id INT PRIMARY KEY IDENTITY(1,1),
    Number VARCHAR(50) NOT NULL,
    [User] VARCHAR(50),
    UseTime DATETIME,
    IsFlag BIT DEFAULT(0) NOT NULL,
    CreateTime DATETIME DEFAULT(GETDATE()) NOT NULL
)
INSERT INTO dbo.Coupon(Number) VALUES ( '10000001')
INSERT INTO dbo.Coupon(Number) VALUES ( '10000002')
INSERT INTO dbo.Coupon(Number) VALUES ( '10000003')
INSERT INTO dbo.Coupon(Number) VALUES ( '10000004')
INSERT INTO dbo.Coupon(Number) VALUES ( '10000005')
INSERT INTO dbo.Coupon(Number) VALUES ( '10000006')

--SELECT * FROM dbo.Coupon WITH(NOLOCK) --查询数据
--UPDATE Coupon SET [User]=NULL, UseTime=NULL, IsFlag=0 --还原数据

-----------------1、模拟高并发普通更新-----------------
DECLARE @User VARCHAR(50)    --模拟要使用优惠券的用户
DECLARE @TempId INT            --模拟抽选出来的要使用的优惠券
SET @User='a'
BEGIN TRAN
SELECT @TempId=Id FROM dbo.Coupon WHERE IsFlag=0    --高并发时此语句有可能另外一个该事务已取出的Id
--WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
COMMIT TRAN
--ROLLBACK TRAN

-----------------2、悲观锁解决方案-----------------
DECLARE @User VARCHAR(50)    --模拟要使用优惠券的用户
DECLARE @TempId INT            --模拟抽选出来的要使用的优惠券
SET @User='a'
BEGIN TRAN
SELECT @TempId=Id FROM dbo.Coupon WITH(UPDLOCK) WHERE IsFlag=0    --高并发时此语句会锁定取出的Id数据行
--WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
COMMIT TRAN
--ROLLBACK TRAN

-----------------3、乐观锁解决方案-----------------
ALTER TABLE dbo.Coupon ADD RowVer ROWVERSION NOT NULL --增加数据行版本戳类型字段(微软新推荐数据字段,该字段每张表只能有一个,会在创建行或更新行时自动进行修改无需人为干涉,该字段不能建立索引及主键因为会频繁修改)

DECLARE @User VARCHAR(50)    --模拟要使用优惠券的用户
DECLARE @TempId INT            --模拟抽选出来的要使用的优惠券
DECLARE @RowVer BINARY(8)    --抽选出来的优惠券的版本(ROWVERSION数据类型存储大小为8字节)
SET @User='a'

BEGIN TRY
    BEGIN TRAN
    SELECT @TempId=Id, @RowVer=RowVer FROM dbo.Coupon WHERE IsFlag=0    --取出可用的Id及对应的版本戳
    --WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
    UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId AND RowVer=@RowVer
    IF(@@ROWCOUNT > 0)
        BEGIN
            PRINT('修改成功')
            COMMIT TRAN
        END
    ELSE
        BEGIN
            PRINT('该数据已被其他用户修改')
            ROLLBACK TRAN
        END
END TRY
BEGIN CATCH
    ROLLBACK TRAN
END CATCH

--------------------------B客户端连接 Lock(乐观锁)------------------------
--此测试需要开两个SQL Management Studio客户端,在A客户端使用WAITFOR DELAY来模拟并发占用,在B客户端执行与A客户端相同的SQL脚本即可(注释掉WAITFOR),所以在此不放相同代码了。
复制代码

在乐观锁和悲观锁之间进行选择的标准是:冲突的频率与严重性。如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。但是,如果冲突的结果对于用户来说痛苦的,那么就需要使用悲观策略。

我认为如果同一张表的并发很高,但并发处理同一条数据的冲突几率很低,那就应该使用乐观锁,反之,如果同一张表的并发不高,但同时处理同一条数据的几率很高,就应该使用悲观锁。

乐观锁与悲观锁——解决并发问题 - WhyWin - 博客园

mikel阅读(666)

来源: 乐观锁与悲观锁——解决并发问题 – WhyWin – 博客园

引言
为什么需要锁(并发控制)?

  在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。

典型的冲突有:

  • 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
  • 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。

为了解决这些并发带来的问题。 我们需要引入并发控制机制。

并发控制机制

  悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。[1]

  乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。

 

乐观锁应用

乐观锁介绍:

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。那么我们如何实现乐观锁呢,一般来说有以下2种方式:

1.使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。用下面的一张图来说明:

如上图所示,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中B)会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败。

 

2.乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

 

使用举例:以MySQL InnoDB为例

还是拿之前的实例来举:商品goods表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单,那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。

 

下单操作包括3步骤:

1.查询出商品信息

select (status,status,version) from t_goods where id=#{id}

2.根据商品信息生成订单

3.修改商品status为2

update t_goods 

set status=2,version=version+1

where id=#{id} and version=#{version};

那么为了使用乐观锁,我们首先修改t_goods表,增加一个version字段,数据默认version值为1。

t_goods表初始数据如下:

复制代码
mysql> select * from t_goods;
+----+--------+------+---------+
| id | status | name | version |
+----+--------+------+---------+
|  1 |      1 | 道具 |       1 |
|  2 |      2 | 装备 |       2 |
+----+--------+------+---------+
2 rows in set

mysql>
复制代码

 

对于乐观锁的实现,我使用MyBatis来进行实践,具体如下:

Goods实体类:

复制代码
/**
 * ClassName: Goods <br/>
 * Function: 商品实体. <br/>*/
public class Goods implements Serializable {

    /**
     * serialVersionUID:序列化ID.
     */
    private static final long serialVersionUID = 6803791908148880587L;
    
    /**
     * id:主键id.
     */
    private int id;
    
    /**
     * status:商品状态:1未下单、2已下单.
     */
    private int status;
    
    /**
     * name:商品名称.
     */
    private String name;
    
    /**
     * version:商品数据版本号.
     */
    private int version;
    
    @Override
    public String toString(){
        return "good id:"+id+",goods status:"+status+",goods name:"+name+",goods version:"+version;
    }

    //setter and getter

}
复制代码

 

GoodsDao

复制代码
/**
 * updateGoodsUseCAS:使用CAS(Compare and set)更新商品信息

 * @param goods 商品对象
 * @return 影响的行数
 */
int updateGoodsUseCAS(Goods goods);
复制代码

 

mapper.xml

复制代码
<update id="updateGoodsUseCAS" parameterType="Goods">
    <![CDATA[
        update t_goods
        set status=#{status},name=#{name},version=version+1
        where id=#{id} and version=#{version}
    ]]>
</update>
复制代码

 

 

GoodsDaoTest测试类

复制代码
@Test
public void goodsDaoTest(){
    int goodsId = 1;
    //根据相同的id查询出商品信息,赋给2个对象
    Goods goods1 = this.goodsDao.getGoodsById(goodsId);
    Goods goods2 = this.goodsDao.getGoodsById(goodsId);
    
    //打印当前商品信息
    System.out.println(goods1);
    System.out.println(goods2);
    
    //更新商品信息1
    goods1.setStatus(2);//修改status为2
    int updateResult1 = this.goodsDao.updateGoodsUseCAS(goods1);
    System.out.println("修改商品信息1"+(updateResult1==1?"成功":"失败"));
    
    //更新商品信息2
    goods1.setStatus(2);//修改status为2
    int updateResult2 = this.goodsDao.updateGoodsUseCAS(goods1);
    System.out.println("修改商品信息2"+(updateResult2==1?"成功":"失败"));
}
复制代码

 

输出结果:

good id:1,goods status:1,goods name:道具,goods version:1  
good id:1,goods status:1,goods name:道具,goods version:1  
修改商品信息1成功  
修改商品信息2失败

 

 

说明:

在GoodsDaoTest测试方法中,我们同时查出同一个版本的数据,赋给不同的goods对象,然后先修改good1对象然后执行更新操作,执行成功。然后我们修改goods2,执行更新操作时提示操作失败。此时t_goods表中数据如下:

复制代码
mysql> select * from t_goods;
+----+--------+------+---------+
| id | status | name | version |
+----+--------+------+---------+
|  1 |      2 | 道具 |       2 |
|  2 |      2 | 装备 |       2 |
+----+--------+------+---------+
2 rows in set

mysql>
复制代码

 

我们可以看到 id为1的数据version已经在第一次更新时修改为2了。所以我们更新good2时update where条件已经不匹配了,所以更新不会成功,具体SQL如下:

update t_goods 
set status=2,version=version+1
where id=#{id} and version=#{version};

这样我们就实现了乐观锁

 

悲观锁应用

需要使用数据库的锁机制,比如SQL SERVER 的TABLOCKX(排它表锁) 此选项被选中时,SQL  Server  将在整个表上置排它锁直至该命令或事务结束。这将防止其他进程读取或修改表中的数据。

SQLServer中使用

Begin Tran
select top 1 @TrainNo=T_NO
from Train_ticket   with (UPDLOCK)   where S_Flag=0

update Train_ticket
set T_Name=user,
T_Time=getdate(),
S_Flag=1
where T_NO=@TrainNo
commit

我们在查询的时候使用了with (UPDLOCK)选项,在查询记录的时候我们就对记录加上了更新锁,表示我们即将对此记录进行更新. 注意更新锁和共享锁是不冲突的,也就是其他用户还可以查询此表的内容,但是和更新锁和排它锁是冲突的.所以其他的更新用户就会阻塞.

结论

在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.

   致谢:感谢您的耐心阅读!

Android 虚拟多开系列一——技术调研 - Binary-Stream - 博客园

mikel阅读(1189)

来源: Android 虚拟多开系列一——技术调研 – Binary-Stream – 博客园

参考链接:http://weishu.me

Github源码链接:

            国内Xposed框架源码链接
                              VirtualApp:VirtualXposed 是基于 VirtualApp 和 epic 在 非root 环境下运行 Xposed 模块的实现(支持5.0——9.0)。
                              exposed   :exposed 致力于为 App 提供 Xposed 运行环境。
    框架实际应用
一、前情提要
Android插件技术事件:
            1. 2017年淘宝 Atlas 插件化项目的开源标志着插件化的落幕;
            2. 2018年 Android 9.0 上私有API的限制标志着:Android插件化技术进程必将要退出历史潮流。
如今的插件化技术可能的发展方向:
            1. 模块化/解耦被抽离,逐渐演进为稳定、务实的组件化方案;
            2. 插件化的黑科技特性被进一步发掘,inline hook / method hook大行其道,走向双开,虚拟环境;
二、概念解释
Android 插件化模块化组件化
        Android插件化:App中各个独立的功能模块都可以打包成apk,让宿主程序把apk加载进来,再运行里面的各个Activity,Service等。
                       http://www.cnblogs.com/android-blogs/p/5703355.html
        Android插件化作用:     (1). 模块解耦;
                                          (2). 动态升级(动态更新插件);
                                          (3). 高效并行开发(编译速度更快);
                                          (4). 按需加载,内存占用更低;
                                          (5). 节省升级流量(不用每次更新都下载一个完整的App);
                                          (6). 突破 Dex Max method 65535 上线 ;
        Android组件化:组件化和插件化的概念相差不大。较大的区别就是:组件是指通用及复用性较高的构件,如 图片缓存 就可以看成一个组件被多个App共用。(注:对整个App来说,其归类方式也不一样。插件是针对业务级的解耦框架(如:App的皮肤样式),组件则是针对功能级的代码框架(如:图片缓存、网络操作、数据库操作))
        Android组件化作用:     (1).  模块解耦;
                                          (2).  多重复用,避免重复造轮子;
                                          (3).  降低维护成本,提高开发效率;
        Android模块化:模块化与组件化一样,也是一种与平台无关的解耦手段,被广泛应用在架构层面。二者通过组合方式来使用。它们只是架构方面的一种思想,在代码的实现层面上没有多大区别。组件通常指的是底层模块,公共组件等。而模块既可表示上层的业务,也可表示组件中的某个业务功能,如图片组件中的缓存模块,下载模块。所以模块的应用范围更广。
        Android模块化作用:     (1).  业务模块解耦;
                                          (2).  降低维护成本,提高开发效率;
三、实际应用
  调研需求:虚拟多开工具软件开发
  调研结果:双开;     ✓
         一键发好友;  ✓
       一键发群;   ✓
       防消息撤回;  ✓
         防朋友圈删除; ✓
       ……
未完待续……

Android 虚拟多开系列二——技术原理 - Binary-Stream - 博客园

mikel阅读(1841)

来源: Android 虚拟多开系列二——技术原理 – Binary-Stream – 博客园

目录
        Android虚拟多开应用有哪些?
        Android虚拟多开应用技术原理有哪几类?
        Android虚拟多开需求分析
        反虚拟多开技术
正文
一、Android虚拟多开应用列表
应用名称 版本号 开源 公司名称 下载链接
太极 维术(个人) 微信公众号:虚拟框架
VirtualApp 2017.12月停止开源更新 罗盒科技 VirtualApp
LBE平行空间 LBE LBE平行空间
360分身大师 360 360分身大师
DroidPlugin 360 DroidPlugin
小米应用分身 小米 小米手机
双开助手 卓盟 双开助手
360奇酷手机 360 360奇酷手机
克隆大师 未知 克隆大师(项目已停)
二、Android虚拟多开应用技术原理
        1)修改APK(例:克隆大师)
        2)修改Framework(Android多用户机制。例:小米应用分身、360奇酷手机、等)
        3)通过虚拟化技术实现(例:360分身大师、LBE平行空间)
        4)以插件机制运行(例:DroidPlugin)
      A. 原理简述:
         现在市场上常见的虚拟多开应用主要是基于虚拟化技术实现,而虚拟化技术主要通过 Hook 技术实现,因此我们主要考察 Hook 技术。
         Hook技术的分类,在非虫写的《Android软件安全权威指南》第十章有详细描述,推荐。现总结如下:
         按 Java 层 与 Native 层分类, Hook 技术可以分为 Java 层的 Hook 与 Native 层的 Hook。根据代码的运行环境,Java层的 Hook 可以分为 Dalvik Hook 与 ART Hook。根据 ELF 文件的特点,Native层的 Hook 可以分为基于动态库加载劫持的 LD_PRELOAD Hook、针对 .got.plt 节区的 GOT Hook 及针对汇编指令级别的 Inline Hook。
         Hook技术主要分为五类:
                 a. Dalvik Hook;
                 b. ART Hook;
                 c. LOAD_PRELAD Hook;
                 d. GOT Hook;
                 e. Inline Hook;
         已有的Hook框架:
                 a. Xposed(支持Java层的 Dalvik Hook 和 ART Hook,但不支持 Native 层的 Hook)
                 b. legend(支持Java层的 Dalvik Hook 和 ART Hook)
                 e. epic
                 c. whale(跨平台的Hook框架,支持Java层的 Dalvik Hook 和 ART Hook)
         VirtualApp 采用了 Hook技术实现了在Android平台上的沙盒环境(容器),达到可以完全控制 其内运行的App的目的。
     B. 实践
         笔者主要看了 weishu 和  lody 对该技术的研究以及实现,因此主要讲这两位开发者对该问题的研究和实现。
             1) weishu的主要产品是 taichi,其产品发展图如下所示:
            2)lody主要产品是 VirtualApp,非常优秀的一款软件,weishu的 VirtualXpose 也是基于 VirtualApp 来实现的。
                 分析该软件原理的文章非常之多,也很专业,笔者在这里仅列出两篇实践应用的文章,用以实现 基于VirtualApp来hook其他第三方应用。
                 另:笔者通过借鉴以上两篇文章实现了 基于 VirtualApp 并结合 whale hook框架 来hook其他第三方应用的目的,在之后的文章将详述实现过程。感谢。
三、Android虚拟多开需求分析
       多账号同时运行;
   运营工具;
四、反虚拟多开技术
       VirtualApp使恶意应用具有免杀能力;
       VirtualApp给检测引擎识别恶意应用带来难度,因为其母包没有恶意应用,但可动态加载子包。
       相应的解决方案文中有详细描述。

小白也能看懂的插件化DroidPlugin原理(一)-- 动态代理 - codingblock - 博客园

mikel阅读(1022)

来源: 小白也能看懂的插件化DroidPlugin原理(一)– 动态代理 – codingblock – 博客园

  前言:插件化在Android开发中的优点不言而喻,也有很多文章介绍插件化的优势,所以在此不再赘述。前一阵子在项目中用到 DroidPlugin 插件框架 ,近期准备投入生产环境时出现了一些小问题,所以决心花些时间研究了一下 DroidPlugin 插件框架的原理,以便再出现问题时也能从容应对。打开源码后发现尽是大把大把的 hook、binder、classloader 等等,很难摸清头绪,幸运的是,有很多热心的大神已经对 DroidPlugin 的原理进行了透彻的剖析,文末会有本人对参考文章的致谢。

本系列文章的代码已经上传至github,下载地址:https://github.com/lgliuwei/DroidPluginStudy 本篇文章对应的代码在 com.liuwei.proxy_hook.proxy 包内。

· 代理模式

在 DroidPlugin 中用到了大量的动态代理,所以如果我们想理解 DroidPlugin 的原理,首先我们需要知道什么是动态代理,说到动态代理,我们难免会想起静态代理,那么代理是什么呢?

代理模式的意图是通过提供一个代理( Proxy )或者占位符来控制对该对象的访问。类比我们生活中,代理也是随处可见,其中中介就是一个很好的例子,把代理看做生活中的中介,将更加易于理解,试想一下,如果我们想租房或者买房的话通过中间是不是就可以让我们非常省心。

一、静态代理

为了保证与所代理的对象功能行为的一致性,代理类一般需要实现实体类所实现的同一个接口,以下即为一个最基本的代理模式的结构。

首先先定义一个接口,供实体类和代理类实现。(如:接口 Sbuject1 )

复制代码
1 /**
2  * Created by liuwei on 17/3/1.
3  */
4 public interface Subject1 {
5     void method1();
6     void method2();
7 }
复制代码

然后创建一个 Subject1 的实现类。

复制代码
 1 /**
 2  * 实体类
 3  * Created by liuwei on 17/3/1.
 4  */
 5 public class RealSubject1 implements Subject1 {
 6     @Override
 7     public void method1() {
 8         Logger.i(RealSubject1.class, "我是RealSubject1的方法1");
 9     }
10     @Override
11     public void method2() {
12         Logger.i(RealSubject1.class, "我是RealSubject1的方法2");
13     }
14 }
复制代码

再为 RealSubject1 创建一个代理类。

复制代码
 1 /**
 2  * 静态代理类
 3  * Created by liuwei on 17/3/1.
 4  */
 5 public class ProxySubject1 implements Subject1 {
 6     private Subject1 subject1;
 7     public ProxySubject1(Subject1 subject1) {
 8         this.subject1 = subject1;
 9     }
10     @Override
11     public void method1() {
12         Logger.i(ProxySubject1.class, "我是代理,我会在执行实体方法1之前先做一些预处理的工作");
13         subject1.method1();
14     }
15     @Override
16     public void method2() {
17         Logger.i(ProxySubject1.class, "我是代理,我会在执行实体方法2之前先做一些预处理的工作");
18         subject1.method2();
19     }
20 }
复制代码

可以发现,代理模式还是很简单的,很快我们就写好一个最基本的代理结构,接下来写个测试类跑一下看看效果。

复制代码
 1 /**
 2  * Created by liuwei on 17/3/1.
 3  */
 4 public class ProxyTest {
 5     public static void main(String[] args){
 6         // static proxy
 7         ProxySubject1 proxySubject1 = new ProxySubject1(new RealSubject1());
 8         proxySubject1.method1();
 9         proxySubject1.method2();
10 }
复制代码

输出结果非常简单,这里就不再贴出来了。我们看到,在测试类中只需要调用 ProxySubject1 的对像即可对实现对 RealSubject1 的操作。同时我们也发现在初始化 ProxySubject1 时需要传入 RealSubject1 的对象,当然,我们完全可以把获取 RealSubject1 的对象封装到代理类内部,这只是代理模式根据业务需要的不同体现而已。有很多人把这一点作为区分代理模式和适配器模式的依据,这个是不对的,由于本篇的重点是为插件化的原理做铺垫,至于代理模式和适配器模式的区别日后会专门写一篇文章介绍,这里就不细说了。

其实,从这个简单的示例中也许并没有体现出代理模式的优势,而且还要多创建一个代理类,反而看起来好像更麻烦了。其实代理模式很明显的好处就是通过代理,可以控制对实体对象的访问,从而提高了安全性。而且可以在调用实体类的方法时做一些预处理和善后的工作,这样就保证了实体类可以抛开复杂的业务逻辑而只去实现一些最纯粹的功能,提高了代码的可读性和灵活性。

二、动态代理

动态代理是本文的重点,也是 DroidPlugin 插件化框架的基础。动态代理乍一听起来好像也挺高大上的,但幸运的是,它并没有我们想象中那么高深莫测,所以我们大可不必对它有任何的畏惧之感。

假设我们在上文静态代理的例子中又多了一个 RealSubject2 的类,它实现的接口是 Subject2,这时候我们如果想对 RealSubject2 进行代理需要如何做?这个简单,我们直接类比 ProxySubject1 再创建一个 ProxySubject2 即可,这样是可以的,但如果有非常多的实体类并且都实现了不同的接口,那我们岂不是需要创建很多的代理类:ProxySubject1,ProxySubject2 … ProxySubjectN!还有没有更优雅一些的方法?答案是肯定的,动态代理即可解决这个问题。(当然,这并不是动态代理唯一的优点)

动态代理是在实现阶段不需要关心代理谁,在运行阶段才指定代理对象。创建一个动态代理类很简单,JDK已经给我们提供好了动态代理接口  InvocationHandler 我们只需要实现它即可创建一个动态代理类,以下是一个简单的小例子:

复制代码
 1 /**
 2  * 动态代理
 3  * Created by liuwei on 17/3/1.
 4  * 注:动态代理的步骤:
 5  *  1、写一个InvocationHandler的实现类,并实现invoke方法,return method.invoke(...);
 6  *  2、使用Proxy类的newProxyInstance方法生成一个代理对象。例如:生成Subject1的代理对象,注意第三个参数中要将一个实体对象传入
 7  *          Proxy.newProxyInstance(
 8                          Subject1.class.getClassLoader(),
 9                          new Class[] {Subject1.class},
10                          new DynamicProxyHandler(new RealSubject1()));
11 
12  */
13 public class DynamicProxyHandler implements InvocationHandler {
14     private Object object;
15 
16     public DynamicProxyHandler(Object object) {
17         this.object = object;
18     }
19 
20     @Override
21     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
22         Logger.i(DynamicProxyHandler.class, "我正在动态代理[" + object.getClass().getSimpleName() + "]的[" + method.getName() + "]方法");
23         return method.invoke(object, args);
24     }
25 
26     /**
27      * 调用Proxy.newProxyInstance即可生成一个代理对象
28      * @param object
29      * @return
30      */
31     public static Object newProxyInstance(Object object) {
32         // 传入被代理对象的classloader,实现的接口,还有DynamicProxyHandler的对象即可。
33         return Proxy.newProxyInstance(object.getClass().getClassLoader(),
34                 object.getClass().getInterfaces(),
35                 new DynamicProxyHandler(object));
36     }
37 }
复制代码

这是一个名为 DynamicProxyHandler 的动态代理类,其中 invoke 方法完成了对代理对象方法的调用,是必须实现的。接下来使用此类代理其他的实体类也非常简单,只需使用 Proxy 的newProxyInstance() 方法并传入相应的参数即可获取一个代理对象,接下来我们在测试类里面添加一下代码,代码如下:

复制代码
 1 /**
 2  * Created by liuwei on 17/3/1.
 3  */
 4 public class ProxyTest {
 5     public static void main(String[] args){
 6         // static proxy
 7         ProxySubject1 proxySubject1 = new ProxySubject1(new RealSubject1());
 8         proxySubject1.method1();
 9         proxySubject1.method2();
10 
11         // 如果想对RealSubject2代理显然不得不重新再写一个代理类。
12         ProxySubject2 proxySubject2 = new ProxySubject2(new RealSubject2());
13         proxySubject2.method1();
14         proxySubject2.method2();
15 
16         Logger.i(ProxyTest.class, "----------分割线----------\n");
17 
18         // 如果写一个代理类就能对上面两个都能代理就好了,动态代理就解决了这个问题
19         Subject1 dynamicProxyHandler1 = (Subject1) DynamicProxyHandler.newProxyInstance(new RealSubject1());
20         dynamicProxyHandler1.method1();
21         dynamicProxyHandler1.method2();
22 
23         Subject2 dynamicProxyHandler2 = (Subject2)DynamicProxyHandler.newProxyInstance(new RealSubject2());
24         dynamicProxyHandler2.method1();
25         dynamicProxyHandler2.method2();
26     }
27 }
复制代码

输出结果非常简单,这里不再给出。

三、小结

至此,相信我们对动态代理已经有一个基本的认识,其实代理模式除了上文中提到的普通代理(静态代理的一种)、动态代理之外还有很多种方式,如远程代理、虚拟代理、智能代理等等,这里就不一一介绍了。

其实插件化的原理简单来说是使用动态代理,通过反射等机制将系统中的一些方法hook掉,从而达到劫持系统方法的目的以实现对系统方法的篡改。例如通过 hook 掉 AMS 的 startActivity 方法来启动一个没有在清单文件中配置的 Activity 。下一篇文章将详细介绍 Hook 机制,以及反射在 Hook 中的实际体现。

  致谢:最后我想说的是“吃水不忘挖井人”!非常感谢术哥《Android插件化原理解析——概要》系列文章,本人正是在参考了这些内容的思路之后才有能力写下本系列文章。本人在Android的插件化领域可以说算是一个小白,写下本系列文章的目的一方面是在实践中加深自己的理解,另一方面是根据本人以小白角度对插件化原理的体会用更加简单易懂的方式传达出来,从而帮助小白也能读懂插件化!

本文链接:http://www.cnblogs.com/codingblock/p/6580364.html

作者:CodingBlock
出处:http://www.cnblogs.com/codingblock/
本文版权归作者和共博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如果文中有什么错误,欢迎指出。以免更多的人被误导。