Fork me on GitHub
Wang's Blog


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

分布式事务(二):基于可靠消息的分布式事务

发表于 2018-08-31 | 分类于 Java |

项目使用技术

springboot、dubbo、zookeeper、定时任务

一、项目结构

maven父子工程:

父工程:consis

子工程:api-service、order、product、message

api-service:该项目主要是提供接口调用的,还包含实体类、枚举等一些通用内容

order:该项目是专门处理订单相关操作的系统

product:该项目是专门处理产品相关操作的系统

message:该项目是提供消息服务的系统,好包括定时任务

它们的依赖关系如下图:

enter image description here

根据上一篇的原理分别介绍下各个系统的实现

二、order订单系统

核心代码:

@Override
@Transactional
public void add(Orders order) {
    String messageBody = JSONObject.toJSONString( order );
    //添加消息到数据库
    String messageId = transactionMessageService.savePreparMessage(order.getMessageId(), messageBody, Constant.ORDER_QUEUE_NAME );
    log.info(">>> 预发送消息,消息编号:{}", messageId);
    boolean flag = false;
    boolean success = false;
    try{

        Orders orders = orderDao.saveAndFlush( order );
        //int i = 1/0 ;
        log.info(">>> 插入订单,订单编号:{}", orders.getId());
        flag = true;
    }catch (Exception e){
        transactionMessageService.delete( messageId );
        log.info(">>> 业务执行异常删除消息,消息编号:{}", messageId, e);
        throw new RuntimeException( ">>> 创建订单失败" );
    }finally {
        if(flag){
            try {
                transactionMessageService.confirmAndSend( messageId );
                success = true;
                log.info(">>> 确认并且发送消息到实时消息中间件,消息编号:{}", messageId);

            }catch (Exception e){
                log.error(">>> 消息确认异常,消息编号:{}", messageId, e);
                if(!success){
                    transactionMessageService.delete( messageId );
                    throw new RuntimeException( ">>> 确认消息异常,创建订单失败" );
                }
            }
        }
    }
}
  • 插入订单表之前,首先创建预发送消息,保存到事务消息表中,此时消息状态为:未发送
  • 插入订单,如果插入订单失败则将事务消息表中预发送消息删除
  • 插入订单成功后,修改消息表预发送消息状态为发送中,并发送消息至mq
  • 如果发送消息失败,则订单回滚并删除事务消息表消息

三、message消息系统

核心代码一:

@Override
public void sendMessageToMessageQueue(String queueName,final String messageBody) {

    jmsTemplate.convertAndSend( queueName,messageBody );

    log.info(">>> 发送消息到mq 队列:{},消息内容:{}", queueName, messageBody);
}
  • 主要是activemq生产者讲消息发送至MQ消息中间件

核心代码二:

/**
 * 定时重发消息(每分钟)
 */
@Scheduled(cron = "0 */1 * * * ?")
public void    handler(){
    //查询transaction_message表中已发送但未被删除的消息
    List<TransactionMessage> list = transactionMessageService.queryRetryList( Constant.MESSAGE_UNDEAD, maxTimeOut, Constant.MESSAGE_SENDING );
    if(list!=null && list.size() > 0){
        for (TransactionMessage message:list){
            try {
                transactionMessageService.retry( message.getMessageId() );
            } catch (Exception e) {
                log.warn(">>> 消息不存在,可能已经被消费,消息编号:{}", message.getMessageId());
            }
        }
    }
}

/**
 * 定时通知工作人员(每隔5分钟)
 */
@Scheduled(cron = "0 */5 * * * ?")
public void    advance(){
    List<Long> messages = transactionMessageService.queryDeadList();
    log.warn(">>> 共有:{}条消息需要人工处理", messages.size());
    String ids = JSONObject.toJSONString( messages );
    //发邮件或者是发送短信通知工作人员处理

}
  • 定时重发消息
  • 定时将死亡的消息通知给工作人员,进行人工补偿操作

四、product产品系统

核心代码:

@Transactional
@JmsListener( destination = Constant.ORDER_QUEUE_NAME)
public void    receiveQueue(String msg){
    boolean flag = false;
    Orders orders = JSONObject.parseObject( msg, Orders.class );
    log.info(">>> 接收到mq消息队列,消息编号:{} ,消息内容:{}", orders.getMessageId(), msg);

    TransactionMessage transactionMessage = transactionMessageService.findByMessageId( orders.getMessageId() );
    try {
        //保证幂等性
        if(transactionMessage!=null){
            List<OrderDetail> list = orders.getList();
            for(OrderDetail detail : list){
                Product product = productService.findById( detail.getId() );
                Long skuNum = product.getProductSku() - detail.getNum();
                if(skuNum >= 0){
                    product.setProductSku( skuNum );
                    productService.update( product );
                }else {
                    throw new Exception( ">>> 库存不足,修改库存失败!" );
                }

            }
            //int i = 1 /0 ;
            flag = true;
        }

    }catch (Exception e){
        e.printStackTrace();
        throw new RuntimeException( e );
    }finally {
        if(flag){
            transactionMessageService.delete( orders.getMessageId() );
            DbLog dbLog = dbLogService.findByMesageId( orders.getMessageId() );
            if(dbLog!=null){
                dbLog.setState( "1" );//已处理成功
                dbLogService.update( dbLog );
            }
            log.info(">>> 业务执行成功删除消息! messageId:{}", orders.getMessageId());
        }
    }

}
  • 从mq消息中间件中监听并消费消息,将json消息转为订单对象
  • 根据消息编号查询该消息是否已被消费,保证幂等性
  • 如果消息未被消费(即存在此消息),则产品表扣减库存;如果已经消费(不存在此消息),则不做处理
  • 产品表扣减库存成功,则删除此消息,如果待处理消息日志表中有此消息,则更改状态为1,表示已处理;扣减失败,则不做处理

该项目源码已上传至github和码云,链接如下,希望喜欢的朋友都能给个star支持一下!谢谢~

github链接

https://github.com/wanglinyong/consis

码云链接

https://gitee.com/wanglinyong/consis

分布式事务(一)

发表于 2018-08-27 | 分类于 Java |

一、数据库事务

在介绍分布式事务之前,先简单了解下数据库事务。

数据库事务(Database Transaction) :是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。

事务具有ACID四个特性:

1、原子性(Atomic):事务必须是原子工作单元,对于其修改数据,要么全部成功,要么全部失败。

2、一致性(Consistent):事务在完成时,必须使所有的数据都保持一致状态。

3、隔离性(Insulation):由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。

4、持久性(Duration):事务完成之后,它对于系统的影响是永久性的。

二、分布式事务

在分布式系统中,不止使用一个数据库,比如订单系统使用db_order数据库,产品系统使用的是db_product数据库,在订单系统中只能保证订单相关操作的事务,在产品系统中只能保证产品相关操作的事务。比如:如果在订单系统中进行生成订单、扣减库存的业务,如果出现异常,那么创建订单的事务会回滚,而扣减库存的事务则不会,因为本地事务是不能夸数据库的。跨库的事务就属于分布式事务。

把分布式系统中两个相关操作看成是一个单元,比如创建订单和修改库存的操作,该单元要么一起成功,要么一起失败,这就是分布式事务。

关于分布式事务你不得不知的两个理论:

1、CAP定理

CAP原则又称CAP定理,指的是在一个分布式系统中,WEB服务无法同时满足以下3个特性:

  • 一致性(Consistency) : 在分布式系统中数据一旦更新,所有数据变动都是同步的

  • 可用性(Availability) : 好的响应性能,每个操作都必须有预期的响应结束

  • 分区容错性(Partition tolerance) : 在网络分区的情况下,即使出现单个节点无法可用,系统依然正常对外提供服务

首先在分布式系统中,横向扩展策略依赖于数据分区,所以一般会在一致性和可用性上做出牺牲。

2、BASE理论

BASE理论中的三个特性:

  • Basically Available(基本可用)

  • Soft state(软状态)

  • Eventually consistent(最终一致性)

三个特性分别指的是:

(1)基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用。

(2)软状态,和硬状态对应,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时。

(3)最终一致性强调的是系统所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要试试保证系统数据的强一致性。

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

三、分布式事务解决方案

主要介绍目前主流的两种分布式事务解决方案:

1、TCC-补偿型事务解决方案

TCC指的是Try(尝试)、Confirm(确认)、Cancle(取消),其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  • Try 阶段主要是对业务系统做检测及资源预留

  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。

  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

举个例子,假入 Bob 要向 Smith 转账,思路大概是:

1、首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。

2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。

3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

优点: 跟两阶段提交(2PC)比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些

缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。

2、基于可靠消息的最终一致性解决方案

举例说明:订单系统创建订单成功后、产品系统扣减相应库存

订单系统流程图如下:

enter image description here

1.创建订单之前,创建预发送消息,保存到消息表中,此时消息状态为:未发送

2.创建订单,如果创建订单失败则将消息表预发送消息删除

3.创建订单成功后,修改消息表预发送消息状态为发送中,并发送消息至mq

4.如果发送消息失败,则订单回滚并删除消息表消息;发送成功则万事大吉

产品系统流程图如下:

enter image description here

1.从mq消息中间件中监听并消费消息,将json消息转为订单对象

2.根据消息编号查询该消息是否已被消费,保证幂等性

3.如果消息未被消费(即存在此消息),则产品表扣减库存;如果已经消费(不存在此消息),则不做处理

4.产品表扣减库存成功,则删除此消息;扣减失败,则不做处理

5.定时任务会定时扫描消息表中超时未被消费的消息,然后尝试重发,如果超过最大重试次数后仍未被消费,则记录日志并通知工作人员进行人工补偿操作

基于可靠消息的分布式事务虽然不能保证结果的强一致,但是可以通过可靠消息使得结果最终一致

下一节将给出基于可靠消息的分布式事务demo源码

dubbo快速入门

发表于 2018-08-21 | 分类于 Java |

一、dubbo基本原理

dubbo架构图如下:

enter image description here

节点角色说明

enter image description here

调用关系说明:

0.服务容器负责启动,加载,运行服务提供者。

1.服务提供者在启动时,向注册中心注册自己提供的服务。

2.服务消费者在启动时,向注册中心订阅自己所需的服务。

3.注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

4.服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

5.服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

具体详细内容请参加dubbo官方文档

http://dubbo.apache.org/en-us/

二、入门demo简介

该demo包含三个项目,分别是:

服务提供端项目:provider

服务消费端项目:consumer

共用服务接口项目:api

依赖关系如图:

enter image description here

api项目是一个jar项目,将生成的jar打包到maven仓库,作为provider和consumer项目的依赖导入。

三、dubbo-远程调用(RPC)具体实现

1、启动zookeeper

在启动服务提供者项目之前要先启动zookeeper,因为我们需要把服务提供方注册到zookeeper中,然后才能被服务消费方发现并调用。

关于zookeeper这里不做详细介绍了,简单概括就是分布式服务治理框架,用于服务注册与发现,实现远程调用功能等。

  • zookeeper下载

    http://zookeeper.apache.org/

  • 解压并修改配置

zookeeper下载完成后解压到合适目录,然后进入到zookeeper的conf目录,找到zoo_sample.cfg文件将其重命名为zoo.cfg,然后打开该文件,
将其中的dataDir和dataLogDir修改如下:

dataDir=E:\\Soft\\zookeeper-3.4.6\\data
dataLogDir=E:\\Soft\\zookeeper-3.4.6\\log

修改为你本地硬盘的目录

  • 启动zookeeper

进入到zookeeper的bin目录下,双击zkServer.cmd,启动zookeeper

2.dubbo接口服务api项目

新建maven项目api,注意将其pom.xml中的打包方式改为jar

然后在com.dubbo.api.service包下新建DemoService接口,如下:

public interface DemoService {

    String sayHello(String name);
}

项目结构如图:

enter image description here

然后将项目打包(install一下即可),在E:\temp\api\target\目录下找到该jar,然后打包到本地maven仓库,打开cmd命令窗口,输入以下令如下:

mvn install:install-file -Dfile=E:\temp\api\target\api.jar -DgroupId=com.dubbo.api -DartifactId=api -Dversion=1.0.0 -Dpackaging=jar -DgeneratePom=true -DcreateChecksum=true

注意:jar路径该为你自己的路径

3.dubbo服务提供者provider项目

新建maven项目provider,并在pom.xml中添加以下依赖:

 <dependency>
  <groupId>com.dubbo.api</groupId>
  <artifactId>api</artifactId>
  <version>1.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/dubbo -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>dubbo</artifactId>
  <version>2.5.3</version>
</dependency>

<dependency>
  <groupId>com.101tec</groupId>
  <artifactId>zkclient</artifactId>
  <version>0.10</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>4.0.5.RELEASE</version>
</dependency>

这里引入了上面的生成的api接口依赖、dubbo依赖、zookeeper客服端依赖以及启动项目必需的依赖

然后在com.dubbo.provider.service.impl包下新建DemoServiceImp并实现DemoService接口,如下:

public class DemoServiceImpl implements DemoService{
@Override
public String sayHello(String name) {
    return "Hello " + name;
}

然后在resources下新建provider.xml文件,配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
   http://code.alibabatech.com/schema/dubbo
   http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!--定义了提供方应用信息,用于计算依赖关系;在 dubbo-admin 或 dubbo-monitor 会显示这个名字,方便辨识-->
<dubbo:application name="demo-provider" owner="wly" organization="dubbox"/>
<!--使用 zookeeper 注册中心暴露服务,注意要先开启 zookeeper-->
<dubbo:registry address="zookeeper://localhost:2181"/>
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!--使用 dubbo 协议实现定义好的 api.PermissionService 接口-->
<dubbo:service interface="com.dubbo.api.service.DemoService" ref="demoService" protocol="dubbo" />
<!--具体实现该接口的 bean-->
<bean id="demoService" class="com.dubbo.provider.service.impl.DemoServiceImpl"/>
</beans>

然后在java下直接新建Provider用于启动项目:

public class Provider {
public static void main(String[] args) throws IOException {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("provider.xml");
    context.start();
    System.out.println("dubbo服务提供端已启动....");
    System.in.read(); // 按任意键退出
}
}

provider项目结构如图:

enter image description here

然后直接运行该main函数-dubbo服务提供者已启动!

4.dubbo服务消费者consumer项目

新建maven项目consumer,并在pom.xml中添加以下依赖:

 <dependency>
  <groupId>com.dubbo.api</groupId>
  <artifactId>api</artifactId>
  <version>1.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/dubbo -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>dubbo</artifactId>
  <version>2.5.3</version>
</dependency>
<dependency>
  <groupId>com.101tec</groupId>
  <artifactId>zkclient</artifactId>
  <version>0.10</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>4.0.5.RELEASE</version>
</dependency>

和provider项目添加的依赖一样。

然后在resources下新建comsumer.xml,配置服务消费者,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
   http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-consumer" owner="wly" organization="dubbox"/>
<!--向 zookeeper 订阅 provider 的地址,由 zookeeper 定时推送-->
<dubbo:registry address="zookeeper://localhost:2181"/>
<!--使用 dubbo 协议调用定义好的 api.PermissionService 接口-->
<dubbo:reference id="demoService" interface="com.dubbo.api.service.DemoService"/>
</beans>

然后在java下新建Consumer.class用于启动该项目,如下:

public class Consumer {
public static void main(String[] args) throws IOException {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( "consumer.xml" );
    context.start();
    System.out.println("dubbo服务消费端已启动...");
    DemoService demoService = (DemoService)context.getBean( "demoService" );// 获取远程服务代理
    String hello = demoService.sayHello( "world" );//执行远程方法
    System.out.println(hello);//显示调用结果
    System.in.read(); // 按任意键退出
}
}

启动该项目,显示调用结果,如图:

enter image description here

到这里就已经完成了dubbo的远程调用。下面介绍下dubbo-admin管理管理控制台

四、dubbo-admin管理控制台

通过dubbo-admin可以更好的管理dubbo服务。

首先下载dubbo-admin的war包,下载地址:

http://www.java1234.com/a/javabook/javaweb/2018/0224/10496.html

下载完成后,将dubbo-admin-2.6.0.war复制到tomcat的webapps目录下。然后启动tomcat,访问

http://localhost:8080/dubbo-admin-2.6.0/

也可以将dubbo-admin-2.6.0重命名为dubbo-admin访问的时候直接访问http://localhost:8080/dubbo-admin即可

用户名和密码都是root

登录后就可以通过网页进行管理dubbo服务了,如图:

enter image description here

该demo项目以及相关资源下载链接

https://download.csdn.net/download/abcwanglinyong/10617369

Solr工作原理

发表于 2018-07-13 | 分类于 Solr |

Solr简介

Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口。用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件,生成索引;也可以通过Http Get操作提出查找请求,并得到XML格式的返回结果。

要想知道solr的实现原理,首先得了解什么是全文检索、solr的索引创建过程和索引搜索过程。

一、全文检索

首先举个例子:比如现在有5个文档,我现在想从5个文档中查找出包含”solr工作原理”的文档,此时有两种做法:

1.顺序扫描法:对5个文档依次查找,包含目标字段的文档就记录下来,最后查找的结果可能是在2,3文档中,这种查找方式叫做顺序扫描法。

顺序扫描法在文档数量较少的情况下,查找速度还是很快的,但是当文档数量很多时,查找速度就差强人意了。

2.全文检索:对文档内容进行分词,对分词后的结果创建索引,然后通过对索引进行搜索的方式叫做全文检索。

全文检索就相当于根据偏旁部首或者读音去查找字典,在文档很多的情况,这种查找速度肯定比你一个一个文档查找要快。

二、索引创建和搜索过程

1.创建索引

举例子:

文档一:solr是基于Lucene开发的企业级搜索引擎技术

文档二:Solr是一个独立的企业级搜索应用服务器,Solr是一个高性能,基于Lucene的全文搜索服务器

首先经过分词器分词,solr会为分词后的结果(词典)创建索引,然后将索引和文档id列表对应起来,如下图所示:

enter image description here

比如:solr在文档1和文档2中都有出现,所以对应的文档ID列表中既包含文档1的ID也包含文档2的ID,文档ID列表对应到具体的文档,并体现该词典在该文档中出现的频次,频次越多说明权重越大,权重越大搜索的结果就会排在前面。

solr内部会对分词的结果做如下处理:

1.去除停词和标点符号,例如英文的this,that等, 中文的”的”,”一”等没有特殊含义的词

2.会将所有的大写英文字母转换成小写,方便统一创建索引和搜索索引

3.将复数形式转为单数形式,比如students转为student,也是方便统一创建索引和搜索索引

2.索引搜索过程

知道了创建索引的过程,那么根据索引进行搜索就变得简单了。

1.用户输入搜索条件

2.对搜索条件进行分词处理

3.根据分词的结果查找索引

4.根据索引找到文档ID列表

5.根据文档ID列表找到具体的文档,根据出现的频次等计算权重,最后将文档列表按照权重排序返回

Spring-Security登录认证授权原理

发表于 2018-07-10 | 分类于 Java |

spring-security源码下载地址:

https://github.com/spring-projects/spring-security

Spring-Security源码解读:

1.使用ctrl+shift+n组合键查找UsernamePasswordAuthenticationFilter过滤器,该过滤器是用来处理用户认证逻辑的,进入后如图:

(1)可以看到它默认的登录请求url是”/login”,并且只允许POST方式的请求

(2)obtainUsername()方法点进去发现它默认是根据参数名为”username”和”password”来获取用户名和密码的

(3)通过构造方法实例化一个UsernamePasswordAuthenticationToken对象,此时调用的是UsernamePasswordAuthenticationToken的两个参数的构造函数,如果点击进不去,可直接用ctrl+shift+n查找(等依赖自动下载完成就可以跟进了),如图:

其中super(null)调用的是父类的构造方法,传入的是权限集合,因为目前还没有认证通过,所以不知道有什么权限信息,这里设置为null,然后将用户名和密码分别赋值给principal和credentials,同样因为此时还未进行身份认证,所以setAuthenticated(false)

(4)setDetails(request, authRequest)是将当前的请求信息设置到UsernamePasswordAuthenticationToken中

(5)通过调用getAuthenticationManager()来获取AuthenticationManager,通过调用它的authenticate方法来查找支持该token(UsernamePasswordAuthenticationToken)认证方式的provider,然后调用该provider的authenticate方法进行认证)

2.AuthenticationManager是用来管理AuthenticationProvider的接口,通过查找后进入,然后使用ctrl+H组合键查看它的继承关系,找到ProviderManager实现类,它实现了AuthenticationManager接口,查看它的authenticate方法,它里面有段这样的代码:

for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }
    ...
    try {
            result = provider.authenticate(authentication);
    ...
    }
}

通过for循环遍历AuthenticationProvider对象的集合,找到支持当前认证方式的AuthenticationProvider,找到之后调用该AuthenticationProvider的authenticate方法进行认证处理:

result = provider.authenticate(authentication);

3.AuthenticationProvider接口,就是进行身份认证的接口,它里面有两个方法:authenticate认证方法和supports是否支持某种类型token的方法,通过ctrl+h查看继承关系,找到AbstractUserDetailsAuthenticationProvider抽象类,它实现了AuthenticationProvider接口,它的supports方法如下:

public boolean supports(Class<?> authentication) {
    return (UsernamePasswordAuthenticationToken.class
        .isAssignableFrom(authentication));
    }

说明它是支持UsernamePasswordAuthenticationToken类型的AuthenticationProvider

再看它的authenticate认证方法,其中有一段这样的代码:

boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
        cacheWasUsed = false;

        try {
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
    ...
    }

如果从缓存中没有获取到UserDetails,那么它调用retrieveUser方法来获取用户信息UserDetails,这里的retrieveUser是抽象方法,等一会我们看它的子类实现。

用户信息UserDetails是个接口,我们进入查看,它包含以下6个接口方法:

Collection<? extends GrantedAuthority> getAuthorities();//获取权限集合
String getPassword();  //获取密码
String getUsername();    //获取用户名
boolean isAccountNonExpired(); //账户未过期
boolean isAccountNonLocked();    //账户未锁定
boolean isCredentialsNonExpired(); //密码未过期
boolean isEnabled();    //账户可用

查看它的继承关系发现User类实现了该接口,并实现了该接口的所有方法

接着AbstractUserDetailsAuthenticationProvider往下看,找到下面的代码:

preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
            (UsernamePasswordAuthenticationToken) authentication);

preAuthenticationChecks预检查,在最下面的内部类DefaultPreAuthenticationChecks中可以看到,它会检查上面提到的三个boolean方法,即检查账户未锁定、账户可用、账户未过期,如果上面的方法只要有一个返回false,就会抛出异常,那么认证就会失败。

additionalAuthenticationChecks是附加检查,是个抽象方法,等下看子类的具体实现。

下面还有个postAuthenticationChecks.check(user)后检查,在最下面的DefaultPostAuthenticationChecks内部类中可以看到,它会检查密码未过期,如果为false就会抛出异常

如果上面的检查都通过并且没有异常,表示认证通过,会调用下面的方法:

createSuccessAuthentication(principalToReturn, authentication, user);

跟进发现此时通过构造方法实例化对象UsernamePasswordAuthenticationToken时,调用的是三个参数的构造方法:

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
        Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true); // must use super, as we override
}

此时会调用父类的构造方法设置权限信息,并调用父类的setAuthenticated(true)方法,到这里就表示认证通过了。

下面我们看看AbstractUserDetailsAuthenticationProvider的子类,同ctrl+h可查看继承关系,找到DaoAuthenticationProvider

4.DaoAuthenticationProvider类

(1)查看additionalAuthenticationChecks附加检查方法,它主要是检查用户密码的正确性,如果密码为空或者错误都会抛出异常

(2)获取用户信息UserDetails的retrieveUser方法,主要看下面这段代码:

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

它是调用了getUserDetailsService先获取到UserDetailsService对象,通过调用UserDetailsService对象的loadUserByUsername方法获取用户信息UserDetails

找到UserDetailsService,发现它是一个接口,查看继承关系,有很多实现,都是spring-security提供的实现类,并不满足我们的需要,我们想自己制定获取用户信息的逻辑,所以我们可以实现这个接口。比如从我们的数据库中查找用户信息

5.SecurityContextPersistenceFilter过滤器

那么用户认证成功之后,又是怎么保存认证信息的呢,在下一次请求过来是如何判断该用户是否已经认证了呢?

请求进来时会经过SecurityContextPersistenceFilter过滤器,进入SecurityContextPersistenceFilter过滤器并找到以下代码:

SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

从session中获取SecurityContext对象,如果没有就实例化一个SecurityContext对象

SecurityContextHolder.setContext(contextBeforeChainExecution);

将SecurityContext对象设置到SecurityContextHolder中

chain.doFilter(holder.getRequest(), holder.getResponse());

表示放行,执行下一个过滤器

执行完后面的过滤并经过servlet处理之后,响应给浏览器之前再次经过此过滤器。查看以下代码:

SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());

通过SecurityContextHolder获取SecurityContext对象,然后清除SecurityContext,最后将获取的SecurityContext对象放入session中

其中SecurityContextHolder是与ThreadLocal绑定的,即本线程内所有的方法都可以获得SecurityContext对象,而SecurityContext对象中包含了Authentication对象,即用户的认证信息,spring-security判断用户是否认证主要是根据SecurityContext中的Authentication对象来判断。Authentication对象的详细信息如图:

最后整个过程的流程大致如下图:

读取properties文件内容最简单的两种方式

发表于 2018-07-03 | 分类于 Java |

一、使用ResourceBundle读取jdbc.properties文件

public static void main(String[] args) {
    ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
    String value= bundle.getString("driverClasss");
    System.out.println(value);
}

结果如下:

com.mysql.jdbc.Driver

二、自定义工具类PropertiesUtils读取jdbc.properties文件:

1.PropertiesUtils代码如下:

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
/**
 * Created by wly on 2018/7/3.
 */
public class  {
private static Properties props = new Properties();

public static String getValue(String key) {
    return props.getProperty(key);
}

public static void updateProperties(String key, String value) {
    props.setProperty(key, value);
}

static {
    try {
        props.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("jdbc.properties"));
    } catch (FileNotFoundException var1) {
        var1.printStackTrace();
    } catch (IOException var2) {
        var2.printStackTrace();
    }

}
}

2.main方法测试

public static void main(String[] args) {
    String name = getValue( "driverClasss" );
    System.out.println("mysql的driverClasss==="+name);
    updateProperties( "driverClasss","oracle.jdbc.driver.OracleDriver" );
    name = getValue( "driverClasss" );
    System.out.println("修改成Oracle的driverClasss===="+name);
}

测试结果:

mysql的driverClasss===com.mysql.jdbc.Driver

修改成Oracle的driverClasss====oracle.jdbc.driver.OracleDriver

一个是根据key获取对应的value值

一个是根据key值设置对应的value值,注意不会改变propertis文件内容

AccessDeniedException如何处理

发表于 2018-07-02 | 分类于 Java |

有两种情况:

1.用户未登录情况下访问受保护资源

2.用户登录情况下访问被保护资源

一、用户未登录情况下访问受保护资源

用户在未登录的情况下访问受保护资源时会自动跳转到配置的登录页面。主要是以下这个配置起作用:

<security:http auto-config="false" access-decision-manager-ref="accessDecisionManager"
               use-expressions="true" entry-point-ref="loginEntryPoint">

 <bean id="loginEntryPoint"
    class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <!-- 默认登录页的url -->
    <constructor-arg value="/login?error=login"/>
</bean>

</security:http>

但是这里说的访问路径不包含ajax请求的访问,如果是ajax请求的话,你会看到后台报AccessDeniedException异常,而前台没反应。因为对ajax请求不起作用

此时你可以这样来解决,在发送ajax请求的页面判断用户是否为空,为空则直接跳转到登录页面,如下:

 var res = '${user.id}';
    if(res=='' || res==null){
        window.location.href = "/login.jsp";
    }
    $.ajax({
    ...
});
}

登录情况下的ajax请求我们下面介绍

二、用户在登录情况下访问受保护资源

用户在登录情况下访问受保护资源时,可以有两种解决方案:

1.配置403错误页面(有瑕疵)

因为我们知道访问受保护资源时会返回403错误码,我们可以直接配置403错误页面

在web.xml中配置错误页面:

<error-page>
    <error-code>403</error-code>
    <location>/WEB-INF/403.jsp</location>
</error-page>

在/WEB-INF/下引入403.jsp即可

此时访问受保护资源时会直接跳转到我们配置的403错误页面

但是问题又来了,ajax请求访问受保护资源时同样没有反应,且看下面的解决办法

2.自定义MyAccessDeniedHandler拒绝访问处理器(完美)

1.新建MyAccessDeniedHandler并实现AccessDeniedHandler接口:

public class MyAccessDeniedHandler implements AccessDeniedHandler{
private String errorPage;
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
    String header = request.getHeader( "X-Requested-With" );
    if (header != null && "XMLHttpRequest".equals( header )) {
        //ajax请求
        String jsonObject = "{\"access-denied\":true}";
        PrintWriter out = response.getWriter();
        out.print( jsonObject );
        out.flush();
        out.close();
        return;
    } else {
        RequestDispatcher dispatcher = request.getRequestDispatcher( errorPage );
        dispatcher.forward( request, response );
    }
}

public void setErrorPage(String errorPage) {
    if(errorPage != null && !errorPage.startsWith("/")) {
        throw new IllegalArgumentException("errorPage must begin with '/'");
    } else {
        this.errorPage = errorPage;
    }
}
}

(1)通过setter方法获取配置文件中配置的错误页面

(2)根据请求头信息判断该请求是不是ajax请求

(3)如果是ajax请求,则返回json格式数据{access-denied:true}

(4)如果不是ajax请求则将请求转发到配置的错误页面

2.spring-security.xml配置自定义的AccessDeniedHandler:

<security:access-denied-handler ref="accessDeniedHandler"/>
<bean id="accessDeniedHandler" class="wang.dreamland.www.security.MyAccessDeniedHandler">
    <property name="errorPage" value="/accessDenied.jsp"></property>
</bean>

3.在webapp目录下新建accessDenied.jsp文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>无访问权限</title>
</head>
<body>
<span style="color: red">无访问权限</span>
</body>
</html>

4.在发ajax请求的页面做下判断:

$.ajax({
       type:'post',
       url:'/list',
       dataType:'json',
       success:function(data){
           var noAccess = data["access-denied"]
           if(noAccess){
               window.location.href = "${ctx}/accessDenied.jsp";
               return
           }
       ...
   }
   });

如果noAccess=true则跳转到accessDenied.jsp页面

这样就完美解决了登陆后无访问权限跳转的问题。

Python压缩解压zip文件

发表于 2018-06-28 | 分类于 Python |

导入相关模块

import os
import shutil
import zipfile
from os.path import join, getsize

一、python压缩指定文件夹

def zip_file(src_dir):
    zip_name = src_dir +'.zip'
    z = zipfile.ZipFile(zip_name,'w',zipfile.ZIP_DEFLATED)
    for dirpath, dirnames, filenames in os.walk(src_dir):
        fpath = dirpath.replace(src_dir,'')
        fpath = fpath and fpath + os.sep or ''
        for filename in filenames:
            z.write(os.path.join(dirpath, filename),fpath+filename)
            print ('==压缩成功==')
    z.close()

其中

src_dir:你要压缩的文件夹的路径

zip_name:压缩后zip文件的路径及名称

二、python解压zip

def unzip_file(zip_src, dst_dir):
    r = zipfile.is_zipfile(zip_src)
    if r:     
        fz = zipfile.ZipFile(zip_src, 'r')
        for file in fz.namelist():
            fz.extract(file, dst_dir)       
    else:
        print('This is not zip')

其中:

zip_src:是zip文件的全路径

dst_dir:是要解压到的目的文件夹

三、Python其它操作文件方法

1.剪切(移动)文件到指定目录

shutil.move(filename, dst_dir)

2.删除文件夹

shutil.rmtree(src_dir)

3.删除指定文件

os.remove(file_src)

4.新建文件夹

os.mkdir(dst_dir)

5.遍历文件夹

for filename in os.listdir(src_dir):

6.复制文件

shutil.copyfile(src_file,dst_file)  

7.获取文件夹大小

def get_dir_size(dir_path):
    size = 0L
    for root, dirs, files in os.walk(dir_path):
        size += sum([getsize(join(root, name)) for name in files])
    return size

可以根据文件大小做不同的判断,如:

file_size = get_dir_size(DATA_PATH)
max_size = file_size / 1024 / 1024   ##获得的是以Mb为单位的值
if max_size < 100:
    pass

dockpanel动态添加picturebox并绑定图片

发表于 2018-06-04 | 分类于 C# |

关于DockManager的使用之前已经介绍过,这里不再赘述。直接说如何在dockpanel中动态添加picturebox并绑定图片。

1.生成picturebox的方法

 public void generatorPictureBox()
{
    int imageNum = 20;//图片的数量
    PictureBox[] pict;
    pict = new PictureBox[imageNum];
    for (int i = 1; i < imageNum; i++)
    {
        pict[i] = new System.Windows.Forms.PictureBox();       
        pict[i].Location = new Point(5 , 10+(i - 1) * 60);//设置图片位置  竖向排列

        pict[i].SizeMode = PictureBoxSizeMode.Zoom;
        pict[i].Image = Image.FromFile(@"D:\images\" + i + ".png");//导入图片
        pict[i].Size = new Size(50, 50);//设置图片大小
        pict[i].BorderStyle = BorderStyle.None;//取消边框
        pict[i].Image.Tag = i;
        pict[i].Click += singleClick;//给图片绑定点击事件
        dockPanel1.Controls.Add(pict[i]); //添加picturebox

    }

}

2.给图片绑定的点击事件

private void singleClick(object sender, EventArgs e)
  {
      PictureBox p = sender as PictureBox;       
      LOG.Info("Click===============imgIndex===="+p.Image.Tag);
  }

3.在窗体初始化的时候调用generatorPictureBox方法即可

4.效果如下:

Xshell6下载安装

发表于 2018-05-28 |

随着xshell5出现评估期已过的问题,发现好多人不知道怎么下载免费版的Xshell,在这里我将详细告诉大家如何下载和安装最新的Xshell6远程管理工具。

1.进入xshell英文官网,链接地址如下:

http://www.netsarang.com/products/main.html

2.将鼠标悬浮在Download上会出现三个列表,点击第三个Free License,如下图:

或者点击Download,将窗口滚动到最下方,点击Free for Home & School,如下图:

3.之后会进入Xshell6和Xftp6的下载页面,如下图:

4.点击Xshell6会跳转到填写个人信息的页面,打红星的是必填项,注意邮箱要正确,会将下载链接发到你的邮箱,如下图:

5.填写好信息后,点击Submit提交,这时下载链接就会发送到你填写的邮箱了。

6.打开邮箱,点击链接进行下载,如下图:

7.下载完成后根据提示进行安装即可。

xshell6下载完成以后,一般还需要配合xftp6文件上传工具使用,下载步骤同上。

12…5

WangLinYong

JAVA,Python,C#,Node.js,个人博客,Github,hexo

41 日志
8 分类
26 标签
RSS
GitHub Twitter
© 2018 WangLinYong
本站访客数:
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.3