::: {.center}
xx公司

2020-01-01
:::

文档管理

合理地管理主文档,确保文档版本的及时更新,同时保持备份文档和源文档的一致性。

版本管理


本版本修订日期 2019-08-12 生效日期 2019-08-12


版本 生效日期 变更内容 编制人


V1.0 2020-01-01 初稿编写完成 xx

概述

编写目的

说明编写目的,指出本文档的预期读者。

背景

说明系统的项目背景,使用本系统所包含的用户。

范围

说明本文档的适用范围。

参考资料

列出所参考的文档,如其它的用户文档。

软件清单

列出所提交的软件产品的程序及其它必要的附件文件,包括可能的支持软件、第三方包、脚本文件、说明文档等,对清单中的名项给出必要的说明。

运行环境要求

说明系统进行安装部署时,对运行环境的要求,分为硬件环境和软件环境,包括服务器、客户端。

服务器端的运行环境,硬件方面需要指出最低参数要求,软件方面需要指出如操作系统、数据库软件、web应用服务器的名称、版本信息等。

客户端的运行环境,硬件方面需要指出最低参数要求,软件方面需要列出操作系统版本、运行依赖的浏览器版本、需要安装的驱动程序等。

支撑软件的安装、部署和配置

给出所有支撑软件的安装、部署和配置步聚,可以引用第三方文档。

<支撑软件X>的安装、部署和配置

支撑软件X安装、部署和配置步聚。

应用程序的安装、部署和配置

<应用程序x>的安装、部署和配置

安装、部署前的准备工作

说明系统进行安装部署前,需要进行的前期准备工作。如对操作系统、数据库、web应用服务器进行相应的参数设置,数据库的初始化等等。

部署环境概要说明

可以对部署的服务器环境,如文件目录结构,进行概要说明。

依赖

系统在进行安装、部署时,如果需要对外部系统运行或接口有依赖关系,在此列出。

安装、部署和配置步聚

列出详细的安装、部署和配置过程,包括对相关配置文件内容的修改。

程序的启动和停止

给出软件的启动和停止说明

内容审核要点:

所述运行环境和支撑软件是否详细完备;

部署前的准备工作是否详细完备;

安装过程是否详细易懂。

参考

https://www.jianshu.com/p/a7984927cfb9

::: {.center}
xx公司

2020-01-01
:::

文档管理

合理地管理主文档,
确保文档版本的及时更新,同时保持备份文档和源文档的一致性。

版本管理


本版本修订日期 2019-08-12 生效日期 2019-08-12


版本 生效日期 变更内容 编制人


V1.0 2020-01-01 初稿编写完成 xx

概述

背景

说明系统的项目背景,本系统的使用和运维单位等。

范围

说明本文档的适用范围。

源代码交付清单

以表格的形式,列出所提交的源代码光盘中所有的文件目录结构,对各目录和子目录加以说明。对主要源代码程序加以说明。对其它必要的附件(包括第三方包、类库、配置文件、程序的帮助说明文档等)也要加以说明。

编译和打包的说明

编译环境和工具要求

如果需要,对编译环境和所需要工具进行简要说明。

编译和打包步聚

说明对源代码进行编译和打包的详细步聚,要能根据这些步骤生成可用的目标程序。尽量使用脚本方式生成目标程序。

生成物说明

对编译生成的执行程序进行简要说明。

内容审核要点:

交付清单是否详细完备;

说明文字是否明确易懂;

交付物版本是否和实际系统的程序版本一致(能根据本文档生成与实际生产系统一致的程序);

参考

https://www.jianshu.com/p/a7984927cfb9

封面

::: {.center}
xx公司

2020-01-01
:::

文档管理

合理地管理主文档,确保文档版本的及时更新,同时保持备份文档和源文档的一致性。

版本管理


本版本修订日期 2019-08-12 生效日期 2019-08-12


版本 生效日期 变更内容 编制人


V1.0 2020-01-01 初稿编写完成 xx

概述

编写目的

说明编写目的,指出预期的读者范围。

背景

简要描述此次上线部署的背景。

参考资料

列出所参考的文档,如安装部署手册等。

工作内容

说明本次上线部署的工作内容。即要做哪几件事。

实际完成情况

对照工作内容,详细说明本次实际部署过程和工作任务完成情况。

部署测试情况

详细说明本次部署测试情况,测试内容是否都获通过。

遗留工作和待解决问题

如果有遗留工作或问题,需要在此部分列出。

签字栏

本报告作为验收的依据,需要相关方的签字。应留有签字栏。

内容审核要点:

任务描述是否明确;

部署实际完成情况是否描述清楚;

部署测试情况是否描述清楚;

部署部署过程是否与实际情况一致。

参考

https://www.jianshu.com/p/a7984927cfb9

::: {.center}
xx公司

2020-01-01
:::

文档管理

合理地管理主文档,
确保文档版本的及时更新,同时保持备份文档和源文档的一致性。

版本管理


本版本修订日期 2019-08-12 生效日期 2019-08-12


版本 生效日期 变更内容 编制人


V1.0 2020-01-01 初稿编写完成 xx

概述

编写目的

说明编写目的。本文档主要供开发方项目组内部使用,其目的包括:

  • 便于项目组测试人员通过版本控制机制,每次生成新的目标程序在干净的环境下进行自测(包括出厂测试);
  • 便于今后源代码的交付。

背景

说明软件系统的名称和作用,以及本可执行程序属于软件系统的哪一部分。

参考资料

列出所参考的文档,如需求规格说明、设计说明等。

工具和支撑软件要求

指出目标程序生成的工具要求,如对于使用ant
脚本生成目标程序的方式,需要ant工具的支持。
指出目标程序执行的支撑软件要求,如虚拟机、应用服务器或其它底层软件支持。
注:建议尽量提供生成脚本而不依赖于可视化开发工具来生成目标代码,以便于测试方能根据版本控制库中的源码迅速生成目标程序进行测试和回归测试。

源代码获取

指出从何处及如何获取源代码,包括脚本程序代码。一般情况下,在开发团队内部,源代码的获取方法和步骤与项目源代码配置管理方式相关。

目标程序生成步骤

详细说明目标程序生成步骤,步骤的多少与项目复杂度、所编制脚本功能强弱等因素相关。
注:不管步骤多少,整个步骤应是明确和完备的,并提供方便的清除所生成目标程序的方法。

目标程序的执行

说明如何运行生成的目标程序,一般包括以下内容:

  • 从何处获取生成的目标程序;
  • 执行前的准备工作,如配置文件的修改、数据库表定义/删除的脚本的执行、数据库表的初始化数据生成/清除脚本的执行等;
  • 软硬件运行环境的建立;
  • 程序的部署和执行。

内容审核要点:

  • 所述各目标程序生成说明文档是否涵盖项目整个软件系统;
  • 所述步骤是否明确和详细;
  • 是否内部有版本控制机制。

参考

https://www.jianshu.com/p/a7984927cfb9

::: {.center}
xx公司

2020-01-01
:::

文档管理

合理地管理主文档,
确保文档版本的及时更新,同时保持备份文档和源文档的一致性。

版本管理


本版本修订日期 2019-08-12 生效日期 2019-08-12


版本 生效日期 变更内容 编制人


V1.0 2020-01-01 初稿编写完成 xx

概述

编写目的

描述该文档的编写目的。

参考资料

列出此文档所参考的文档,如软件需求规格说明、软件设计说明等。

分类

(若有)对存储数据分类进行简要说明(数据可以按照子系统进行划分)。

  • 第全局类数据及其说明
  • 第二类数据及其说明
  • 第三类数据及其说明

使用它的程序

描述对应不同分类的数据,所使用它的外部程序或者业务系统。

约定

描述数据库设计方面的标识约定、设计约定、特殊约定等。

数据定义

全局数据

主要应用范围: 作用: 数据库定义文件: ER图文件:

表结构设计

  1. .1 表一与触发器

    表一结构说明,包括:表名、表说明(内容、作用)、索引、各列属性。
    各列属性,包括:列英文名、列中文名、 数据类型、长度、 列取值含义等。
    触发器列表,包括:名称、说明、定义等。

  2. .2 表二与触发器

    ……

视图设计

  1. .1 视图一

    定义: 用途:

  2. .1 视图二

    ……

存储过程设计

  1. .1 过程一

    定义: 用途: 输入: 输出:

.2 过程二

……

第二类数据

主要应用范围: 作用: 数据库定义文件: ER图文件:

表结构设计

……

视图设计

……

存储过程设计

……

数据库角色定义

第一类角色

角色职能: 角色权限:

第二类角色

……

数据库安全设计

针对数据库安全方面的要求进行的设计,包括保密性、安全性等方面。

数据库备份设计

针对数据的备份要求,描述数据库的备份策略、备份和恢复的手段、备份计划、操作步骤等方面的内容。

内容审核要点:

  • 本文档内容与软件设计文档、软件需求文档是否一致性;
  • 所述内容是否完备;
  • 本文档内容本身是否前后一致;
  • 表结构描述是否清楚明确; (若有)触发器描述是否清楚明确;
    (若有)视图描述是否清楚明确; (若有)存储过程描述是否清楚明确;
  • 数据库角色设置是否清楚合理。
  • 是否满足了安全、备份的要求。

参考

https://www.jianshu.com/p/a7984927cfb9

概述

接口的概念

在应用软件系统中,接口是程序和系统与外界交互的窗口。本文中所阐述的接口,包括:

应用软件系统提供软件功能供外部软件程序调用;
应用软件系统调用外部系统提供的软件功能;
应用软件系统依据应用级别的交互协议与外系统进行功能和数据的交换;
应用软件系统通过公用文件目录或数据库与外部系统交换信息。

此处中所阐述的接口不包括:

应用软件系统内部功能模块之间的接口调用;
应用软件系统提供的人机交互界面。

接口处理策略

xxxxxxxxxxx是一个庞大的由众多应用子系统构成的复杂分布式系统。各应用子系统之间存在众多的软件接口关系。
在xxxxxxxxxxx中,我们采用SOA的体系架构来解决各应用子系统之间、应用子系统与外部系统之间的接口问题。通过基于ESB服务总线技术的应用支撑平台来发布和管理应用软件系统对外提供公用服务。在这里,服务系指精确定义、封装完善、独立于其他服务所处环境和状态的软件功能。接口服务系指以服务的方式提供的接口实现。
通过支撑平台,各业务子系统接口之间的点对点关系,变成了各业务子系统接口与支撑平台的关系,从而简化了系统间的逻辑关系,应用支撑平台是各业务子系统相互连接的中介和纽带。各业务子系统之间在进行服务请求和服务调用时所需的一些附加工作,如通信协议的转换、路由、消息格式转换、安全性保证等,也可以由应用支撑平台来提供支持。此外,应用支撑平台提供适配器方式以支持文件同步、数据库同步、JMS、FTP等,应用支持平台还提供定制适配器功能接入特殊的外部系统。
通过应用支持平台发布公用服务,将有利于接口服务的复用,以及接口服务的维护和管理。

接口文档规范的必要性

xxxxxxxxxxx建设过程中,各应用软件系统的接口定义、实现、发布和管理是处理好接口问题的四个关键环节,他们贯穿在软件生命周期的全过程。为了保证在这些环节对应用软件系统之间的接口问题进行有效的处理,保证接口的良好定义、合理实现、受控发布和方便管理,有必要对相关技术文档提出规范化要求。
涉及接口定义的技术文档主要有业务需求说明书、软件需求规格说明书;涉及接口实现的技术文档主要有软件概要设计说明书、软件详细设计说明书、用户手册;涉及接口服务发布和管理的技术文档包括接口服务发布和管理规范等。

接口定义的文档规范要求

业务需求说明书中的接口描述

在xxxxxxxxxxx中,业务需求说明书由业主方任命的子项目组负责编制。业务需求说明书中有关接口描述的要求:

  • 宜有专门章节阐述子项目拟分包系统与其它子项目系统及外部系统之间的接口关系;
  • 在描述本系统与外部某系统接口关系时,宜:

具体阐明本系统的哪项或哪些业务功能与所述外部系统存在接口关系;
阐明接口的类型,如向外系统提供数据、从外系统获取数据、对外系统提供功能服务、向外系统请求某项功能服务;
对所述接口作必要的文字解释。

软件需求规格说明书中的接口描述

软件需求规格说明书系由承建方在业务需求说明书的基础上,在系统功能、性能、外部限制条件等方面的进一步明确和细化,作为系统设计、实现、测试的依据。是涉及接口定义的重要文档。
软件需求规格说明书中有关接口定义和描述的要求如下:

  • 软件需求规格说明书中有关与外部系统接口定义的范围应涵盖业务需求说明书中有关接口的描述,并补充业务需求书中遗漏的接口内容;
  • 本系统作为接口对外提供的软件功能,应在软件需求规格说明书中明确阐明该功能。包括接口功能编号、名称、性质(功能服务、数据交换、文件交换等)、接口功能的各输入参数及数据类型,返回的结果数据类型等;
  • 如果接口是作为某种信息交互协议(如Z39.50)的服务端提供的功能,要求阐明支持的协议版本以及是否完全支持该协议,如果不完全支持,要明确列举支持或不支持的功能;
  • 如果接口是某种信息交互协议(如Z39.50)的客户端,且外部系统提供的相应协议服务端功能是本系统相关功能实现的必要条件,须将对外部系统的相关接口实现要求列入软件需求规格说明书中的限制条件部分;
  • 如果系统功能涉及到要使用或访问外部系统提供的功能,须将其列入需求规格说明书中限制条件部分。

接口定义的变更

如果在软件需求评审通过后的实施过程中,原定义或描述的接口发生了变化,须通过变更流程修改软件需求规格说明书;或编制需求文档的补充材料,与原需求规格说明书一起,作为测试和验收的依据。

接口实现的文档规范要求

软件设计说明书中的接口服务实现描述

在软件概要设计说明书中,有关接口实现的要求如下:

  • 须覆盖软件需求说明书中所述对外提供接口功能;
  • 确定接口的实现策略。如采用Web
    Service、EJB、通过网络端口连接对外提供服务、文件目录或数据库同步等。
  • 对外提供的接口功能,需要进行形式化描述,如果不能确定在软件概要设计说明书完全进行接口功能的形式化表述,应在该说明书中明示并在详细设计说明书中给出以供编码实现之用。

用户文档中的接口使用描述

在设计说明书中描述的所有对外提供的接口功能,最终都要落实在用户文档中。具体的用户文档名称,取决于项目合同和软件开发计划中对最终交付文件的规定。如用户使用手册、用户参考手册、用户技术手册等。
各应用软件系统通过该系统的用户文档对外正式公布本系统对外的接口服务的功能和调用方式。

基本设置 springboot整合

  1. 引入依赖包
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

配置文件

  1. 创建shiro.ini
1
2
3
4
5
6
7
8
#格式: 用户名=密码,角色1,角色2
[users]
ck=123456,admin

#格式:角色=权限1,权限2
[roles]
admin=add
teacher=update
  1. 使用api测试验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public Map<String,Object> userLoginAction (String userName,String passWord){

Map<String,Object> resultMap = new HashMap<>();
//初始化SecurityManager对象
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//通过SecurityManager工厂对象,获取SecurityManager实例对象.
SecurityManager securityManager = factory.getInstance();
// 把 securityManager 实例 绑定到 SecurityUtils
SecurityUtils.setSecurityManager(securityManager);
//组建Subject主体.
Subject subject = SecurityUtils.getSubject();
//创建 token 令牌
UsernamePasswordToken token = new UsernamePasswordToken(userName,passWord);
//用户登录操作.
try{
subject.login(token);
resultMap.put("code","200");
resultMap.put("msg","用户登录成功");
}catch (AuthenticationException e){
//登录失败原因 1 用户不存在 2 用户密码不正确
resultMap.put("code","-1");
resultMap.put("msg","用户登录失败");
}
return resultMap;

}

自定义

  1. 自定义配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import org.apache.shiro.mgt.SecurityManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

@Slf4j
@Configuration
public class CustomShiroConfiguration {

/**
* Filter Name Class
* anon org.apache.shiro.web.filter.authc.AnonymousFilter
* authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
* authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
* perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
* port org.apache.shiro.web.filter.authz.PortFilter
* rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
* roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
* ssl org.apache.shiro.web.filter.authz.SslFilter
* user org.apache.shiro.web.filter.authc.UserFilter
* anon:所有 url 都都可以匿名访问
* authc: 需要认证才能进行访问
* user:配置记住我或认证通过可以访问
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
log.info("注入Shiro的Web过滤器-->shiroFilter", ShiroFilterFactoryBean.class);
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

//Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
//要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
//登录成功后要跳转的连接,逻辑也可以自定义,例如返回上次请求的页面
shiroFilterFactoryBean.setSuccessUrl("/index");
//用户访问未对其授权的资源时,所显示的连接
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
/*定义shiro过滤器,例如实现自定义的FormAuthenticationFilter,需要继承FormAuthenticationFilter **本例中暂不自定义实现,在下一节实现验证码的例子中体现 */

/*定义shiro过滤链 Map结构 * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 *
anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 * authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org
.apache.shiro.web.filter.authc.FormAuthenticationFilter */
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");

// <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/login", "anon");//anon 可以理解为不拦截
filterChainDefinitionMap.put("/reg", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
filterChainDefinitionMap.put("/pages/**", "anon");
filterChainDefinitionMap.put("/api/**", "anon");
filterChainDefinitionMap.put("/dists/img/*", "anon");
filterChainDefinitionMap.put("/**", "authc");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

return shiroFilterFactoryBean;
}

// @Bean
// public EhCacheManager ehCacheManager() {
// EhCacheManager cacheManager = new EhCacheManager();
// return cacheManager;
// }

@Bean
public CustomShiroRealm customShiroRealm() {
return new CustomShiroRealm();
}

@Bean
public SecurityManager securityManager(CustomShiroRealm userRealm) {
log.info("注入Shiro的Web过滤器-->securityManager", ShiroFilterFactoryBean.class);
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
//注入缓存管理器;
// securityManager.setCacheManager(ehCacheManager());//这个如果执行多次,也是同样的一个对象;
return securityManager;
}

/**
* Shiro生命周期处理器 * @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}

/**
* 开启Shiro的注解
* (如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}

/**
* 异常处理
*
* @return
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
mappings.setProperty("UnauthorizedException", "403");
r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
//r.setWarnLogCategory("example.MvcLogger"); // No default
return r;
}

/**
* 自定义方式
*/
class CustomShiroRealm extends AuthorizingRealm {

/**
* 授权, 通过角色授权, 这里设置角色信息即可
* 参考: https://gitee.com/ityouknow/spring-boot-examples/blob/master/spring-boot-shiro/src/main/java/com/neo
* /config/MyShiroRealm.java
* https://blog.csdn.net/fu_fei_wen/article/details/77571989
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//在实际开发中,开发者根据自身情况自行获取用户信息。此处简化如下:
String role = "admin";
String permission = "add";

log.info("{}, role:{}, permission:{}", Thread.currentThread().getStackTrace()[1].getMethodName(), role,
permission);

//设置权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole(role);
simpleAuthorizationInfo.addStringPermission(permission);

return simpleAuthorizationInfo;
}


/**
* 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
//在实际开发中,开发者根据自身情况自行获取用户信息。此处简化如下:
String name = "zhangsan";
String pwd = "11";

UsernamePasswordToken upt = (UsernamePasswordToken) token;
String username = upt.getUsername();
String password = new String(upt.getPassword());

if (name.equals(username) && pwd.equals(password)) {
return new SimpleAuthenticationInfo(username, password, "zhangsansan");
}
return null;
}
}
}

  1. 登录控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.lx.demo.springbootshiro.web.controller;

import com.lx.demo.springbootshiro.domain.User;
import com.lx.demo.springbootshiro.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.validation.Valid;

@Slf4j
@Controller
public class SecurityController {

@Autowired
private UserService userService;

@GetMapping("/login")
public String loginForm() {
return "login";
}

@GetMapping("/index")
public String index(){
return "index";
}

@PostMapping("/login")
public String login(@Valid User user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "login";
}
String loginName = user.getLoginName();
log.info("准备登陆用户 => {}", loginName);
UsernamePasswordToken token = new UsernamePasswordToken(loginName,user.getPassword());
//获取当前的Subject
Subject currentUser = SecurityUtils.getSubject();
try {
//在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
//每个Realm都能在必要时对提交的AuthenticationTokens作出反应
//所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
log.info("对用户[" + loginName + "]进行登录验证..验证开始");
currentUser.login(token);
log.info("对用户[" + loginName + "]进行登录验证..验证通过");
} catch (UnknownAccountException uae) {
log.info("对用户[" + loginName + "]进行登录验证..验证未通过,未知账户");
redirectAttributes.addFlashAttribute("message", "未知账户");
} catch (IncorrectCredentialsException ice) {
log.info("对用户[" + loginName + "]进行登录验证..验证未通过,错误的凭证");
redirectAttributes.addFlashAttribute("message", "密码不正确");
} catch (LockedAccountException lae) {
log.info("对用户[" + loginName + "]进行登录验证..验证未通过,账户已锁定");
redirectAttributes.addFlashAttribute("message", "账户已锁定");
} catch (ExcessiveAttemptsException eae) {
log.info("对用户[" + loginName + "]进行登录验证..验证未通过,错误次数过多");
redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数过多");
} catch (AuthenticationException ae) {
//通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
log.info("对用户[" + loginName + "]进行登录验证..验证未通过,堆栈轨迹如下");
ae.printStackTrace();
redirectAttributes.addFlashAttribute("message", "用户名或密码不正确");
}
//验证是否登录成功
if (currentUser.isAuthenticated()) {
log.info("用户[" + loginName + "]登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)");
return "redirect:/index";
} else {
token.clear();
return "redirect:/login";
}
}

@GetMapping("/logout")
public String logout(RedirectAttributes redirectAttributes) {
//使用权限管理工具进行用户的退出,跳出登录,给出提示信息
SecurityUtils.getSubject().logout();
redirectAttributes.addFlashAttribute("message", "您已安全退出");
return "redirect:/login";
}


// @GetMapping("/reg")
// @ResponseBody
// public Result<String> reg(@Valid User user, BindingResult bindingResult) {
// if (bindingResult.hasErrors()) {
// return Result.error("用户信息填写不完整");
// }
// userService.save(user);
// return Result.ok();
// }
}
  1. 登录后一段时间内所有请求都可以通过

测试

  1. 通过浏览器访问,正常登录即可
  2. 如果通过curl访问需要保留session信息,下次请求带入,否则需要一直校验

集成redis-cache

集成缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  /**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}

/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost("localhost");
redisManager.setPort(6379);
redisManager.setExpire(1800);// 配置缓存过期时间
redisManager.setTimeout(0);
// redisManager.setPassword(password);
return redisManager;
}

/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}

/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}

支持多客户端登录踢出

  1. 设置过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package com.lx.demo.springbootshiro.filter;

import com.lx.demo.springbootshiro.domain.User;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
* 踢出最好在chrome无痕模式下测试同一个用户, 这样可以保证每次都有新sessionid
* 默认只允许一个用户登录在一个地方登录, 这样第二个登录后,第一个刷新页面发现被踢出
*/
public class KickoutSessionControlFilter extends AccessControlFilter {

private String kickoutUrl; //踢出后到的地址
private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
private int maxSession = 1; //同一个帐号最大会话数 默认1

private SessionManager sessionManager;
private Cache<String, Deque<Serializable>> cache;

public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}

public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}

public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}

public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}

//设置Cache的key的前缀
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro_redis_cache");
}

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if (!subject.isAuthenticated() && !subject.isRemembered()) {
//如果没有登录,直接进行之后的流程
return true;
}


Session session = subject.getSession();
String username = subject.getPrincipal().toString();
Serializable sessionId = session.getId();

//读取缓存 没有就存入
Deque<Serializable> deque = cache.get(username);

//如果此用户没有session队列,也就是还没有登录过,缓存中没有
//就new一个空队列,不然deque对象为空,会报空指针
if (deque == null) {
deque = new LinkedList<Serializable>();
}

//如果队列里没有此sessionId,且用户没有被踢出;放入队列
if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
//将sessionId存入队列
deque.push(sessionId);
//将用户的sessionId队列缓存
cache.put(username, deque);
}

//如果队列里的sessionId数超出最大会话数,开始踢人
while (deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if (kickoutAfter) { //如果踢出后者
kickoutSessionId = deque.removeFirst();
//踢出后再更新下缓存队列
cache.put(username, deque);
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
//踢出后再更新下缓存队列
cache.put(username, deque);
}


try {
//获取被踢出的sessionId的session对象
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if (kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
}
}

//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
//会话被踢出了
try {
//退出登录
subject.logout();
} catch (Exception e) { //ignore
}
saveRequest(request);

Map<String, String> resultMap = new HashMap<String, String>();
//判断是不是Ajax请求
if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
resultMap.put("user_status", "300");
resultMap.put("message", "您已经在其他地方登录,请重新登录!");
//输出json串
out(response, resultMap);
} else {
//重定向
WebUtils.issueRedirect(request, response, kickoutUrl);
}
return false;
}
return true;
}

private void out(ServletResponse hresponse, Map<String, String> resultMap)
throws IOException {
try {
hresponse.setCharacterEncoding("UTF-8");
PrintWriter out = hresponse.getWriter();
// out.println(JSON.toJSONString(resultMap));
out.flush();
out.close();
} catch (Exception e) {
System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
}
}
}
  1. 配置过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  /**
* 限制同一账号登录同时登录人数控制
*
* @return
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter() {
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
kickoutSessionControlFilter.setCacheManager(cacheManager());
kickoutSessionControlFilter.setSessionManager(sessionManager());
kickoutSessionControlFilter.setKickoutAfter(false);
kickoutSessionControlFilter.setMaxSession(1);
kickoutSessionControlFilter.setKickoutUrl("/logout");
return kickoutSessionControlFilter;
}

  1. springboot集成问题处理及注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  /**
* org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code,
* @return
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}

测试

通过无痕模式访问,每次变更sessionid,就被干出去了

权限控制

注解方式

注: 处理顺序与标题顺序一致

RequiresRoles:

当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。

1
2
3
4
5
6
7
8
//属于user角色
@RequiresRoles("user")

//必须同时属于user和admin角色
@RequiresRoles({"user","admin"})

//属于user或者admin之一;修改logical为OR 即可
@RequiresRoles(value={"user","admin"},logical=Logical.OR)

RequiresPermissions:

当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。

1
2
3
4
5
6
7
8
//符合index:hello权限要求
@RequiresPermissions("index:hello")

//必须同时复核index:hello和index:world权限要求
@RequiresPermissions({"index:hello","index:world"})

//符合index:hello或index:world权限要求即可
@RequiresPermissions(value={"index:hello","index:world"},logical=Logical.OR)

RequiresAuthentication:

使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。

RequiresUser

当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

RequiresGuest:

使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是”gust”身份,不需要经过认证或者在原先的session中存在记录。

动态权限控制

这里要特别注意/**的控制,必须放最后,否则后续的权限可能不起作用

  1. 增加权限控制类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping(value = "/filter")
public class FilterManageController {

//加载自定义的拦截工厂
@Autowired
private ShiroFilterFactoryBean myShiroFilterFactoryBean;

@GetMapping("/all")
public Map<String, String> findAll() {
return myShiroFilterFactoryBean.getFilterChainDefinitionMap();
}

@GetMapping(value = "/add")
public Map<String, String> addFilter() {
Map<String, String> filterMap = new HashMap<>();
filterMap.put("/user/login**", "anon");
filterMap.put("/admin/**", "anon");//把 admin 设置成不需要拦截
filterMap.put("/super/**", "authc,roles[super],perms[super:info]");
//管理员才可以操做
filterMap.put("/users/**", "authc,roles[user]");
filterMap.put("/**", "authc,kickout");
addAndUpdatePermission(filterMap);

return myShiroFilterFactoryBean.getFilterChainDefinitionMap();
}

/**
* 动态更新新的权限, 更新原权限,增加新权限
*
* @param filterMap
*/
private synchronized void addAndUpdatePermission(Map<String, String> filterMap) {

AbstractShiroFilter shiroFilter = null;
try {
shiroFilter = (AbstractShiroFilter) myShiroFilterFactoryBean.getObject();

// 获取过滤管理器
PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter
.getFilterChainResolver();
DefaultFilterChainManager filterManager =
(DefaultFilterChainManager) filterChainResolver.getFilterChainManager();

//清空拦截管理器中的存储
filterManager.getFilterChains().clear();
/*
清空拦截工厂中的存储,如果不清空这里,还会把之前的带进去
ps:如果仅仅是更新的话,可以根据这里的 map 遍历数据修改,重新整理好权限再一起添加
*/
// myShiroFilterFactoryBean.getFilterChainDefinitionMap().clear();

Map<String, String> chains = myShiroFilterFactoryBean.getFilterChainDefinitionMap();
chains.remove("/**");
//把修改后的 map 放进去
filterMap.entrySet().forEach(fm -> {
chains.putIfAbsent(fm.getKey(), fm.getValue());
});
// chains.putAll(filterMap);

//这个相当于是全量添加
for (Map.Entry<String, String> entry : chains.entrySet()) {
//要拦截的地址
String url = entry.getKey().trim().replace(" ", "");
//地址持有的权限
String chainDefinition = entry.getValue().trim().replace(" ", "");
//生成拦截
filterManager.createChain(url, chainDefinition);
}
} catch (Exception e) {
log.error("updatePermission error,filterMap=" + filterMap, e);
}
}
}

  1. 测试 修改前可直接删除用户, 修改权限后,删除用户的角色必须是user

参考

  1. https://blog.csdn.net/fu_fei_wen/article/details/77571989
  2. https://waylau.gitbooks.io/apache-shiro-1-2-x-reference/content/

代码路径

安装

archlinux 安装启动

  • yaourt -S rabbitmq
  • systemctl start rabbitmq.service
  • 测试服务启动
    • rabbitmqctl status
  • 安装管理客户端

centos(阿里ecs)

配置

增加用户并授权

  1. rabbitmqctl adduser admin 123456 # 增加用户并设置用户名密码
  2. rabbitmqctl setusertags admin administrator # 授权
  3. rabbitmqctl setpermissions -p / admin '.*' '.*' '.*' #
    不知道干啥的但是需要不然访问不了

常用操作

相关概念

通常我们谈到队列服务, 会有三个概念: 发消息者、队列、收消息者,RabbitMQ
在这个基本概念之上, 多做了一层抽象, 在发消息者和 队列之间, 加入了交换器
(Exchange). 这样发消息者和队列就没有直接联系,
转而变成发消息者把消息给交换器, 交换器根据调度策略再把消息再给队列。

虚拟主机

一个虚拟主机持有一组交换机、队列和绑定。为什么需要多个虚拟主机呢?很简单,
RabbitMQ 当中,用户只能在虚拟主机的粒度进行权限控制。
因此,如果需要禁止A组访问B组的交换机/队列/绑定,必须为A和B分别创建一个虚拟主机。每一个
RabbitMQ 服务器都有一个默认的虚拟主机”/“。

交换机(Exchange)

交换机的功能主要是接收消息并且转发到绑定的队列,交换机不存储消息,在启用ack模式后,交换机找不到队列会返回错误。交换机有四种类型:Direct,
topic, Headers and Fanout Direct:direct 类型的行为是”先匹配, 再投送”.
即在绑定时设定一个 routingkey, 消息的routingkey 匹配时,
才会被交换器投送到绑定的队列中去, 默认方式
Topic:按规则转发消息(最灵活), 主要通过通配符 Headers:设置 header
attribute 参数类型的交换机 Fanout:转发消息到所有绑定队列

springboot整合

redirexchange 默认

  1. 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  2. 配置rabbit地址, application.yaml

    1
    2
    3
    4
    5
    6
    7
    spring:
    rabbitmq:
    host: localhost
    password: guest
    port: 5672
    username: guest

  3. 增加queue配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class RabbitConfig {

    /**
    * 创建一个叫hello的queue
    * @return
    */
    @Bean
    public Queue queue(){
    return new Queue("ttang");
    }
    }

  4. producer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Component
    @Slf4j
    public class RabbitProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMsgString(){
    String context = "ttang say: " + new Date();
    log.info(context);
    rabbitTemplate.convertAndSend("ttang", context);
    }

    public void sendMsgInt(int i){
    log.info("{}, {}", Thread.currentThread().getStackTrace()[1].getMethodName(), i);
    rabbitTemplate.convertAndSend("ttang", i);
    }
    }

  5. consumer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /**
    *
    * 接收消息
    *
    */
    @Component
    @RabbitListener(queues = "ttang")
    @Slf4j
    public class RabbitConsumer {

    @RabbitHandler
    public void receiveMsgString(String tt){
    log.info("收到ttang消息, {}", tt);
    }

    /**
    * Caused by: org.springframework.amqp.AmqpException: Ambiguous methods for payload type: class java.lang.String:
    * msg and msg2
    * 1. 不能有两个payload同时匹配, 会根据消息类型自动匹配
    * @param tt
    */
    @RabbitHandler
    public void receiveMsgInt(Integer tt){
    log.info("收到ttang消息2, {}", tt);
    }
    }

  6. test

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class RabbitConsumerTest {

    @Autowired
    private RabbitProducer producer;

    @Test
    public void msg() {
    // final ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
    // for (int i = 0; i < 10; i++) {
    // fixedThreadPool.execute(() -> {
    // producer.sendMsgString();
    // });
    // }

    for (int i = 0; i < 10; i++) {
    producer.sendMsgInt(i);
    producer.sendMsgString();
    }
    }
    }

topicexchange 模式匹配订阅

依赖、地址同上

  1. queue,exchange配置, 配置路由规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    /**
    * 需要三个bean实例 queue, exchange, bindexchange
    *
    * queue1比queue2只有一个字母之差,模拟匹配
    */
    @Configuration
    public class TopicExchangeConfig {

    /**
    * 定制主题
    * @return
    */
    @Bean
    public TopicExchange topicExchange(){
    return new TopicExchange("topicExchange");
    }

    @Bean
    public Queue queueMessage(){
    return new Queue("topic.message");
    }

    /**
    * 精确匹配topic.message
    * @param queueMessage
    * @param topicExchange
    * @return
    */
    @Bean
    Binding bindingMessage(Queue queueMessage, TopicExchange topicExchange){
    return BindingBuilder.bind(queueMessage).to(topicExchange).with("topic.message");
    }

    @Bean
    public Queue queueMessages(){
    return new Queue("topic.messages");
    }

    /**
    * 匹配多个
    * *表示一个词.
    * #表示零个或多个词.
    *
    * 这个queue能接收多个消息
    * @param queueMessages
    * @param topicExchange
    * @return
    */
    @Bean
    Binding bindingMessages(Queue queueMessages, TopicExchange topicExchange){
    return BindingBuilder.bind(queueMessages).to(topicExchange).with("topic.#");
    }
    }

  2. 生产者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @Component
    @Slf4j
    public class TopicExchangeProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
    * 同时匹配 topic.message和 topic.# 所以会发到这两个queue里
    * @param msg
    */
    public void sendMessage(String msg){
    final StackTraceElement element = Thread.currentThread().getStackTrace()[1];
    log.info("{}, {}", element.getMethodName(), msg);
    rabbitTemplate.convertAndSend("topicExchange", "topic.message"
    , String.join(",", Arrays.asList(element.getClassName(), element.getMethodName(), msg)));
    }

    /**
    * 只能匹配到topic.#, 多了个s, 所以只会匹配发送到 queuemessages
    * BindingBuilder.bind(queueMessages).to(topicExchange).with("topic.#");
    * 相应的接收也只会是topic.messages
    * @param msg
    */
    public void sendMessages(String msg){
    final StackTraceElement element = Thread.currentThread().getStackTrace()[1];
    log.info("{}, {}", element.getMethodName(), msg);
    rabbitTemplate.convertAndSend("topicExchange", "topic.messages"
    , String.join(",", Arrays.asList(element.getClassName(), element.getMethodName(), msg)));
    }
    }

  3. 消费者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * 监听topic.message
    */
    @Slf4j
    @Component
    @RabbitListener(queues = "topic.message")
    public class TopicConsumerMessage {

    @RabbitHandler
    public void receiveMsgString(String msg){
    final StackTraceElement element = Thread.currentThread().getStackTrace()[1];
    log.info("{}, {}", element.getMethodName(), msg);
    }
    }

    /**
    * 监听topic.message
    */
    @Slf4j
    @Component
    @RabbitListener(queues = "topic.messages")
    public class TopicConsumerMessages {

    @RabbitHandler
    public void receiveMsgString(String msg){
    final StackTraceElement element = Thread.currentThread().getStackTrace()[1];
    log.info("{}, {}", element.getMethodName(), msg);
    }
    }

  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class TopicExchangeProducerTest {

    @Autowired
    private TopicExchangeProducer topicExchangeProducer;

    /**
    * 2019-08-11 12:54:46.482 INFO 18374 --- [ main] c.l.d.s.t.p.TopicExchangeProducer :
    * sendMessage, 我爱你 糖糖
    * <p>
    * <p>
    * 2019-08-11 12:54:46.502 INFO 18374 --- [ntContainer#2-1] c.l.d.s.t.c.TopicConsumerMessages :
    * receiveMsgString, com.lx.demo.springbootrabbitmq.tpcexchange.producer.TopicExchangeProducer,sendMessage,我爱你 糖糖
    * 2019-08-11 12:54:46.503 INFO 18374 --- [ntContainer#1-1] c.l.d.s.t.consumer.TopicConsumerMessage :
    * receiveMsgString, com.lx.demo.springbootrabbitmq.tpcexchange.producer.TopicExchangeProducer,sendMessage,我爱你 糖糖
    * 这里producer发送用的routing_key topic.message匹配topic.message和topic.#, 所以可以干到两个queue里, 相应的消费两个queue的都能接收到
    */
    @Test
    public void sendMessage() {
    topicExchangeProducer.sendMessage("我爱你 糖糖");
    }

    /**
    * 2019-08-11 12:57:00.110 INFO 18687 --- [ main] c.l.d.s.t.p.TopicExchangeProducer :
    * sendMessages, i love you, ttang
    * <p>
    * <p>
    * 2019-08-11 12:57:00.128 INFO 18687 --- [ Thread-2] o.s.a.r.l.SimpleMessageListenerContainer : Waiting
    * for workers to finish.
    * 2019-08-11 12:57:00.128 INFO 18687 --- [ntContainer#2-1] c.l.d.s.t.c.TopicConsumerMessages :
    * receiveMsgString, com.lx.demo.springbootrabbitmq.tpcexchange.producer.TopicExchangeProducer,sendMessages,i
    * love you, ttang
    * messages的消息发送者用的routing_key只能匹配topic.#, 所以只能到topic.messages这个queue
    */
    @Test
    public void sendMessages() {
    topicExchangeProducer.sendMessages("i love you, ttang");
    }
    }

fanoutexchange 发布订阅

依赖、地址同上

  1. queue配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    /**
    * 定义多个queue, 一个Fanoutexchange, 配置绑定binding
    * 这样发送到交换机上的消息都会传递到所有绑定queue
    *
    * 这个最像传统的主题订阅模式
    */
    @Configuration
    public class FanoutExchangeConfig {

    /**
    * 创建交换机
    * @return
    */
    @Bean
    FanoutExchange fanoutExchange(){
    return new FanoutExchange("fanoutExchange");
    }

    /**
    * 创建queue
    * @return
    */
    @Bean
    Queue fanoutQueue1(){
    return new Queue("fanout.1");
    }

    /**
    * 这里不在需要指定binding_key, 就算配了也么得用
    * @param fanoutQueue1
    * @param fanoutExchange
    * @return
    */
    @Bean
    Binding binding1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
    return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    @Bean
    Queue fanoutQueue2(){
    return new Queue("fanout.2");
    }

    @Bean
    Binding binding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
    return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }

    @Bean
    Queue fanoutQueue3(){
    return new Queue("fanout.3");
    }

    /**
    * Description:
    *
    * Parameter 0 of method binding1 in com.lx.demo.springbootrabbitmq.fanoutexchange.config.FanoutExchangeConfig
    * required a single bean, but 6 were found:
    * - queue: defined by method 'queue' in class path resource
    * [com/lx/demo/springbootrabbitmq/direxchange/config/RabbitConfig.class]
    * - fanoutQueue1: defined by method 'fanoutQueue1' in class path resource
    * [com/lx/demo/springbootrabbitmq/fanoutexchange/config/FanoutExchangeConfig.class]
    * - fanoutQueue2: defined by method 'fanoutQueue2' in class path resource
    * [com/lx/demo/springbootrabbitmq/fanoutexchange/config/FanoutExchangeConfig.class]
    * - fanoutQueue3: defined by method 'fanoutQueue3' in class path resource
    * [com/lx/demo/springbootrabbitmq/fanoutexchange/config/FanoutExchangeConfig.class]
    * - queueMessage: defined by method 'queueMessage' in class path resource
    * [com/lx/demo/springbootrabbitmq/tpcexchange/config/TopicExchangeConfig.class]
    * - queueMessages: defined by method 'queueMessages' in class path resource
    * [com/lx/demo/springbootrabbitmq/tpcexchange/config/TopicExchangeConfig.class]
    *
    * @param fanoutQueue3
    * @param fanoutExchange
    * @return
    */
    @Bean
    Binding binding3(Queue fanoutQueue3, FanoutExchange fanoutExchange){
    return BindingBuilder.bind(fanoutQueue3).to(fanoutExchange);
    }

    }

  2. 生产者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Component
    @Slf4j
    public class FanoutExchangeProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
    * fanout的方式不再需要routing-key
    * @param msg
    */
    public void sendMsg(String msg){
    log.info("{}, {}", Thread.currentThread().getStackTrace()[1].getMethodName(), msg);
    // 这里一定要注意函数重载,只有三个参数时,第一个才是exchange
    rabbitTemplate.convertAndSend("fanoutExchange","", msg);
    }
    }

    注意, 发送时是三个参数

  3. 消费者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Slf4j
    @Component
    @RabbitListener(queues = "fanout.1")
    public class FanoutConsumer1 {

    @RabbitHandler
    public void receiveMessageString(String msg){
    log.info("{}, {}", Thread.currentThread().getStackTrace()[1].getMethodName(), msg);
    }
    }

  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class FanoutExchangeProducerTest {

    @Autowired
    private FanoutExchangeProducer fanoutExchangeProducer;

    /**
    * 2019-08-11 13:48:16.402 INFO 26183 --- [ main] c.l.d.s.f.p.FanoutExchangeProducer : sendMsg,
    * 大渣吼,我是范二特
    *
    *
    * 2019-08-11 13:48:16.425 INFO 26183 --- [ntContainer#2-1] c.l.d.s.f.consumer.FanoutConsumer2 :
    * receiveMessageString, 大渣吼,我是范二特
    * 2019-08-11 13:48:16.425 INFO 26183 --- [ntContainer#1-1] c.l.d.s.f.consumer.FanoutConsumer1 :
    * receiveMessageString, 大渣吼,我是范二特
    * 2019-08-11 13:48:16.425 INFO 26183 --- [ntContainer#3-1] c.l.d.s.f.consumer.FanoutConsumer3 :
    * receiveMessageString, 大渣吼,我是范二特
    *
    * 发送一条消息,上述三个消费者都收到了消息
    */
    @Test
    public void sendMsg() {
    fanoutExchangeProducer.sendMsg("大渣吼,我是范二特");
    }
    }

完整代码

https://github.com/microzhao/demo/tree/master/springboot/springboot-rabbitmq

mybatis多数据源

构建主从数据库

  1. 主库写入
  2. 从库读取

springboot多数据源配置

  1. 修改application.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 配置主从数据源
    spring:
    datasource:
    master:
    jdbcUrl: jdbc:mysql://localhost:3306/mybatismaster?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: root
    slaver:
    jdbcUrl: jdbc:mysql://localhost:3306/mybatisslaver?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: root
  2. 修改configuration, 分别指定不同的mapper

    • master
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    package com.lx.demo.config;

    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;

    import javax.sql.DataSource;

    /**
    * 主域配置
    * 包括datasource session等, 都搞一套自己的
    */
    @Configuration
    @MapperScan(basePackages = "com.lx.demo.mapper.master", sqlSessionTemplateRef = "masterSqlSessionTemplate")
    public class DataSourceConfigMaster {

    /**
    * 创建主datasource, 这里多个数据源时必须指定一个为primary
    * @return
    */
    @ConfigurationProperties("spring.datasource.master")
    @Bean(name = "masterDataSource")
    @Primary
    public DataSource dataSource(){
    return DataSourceBuilder.create().build();
    }

    /**
    *
    * @param dataSource
    * @return
    * @throws Exception
    */
    @Bean(name = "masterSqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
    final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    //如果使用xml的方式, 需要设置mybatis配置文件路径
    // sqlSessionFactoryBean.setConfigLocation("");
    return sqlSessionFactoryBean.getObject();
    }

    /**
    * 主域 事务管理器
    * @param dataSource
    * @return
    */
    @Bean(name = "masterDataSourceTransactionManager")
    @Primary
    public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("masterDataSource") DataSource dataSource){
    return new DataSourceTransactionManager(dataSource);
    }

    /**
    * 主 sqlsessiontemplet
    * @param sqlSessionFactory
    * @return
    */
    @Bean(name = "masterSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
    return new SqlSessionTemplate(sqlSessionFactory);
    }
    }

    • slaver
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    package com.lx.demo.config;

    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;

    import javax.sql.DataSource;

    /**
    * 从域配置
    * 包括datasource session等, 都搞一套自己的
    */
    @Configuration
    @MapperScan(basePackages = "com.lx.demo.mapper.slaver", sqlSessionTemplateRef = "slaverSqlSessionTemplate")
    public class DataSourceConfigSlaver {

    /**
    * 创建从datasource, 这里多个数据源时必须指定一个为primary
    * @return
    */
    @ConfigurationProperties("spring.datasource.slaver")
    @Bean(name = "slaverDataSource")
    public DataSource dataSource(){
    return DataSourceBuilder.create().build();
    }

    /**
    *
    * @param dataSource
    * @return
    * @throws Exception
    */
    @Bean(name = "slaverSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("slaverDataSource") DataSource dataSource) throws Exception {
    final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    //如果使用xml的方式, 需要设置mybatis配置文件路径
    // sqlSessionFactoryBean.setConfigLocation("");
    return sqlSessionFactoryBean.getObject();
    }

    /**
    * 从域 事务管理器
    * @param dataSource
    * @return
    */
    @Bean(name = "slaverDataSourceTransactionManager")
    public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("slaverDataSource") DataSource dataSource){
    return new DataSourceTransactionManager(dataSource);
    }

    /**
    * 从 sqlsessiontemplet
    * @param sqlSessionFactory
    * @return
    */
    @Bean(name = "slaverSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("slaverSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
    return new SqlSessionTemplate(sqlSessionFactory);
    }
    }

  3. 分别测试写入读取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class UserController {

    /**
    * 同时注入主从库, 代码控制多数据源使用
    * 该方式只是多来源,其实没有动态切换
    */
    @Autowired
    private UserMapperMaster userMapperMaster;

    @Autowired
    private UserMapperSlaver userMapperSlaver;

    /**
    * 获取数据走从域
    * @param id
    * @return
    */
    @GetMapping("/user/{id}")
    public User selectOne(@PathVariable Long id){
    final User user = userMapperSlaver.getOne(id);
    return user;
    }

    @GetMapping("/users")
    public List<User> users(){
    return userMapperSlaver.getAll();
    }

    /**
    * 写入数据走主域
    */
    @PostMapping("/users")
    public void save(@RequestBody User user){
    userMapperMaster.insert(user);
    }
    }

通过注解切换主从数据源(个数固定)

与上述方式类似,只不过现在依赖一个bean, 然后通过注解方式切换数据源

  1. 主从数据源初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    /**
    * 配置多数据源
    */
    @Configuration
    @MapperScan(basePackages = "com.lx.demo.mapper.dynamic", sqlSessionTemplateRef = "dynamicSqlSessionTemplate")
    public class DataSourceConfigDynamic {

    /**
    * 创建主datasource, 这里多个数据源时必须指定一个为primary
    * @return
    */
    @ConfigurationProperties("spring.datasource.master")
    @Bean
    @Primary
    public DataSource dataSource1(){
    return DataSourceBuilder.create().build();
    }

    /**
    * 创建从datasource, 这里多个数据源时必须指定一个为primary
    * @return
    */
    @ConfigurationProperties("spring.datasource.slaver")
    @Bean
    public DataSource dataSource2(){
    return DataSourceBuilder.create().build();
    }

    /**
    * 动态数据源: 通过AOP在不同数据源之间动态切换
    * @return
    */
    @Bean(name = "dynamicDataSource")
    public DataSource dataSource() {
    DynamicDataSource dynamicDataSource = new DynamicDataSource();
    // 默认数据源
    dynamicDataSource.setDefaultTargetDataSource(dataSource1());

    // 配置多数据源
    Map<Object, Object> dsMap = new HashMap(5);
    dsMap.put("master", dataSource1());
    dsMap.put("slaver", dataSource2());
    dynamicDataSource.setTargetDataSources(dsMap);

    return dynamicDataSource;
    }

    /**
    *
    * @param dataSource
    * @return
    * @throws Exception
    */
    @Bean(name = "dynamicSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
    final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    //如果使用xml的方式, 需要设置mybatis配置文件路径
    // sqlSessionFactoryBean.setConfigLocation("");
    return sqlSessionFactoryBean.getObject();
    }

    /**
    * 主域 事务管理器
    * @param dataSource
    * @return
    */
    @Bean(name = "dynamicDataSourceTransactionManager")
    public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource){
    return new DataSourceTransactionManager(dataSource);
    }

    /**
    * 主 sqlsessiontemplet
    * @param sqlSessionFactory
    * @return
    */
    @Bean(name = "dynamicSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("dynamicSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
    return new SqlSessionTemplate(sqlSessionFactory);
    }
    }

    • 注意,
      这里不要使用注入bean的方式初始化动态数据源,否则会产生循环依赖,直接用方法datasource1()这种即可
  2. 主从数据源管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    /**
    * 主从数据源切换, 保存到threadLocal动态切换
    */
    @Slf4j
    public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
    * 默认数据源
    */
    public static final String DEFAULT_DS = "master";

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    /**
    * 设置数据源
    * @param dbType
    */
    public static void setDB(String dbType) {
    log.debug("切换到{}数据源", dbType);
    contextHolder.set(dbType);
    }

    /**
    *
    * 获取数据源名
    * @return
    */
    public static String getDB() {
    return (contextHolder.get());
    }

    /**
    *
    * 清除数据源名
    */
    public static void clearDB() {
    contextHolder.remove();
    }

    @Override
    protected Object determineCurrentLookupKey() {
    log.debug("数据源为{}", DynamicDataSource.getDB());

    return DynamicDataSource.getDB();
    }

    }

  3. 利用反射执行方法时候动态切换数据源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    /**
    * https://blog.csdn.net/neosmith/article/details/61202084
    */
    @Aspect
    @Component
    public class DynamicDataSourceAspect {

    @Before("@annotation(com.lx.demo.annotation.DS)")
    public void beforeSwitchDS(JoinPoint point) {

    //获得当前访问的class
    Class<?> className = point.getTarget().getClass();

    //获得访问的方法名
    String methodName = point.getSignature().getName();
    //得到方法的参数的类型
    Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes();
    String dataSource = DynamicDataSource.DEFAULT_DS;
    try {
    // 得到访问的方法对象
    Method method = className.getMethod(methodName, argClass);

    // 判断是否存在@DS注解
    if (method.isAnnotationPresent(DS.class)) {
    DS annotation = method.getAnnotation(DS.class);
    // 取出注解中的数据源名
    dataSource = annotation.value();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }

    // 切换数据源
    DynamicDataSource.setDB(dataSource);
    }

    /**
    * Caused by: java.lang.IllegalArgumentException: error Type referred to is not an annotation type: DS
    * @param point
    */
    @After("@annotation(com.lx.demo.annotation.DS)")
    public void afterSwitchDS(JoinPoint point) {
    DynamicDataSource.clearDB();
    }
    }

  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 增加动态主从数据源注解, 配置后自动切换数据源
    * @param user
    */
    @DS("slaver")
    @PostMapping("/users-dy")
    public void saveByDynamic(@RequestBody User user){
    userMapperDynamic.insert(user);
    }

    @DS("slaver")
    @GetMapping("/users-dy")
    public List<User> usersByDynamic(){
    return userMapperDynamic.getAll();
    }


动态数据源(可以任意添加, 暂未实现)

问题列表

  1. 使用mybatisplus实现多数据源目前有问题,会报错
    org.apache.ibatis.binding.BindingException: Invalid bound statement
    (not found): com.yk.yearmeet.modular.service.UserService.list
  2. aspect @annotation(类的全限定名), 否则报错
    https://blog.csdn.net/yangshangwei/article/details/77619875 Caused
    by: java.lang.IllegalArgumentException: error Type referred to is
    not an annotation type: DS 这种方式才可以,
    @Before("@annotation(com.lx.demo.annotation.DS)")

代码路径

快捷键

自动代码

  • 常用的有fori/sout/psvm+Tab即可生成循环、System.out、main方法等boilerplate样板代码
  • 例如要输入for(User user : users)只需输入user.for+Tab
  • 再比如,要输入Date birthday =
    user.getBirthday();只需输入user.getBirthday().var+Tab即可。代码标签输入完成后,按Tab,生成代码。
  • Ctrl+Alt+O 优化导入的类和包, 清理import
  • Alt+Insert 生成代码(如get,set方法,构造函数等) 或者右键(Generate)
  • fori/sout/psvm + Tab
  • Ctrl+Alt+T 生成try catch 或者 Alt+enter
  • CTRL+ALT+T 把选中的代码放在 TRY{} IF{} ELSE{} 里
  • Ctrl + O 重写方法
  • Ctrl + I 实现方法
  • Ctr+shift+U 大小写转化
  • ALT+回车 导入包,自动修正
  • ALT+/ 代码提示
  • CTRL+J 自动代码
  • Ctrl+Shift+J,整合两行为一行
  • CTRL+空格 代码提示
  • CTRL+SHIFT+SPACE 自动补全代码
  • CTRL+ALT+L 格式化代码
  • CTRL+ALT+I 自动缩进
  • CTRL+E 最近更改的代码
  • CTRL+ALT+SPACE 类名或接口名提示
  • CTRL+P 方法参数提示
  • CTRL+Q,可以看到当前方法的声明
  • Shift+F6 重构重命名 (包、类、方法、变量、甚至注释等)
  • Ctrl+Alt+V 提取变量
  • 提取当前选择为变量(extract variable):CTRL+ALT+V
  • 提取当前选择为属性(extract field):CTRL+ALT+F
  • 提取当前选择为常量(extract constant):CTRL+ALT+C
  • 提取当前选择为方法(extract method):CTRL+ALT+M
  • 提取当前选择为方法参数(extract parameter):CTRL+ALT+P
  • 重构类、方法(change signarture):CTRL+F6
  • 提取代码块至if、try等结构中(surround with):CTRL+ALT+T
  • 创建模块文件等(new):CTRL+ALT+N
  • 创建测试用例(test):CTRL+SHIFT+T
  • 重构菜单(refactor for this):CTRL+T

查询快捷键

  • Ctrl+Shift+Backspace可以跳转到上次编辑的地
  • CTRL+ALT+ left/right 前后导航编辑过的地方
  • ALT+7 靠左窗口显示当前文件的结构
  • Ctrl+F12 浮动显示当前文件的结构, 查找当前类中的方法
  • ALT+F7 找到你的函数或者变量或者类的所有引用到的地方
  • CTRL+ALT+F7 找到你的函数或者变量或者类的所有引用到的地方
  • Ctrl+Shift+Alt+N 查找类中的方法或变量
  • 双击SHIFT 在项目的所有目录查找文件
  • Ctrl+N 查找类
  • Ctrl+Shift+N 查找文件
  • CTRL+G 定位行
  • CTRL+F 在当前窗口查找文本
  • CTRL+SHIFT+F 在指定窗口查找文本
  • CTRL+R 在 当前窗口替换文本
  • CTRL+SHIFT+R 在指定窗口替换文本
  • ALT+SHIFT+C 查找修改的文件
  • CTRL+E 最近打开的文件
  • F3 向下查找关键字出现位置
  • SHIFT+F3 向上一个关键字出现位置
  • 选中文本,按Alt+F3 ,高亮相同文本,F3逐个往下查找相同文本
  • F4 查找变量来源
    • //CTRL+SHIFT+O 弹出显示查找内容
    • Ctrl+W 选中代码,连续按会有其他效果, vim shift %
  • F2 或Shift+F2 高亮错误或警告快速定位
  • Ctrl+Up/Down 光标跳转到第一行或最后一行下
    • Ctrl+B 快速打开光标处的类或方法
  • CTRL+ALT+B 找所有的子类
  • CTRL+SHIFT+B 找变量的类
    • Ctrl+Shift+上下键 上下移动代码
  • Ctrl+X 删除行
  • Ctrl+D 复制行
  • Ctrl+/ 或 Ctrl+Shift+/ 注释(// 或者/*…*/ )
    • Ctrl+H 显示类结构图
  • Ctrl+Q 显示注释文档
    • Alt+F1 查找代码所在位置
  • Alt+1 快速打开或隐藏工程面板
    • Alt+ left/right 切换代码视图
  • ALT+ ↑/↓ 在方法间快速移动定位
  • CTRL+ALT+ left/right 前后导航编辑过的地方
  • Ctrl+Shift+Backspace可以跳转到上次编辑的地
  • Alt+6 查找TODO

其他快捷键

  • SHIFT+ENTER 另起一行
  • CTRL+Z 倒退(撤销)
  • CTRL+SHIFT+Z 向前(取消撤销)
  • CTRL+ALT+F12 资源管理器打开文件夹
  • ALT+F1 查找文件所在目录位置
  • SHIFT+ALT+INSERT 竖编辑模式
  • CTRL+F4 关闭当前窗口
  • Ctrl+Alt+V,可以引入变量。例如:new String(); 自动导入变量定义
  • Ctrl+~,快速切换方案(界面外观、代码风格、快捷键映射等菜单),
    可以切换主题

svn快捷键

  • ctrl+k 提交代码到SVN
  • ctrl+t 更新代码

调试快捷键

  • 其实常用的 就是F8 F7 F9 最值得一提的 就是Drop Frame
    可以让运行过的代码从头再来
  • alt+F8 debug时选中查看值
  • Alt+Shift+F9,选择 Debug
  • Alt+Shift+F10,选择 Run
  • Ctrl+Shift+F9,编译
  • Ctrl+Shift+F8,查看断点
    • F7,步入
  • Shift+F7,智能步入
  • Alt+Shift+F7,强制步入
  • F8,步过
  • Shift+F8,步出
  • Alt+Shift+F8,强制步过
    • Alt+F9,运行至光标处
  • Ctrl+Alt+F9,强制运行至光标处
  • F9,恢复程序
  • Alt+F10,定位到断点

重构

  • Ctrl+Alt+Shift+T,弹出重构菜单
  • Shift+F6,重命名
  • F6,移动
  • F5,复制
  • Alt+Delete,安全删除
  • Ctrl+Alt+N,内联

十大Intellij IDEA快捷键

Intellij
IDEA中有很多快捷键让人爱不释手,stackoverflow上也有一些有趣的讨论。每个人都有自己的最爱,想排出个理想的榜单还真是困难。以前也整理过Intellij的快捷键,这次就按照我日常开发时的使用频率,简单分类列一下我最喜欢的十大快捷神键吧。

  1. 智能提示:
    Intellij首当其冲的当然就是Intelligence智能!基本的代码提示用Ctrl+Space,还有更智能地按类型信息提示Ctrl+Shift+Space,但因为Intellij总是随着我们敲击而自动提示,所以很多时候都不会手动敲这两个快捷键(除非提示框消失了)。用F2/
    Shift+F2移动到有错误的代码,Alt+Enter快速修复(即Eclipse中的Quick
    Fix功能)。当智能提示为我们自动补全方法名时,我们通常要自己补上行尾的反括号和分号,当括号嵌套很多层时会很麻烦,这时我们只需敲Ctrl+Shift+Enter就能自动补全末尾的字符。而且不只是括号,例如敲完if/for时也可以自动补上{}花括号。最后要说一点,Intellij能够智能感知Spring、Hibernate等主流框架的配置文件和类,以静制动,在看似”静态”的外表下,智能地扫描理解你的项目是如何构造和配置的。

  2. 重构:
    Intellij重构是另一完爆Eclipse的功能,其智能程度令人瞠目结舌,比如提取变量时自动检查到所有匹配同时提取成一个变量等。尤其看过《重构改善既有代码设计》之后,有了Intellij的配合简直是令人大呼过瘾!也正是强大的智能和重构功能,使Intellij下的TDD开发非常顺畅。切入正题,先说一个无敌的重构功能大汇总快捷键Ctrl+Shift+Alt+T,叫做Refactor
    This。按法有点复杂,但也符合Intellij的风格,很多快捷键都要双手完成,而不像Eclipse不少最有用的快捷键可以潇洒地单手完成(不知道算不算Eclipse的一大优点),但各位用过Emacs的话就会觉得也没什么了(非Emacs黑)。此外,还有些最常用的重构技巧,因为太常用了,若每次都在Refactor
    This菜单里选的话效率有些低。比如Shift+F6直接就是改名,Ctrl+Alt+V则是提取变量。

  3. 代码生成:
    这一点类似Eclipse,虽不是独到之处,但因为日常使用频率极高,所以还是罗列在榜单前面。常用的有fori/sout/psvm+Tab即可生成循环、System.out、main方法等boilerplate样板代码,用Ctrl+J可以查看所有模板。后面”辅助”一节中将会讲到Alt+Insert,在编辑窗口中点击可以生成构造函数、toString、getter/setter、重写父类方法等。这两个技巧实在太常用了,几乎每天都要生成一堆main、System.out和getter/setter。另外,Intellij
    IDEA 13中加入了后缀自动补全功能(Postfix
    Completion),比模板生成更加灵活和强大。例如要输入for(User user :
    users)只需输入user.for+Tab。再比如,要输入Date birthday =
    user.getBirthday();只需输入user.getBirthday().var+Tab即可。

  4. 编辑:
    编辑中不得不说的一大神键就是能够自动按语法选中代码的Ctrl+W以及反向的Ctrl+Shift+W了。此外,Ctrl+Left/Right移动光标到前/后单词,Ctrl+[/]移动到前/后代码块,这些类Vim风格的光标移动也是一大亮点。以上Ctrl+Left/Right/[]加上Shift的话就能选中跳跃范围内的代码。Alt+Forward/Backward移动到前/后方法。还有些非常普通的像Ctrl+Y删除行、Ctrl+D复制行、Ctrl+</>折叠代码就不多说了。关于光标移动再多扩展一点,除了Intellij本身已提供的功能外,我们还可以安装ideaVim或者emacsIDEAs享受到Vim的快速移动和Emacs的AceJump功能(超爽!)。另外,Intellij的书签功能也是不错的,用Ctrl+Shift+Num定义110书签(再次按这组快捷键则是删除书签),然后通过Ctrl+Num跳转。这避免了多次使用前/下一编辑位置Ctrl+Left/Right来回跳转的麻烦,而且此快捷键默认与Windows热键冲突(默认多了Alt,与Windows改变显示器显示方向冲突,一不小心显示器就变成倒着显式的了,冏啊)。

  5. 查找打开:
    类似Eclipse,Intellij的Ctrl+N/Ctrl+Shift+N可以打开类或资源,但Intellij更加智能一些,我们输入的任何字符都将看作模糊匹配,省却了Eclipse中还有输入*的麻烦。最新版本的IDEA还加入了Search
    Everywhere功能,只需按Shift+Shift即可在一个弹出框中搜索任何东西,包括类、资源、配置项、方法等等。类的继承关系则可用Ctrl+H打开类层次窗口,在继承层次上跳转则用Ctrl+B/Ctrl+Alt+B分别对应父类或父方法定义和子类或子方法实现,查看当前类的所有方法用Ctrl+F12。要找类或方法的使用也很简单,Alt+F7。要查找文本的出现位置就用Ctrl+F/Ctrl+Shift+F在当前窗口或全工程中查找,再配合F3/Shift+F3前后移动到下一匹配处。Intellij更加智能的又一佐证是在任意菜单或显示窗口,都可以直接输入你要找的单词,Intellij就会自动为你过滤。

  6. 其他辅助:
    以上这些神键配上一些辅助快捷键,即可让你的双手90%以上的时间摆脱鼠标,专注于键盘仿佛在进行钢琴表演。这些不起眼却是至关重要的最后一块拼图有:

    • 命令:Ctrl+Shift+A可以查找所有Intellij的命令,并且每个命令后面还有其快捷键。所以它不仅是一大神键,也是查找学习快捷键的工具。
    • 新建:Alt+Insert可以新建类、方法等任何东西。
    • 格式化代码:格式化import列表Ctrl+Alt+O,格式化代码Ctrl+Alt+L。
    • 切换窗口:Alt+Num,常用的有1项目结构,3搜索结果,4/5运行调试。Ctrl+Tab切换标签页,Ctrl+E/Ctrl+Shift+E打开最近打开过的或编辑过的文件。
    • 单元测试:Ctrl+Shift+T创建单元测试用例。
    • 运行:Alt+Shift+F10运行程序,Shift+F9启动调试,Ctrl+F2停止。
    • 调试:F7/F8/F9分别对应Step into,Step over,Continue。

    此外还有些我自定义的,例如水平分屏Ctrl+|等,和一些神奇的小功能Ctrl+Shift+V粘贴很早以前拷贝过的,Alt+Shift+Insert进入到列模式进行按列选中。

    • Top #10切来切去:Ctrl+Tab
    • Top #9选你所想:Ctrl+W
    • Top #8代码生成:Template/Postfix +Tab
    • Top #7发号施令:Ctrl+Shift+A
    • Top #6无处藏身:Shift+Shift
    • Top #5自动完成:Ctrl+Shift+Enter
    • Top #4创造万物:Alt+Insert

    太难割舍,前三名并列吧!

    • Top #1智能补全:Ctrl+Shift+Space
    • Top #1自我修复:Alt+Enter
    • Top #1重构一切:Ctrl+Shift+Alt+T

全选复选框

  1. shift全部选中
  2. 空格勾选

一些实用的设置

  • live templete: idea中可以自定义一些自动生成方式, 很强大,
    类似sout生成, 目前将用户配置user.xml备份上传

  • 隐藏页签 setting–>editor–>general–>placement/none

  • 无法输入中文, fcitx, 修改idea安装目录下的执行命令idea.sh,
    在最前面加入与.xinitc一样的一些fcitx的环境变量

    • vim /usr/bin/idea.sh

    • 开头加入

      #!/bin/sh
      export XMODIFIERS="@im=fcitx"
      export GTK_IM_MODULE="fcitx"
      export QT_IM_MODULE="fcitx"
      if [ -z "$IDEA_JDK" ] ; then
              IDEA_JDK="/usr/lib/jvm/java-8-openjdk"
      fi
      exec env IDEA_JDK=$IDEA_JDK /usr/share/intellijidea-ce/bin/idea.sh $@
      
    • 使用的jdk版本可以加入环境变量 export IDEAJDK="$JAVAHOME"

    • 注意,确认下idea.sh中引号是否正确,不然还是不能输入中文

  • idea重新安装后无法import project, 改用社区版本+ remote断点的方式

    • 首先在weblogic域中打开debugflag(默认端口8453)(setdomainenv),
      或者将内容复制到startweblogic.sh启动参数中

      1
      2
      JAVA_DEBUG="-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=4000,server=y,suspend=n"
      export JAVA_DEBUG
    • 启动weblogic服务后,idea中新建remote,
      修改访问的地址以及端口即可, 注意,一定要指定modu

  • idea vim插件 :actionlist 显示所有的快捷键,(好牛逼..)

  • ctrl-shift v: idea的历史剪贴板

  • 神奇的Inject language

    • Inject language or reference。
    • alt-enter
    • enter
    • json file
    • alter-enter – > json file fragment
    • 编辑完成后ctrl-f4退出
  • 擦屁股快捷键ctrl+shift+enter

  • ctrl-w alt-j批量修改

  • 代码在项目中的定位: alt-f1 –> 选择project –> enter, 如果还想回去
    f4

  • idea配置同步, setting>repository

    1. github建立一个新的项目
    2. setting–> resp中url使用这个项目
    3. 这就是个github项目随便玩.
  • 隐藏页签, 在页签右键,none

  • 社区版没有 import setting选项,所以安装主题ui就直接搜plugin,
    然后找对应主题名即可

修改host, 防止idea检测注册信息

127.0.0.1 localhost.localdomain localhost arch ::1 localhost.localdomain
localhost arch ### idea 0.0.0.0 account.jetbrains.com

插件推荐

  • ideavim

  • lombok 可选, 定义bean的时候不用在写getset

  • Alibaba Java Coding Guidelines plugin 阿里规范

  • findbugs 避免低级bug

  • maven helper据说很吊, 暂时没用, 排查maven依赖的

  • generateallsetter 自定调用一个bean的set方法

    1
    2
    3
    User user = new User();
    alt-enter
    选择自动生成
  • key promoter 这个插件是告诉你个傻x, 输入几次了还及不住快捷键

0%