diff --git a/java/spring/1.spring基础.md b/java/spring/1.spring基础.md new file mode 100644 index 0000000..c372ca3 --- /dev/null +++ b/java/spring/1.spring基础.md @@ -0,0 +1,182 @@ +--- +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. 最简单 + + `` + +2. 带构造器 + + ```xml + + //基本数据类型使用value + //对象使用ref + + ``` + +3. 通过工厂方法创建 + + 如果想声明的Bean没有一个公开的构造函数,通过factory-method属性来装配工厂生产的Bean + + ```xml + //getInstance为获取实例的静态方法。 + ``` + +#### 4.Bean的作用域 + +​ 所有Spring Bean默认都是单例的。通过配置scope属性为prototype可每次请求产生一个新的实例。 + +```xml + +``` + +scope可选值: + +- `singleton`:每个容器中,一个Bean对象只有一个实例。(**默认**) +- `prototype`:允许实例化任意次 ,每次请求都会创建新的 +- `request`:作用域为一次http请求 +- `session`:作用域为一个http session会话 +- `global-session`:作用域为一个全局http session,仅在Protlet上下文中有效 + +#### 5.初始化和销毁Bean + +​ 当实例化需要执行初始化操作,或者销毁时需要执行清理工作。两种实现方式: + +1. xml配置,类中编写初始化方法和销毁方法,在bean中定义。 + + ```xml + + ``` + + 也可在Beans中定义默认初始化和销毁方法。 + + ```xml + + ``` + +2. 实现`InitializingBean `和`DisposableBean`接口 + +#### 6.setter注入 + +​ 在bean中使用``元素配置属性,使用方法类似于`` + +```xml + //注入基本数据类型 + //注入类 +``` + + 可使用p简写,**-ref**后缀说明装配的是一个引用 + +```xml + +``` + +#### 7.注入内部Bean + +​ 既定义其他Bean内部的Bean,避免共享问题,可在属性节点或者构造器参数节点上使用。 + +```xml + + //没有id属性,因为不会被其他bean使用 + + + + +``` + +#### 8.装配集合 + +| 集合元素 | 用途 | +| ---------------- | ------------------------------ | +| \ | 装配list类型,允许重复 | +| \ | set,不能重复 | +| \ | map类型 | +| \ | properties类型,键值都为String | + +- list + + ```xml + + + + + + + + 用来定义上下文中的其他引用,还可使用,, + ``` + +- set + + ```xml + + + + ``` + + 用法和list相同,只是不能重复 + +- Map + + ```XML + + + + ``` + + entry元素由一个key,一个value组成,分别有两种形式。 + + | key | 键为String | + | :-------- | -------------- | + | key-ref | 键为Bean的引用 | + | value | 值为String | + | value-ref | 值为Bean的引用 | + +- props + + ```xml + + guitar + + ``` + + 键值都是String + +#### 9.装配空值 + +```xml + +``` + diff --git a/java/spring/2.最小化XML配置.md b/java/spring/2.最小化XML配置.md new file mode 100644 index 0000000..ecfd55b --- /dev/null +++ b/java/spring/2.最小化XML配置.md @@ -0,0 +1,231 @@ +--- +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 | \ | +| ByType | 根据Bean类型自动装配 | \ | +| contructor | 根据Bean的构造器入参具有相同类型 | 同上 | +| Autodetect | 首先使用contructor,失败再尝试byType | 同上 | + +  byType在出现多个匹配项时不会自动选择一个然是报错,为避免报错,有两种办法:1.使用\元素的primary属性,设置为首选Bean,但所有bean的默认primary都是true,因此我们需要将所有非首选Bean设置为false;2.将Bean的`autowire-candidate`熟悉设置为**false **,取消 这个Bean的候选资格,这个Bean便不会自动注入了。 + +  contructor自动装配和byType有一样的局限性,当发现多个Bean匹配某个构造器入参时,Spring不会尝试选择其中一个;此外,如果一个类有多个构造器都满足自动装配的条件,Spring也不会猜测哪个更合适使用。 + +### 2、默认自动装配 + +  如果需要为Spring应用上下文中的每个Bean(或者其中的大多数)配置相同的autowire属性,可以在根元素\上增加一个default-autowire属性,默认该属性设置为none。该属性只应用于指定配置文件中的所有Bean,并不是Spring上下文中的所有Bean。 + +### 3、混合使用自动装配和显式装配 + +   当我们对某个Bean使用了自动装配策略,并不代表我们不能对该Bean的某些属性进行显示装配,任然可以为任意一个属性配置\元素,显式装配将会覆盖自动装配。**但是**当使用constructor自动装配策略时,我们必须让Spring自动装配构造器所有入参,不能使用\元素进行混合。 + +## 二、注解装配 + +  从Spring2.5开始,可以使用注解自动装配Bean的属性,使用注解允许更细粒度的自动装配,可选择性的标注某一个属性来对其应用自动装配。Spring容器默认禁用注解装配,需要在Spring配置中启用,最简单的启用方式是使用Spring的context命令空间配置中的``,如下所示: + +```xml + + + + +``` + +  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 + + + + ``` + + 注解如下: + + ```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 knifes; + +@Inject +public KnifeJuggler(Provider knifeProvider){ + knives = new HashSet(); + 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中使用上面说到的``,可以做到自动装配,但还是要在xml中申明Bean。Spring还有另一个元素``,元素除了完成自动装配的功能,还允许Spring自动检测Bean和定义Bean ,用法如下: + +```xml + + + + +``` + +开启后支持如下注解: + +| 注解 | 解释 | +| ----------- | ------------------------------------ | +| @Component | 通用的构造型注解,标识类为Spring组件 | +| @Controller | 标识该类定义为Spring MVC controller | +| @Repository | 标识该类定义为数据仓库 | +| @Service | 标识该类定义为服务 | + +  使用上述注解是Bean的ID默认为无限定类名。使用`@Component("name")`指定ID。 + +### 1、过滤组建扫描 + +  通过为配置<context:include-filter>和<context:exclude-filter>子元素,我们可以随意调整扫描行为。下面的配置自动注册所有的TestInterface实现类: + +```xml + + + +``` + +其中的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配置,就是上面说到的``,该标签还会自动加载使用`@Configuration`注解所标识的类 + +- @Configuration注解相当于XML配置中的\元素,这个注解将会告知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的配置实现理解也会更加深刻。 \ No newline at end of file diff --git a/java/spring/3.面向切面的Spring.md b/java/spring/3.面向切面的Spring.md new file mode 100644 index 0000000..1d6a3bd --- /dev/null +++ b/java/spring/3.面向切面的Spring.md @@ -0,0 +1,249 @@ +--- +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 + +``` + +然后申明通知: + +```xml + + + + + + + + + + + + + + + + + + +``` + + + +#### 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 + + + + ``` + + 顾名思义\<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属性指定了将被引入的接口。 + diff --git a/java/spring/picFolder/切面示例.png b/java/spring/picFolder/切面示例.png new file mode 100644 index 0000000..81f8abf Binary files /dev/null and b/java/spring/picFolder/切面示例.png differ diff --git a/java/spring/picFolder/引入新功能.png b/java/spring/picFolder/引入新功能.png new file mode 100644 index 0000000..edc1877 Binary files /dev/null and b/java/spring/picFolder/引入新功能.png differ diff --git a/java/springboot系列/springboot搭建/picFolder/1532967570728.png b/java/springboot系列/springboot搭建/picFolder/1532967570728.png new file mode 100644 index 0000000..00a4c81 Binary files /dev/null and b/java/springboot系列/springboot搭建/picFolder/1532967570728.png differ diff --git a/java/springboot系列/springboot搭建/picFolder/1532967772110.png b/java/springboot系列/springboot搭建/picFolder/1532967772110.png new file mode 100644 index 0000000..f034903 Binary files /dev/null and b/java/springboot系列/springboot搭建/picFolder/1532967772110.png differ diff --git a/java/springboot系列/springboot搭建/picFolder/1532967825469.png b/java/springboot系列/springboot搭建/picFolder/1532967825469.png new file mode 100644 index 0000000..83a3b69 Binary files /dev/null and b/java/springboot系列/springboot搭建/picFolder/1532967825469.png differ diff --git a/java/springboot系列/springboot搭建/picFolder/1532967938985.png b/java/springboot系列/springboot搭建/picFolder/1532967938985.png new file mode 100644 index 0000000..89cafd9 Binary files /dev/null and b/java/springboot系列/springboot搭建/picFolder/1532967938985.png differ diff --git a/java/springboot系列/springboot搭建/picFolder/1532968024509.png b/java/springboot系列/springboot搭建/picFolder/1532968024509.png new file mode 100644 index 0000000..59f563c Binary files /dev/null and b/java/springboot系列/springboot搭建/picFolder/1532968024509.png differ diff --git a/java/springboot系列/springboot搭建/picFolder/1532969025023.png b/java/springboot系列/springboot搭建/picFolder/1532969025023.png new file mode 100644 index 0000000..07677e4 Binary files /dev/null and b/java/springboot系列/springboot搭建/picFolder/1532969025023.png differ diff --git a/java/springboot系列/springboot搭建/springboot搭建.md b/java/springboot系列/springboot搭建/springboot搭建.md new file mode 100644 index 0000000..67d8a6a --- /dev/null +++ b/java/springboot系列/springboot搭建/springboot搭建.md @@ -0,0 +1,51 @@ +[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字符串。 diff --git a/java/springboot系列/springsecurity/picFolder/pic1.png b/java/springboot系列/springsecurity/picFolder/pic1.png new file mode 100644 index 0000000..454f4b6 Binary files /dev/null and b/java/springboot系列/springsecurity/picFolder/pic1.png differ diff --git a/java/springboot系列/springsecurity/picFolder/pic2.png b/java/springboot系列/springsecurity/picFolder/pic2.png new file mode 100644 index 0000000..9d3e516 Binary files /dev/null and b/java/springboot系列/springsecurity/picFolder/pic2.png differ diff --git a/java/springboot系列/springsecurity/springboot+security整合1.md b/java/springboot系列/springsecurity/springboot+security整合1.md new file mode 100644 index 0000000..9e36d70 --- /dev/null +++ b/java/springboot系列/springsecurity/springboot+security整合1.md @@ -0,0 +1,257 @@ +[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 + + org.springframework.boot + spring-boot-starter-security + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 1.3.2 + + + mysql + mysql-connector-java + runtime + +``` + 数据库为传统的用户--角色--权限,权限表记录了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 authorities; + + public User(string name,string password) { + this.id = id; + this.password = password; + this.name = name; + this.age = age; + } + + public void setAuthorities(List authorities) { + this.authorities = authorities; + } + + @Override + public Collection 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 authorities = new ArrayList<>(); + List 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即可实现记住我功能。 \ No newline at end of file diff --git a/java/springboot系列/springsecurity/springboot+security整合2.md b/java/springboot系列/springsecurity/springboot+security整合2.md new file mode 100644 index 0000000..dcc29b0 --- /dev/null +++ b/java/springboot系列/springsecurity/springboot+security整合2.md @@ -0,0 +1,93 @@ +[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; +} +``` +完成。 \ No newline at end of file diff --git a/java/springboot系列/springsecurity/springboot+security整合3.md b/java/springboot系列/springsecurity/springboot+security整合3.md new file mode 100644 index 0000000..f11c32e --- /dev/null +++ b/java/springboot系列/springsecurity/springboot+security整合3.md @@ -0,0 +1,220 @@ +[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 jurisdictions; + + private void loadResource() { + this.jurisdictions = jurisdictionMapper.selectAllPermission(); + } + + + @Override + public Collection getAttributes(Object object) throws IllegalArgumentException { + if (jurisdictions == null) this.loadResource(); + HttpServletRequest request = ((FilterInvocation) object).getRequest(); + Set 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 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 configAttributes) + throws AccessDeniedException, InsufficientAuthenticationException { + //无需验证放行 + if(configAttributes==null || configAttributes.size()==0) + return; + if(!authentication.isAuthenticated()){ + throw new InsufficientAuthenticationException("未登录"); + } + Collection 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) +``` +完成 \ No newline at end of file diff --git a/java/springboot系列/websocket/springboot整合websocket.md b/java/springboot系列/websocket/springboot整合websocket.md new file mode 100644 index 0000000..ec2ab00 --- /dev/null +++ b/java/springboot系列/websocket/springboot整合websocket.md @@ -0,0 +1,209 @@ +[id]:2018-08-25 +[type]:javaee +[tag]:java,spring,websocket + +

一、背景

+ +  我们都知道http协议只能浏览器单方面向服务器发起请求获得响应,服务器不能主动向浏览器推送消息。想要实现浏览器的主动推送有两种主流实现方式: + +- 轮询:缺点很多,但是实现简单 +- websocket:在浏览器和服务器之间建立tcp连接,实现全双工通信 + +  springboot使用websocket有两种方式,一种是实现简单的websocket,另外一种是实现**STOMP**协议。这一篇实现简单的websocket,STOMP下一篇在讲。 + +**注意:如下都是针对使用springboot内置容器** + +

二、实现

+ +

1、依赖引入

+ +  要使用websocket关键是`@ServerEndpoint`这个注解,该注解是javaee标准中的注解,tomcat7及以上已经实现了,如果使用传统方法将war包部署到tomcat中,只需要引入如下javaee标准依赖即可: +```xml + + javax + javaee-api + 7.0 + provided + +``` +如使用springboot内置容器,无需引入,springboot已经做了包含。我们只需引入如下依赖即可: +```xml + + org.springframework.boot + spring-boot-starter-websocket + 1.5.3.RELEASE + pom + +``` + +

2、注入Bean

+ +  首先注入一个**ServerEndpointExporter**Bean,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint。代码如下: +```java +@Configuration +public class WebSocketConfig { + @Bean + public ServerEndpointExporter serverEndpointExporter(){ + return new ServerEndpointExporter(); + } +} +``` + +

3、申明endpoint

+ +  建立**MyWebSocket.java**类,在该类中处理websocket逻辑 +```java +@ServerEndpoint(value = "/websocket") //接受websocket请求路径 +@Component //注册到spring容器中 +public class MyWebSocket { + + + //保存所有在线socket连接 + private static Map 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--; + } +} +``` + +

4、客户的实现

+ +  客户端使用h5原生websocket,部分浏览器可能不支持。代码如下: +```html + + + + websocket测试 + + + + + + + + + +``` + +

三、测试

+ +  建立一个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, …} +``` +
+  源码可在[github]()上下载,记得点赞,star哦 + diff --git a/java/springboot系列/数据库/picFolder/pic1.png b/java/springboot系列/数据库/picFolder/pic1.png new file mode 100644 index 0000000..5d6c5eb Binary files /dev/null and b/java/springboot系列/数据库/picFolder/pic1.png differ diff --git a/java/springboot系列/数据库/picFolder/pic2.png b/java/springboot系列/数据库/picFolder/pic2.png new file mode 100644 index 0000000..d5a1b00 Binary files /dev/null and b/java/springboot系列/数据库/picFolder/pic2.png differ diff --git a/java/springboot系列/数据库/picFolder/pic3.png b/java/springboot系列/数据库/picFolder/pic3.png new file mode 100644 index 0000000..ae72463 Binary files /dev/null and b/java/springboot系列/数据库/picFolder/pic3.png differ diff --git a/java/springboot系列/数据库/picFolder/pic4.png b/java/springboot系列/数据库/picFolder/pic4.png new file mode 100644 index 0000000..81bb700 Binary files /dev/null and b/java/springboot系列/数据库/picFolder/pic4.png differ diff --git a/java/springboot系列/数据库/springboot整合mybatis(xml和注解).md b/java/springboot系列/数据库/springboot整合mybatis(xml和注解).md new file mode 100644 index 0000000..033fc9e --- /dev/null +++ b/java/springboot系列/数据库/springboot整合mybatis(xml和注解).md @@ -0,0 +1,366 @@ +[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 + + + 4.0.0 + + com.example + mybatis-test + 0.0.1-SNAPSHOT + jar + + mybatis-test + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.3.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + mysql + mysql-connector-java + runtime + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 1.3.2 + + + + com.alibaba + druid-spring-boot-starter + 1.1.9 + + + + com.github.pagehelper + pagehelper-spring-boot-starter + 1.2.5 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +``` + +## 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 selectAll(); +} +``` + +​ 在mapper文件夹下创建UserMapper.xml,具体的xml编写方法查看文首的官方文档。 + +```xml + + + + + user + + + id,name,age,password + + + + INSERT INTO + + name,password, + + age + + + + #{name,jdbcType=VARCHAR},#{password}, + + #{age} + + + + + + + + +``` + +​ 至此使用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 getAll(){ + return userDao.selectAll(); + } + //测试分页 + public PageInfo getAll(int pageNum,int pageSize){ + PageHelper.startPage(pageNum,pageSize); + List users = userDao.selectAll(); + System.out.println(users.size()); + PageInfo 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 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 selectAll(); +} +``` + +然后重新启动项目测试,测试结果跟上面完全一样。 + +``` +如果对你有帮助记得点赞、收藏哦! +``` \ No newline at end of file diff --git a/java/springboot系列/消息队列/picFolder/pic1.png b/java/springboot系列/消息队列/picFolder/pic1.png new file mode 100644 index 0000000..5777f11 Binary files /dev/null and b/java/springboot系列/消息队列/picFolder/pic1.png differ diff --git a/java/springboot系列/消息队列/picFolder/pic2.png b/java/springboot系列/消息队列/picFolder/pic2.png new file mode 100644 index 0000000..f56e448 Binary files /dev/null and b/java/springboot系列/消息队列/picFolder/pic2.png differ diff --git a/java/springboot系列/消息队列/picFolder/pic3.png b/java/springboot系列/消息队列/picFolder/pic3.png new file mode 100644 index 0000000..875c341 Binary files /dev/null and b/java/springboot系列/消息队列/picFolder/pic3.png differ diff --git a/java/springboot系列/消息队列/springboot整合activeMQ(1).md b/java/springboot系列/消息队列/springboot整合activeMQ(1).md new file mode 100644 index 0000000..5a77f8d --- /dev/null +++ b/java/springboot系列/消息队列/springboot整合activeMQ(1).md @@ -0,0 +1,117 @@ +[id]:2018-09-05 +[type]:javaee +[tag]:java,spring,springboot,activemq + + +**说明:acitveMQ版本为:5.9.1,springboot版本为2.0.3**
+## 一. 下载安装(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 + + org.springframework.boot + spring-boot-starter-activemq + +``` +  然后编辑配合文件,加上一个配置: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,然后再让消费者上线会立即消费掉队列中的消息。 \ No newline at end of file diff --git a/java/springboot系列/消息队列/springboot整合activeMQ(2).md b/java/springboot系列/消息队列/springboot整合activeMQ(2).md new file mode 100644 index 0000000..0665e22 --- /dev/null +++ b/java/springboot系列/消息队列/springboot整合activeMQ(2).md @@ -0,0 +1,158 @@ +[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 + + + + +``` + +####2、修改activemq.xml + +  然后修改conf->activemq.xml + +- 设置连接方式 + + 默认是下面五种连接方式都打开,这里我们只要tcp,把其他的都注释掉,然后在这里设置activemq的服务端口,可以看到每种连接方式都对应一个端口。 + + ```xml + + + + + + ``` + + + +- 设置jdbc数据库 + + mysql数据库中创建activemq库,在`broker`标签的下面也就是根标签`beans`的下一级创建一个bean节点,内容如下: + + ```xml + + + + + + + + ``` + +- 设置数据源 + + 首先修改broker节点,设置name和persistent(默认为true),也可不做修改,修改后如下: + + ```xml + + ``` + + 然后设置持久化方式,使用到我们之前设置的mysql-qs + + ```xml + + + + + ``` + +#### 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 + + + +``` + +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 +``` + +证明负载均衡成功。 \ No newline at end of file diff --git a/java/springboot系列/读写分离配置/picFolder/pic1.png b/java/springboot系列/读写分离配置/picFolder/pic1.png new file mode 100644 index 0000000..32f919c Binary files /dev/null and b/java/springboot系列/读写分离配置/picFolder/pic1.png differ diff --git a/java/springboot系列/读写分离配置/picFolder/pic2.png b/java/springboot系列/读写分离配置/picFolder/pic2.png new file mode 100644 index 0000000..9f9c501 Binary files /dev/null and b/java/springboot系列/读写分离配置/picFolder/pic2.png differ diff --git a/java/springboot系列/读写分离配置/picFolder/pic3.png b/java/springboot系列/读写分离配置/picFolder/pic3.png new file mode 100644 index 0000000..84a4538 Binary files /dev/null and b/java/springboot系列/读写分离配置/picFolder/pic3.png differ diff --git a/java/springboot系列/读写分离配置/springboot配置读写分离.md b/java/springboot系列/读写分离配置/springboot配置读写分离.md new file mode 100644 index 0000000..a9a3c74 --- /dev/null +++ b/java/springboot系列/读写分离配置/springboot配置读写分离.md @@ -0,0 +1,315 @@ +[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 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 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) \ No newline at end of file diff --git a/java/springcloud实战/1.springCloudConfig使用.md b/java/springcloud实战/1.springCloudConfig使用.md new file mode 100644 index 0000000..d8aca31 --- /dev/null +++ b/java/springcloud实战/1.springCloudConfig使用.md @@ -0,0 +1,283 @@ +--- +id="2018-11-19-15-57-00" +title="springCloud之config" +headWord="本篇主要用于记录如何在spring cloud中将服务配置与服务代码分离开来,通过向集中的配置服务请求获取某个微服务需要的配置。同时如何对敏感信息进行加密,比如密码一类的配置项" +tags=["spring-boot", "spring-cloud-config","git"] +category="java" +serie="springCloud实战" +--- + +## 一、前言 + +  在开发普通的 web 应用中,我们通常是将配置项写在单独的配置文件中,比如`application.yml`,`application.properties`,但是在微服务架构中,可能会出现数百个微服务,如果每个微服务将配置文件写在自身的配置文件中,会导致配置文件的管理非常复杂。因此集中式的配置管理是非常有必要的,每个服务启动时从集中式的存储库中读取需要的配置信息。其模型如下: +![配置管理概念架构](./picFolder/配置管理概念架构.png) +简单来说就是如下几点: + +1. 启动一个微服务实例时向配置管理服务请求获取其所在环境的特定配置文件 +2. 实际的配置信息驻留在存储库中。可以选择不同的实现来保存配置数据,包含:源代码控制下的文件、关系数据库或键值数据存储 +3. 应用程序配置数据的实际管理和应用程序无关。配置的更改通常通过构建和部署管道来处理 +4. 进行配置管理更改时,必须通知使用该配置的服务实例 + +  由于本系列为 spring cloud,所以使用`Spring Cloud Config`来构建配置管理,当然还有很多其他优秀的解决方案(Etcd,Eureka,Consul...)。 + +## 二、构建配置服务 + +  spring cloud 是建立在 spring boot 的基础上的,因此需要有 spring boot 的构建基础。 + +### 1、pom 编写 + +  pom 主要依赖如下(篇幅原因列出主要内容,完整代码请到 github 上查看),spring boot 版本和 spring cloud 版本如下,之后不在赘述: + +```xml + + org.springframework.boot + spring-boot-starter-parent + 1.4.4.RELEASE + + + + + + org.springframework.cloud + spring-cloud-dependencies + Camden.SR5 + pom + import + + + + + + UTF-8 + UTF-8 + 1.8 + Camden.SR5 + + + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-starter-config + + + + org.springframework.boot + spring-boot-starter-test + test + + +``` + +### 2、注解引导类 + +  只需在 spring boot 启动类上加入一个`@EnableConfigServer`注解即可。 + +### 3、配置服务配置编写(使用文件存储) + +  这里是给**配置服务**使用的配置文件,用于声明端口,存储库类别等信息,并不是给其他微服务使用的配置。配置如下(使用文件存储配置信息): + +```yaml +server: + port: 8888 +spring: + profiles: + # 使用文件系统来存储配置信息,需要设置为native + active: native +cloud: + config: + server: + native: + # 使用文件来存放配置文件,为每个应用程序提供用逗号分隔的文件夹列表 + searchLocations: file:///D:/configFolder/licensingservice +``` + +### 4、创建供应用程序使用的配置文件 + +  通过上面的`searchLocations`可知目前有一个名为 licensingservice 的应用程序,在对应目录下创建如下三个配置文件: + +- licensingservice.yml + +```yaml +server: + port: 10010 +spring: + application: + name: licensingservice +``` + +- licensingservice-dev.yml + +```yaml +server: + port: 10011 +``` + +- licensingservice-prod.yml + +```yaml +server: + port: 10012 +``` + +配置文件命名约定为:`应用程序名称-环境名称.yml`。现在启动应用便能通过 http 请求来获取配置了。 + +  请求[localhost:8888/licensingservice/default](localhost:8888/licensingservice/default),返回结果如下: + +```json +{ + "name": "licensingservice", + "profiles": ["default"], + "label": null, + "version": null, + "state": null, + "propertySources": [ + { + "name": "file:///D:/configFolder/licensingservice/licensingservice.yml", + "source": { + "server.port": 10001, + "spring.application.name": "licensingservice" + } + } + ] +} +``` + +  请求[localhost:8888/licensingservice/dev](localhost:8888/licensingservice/dev),返回结果如下: + +```json +{ + "name": "licensingservice", + "profiles": ["dev"], + "label": null, + "version": null, + "state": null, + "propertySources": [ + { + "name": "file:///D:/configFolder/licensingservice/licensingservice-dev.yml", + "source": { + "server.port": 10011 + } + }, + { + "name": "file:///D:/configFolder/licensingservice/licensingservice.yml", + "source": { + "server.port": 10001, + "spring.application.name": "licensingservice" + } + } + ] +} +``` + +## 二、和 spring boot 客户端集成 + +  上面写了如何使用 spring cloud config 构建配置服务,这一节来构建 licensingserivce 服务,使用上面的配置服务来获取配置文件。 + +### 1、创建 springboot 工程 + +  创建 springboot 项目 licensingservice,主要依赖如下: + +```xml + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-config-client + +``` + +### 2、编写配置文件 + +  共两个配置文件,`application.yml`,`bootstrap.yml` + +- application.yml + + 本配置文件用于存放留在本地配置信息,如果存在同名配置,本地的会被覆盖,不会生效 + + ```yaml + server: + port: 10099 + ``` + +- bootstrap.yml + + 给 spring cloud config client 读取的配置文件,根据该配置向配置中心请求 + + ```yaml + spring: + application: + #指定名称,以便spring cloud config客户端知道查找哪个配置 + name: licensingservice + profiles: + #指定环境(default,dev,prod) + active: dev + cloud: + config: + #指定config server地址 + uri: http://localhost:8888 + ``` + + PS:如果想要覆盖 bootstrap.yml 的配置可在启动命令加上-d 参数,如: + + `java -Dsptring.cloud.config.uri=.... -Dspring.profiles.active=prod xxxxx.jar` + +### 3、启动 + +  启动 licensingservice 可以发现启动端口为 10011,说明远程读取配置生效了。 + +## 三、使用 git 作为配置服务的数据源 + +### 1、创建源配置文件 + +  在 github 某个仓库下创建配置文件,比如在[https://github.com/FleyX/demo-project](https://github.com/FleyX/demo-project)仓库下的**springcloud/config**目录下创建 licengingservice 服务的配置文件。 + +### 2、修改 config server 配置文件 + +  修改 confsvr 中的 application.yml + +```yaml +server: + port: 8888 +spring: + profiles: + # 使用文件系统来存储配置信息,需要设置为native,git设置为git + active: git + application: + name: test + cloud: + config: + server: + native: + # 使用文件来存放配置文件,为每个应用程序提供用逗号分隔的文件夹列表 + searchLocations: file:///D:/configFolder/licensingservice + git: + uri: https://github.com/FleyX/demo-project + # 查找配置文件路径(,分隔) + search-paths: springcloud/config/licensingservice + #如果为公开仓库,用户名密码可不填写 + username: + password: + #配置git仓库的分支 + label: master +``` + +### 3、启动 + +  重新启动,即可发现配置成功生效。 + +## 四、配置刷新 + +  使用 spring cloud 配置服务器时,有一个问题是如何在属性变化时动态刷新应用程序。spring cloud 配置服务始终提供最新版本的属性,对低层存储库属性的更改将会是最新的。但是 config client 并不会知道配置的变更,因此不会自动刷新属性。 + +  Spring Boot Actuator 提供了一个`@RefreshScope`属性来重新读取应用程序配置信息,开发人员可通过`/refresh`进行刷新。该注释需要注释在启动入口类上。注意:**只会加载自定义 Spring 属性,例如数据库,端口等配置不会重新加载**。 + +## 总结 + +  本篇只是用到了 spring-cloud-config 这个来进行配置集中管理,并没有涉及到微服务,在下一篇将开始微服务的学习。 +  本篇两个项目代码存放于:[记得补充啊]() diff --git a/java/springcloud实战/2.springCloud服务发现.md b/java/springcloud实战/2.springCloud服务发现.md new file mode 100644 index 0000000..41b93a1 --- /dev/null +++ b/java/springcloud实战/2.springCloud服务发现.md @@ -0,0 +1,440 @@ +--- +id="2018-11-22-15-57-00" +title="springCloud之config" +headWord="在任何分布式架构中,都需要找到机器所在的物理地址,这个概念自分布式计算开始就已经存在,并且被正式称为服务发现,本篇是对服务发现的一个学习总结" +tags=["spring-boot", "spring-cloud-config","git"] +category="java" +serie="springCloud实战" +--- + +# 一、服务发现架构 + +  服务发现架构通常具有下面 4 个概念: + +1. 服务注册:服务如何使用服务发现代理进行注册? +2. 服务地址的客户端查找:服务客户端查找服务信息的方法是什么? +3. 信息共享:如何跨节点共享服务信息? +4. 健康监测:服务如何将它的健康信息传回给服务发现代理? + +下图展示了这 4 个概念的流程,以及在服务发现模式实现中通常发生的情况: +![服务发现架构](./picFolder/服务发现架构.png) + +  通常服务实例都只向一个服务发现实例注册,服务发现实例之间再通过数据传输,让每个服务实例注册到所有的服务发现实例中。 +  服务在向服务发现实例注册后,这个服务就能被服务消费者调用了。服务消费者可以使用多种模型来"发现"服务。 + +1. 每次调用服务时,通过服务发现层来获取目标服务地址并进行调用。这种用的比较少,弊端较多。首先是每次服务调用都通过服务发现层来完成,耗时会比直接调用高。最主要的是这种方法很脆弱,消费端完全依赖于服务发现层来查找和调用服务。 +2. 更健壮的方法是使用所谓的客户端负载均衡。 + +  如下图所示: +![客户端负载均衡](./picFolder/客户端负载均衡模型.png +  在这个模型中,当服务消费者需要调用一个服务时: + +  (1)联系服务发现层,获取所请求服务的所有服务实例,然后放到本地缓存中。 + +  (2)每次调用该服务时,服务消费者从缓存中取出一个服务实例的位置,通常这个'取出'使用简单的复制均衡算法,如“轮询”,“随机",以确保服务调用分布在所有实例之间。 + +  (3)客户端将定期与服务发现层进行通信,并刷新服务实例的缓存。 + +  (4)如果在调用服务的过程中,服务调用失败,那么本地缓存将从服务发现层中刷新数据,再次尝试。 + +# 二、spring cloud 实战 + +  使用 spring cloud 和 Netflix Eureka 搭建服务发现实例。 + +## 1、构建 Spring Eureka 服务 + +  eurekasvr POM 主要配置如下: + +```xml + + + org.springframework.cloud + spring-cloud-starter-eureka-server + +``` + +  applicaiton.yml 配置如下: + +```yaml +server: + port: 8761 + +eureka: + client: + #不注册自己 + register-with-eureka: false + #不在本地缓存注册表信息 + fetch-registry: false + server: + #接受请求前的等待实际,开发模式下不要开启 + #wait-time-in-ms-when-sync-empty: 5 +``` + +  最后在启动类上加入注释`@SpringBootApplication`即可启动服务中心。服务中心管理页面:[http://localhost:8761](http://localhost:8761) + +## 2、将服务注册到服务中心 + +  这里我们编写一个新服务注册到服务中心,organizationservice:组织服务。并将上一篇的两个服务:confsvr:配置中心服务,licensingservice:授权服务注册到服务中心。 + +### a、confvr 注册 + +  首先修改 POM 文件: + +```XML + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-starter-eureka + +``` + +  然后修改配置文件 application.yml: + +```yaml +server: + port: 8888 + +eureka: + instance: + #注册服务的IP,而不是服务器名 + prefer-ip-address: true + client: + #向eureka注册服务 + register-with-eureka: true + #拉取注册表的本地副本 + fetch-registry: true + service-url: + #Eureka服务的位置(如果有多个注册中心,使用,分隔) + defaultZone: http://localhost:8761/eureka/ + +spring: + profiles: + # 使用文件系统来存储配置信息,需要设置为native + active: native + application: + name: confsvr + cloud: + config: + server: + native: + # 使用文件来存放配置文件,为每个应用程序提供用逗号分隔的文件夹列表 + searchLocations: file:///D:/configFolder/licensingservice,file:///D:/configFolder/organizationservice +``` + +  最后在启动类加入注解`@EnableDiscoveryClient`,启动即可在 eureka 管理页面发现。 + +### b、licensingservice 注册 + +  首先修改 POM + +```xml + + org.springframework.cloud + spring-cloud-starter-eureka + + + org.springframework.cloud + spring-cloud-config-client + +``` + +  然后修改配置文件 bootstrap.yml + +```yaml +spring: + application: + #指定名称,以便spring cloud config客户端知道查找哪个配置 + name: licensingservice + profiles: + #指定环境 + active: dev + cloud: + config: + #设为true便会自动获取从配置中心获取配置文件 + enabled: true +eureka: + instance: + prefer-ip-address: true + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ +``` + +  最后在启动类加入注解`@EnableDiscoveryClient`,启动即可在 eureka 管理页面发现本服务实例。 + +### c、创建 organizationservice + +  首先在文件夹**file:///D:/configFolder/organizationservice**下创建两个配置文件:organizationservice.yml,organizationservice-dev.yml,内容分别为: + +```yaml +#organizationservice-dev.yml +server: + port: 10012 +``` + +```yaml +#organizationservice.yml +spring: + application: + name: organizationservice +``` + +  主要 POM 配置如下: + +```xml + + org.springframework.cloud + spring-cloud-starter-eureka + + + org.springframework.cloud + spring-cloud-config-client + +``` + +  然后修改配置文件,bootstrap.yml + +```yaml +spring: + application: + #指定名称,以便spring cloud config客户端知道查找哪个配置 + name: organizationservice + profiles: + #指定环境 + active: dev + cloud: + config: + enabled: true +eureka: + instance: + prefer-ip-address: true + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ +``` + +  最后在启动类加入注解`@EnableDiscoveryClient`,启动。 + +## 3、使用服务发现来查找服务 + +  现在已经有两个注册服务了,现在来让许可证服务调用组织服务,获取组织信息。首先在 organizationservice 服务中的 controller 包中加入一个 controller 类,让它能够响应请求: + +```java +//OrganizationController.java +@RestController +public class OrganizationController { + + @GetMapping(value = "/organization/{orgId}") + public Object getOrganizationInfo(@PathVariable("orgId") String orgId) { + Map data = new HashMap<>(2); + data.put("id", orgId); + data.put("name", orgId + "公司"); + return data; + } +} +``` + +  接下来让许可证服务通过 Eureka 来找到组织服务的实际位置,然后调用该接口。为了达成目的,我们将要学习使用 3 个不同的 Spring/Netflix 客户端库,服务消费者可以使用它们来和 Ribbon 进行交互。从最低级别到最高级别,这些库包含了不同的与 Ribbon 进行交互的抽象封装层次: + +- Spring DiscoveryClient +- 启用了 RestTemplate 的 Spring DiscoveryClient +- Neflix Feign 客户端 + +### a、使用 Spring DiscoveryClient + +  该工具提供了对 Ribbon 和 Ribbon 中缓存的注册服务最低层次的访问,可以查询通过 Eureka 注册的所有服务以及这些服务对应的 URL。 + +  首先在 licensingservice 的启动类中加入`@EnableDiscoveryClient`注解来启用 DiscoveryClient 和 Ribbon 库。 + +  然后在 service 包下创建 OrganizationService.java + +```java +@Service +public class OrganizationService { + + private static final String SERVICE_NAME = "organizationservice"; + private DiscoveryClient discoveryClient; + + @Autowired + public OrganizationService(DiscoveryClient discoveryClient) { + this.discoveryClient = discoveryClient; + } + + /** + * 使用Spring DiscoveryClient查询 + * + * @param id + * @return + */ + public Organization getOrganization(String id) { + RestTemplate restTemplate = new RestTemplate(); + List instances = discoveryClient.getInstances(SERVICE_NAME); + if (instances.size() == 0) { + throw new RuntimeException("无可用的服务"); + } + String serviceUri = String.format("%s/organization/%s", instances.get(0).getUri().toString(), id); + ResponseEntity responseEntity = restTemplate.exchange(serviceUri, HttpMethod.GET + , null, Organization.class, id); + return responseEntity.getBody(); + } +} +``` + +  接着在 controller 包中新建 LicensingController.java + +```java +@RestController +public class LicensingController { + + private OrganizationService organizationService; + + @Autowired + public LicensingController(OrganizationService organizationService) { + this.organizationService = organizationService; + } + + @GetMapping("/licensing/{orgId}") + public Licensing getLicensing(@PathVariable("orgId") String orgId) { + Licensing licensing = new Licensing(); + licensing.setValid(false); + licensing.setOrganization(organizationService.getOrganization(orgId)); + return licensing; + } +} +``` + +  启动所有项目,访问[localhost:10011/licensing/12](localhost:10011/licensing/12),可以看到返回如下结果: + +```json +{ + "organization": { + "id": "12", + "name": "12公司" + }, + "valid": false +} +``` + +  在实际开发中,基本上是用不到这个的,除非是为了查询 Ribbon 以获取某个服务的所有实例信息,才会直接使用。如果直接使用它存在以下两个问题: + +1. 没有利用 Ribbon 的客户端负载均衡 +2. 和业务无关的代码写得太多 + +### b、使用带 Ribbon 功能的 Spring RestTemplate 调用服务 + +  这种方法是较为常用的微服务通信机制之一。要启动该功能,需要使用 Spring Cloud 注解@LoadBanced 来定义 RestTemplate bean 的构造方法。方便起见直接在启动类中定义 bean: + +```java +#LicensingserviceApplication.java +@SpringBootApplication +@EnableDiscoveryClient //使用不带Ribbon功能的Spring RestTemplate,其他情况下可删除 +public class LicensingserviceApplication { + + /** + * 使用带有Ribbon 功能的Spring RestTemplate,其他情况可删除 + */ + @LoadBalanced + @Bean + public RestTemplate getRestTemplate(){ + return new RestTemplate(); + } + + public static void main(String[] args) { + SpringApplication.run(LicensingserviceApplication.class, args); + } +} +``` + +  接着 service 包下增加一个类:OrganizationByRibbonService.java + +```java +@Component +public class OrganizationByRibbonService { + + private RestTemplate restTemplate; + + @Autowired + public OrganizationByRibbonService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public Organization getOrganizationWithRibbon(String id) { + ResponseEntity responseEntity = restTemplate.exchange("http://organizationservice/organization/{id}", + HttpMethod.GET, null, Organization.class, id); + return responseEntity.getBody(); + } +} +``` + +  最后就是在 LicensingController.js 中加一个访问路径: + +```java +//不要忘记注入OrganizationByRibbonService服务 +@GetMapping("/licensingByRibbon/{orgId}") + public Licensing getLicensingByRibbon(@PathVariable("orgId") String orgId) { + Licensing licensing = new Licensing(); + licensing.setValid(false); + licensing.setOrganization(organizationService.getOrganization(orgId)); + return licensing; + } +} +``` + +  访问[localhost:10011/licensingByRibbon/113](localhost:10011/licensingByRibbon/113),即可看到结果。 + +### c、使用 Netflix Feign 客户端调用 + +  Feign 客户端是 Spring 启用 Ribbon 的 RestTemplate 类的替代方案。开发人员只需定义一个接口,然后使用 Spring 注解来标注接口,即可调用目标服务。除了编写接口定义无需编写其他辅助代码。 + +  首先启动类上加一个`@EnableFeignClients`注解启用 feign 客户端。然后在 POM 中加入 Feign 的依赖 + +```xml + + org.springframework.cloud + spring-cloud-starter-feign + +``` + +  然后在 client 包下新建 OrganizationFeignClient.java + +```java +@FeignClient("organizationservice")//使用FeignClient注解指定目标服务 +public interface OrganizationFeignClient { + + /** + * 获取组织信息 + * + * @param orgId 组织id + * @return Organization + */ + @RequestMapping(method = RequestMethod.GET, value = "/organization/{orgId}", consumes = "application/json") + Organization getOrganization(@PathVariable("orgId") String orgId); +} +``` + +  最后修改`LicensingController.java`,加入一个路由调用 Feign。 + +```java +//注入OrganizationFeignClient,使用构造注入 + +@GetMapping("/licensingByFeign/{orgId}") +public Licensing getLicensingByFeign(@PathVariable("orgId") String orgId) { + Licensing licensing = new Licensing(); + licensing.setValid(false); + licensing.setOrganization(organizationFeignClient.getOrganization(orgId)); + return licensing; +} +``` + +访问[localhost:10011/licensingByFeign/11313](localhost:10011/licensingByFeign/11313),即可看到结果。 + +# 总结 + +  这一节磨磨蹭蹭写了好几天,虽然例子很简单,但是相信应该是能够看懂的。由于篇幅原因代码没有全部贴上,想要查看完整代码,可以访问这个链接:[记得补全啊]()。 diff --git a/java/springcloud实战/file/服务发现架构.xml b/java/springcloud实战/file/服务发现架构.xml new file mode 100644 index 0000000..08479f0 --- /dev/null +++ b/java/springcloud实战/file/服务发现架构.xml @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/java/springcloud实战/file/配置管理概念架构.xml b/java/springcloud实战/file/配置管理概念架构.xml new file mode 100644 index 0000000..40ebee5 --- /dev/null +++ b/java/springcloud实战/file/配置管理概念架构.xml @@ -0,0 +1 @@ +3ZlNc5s8EIB/jY7t8P1xBBu3h3SmMzn0fY+ykW1ajBghx3F/fVdiscHglMQumUkOGWm1+mD3YXeFiT3bPX8RtNx+4ynLiWWkz8SeE8sKHQ/+K8GxFnheUAs2IktrkXkWPGa/GQoNlO6zlFUdRcl5LrOyK1zxomAr2ZFRIfihq7bmeXfXkm5YT/C4onlf+iNL5RalpmGcB76ybLPFrQMXB5Z09Wsj+L7A/Yhlr/VfPbyjzVqoX21pyg8tkZ0QeyY4l3Vr9zxjuTJtY7Z63uLK6OncghVyzAR0SyWPzaPrwzM1bBA73spdDk0TmrCiOP6Hct35X3U+29D9yaQ8ohvpXnIQcSG3fMMLmj9wXuIa9W4s7Vn/fF4UVXwvVqhlIQFUbBhqOSdbAYKM7xgcB1QEy6nMnrqrU4Rhc9I7GwQaaJNh++DWTzTf46IkcUmckCghiUfCGQnmShJEJDJVA+QhDDlKJ4h71gV/l6q54ruSF/qhY5pnmwJkOVurblXSVVZsHnRvbnsnsz0xIdnzy4brm6SZ4CFx+EaaDvYPLb4bKrcttJt5t1gx7NlBIfCI3QIMAQ95FbyxcF0D1H8zePb9wcOp33kG+56841hd7/SsXh8BZ10Y/nSMUb6wPyrRlj0h0abZM8RrkH6/WOr2kfYmiqXuRyXPNqckz+ib8dIyqgIprz4oFk102agbrzWAY18kk8AdZwDTvYMFnAGOQgJHUPj4JJ5roHz1X3Hkk9AggTeA2JRAGcMGnYIY7wPZC4ELp3zh7JtC/b8N56Z1Y/AerkdcZ7J6xHo5k7J8yQ/JWTAmp352O1n1NSWk2g3ais4MroM3+qWbZ+/iqkgIemwplMoF1XhPNlfYxTX9i0rUNy78Wx/gzd4eulD1gxF0FyQMdAyKSezoBox6WtkgYaTj1IJE7qjSIQlIAAEuUg1QCGZaOSCxr6d7JDZ6FEJ4kl3CBKuy35g2FVVoe9B2Y+LOW9FvBWwwAYIGpAgHdlmaaoxzumR5fPpaMOM5F3rf5nvBC4FyTAL/Swb/BC8JWKTt6QC/Bbw1dOAyn+zuDL5eV+zWIGGOw2aQhLkGCSQWiXQjjkjYj+nvVk36xjveY6w+92+7x3Rj7gSJz+kH2JuT4eiSYKgE9VTECRxNWKwgU4DamjkNaGy1iiwYMocobNn2Dmh55sVFxeujZRsDaN2jTDdv++rzjmgN3JHNqT44mlduyTOV9jAvmjqxRZoxV3XD/mfcJpztd3m0krydix5U6vnOq0xmXEW3JZeS78blpMt8JpWt4/tS6ztuh1onGKL2H8XD4c9j15JI7ZBooR0S62oEyhIHddQ9ytEvvY9ZCcsS6Caq5EgW2rEzFTxgkVAHj8hTu9RlCaToemVV+bSUQ1dP99Sydb2E5znphPoYdSllKOHgrIvn+nsJVEnBf7GGDHyH11meX4jGF0KHbSbZI2RWtedB0LIXFO6AVGB1kToFuHaO9QeYsl7PFHTPv9/Utc75NzI7+QM= \ No newline at end of file diff --git a/java/springcloud实战/picFolder/客户端负载均衡模型.png b/java/springcloud实战/picFolder/客户端负载均衡模型.png new file mode 100644 index 0000000..ee78f6e Binary files /dev/null and b/java/springcloud实战/picFolder/客户端负载均衡模型.png differ diff --git a/java/springcloud实战/picFolder/服务发现架构.png b/java/springcloud实战/picFolder/服务发现架构.png new file mode 100644 index 0000000..532e386 Binary files /dev/null and b/java/springcloud实战/picFolder/服务发现架构.png differ diff --git a/java/springcloud实战/picFolder/配置管理概念架构.png b/java/springcloud实战/picFolder/配置管理概念架构.png new file mode 100644 index 0000000..18737c3 Binary files /dev/null and b/java/springcloud实战/picFolder/配置管理概念架构.png differ diff --git a/java/其他/java导出EXCEL文件.md b/java/其他/java导出EXCEL文件.md new file mode 100644 index 0000000..44cb1a5 --- /dev/null +++ b/java/其他/java导出EXCEL文件.md @@ -0,0 +1,181 @@ +[id]:2018-09-22 +[type]:java +[tag]:java,reflect,excel,hssfworksheet + +## 一、背景 + +  最近在java上做了一个EXCEL的导出功能,写了一个通用类,在这里分享分享,该类支持多sheet,且无需手动进行复杂的类型转换,只需提供三个参数即可: + +- `fileName` + + excel文件名 + +- `HasMap> data` + + 具体的数据,每个List代表一张表的数据,?表示可为任意的自定义对象 + +- `LinkedHashMap 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 dataA = .....; +List dataB = .....; +``` + +我们将这两个导出到excel中,首先需要定义sheet: + +```java +String[][] sheetA = { + {"name","姓名"} + ,{"address","住址"} +} +String[][] sheetB = { + {"id","ID"} + ,{"sum","余额"} + ,{"cat","猫的名字"} +} +``` + +然后将数据汇总构造一个ExcelUtil: + +```java +String fileName = "测试Excel"; +HashMap> data = new HashMap<>(); +//ASheet为表名,后面headers里的key要跟这里一致 +data.put("ASheet",dataA); +data.put("BSheet",dataB); +LinkedHashMap 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 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地址:[点击跳转]() \ No newline at end of file