结构修改
@ -1,182 +0,0 @@
|
|||||||
---
|
|
||||||
id="2018-10-20-10-38-05"
|
|
||||||
title="spring基础"
|
|
||||||
headWord="spring是为了解决企业级应用开发的复杂性而创建的,spring最根本的使命是:简化Java开发。为降低开发复杂性有以下四种关键策略。 "
|
|
||||||
tags=["java", "spring"]
|
|
||||||
category="java"
|
|
||||||
serie="spring学习"
|
|
||||||
---
|
|
||||||
[id]:2018-08-12_1
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring
|
|
||||||
|
|
||||||
  spring是为了解决企业级应用开发的复杂性而创建的,spring最根本的使命是:简化Java开发。为降低开发复杂性有以下四种关键策略。
|
|
||||||
|
|
||||||
- 基于POJO的轻量级和最小侵入性编程
|
|
||||||
- 通过依赖注入和面向接口实现松耦合
|
|
||||||
- 基于切面和惯例进行声明式编程
|
|
||||||
- 通过切面和模板减少样板式代码
|
|
||||||
|
|
||||||
#### 1.依赖注入
|
|
||||||
|
|
||||||
  假设类A依赖类B,通常做法是在类A中声明类B,然后使用,这样一方面具有极高的耦合性,将类A与类B绑定在一起;另一方面也让单元测试变得很困难,无法在A外部获得B的执行情况。
|
|
||||||
|
|
||||||
  通过依赖注入,对象的依赖管理将不用对象本身来管理,将由一个第三方组件在创建对象时设定,依赖关系将被自动注入到对应的对象中去。
|
|
||||||
|
|
||||||
#### 2.创建应用上下文
|
|
||||||
|
|
||||||
- `ClassPathXmlApplicationContext()`从类路径创建
|
|
||||||
- `FileSystemXmlApplicationContext()`读取文件系统下的xml配置
|
|
||||||
- `XmlWebApplicationContext()` 读取web应用下的XML配置文件并装载上下文定义
|
|
||||||
|
|
||||||
#### 3.声明Bean
|
|
||||||
|
|
||||||
1. 最简单
|
|
||||||
|
|
||||||
`<bean id="bean1" class="com.example.Class"/>`
|
|
||||||
|
|
||||||
2. 带构造器
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean id="bean1" class="com.example.Class">
|
|
||||||
<contructor-arg value="12"/> //基本数据类型使用value
|
|
||||||
<contructor-arg ref="bean2"/> //对象使用ref
|
|
||||||
</bean>
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 通过工厂方法创建
|
|
||||||
|
|
||||||
如果想声明的Bean没有一个公开的构造函数,通过factory-method属性来装配工厂生产的Bean
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean id="bean1" class="com.example.class" factory-method="getInstance"/>//getInstance为获取实例的静态方法。
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.Bean的作用域
|
|
||||||
|
|
||||||
所有Spring Bean默认都是单例的。通过配置scope属性为prototype可每次请求产生一个新的实例。
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean id="bean3" class="com.example.class" scope="prototype">
|
|
||||||
```
|
|
||||||
|
|
||||||
scope可选值:
|
|
||||||
|
|
||||||
- `singleton`:每个容器中,一个Bean对象只有一个实例。(**默认**)
|
|
||||||
- `prototype`:允许实例化任意次 ,每次请求都会创建新的
|
|
||||||
- `request`:作用域为一次http请求
|
|
||||||
- `session`:作用域为一个http session会话
|
|
||||||
- `global-session`:作用域为一个全局http session,仅在Protlet上下文中有效
|
|
||||||
|
|
||||||
#### 5.初始化和销毁Bean
|
|
||||||
|
|
||||||
当实例化需要执行初始化操作,或者销毁时需要执行清理工作。两种实现方式:
|
|
||||||
|
|
||||||
1. xml配置,类中编写初始化方法和销毁方法,在bean中定义。
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean id="bean4" class="com.example.Class" init-method="start" destroy-method="destroy"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
也可在Beans中定义默认初始化和销毁方法。
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<beans . . . default-init-method="" default-destroy-method=""/>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 实现`InitializingBean `和`DisposableBean`接口
|
|
||||||
|
|
||||||
#### 6.setter注入
|
|
||||||
|
|
||||||
在bean中使用`<property>`元素配置属性,使用方法类似于`<constructor-arg>`
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<property name="name" value="fxg"/> //注入基本数据类型
|
|
||||||
<property name="sex" ref="sex"/> //注入类
|
|
||||||
```
|
|
||||||
|
|
||||||
可使用p简写,**-ref**后缀说明装配的是一个引用
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean id="bean5" class="com.example.class"
|
|
||||||
p:name="fxb"
|
|
||||||
p:sex-ref="sex"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.注入内部Bean
|
|
||||||
|
|
||||||
既定义其他Bean内部的Bean,避免共享问题,可在属性节点或者构造器参数节点上使用。
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<property name="sex">
|
|
||||||
<bean class="com.example.sex"/> //没有id属性,因为不会被其他bean使用
|
|
||||||
</property>
|
|
||||||
<constructor-arg>
|
|
||||||
<bean class="com.example.sex"/>
|
|
||||||
</constructor-arg>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 8.装配集合
|
|
||||||
|
|
||||||
| 集合元素 | 用途 |
|
|
||||||
| ---------------- | ------------------------------ |
|
|
||||||
| \<list\> | 装配list类型,允许重复 |
|
|
||||||
| \<set\> | set,不能重复 |
|
|
||||||
| \<map\> | map类型 |
|
|
||||||
| \<props\> | properties类型,键值都为String |
|
|
||||||
|
|
||||||
- list
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<property name="instruments">
|
|
||||||
<list>
|
|
||||||
<ref bean="guitar"/>
|
|
||||||
<ref bean="cymbal"/>
|
|
||||||
<ref bean="harmonica"/>
|
|
||||||
</list>
|
|
||||||
</property>
|
|
||||||
<ref>用来定义上下文中的其他引用,还可使用<value>,<bean>,<null/>
|
|
||||||
```
|
|
||||||
|
|
||||||
- set
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<set>
|
|
||||||
<ref bean="fasdf"/>
|
|
||||||
</set>
|
|
||||||
```
|
|
||||||
|
|
||||||
用法和list相同,只是不能重复
|
|
||||||
|
|
||||||
- Map
|
|
||||||
|
|
||||||
```XML
|
|
||||||
<map>
|
|
||||||
<entry key="GUITAR" value-ref="guitar"/>
|
|
||||||
</map>
|
|
||||||
```
|
|
||||||
|
|
||||||
entry元素由一个key,一个value组成,分别有两种形式。
|
|
||||||
|
|
||||||
| key | 键为String |
|
|
||||||
| :-------- | -------------- |
|
|
||||||
| key-ref | 键为Bean的引用 |
|
|
||||||
| value | 值为String |
|
|
||||||
| value-ref | 值为Bean的引用 |
|
|
||||||
|
|
||||||
- props
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<props>
|
|
||||||
<prop key="GUITAR">guitar</prop>
|
|
||||||
</props>
|
|
||||||
```
|
|
||||||
|
|
||||||
键值都是String
|
|
||||||
|
|
||||||
#### 9.装配空值
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<property name="name"><null/></property>
|
|
||||||
```
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
|||||||
---
|
|
||||||
id="2018-10-21-10-38-05"
|
|
||||||
title="spring之最小化XML配置"
|
|
||||||
headWord="spring是为了解决企业级应用开发的复杂性而创建的,但是最初的Spring在随着应用程序的规模越来越大的情况下,xml配置文件也随之膨胀,变得不便于阅读与管理,随后就有了一系列的手段来减少xml配置,直到一行都没有"
|
|
||||||
tags=["java", "spring"]
|
|
||||||
category="java"
|
|
||||||
serie="spring学习"
|
|
||||||
---
|
|
||||||
## 一、自动装配
|
|
||||||
|
|
||||||
### 1、四种类型的自动装配
|
|
||||||
|
|
||||||
| 类型 | 解释 | xml配置 |
|
|
||||||
| ---------- | ------------------------------------ | ---------------------------------------------- |
|
|
||||||
| byName | 根据Bean的name或者id | \<bean id="bean" class="…" autowire="byName"/> |
|
|
||||||
| ByType | 根据Bean类型自动装配 | \<bean id="bean" class="…" autowire="byType"/> |
|
|
||||||
| contructor | 根据Bean的构造器入参具有相同类型 | 同上 |
|
|
||||||
| Autodetect | 首先使用contructor,失败再尝试byType | 同上 |
|
|
||||||
|
|
||||||
  byType在出现多个匹配项时不会自动选择一个然是报错,为避免报错,有两种办法:1.使用\<bean>元素的primary属性,设置为首选Bean,但所有bean的默认primary都是true,因此我们需要将所有非首选Bean设置为false;2.将Bean的`autowire-candidate`熟悉设置为**false **,取消 这个Bean的候选资格,这个Bean便不会自动注入了。
|
|
||||||
|
|
||||||
  contructor自动装配和byType有一样的局限性,当发现多个Bean匹配某个构造器入参时,Spring不会尝试选择其中一个;此外,如果一个类有多个构造器都满足自动装配的条件,Spring也不会猜测哪个更合适使用。
|
|
||||||
|
|
||||||
### 2、默认自动装配
|
|
||||||
|
|
||||||
  如果需要为Spring应用上下文中的每个Bean(或者其中的大多数)配置相同的autowire属性,可以在根元素\<beans>上增加一个default-autowire属性,默认该属性设置为none。该属性只应用于指定配置文件中的所有Bean,并不是Spring上下文中的所有Bean。
|
|
||||||
|
|
||||||
### 3、混合使用自动装配和显式装配
|
|
||||||
|
|
||||||
  当我们对某个Bean使用了自动装配策略,并不代表我们不能对该Bean的某些属性进行显示装配,任然可以为任意一个属性配置\<property>元素,显式装配将会覆盖自动装配。**但是**当使用constructor自动装配策略时,我们必须让Spring自动装配构造器所有入参,不能使用\<constructor-arg>元素进行混合。
|
|
||||||
|
|
||||||
## 二、注解装配
|
|
||||||
|
|
||||||
  从Spring2.5开始,可以使用注解自动装配Bean的属性,使用注解允许更细粒度的自动装配,可选择性的标注某一个属性来对其应用自动装配。Spring容器默认禁用注解装配,需要在Spring配置中启用,最简单的启用方式是使用Spring的context命令空间配置中的`<context:annotation-config>`,如下所示:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<beans ...>
|
|
||||||
<context:annotation-config/>
|
|
||||||
<!-- bean declarations go here -->
|
|
||||||
</beans>
|
|
||||||
```
|
|
||||||
|
|
||||||
  Spring3支持几种不同的用于自动装配的注解:
|
|
||||||
|
|
||||||
- Spring自带的@Autowired注解
|
|
||||||
- JSR-330的@Inject注解
|
|
||||||
- JSR-250的@Resource注解
|
|
||||||
|
|
||||||
### 1、使用@Autowired
|
|
||||||
|
|
||||||
  @Autowired用于对被注解对象启动ByType的自动装配,可用于以下对象:
|
|
||||||
|
|
||||||
- 类属性,即使私有属性也能注入
|
|
||||||
- set方法
|
|
||||||
- 构造器
|
|
||||||
- 任意需要装配Bean的方法
|
|
||||||
|
|
||||||
在使用@Autowired时有两种情况会出错:没有匹配的Bean和存在多个匹配的Bean,但是都有对应的解决方法。
|
|
||||||
|
|
||||||
- 当没有匹配Bean时,自动装配会抛出NoSuchBeanDefinitionException,如果不想抛出可使用required属性,设置为false来配置可选的自动装配,即装配失败就不进行装配,不会报错。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Autowired(required=false)
|
|
||||||
```
|
|
||||||
|
|
||||||
当使用构造器配置时,只有一个构造器可以将required属性设置为true,其他都只能设置为false。此外,当使用注解标注多个构造器时,Spring会从所有满足装配条件的构造器中选择入参最多的那个。
|
|
||||||
|
|
||||||
- 当存在多个Bean满足装配条件时,Spring也会抛出NoSuchBeanDefinitionException错误,为了选择指定的Bean,我们可以使用@Qualifier注解进行筛选:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("name1")//筛选名为name1的Bean
|
|
||||||
private TestClass testClass;
|
|
||||||
```
|
|
||||||
|
|
||||||
除了通过Bean的ID来缩小选择范围,我们还可以通过直接在Bean上使用qualifier来缩小范围,限制Bean的类型,xml如下:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean class="com.test.xxx">
|
|
||||||
<qualifier value="stringed"/>
|
|
||||||
</bean>
|
|
||||||
```
|
|
||||||
|
|
||||||
注解如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Qualifier("stringed")
|
|
||||||
public class xxx{}
|
|
||||||
```
|
|
||||||
|
|
||||||
还可以创建**自定义限定器(Qualifier)**
|
|
||||||
|
|
||||||
  创建自定义限定器只需要使用@Qualifier注解作为它的源注解即可,如下创建了一个Stringed限定器:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Target({ElementType.FIELD,ElementType.PARAMETER,ElementType.TYPE})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Qualifier
|
|
||||||
public @interface Stringed{}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后使用它注解一个Bean:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Stringed
|
|
||||||
public class Guitar{}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后就可以进行限定了:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Autowired
|
|
||||||
@Stringed
|
|
||||||
private Guitar guitar;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2、使用@Inject自动注入
|
|
||||||
|
|
||||||
  为统一各种依赖注入框架的编程模型,JCP(Java Community Process)发布的Java依赖注入规范,被称为JSR-330,从Spring3开始,Spring已经开始兼容该依赖注入模型。
|
|
||||||
|
|
||||||
  和@Autowired一样,@Inject可以用来自动装配属性、方法和构造器。但是@Inject没有required属性,因此依赖关系必须存在,如不存在将抛出异常。
|
|
||||||
|
|
||||||
  JSR-330还提供另一种注入技巧,注入一个Provider。Provider接口可以实现Bean引用的延迟注入以及注入Bean的多个实例等功能。
|
|
||||||
|
|
||||||
  例如我们有一个KnifeJuggler类需要注入一个或多个Knife实例,假设Knife Bean的作用域声明为prototype,下面的KnifeJuggler的构造器将获得多个Knife Bean:
|
|
||||||
|
|
||||||
```java
|
|
||||||
private Set<Knife> knifes;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public KnifeJuggler(Provider<Knife> knifeProvider){
|
|
||||||
knives = new HashSet<Knife>();
|
|
||||||
for(int i=0;i<5;i++){
|
|
||||||
knives.add(knifeProvider.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
  相对于@Autowired所对应的@Qualifier,@Inject对应的是@Named注解。事实上JSR-330中也有@Qualifier注解,不过不建议直接使用,建议通过该注解来创建自定义的限定注解,和Spring的@Qualifier创建过程类似。
|
|
||||||
|
|
||||||
### 3、注解中使用表达式
|
|
||||||
|
|
||||||
  Spring3中引入的`@Value`属性可用来装配String类型的值和基本类型的值。借助SpEL表达式,@Value不光可以装配硬编码值还可以在运行期动态计算表达式并装配,例如下面的:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Value("#{systemProperties.name}")
|
|
||||||
private String name;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 三、自动检测Bean
|
|
||||||
|
|
||||||
  在Spring中使用上面说到的`<context:annotation-config>`,可以做到自动装配,但还是要在xml中申明Bean。Spring还有另一个元素`<context:component-scan>`,元素除了完成自动装配的功能,还允许Spring自动检测Bean和定义Bean ,用法如下:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<beans ...>
|
|
||||||
<context:component-scan base-package="com.springtest">
|
|
||||||
</context:component-scan>
|
|
||||||
</beans>
|
|
||||||
```
|
|
||||||
|
|
||||||
开启后支持如下注解:
|
|
||||||
|
|
||||||
| 注解 | 解释 |
|
|
||||||
| ----------- | ------------------------------------ |
|
|
||||||
| @Component | 通用的构造型注解,标识类为Spring组件 |
|
|
||||||
| @Controller | 标识该类定义为Spring MVC controller |
|
|
||||||
| @Repository | 标识该类定义为数据仓库 |
|
|
||||||
| @Service | 标识该类定义为服务 |
|
|
||||||
|
|
||||||
  使用上述注解是Bean的ID默认为无限定类名。使用`@Component("name")`指定ID。
|
|
||||||
|
|
||||||
### 1、过滤组建扫描
|
|
||||||
|
|
||||||
  通过为<context:component-scan >配置<context:include-filter>和<context:exclude-filter>子元素,我们可以随意调整扫描行为。下面的配置自动注册所有的TestInterface实现类:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<context:component-scan base-package="com.fxb.springtest">
|
|
||||||
<context:include-filter type="assignable"
|
|
||||||
expression="com.fxb.springTest.TestInterface"/>
|
|
||||||
</context:component-scan>
|
|
||||||
```
|
|
||||||
|
|
||||||
其中的type和expression属性一起协作来定义组件扫描策略。type有以下值可选择:
|
|
||||||
|
|
||||||
| 过滤器类型 | 描述 |
|
|
||||||
| ---------- | ------------------------------------------------------------ |
|
|
||||||
| annotation | 过滤器扫描使用指定注解所标注的类。通过expression属性指定要扫描的注解 |
|
|
||||||
| assignable | 过滤器扫描派生于expression属性所指定类型的那些类 |
|
|
||||||
| aspectj | 过滤器扫描于expression属性所指定的AspectJ表达式所匹配的那些类 |
|
|
||||||
| custom | 使用自定义的org.springframework.core.type.TypeFilter实现类,该类由expression属性指定 |
|
|
||||||
| regex | 过滤器扫描类的名称与expression属性所指定的正则表达式所匹配的类 |
|
|
||||||
|
|
||||||
  exclude-filter使用和include-filter类似,只是效果相反。
|
|
||||||
|
|
||||||
## 四、使用Spring基于Java的配置
|
|
||||||
|
|
||||||
  在Spring3.0中几乎可以不使用XML而使用纯粹的Java代码来配置Spring应用。
|
|
||||||
|
|
||||||
- 首先还是需要极少量的XML来启用Java配置,就是上面说到的`<context:component-scan>`,该标签还会自动加载使用`@Configuration`注解所标识的类
|
|
||||||
|
|
||||||
- @Configuration注解相当于XML配置中的\<beans>元素,这个注解将会告知Spring:这个类包含一个或多个Spring Bean的定义,这些定义是使用@Bean注解所标注的方法
|
|
||||||
|
|
||||||
- 申明一个简单的Bean代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class TestConfig{
|
|
||||||
@Bean
|
|
||||||
public Animal duck(){
|
|
||||||
return new Ducker();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
@Bean告知Spring这个方法将返回一个对象,该对象应该被注册为Spring应用上下文中的一个Bean,方法名作为该Bean的ID 。想要使用另一个Bean的引用也很简单,如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Bean
|
|
||||||
public Food duckFood(){
|
|
||||||
return new DuckFood();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean //通过方法名引用一个Bean,并不会创建一个新的实例
|
|
||||||
public Animal duck(){
|
|
||||||
return new Ducker(DuckFood());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 五、小结
|
|
||||||
|
|
||||||
  终于写完了spring 的最小化配置,对spring的各种注解也有了一些了解,再不是之前看到注解一脸莫名其妙了,虽然现在Springboot已经帮我们做了零XML配置,但觉得还是有必要了解下XML配置实现,这样对Java的配置实现理解也会更加深刻。
|
|
@ -1,249 +0,0 @@
|
|||||||
---
|
|
||||||
id="2018-10-22-10-38-05"
|
|
||||||
title="spring之面向切面"
|
|
||||||
headWord="Spring的基础是IOC和AOP,前面两节对IOC和DI做了简单总结,这里再对AOP进行一个学习总结,Spring基础就算有一个初步了解了。"
|
|
||||||
tags=["java", "spring"]
|
|
||||||
category="java"
|
|
||||||
serie="spring学习"
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
  Spring的基础是IOC和AOP,前面两节对IOC和DI做了简单总结,这里再对AOP进行一个学习总结,Spring基础就算有一个初步了解了。
|
|
||||||
|
|
||||||
## 一.面向切面编程
|
|
||||||
|
|
||||||
  在软件开发中,我们可能需要一些跟业务无关但是又必须做的东西,比如日志,事务等,这些分布于应用中多处的功能被称为横切关注点,通常横切关注点从概念上是与应用的业务逻辑相分离的。如何将这些横切关注点与业务逻辑在代码层面进行分离,是面向切面编程(**AOP**)所要解决的。
|
|
||||||
|
|
||||||
横切关注点可以被描述为影响应用多处的功能,切面能够帮助我们模块化横切关注点。下图直观呈现了横切关注点的概念:
|
|
||||||
|
|
||||||
![横切关注点](./picFolder/切面示例.png)
|
|
||||||
|
|
||||||
途中CourseService,StudentService,MiscService都需要类似安全、事务这样的辅助功能,这些辅助功能就被称为横切关注点。
|
|
||||||
|
|
||||||
  **继承**和**委托**是最常见的实现重用通用功能的面向对象技术。但是如果在整个程序中使用相同的基类继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。
|
|
||||||
|
|
||||||
切面提供了取代继承和委托的另一种选择,而且更加清晰简洁。在面向切面编程时,我们任然在一个地方定义通用功能,但是我们可以通过声明的方式定义这个功能以何种方式在何处应用,而无需修改受影响的类,受影响类完全感受不到切面的存在。
|
|
||||||
|
|
||||||
## 二.AOP常用术语
|
|
||||||
|
|
||||||
  下面是AOP中常用的名词。
|
|
||||||
|
|
||||||
### 1. 通知(Advice)
|
|
||||||
|
|
||||||
  通知定义了切面是什么以及何时使用。出了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。Sping切面可以应用以下5种类型的通知。
|
|
||||||
|
|
||||||
- **Before** 在方法被调用之前调用通知
|
|
||||||
- **After** 在方法完成之后调用通知,无论方法执行是否成功
|
|
||||||
- **After-returning** 在方法成功执行后调用通知
|
|
||||||
- **After-throwing** 在方法抛出异常后调用通知
|
|
||||||
- **Around** 通知包裹了被通知的方法,在被通知的方法调用前和调用后执行
|
|
||||||
|
|
||||||
###2.连接点(Joinpoint)
|
|
||||||
|
|
||||||
  应用可能有很多个时机应用通知,这些时机被称为连接点。连接点是应用在执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、甚至是修改字段时。切面代码可以利用这些切入到应用的正常流程中,并添加新的行为。
|
|
||||||
|
|
||||||
### 3.切点(Pointcut)
|
|
||||||
|
|
||||||
  切点定义了通知所要织入的一个或多个连接点。如果说通知定义了切面的“**什么**”和“**何时**”,那么切点就定义了“**何处**”。通常使用明确的类和方法名称来指定切点,或者利用正则表达式定义匹配的类和方法来指定这些切点。有些AOP框架允许我们创建动态的切点,可以更具运行时的策略来决定是否应用通知。
|
|
||||||
|
|
||||||
### 4.切面(Aspect)
|
|
||||||
|
|
||||||
  切面是通知和切点的结合。通知和切点定义了关于切面的全部内容,**是什么**,在**何时**、**何处**完成其功能。
|
|
||||||
|
|
||||||
### 5.引入
|
|
||||||
|
|
||||||
  引入允许我们想现有的类添加新方法或属性。即在无需修改现有类的情况下让它们具有新的行为和状态。
|
|
||||||
|
|
||||||
### 6.织入
|
|
||||||
|
|
||||||
  织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入。
|
|
||||||
|
|
||||||
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译期,比如AspectJ的织入编译期
|
|
||||||
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的加载器,它可以在目标类被引入应用之前增强该目标类的字节码,例如AspectJ5的**LTW**(load-time weaving)
|
|
||||||
- 运行期:切面在应用运行的某个时刻被织入。一般情况下AOP容器会为目标对象动态创建一个代理对象
|
|
||||||
|
|
||||||
##三.Spring AOP
|
|
||||||
|
|
||||||
  Spring在运行期通知对象,通过在代理类中包裹切面,Spring在运行期将切面织入到Spring管理的Bean中。代理类封装了目标类,并拦截被通知的方法的调用,再将调用转发给真正的目标Bean。由于Spring是基于动态代理,所有Spring只支持方法连接点,如果需要方法拦截之外的连接点拦截,我们可以利用Aspect来协助SpringAOP。
|
|
||||||
|
|
||||||
  Spring在运行期通知对象,通过在代理类中包裹切面,Spring在运行期将切面织入到Spring管理的Bean中。代理类封装了目标类,并拦截被通知的方法的调用,再将调用转发给真正的目标Bean。由于Spring是基于动态代理,所有Spring只支持方法连接点,如果需要方法拦截之外的连接点拦截,我们可以利用Aspect来协助SpringAOP。
|
|
||||||
|
|
||||||
### 1、定义切点
|
|
||||||
|
|
||||||
  在SpringAOP中,需要使用AspectJ的切点表达式语言来定义切点。Spring只支持AspectJ的部分切点指示器,如下表所示:
|
|
||||||
|
|
||||||
| AspectJ指示器 | 描述 |
|
|
||||||
| ------------- | ------------------------------------------------------------ |
|
|
||||||
| arg() | 限制连接点匹配参数为指定类型的执行方法 |
|
|
||||||
| @args() | 限制连接点匹配参数由指定注解标注的执行方法 |
|
|
||||||
| execution() | 用于匹配是连接点的执行方法 |
|
|
||||||
| this() | 限制连接点匹配AOP代理的Bean引用为指导类型的类 |
|
|
||||||
| target() | 限制连接点匹配目标对象为指定类型的类 |
|
|
||||||
| @target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解 |
|
|
||||||
| within() | 限制连接点匹配指定的类型 |
|
|
||||||
| @within() | 限制连接点匹配指定注解所标注的类型(当使用SpringAOP时,方法定义在由指定的注解所标注的类里) |
|
|
||||||
| @annotation | 限制匹配带有指定注解连接点 |
|
|
||||||
| bean() | 使用Bean ID或Bean名称作为参数来限制切点只匹配特定的Bean |
|
|
||||||
|
|
||||||
 其中只有execution指示器是唯一的执行匹配,其他都是限制匹配。因此execution指示器是
|
|
||||||
|
|
||||||
其中只有execution指示器是唯一的执行匹配,其他都是限制匹配。因此execution指示器是我们在编写切点定义时最主要使用的指示器。
|
|
||||||
|
|
||||||
### 2、编写切点
|
|
||||||
|
|
||||||
  假设我们要使用execution()指示器选择Hello类的sayHello()方法,表达式如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
execution(* com.test.Hello.sayHello(..))
|
|
||||||
```
|
|
||||||
|
|
||||||
方法表达式以*** **号开始,说明不管方法返回值的类型。然后指定全限定类名和方法名。对于方法参数列表,我们使用(**)标识切点选择任意的sayHello()方法,无论方法入参是什么。
|
|
||||||
|
|
||||||
  同时我们可以使用&&(and),||(or),!(not)来连接指示器,如下所示:
|
|
||||||
|
|
||||||
```java
|
|
||||||
execution(* com.test.Hello.sayHello(..)) and !bean(xiaobu)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3、申明切面
|
|
||||||
|
|
||||||
  在经典Spring AOP中使用ProxyFactoryBean非常复杂,因此提供了申明式切面的选择,在Spring的AOP配置命名空间中有如下配置元素:
|
|
||||||
|
|
||||||
| AOP配置元素 | 描述 |
|
|
||||||
| ------------------------------ | ----------------------------------------------------------- |
|
|
||||||
| <aop:advisor > | 定义AOP通知器 |
|
|
||||||
| <aop:after > | 定义AOP后置通知(无论被通知方法是否执行成功) |
|
|
||||||
| <aop:after-returning > | 定义AOP after-returning通知 |
|
|
||||||
| <aop:after-throwing > | 定义after-throwing |
|
|
||||||
| <aop:around > | 定义AOP环绕通知 |
|
|
||||||
| <aop:aspect > | 定义切面 |
|
|
||||||
| <aop:aspectj-autoproxy > | 启用@AspectJ注解驱动的切面 |
|
|
||||||
| <aop:before > | 定义AOP前置通知 |
|
|
||||||
| <aop:config > | 顶层的AOP配置元素。大多数的<aop:* >元素必须包含在其中 |
|
|
||||||
| <aop:declare-parents > | 为被通知的对象引入额外的接口,并透明的实现 |
|
|
||||||
| <aop:pointcut > | 定义切点 |
|
|
||||||
|
|
||||||
### 4、实现
|
|
||||||
|
|
||||||
假设有一个演员类`Actor`,演员类中有一个表演方法`perform()`,然后还有一个观众类`Audience`,这两个类都在包`com.example.springtest`下,Audience类主要方法如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class Audience{
|
|
||||||
//搬凳子
|
|
||||||
public void takeSeats(){}
|
|
||||||
//欢呼
|
|
||||||
public void applaud(){}
|
|
||||||
//计时,环绕通知需要一个ProceedingJoinPoint参数
|
|
||||||
public void timing(ProceedingJoinPoint joinPoint){
|
|
||||||
joinPoint.proceed();
|
|
||||||
}
|
|
||||||
//演砸了
|
|
||||||
public void demandRefund(){}
|
|
||||||
//测试带参数
|
|
||||||
public void dealString(String word){}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### a、xml配置实现
|
|
||||||
|
|
||||||
  首先将Audience配置到springIOC中:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean id="audience" class="com.example.springtest.Audience"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
然后申明通知:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<aop:config>
|
|
||||||
<aop:aspect ref="audience">
|
|
||||||
<!-- 申明切点 -->
|
|
||||||
<aop:pointcut id="performance" expression="execution(* com.example.springtest.Performer.perform(..))"/>
|
|
||||||
<!-- 声明传递参数切点 -->
|
|
||||||
<aop:pointcut id="performanceStr" expression="execution(* com.example.springtest.Performer.performArg(String) and args(word))"/>
|
|
||||||
<!-- 前置通知 -->
|
|
||||||
<aop:before pointcut-ref="performance" method="takeSeats"/>
|
|
||||||
<!-- 执行成功通知 -->
|
|
||||||
<aop:after-returning pointcout-ref="performance" method="applaud"/>
|
|
||||||
<!-- 执行异常通知 -->
|
|
||||||
<aop:after-throwing pointcut-ref="performance" method="demandRefund"/>
|
|
||||||
<!-- 环绕通知 -->
|
|
||||||
<aop:around pointcut-ref="performance" method="timing"/>
|
|
||||||
<!-- 传递参数 -->
|
|
||||||
<aop:before pointcut-ref="performanceStr" arg-names="word" method="dealString"/>
|
|
||||||
</aop:aspect>
|
|
||||||
</aop:config>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### b、注解实现
|
|
||||||
|
|
||||||
直接在Audience类上加注解(Aspect注解并不能被spring自动发现并注册,要么写到xml中,要么使用@Aspectj注解或者加一个@Component注解),如下所示:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Aspect
|
|
||||||
public class Audience{
|
|
||||||
//定义切点
|
|
||||||
@Pointcut(execution(* com.example.springtest.Performer.perform(..)))
|
|
||||||
public void perform(){}
|
|
||||||
|
|
||||||
//定义带参数切点
|
|
||||||
@Pointcut(execution(* com.example.springtest.Performer.performArg(String) and args(word)))
|
|
||||||
public void performStr(String word){}
|
|
||||||
|
|
||||||
//搬凳子
|
|
||||||
@Before("perform()")
|
|
||||||
public void takeSeats(){}
|
|
||||||
|
|
||||||
//欢呼
|
|
||||||
@AfterReturning("perform()")
|
|
||||||
public void applaud(){}
|
|
||||||
|
|
||||||
//计时,环绕通知需要一个ProceedingJoinPoint参数
|
|
||||||
@Around("perform()")
|
|
||||||
public void timing(ProceedingJoinPoint joinPoint){
|
|
||||||
joinPoint.proceed();
|
|
||||||
}
|
|
||||||
|
|
||||||
//演砸了
|
|
||||||
@AfterThrowing("perform()")
|
|
||||||
public void demandRefund(){}
|
|
||||||
|
|
||||||
//带参数
|
|
||||||
@Before("performStr(word)")
|
|
||||||
public void dealString(String word){}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### c、通过切面引入新功能
|
|
||||||
|
|
||||||
  既然可以用AOP为对象拥有的方法添加新功能,那为什么不能为对象增加新的方法呢?利用被称为**引入**的AOP概念,切面可以为Spring Bean添加新的方法,示例图如下:
|
|
||||||
|
|
||||||
![引入](./picFolder/引入新功能.png)
|
|
||||||
|
|
||||||
当引入接口的方法被调用时,代理将此调用委托给实现了新接口的某个其他对象。实际上,Bean的实现被拆分到了多个类。
|
|
||||||
|
|
||||||
- xml引入需要使用<aop:declare-parents >元素:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<aop:aspect>
|
|
||||||
<aop:declare-parents types-matching="com.fxb.springtest.Performer+"
|
|
||||||
implement-interface="com.fxb.springtest.AddTestInterface"
|
|
||||||
default-impl="com.fxb.springtest.AddTestImpl"/>
|
|
||||||
</aop:aspect>
|
|
||||||
```
|
|
||||||
|
|
||||||
顾名思义\<declare-parents>声明了此切面所通知的Bean在它的对象层次结构中有了新的父类型。其中types-matching指定增强的类;implement-interface指定实现新方法的接口;default-imple指定实现了implement-interface接口的实现类,也可以用delegate-ref来指定一个Bean的引用。
|
|
||||||
|
|
||||||
- 注解引入,通过`@DeclareParents`注解
|
|
||||||
|
|
||||||
```xml
|
|
||||||
@DeclareParents(value="com.fxb.springtest.Performer+",
|
|
||||||
defaultImpl=AddTestImpl.class)
|
|
||||||
public static AddTestInterface addTestInterface;
|
|
||||||
```
|
|
||||||
|
|
||||||
同xml实现一样,注解也由三部分组成:1、value属性相当于tpes-matching属性,标识被增强的类;2、defaultImpl等同于default-imple,指定接口的实现类;3、有@DeclareParents注解所标注的static属性指定了将被引入的接口。
|
|
||||||
|
|
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB |
@ -1,51 +0,0 @@
|
|||||||
[id]:2018-08-13
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,springboot
|
|
||||||
  前面的博客有说到spring boot搭建见另一篇博文,其实那篇博文还没写,现在来填个坑。我们使用spring initializr来构建,idea和eclipse都支持这种方式,构建过程类似,这里以idea为例,详细记录构建过程。
|
|
||||||
|
|
||||||
###1.选择spring initializr
|
|
||||||
|
|
||||||
![1532967570728](./picFolder/1532967570728.png)
|
|
||||||
|
|
||||||
next
|
|
||||||
|
|
||||||
#### 2.设置参数
|
|
||||||
|
|
||||||
![1532967772110](./picFolder/1532967772110.png)
|
|
||||||
|
|
||||||
next
|
|
||||||
|
|
||||||
#### 3.选择依赖
|
|
||||||
|
|
||||||
  在这里选择spring boot版本和web依赖(忽略sql的依赖,如有需要[点击这里](f),单独将mybatis的整合),后面也可手动编辑pom文件修改增加删除依赖
|
|
||||||
|
|
||||||
![1532967938985](./picFolder/1532967938985.png)
|
|
||||||
|
|
||||||
这里我们选择web搭建一个简单的REST风格demo。然后next。
|
|
||||||
|
|
||||||
####4.设置项目存放地址
|
|
||||||
|
|
||||||
![1532968024509](./picFolder/1532968024509.png)
|
|
||||||
|
|
||||||
这样就成功构建了一个springboot项目。
|
|
||||||
|
|
||||||
#### 5.测试
|
|
||||||
|
|
||||||
  现在新建一个controller包,包下新建一个HelloController,创建之后项目目录结构如下:
|
|
||||||
|
|
||||||
![1532969025023](./picFolder/1532969025023.png)
|
|
||||||
|
|
||||||
HelloController代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/home")
|
|
||||||
public class HelloController{
|
|
||||||
@GetMapping("/hello")
|
|
||||||
public String sayHello(){
|
|
||||||
return "hello";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后运行项目,访问localhost:8080/home/hello即可看到hello字符串。
|
|
Before Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 54 KiB |
@ -1,257 +0,0 @@
|
|||||||
[id]:2018-08-20
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,springsecurity,scurity
|
|
||||||
|
|
||||||
**说明springboot版本2.0.3**
|
|
||||||
|
|
||||||
##一、 介绍
|
|
||||||
|
|
||||||
  Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
|
|
||||||
|
|
||||||
##二、 环境搭建
|
|
||||||
|
|
||||||
  建立springboot2项目,加入security依赖,mybatis依赖
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mybatis.spring.boot</groupId>
|
|
||||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
|
||||||
<version>1.3.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>mysql</groupId>
|
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
数据库为传统的用户--角色--权限,权限表记录了url和method,springboot配置文件如下:
|
|
||||||
```yml
|
|
||||||
mybatis:
|
|
||||||
type-aliases-package: com.example.demo.entity
|
|
||||||
server:
|
|
||||||
port: 8081
|
|
||||||
spring:
|
|
||||||
datasource:
|
|
||||||
driver-class-name: com.mysql.jdbc.Driver
|
|
||||||
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
http:
|
|
||||||
encoding:
|
|
||||||
charset: utf-8
|
|
||||||
enabled: true
|
|
||||||
```
|
|
||||||
springboot启动类中加入如下代码,设置路由匹配规则。
|
|
||||||
```java
|
|
||||||
@Override
|
|
||||||
protected void configurePathMatch(PathMatchConfigurer configurer) {
|
|
||||||
configurer.setUseSuffixPatternMatch(false) //设置路由是否后缀匹配,譬如/user能够匹配/user.,/user.aa
|
|
||||||
.setUseTrailingSlashMatch(false); //设置是否后缀路径匹配,比如/user能够匹配/user,/user/
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 三、 security配置
|
|
||||||
|
|
||||||
  默认情况下security是无需任何自定义配置就可使用的,我们不考虑这种方式,直接讲如何个性化登录过程。
|
|
||||||
|
|
||||||
#### 1、 建立security配置文件,目前配置文件中还没有任何配置。
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2、 个性化登录,security中的登录如下:
|
|
||||||
![登录过程](./picFolder/pic1.png)
|
|
||||||
- security需要一个user的实体类实现`UserDetails`接口,该实体类最后与系统中用户的实体类分开,代码如下:
|
|
||||||
```java
|
|
||||||
public class SecurityUser implements UserDetails{
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private String password;
|
|
||||||
private String name;
|
|
||||||
List<GrantedAuthority> authorities;
|
|
||||||
|
|
||||||
public User(string name,string password) {
|
|
||||||
this.id = id;
|
|
||||||
this.password = password;
|
|
||||||
this.name = name;
|
|
||||||
this.age = age;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAuthorities(List<GrantedAuthority> authorities) {
|
|
||||||
this.authorities = authorities;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<GrantedAuthority> getAuthorities() {
|
|
||||||
return this.authorities;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override //获取校验用户名
|
|
||||||
public String getUsername() {
|
|
||||||
return String.valueOf(this.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override //获取校验用密码
|
|
||||||
public String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override //账户是否未过期
|
|
||||||
public boolean isAccountNonExpired() {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override //账户是否未锁定
|
|
||||||
public boolean isAccountNonLocked() {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override //帐户密码是否未过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
|
|
||||||
public boolean isCredentialsNonExpired() {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override //账户是否可用
|
|
||||||
public boolean isEnabled() {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 编写了实体类还需要编写一个服务类SecurityService实现`UserDetailsService`接口,重写loadByUsername方法,通过这个方法根据用户名获取用户信息,代码如下:
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class SecurityUserService implements UserDetailsService {
|
|
||||||
@Autowired
|
|
||||||
private JurisdictionMapper jurisdictionMapper;
|
|
||||||
@Autowired
|
|
||||||
private UserMapper userMapper;
|
|
||||||
private Logger log = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
|
||||||
log.info("登录用户id为:{}",username);
|
|
||||||
int id = Integer.valueOf(username);
|
|
||||||
User user = userMapper.getById(id);
|
|
||||||
if(user==null) {
|
|
||||||
//抛出错误,用户不存在
|
|
||||||
throw new UsernameNotFoundException("用户名 "+username+"不存在");
|
|
||||||
}
|
|
||||||
//获取用户权限
|
|
||||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
|
||||||
List<Jurisdiction> jurisdictions = jurisdictionMapper.selectByUserId(id);
|
|
||||||
for(Jurisdiction item : jurisdictions) {
|
|
||||||
GrantedAuthority authority = new MyGrantedAuthority(item.getMethod(),item.getUrl());
|
|
||||||
authorities.add(authority);
|
|
||||||
}
|
|
||||||
SecurityUser securityUser = new SecurityUser(user.getName(),user.getPassword(),authority):
|
|
||||||
user.setAuthorities(authorities);
|
|
||||||
return securityUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 通常我们会对密码进行加密,所有还要编写一个passwordencode类,实现PasswordEncoder接口,代码如下:
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class MyPasswordEncoder implements PasswordEncoder {
|
|
||||||
private Logger log = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
@Override //不清楚除了在下面方法用到还有什么用处
|
|
||||||
public String encode(CharSequence rawPassword) {
|
|
||||||
return StringUtil.StringToMD5(rawPassword.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
//判断密码是否匹配
|
|
||||||
@Override
|
|
||||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
|
||||||
return encodedPassword.equals(this.encode(rawPassword));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3、 编辑配置文件
|
|
||||||
- 编写config Bean以使用上面定义的验证逻辑,securityUserService、myPasswordEncoder通过@Autowired引入。
|
|
||||||
```java
|
|
||||||
@Override
|
|
||||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
|
||||||
auth.userDetailsService(securityUserService)
|
|
||||||
.passwordEncoder(myPasswordEncoder);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 然后编写configure Bean(和上一个不一样,参数不同),实现security验证逻辑,代码如下:
|
|
||||||
```java
|
|
||||||
@Override
|
|
||||||
protected void configure(HttpSecurity http) throws Exception {
|
|
||||||
http
|
|
||||||
.csrf() //跨站
|
|
||||||
.disable() //关闭跨站检测
|
|
||||||
.authorizeRequests()//验证策略策略链
|
|
||||||
.antMatchers("/public/**").permitAll()//无需验证路径
|
|
||||||
.antMatchers("/login").permitAll()//放行登录
|
|
||||||
.antMatchers(HttpMethod.GET, "/user").hasAuthority("getAllUser")//拥有权限才可访问
|
|
||||||
.antMatchers(HttpMethod.GET, "/user").hasAnyAuthority("1","2")//拥有任一权限即可访问
|
|
||||||
//角色类似,hasRole(),hasAnyRole()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
.and()
|
|
||||||
.formLogin()
|
|
||||||
.loginPage("/public/unlogin") //未登录跳转页面,设置了authenticationentrypoint后无需设置未登录跳转页面
|
|
||||||
.loginProcessingUrl("/public/login")//处理登录post请求接口,无需自己实现
|
|
||||||
.successForwardUrl("/success")//登录成功转发接口
|
|
||||||
.failureForwardUrl("/failed")//登录失败转发接口
|
|
||||||
.usernameParameter("id") //修改用户名的表单name,默认为username
|
|
||||||
.passwordParameter("password")//修改密码的表单name,默认为password
|
|
||||||
.and()
|
|
||||||
.logout()//自定义登出
|
|
||||||
.logoutUrl("/public/logout") //自定义登出api,无需自己实现
|
|
||||||
.logoutSuccessUrl("public/logoutSuccess")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
到这里便可实现security与springboot的基本整合。
|
|
||||||
|
|
||||||
## 四、实现记住我功能
|
|
||||||
|
|
||||||
#### 1、 建表
|
|
||||||
|
|
||||||
  记住我功能需要数据库配合实现,首先要在数据库建一张表用户保存cookie和用户名,数据库建表语句如下:不能做修改
|
|
||||||
```sql
|
|
||||||
CREATE TABLE `persistent_logins` (
|
|
||||||
`username` varchar(64) NOT NULL,
|
|
||||||
`series` varchar(64) NOT NULL,
|
|
||||||
`token` varchar(64) NOT NULL,
|
|
||||||
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`series`)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2、 编写rememberMeservice Bean
|
|
||||||
  代码如下:
|
|
||||||
```java
|
|
||||||
@Bean
|
|
||||||
public RememberMeServices rememberMeServices(){
|
|
||||||
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
|
|
||||||
jdbcTokenRepository.setDataSource(dataSource);
|
|
||||||
PersistentTokenBasedRememberMeServices rememberMeServices =
|
|
||||||
new PersistentTokenBasedRememberMeServices("INTERNAL_SECRET_KEY",securityUserService,jdbcTokenRepository);
|
|
||||||
//还可设置许多其他属性
|
|
||||||
rememberMeServices.setCookieName("kkkkk"); //客户端cookie名
|
|
||||||
return rememberMeServices;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
dataSource为@Autowired引入
|
|
||||||
|
|
||||||
#### 3、 配置文件设置remember
|
|
||||||
  在config(HttpSecurity http)中加入记住我功能
|
|
||||||
```java
|
|
||||||
.rememberMe()
|
|
||||||
.rememberMeServices(rememberMeServices())
|
|
||||||
.key("INTERNAL_SECRET_KEY")
|
|
||||||
```
|
|
||||||
在登录表单中设置remember-me即可实现记住我功能。
|
|
@ -1,93 +0,0 @@
|
|||||||
[id]:2018-08-21
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,springsecurity,scurity
|
|
||||||
|
|
||||||
  紧接着上一篇,上一篇中登录验证都由security帮助我们完成了,如果我们想要增加一个验证码登录或者其它的自定义校验就没办法了,因此这一篇讲解如何实现这个功能。
|
|
||||||
|
|
||||||
##一、 实现自定义登录校验类
|
|
||||||
|
|
||||||
  继承UsernamePasswordAuthenticationFilter类来拓展登录校验,代码如下:
|
|
||||||
```java
|
|
||||||
public class MyUsernamePasswordAuthentication extends UsernamePasswordAuthenticationFilter{
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
|
|
||||||
throws AuthenticationException {
|
|
||||||
//我们可以在这里进行额外的验证,如果验证失败抛出继承AuthenticationException的自定义错误。
|
|
||||||
log.info("在这里进行验证码判断");
|
|
||||||
//只要最终的验证是账号密码形式就无需修改后续过程
|
|
||||||
return super.attemptAuthentication(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
super.setAuthenticationManager(authenticationManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##二、 将自定义登录配置到security中
|
|
||||||
  编写自定义登录过滤器后,configure Bean修改为如下:
|
|
||||||
```java
|
|
||||||
@Override
|
|
||||||
protected void configure(HttpSecurity http) throws Exception {
|
|
||||||
http
|
|
||||||
.csrf() //跨站
|
|
||||||
.disable() //关闭跨站检测
|
|
||||||
//自定义鉴权过程,无需下面设置
|
|
||||||
.authorizeRequests()//验证策略
|
|
||||||
.antMatchers("/public/**").permitAll()//无需验证路径
|
|
||||||
.antMatchers("/user/**").permitAll()
|
|
||||||
.antMatchers("/login").permitAll()//放行登录
|
|
||||||
.antMatchers(HttpMethod.GET, "/user").hasAuthority("getAllUser")//拥有权限才可访问
|
|
||||||
.antMatchers(HttpMethod.GET, "/user").hasAnyAuthority("1","2")//拥有任一权限即可访问
|
|
||||||
//角色类似,hasRole(),hasAnyRole()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
.and()
|
|
||||||
//自定义异常处理
|
|
||||||
.exceptionHandling()
|
|
||||||
.authenticationEntryPoint(myAuthenticationEntryPoint)//未登录处理
|
|
||||||
.accessDeniedHandler(myAccessDeniedHandler)//权限不足处理
|
|
||||||
.and()
|
|
||||||
//加入自定义登录校验
|
|
||||||
.addFilterBefore(myUsernamePasswordAuthentication(),UsernamePasswordAuthenticationFilter.class)
|
|
||||||
.rememberMe()//默认放在内存中
|
|
||||||
.rememberMeServices(rememberMeServices())
|
|
||||||
.key("INTERNAL_SECRET_KEY")
|
|
||||||
// 重写usernamepasswordauthenticationFilter后,下面的formLogin()设置将失效,需要手动设置到个性化过滤器中
|
|
||||||
// .and()
|
|
||||||
// .formLogin()
|
|
||||||
// .loginPage("/public/unlogin") //未登录跳转页面,设置了authenticationentrypoint后无需设置未登录跳转页面
|
|
||||||
// .loginProcessingUrl("/public/login")//登录api
|
|
||||||
// .successForwardUrl("/success")
|
|
||||||
// .failureForwardUrl("/failed")
|
|
||||||
// .usernameParameter("id")
|
|
||||||
// .passwordParameter("password")
|
|
||||||
// .failureHandler(myAuthFailedHandle) //登录失败处理
|
|
||||||
// .successHandler(myAuthSuccessHandle)//登录成功处理
|
|
||||||
// .usernameParameter("id")
|
|
||||||
.and()
|
|
||||||
.logout()//自定义登出
|
|
||||||
.logoutUrl("/public/logout")
|
|
||||||
.logoutSuccessUrl("public/logoutSuccess")
|
|
||||||
.logoutSuccessHandler(myLogoutSuccessHandle);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
然后再编写Bean,代码如下:
|
|
||||||
```java
|
|
||||||
@Bean
|
|
||||||
public MyUsernamePasswordAuthentication myUsernamePasswordAuthentication(){
|
|
||||||
MyUsernamePasswordAuthentication myUsernamePasswordAuthentication = new MyUsernamePasswordAuthentication();
|
|
||||||
myUsernamePasswordAuthentication.setAuthenticationFailureHandler(myAuthFailedHandle); //设置登录失败处理类
|
|
||||||
myUsernamePasswordAuthentication.setAuthenticationSuccessHandler(myAuthSuccessHandle);//设置登录成功处理类
|
|
||||||
myUsernamePasswordAuthentication.setFilterProcessesUrl("/public/login");
|
|
||||||
myUsernamePasswordAuthentication.setRememberMeServices(rememberMeServices()); //设置记住我
|
|
||||||
myUsernamePasswordAuthentication.setUsernameParameter("id");
|
|
||||||
myUsernamePasswordAuthentication.setPasswordParameter("password");
|
|
||||||
return myUsernamePasswordAuthentication;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
完成。
|
|
@ -1,220 +0,0 @@
|
|||||||
[id]:2018-08-22
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,springsecurity,scurity
|
|
||||||
|
|
||||||
  这篇讲解如何自定义鉴权过程,实现根据数据库查询出的url和method是否匹配当前请求的url和method来决定有没有权限。security鉴权过程如下:
|
|
||||||
![鉴权流程](./picFolder/pic2.png)
|
|
||||||
|
|
||||||
##一、 重写metadataSource类
|
|
||||||
|
|
||||||
1. 编写MyGranteAuthority类,让权限包含url和method两个部分。
|
|
||||||
```java
|
|
||||||
public class MyGrantedAuthority implements GrantedAuthority {
|
|
||||||
private String method;
|
|
||||||
private String url;
|
|
||||||
|
|
||||||
public MyGrantedAuthority(String method, String url) {
|
|
||||||
this.method = method;
|
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAuthority() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMethod() {
|
|
||||||
return method;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if(this==obj) return true;
|
|
||||||
if(obj==null||getClass()!= obj.getClass()) return false;
|
|
||||||
MyGrantedAuthority grantedAuthority = (MyGrantedAuthority)obj;
|
|
||||||
if(this.method.equals(grantedAuthority.getMethod())&&this.url.equals(grantedAuthority.getUrl()))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
2. 编写MyConfigAttribute类,实现ConfigAttribute接口,代码如下:
|
|
||||||
```java
|
|
||||||
public class MyConfigAttribute implements ConfigAttribute {
|
|
||||||
private HttpServletRequest httpServletRequest;
|
|
||||||
private MyGrantedAuthority myGrantedAuthority;
|
|
||||||
|
|
||||||
public MyConfigAttribute(HttpServletRequest httpServletRequest) {
|
|
||||||
this.httpServletRequest = httpServletRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MyConfigAttribute(HttpServletRequest httpServletRequest, MyGrantedAuthority myGrantedAuthority) {
|
|
||||||
this.httpServletRequest = httpServletRequest;
|
|
||||||
this.myGrantedAuthority = myGrantedAuthority;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpServletRequest getHttpServletRequest() {
|
|
||||||
return httpServletRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAttribute() {
|
|
||||||
return myGrantedAuthority.getUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MyGrantedAuthority getMyGrantedAuthority() {
|
|
||||||
return myGrantedAuthority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. 编写MySecurityMetadataSource类,获取当前url所需要的权限
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private JurisdictionMapper jurisdictionMapper;
|
|
||||||
private List<Jurisdiction> jurisdictions;
|
|
||||||
|
|
||||||
private void loadResource() {
|
|
||||||
this.jurisdictions = jurisdictionMapper.selectAllPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
|
|
||||||
if (jurisdictions == null) this.loadResource();
|
|
||||||
HttpServletRequest request = ((FilterInvocation) object).getRequest();
|
|
||||||
Set<ConfigAttribute> allConfigAttribute = new HashSet<>();
|
|
||||||
AntPathRequestMatcher matcher;
|
|
||||||
for (Jurisdiction jurisdiction : jurisdictions) {
|
|
||||||
//使用AntPathRequestMatcher比较可让url支持ant风格,例如/user/*/a
|
|
||||||
//*匹配一个或多个字符,**匹配任意字符或目录
|
|
||||||
matcher = new AntPathRequestMatcher(jurisdiction.getUrl(), jurisdiction.getMethod());
|
|
||||||
if (matcher.matches(request)) {
|
|
||||||
ConfigAttribute configAttribute = new MyConfigAttribute(request,new MyGrantedAuthority(jurisdiction.getMethod(),jurisdiction.getUrl()));
|
|
||||||
allConfigAttribute.add(configAttribute);
|
|
||||||
//这里是获取到一个权限就返回,根据校验规则也可获取多个然后返回
|
|
||||||
return allConfigAttribute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//未匹配到,说明无需权限验证
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<ConfigAttribute> getAllConfigAttributes() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supports(Class<?> clazz) {
|
|
||||||
return FilterInvocation.class.isAssignableFrom(clazz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##二、 编写MyAccessDecisionManager类
|
|
||||||
|
|
||||||
  实现AccessDecisionManager接口以实现权限判断,直接return说明验证通过,如不通过需要抛出对应错误,代码如下:
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class MyAccessDecisionManager implements AccessDecisionManager{
|
|
||||||
private Logger log = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
|
|
||||||
throws AccessDeniedException, InsufficientAuthenticationException {
|
|
||||||
//无需验证放行
|
|
||||||
if(configAttributes==null || configAttributes.size()==0)
|
|
||||||
return;
|
|
||||||
if(!authentication.isAuthenticated()){
|
|
||||||
throw new InsufficientAuthenticationException("未登录");
|
|
||||||
}
|
|
||||||
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
|
||||||
for(ConfigAttribute attribute : configAttributes){
|
|
||||||
MyConfigAttribute urlConfigAttribute = (MyConfigAttribute)attribute;
|
|
||||||
for(GrantedAuthority authority: authorities){
|
|
||||||
MyGrantedAuthority myGrantedAuthority = (MyGrantedAuthority)authority;
|
|
||||||
if(urlConfigAttribute.getMyGrantedAuthority().equals(myGrantedAuthority))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new AccessDeniedException("无权限");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supports(ConfigAttribute attribute) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supports(Class<?> clazz) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##三、 编写MyFilterSecurityInterceptor类
|
|
||||||
  该类继承AbstractSecurityInterceptor类,实现Filter接口,代码如下:
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
|
|
||||||
|
|
||||||
//注入上面编写的两个类
|
|
||||||
@Autowired
|
|
||||||
private MySecurityMetadataSource mySecurityMetadataSource;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
|
|
||||||
super.setAccessDecisionManager(myAccessDecisionManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig arg0) throws ServletException {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
|
||||||
FilterInvocation fi = new FilterInvocation(request, response, chain);
|
|
||||||
invoke(fi);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void invoke(FilterInvocation fi) throws IOException, ServletException {
|
|
||||||
//这里进行权限验证
|
|
||||||
InterceptorStatusToken token = super.beforeInvocation(fi);
|
|
||||||
try {
|
|
||||||
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
|
|
||||||
} finally {
|
|
||||||
super.afterInvocation(token, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Class<?> getSecureObjectClass() {
|
|
||||||
return FilterInvocation.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SecurityMetadataSource obtainSecurityMetadataSource() {
|
|
||||||
return this.mySecurityMetadataSource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 四、 加入到security的过滤器链中
|
|
||||||
```java
|
|
||||||
.addFilterBefore(urlFilterSecurityInterceptor,FilterSecurityInterceptor.class)
|
|
||||||
```
|
|
||||||
完成
|
|
@ -1,209 +0,0 @@
|
|||||||
[id]:2018-08-25
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,websocket
|
|
||||||
|
|
||||||
<h3 id="#一、背景">一、背景</h3>
|
|
||||||
|
|
||||||
  我们都知道http协议只能浏览器单方面向服务器发起请求获得响应,服务器不能主动向浏览器推送消息。想要实现浏览器的主动推送有两种主流实现方式:
|
|
||||||
|
|
||||||
- 轮询:缺点很多,但是实现简单
|
|
||||||
- websocket:在浏览器和服务器之间建立tcp连接,实现全双工通信
|
|
||||||
|
|
||||||
  springboot使用websocket有两种方式,一种是实现简单的websocket,另外一种是实现**STOMP**协议。这一篇实现简单的websocket,STOMP下一篇在讲。
|
|
||||||
|
|
||||||
**注意:如下都是针对使用springboot内置容器**
|
|
||||||
|
|
||||||
<h3 id="二、实现">二、实现</h3>
|
|
||||||
|
|
||||||
<h4 id="1、依赖引入">1、依赖引入</h4>
|
|
||||||
|
|
||||||
  要使用websocket关键是`@ServerEndpoint`这个注解,该注解是javaee标准中的注解,tomcat7及以上已经实现了,如果使用传统方法将war包部署到tomcat中,只需要引入如下javaee标准依赖即可:
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>javax</groupId>
|
|
||||||
<artifactId>javaee-api</artifactId>
|
|
||||||
<version>7.0</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
如使用springboot内置容器,无需引入,springboot已经做了包含。我们只需引入如下依赖即可:
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
|
||||||
<version>1.5.3.RELEASE</version>
|
|
||||||
<type>pom</type>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
<h4 id="2、注入Bean">2、注入Bean</h4>
|
|
||||||
|
|
||||||
  首先注入一个**ServerEndpointExporter**Bean,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint。代码如下:
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class WebSocketConfig {
|
|
||||||
@Bean
|
|
||||||
public ServerEndpointExporter serverEndpointExporter(){
|
|
||||||
return new ServerEndpointExporter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<h4 id="3、申明endpoint">3、申明endpoint</h4>
|
|
||||||
|
|
||||||
  建立**MyWebSocket.java**类,在该类中处理websocket逻辑
|
|
||||||
```java
|
|
||||||
@ServerEndpoint(value = "/websocket") //接受websocket请求路径
|
|
||||||
@Component //注册到spring容器中
|
|
||||||
public class MyWebSocket {
|
|
||||||
|
|
||||||
|
|
||||||
//保存所有在线socket连接
|
|
||||||
private static Map<String,MyWebSocket> webSocketMap = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
//记录当前在线数目
|
|
||||||
private static int count=0;
|
|
||||||
|
|
||||||
//当前连接(每个websocket连入都会创建一个MyWebSocket实例
|
|
||||||
private Session session;
|
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(this.getClass());
|
|
||||||
//处理连接建立
|
|
||||||
@OnOpen
|
|
||||||
public void onOpen(Session session){
|
|
||||||
this.session=session;
|
|
||||||
webSocketMap.put(session.getId(),this);
|
|
||||||
addCount();
|
|
||||||
log.info("新的连接加入:{}",session.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
//接受消息
|
|
||||||
@OnMessage
|
|
||||||
public void onMessage(String message,Session session){
|
|
||||||
log.info("收到客户端{}消息:{}",session.getId(),message);
|
|
||||||
try{
|
|
||||||
this.sendMessage("收到消息:"+message);
|
|
||||||
}catch (Exception e){
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//处理错误
|
|
||||||
@OnError
|
|
||||||
public void onError(Throwable error,Session session){
|
|
||||||
log.info("发生错误{},{}",session.getId(),error.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
//处理连接关闭
|
|
||||||
@OnClose
|
|
||||||
public void onClose(){
|
|
||||||
webSocketMap.remove(this.session.getId());
|
|
||||||
reduceCount();
|
|
||||||
log.info("连接关闭:{}",this.session.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
//群发消息
|
|
||||||
|
|
||||||
//发送消息
|
|
||||||
public void sendMessage(String message) throws IOException {
|
|
||||||
this.session.getBasicRemote().sendText(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
//广播消息
|
|
||||||
public static void broadcast(){
|
|
||||||
MyWebSocket.webSocketMap.forEach((k,v)->{
|
|
||||||
try{
|
|
||||||
v.sendMessage("这是一条测试广播");
|
|
||||||
}catch (Exception e){
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取在线连接数目
|
|
||||||
public static int getCount(){
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
//操作count,使用synchronized确保线程安全
|
|
||||||
public static synchronized void addCount(){
|
|
||||||
MyWebSocket.count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static synchronized void reduceCount(){
|
|
||||||
MyWebSocket.count--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<h4 id="4、客户的实现">4、客户的实现</h4>
|
|
||||||
|
|
||||||
  客户端使用h5原生websocket,部分浏览器可能不支持。代码如下:
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>websocket测试</title>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<button onclick="sendMessage()">测试</button>
|
|
||||||
<script>
|
|
||||||
let socket = new WebSocket("ws://localhost:8080/websocket");
|
|
||||||
socket.onerror = err => {
|
|
||||||
console.log(err);
|
|
||||||
};
|
|
||||||
socket.onopen = event => {
|
|
||||||
console.log(event);
|
|
||||||
};
|
|
||||||
socket.onmessage = mess => {
|
|
||||||
console.log(mess);
|
|
||||||
};
|
|
||||||
socket.onclose = () => {
|
|
||||||
console.log("连接关闭");
|
|
||||||
};
|
|
||||||
|
|
||||||
function sendMessage() {
|
|
||||||
if (socket.readyState === 1)
|
|
||||||
socket.send("这是一个测试数据");
|
|
||||||
else
|
|
||||||
alert("尚未建立websocket连接");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
<h3 id="三、测试">三、测试</h3>
|
|
||||||
|
|
||||||
  建立一个controller测试群发,代码如下:
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
public class HomeController {
|
|
||||||
|
|
||||||
@GetMapping("/broadcast")
|
|
||||||
public void broadcast(){
|
|
||||||
MyWebSocket.broadcast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
然后打开上面的html,可以看到浏览器和服务器都输出连接成功的信息:
|
|
||||||
```
|
|
||||||
浏览器:
|
|
||||||
Event {isTrusted: true, type: "open", target: WebSocket, currentTarget: WebSocket, eventPhase: 2, …}
|
|
||||||
|
|
||||||
服务端:
|
|
||||||
2018-08-01 14:05:34.727 INFO 12708 --- [nio-8080-exec-1] com.fxb.h5websocket.MyWebSocket : 新的连接加入:0
|
|
||||||
```
|
|
||||||
点击测试按钮,可在服务端看到如下输出:
|
|
||||||
```
|
|
||||||
2018-08-01 15:00:34.644 INFO 12708 --- [nio-8080-exec-6] com.fxb.h5websocket.MyWebSocket : 收到客户端2消息:这是一个测试数据
|
|
||||||
```
|
|
||||||
再次打开html页面,这样就有两个websocket客户端,然后在浏览器访问[localhost:8080/broadcast](localhost:8080/broadcast)测试群发功能,每个客户端都会输出如下信息:
|
|
||||||
```
|
|
||||||
MessageEvent {isTrusted: true, data: "这是一条测试广播", origin: "ws://localhost:8080", lastEventId: "", source: null, …}
|
|
||||||
```
|
|
||||||
<br/>
|
|
||||||
  源码可在[github]()上下载,记得点赞,star哦
|
|
||||||
|
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 117 KiB |
@ -1,366 +0,0 @@
|
|||||||
[id]:2018-09-01
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,mysql,mybatis,xml
|
|
||||||
|
|
||||||
## 写在前面
|
|
||||||
|
|
||||||
刚毕业的第一份工作是java开发,项目中需要用到mybatis,特此记录学习过程,这只是一个简单demo,mybatis用法很多不可能全部写出来,有更复杂的需求建议查看mybatis的官方中文文档,[点击跳转](http://www.mybatis.org/mybatis-3/zh/index.html)。下面时项目环境/版本。
|
|
||||||
|
|
||||||
- 开发工具:IDEA
|
|
||||||
- jdk版本:1.8
|
|
||||||
- springboot版本:2.03
|
|
||||||
|
|
||||||
其他依赖版本见下面pom.xml:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<groupId>com.example</groupId>
|
|
||||||
<artifactId>mybatis-test</artifactId>
|
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<name>mybatis-test</name>
|
|
||||||
<description>Demo project for Spring Boot</description>
|
|
||||||
|
|
||||||
<parent>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
|
||||||
<version>2.0.3.RELEASE</version>
|
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
|
||||||
<java.version>1.8</java.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>mysql</groupId>
|
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<!--mybatis依赖 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mybatis.spring.boot</groupId>
|
|
||||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
|
||||||
<version>1.3.2</version>
|
|
||||||
</dependency>
|
|
||||||
<!--alibaba连接池依赖-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.alibaba</groupId>
|
|
||||||
<artifactId>druid-spring-boot-starter</artifactId>
|
|
||||||
<version>1.1.9</version>
|
|
||||||
</dependency>
|
|
||||||
<!--分页依赖-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.pagehelper</groupId>
|
|
||||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
|
||||||
<version>1.2.5</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 1.创建项目
|
|
||||||
|
|
||||||
使用idea中的spring initializr生成maven项目,项目命令为mybatis-test,选择web,mysql,mybatis依赖,即可成功。(详细过程不赘述,如有需要学习springboot创建过程,可参考[这篇文章]()。
|
|
||||||
|
|
||||||
然后依照上面的pom文件,补齐缺少的依赖。接着创建包entity,service和mybatis映射文件夹mapper,创建。为了方便配置将application.properties改成application.yml。由于我们时REST接口,故不需要static和templates目录。修改完毕后的项目结构如下:
|
|
||||||
|
|
||||||
![项目结构](./picFolder/pic1.png)
|
|
||||||
|
|
||||||
修改启动类,增加`@MapperScan("com.example.mybatistest.dao") `,以自动扫描dao目录,避免每个dao都手动加`@Mapper`注解。代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@SpringBootApplication
|
|
||||||
@MapperScan("com.example.mybatistest.dao")
|
|
||||||
public class MybatisTestApplication {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(MybatisTestApplication.class, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
修改application.yml,配置项目,代码如下:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
mybatis:
|
|
||||||
#对应实体类路径
|
|
||||||
type-aliases-package: com.example.mybatistest.entity
|
|
||||||
#对应mapper映射文件路径
|
|
||||||
mapper-locations: classpath:mapper/*.xml
|
|
||||||
|
|
||||||
#pagehelper物理分页配置
|
|
||||||
pagehelper:
|
|
||||||
helper-dialect: mysql
|
|
||||||
reasonable: true
|
|
||||||
support-methods-arguments: true
|
|
||||||
params: count=countSql
|
|
||||||
returnPageInfo: check
|
|
||||||
|
|
||||||
server:
|
|
||||||
port: 8081
|
|
||||||
|
|
||||||
spring:
|
|
||||||
datasource:
|
|
||||||
name: mysqlTest
|
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
|
||||||
#druid连接池相关配置
|
|
||||||
druid:
|
|
||||||
#监控拦截统计的filters
|
|
||||||
filters: stat
|
|
||||||
driver-class-name: com.mysql.jdbc.Driver
|
|
||||||
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
#配置初始化大小,最小,最大
|
|
||||||
initial-size: 1
|
|
||||||
min-idle: 1
|
|
||||||
max-active: 20
|
|
||||||
#获取连接等待超时时间
|
|
||||||
max-wait: 6000
|
|
||||||
#间隔多久检测一次需要关闭的空闲连接
|
|
||||||
time-between-eviction-runs-millis: 60000
|
|
||||||
#一个连接在池中的最小生存时间
|
|
||||||
min-evictable-idle-time-millis: 300000
|
|
||||||
#打开PSCache,并指定每个连接上PSCache的大小。oracle设置为true,mysql设置为false。分库分表设置较多推荐设置
|
|
||||||
pool-prepared-statements: false
|
|
||||||
max-pool-prepared-statement-per-connection-size: 20
|
|
||||||
http:
|
|
||||||
encoding:
|
|
||||||
charset: utf-8
|
|
||||||
enabled: true
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2.编写代码
|
|
||||||
|
|
||||||
首先创建数据表,sql语句如下:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE `user` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`name` varchar(255) NOT NULL,
|
|
||||||
`age` tinyint(4) NOT NULL DEFAULT '0',
|
|
||||||
`password` varchar(255) NOT NULL DEFAULT '123456',
|
|
||||||
PRIMARY KEY (`id`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
|
|
||||||
```
|
|
||||||
|
|
||||||
然后在entity包中创建实体类User.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class User {
|
|
||||||
private int id;
|
|
||||||
private String name;
|
|
||||||
private int age;
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
public User(int id, String name, int age, String password) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.age = age;
|
|
||||||
this.password = password;
|
|
||||||
}
|
|
||||||
public User(){}
|
|
||||||
//getter setter自行添加
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
在dao包下创建UserDao.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface UserDao {
|
|
||||||
//插入用户
|
|
||||||
int insert(User user);
|
|
||||||
//根据id查询
|
|
||||||
User selectById(String id);
|
|
||||||
//查询所有
|
|
||||||
List<User> selectAll();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
在mapper文件夹下创建UserMapper.xml,具体的xml编写方法查看文首的官方文档。
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
|
||||||
<mapper namespace="com.example.mybatistest.dao.UserDao">
|
|
||||||
<sql id="BASE_TABLE">
|
|
||||||
user
|
|
||||||
</sql>
|
|
||||||
<sql id="BASE_COLUMN">
|
|
||||||
id,name,age,password
|
|
||||||
</sql>
|
|
||||||
|
|
||||||
<insert id="insert" parameterType="com.example.mybatistest.entity.User" useGeneratedKeys="true" keyProperty="id">
|
|
||||||
INSERT INTO <include refid="BASE_TABLE"/>
|
|
||||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
|
||||||
name,password,
|
|
||||||
<if test="age!=null">
|
|
||||||
age
|
|
||||||
</if>
|
|
||||||
</trim>
|
|
||||||
<trim prefix=" VALUE(" suffix=")" suffixOverrides=",">
|
|
||||||
#{name,jdbcType=VARCHAR},#{password},
|
|
||||||
<if test="age!=null">
|
|
||||||
#{age}
|
|
||||||
</if>
|
|
||||||
</trim>
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<select id="selectById" resultType="com.example.mybatistest.entity.User">
|
|
||||||
select
|
|
||||||
<include refid="BASE_COLUMN"/>
|
|
||||||
from
|
|
||||||
<include refid="BASE_TABLE"/>
|
|
||||||
where id=#{id}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectAll" resultType="com.example.mybatistest.entity.User">
|
|
||||||
select
|
|
||||||
<include refid="BASE_COLUMN"/>
|
|
||||||
from
|
|
||||||
<include refid="BASE_TABLE"/>
|
|
||||||
</select>
|
|
||||||
</mapper>
|
|
||||||
```
|
|
||||||
|
|
||||||
至此使用mybatis的代码编写完了,之后要用时调用dao接口中的方法即可。
|
|
||||||
|
|
||||||
## 3.测试
|
|
||||||
|
|
||||||
我们通过编写service,controller然后使用postman进行测试。
|
|
||||||
|
|
||||||
首先编写UserService.java,代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class UserService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserDao userDao;
|
|
||||||
|
|
||||||
public User getByUserId(String id){
|
|
||||||
return userDao.selectById(id);
|
|
||||||
}
|
|
||||||
//获取全部用户
|
|
||||||
public List<User> getAll(){
|
|
||||||
return userDao.selectAll();
|
|
||||||
}
|
|
||||||
//测试分页
|
|
||||||
public PageInfo<User> getAll(int pageNum,int pageSize){
|
|
||||||
PageHelper.startPage(pageNum,pageSize);
|
|
||||||
List<User> users = userDao.selectAll();
|
|
||||||
System.out.println(users.size());
|
|
||||||
PageInfo<User> result = new PageInfo<>(users);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int insert(User user){
|
|
||||||
return userDao.insert(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
编写UserController.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
public class UserController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
@GetMapping("/user/{userId}")
|
|
||||||
public User getUser(@PathVariable String userId){
|
|
||||||
return userService.getByUserId(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/user")
|
|
||||||
public List<User> getAll(){
|
|
||||||
return userService.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/user/page/{pageNum}")
|
|
||||||
public Object getPage(@PathVariable int pageNum,
|
|
||||||
@RequestParam(name = "pageSize",required = false,defaultValue = "10") int pageSize){
|
|
||||||
return userService.getAll(pageNum,pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/user")
|
|
||||||
public Object addOne(User user){
|
|
||||||
userService.insert(user);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
启动项目,通过postman进行请求测试,测试结果如下:
|
|
||||||
|
|
||||||
- 插入数据:
|
|
||||||
|
|
||||||
![插入](./picFolder/pic2.png)
|
|
||||||
|
|
||||||
- 查询数据
|
|
||||||
|
|
||||||
![查询](./picFolder/pic3.png)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- 分页查询
|
|
||||||
|
|
||||||
![分页查询](./picFolder/pic4.png)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 4.注解编写sql
|
|
||||||
|
|
||||||
上面使用的是xml方式编写sql代码,其实mybatis也支持在注解中编写sql,这样可以避免编写复杂的xml查询文件,但同时也将sql语句耦合到了代码中,也不易实现复杂查询,因此多用于简单sql语句的编写。
|
|
||||||
|
|
||||||
要使用注解首先将applicaton.yml配置文件中的`mapper-locations: classpath:mapper/*.xml`注释掉。然后在UserDao.java中加入sql注解,代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface UserDao {
|
|
||||||
//插入用户
|
|
||||||
@Insert("insert into user(name,age,password) value(#{name},#{age},#{password})")
|
|
||||||
@Options(useGeneratedKeys=true,keyColumn="id",keyProperty="id")
|
|
||||||
int insert(User user);
|
|
||||||
//根据id查询
|
|
||||||
@Select("select * from user where id=#{id}")
|
|
||||||
User selectById(String id);
|
|
||||||
//查询所有
|
|
||||||
@Select("select * from user")
|
|
||||||
List<User> selectAll();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后重新启动项目测试,测试结果跟上面完全一样。
|
|
||||||
|
|
||||||
```
|
|
||||||
如果对你有帮助记得点赞、收藏哦!
|
|
||||||
```
|
|
Before Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 37 KiB |
@ -1,117 +0,0 @@
|
|||||||
[id]:2018-09-05
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,springboot,activemq
|
|
||||||
|
|
||||||
|
|
||||||
**说明:acitveMQ版本为:5.9.1,springboot版本为2.0.3**<br/>
|
|
||||||
## 一. 下载安装(windows)
|
|
||||||
  官方下载地址:[点我跳转](http://activemq.apache.org/download-archives.html),选择windows安装包下载,然后解压,解压后运行bin目录下的**activemq.bat**启动服务,无报错即可启动成功。默认管理地址为:[localhost:8161/admin](localhost:8161/admin),默认管理员账号密码为**admin**/**admin**。
|
|
||||||
|
|
||||||
## 二. springboot整合
|
|
||||||
|
|
||||||
###  1. 创建springboot项目
|
|
||||||
|
|
||||||
  创建springboot web项目,加入spring-boot-starter-activemq依赖。
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-activemq</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
  然后编辑配合文件,加上一个配置:61616为activeMQ的默认端口,暂时不做其他配置,使用默认值。
|
|
||||||
```yml
|
|
||||||
spring:
|
|
||||||
activemq:
|
|
||||||
broker-url: tcp://localhost:61616
|
|
||||||
```
|
|
||||||
###  2. 创建生产者消费者
|
|
||||||
  springboot中activeMQ的默认配置为**生产-消费者模式**,还有一种模式为**发布-订阅模式**后面再讲。项目目录如下:
|
|
||||||
![项目目录](./picFolder/pic1.png)
|
|
||||||
|
|
||||||
  首先编写配置类Config.java,代码如下
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class Config {
|
|
||||||
@Bean(name = "queue2")
|
|
||||||
public Queue queue2(){
|
|
||||||
return new ActiveMQQueue("active.queue2");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "queue1")
|
|
||||||
public Queue queue1(){
|
|
||||||
return new ActiveMQQueue("active.queue1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
上面的代码建立了两个消息队列queue1,queue2,分别由queue1和queue2这两个Bean注入到Spring容器中。程序运行后会在activeMQ的管理页面->queue中看到如下:
|
|
||||||
![队列](./picFolder/pic2.png)
|
|
||||||
|
|
||||||
  生产者Producer.java代码如下:
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
public class Producer {
|
|
||||||
@Autowired
|
|
||||||
private JmsMessagingTemplate jmsMessagingTemplate;
|
|
||||||
@Autowired()
|
|
||||||
@Qualifier("queue2")
|
|
||||||
private Queue queue2;
|
|
||||||
@Autowired()
|
|
||||||
@Qualifier("queue1")
|
|
||||||
private Queue queue1;
|
|
||||||
|
|
||||||
@GetMapping("/queue2")
|
|
||||||
public void sendMessage1(String message){
|
|
||||||
jmsMessagingTemplate.convertAndSend(queue2,"I'm from queue2:"+message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/queue1")
|
|
||||||
public void sendMessage2(String message){
|
|
||||||
jmsMessagingTemplate.convertAndSend(queue1,"I'm from queue1:"+message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
上面的类创建了两个GET接口,访问这两个接口分别向queue1和queue2中发送消息。
|
|
||||||
|
|
||||||
消费者Comsumer.java代码如下:
|
|
||||||
```java
|
|
||||||
@Component //将该类注解到Spring 容器中
|
|
||||||
public class Comsumer {
|
|
||||||
//接受消息队列1消息
|
|
||||||
@JmsListener(destination = "active.queue1") //监听active.queue1消息队列
|
|
||||||
public void readActiveQueue11(String message){
|
|
||||||
System.out.println(1+message);
|
|
||||||
}
|
|
||||||
|
|
||||||
//接受消息队列1消息
|
|
||||||
@JmsListener(destination = "active.queue1")
|
|
||||||
public void readActiveQueue12(String message){
|
|
||||||
System.out.println(2+message);
|
|
||||||
}
|
|
||||||
|
|
||||||
//接受消息队列2消息
|
|
||||||
@JmsListener(destination = "active.queue2")
|
|
||||||
public void readActiveQueue21(String message){
|
|
||||||
System.out.println(1+message);
|
|
||||||
}
|
|
||||||
|
|
||||||
//接受消息队列2消息
|
|
||||||
@JmsListener(destination = "active.queue2")
|
|
||||||
public void readActiveQueue22(String message){
|
|
||||||
System.out.println(2+message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
上面的代码定义了4个消费者,每两个消费一个消息队列。
|
|
||||||
|
|
||||||
##  3. 运行
|
|
||||||
|
|
||||||
  启动项目后分别向/queue1?message=niihao,/queue2?message=nihaoa发送http请求,然后我们可以在控制台中看到如下输出:
|
|
||||||
```
|
|
||||||
2I'm from queue2:nihaoa
|
|
||||||
1I'm from queue2:nihaoa
|
|
||||||
2I'm from queue1:nihao
|
|
||||||
1I'm from queue1:nihao
|
|
||||||
```
|
|
||||||
消息都成功被消费者消费,从打印结果也可看出生产者消费者的一个特点:一个消息只会被一个消费者消费。同时在管理页面中可以看到:
|
|
||||||
![运行结果](./picFolder/pic3.png)
|
|
||||||
每个消息队列有两个消费者,队列进入了三个消息,出了三个消息,说明消息都被消费掉了,如果注释掉消费者代码,再次运行,然后发送消息就会发现MessagesEnqueued数量大于MessagesDequeued,然后再让消费者上线会立即消费掉队列中的消息。
|
|
@ -1,158 +0,0 @@
|
|||||||
[id]:2018-09-06
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,activemq
|
|
||||||
|
|
||||||
|
|
||||||
  单个MQ节点总是不可靠的,一旦该节点出现故障,MQ服务就不可用了,势必会产生较大的损失。这里记录activeMQ如何开启主从备份,一旦master(主节点故障),slave(从节点)立即提供服务,实现原理是运行多个MQ使用同一个持久化数据源,这里以jdbc数据源为例。同一时间只有一个节点(节点A)能够抢到数据库的表锁,其他节点进入阻塞状态,一旦A发生错误崩溃,其他节点就会重新获取表锁,获取到锁的节点成为master,其他节点为slave,如果节点A重新启动,也将成为slave。
|
|
||||||
|
|
||||||
主从备份解决了单节点故障的问题,但是同一时间提供服务的只有一个master,显然是不能面对数据量的增长,所以需要一种横向拓展的集群方式来解决面临的问题。
|
|
||||||
|
|
||||||
### 一、activeMQ设置
|
|
||||||
|
|
||||||
#### 1、平台版本说明:
|
|
||||||
|
|
||||||
- 平台:windows
|
|
||||||
- activeMQ版本:5.9.1,[下载地址](https://www.apache.org/dist/activemq/5.9.1/apache-activemq-5.9.1-bin.zip.asc)
|
|
||||||
- jdk版本:1.8
|
|
||||||
|
|
||||||
#### 2、下载jdbc依赖
|
|
||||||
|
|
||||||
  下载下面三个依赖包,放到activeMQ安装目录下的lib文件夹中。
|
|
||||||
|
|
||||||
[mysql驱动](http://central.maven.org/maven2/mysql/mysql-connector-java/5.1.38/mysql-connector-java-5.1.38.jar)
|
|
||||||
|
|
||||||
[dhcp依赖](http://central.maven.org/maven2/org/apache/commons/commons-dbcp2/2.1.1/commons-dbcp2-2.1.1.jar)
|
|
||||||
|
|
||||||
[commons-pool2依赖](http://maven.aliyun.com/nexus/service/local/artifact/maven/redirect?r=jcenter&g=org.apache.commons&a=commons-pool2&v=2.6.0&e=jar)
|
|
||||||
|
|
||||||
###二、主从备份
|
|
||||||
|
|
||||||
####1、修改jettty
|
|
||||||
|
|
||||||
  首先修改conf->jetty.xml,这里是修改activemq的web管理端口,管理界面账号密码默认为admin/admin
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean id="jettyPort" class="org.apache.activemq.web.WebConsolePort" init-method="start">
|
|
||||||
<!-- the default port number for the web console -->
|
|
||||||
<property name="port" value="8161"/>
|
|
||||||
</bean>
|
|
||||||
```
|
|
||||||
|
|
||||||
####2、修改activemq.xml
|
|
||||||
|
|
||||||
  然后修改conf->activemq.xml
|
|
||||||
|
|
||||||
- 设置连接方式
|
|
||||||
|
|
||||||
默认是下面五种连接方式都打开,这里我们只要tcp,把其他的都注释掉,然后在这里设置activemq的服务端口,可以看到每种连接方式都对应一个端口。
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<transportConnectors>
|
|
||||||
<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
|
|
||||||
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
|
|
||||||
<!-- <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
|
|
||||||
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
|
|
||||||
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
|
|
||||||
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/> -->
|
|
||||||
</transportConnectors>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- 设置jdbc数据库
|
|
||||||
|
|
||||||
mysql数据库中创建activemq库,在`broker`标签的下面也就是根标签`beans`的下一级创建一个bean节点,内容如下:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<bean id="mysql-qs" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
|
|
||||||
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
|
|
||||||
<property name="url" value="jdbc:mysql://localhost:3306/activemq?relaxAutoCommit=true"/>
|
|
||||||
<property name="username" value="root"/>
|
|
||||||
<property name="password" value="123456"/>
|
|
||||||
<property name="poolPreparedStatements" value="true"/>
|
|
||||||
</bean>
|
|
||||||
```
|
|
||||||
|
|
||||||
- 设置数据源
|
|
||||||
|
|
||||||
首先修改broker节点,设置name和persistent(默认为true),也可不做修改,修改后如下:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="mq1" persistent="true" dataDirectory="${activemq.data}">
|
|
||||||
```
|
|
||||||
|
|
||||||
然后设置持久化方式,使用到我们之前设置的mysql-qs
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<persistenceAdapter>
|
|
||||||
<!-- <kahaDB directory="${activemq.data}/kahadb"/> -->
|
|
||||||
<jdbcPersistenceAdapter dataDirectory="${activemq.base}/activemq-data" dataSource="#mysql-qs"/>
|
|
||||||
</persistenceAdapter>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3、启动
|
|
||||||
|
|
||||||
  设置完毕后启动activemq(双击bin中的acitveMQ.jar),启动完成后可以看到如下日志信息:
|
|
||||||
|
|
||||||
```verilog
|
|
||||||
INFO | Using a separate dataSource for locking: org.apache.commons.dbcp2.BasicDataSource@179ece50
|
|
||||||
INFO | Attempting to acquire the exclusive lock to become the Master broker
|
|
||||||
INFO | Becoming the master on dataSource: org.apache.commons.dbcp2.BasicDataSource@179ece50
|
|
||||||
```
|
|
||||||
|
|
||||||
接着我们修改一下tcp服务端口,改为61617,然后重新启动,日志信息如下:
|
|
||||||
|
|
||||||
```verilog
|
|
||||||
INFO | Using a separate dataSource for locking: org.apache.commons.dbcp2.BasicDataSource@179ece50
|
|
||||||
INFO | Attempting to acquire the exclusive lock to become the Master broker
|
|
||||||
INFO | Failed to acquire lock. Sleeping for 10000 milli(s) before trying again...
|
|
||||||
INFO | Failed to acquire lock. Sleeping for 10000 milli(s) before trying again...
|
|
||||||
INFO | Failed to acquire lock. Sleeping for 10000 milli(s) before trying again...
|
|
||||||
INFO | Failed to acquire lock. Sleeping for 10000 milli(s) before trying again...
|
|
||||||
```
|
|
||||||
|
|
||||||
可以看到从节点一直在尝试获取表锁成为主节点,这样一旦主节点失效,从节点能够立刻取代主节点提供服务。这样我们便实现了主从备份。
|
|
||||||
|
|
||||||
### 三、负载均衡
|
|
||||||
|
|
||||||
  activemq可以实现多个mq之间进行路由,假设有两个mq,分别为brokerA和brokerB,当一条消息发送到brokerA的队列test中,有一个消费者连上了brokerB,并且想要获取test队列,brokerA中的test队列就会路由到brokerB上。
|
|
||||||
|
|
||||||
   开启负载均衡需要设置`networkConnectors`节点,静态路由配置如下:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<networkConnectors>
|
|
||||||
<networkConnector uri="static:failover://(tcp://localhost:61616,tcp://localhost:61617)" duplex="false"/>
|
|
||||||
</networkConnectors>
|
|
||||||
```
|
|
||||||
|
|
||||||
brokerA和brokerB都要设置该配置,以连上对方。
|
|
||||||
|
|
||||||
### 四、测试
|
|
||||||
|
|
||||||
####1、建立mq
|
|
||||||
|
|
||||||
  组建两组broker,每组做主从配置。
|
|
||||||
|
|
||||||
- brokerA:
|
|
||||||
- 主:设置web管理端口8761,设置mq名称`mq`,设置数据库地址为activemq,设置tcp服务端口61616,设置负载均衡静态路由`static:failover://(tcp://localhost:61618,tcp://localhost:61619)`,然后启动
|
|
||||||
- 从:上面的基础上修改tcp服务端口为61617,然后启动
|
|
||||||
- brokerB:
|
|
||||||
- 主:设置web管理端口8762,设置mq名称`mq1`,设置数据库地址activemq1,设置tcp服务端口61618,设置负载均衡静态路由`static:failover://(tcp://localhost:61616,tcp://localhost:61617)`,然后启动
|
|
||||||
- 从:上面的基础上修改tcp服务端口为61619,然后启动
|
|
||||||
|
|
||||||
#### 2、springboot测试
|
|
||||||
|
|
||||||
   沿用上一篇的项目,修改配置文件的broker-url为`failover:(tcp://localhost:61616,tcp://localhost:61617,tcp://localhost:61618,tcp://localhost:61619)`,然后启动项目访问会在控制台看到如下日志:
|
|
||||||
|
|
||||||
```java
|
|
||||||
2018-07-31 15:09:25.076 INFO 12780 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to tcp://localhost:61618
|
|
||||||
1I'm from queue1:hello
|
|
||||||
2018-07-31 15:09:26.599 INFO 12780 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to tcp://localhost:61618
|
|
||||||
2I'm from queue1:hello
|
|
||||||
2018-07-31 15:09:29.002 INFO 12780 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to tcp://localhost:61616
|
|
||||||
1I'm from queue1:hello
|
|
||||||
2018-07-31 15:09:34.931 INFO 12780 --- [ActiveMQ Task-1] o.a.a.t.failover.FailoverTransport : Successfully connected to tcp://localhost:61618
|
|
||||||
2I'm from queue1:hello
|
|
||||||
```
|
|
||||||
|
|
||||||
证明负载均衡成功。
|
|
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 44 KiB |
@ -1,315 +0,0 @@
|
|||||||
[id]:2018-09-10
|
|
||||||
[type]:javaee
|
|
||||||
[tag]:java,spring,springboot,mybatis,读写分离
|
|
||||||
|
|
||||||
  近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离。这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,还不带格式的,看的真心难受)。
|
|
||||||
|
|
||||||
[完整代码](https://github.com/FleyX/demo-project/tree/master/dxfl)
|
|
||||||
|
|
||||||
## 1、背景
|
|
||||||
|
|
||||||
  一个项目中数据库最基础同时也是最主流的是单机数据库,读写都在一个库中。当用户逐渐增多,单机数据库无法满足性能要求时,就会进行读写分离改造(适用于读多写少),写操作一个库,读操作多个库,通常会做一个数据库集群,开启主从备份,一主多从,以提高读取性能。当用户更多读写分离也无法满足时,就需要分布式数据库了(可能以后会学习怎么弄)。
|
|
||||||
|
|
||||||
  正常情况下读写分离的实现,首先要做一个一主多从的数据库集群,同时还需要进行数据同步。这一篇记录如何用mysql搭建一个一主多次的配置,下一篇记录代码层面如何实现读写分离。
|
|
||||||
|
|
||||||
## 2、搭建一主多从数据库集群
|
|
||||||
|
|
||||||
  主从备份需要多台虚拟机,我是用wmware完整克隆多个实例,注意直接克隆的虚拟机会导致每个数据库的uuid相同,需要修改为不同的uuid。修改方法参考这个:[点击跳转](https://blog.csdn.net/pratise/article/details/80413198)。
|
|
||||||
|
|
||||||
- 主库配置
|
|
||||||
|
|
||||||
主数据库(master)中新建一个用户用于从数据库(slave)读取主数据库二进制日志,sql语句如下:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
mysql> CREATE USER 'repl'@'%' IDENTIFIED BY '123456';#创建用户
|
|
||||||
mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';#分配权限
|
|
||||||
mysql>flush privileges; #刷新权限
|
|
||||||
```
|
|
||||||
|
|
||||||
同时修改mysql配置文件开启二进制日志,新增部分如下:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
[mysqld]
|
|
||||||
server-id=1
|
|
||||||
log-bin=master-bin
|
|
||||||
log-bin-index=master-bin.index
|
|
||||||
```
|
|
||||||
|
|
||||||
然后重启数据库,使用`show master status;`语句查看主库状态,如下所示:
|
|
||||||
|
|
||||||
![主库状态](./picFolder/pic1.png)
|
|
||||||
|
|
||||||
- 从库配置
|
|
||||||
|
|
||||||
同样先新增几行配置:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
[mysqld]
|
|
||||||
server-id=2
|
|
||||||
relay-log-index=slave-relay-bin.index
|
|
||||||
relay-log=slave-relay-bin
|
|
||||||
```
|
|
||||||
|
|
||||||
然后重启数据库,使用如下语句连接主库:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CHANGE MASTER TO
|
|
||||||
MASTER_HOST='192.168.226.5',
|
|
||||||
MASTER_USER='root',
|
|
||||||
MASTER_PASSWORD='123456',
|
|
||||||
MASTER_LOG_FILE='master-bin.000003',
|
|
||||||
MASTER_LOG_POS=154;
|
|
||||||
```
|
|
||||||
|
|
||||||
接着运行`start slave;`开启备份,正常情况如下图所示:Slave_IO_Running和Slave_SQL_Running都为yes。
|
|
||||||
|
|
||||||
![1536223020742](./picFolder/pic2.png)
|
|
||||||
|
|
||||||
可以用这个步骤开启多个从库。
|
|
||||||
|
|
||||||
  默认情况下备份是主库的全部操作都会备份到从库,实际可能需要忽略某些库,可以在主库中增加如下配置:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
# 不同步哪些数据库
|
|
||||||
binlog-ignore-db = mysql
|
|
||||||
binlog-ignore-db = test
|
|
||||||
binlog-ignore-db = information_schema
|
|
||||||
|
|
||||||
# 只同步哪些数据库,除此之外,其他不同步
|
|
||||||
binlog-do-db = game
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3、代码层面进行读写分离
|
|
||||||
|
|
||||||
  代码环境是springboot+mybatis+druib连接池。想要读写分离就需要配置多个数据源,在进行写操作是选择写的数据源,读操作时选择读的数据源。其中有两个关键点:
|
|
||||||
|
|
||||||
- 如何切换数据源
|
|
||||||
- 如何根据不同的方法选择正确的数据源
|
|
||||||
|
|
||||||
### 1)、如何切换数据源
|
|
||||||
|
|
||||||
  通常用springboot时都是使用它的默认配置,只需要在配置文件中定义好连接属性就行了,但是现在我们需要自己来配置了,spring是支持多数据源的,多个datasource放在一个HashMap`TargetDataSource`中,通过`dertermineCurrentLookupKey`获取key来觉定要使用哪个数据源。因此我们的目标就很明确了,建立多个datasource放到TargetDataSource中,同时重写dertermineCurrentLookupKey方法来决定使用哪个key。
|
|
||||||
|
|
||||||
### 2)、如何选择数据源
|
|
||||||
|
|
||||||
  事务一般是注解在Service层的,因此在开始这个service方法调用时要确定数据源,有什么通用方法能够在开始执行一个方法前做操作呢?相信你已经想到了那就是**切面 **。怎么切有两种办法:
|
|
||||||
|
|
||||||
- 注解式,定义一个只读注解,被该数据标注的方法使用读库
|
|
||||||
- 方法名,根据方法名写切点,比如getXXX用读库,setXXX用写库
|
|
||||||
|
|
||||||
### 3)、代码编写
|
|
||||||
|
|
||||||
#### a、编写配置文件,配置两个数据源信息
|
|
||||||
|
|
||||||
  只有必填信息,其他都有默认设置
|
|
||||||
|
|
||||||
```yml
|
|
||||||
mysql:
|
|
||||||
datasource:
|
|
||||||
#读库数目
|
|
||||||
num: 1
|
|
||||||
type-aliases-package: com.example.dxfl.dao
|
|
||||||
mapper-locations: classpath:/mapper/*.xml
|
|
||||||
config-location: classpath:/mybatis-config.xml
|
|
||||||
write:
|
|
||||||
url: jdbc:mysql://192.168.226.5:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
driver-class-name: com.mysql.jdbc.Driver
|
|
||||||
read:
|
|
||||||
url: jdbc:mysql://192.168.226.6:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
driver-class-name: com.mysql.jdbc.Driver
|
|
||||||
```
|
|
||||||
|
|
||||||
#### b、编写DbContextHolder类
|
|
||||||
|
|
||||||
  这个类用来设置数据库类别,其中有一个ThreadLocal用来保存每个线程的是使用读库,还是写库。代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* Description 这里切换读/写模式
|
|
||||||
* 原理是利用ThreadLocal保存当前线程是否处于读模式(通过开始READ_ONLY注解在开始操作前设置模式为读模式,
|
|
||||||
* 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式
|
|
||||||
* @author fxb
|
|
||||||
* @date 2018-08-31
|
|
||||||
*/
|
|
||||||
public class DbContextHolder {
|
|
||||||
|
|
||||||
private static Logger log = LoggerFactory.getLogger(DbContextHolder.class);
|
|
||||||
public static final String WRITE = "write";
|
|
||||||
public static final String READ = "read";
|
|
||||||
|
|
||||||
private static ThreadLocal<String> contextHolder= new ThreadLocal<>();
|
|
||||||
|
|
||||||
public static void setDbType(String dbType) {
|
|
||||||
if (dbType == null) {
|
|
||||||
log.error("dbType为空");
|
|
||||||
throw new NullPointerException();
|
|
||||||
}
|
|
||||||
log.info("设置dbType为:{}",dbType);
|
|
||||||
contextHolder.set(dbType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getDbType() {
|
|
||||||
return contextHolder.get() == null ? WRITE : contextHolder.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clearDbType() {
|
|
||||||
contextHolder.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### c、重写determineCurrentLookupKey方法
|
|
||||||
|
|
||||||
  spring在开始进行数据库操作时会通过这个方法来决定使用哪个数据库,因此我们在这里调用上面DbContextHolder类的`getDbType()`方法获取当前操作类别,同时可进行读库的负载均衡,代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
|
|
||||||
|
|
||||||
@Value("${mysql.datasource.num}")
|
|
||||||
private int num;
|
|
||||||
|
|
||||||
private final Logger log = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Object determineCurrentLookupKey() {
|
|
||||||
String typeKey = DbContextHolder.getDbType();
|
|
||||||
if (typeKey == DbContextHolder.WRITE) {
|
|
||||||
log.info("使用了写库");
|
|
||||||
return typeKey;
|
|
||||||
}
|
|
||||||
//使用随机数决定使用哪个读库
|
|
||||||
int sum = NumberUtil.getRandom(1, num);
|
|
||||||
log.info("使用了读库{}", sum);
|
|
||||||
return DbContextHolder.READ + sum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### d、编写配置类
|
|
||||||
|
|
||||||
  由于要进行读写分离,不能再用springboot的默认配置,我们需要手动来进行配置。首先生成数据源,使用@ConfigurProperties自动生成数据源:
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 写数据源
|
|
||||||
*
|
|
||||||
* @Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
|
|
||||||
* 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
|
|
||||||
*/
|
|
||||||
@Primary
|
|
||||||
@Bean
|
|
||||||
@ConfigurationProperties(prefix = "mysql.datasource.write")
|
|
||||||
public DataSource writeDataSource() {
|
|
||||||
return new DruidDataSource();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
读数据源类似,注意有多少个读库就要设置多少个读数据源,Bean名为read+序号。
|
|
||||||
|
|
||||||
  然后设置数据源,使用的是我们之前写的MyAbstractRoutingDataSource类
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public AbstractRoutingDataSource routingDataSource() {
|
|
||||||
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
|
|
||||||
Map<Object, Object> targetDataSources = new HashMap<>(2);
|
|
||||||
targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
|
|
||||||
targetDataSources.put(DbContextHolder.READ+"1", read1());
|
|
||||||
proxy.setDefaultTargetDataSource(writeDataSource());
|
|
||||||
proxy.setTargetDataSources(targetDataSources);
|
|
||||||
return proxy;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
  接着需要设置sqlSessionFactory
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 多数据源需要自己设置sqlSessionFactory
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public SqlSessionFactory sqlSessionFactory() throws Exception {
|
|
||||||
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
|
|
||||||
bean.setDataSource(routingDataSource());
|
|
||||||
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
|
||||||
// 实体类对应的位置
|
|
||||||
bean.setTypeAliasesPackage(typeAliasesPackage);
|
|
||||||
// mybatis的XML的配置
|
|
||||||
bean.setMapperLocations(resolver.getResources(mapperLocation));
|
|
||||||
bean.setConfigLocation(resolver.getResource(configLocation));
|
|
||||||
return bean.getObject();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
  最后还得配置下事务,否则事务不生效
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public DataSourceTransactionManager dataSourceTransactionManager() {
|
|
||||||
return new DataSourceTransactionManager(routingDataSource());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4)、选择数据源
|
|
||||||
|
|
||||||
  多数据源配置好了,但是代码层面如何选择选择数据源呢?这里介绍两种办法:
|
|
||||||
|
|
||||||
#### a、注解式
|
|
||||||
|
|
||||||
  首先定义一个只读注解,被这个注解方法使用读库,其他使用写库,如果项目是中途改造成读写分离可使用这个方法,无需修改业务代码,只要在只读的service方法上加一个注解即可。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Target({ElementType.METHOD,ElementType.TYPE})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
public @interface ReadOnly {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
  然后写一个切面来切换数据使用哪种数据源,重写getOrder保证本切面优先级高于事务切面优先级,在启动类加上`@EnableTransactionManagement(order = 10) `,为了代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Aspect
|
|
||||||
@Component
|
|
||||||
public class ReadOnlyInterceptor implements Ordered {
|
|
||||||
private static final Logger log= LoggerFactory.getLogger(ReadOnlyInterceptor.class);
|
|
||||||
|
|
||||||
@Around("@annotation(readOnly)")
|
|
||||||
public Object setRead(ProceedingJoinPoint joinPoint,ReadOnly readOnly) throws Throwable{
|
|
||||||
try{
|
|
||||||
DbContextHolder.setDbType(DbContextHolder.READ);
|
|
||||||
return joinPoint.proceed();
|
|
||||||
}finally {
|
|
||||||
//清楚DbType一方面为了避免内存泄漏,更重要的是避免对后续在本线程上执行的操作产生影响
|
|
||||||
DbContextHolder.clearDbType();
|
|
||||||
log.info("清除threadLocal");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getOrder() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### b、方法名式
|
|
||||||
|
|
||||||
  这种方法不许要注解,但是需要事务名称按一定规则编写,然后通过切面来设置数据库类别,比如`setXXX`设置为写、`getXXX`设置为读,代码我就不写了,应该都知道怎么写。
|
|
||||||
|
|
||||||
### 4、测试
|
|
||||||
|
|
||||||
  编写好代码来试试结果如何,下面是运行截图:
|
|
||||||
|
|
||||||
![1536312274474](./picFolder/pic3.png)
|
|
||||||
|
|
||||||
  断断续续写了好几天终于是写完了,,,如果有帮助到你,,欢迎star哦,,这里是完整代码地址:[点击跳转](https://github.com/FleyX/demo-project/tree/master/dxfl)
|
|
@ -1,181 +0,0 @@
|
|||||||
[id]:2018-09-22
|
|
||||||
[type]:java
|
|
||||||
[tag]:java,reflect,excel,hssfworksheet
|
|
||||||
|
|
||||||
## 一、背景
|
|
||||||
|
|
||||||
  最近在java上做了一个EXCEL的导出功能,写了一个通用类,在这里分享分享,该类支持多sheet,且无需手动进行复杂的类型转换,只需提供三个参数即可:
|
|
||||||
|
|
||||||
- `fileName`
|
|
||||||
|
|
||||||
excel文件名
|
|
||||||
|
|
||||||
- `HasMap<String,List<?>> data`
|
|
||||||
|
|
||||||
具体的数据,每个List代表一张表的数据,?表示可为任意的自定义对象
|
|
||||||
|
|
||||||
- `LinkedHashMap<String,String[][]> headers`
|
|
||||||
|
|
||||||
`Stirng`代表sheet名。每个`String[][] `代表一个sheet的定义,举个例子如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
String[][] header = {
|
|
||||||
{"field1","参数1"}
|
|
||||||
,{"field2","参数2"}
|
|
||||||
,{"field3","参数3"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
其中的field1,field2,field3为对象中的属性名,参数1,参数2,参数3为列名,实际上这个指定了列的名称和这个列用到数据对象的哪个属性。
|
|
||||||
|
|
||||||
## 二、怎么用
|
|
||||||
|
|
||||||
  以一个例子来说明怎么用,假设有两个类A和B定义如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class A{
|
|
||||||
private String name;
|
|
||||||
private String address;
|
|
||||||
}
|
|
||||||
public class B{
|
|
||||||
private int id;
|
|
||||||
private double sum;
|
|
||||||
private String cat;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
现在我们通过查询数据库获得了A和B的两个列表:
|
|
||||||
|
|
||||||
```java
|
|
||||||
List<A> dataA = .....;
|
|
||||||
List<B> dataB = .....;
|
|
||||||
```
|
|
||||||
|
|
||||||
我们将这两个导出到excel中,首先需要定义sheet:
|
|
||||||
|
|
||||||
```java
|
|
||||||
String[][] sheetA = {
|
|
||||||
{"name","姓名"}
|
|
||||||
,{"address","住址"}
|
|
||||||
}
|
|
||||||
String[][] sheetB = {
|
|
||||||
{"id","ID"}
|
|
||||||
,{"sum","余额"}
|
|
||||||
,{"cat","猫的名字"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
然后将数据汇总构造一个ExcelUtil:
|
|
||||||
|
|
||||||
```java
|
|
||||||
String fileName = "测试Excel";
|
|
||||||
HashMap<String,List<?>> data = new HashMap<>();
|
|
||||||
//ASheet为表名,后面headers里的key要跟这里一致
|
|
||||||
data.put("ASheet",dataA);
|
|
||||||
data.put("BSheet",dataB);
|
|
||||||
LinkedHashMap<String,String[][]> headers = new LinkedHashMap<>();
|
|
||||||
headers.put("ASheet",sheetA);
|
|
||||||
headers.put("BSheet",sheetB);
|
|
||||||
ExcelUtil excelUtil = new ExcelUtil(fileName,data,headers);
|
|
||||||
//获取表格对象
|
|
||||||
HSSFWorkbook workbook = excelUtil.createExcel();
|
|
||||||
//这里内置了一个写到response的方法(判断浏览器类型设置合适的参数),如果想写到文件也是类似的
|
|
||||||
workbook.writeToResponse(workbook,request,response);
|
|
||||||
```
|
|
||||||
|
|
||||||
当然通常数据是通过数据库查询的,这里为了演示方便没有从数据库查找。
|
|
||||||
|
|
||||||
## 三、实现原理
|
|
||||||
|
|
||||||
  这里简单说明下实现过程,从调用`createExcel()`这里开始
|
|
||||||
|
|
||||||
####1、遍历headers创建sheet
|
|
||||||
|
|
||||||
```java
|
|
||||||
public HSSFWorkbook createExcel() throws Exception {
|
|
||||||
try {
|
|
||||||
HSSFWorkbook workbook = new HSSFWorkbook();
|
|
||||||
//遍历headers创建表格
|
|
||||||
for (String key : headers.keySet()) {
|
|
||||||
this.createSheet(workbook, key, headers.get(key), this.data.get(key));
|
|
||||||
}
|
|
||||||
return workbook;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("创建表格失败:{}", e.getMessage());
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
将workbook,sheet名,表头数据,行数据传入crateSheet方法中创建sheet。
|
|
||||||
|
|
||||||
#### 2、创建表头
|
|
||||||
|
|
||||||
  表头也就是一个表格的第一行,通常用来对列进行说明
|
|
||||||
|
|
||||||
```java
|
|
||||||
HSSFSheet sheet = workbook.createSheet(sheetName);
|
|
||||||
// 列数
|
|
||||||
int cellNum = header.length;
|
|
||||||
// 单元行,单元格
|
|
||||||
HSSFRow row;
|
|
||||||
HSSFCell cell;
|
|
||||||
// 表头单元格样式
|
|
||||||
HSSFCellStyle columnTopStyle = this.getColumnTopStyle(workbook);
|
|
||||||
// 设置表头
|
|
||||||
row = sheet.createRow(0);
|
|
||||||
for (int i = 0; i < cellNum; i++) {
|
|
||||||
cell = row.createCell(i);
|
|
||||||
cell.setCellStyle(columnTopStyle);
|
|
||||||
String str = header[i][1];
|
|
||||||
cell.setCellValue(str);
|
|
||||||
// 设置列宽为表头的文字宽度+6个半角符号宽度
|
|
||||||
sheet.setColumnWidth(i, (str.getBytes("utf-8").length + 6) * 256);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3、插入行数据
|
|
||||||
|
|
||||||
  这里是最重要的部分,首先通过数据的类对象获取它的反射属性Field类,然后将属性名和Field做一个hash映射,避免循环查找,提高插入速度,接着通过一个switch语句,根据属性类别设值,主要代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 设置单元格,根据fieldName获取对应的Field类,使用反射得到值
|
|
||||||
*
|
|
||||||
* @param cell 单元格实例
|
|
||||||
* @param obj 存有属性的对象实例
|
|
||||||
* @param fieldMap 属性名与Field的映射
|
|
||||||
* @param fieldName 属性名
|
|
||||||
*/
|
|
||||||
private void setCell(HSSFCell cell, Object obj, Map<String, Field> fieldMap, String fieldName) throws Exception {
|
|
||||||
//获取该属性的Field对象
|
|
||||||
Field field = fieldMap.get(fieldName);
|
|
||||||
//通过反射获取属性的值,由于不能确定该值的类型,用下面的判断语句进行合适的转型
|
|
||||||
Object value = field.get(obj);
|
|
||||||
if (value == null) {
|
|
||||||
cell.setCellValue("");
|
|
||||||
} else {
|
|
||||||
switch (field.getGenericType().getTypeName()) {
|
|
||||||
case "java.lang.String":
|
|
||||||
cell.setCellValue((String) value);
|
|
||||||
break;
|
|
||||||
case "java.lang.Integer":
|
|
||||||
case "int":
|
|
||||||
cell.setCellValue((int) value);
|
|
||||||
break;
|
|
||||||
case "java.lang.Double":
|
|
||||||
case "double":
|
|
||||||
cell.setCellValue((double) value);
|
|
||||||
break;
|
|
||||||
case "java.util.Date":
|
|
||||||
cell.setCellValue(this.dateFormat.format((Date) value));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cell.setCellValue(obj.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
完整代码可以到github上查看下载,这里就不列出来了。
|
|
||||||
|
|
||||||
github地址:[点击跳转]()
|
|