二二个人博客

愿居于一城,与卿所见美好......

推荐文章

浅析VO、DTO、DO、PO的概念、区别和用处

    转载:http://www.cnblogs.com/qixuejia/p/4390086.html

    本篇文章主要讨论一下我们经常会用到的一些对象:VO、DTO、DO和PO。

    由于不同的项目和开发人员有不同的命名习惯,这里我首先对上述的概念进行一个简单描述,名字只是个标识,我们重点关注其概念:

     

    概念:

    VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。

    DTO(Data Transfer Object):数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。

    DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。

    PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。

     

    模型:

           下面以一个时序图建立简单模型来描述上述对象在三层架构应用中的位置

     

     

    l         用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO。

    l         展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层。

    l         服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务。

    l         服务层把DO转换为持久层对应的PO(可以使用ORM工具,也可以不用),调用持久层的持久化方法,把PO传递给它,完成持久化操作。

    l         对于一个逆向操作,如读取数据,也是用类似的方式转换和传递,略。

     

    VO与DTO的区别

           大家可能会有个疑问(在笔者参与的项目中,很多程序员也有相同的疑惑):既然DTO是展示层与服务层之间传递数据的对象,为什么还需要一个VO呢?对!对于绝大部分的应用场景来说,DTO和VO的属性值基本是一致的,而且他们通常都是POJO,因此没必要多此一举,但不要忘记这是实现层面的思维,对于设计层面来说,概念上还是应该存在VO和DTO,因为两者有着本质的区别,DTO代表服务层需要接收的数据和返回的数据,而VO代表展示层需要显示的数据。

           用一个例子来说明可能会比较容易理解:例如服务层有一个getUser的方法返回一个系统用户,其中有一个属性是gender(性别),对于服务层来说,它只从语义上定义:1-男性,2-女性,0-未指定,而对于展示层来说,它可能需要用“帅哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。说到这里,可能你还会反驳,在服务层直接就返回“帅哥美女”不就行了吗?对于大部分应用来说,这不是问题,但设想一下,如果需求允许客户可以定制风格,而不同风格对于“性别”的表现方式不一样,又或者这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,那么,问题就来了。再者,回到设计层面上分析,从职责单一原则来看,服务层只负责业务,与具体的表现形式无关,因此,它返回的DTO,不应该出现与表现形式的耦合。

           理论归理论,这到底还是分析设计层面的思维,是否在实现层面必须这样做呢?一刀切的做法往往会得不偿失,下面我马上会分析应用中如何做出正确的选择。

     

    VO与DTO的应用

           上面只是用了一个简单的例子来说明VO与DTO在概念上的区别,本节将会告诉你如何在应用中做出正确的选择。

           在以下才场景中,我们可以考虑把VO与DTO二合为一(注意:是实现层面):

    l         当需求非常清晰稳定,而且客户端很明确只有一个的时候,没有必要把VO和DTO区分开来,这时候VO可以退隐,用一个DTO即可,为什么是VO退隐而不是DTO?回到设计层面,服务层的职责依然不应该与展示层耦合,所以,对于前面的例子,你很容易理解,DTO对于“性别”来说,依然不能用“帅哥美女”,这个转换应该依赖于页面的脚本(如JavaScript)或其他机制(JSTL、EL、CSS)

    l         即使客户端可以进行定制,或者存在多个不同的客户端,如果客户端能够用某种技术(脚本或其他机制)实现转换,同样可以让VO退隐

     

    以下场景需要优先考虑VO、DTO并存:

    l         上述场景的反面场景

    l         因为某种技术原因,比如某个框架(如Flex)提供自动把POJO转换为UI中某些Field时,可以考虑在实现层面定义出VO,这个权衡完全取决于使用框架的自动转换能力带来的开发和维护效率提升与设计多一个VO所多做的事情带来的开发和维护效率的下降之间的比对。

    l         如果页面出现一个“大视图”,而组成这个大视图的所有数据需要调用多个服务,返回多个DTO来组装(当然,这同样可以通过服务层提供一次性返回一个大视图的DTO来取代,但在服务层提供一个这样的方法是否合适,需要在设计层面进行权衡)。

     

    DTO与DO的区别

           首先是概念上的区别,DTO是展示层和服务层之间的数据传输对象(可以认为是两者之间的协议),而DO是对现实世界各种业务角色的抽象,这就引出了两者在数据上的区别,例如UserInfo和User(对于DTO和DO的命名规则,请参见笔者前面的一篇博文),对于一个getUser方法来说,本质上它永远不应该返回用户的密码,因此UserInfo至少比User少一个password的数据。而在领域驱动设计中,正如第一篇系列文章所说,DO不是简单的POJO,它具有领域业务逻辑。

     

    DTO与DO的应用

           从上一节的例子中,细心的读者可能会发现问题:既然getUser方法返回的UserInfo不应该包含password,那么就不应该存在password这个属性定义,但如果同时有一个createUser的方法,传入的UserInfo需要包含用户的password,怎么办?在设计层面,展示层向服务层传递的DTO与服务层返回给展示层的DTO在概念上是不同的,但在实现层面,我们通常很少会这样做(定义两个UserInfo,甚至更多),因为这样做并不见得很明智,我们完全可以设计一个完全兼容的DTO,在服务层接收数据的时候,不该由展示层设置的属性(如订单的总价应该由其单价、数量、折扣等决定),无论展示层是否设置,服务层都一概忽略,而在服务层返回数据时,不该返回的数据(如用户密码),就不设置对应的属性。

           对于DO来说,还有一点需要说明:为什么不在服务层中直接返回DO呢?这样可以省去DTO的编码和转换工作,原因如下:

    l         两者在本质上的区别可能导致彼此并不一一对应,一个DTO可能对应多个DO,反之亦然,甚至两者存在多对多的关系。

    l         DO具有一些不应该让展示层知道的数据

    l         DO具有业务方法,如果直接把DO传递给展示层,展示层的代码就可以绕过服务层直接调用它不应该访问的操作,对于基于AOP拦截服务层来进行访问控制的机制来说,这问题尤为突出,而在展示层调用DO的业务方法也会因为事务的问题,让事务难以控制。

    l         对于某些ORM框架(如Hibernate)来说,通常会使用“延迟加载”技术,如果直接把DO暴露给展示层,对于大部分情况,展示层不在事务范围之内(Open session in view在大部分情况下不是一种值得推崇的设计),如果其尝试在Session关闭的情况下获取一个未加载的关联对象,会出现运行时异常(对于Hibernate来说,就是LazyInitiliaztionException)。

    l         从设计层面来说,展示层依赖于服务层,服务层依赖于领域层,如果把DO暴露出去,就会导致展示层直接依赖于领域层,这虽然依然是单向依赖,但这种跨层依赖会导致不必要的耦合。

     

    对于DTO来说,也有一点必须进行说明,就是DTO应该是一个“扁平的二维对象”,举个例子来说明:如果User会关联若干个其他实体(例如Address、Account、Region等),那么getUser()返回的UserInfo,是否就需要把其关联的对象的DTO都一并返回呢?如果这样的话,必然导致数据传输量的大增,对于分布式应用来说,由于涉及数据在网络上的传输、序列化和反序列化,这种设计更不可接受。如果getUser除了要返回User的基本信息外,还需要返回一个AccountId、AccountName、RegionId、RegionName,那么,请把这些属性定义到UserInfo中,把一个“立体”的对象树“压扁”成一个“扁平的二维对象”,笔者目前参与的项目是一个分布式系统,该系统不管三七二十一,把一个对象的所有关联对象都转换为相同结构的DTO对象树并返回,导致性能非常的慢。

     

     

    DO与PO的区别

           DO和PO在绝大部分情况下是一一对应的,PO是只含有get/set方法的POJO,但某些场景还是能反映出两者在概念上存在本质的区别:

    l         DO在某些场景下不需要进行显式的持久化,例如利用策略模式设计的商品折扣策略,会衍生出折扣策略的接口和不同折扣策略实现类,这些折扣策略实现类可以算是DO,但它们只驻留在静态内存,不需要持久化到持久层,因此,这类DO是不存在对应的PO的。

    l         同样的道理,某些场景下,PO也没有对应的DO,例如老师Teacher和学生Student存在多对多的关系,在关系数据库中,这种关系需要表现为一个中间表,也就对应有一个TeacherAndStudentPO的PO,但这个PO在业务领域没有任何现实的意义,它完全不能与任何DO对应上。这里要特别声明,并不是所有多对多关系都没有业务含义,这跟具体业务场景有关,例如:两个PO之间的关系会影响具体业务,并且这种关系存在多种类型,那么这种多对多关系也应该表现为一个DO,又如:“角色”与“资源”之间存在多对多关系,而这种关系很明显会表现为一个DO——“权限”。

    l         某些情况下,为了某种持久化策略或者性能的考虑,一个PO可能对应多个DO,反之亦然。例如客户Customer有其联系信息Contacts,这里是两个一对一关系的DO,但可能出于性能的考虑(极端情况,权作举例),为了减少数据库的连接查询操作,把Customer和Contacts两个DO数据合并到一张数据表中。反过来,如果一本图书Book,有一个属性是封面cover,但该属性是一副图片的二进制数据,而某些查询操作不希望把cover一并加载,从而减轻磁盘IO开销,同时假设ORM框架不支持属性级别的延迟加载,那么就需要考虑把cover独立到一张数据表中去,这样就形成一个DO对应对个PO的情况。

    l         PO的某些属性值对于DO没有任何意义,这些属性值可能是为了解决某些持久化策略而存在的数据,例如为了实现“乐观锁”,PO存在一个version的属性,这个version对于DO来说是没有任何业务意义的,它不应该在DO中存在。同理,DO中也可能存在不需要持久化的属性。

     

    DO与PO的应用

           由于ORM框架的功能非常强大而大行其道,而且JavaEE也推出了JPA规范,现在的业务应用开发,基本上不需要区分DO与PO,PO完全可以通过JPA,Hibernate Annotations/hbm隐藏在DO之中。虽然如此,但有些问题我们还必须注意:

    l         对于DO中不需要持久化的属性,需要通过ORM显式的声明,如:在JPA中,可以利用@Transient声明。

    l         对于PO中为了某种持久化策略而存在的属性,例如version,由于DO、PO合并了,必须在DO中声明,但由于这个属性对DO是没有任何业务意义的,需要让该属性对外隐藏起来,最常见的做法是把该属性的get/set方法私有化,甚至不提供get/set方法,但对于Hibernate来说,这需要特别注意,由于Hibernate从数据库读取数据转换为DO时,是利用反射机制先调用DO的空参数构造函数构造DO实例,然后再利用JavaBean的规范反射出set方法来为每个属性设值,如果不显式声明set方法,或把set方法设置为private,都会导致Hibernate无法初始化DO,从而出现运行时异常,可行的做法是把属性的set方法设置为protected。

    l         对于一个DO对应多个PO,或者一个PO对应多个DO的场景,以及属性级别的延迟加载,Hibernate都提供了很好的支持,请参考Hibnate的相关资料。

     

     

        到目前为止,相信大家都已经比较清晰的了解VO、DTO、DO、PO的概念、区别和实际应用了。通过上面的详细分析,我们还可以总结出一个原则:分析设计层面和实现层面完全是两个独立的层面,即使实现层面通过某种技术手段可以把两个完全独立的概念合二为一,在分析设计层面,我们仍然(至少在头脑中)需要把概念上独立的东西清晰的区分开来,这个原则对于做好分析设计非常重要(工具越先进,往往会让我们越麻木)。第一篇系列博文抛砖引玉,大唱领域驱动设计的优势,但其实领域驱动设计在现实环境中还是有种种的限制,需要选择性的使用,正如我在《田七的智慧》博文中提到,我们不能永远的理想化的去选择所谓“最好的设计”,在必要的情况下,我们还是要敢于放弃,因为最合适的设计才是最好的设计。本来,系列中的第二篇博文应该是讨论领取驱动设计的限制和如何选择性的使用,但请原谅我的疏忽,下一篇系列博文会把这个主题补上,敬请关注。

    阅读全文>>

作者:二二分类:【javaEE浏览(171评论(0

2021-01-07 17:44:00

异构数据源同步工具DataX Web用户手册(一、安装)

    异构数据源同步工具DataX Web用户手册(一、安装)


    DataX Web用户手册(一、安装)

    一、github下载master分支或者release版本到本地

    DataX Web下载地址



    二、安装DataX

    • 方法一、直接下载DataX工具包:DataX下载地址

      下载后解压至本地某个目录,进入bin目录,即可运行同步作业:

      $ cd  {YOUR_DATAX_HOME}/bin
      $ python datax.py {YOUR_JOB.json}

      自检脚本:
         python {YOUR_DATAX_HOME}/bin/datax.py {YOUR_DATAX_HOME}/job/job.json

    • 方法二、下载DataX源码,自己编译:DataX源码

      (1)、下载DataX源码:

      $ git clone git@github.com:alibaba/DataX.git

      (2)、通过maven打包:

      $ cd  {DataX_source_code_home}
      $ mvn -U clean package assembly:assembly -Dmaven.test.skip=true

      打包成功,日志显示如下:

      [INFO] BUILD SUCCESS
      [INFO] -----------------------------------------------------------------
      [INFO] Total time: 08:12 min
      [INFO] Finished at: 2015-12-13T16:26:48+08:00
      [INFO] Final Memory: 133M/960M
      [INFO] -----------------------------------------------------------------

      打包成功后的DataX包位于 {DataX_source_code_home}/target/datax/datax/ ,结构如下:

      $ cd  {DataX_source_code_home}
      $ ls ./target/datax/datax/
      bin        conf        job        lib        log        log_perf    plugin        script        tmp
    • 配置示例:从stream读取数据并打印到控制台

      • 第一步、创建创业的配置文件(json格式)

        可以通过命令查看配置模板: python datax.py -r {YOUR_READER} -w {YOUR_WRITER}

        $ cd  {YOUR_DATAX_HOME}/bin
        $  python datax.py -r streamreader -w streamwriter
        DataX (UNKNOWN_DATAX_VERSION), From Alibaba !
        Copyright (C) 2010-2015, Alibaba Group. All Rights Reserved.
        Please refer to the streamreader document:
            https://github.com/alibaba/DataX/blob/master/streamreader/doc/streamreader.md 
        
        Please refer to the streamwriter document:
             https://github.com/alibaba/DataX/blob/master/streamwriter/doc/streamwriter.md 
         
        Please save the following configuration as a json file and  use
             python {DATAX_HOME}/bin/datax.py {JSON_FILE_NAME}.json 
        to run the job.
        
        {
            "job": {
                "content": [
                    {
                        "reader": {
                            "name": "streamreader", 
                            "parameter": {
                                "column": [], 
                                "sliceRecordCount": ""
                            }
                        }, 
                        "writer": {
                            "name": "streamwriter", 
                            "parameter": {
                                "encoding": "", 
                                "print": true
                            }
                        }
                    }
                ], 
                "setting": {
                    "speed": {
                        "channel": ""
                    }
                }
            }
        }

        根据模板配置json如下:

        #stream2stream.json
        {
          "job": {
            "content": [
              {
                "reader": {
                  "name": "streamreader",
                  "parameter": {
                    "sliceRecordCount": 10,
                    "column": [
                      {
                        "type": "long",
                        "value": "10"
                      },
                      {
                        "type": "string",
                        "value": "hello,你好,世界-DataX"
                      }
                    ]
                  }
                },
                "writer": {
                  "name": "streamwriter",
                  "parameter": {
                    "encoding": "UTF-8",
                    "print": true
                  }
                }
              }
            ],
            "setting": {
              "speed": {
                "channel": 5
               }
            }
          }
        }
      • 第二步:启动DataX

        $ cd {YOUR_DATAX_DIR_BIN}
        $ python datax.py ./stream2stream.json 

        同步结束,显示日志如下:

        ...
        2015-12-17 11:20:25.263 [job-0] INFO  JobContainer - 
        任务启动时刻                    : 2015-12-17 11:20:15
        任务结束时刻                    : 2015-12-17 11:20:25
        任务总计耗时                    :                 10s
        任务平均流量                    :              205B/s
        记录写入速度                    :              5rec/s
        读出记录总数                    :                  50
        读写失败总数                    :                   0

    三、执行doc/db下面的datax_web.sql文件(注意老版本更新语句有指定库名)

    四、修改项目配置

    1.修改datax_admin下resources/application.yml文件

    #数据源
      datasource:
        username: root
        password: root
        url: jdbc:mysql://localhost:3306/datax_web?serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false&useSSL=false&nullNamePatternMatchesAll=true&useUnicode=true&characterEncoding=UTF-8
        driver-class-name: com.mysql.jdbc.Driver

    修改数据源配置,目前仅支持mysql

    # 配置mybatis-plus打印slq日志
    logging:
      level:
        com.wugui.datax.admin.mapper: error
      path: ./data/applogs/admin

    修改日志路径path

      # datax-web email
      mail:
        host: smtp.qq.com
        port: 25
        username: xxx@qq.com
        password: xxx
        properties:
          mail:
            smtp:
              auth: true
              starttls:
                enable: true
                required: true
            socketFactory:
              class: javax.net.ssl.SSLSocketFactory

    修改邮件发送配置(不需要可以不修改)

    2.修改datax_executor下resources/application.yml文件

    # log config
    logging:
      config: classpath:logback.xml
      path: ./data/applogs/executor/jobhandler

    修改日志路径path

    datax:
      job:
        admin:
          ### datax-web admin address
          addresses: http://127.0.0.1:8080
        executor:
          appname: datax-executor
          ip:
          port: 9999
          ### job log path
          logpath: ./data/applogs/executor/jobhandler
          ### job log retention days
          logretentiondays: 30
      executor:
        jsonpath: /Users/mac/data/applogs
    
      pypath: /Users/mac/tools/datax/bin/datax.py

    修改datax.job配置

    • admin.addresses datax_admin部署地址,如调度中心集群部署存在多个地址则用逗号分隔,执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";
    • executor.appname 执行器AppName,每个执行器机器集群的唯一标示,执行器心跳注册分组依据;
    • executor.ip 默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
    • executor.port 执行器Server端口号,默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
    • executor.logpath 执行器运行日志文件存储磁盘路径,需要对该路径拥有读写权限;
    • executor.logretentiondays 执行器日志文件保存天数,过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
    • executor.jsonpath datax json临时文件保存路径
    • pypath DataX启动脚本地址,例如:xxx/datax/bin/datax.py

    如果系统配置DataX环境变量(DATAX_HOME)logpath、jsonpath、pypath可不配,log文件和临时json存放在环境变量路径下。

    五、启动项目

    1.本地idea开发环境

    • 1.运行datax_admin下 DataXAdminApplication
    • 2.运行datax_executor下 DataXExecutorApplication

    admin启动成功后日志会输出三个地址,两个接口文档地址,一个前端页面地址

    2.linux环境部署

    • 1.本地安装好maven环境,安装此处细节忽略
    • 2.执行mvn package -Dmaven.test.skip=true
    • 3.打包成功后分别将datax-admin、datax-executor模块target下datax-admin-2.1.1.jar、datax-executor-2.1.1.jar放到指定目录
    • 4.分别启动datax-admin-1.0.0.jar、datax-executor-1.0.0.jar
    • 5.启动命令demo:
      nohup java -Xmx1024M -Xms1024M -Xmn448M -XX:MaxMetaspaceSize=192M -XX:MetaspaceSize=192M -jar datax-admin-2.1.1.jar&
      nohup java -Xmx1024M -Xms1024M -Xmn448M -XX:MaxMetaspaceSize=192M -XX:MetaspaceSize=192M -jar datax-executor-2.1.1.jar&

    六、启动成功

    启动成功后打开页面(默认管理员用户名:admin 密码:123456)
    http://localhost:8080/index.html#/dashboard


    七、集群部署

    • 调度中心、执行器支持集群部署,提升调度系统容灾和可用性。
    • 1.调度中心集群:

      DB配置保持一致;
      集群机器时钟保持一致(单机集群忽视);

    • 2.执行器集群:

      执行器回调地址(admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。

      同一个执行器集群内AppName(executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。

    阅读全文>>

作者:二二分类:【javaEE浏览(516评论(0

2020-12-25 17:25:38

Spring Boot 多数据源 Redis 配置lettuce客户端

    概述

    本文基于spring boot 2.0.7,讲解如何配置多数据源redis,采用lettuce做为redis客户端,并附上示例代码。

    

    redis配置

    配置文件

    skyarthur:
      redis1:
        host: 127.0.0.1
        port: 6378
        lettuce:
          pool:
            min-idle: 5
            max-idle: 10
            max-active: 8
            max-wait: 1ms
          shutdown-timeout: 100ms
      redis2:
        host: 127.0.0.1
        port: 6379
        lettuce:
          pool:
            min-idle: 5
            max-idle: 10
            max-active: 8
            max-wait: 1ms
          shutdown-timeout: 100ms

    共配置2个redis数据源,并采用 lettuce pool 做为redis客户端

    配置类

    package com.skyarthur.springboot.config;
    import io.lettuce.core.resource.ClientResources;
    import io.lettuce.core.resource.DefaultClientResources;
    import lombok.Getter;
    import lombok.Setter;
    import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.connection.RedisPassword;
    import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
    import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
    import org.springframework.data.redis.core.StringRedisTemplate;
    /**
     * Created by skyarthur on 2019-12-21
     */
    @Configuration
    @ConfigurationProperties(prefix = "skyarthur")
    @Getter
    @Setter
    public class RedisConfig {
        private RedisProperties redis1;
        private RedisProperties redis2;
        @Bean(destroyMethod = "shutdown")
        @ConditionalOnMissingBean(ClientResources.class)
        public DefaultClientResources lettuceClientResources() {
            return DefaultClientResources.create();
        }
        @Bean(name = "stringRedisTemplate1")
        @Primary
        public StringRedisTemplate stringRedisTemplate1(
                @Qualifier("Redis1LettuceConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
        @Bean(name = "Redis1LettuceConnectionFactory")
        @Primary
        public LettuceConnectionFactory Redis1LettuceConnectionFactory(ClientResources clientResources) {
            RedisStandaloneConfiguration redis1StandaloneConfiguration = getStandaloneConfig(redis1);
            LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(clientResources, redis1);
            return new LettuceConnectionFactory(redis1StandaloneConfiguration, clientConfig);
        }
        @Bean(name = "stringRedisTemplate2")
        public StringRedisTemplate stringRedisTemplate2(
                @Qualifier("Redis2LettuceConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
        @Bean(name = "Redis2LettuceConnectionFactory")
        public LettuceConnectionFactory Redis2LettuceConnectionFactory(ClientResources clientResources) {
            RedisStandaloneConfiguration redis1StandaloneConfiguration = getStandaloneConfig(redis2);
            LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(clientResources, redis2);
            return new LettuceConnectionFactory(redis1StandaloneConfiguration, clientConfig);
        }
        /**
         * redis standalone config
         *
         * @param redisProperties redis 配置参数
         * @return RedisStandaloneConfiguration
         */
        private RedisStandaloneConfiguration getStandaloneConfig(RedisProperties redisProperties) {
            RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
            config.setHostName(redisProperties.getHost());
            config.setPort(redisProperties.getPort());
            config.setPassword(RedisPassword.of(redisProperties.getPassword()));
            config.setDatabase(redisProperties.getDatabase());
            return config;
        }
        /**
         * 构建 LettuceClientConfiguration
         *
         * @param clientResources clientResources
         * @param redisProperties redisProperties
         * @return LettuceClientConfiguration
         */
        private LettuceClientConfiguration getLettuceClientConfiguration(ClientResources clientResources,
                                                                         RedisProperties redisProperties) {
            LettuceClientConfiguration.LettuceClientConfigurationBuilder builder =
                    createBuilder(redisProperties.getLettuce().getPool());
            if (redisProperties.isSsl()) {
                builder.useSsl();
            }
            if (redisProperties.getTimeout() != null) {
                builder.commandTimeout(redisProperties.getTimeout());
            }
            if (redisProperties.getLettuce() != null) {
                RedisProperties.Lettuce lettuce = redisProperties.getLettuce();
                if (lettuce.getShutdownTimeout() != null
                        && !lettuce.getShutdownTimeout().isZero()) {
                    builder.shutdownTimeout(
                            redisProperties.getLettuce().getShutdownTimeout());
                }
            }
            builder.clientResources(clientResources);
            return builder.build();
        }
        /**
         * 创建 LettuceClientConfigurationBuilder
         *
         * @param pool 连接池配置
         * @return LettuceClientConfigurationBuilder
         */
        private LettuceClientConfiguration.LettuceClientConfigurationBuilder createBuilder(RedisProperties.Pool pool) {
            if (pool == null) {
                return LettuceClientConfiguration.builder();
            }
            return LettucePoolingClientConfiguration.builder()
                    .poolConfig(getPoolConfig(pool));
        }
        /**
         * pool config
         *
         * @param properties redis 参数配置
         * @return GenericObjectPoolConfig
         */
        private GenericObjectPoolConfig getPoolConfig(RedisProperties.Pool properties) {
            GenericObjectPoolConfig config = new GenericObjectPoolConfig();
            config.setMaxTotal(properties.getMaxActive());
            config.setMaxIdle(properties.getMaxIdle());
            config.setMinIdle(properties.getMinIdle());
            if (properties.getMaxWait() != null) {
                config.setMaxWaitMillis(properties.getMaxWait().toMillis());
            }
            return config;
        }
    } //+V:BGM7756,免费领取资料

    以上配置主要参考 spring 官方配置 RedisAutoConfiguration, LettuceConnectionConfiguration。简单来说:RedisAutoConfiguration注册了2个bean,RedisTemplate和StringRedisTemplate,这两个bean又依赖RedisConnectionFactory的注入,在RedisAutoConfiguration可以看到,RedisConnectionFactory有两个实现,LettuceConnectionFactory和JedisConnectionFactory,具体可以直接参考官方配置LettuceConnectionConfiguration

    单测

    package com.skyarthur.springboot.config;
    import com.skyarthur.springboot.ApplicationTests;
    import com.skyarthur.springboot.common.enums.RedisType;
    import org.junit.Assert;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.core.StringRedisTemplate;
    /**
     * Created by skyarthur on 2019-12-21
     */
    public class RedisConfigTest extends ApplicationTests {
        @Autowired
        @Qualifier("stringRedisTemplate1")
        private StringRedisTemplate stringRedisTemplate1;
        @Autowired
        @Qualifier("stringRedisTemplate2")
        private StringRedisTemplate stringRedisTemplate2;
        @Test
        public void test() {
            LettuceConnectionFactory factory = (LettuceConnectionFactory) stringRedisTemplate1.getConnectionFactory();
            Assert.assertNotNull(factory);
            Assert.assertEquals(6378, factory.getPort());
            Assert.assertEquals("127.0.0.1", factory.getHostName());
            factory = (LettuceConnectionFactory) stringRedisTemplate2.getConnectionFactory();
            Assert.assertNotNull(factory);
            Assert.assertEquals(6379, factory.getPort());
            Assert.assertEquals("127.0.0.1", factory.getHostName());
            stringRedisTemplate1.opsForValue().set(RedisType.PERSON_INFO.getRealKey("test"), "6378");
            stringRedisTemplate2.opsForValue().set("test", "6379");
            Assert.assertEquals("6378", stringRedisTemplate1.opsForValue().get(RedisType.PERSON_INFO.getRealKey("test")));
            Assert.assertEquals("6379", stringRedisTemplate2.opsForValue().get("test"));
        }
    } //+V:BGM7756,免费领取资料

    阅读全文>>

作者:二二分类:【javaEE浏览(231评论(0

2020-12-17 18:51:55

sharehoo社区自定义注解实现登陆状态校验

    Spring AOP

    Spring AOP 即面向切面,是对OOP面向对象的一种延伸。
    AOP机制可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。

    我们通过AOP机制可以实现:Authentication 权限检查、Caching 缓存、Context passing 内容传递、Error handling 错误处理、日志打印等功能,这里我们讲一下怎么用Spring AOP来实现权限检查。

    SpringBoot 通过自定义注解实现权限检查

    Maven依赖

    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.2</version>
        <optional>true</optional>
    </dependency>
    
    <!--Spring AOP-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    MyPermissionTag.class自定义注解

    • @Retention: 用来修饰注解,是注解的注解,称为元注解。
    • @Target:用来说明对象的作用范围
    /**
     * 用户请求权限校验
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface MyPermissionTag {
        String value() default "";
        String name() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里特别讲一下@Retention,按生命周期来划分可分为3类:

    • RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃(运行时去动态获取注解信息);
    • RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期(在编译时进行一些预处理操作);
    • RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在(做一些检查性的操作);

    这3个生命周期分别对应于:Java源文件(.java文件) —> .class文件 —> 内存中的字节码。

    AuthInterceptor 权限检查的切面

    这里简单介绍一下,切面的执行方法和其执行顺序:

    • @Around 通知方法将目标方法封装起来
    • @Before 通知方法会在目标方法调用之前执行
    • @After 通知方法会在目标方法返回或者异常后执行
    • @AfterReturning 通知方法会在目标方法返回时执行
    • @Afterthrowing 通知方法会在目标方法抛出异常时执行

    这里以一个返回正常的情况为例:(异常替换最后一步即可)

    HasLoginAspect.class

    • 注意要在启动类扫描这个class,并且添加 @EnableAspectJAutoProxy(proxyTargetClass = true)
    package com.sharehoo.config.aspect;
    import com.alibaba.fastjson.JSONObject;
    import com.sharehoo.config.lang.Consts;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    import org.springframework.util.ObjectUtils;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    
    /**
     * @Author miki
     * @Description 用户是否登录的鉴权注解 *
     * @Date 14:48 2020/11/30
     * @Param
     * @return
     **/
    
    
    @Component
    @Aspect
    @SuppressWarnings({"unused"})
    public class HasLoginAspect {
    	
    	public static final Logger logger = LoggerFactory.getLogger(HasLoginAspect.class);
    
    	public HasLoginAspect(){
    	    logger.info("创建aop切面拦截...");
        }
    
        /**
         * 参数处理
         *
         * @param point
         */
        @Before("@annotation(com.sharehoo.config.annotation.HasLogin)")
        public void beforeProReq(JoinPoint point) {
            logger.info("前置拦截-开始");
    
            logger.info("前置拦截-结束");
        }
    
    
        @Around("@annotation(com.sharehoo.config.annotation.HasLogin)")
        public Object checkRequestHead(ProceedingJoinPoint joinPoint) throws Throwable {
            logger.debug("===>check access token start:{}", joinPoint.getArgs());
            long begin = System.nanoTime();
    
            HttpServletRequest request =
                    ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            request.setCharacterEncoding("UTF-8");
            HttpServletResponse response =
                    ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
    
            HttpSession session = request.getSession();
            Object user= session.getAttribute(Consts.CURRENTUSER);
    
            String requestUri = request.getRequestURI();
            String requestMethod = request.getMethod();
    
            logger.info(
                    "===>get the equest url:{}; and request method:{}; ",
                    requestUri, requestMethod);
    
            long end = System.nanoTime();
            logger.info("API deal time log {}:{}",
                    joinPoint.getTarget().getClass() + "." + joinPoint.getSignature().getName(),
                    (end - begin) / 1000000);
            if(ObjectUtils.isEmpty(user)){
                response.sendRedirect("http://sharehoo.cn/login?prePage=http://sharehoo.cn" + requestUri);
                return null;
            }
    
            return joinPoint.proceed();
        }
    
    
        /**
         * 参数处理
         *
         * @param point
         */
        @After("@annotation(com.sharehoo.config.annotation.HasLogin)")
        public void afterProReq(JoinPoint point) {
    
            logger.info("登录校验拦截-结束");
        }
    }
    阅读全文>>

作者:二二分类:【javaEE浏览(226评论(0

2020-12-07 20:27:22

spring-@Autowired注入与构造函数注入使用

    前言

    因为业务关系也看了些同事写的代码,因为公司没有明确规定,有一部分人在注入依赖的时候使用了用Spring推荐的构造器注入的方式,一部分人使用了@Autowired的注解进行注入。

    因此,接下来我试着总结归纳一下相关的情况,做一次回顾。

    用自己的话去说出来才算是真的掌握。

    使用介绍

    1.@Autowired注入

     

    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestController {
    4. @Autowired
    5. private List<TestService> testServices;
    6. @Autowired
    7. private List<ChainAsbtract> chains;
    8. private ChainAsbtract target;
    9. }

    2.构造器注入

    Spring4.3+之后,constructor注入支持非显示注入方式。

    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestController {
    4. // @Autowired
    5. private final List<TestService> testServices;
    6. // @Autowired
    7. private final List<ChainAsbtract> chains;
    8. // @Autowired
    9. public TestController(List<TestService> testServices, List<ChainAsbtract> chains) {
    10. this.testServices = testServices;
    11. this.chains = chains;
    12. }
    13. }

     3.setter注入

    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestController {
    4. // @Autowired
    5. private final List<TestService> testServices;
    6. // @Autowired
    7. private final List<ChainAsbtract> chains;
    8. @Autowired
    9. public void setTestServices(List<TestService> testServices){
    10. this.testServices = testServices;
    11. }
    12. @Autowired
    13. public void setTestServices(List<ChainAsbtract> chains){
    14. this.chains = chains;
    15. }
    16. }

    事实上,spring在4.x版本后就推荐使用构造器的方式的来注入fileld

    官方推荐理由

    单一职责: 当使用构造函数注入的时候,你会很容易发现参数是否过多,这个时候需要考虑你这个类的职责是否过大,考虑拆分的问题;而当使用@Autowired注入field的时候,不容易发现问题

    依赖不可变: 只有使用构造函数注入才能注入final

    依赖隐藏:使用依赖注入容器意味着类不再对依赖对象负责,获取依赖对象的职责就从类抽离出来,IOC容器会帮你自动装备。这意味着它应该使用更明确清晰的公用接口方法或者构造器,这种方式就能很清晰的知道类需要什么和到底是使用setter还是构造器

    降低容器耦合度: 依赖注入框架的核心思想之一是托管类不应依赖于所使用的DI容器。换句话说,它应该只是一个普通的POJO,只要您将其传递给所有必需的依赖项,就可以独立地实例化。这样,您可以在单元测试中实例化它,而无需启动IOC容器并单独进行测试(使用一个可以进行集成测试的容器)。如果没有容器耦合,则可以将该类用作托管或非托管类,甚至可以切换到新的DI框架。

     另外,在使用构造器的使用能避免注入的依赖是空的情况。

    因为在bean的生命周期里面先执行的是bean的构造器,然后才给bean里面的属性赋值。具体内容在bean的生命周期里面,后面我学习之后按照自己的理解写写。

    阅读全文>>

作者:二二分类:【javaEE浏览(196评论(0

2020-11-29 21:50:20