ramostear.comramostear.com 谭朝红的技术分享博客

格言 编程是一门技术,也是一门艺术 !

Spring Boot Actuator的使用

Spring Boot Actuator的使用

Spring Boot(十六)— Actuator

Spring Boot Actuator提供了一个安全的、用于监控和管理Spring Boot应用程序的端点。在默认情况下,所有的Actuator端点都被安全保护机制所保护起来的。在本章节中,我们将学习有关Spring Boot Actuator的基础知识。

启动Spring Boot Actuator

为了在Spring Boot应用程序中启用Actuator功能,我们需要在Maven build文件中添加Spring Boot Actuator Starter依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置Actuator

在application.properties文件中,我们需要进行如下的配置:

management.endpoint.env.enabled=true
management.endpoints.web.exposure.include=*
management.server.port=9000

说明:第一行配置是开启端点管理功能,第二行配置是禁用端点的安全检查,最后是为Actuator配置了一个独立的运行端口

如果采用的是YAML文件格式作为Spring Boot应用程序的配置文件,可以使用下面的方式进行配置:

management:
  endpoint:
      env:
        enable:true
  endpoints:
    web:
      exposure:
        include:*
  server:
    port:9000

注意:在Spring Boot 2.0的版本中,原来的management.security.enabled和management.port两个属性配置已经被废弃

启动应用程序

使用maven命令mvn clean install对项目进行打包,当控制台窗口提示“BUILD SUCCESS”信息后,你可以在当前工程目录下找到相应的JAR文件。

现在,你可以使用下面的命令来运行JAR文件:

java -jar JARFILE

下面是应用程序启动成功后控制台窗口的日志信息:

2019-03-12 00:34:58.880  INFO 14084 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2019-03-12 00:34:58.901  INFO 14084 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]

....

2019-03-12 00:34:59.710  INFO 14084 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 9000 (http)
2019-03-12 00:34:59.711  INFO 14084 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]

...

2019-03-12 00:34:59.757  INFO 14084 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 15 endpoint(s) beneath base path '/actuator'
2019-03-12 00:34:59.928  INFO 14084 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9000 (http) with context path ''
2019-03-12 00:34:59.959  INFO 14084 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''

通过日志信息可知,应用程序已经在Tomcat 8080端口上启动,Actuator以9000端口启动。

测试

打开Postman测试工具,在地址栏输入:http://localhost:9000/actuator 并发送请求:

可以看到,Spring Boot Actuator列举了所有的管理端点信息,接下来,将通过一个表格对Actuator中几个重要的端点加以说明:

EndPoint 说明
/metrics 查看应用程序指标,如内存使用率,线程信息、类以及系统运行时间等信息
/env 查看应用程序的环境变量
/beans 查看应用程序中的Bean及其类型、生命周期和依赖关系
/health 查看应用程序的健康状况
/info 查看有关Spring Boot应用程序的基本信息
/trace 查看Rest端点的Traces列表信息
Spring Boot应用中进行任务调度

Spring Boot应用中进行任务调度

Spring Boot(十五)— 任务调度

任务调度(也可以称为定时任务)是指在特定的时间段去执行一个规定的任务过程。Spring Boot为开发者提供了一个更优雅的方式创建任务调度程序。在本章节中,我们将学习使用Spring Boot来创建任务调度程序。

任务调度分为两种类型,一种是间隔时间执行的任务,如每隔3秒执行一次任务程序;另外一种时指定具体时间的任务,如在每天的凌晨整点备份数据库。

Cron表达式

在开始讲解定时任务之前,先来看一下定时任务中的Cron表达式的相关内容。Cron表达式用于配置CronTrigger实例,它是org.quartz.Trigger的子类。Cron表达式被放置在@Scheduled 注释标签中,下面的代码给出了一个cron表达式的样例:

@Scheduled(cron = "0/5 * 22 * * ?")
public void cronJobSchedule(){
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    Date now = new Date();
    logger.info("Java cron job expression scheduler::"+sdf.format(now));
}

在cron表达式中,一共有七位表达式参数,我们将使用一张表格来了解各个参数的用途:

位数 说明 范围
第一位 表示秒 取值范围:0-59
第二位 表示分钟 取值范围:0-59
第三位 表示小时 取值范围:0-23
第四位 表示日期 取值范围:1-31
第五位 表示月份 取值范围:1-12
第六位 表示星期 取值范围:1-7
第七位 表示年份,通常置空 取值范围:1970-2099

说明,在第六位星期参数中,1表示的是星期日,除使用数字表示外,还可以使用表示星期的英文缩写来设置

了解了cron表达式的语法规则后,我们再来了解一下表达式中各种占位符的含义。cron表达式中一共可以使用的占位符有5个,如下表所示:

占位符 说明 示例
(星号)* 可以理解为一个周期 每秒、没分、每小时等
(问好)? 只能出现在日期和星期两个位置中 表示时间不确定
(横线)- 表示一个时间范围 如在小时中10-11,表示从上午10点到上午11点
(逗号), 表示一个列表值 如在星期中使用:1,3,5 表示星期一、星期三和星期五
(斜杠)/ 表示一个开始时间和间隔时间周期 在分钟中使用:0/15 表示从0分开始,每15分钟运行一次

下面将列举一些示例来说明cron表达式和占位符:

表达式 说明
0 0 0 每天00:00:00执行任务
0 30 10 每天上午10:30:00执行任务
0 30 10 ? 每天上午10:30:00执行任务
0 0/15 10 ? 每天上午10:00:00、10:15:00、10:30:00和10:45:00这四个时间点执行任务
0 0 0 ? * 1 每个星期天的凌晨整点执行任务
0 0 0 ? * 1#3 每个月的第三个星期天的凌晨整点执行任务

开启定时任务

在Spring Boot应用程序中,需要使用@EnableScheduling注解来开启对定时任务的支持,并且该注解应该用于应用程序主类上,下面的代码演示了在Spring Boot中开启定时任务支持。

package com.ramostear.application;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class, args);
    }

}

固定时间任务

下面我们将使用cron表达式创建一个固定时间的调度任务来实现每天的22点到23点每隔15秒钟执行一次任务。

package com.ramostear.application.schedule;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author ramostear
 * @create-time 2019/3/11 0011-21:49
 * @modify by :
 * @since:
 */
@Component
public class DemoScheduler {

    private static final Logger logger = LoggerFactory.getLogger(DemoScheduler.class);

    @Scheduled(cron = "0/15 * 22-23 * * ?")
    public void cronJobSchedule(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        Date now = new Date();
        logger.info("Java cron job expression scheduler::"+sdf.format(now));
    }
}

下面的文本是应用程序在23:09分启动后,控制台窗口输出的日志信息:

[2019-03-11T23:09:00.000z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][26][INFO ] 
Java cron job expression scheduler::2019-03-11 23:09:00.001
[2019-03-11T23:09:15.015z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][26][INFO ] 
Java cron job expression scheduler::2019-03-11 23:09:15.002
[2019-03-11T23:09:30.030z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][26][INFO ] 
Java cron job expression scheduler::2019-03-11 23:09:30.001
[2019-03-11T23:09:45.045z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][26][INFO ] 
Java cron job expression scheduler::2019-03-11 23:09:45.002
[2019-03-11T23:10:00.000z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][26][INFO ] 
Java cron job expression scheduler::2019-03-11 23:10:00.002

固定周期任务

固定时间周期调度任务是指从应用程序启动开始,在固定的时间周期执行一次任务,如每1000毫秒执行一次、每1分钟执行一次。下面的代码给出了创建固定周期调度任务的示例:

@Scheduled(fixedRate = 1000)
public void fiexdRateSchedule(){
    //TODO ...
}

通过配置fixedRate参数的值,实现从应用程序启动后,每隔5秒钟执行一次任务。

package com.ramostear.application.schedule;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author ramostear
 * @create-time 2019/3/11 0011-21:49
 * @modify by :
 * @since:
 */
@Component
public class DemoScheduler {

    private static final Logger logger = LoggerFactory.getLogger(DemoScheduler.class);

    @Scheduled(fixedRate=5000)
    public void fixedRateSchedule(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        Date now = new Date();
        logger.info("Java fixedRate scheduler::"+sdf.format(now));
    }
}

下面的文本是在23:18分启动应用程序后,控制台窗口输出的日志信息。

[2019-03-11T23:18:22.022z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][33][INFO ] 
Java fixedRate scheduler::2019-03-11 23:18:22.062
[2019-03-11T23:18:27.027z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][33][INFO ] 
Java fixedRate scheduler::2019-03-11 23:18:27.062
[2019-03-11T23:18:32.032z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][33][INFO ] 
Java fixedRate scheduler::2019-03-11 23:18:32.062
[2019-03-11T23:18:37.037z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][33][INFO ] 
Java fixedRate scheduler::2019-03-11 23:18:37.063
[2019-03-11T23:18:42.042z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][33][INFO ] 
Java fixedRate scheduler::2019-03-11 23:18:42.062

时延调度

所谓的时延调度,是通过设置initialDelayfixedDelay属性的值来控制当应用程序启动成功后,推迟一定时间再执行调度任务,其使用的语法如下:

@Scheduled(fixedDelay=1000,initialDelay=1000)
public fixedDelaySchedule(){

}

现在,我们将实现一个特定的定时任务需求:在应用程序启动成功后延迟10秒钟,再以每5秒钟的频率执行任务:

package com.ramostear.application.schedule;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author ramostear
 * @create-time 2019/3/11 0011-21:49
 * @modify by :
 * @since:
 */
@Component
public class DemoScheduler {

    private static final Logger logger = LoggerFactory.getLogger(DemoScheduler.class);

    @Scheduled(fixedDelay = 5000,initialDelay = 10000)
    public void fixedRateAndInitialDelaySchedule(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        Date now = new Date();
        logger.info("Java fixedRate and initialDelay scheduler::"+sdf.format(now));
    }
}

下面的文本显示了在23:30:59启动应用程序,控制台窗口输出的日志信息:

[2019-03-11T23:31:01.001z][org.springframework.boot.StartupInfoLogger][main][59][INFO ] Started ScheduleApplication in 2.193 seconds (JVM running for 2.904)
[2019-03-11T23:31:11.011z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][40][INFO ] Java fixedRate and initialDelay scheduler::2019-03-11 23:31:11.639
[2019-03-11T23:31:16.016z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][40][INFO ] Java fixedRate and initialDelay scheduler::2019-03-11 23:31:16.640
[2019-03-11T23:31:21.021z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][40][INFO ] Java fixedRate and initialDelay scheduler::2019-03-11 23:31:21.641
[2019-03-11T23:31:26.026z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][40][INFO ] Java fixedRate and initialDelay scheduler::2019-03-11 23:31:26.643
[2019-03-11T23:31:31.031z][com.ramostear.application.schedule.DemoScheduler][scheduling-1][40][INFO ] Java fixedRate and initialDelay scheduler::2019-03-11 23:31:31.644

从日志信息中可以看到,应用程序在23:31:01启动成功。等待十秒后,在23:31:11输出了第一条日志信息,往后每隔5秒输出一条日志信息。

注意:上面所设置的参数都是以毫秒为单位

本章节的课程源码已经上传至Github代码仓库,你可以访问下面的链接获取课程源码:https://github.com/ramostear/spring-boot-schedule-tutorial

在Spring Boot中提供国际化支持

在Spring Boot中提供国际化支持

Spring Boot(十四)— 国际化支持

国际化是一个可以让应用程序适应不同语言和区域显示而无需修改工程代码的一种软件本地化自适应能力。在本章节中,将使用Spring Boot与Thymeleaf来演示国际化的支持,系统会自动根据当前的语言环境或者Session中的语言来读取对应的语言模板。

依赖

需要将Spring Boot Web Starter和Spring Boot Thymeleaf Starter依赖加入到工程中。下面是Maven的build文件内容:

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ramostear</groupId>
    <artifactId>i18n</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>i18n</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </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>

配置LocaleResolver

需要为应用程序配置一个默认的本地化语言环境的解析器。在Spring Boot应用程序中,可以通过下面的方式配置一个默认的 LocaleResolver:

@Bean
    public LocaleResolver localeResolver(){
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        resolver.setDefaultLocale(Locale.US);
        return resolver;
    }

配置拦截器

需要使用LocaleChangeInterceptor拦截语言参数的变化,并设置语言参数的名称。

@Bean
    public LocaleChangeInterceptor localeChangeInterceptor(){
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        return interceptor;
    }

注册拦截器

为了让LocaleChangeInterceptor生效,需要将拦截器注册到Spring Boot的拦截器注册表中。

@Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(localeChangeInterceptor());
    }

国际化消息资源

在默认的情况下,Spring Boot 应用程序从类路径src/main/resources目录中获取国际化消息资源。缺省语言资源文件名为message.properties,其他不同语言的资源文件命名模式为:message_xx.properties.xx代表区域代码。

我们提供两种语言的显示,一种时默认的英语消息,一种是中文消息。英语的资源文件配置在index.properties中进行设置。

index.msg = welcome to spring boot i18n tutorial.

中文资源在index_zh_CN.properties中进行设置。

index.msg = 欢迎阅读Spring Boot 国际化教程.

视图模板

在模板页面中,使用语法#{key}的方式获取国际化资源文件设置的值

<h1 th:text="#{index.msg}"></h1>

控制器

这里我们创建一个简单的控制器,返回视图路径即可:

package com.ramostear.application.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @author ramostear
 * @create-time 2019/3/11 0011-18:12
 * @modify by :
 * @since:
 */
@Controller
public class I18NController {

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

运行测试

启动应用程序,打开浏览器并在地址栏输入:http://localhost:8080/index

此时,没有给任何的语言参数,Spring Boot应用程序默认使用US资源文件。

接下来,修改请求地址为:http://localhost:8080/index?lang=zh_CN :

现在,已经成功将界面从英文状态切换到中文状态进行显示。

代码清单

本次教程的源代码已经上传到github仓库,点击下面的链接可以获得所有的源码:https://github.com/ramostear/spring-boot-i18n-tutorial

如何在Spring Boot中解决跨域问题?

如何在Spring Boot中解决跨域问题?

Spring Boot(十三)— CORS跨域支持

CORS,全称Cross-Origin Resource Sharing,是一种允许当前域(domain)的资源(比如html/js/web service)被其他域(domain)的脚本请求访问的机制,通常由于同域安全策略(the same-origin security policy)浏览器会禁止这种跨域请求。它会在你进行如下的请求时被触发:

在本章节的内容中,我将以一个简单的案例来讲解在Spring Boot中如何处理跨域请求资源。

1. 局部跨域

在Spring Boot应用程序中,你可以使用@CrossOrign注解来开启单个控制器支持跨域请求操作。需要注意@CrossOrign的作用范围只在被注释的Rest API(局部支持跨域)。下面的代码给出了一个简单的演示:

@GetMapping("/users")
@CrossOrign(origins="http://localhost:8080")
public ResponseEntity<Object> users(){
    return null;
}

2. 全局跨域

Spring Boot提供了显示Bean装配的方式配置允许全局跨域操作。你可以参照下面给出的代码配置允许全局跨域请求:

@Bean
    public WebMvcConfigurationSupport crossConfig(){
        return new WebMvcConfigurationSupport(){
            @Override
            public void addCorsMappings(CorsRegistry registry){
                registry.addMapping("/users").allowedOrigins("http://localhost:8080");
            }
        };
    }

3. 跨域调用

现在,创建一个控制调用第六章—RESTful Web服务,获取用户的信息。在这里,我们使用RestTemplate调用User Restful Web服务,如何使用RestTemplate,请阅读第十章 — RestTemplate相关内容。下面的代码显示了使用RestTemplate进行跨域调用Restful Web服务。

package com.ramostear.application.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;

/**
 * @author ramostear
 * @create-time 2019/3/11 0011-7:14
 * @modify by :
 * @since:
 */
@RestController
public class UserController {
    private static final String ROOT_URI = "http://localhost:8080/users";

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/users")
    public String getUsers(){
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        HttpEntity<String> entity = new HttpEntity<>(headers);
        return restTemplate.exchange(ROOT_URI, HttpMethod.GET,entity,String.class).getBody();
    }

}

除此之外,需要修改application.propertiesserver.port属性的值为8090(或者其他与User Restful Web服务不冲突的端口号):

server.port= 8090

现在,分别启动两个应用程序,并测试跨域调用是否成功。启动Postman测试应用程序,在地址栏输入:http://localhost:8090/users, 发送请求,并观察页面变化:

在Spring Boot中定义服务组件

在Spring Boot中定义服务组件

Spring Boot(十二)— 服务组件

所谓的服务组件(Service Component)— 就是用于处理系统业务逻辑的类,如果按照系统分层设计理论来划分,服务组件是位于业务层当中的类。在Spring Boot中,服务组件是一个被@Service注解进行注释的类,这些类用于编写系统的业务代码。在本章节中,将讲解如何创建并使用服务组件。

在开始正文之前,先来看两段示例代码。使用服务组件之前,我们需要定义服务组件接口类,用于索引服务组件提供的服务,代码如下所示:

public interface UserService{
    // TODO ...
}

然后,需要使用@Service注解对服务组件接口实现类进行注释,演示代码如下:

@Service(value="userService")
public class UserServiceImpl implements UserService{
    //TODO ...
}

最后,使用@Autowired注解来自动引用服务组件,代码如下:

@Controller
public class DemoController{
    @Autowired
    UserService userService;
    //TODO ...
}

在本次讲解中,我们依然以对用户的增、删、改、查为案例,将控制器中的业务方法迁移到服务组件中。

1. 创建服务接口

创建一个包含添加用户、更新用户、删除用户和查询用户的服务接口类 — 用户服务组件接口类。详细代码如下:

package com.ramostear.application.service;

import com.ramostear.application.model.User;

import java.util.Collection;

/**
 * Created by ramostear on 2019/3/11 0011.
 */
public interface UserService {

    /**
     * create user
     * @param user
     */
    void create(User user);

    /**
     * update user info by ID
     * @param id
     * @param user
     */
    void update(long id,User user);

    /**
     * delete user by ID
     * @param id
     */
    void delete(long id);

    /**
     * query all user
     * @return
     */
    Collection<User> findAll();
}

2. 实现服务接口

创建一个接口实现类,用于实现其中的增、删、改、查四个业务方法,并用@Service注解进行标注,具体代码如下:

package com.ramostear.application.service.impl;

import com.ramostear.application.model.User;
import com.ramostear.application.service.UserService;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * @author ramostear
 * @create-time 2019/3/11 0011-4:29
 * @modify by :
 * @since:
 */
@Service(value="userService")
public class UserServiceImpl implements UserService {

    private static Map<Long,User> userRepo = new HashMap<>();

    @PostConstruct
    public void initUserRepo(){
        User admin = new User();
        admin.setId(1).setName("admin");
        userRepo.put(admin.getId(),admin);

        User editor = new User();
        editor.setId(2).setName("editor");
        userRepo.put(editor.getId(),editor);
    }
    @Override
    public void create(User user) {
        userRepo.put(user.getId(),user);
    }

    @Override
    public void update(long id, User user) {
        userRepo.remove(id);
        user.setId(id);
        userRepo.put(id,user);
    }

    @Override
    public void delete(long id) {
        userRepo.remove(id);
    }

    @Override
    public Collection<User> findAll() {
        return userRepo.values();
    }
}

3. 使用服务组件

接下来,定义一个用户控制器,使用@Autowired注解来应用用户服务组件,实现对用户的增、删、改、查功能:

package com.ramostear.application.controller;

import com.ramostear.application.model.User;
import com.ramostear.application.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

/**
 * @author ramostear
 * @create-time 2019/3/11 0011-4:42
 * @modify by :
 * @since:
 */
@RestController
public class UserController {

    @Autowired
    UserService userService;


    @GetMapping("/users")
    public ResponseEntity<Object> users(){
        return new ResponseEntity<>(userService.findAll(), HttpStatus.OK);
    }

    @PostMapping("/users")
    public ResponseEntity<Object> create(@RequestBody User user){
       userService.create(user);
       return new ResponseEntity<>("User is created successfully.",HttpStatus.CREATED);
    }

    @PutMapping("/users/{id}")
    public ResponseEntity<Object> update(@PathVariable(name="id") long id,@RequestBody User user){
        userService.update(id,user);
        return new ResponseEntity<>("User is updated successfully.",HttpStatus.OK);
    }

    @DeleteMapping("/users/{id}")
    public ResponseEntity<Object> delete(@PathVariable(name = "id")long id){
        userService.delete(id);
        return new ResponseEntity<>("User is deleted successfully.",HttpStatus.OK);
    }
}

4. 数据模型

用户对象的代码沿用以往章节的User.java代码:

package com.ramostear.application.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * @author ramostear
 * @create-time 2019/3/6 0006-3:12
 * @modify by :
 * @since:
 */
@Getter
@Setter
@NoArgsConstructor
public class User {
    private long id;
    private String name;

    public User setId(long id){
        this.id = id;
        return this;
    }

    public User setName(String name){
        this.name = name;
        return this;
    }
}

注:应用程序主类和Maven build文件与之前章节的代码形同,不再列举。

5. 运行测试

启动Spring Boot应用程序,然后打开Postman测试应用程序,分别进行如下的测试。

GET 请求:获取所有的用户信息。

URL地址:http://localhost:8080/users

获取用户信息

POST 请求:新增一位用户信息

URL地址:http://localhost:8080/users

请求参数:{“id”:3,”name”:”reader”}

新增用户

PUT请求:修改用户信息

URL地址:http://localhost:8080/users/3

请求参数:{“id”:3,”name”:”ramostear”}

修改用户

DELETE请求:删除用户信息

URL地址:http://localhost:8080/users/3

删除用户

6. 附件

本章节用于演示的项目源码已经上传到Github代码仓库,你可以通过下面的地址链接免费获取本章节的全部源码信息:https://github.com/ramostear/Spring-Boot-Service-Component

设计模式-适配器模式

设计模式-适配器模式

设计模式-适配器模式(Adapter)

1.设计意图

适配器模式是将一个类的接口转换成另一个类想要的接口,适配器让原本两个不兼容的类能够兼容。
适配器模式类图

2. 演示案例

现实生活中我们到处都可以看到适配器

  • 1.如果你需要将SD卡中的数据传输到电脑上,为了将数据传输到电脑上,你可能需要一个与计算机USB接口兼容的读卡器,以便将SD卡连接到计算机。此时的读卡器就是一个适配器。
  • 2.如果你需要给手机充电,Type-c接口和普通充电接口无法对接,此时你需要一个充电转换器或者电源适配器,以便能够对接充电,这里的转换器就是一个适配器。
  • 3.同声传译人员将英文话翻译成中文话,翻译人员就是一个“适配器”

简而言之

适配器模式允许你在适配器中包装一个原本不兼容的对象,使其与另外一个类兼容

维基百科:

在软件工程中,适配器模式是一种软件设计模式,它允许现有类的接口作用于另外一个接口。它通常用于在不修改源代码的前提下使现有的类能够与其他类一起工作。

3. 程序示例

我们就以SD卡通过读卡器将数据传输到电脑中为案例来讲解适配器模式。
首先我们创建一个存储卡类和一个数据传输接口。存储卡类中有一个现实内存大小的方法:memory();数据传输接口中定义两个方法:read()和write();
SDCard.java

package com.ramostear.pattern.adapter;
/**
 * @author ramostear
 * @create-time 2019/1/4 0004-19:41
 * @modify by :
 * @info:[存储卡类]
 * @since:
 */

public class SDCard {

   public void memory(){
       System.out.println("8GB");
   }

}

DataTransfer.java

package com.ramostear.pattern.adapter;

/**
 * @author ramostear
 * @create-time 2019/1/4 0004-19:54
 * @modify by :
 * @info:[数据传输接口]
 * @since:
 */
public interface DataTransfer {

    void read();

    void write();
}

接下来我们将定义一个读卡器类,并将SD卡 “插入” 读卡器中,且实现数据读写接口
CardReader.java

package com.ramostear.pattern.adapter;

/**
 * @author ramostear
 * @create-time 2019/1/4 0004-19:56
 * @modify by :
 * @info:[读卡器]
 * @since:
 */
public class CardReader implements  DataTransfer{

    private SDCard sdCard;


    public CardReader() {
        this.sdCard = new SDCard();
    }

    @Override
    public void read() {
        sdCard.memory();
        System.out.println("read data from sdCard...");
    }

    @Override
    public void write() {
        sdCard.memory();
        System.out.println("write data to sdCard...");
    }
}

然后我们定义一个计算机类,预留“USB”的接口,并设置数据读写入口
Computer.java

package com.ramostear.pattern.adapter;
/**
 * @author ramostear
 * @create-time 2019/1/4 0004-20:01
 * @modify by :
 * @info:[计算机类]
 * @since:
 */
public class Computer {

    private DataTransfer dataTransfer;

    public Computer(){}

    public Computer(DataTransfer dataTransfer){
        this.dataTransfer = dataTransfer;
    }

    public DataTransfer getDataTransfer() {
        return dataTransfer;
    }

    public void setDataTransfer(DataTransfer dataTransfer) {
        this.dataTransfer = dataTransfer;
    }

    public void read(){
        dataTransfer.read();
    }

    public void write(){
        dataTransfer.write();
    }
}

最后,让我们将装入SD卡的读卡器插入到计算机的USB口中,启动计算机并对SD卡中的数据进行读写
App.java

package com.ramostear.pattern.adapter;
/**
 * @author ramostear
 * @create-time 2019/1/4 0004-20:04
 * @modify by :
 * @info:[测试类]
 * @since:
 */
public class App {

    public static void main(String[] args){
        /**
         * 1. 将SD卡插入读卡器
         * 2. 将读卡器插入电脑USB接口
         * 3. 电脑开机
         * 4. 从SD卡中读入数据到电脑,或者将输入从电脑中写入SD卡
         */
        Computer computer = new Computer(new CardReader());
        computer.read();
        computer.write();
    }

}

控制台输出:
控制台输出
至此,适配器模式设计实现完成。

4. 适用场景

当满足(不限于)以下应用场景时:

  • 你想复用现有的类,但它所提供的接口与你期望的接口不匹配时
  • 你希望创建一个可重用的类,并让该类与其他不可预见的类(不一定具有兼容接口的类)协作时
  • 大多数使用第三方库的应用程序使用适配器作为应用程序和第三方库之间的中间层,以将应用程序与库分离。如果必须使用另一个库,则只需要新库的适配器,而不必更改应用程序代码
  • 你需要使用几个现有的子类,但通过对每个子类进行子类化来调整它们的接口是不切实际的

5. 使用建议

5.1 类适配器

由于类适配器是适配者类的子类,因此可以在适配器类中调整适配者类中的方法,使得适配器的灵活性更强。但是如果在Java这样的单继承语言中,一次最多只能适配一个适配者类,且目标抽象类只能为接口,不能为类。

5.2 对象适配器

把多个不同的适配者适配到同一个目标类,同一个适配器可以把适配者和它的子类都适配到一个目标接口。

6. 实际案例

设计模式-抽象工厂模式

设计模式-抽象工厂模式

设计模式三十六计之抽象工厂模式(Abstract Factory)

1.设计意图

提供一个接口,用于创建相关或者从属对象的族,而不是指定他们的具体类。以下以生产计算机为例给出UML类图:
abstract-factory-pattern-uml

2.演示案例

假设我们要生产一台计算机(广义的),一台计算机有一些共同的物件。小型计算机(以手机为例)需要有触控屏、微处理器和小型化的内存条。大型计算机(以PC机为例)需要有显示屏、多核处理器和内存条。计算机的各个部件存在着相互依赖关系。

简而言之

抽象工厂即工厂的工厂,它将单独但相关/依赖的工厂分组在一起而不是指定具体类别的工厂。

维基百科:

抽象工厂模式提供了一种方法来封装一组具有共同主题的单个工厂,而不指定它们的具体类。

3.代码示例

以上述的生产计算机为案例,首选我们需要定义一些部件接口并实现这些部件接口

Memory.java

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:23
 * @modify by :
 * @info:[内存接口类]
 * @since:
 */
public interface Memory {

    String getDescription();
}

Screen.java

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:22
 * @modify by :
 * @info:[屏幕接口类]
 * @since:
 */
public interface Screen {

    String getDescription();
}

Processor.java

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:24
 * @modify by :
 * @info:[处理器接口类]
 * @since:
 */
public interface Processor {

    String getDescription();
}

PhoneMemory.java

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:28
 * @modify by :
 * @info:[手机内存类]
 * @since:
 */
public class PhoneMemory implements Memory{

    static final String DESCRIPTION = "This is phone memory";

    @Override
    public String getDescription() {
        return DESCRIPTION;
    }
}

PhoneScreen.java

package com.ramostear.pattern.abstractfactory;
/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:26
 * @modify by :
 * @info:[手机屏幕类]
 * @since:
 */
public class PhoneScreen implements Screen{

    static final String DESCRIPTION = "This is phone screen";

    @Override
    public String getDescription() {
        return DESCRIPTION;
    }
}

PhoneProcessor.java

package com.ramostear.pattern.abstractfactory;
/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:29
 * @modify by :
 * @info:[手机处理器类]
 * @since:
 */
public class PhoneProcessor implements Processor{

    static final String DESCRIPTION = "This is phone processor";

    @Override
    public String getDescription() {
        return DESCRIPTION;
    }
}

ComputerMomory.java

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:38
 * @modify by :
 * @info:[电脑内存条]
 * @since:
 */
public class ComputerMemory implements Memory{

    static final String DESCRIPTION = "This is computer memory";

    @Override
    public String getDescription() {
        return DESCRIPTION;
    }
}

ComputerScreen.java

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:36
 * @modify by :
 * @info:[电脑屏幕]
 * @since:
 */
public class ComputerScreen implements Screen{

    static final String DESCRIPTION = "This is computer screen";

    @Override
    public String getDescription() {
        return DESCRIPTION;
    }
}

ComputerProcessor.java

package com.ramostear.pattern.abstractfactory;
/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:39
 * @modify by :
 * @info:[电脑处理器]
 * @since:
 */
public class ComputerProcessor implements Processor{

    static final String DESCRIPTION = "This is computer processor";

    @Override
    public String getDescription() {
        return DESCRIPTION;
    }
}

然后,我们定义一个抽象的电子产品生产工厂类并创建两个它的实现类:
ElectronicFactory.java

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:32
 * @modify by :
 * @info:[电子设备生产工厂接口类]
 * @since:
 */
public interface ElectronicFactory {
    /**
     * 生产屏幕
     * @return
     */
    Screen produceScreen();

    /**
     * 生产内存条
     * @return
     */
    Memory produceMemory();

    /**
     * 生产处理器
     * @return
     */
    Processor produceProcessor();

}

ComputerFactory.java

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:40
 * @modify by :
 * @info:[电脑生产工厂]
 * @since:
 */
public class ComputerFactory implements ElectronicFactory{

    @Override
    public Screen produceScreen() {
        return new ComputerScreen();
    }

    @Override
    public Memory produceMemory() {
        return new ComputerMemory();
    }

    @Override
    public Processor produceProcessor() {
        return new ComputerProcessor();
    }
}

PhoneFactory.java

package com.ramostear.pattern.abstractfactory;
/**
 * @author ramostear
 * @create-time 2019/1/5 0005-2:35
 * @modify by :
 * @info:[手机生产工厂]
 * @since:
 */
public class PhoneFactory implements ElectronicFactory{


    @Override
    public Screen produceScreen() {
        return new PhoneScreen();
    }

    @Override
    public Memory produceMemory() {
        return new PhoneMemory();
    }

    @Override
    public Processor produceProcessor() {
        return new PhoneProcessor();
    }
}

现在我们已经拥有了一个抽象的工厂,它可以让我们生产相关的电子产品部件,即手机工厂可以生产手机屏幕、手机处理器和手机内存条,同样电脑工厂可以生产电脑显示器、电脑内存条和电脑处理器等。我们来简单的测试一下:

public class SimpleTest {

    public static void main(String[] args){
        ElectronicFactory factory = new PhoneFactory();
        Screen screen = factory.produceScreen();
        Memory memory = factory.produceMemory();
        Processor processor = factory.produceProcessor();

        System.out.println(screen.getDescription()+"\n"+memory.getDescription()+"\n"+processor.getDescription());

    }
}

控制台输出:

现在,我们可以为不同的电子产品生产工厂设计一个工厂,即工厂的工厂。本例子中,我们创建一个FacotryMaker类,负责返回PhoneFactory或者ComputerFactory,客户端可以通过FactoryMacker工厂来创建所需的工厂,进而生产不同的电子产品部件(屏幕、处理器、内存条)。
首先定义一个枚举类型的类FactoryType,用于给FactoryMacker提供选择参考:

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-4:16
 * @modify by :
 * @info:[工厂类型]
 * @since:
 */
public enum FactoryType {
    PHONE,COMPUTER;
}

然后定义一个生产工厂的工厂类FactoryMacker:

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-4:16
 * @modify by :
 * @info:[工厂创建器:工厂的工厂]
 * @since:
 */
public  class FactoryMacker {

    /**
     * 此工厂方法负责创建具体的工厂类
     * @param type
     * @return
     */
    public static ElectronicFactory makeFactory(FactoryType type){
        switch (type){
            case PHONE:
                return new PhoneFactory();
            case COMPUTER:
                return new ComputerFactory();
            default:
                throw new IllegalArgumentException("FactoryType not supported.");
        }
    }
}

最后,我们定义一个AbstractFactory类来封装上述的单个工厂类:

package com.ramostear.pattern.abstractfactory;
/**
 * @author ramostear
 * @create-time 2019/1/5 0005-4:21
 * @modify by :
 * @since:
 */
public class AbstractFactory {

    private Screen screen;
    private Memory memory;
    private Processor processor;

    public void createFactory(final ElectronicFactory factory){
        setScreen(factory.produceScreen());
        setMemory(factory.produceMemory());
        setProcessor(factory.produceProcessor());
    }

    public Screen getScreen() {
        return screen;
    }

    private void setScreen(Screen screen) {
        this.screen = screen;
    }

    public Memory getMemory() {
        return memory;
    }

    private void setMemory(Memory memory) {
        this.memory = memory;
    }

    public Processor getProcessor() {
        return processor;
    }

    private void setProcessor(Processor processor) {
        this.processor = processor;
    }
}

现在,整个抽象工厂模式案例已经全部实现。最后测试一下我们创建的抽象工厂模式案例:

package com.ramostear.pattern.abstractfactory;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-4:27
 * @modify by :
 * @info:[对抽象工厂进行测试]
 * @since:
 */
public class TestAbstractFactory {

    public static void main(String[] args){
        AbstractFactory factory = new AbstractFactory();

        System.out.println("produce phone...");

        factory.createFactory(FactoryMacker.makeFactory(FactoryType.PHONE));
        System.out.println(factory.getScreen().getDescription());
        System.out.println(factory.getMemory().getDescription());
        System.out.println(factory.getProcessor().getDescription());


        System.out.println("produce computer...");

        factory.createFactory(FactoryMacker.makeFactory(FactoryType.COMPUTER));
        System.out.println(factory.getScreen().getDescription());
        System.out.println(factory.getMemory().getDescription());
        System.out.println(factory.getProcessor().getDescription());
    }
}

控制台输出:

4.适用性

当满足以下场景时适合适用抽象工厂模式

  • 系统应该独立于其产品的创建、组成和表示方式
  • 一个系统应该配置多个产品系列中的一个
  • 相关产品对象的系列设计为一起使用,您需要强制执行此约束
  • 您希望提供产品的类库,并且只显示它们的接口,而不显示它们的实现
  • 您需要一个运行时值来构造一个特定的依赖项
  • 您需要提供一个或多个仅在运行时已知的参数,然后才能解析依赖项
设计模式-单例模式

设计模式-单例模式

设计模式三十六计之单例模式(Singleton)

解释:单例模式是为了确保在整个应用生命周期内一个类只有一个实例化对象,并且提供该实例化对象的全局访问入口

1.结构和原理

一个最基本的单例模式类包含一个私有的静态变量、一个私有的构造函数和和一个共有的静态函数。其中私有构造函数保证了该类不能通过构造函数来实例化,只能通过共有的静态函数返回一个唯一的私有静态变量。

2.实现单例模式

  • 懒汉模式———线程不安全

    public class Singleton{
      private static Singleton instance;
      private Singleton(){}
    
      public static Singleton getInstance(){
        if(instance == null){
          instance = new Singleton();
        }
        return instance;
      }   
    }
    

    在上述实现中,私有静态变量instance被延迟实例化,这样做的好处是在实际项目中,如果Singleton类没有被使用到,它就不会被实例化,从而减小系统开销。

    注意:懒汉模式在多线程环境中是不安全的,例如当前有n个线程同时执行 getInstance() 方法时,此时的instance都为null,那么Singleton类就会被实例化n次。这与单例模式的设计初衷相悖。

  • 饿汉模式———线程安全

    public class Singleton{
      private static Singleton instance = new Singleton();
      private Singleton(){}
      public static Singleton getInstance(){
        return instance;
      }
    }
    

    备注:在懒汉模式中,线程不安全是由于instance被多次实例化所造成的,在饿汉模式中直接实例化Singleton就解决了线程不安全问题。但是这种方式就失去了延迟实例化的好处。

  • 双重校验锁模式———线程安全

    public class Singleton{
      private volatile static Singleton instance;
      private Singleton(){}
      public static Singleton getInstance(){
        if(instance == null){
          synchronized(Singleton.class){
            if(instance == null){
              instance = new Singleton();
            }
          }
        }
        return instance;
      }  
    }
    

    在双重校验锁模式下,双重锁先判断instance是否被实例化,如果instance没有被实例化,则将实例化instance的语句进行加锁操作,只有当instance真正为null时,才去实例化Singleton。instance只会被实例化一次,之后将不会被再次实例化。

    说明:volatile关键字的作用是在多线程环境下,禁止JVM的指令重排列

  • 静态内部类模式

    public class Singleton{
      private Singleton(){}
    
      public static Singleton getInstance(){
        return SingletonProvider.INSTANCE;
      }
    
      public static class SingletonProvider{
        private static final Singleton INSTANCE = new Singleton();
      }
    }
    

    静态内部类模式与饿汉模式有异曲同工之处

  • 枚举模式

    public enum Singleton{
      INSTANCE;
      private String name;
      //getter()...
      // setter()...
      // otherMethod()...
    }
    

    使用方式:

    public class UserEnumSingleton{
      Singleton instance = Singleton.INSTANCE;
      instance.setName("example");
      System.out.println(instance.getName());
      Singleton instance2 = Singleton.INSTANCE;
      instance2.setName("example2");
      System.out.println(instance2.getName());
      instance.otherMethod();
      //other options...
      //使用java反射原理操作
      try{
        Singleton[] enums = Singleton.class.getEnumConstants();
        for(Singleton instance : enums){
          System.out.println(instance.getName());
        }
      }catch(Exception e){
        e.printStackTrace();
      }
    }
    

    控制台输出结果:

    example
    example2
    example2
    

    借助JDK的枚举来实现单例模式,不仅能够避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

3.总结

优点

单例模式使得应用系统中一个类只被实例化一次,节省系统资源开销,对于系统中需要频繁创建和销毁的对象,使用单例模式可以在一定程度上提高系统的性能。

缺点

由于采用单例模式对类进行设计,就必须要记住获取对象的入口,即共有的静态函数名,而不是采用new关键字进行类的实例化,这在多人协同开发的项目中会给开发人员带来一些困扰(看不到源码),因此需要统一编码规范。

适用的范围

  • 需要频繁的进行创建和销毁的类
  • 实例化过程中耗费时间过长、占用资源过多且被频繁调用的类
  • 频繁操作I/O流或者访问数据库的类
  • 应用中定义的工具类
设计模式-观察者模式

设计模式-观察者模式

设计模式三十六计之观察者模式(Observer)

1. 设计意图

定义对象之间的一对多依赖关系,以便当一个对象更改状态时,将自动通知和更新其所有依赖项。

观察者模式

简而言之

你别来找我,给我你的联系方式,有事我会主动联系你

2.案例演示

以当前最火热的吃鸡游戏作为一个简单的案例来演示观察者模式,当玩家进入游戏时,会收到游戏服务器推送的提示消息,随着游戏的进行,如果某个玩家被Kill掉了,游戏服务器会把此消息推送给房间里的其他玩家。在本案例中,“游戏” 是一个抽象的被观察者,“吃鸡游戏” 是具体的被观察者;“游戏玩家” 是一个抽象的观察者(接口),而玩家A、玩家B等是具体的观察者。案例的UML关系如下图:

UML类图-案例对象关系

3. 示例代码

3.1 抽象的被观察者类(Subject)

AbstractGame.java

package com.ramostear.pattern.observer;
import java.util.ArrayList;
/**
 * @author ramostear
 * @create-time 2019/1/5 0005-23:27
 * @modify by :
 * @info:[抽象的被观测者类]
 * @since:
 */
public abstract class AbstractGame {
    /**
     * 定义一个存放观察者的容器
     */
    public final ArrayList<Observer> obsList = new ArrayList<>();
    /**
     * 注册观察者
     * @param obs   观察者
     * @param <T>
     */
    public <T> void attach(Observer obs){
        if (obs == null){
            throw new NullPointerException("Observer is null.");
        }else{
            this.attachObs(obs);
        }
    }
    /**
     * 注册观察者
     * @param obs
     */
    private void attachObs(Observer obs){
        if (obs == null){
            throw new NullPointerException("class is null");
        }else {
            synchronized (obsList){
                if(!obsList.contains(obs)){
                    obsList.add(obs);
                }
            }
        }
    }
    /**
     * 注销观察者
     * @param obs   观察者
     * @param <T>
     */
    public <T> void detach(Observer obs){
        if(obs == null){
            throw new NullPointerException("Observer is null");
        }else {
            this.detachObs(obs);
        }
    }
    /**
     * 注销观察者
     * @param obs
     */
    private void detachObs(Observer obs){
        if(obs == null){
            throw new NullPointerException("Class is null");
        }else{
            synchronized (obsList){
               obsList.remove(obs);
            }
        }
    }
    /**
     * 通知所有的观察者
     * @param messages
     */
    public abstract void notifyAllObs(String...messages);
    /**
     * 通知某个观察者
     * @param obs
     * @param messages
     */
    public abstract void notifyObs(Observer obs,String...messages);
}

AbstractGame类中定义了添加、删除和通知观察者的方法,同时有一个List类型的容器,用于保存已注册的观察者,当需要通知观察者时,从容器中取出观察者信息。

说明:抽象的被观察者可以定义成一个抽象类或者接口,本案例中采用的是抽象类

3.2 抽象的观察者接口(Observer)

Observer.java

package com.ramostear.pattern.observer;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-23:26
 * @modify by :
 * @info:[观察者接口]
 * @since:
 */
public interface Observer {
    /**
     * 更新状态
     * @param messages
     */
    void update(String... messages);
}

在该接口中定义了一个update() 方法,当被观察者发出通知时,此方法会被调用。

3.3 具体被观察者(ConcreteSubject)

ChikenGame继承了AbstractGame类,并对通知方法进行了具体的实现。
ChikenGame.java

package com.ramostear.pattern.observer;

/**
 * @author ramostear
 * @create-time 2019/1/5 0005-23:55
 * @modify by :
 * @info:[吃鸡游戏类]
 * @since:
 */
public class ChickenGame extends AbstractGame {

    private String roomName;

    public ChickenGame(String roomName) {
        this.roomName = roomName;
    }


    public String getRoomName() {
        return roomName;
    }

    public void setRoomName(String roomName) {
        this.roomName = roomName;
    }

    @Override
    public void notifyAllObs(String... messages) {
        obsList.forEach(obs->{
            this.notifyObs(obs,messages);
        });
    }

    @Override
    public void notifyObs(Observer obs, String... messages) {
       if (obs == null){
           throw new NullPointerException("Observer is null");
       }else{
          obs.update(messages);
       }
    }
}

3.4 具体观察者(ConcreteObserver)

Gamer类实现了Observer接口,并对Observer的update方法进行了具体的实现;这里为了演示,只是简单的对消息进行输出。
Gamer.java

package com.ramostear.pattern.observer;
/**
 * @author ramostear
 * @create-time 2019/1/6 0006-0:06
 * @modify by :
 * @info:[游戏玩家]
 * @since:
 */
public class Gamer implements Observer{

    private String name;

    public Gamer(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void update(String... messages) {
        System.out.println("玩家:"+name);
      for (String message:messages){
          System.out.println("消息->"+message+"\n");
      }
    }
}

3.5 测试本次案例

创建一个吃鸡游戏叫“三国吃鸡演义” ,将刘、关、张三个玩家注册到吃鸡游戏中。游戏发布消息给三个玩家,刘、关、张同时收到游戏发出的消息,当关羽挂掉后,只有刘、张两个玩家收到消息;当张飞再挂掉后,只有刘备收到消息。为了演示观察者模式,最后我们让关羽满血复活,此时刘、关二人收到游戏发出的消息。
App.java

package com.ramostear.pattern.observer;
/**
 * @author ramostear
 * @create-time 2019/1/6 0006-0:08
 * @modify by :
 * @info:[测试类]
 * @since:
 */
public class App {
    public static void main(String[] args){
        ChickenGame game = new ChickenGame("三国吃鸡演义");
        Gamer gamerLiu = new Gamer("刘备");
        Gamer gamerZhang = new Gamer("张飞");
        Gamer gamerGuan = new Gamer("关羽");

        game.attach(gamerLiu);
        game.attach(gamerGuan);
        game.attach(gamerZhang);
        game.notifyAllObs("欢迎进入"+game.getRoomName());
        game.notifyAllObs(new String[]{"刘关张桃园三结义,开始三国吃鸡演义..."});

        game.detach(gamerGuan);
        game.notifyAllObs("#关羽:\"我去!被98K爆了,快来扶我一下!\"");
        game.notifyAllObs("#刘备:\"我去,这货肥得一批!\"");

        game.detach(gamerZhang);
        game.notifyAllObs("#张飞:\"我去,这比是挂!\"");
        game.notifyAllObs("#刘备:\"我去!咋这么多人,我凉了!\"");

        game.attach(gamerGuan);
        game.notifyAllObs("关羽满血复活");
        game.notifyAllObs("#刘备:\"苟住,苟住就能赢!\"");

    }
}

测试结果:
观察者模式案例测试结果

4. 适用性

当满足以下情况中的一种时使用观察者模式

  • 当抽象有两个Aspect时,一个依赖于另一个。 将这些Aspact封装在单独的对象中可让您独立地改变和重用它们.
  • 当一个对象的更改需要更改其他对象时,你不知道到底需要更改多少个关联的对象
  • 当不希望多个对象之前发生紧耦合时

5. 真实案例

设计模式-建造者模式

设计模式-建造者模式

设计模式三十六计之建造者模式(Builder)

1. 设计意图

将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示。
建造者模式类图

2. 演示案例

假设我们需要创建一个用户对象,用户对象的属性有身份证号码、姓名、年龄、性别、族别、地址。最简单的方式是定义一个包含这六个属性的构造函数来完成对象的创建,但是,你想在构建的过程中只想包含其中的一个或者几个属性的时候,问题来了,没有与之对应的构造函数存在,也不可能提前定义多构造函数来覆盖这种动态传参的构造需求。这种类型的需求场景就需要建造者模式了(Builder)。

简而言之

建造者模式的要领是允许你创建不同风格的对象,同时避免构造函数被污染。当一个对象可能有N中风格存在或者对象的创建要涉及到多个步骤的时候适用于建造者模式

维基百科

构建器模式是一种对象创建软件设计模式,旨在寻找伸缩构造器反模式的解决方案。

在开始给出具体的代码前,我们先对比一下常规的一种设计方式。就上述的案例场景,我们可能会给出如下的一个构造函数:

  public User(String id,String name,int age,int gender,String nation,String address){
    //setter...
  }

在这种设计方式下,当用户属性改变时,构造函数参数的数量可能会很快失去控制,并且很难理解构造函数的参数排列,另外,如果用户属性继续增加,构造函数的参数列表将持续增长。这被称为伸缩构造反模式。

3. 程序示例

针对上述提到的问题,最合适的解决方案时使用建造者模式,首先我们需要创建一个User类:


public class User {

  private String id;    //身份证号码
  private String name;  //姓名
  private int age = 0;  //年龄
  private int gender = 0;//0:保密,1:男性,2:女性
  private String nation;  //族别
  private String address; //地址

  private User(Builder builder){
    this.id = builder.id;
    this.name = builder.name;
    this.age = builder.age;
    this.gender = builder.gender;
    this.nation = builder.nation;
    this.address = builder.address;
  }

  //getter...
  //setter...
  //toString...
}

然后我们需要创建一个Bulder类:

public static class Builder{

  private String id;    //身份证号码
  private String name;  //姓名
  private int age = 0;  //年龄
  private int gender = 0;//0:保密,1:男性,2:女性
  private String nation = "none";  //族别
  private String address = "none"; //地址

  /**
   * 假定建造时必须提供身份证号码和姓名
  **/
  public Builder(String id,String name){
    if(id == null || name == null){
      throw new IllegalArgumentException("id and name can not be null");
    }
    this.id = id;
    this.name = name;
  }

  public Builder withAge(int age){
    this.age = age;
    return this;
  }

  public Builder withGender(int gender){
    this.gender = gender;
    return this;
  }

  public Builder withNation(String nation){
    this.nation = nation;
    return this;
  }

  public Builder withAddress(String address){
    this.address = address;
    return this;
  }

  public User build(){
    return new User(this);
  }


}

说明:这里我们使用了一个静态内部类 Builder 来实现一个建造器

最后我们可以这样来创建一个用户对象:


public class TestUserBuilder{

  public static void main(String[] args){

    User zhangSan = new User.Builder("13579","张三").withAge(22).withGender(1).build();

    User wangWu = new User.Builder("24680","王五").withAge(30).withGender(1).withNation("汉族").build();

    User liLei = new User.Builder("123456","李蕾").withAge(18).withGender(2).withNation("苗族")
                         .withAddress("贵州省黔西南布依族苗族自治州").build();

    System.out.println(zhangSan.toString());
    System.out.println(wangWu.toString());
    System.out.println(liLei.toString());
  }
}

输入结果

id:13579,name:张三,age:22,gender:1,nation:none,address:none
id:24680,name:王五,age:30,gender:1,nation:汉族,address:none
id:123456,name:李蕾,age:18,gender:2,name:苗族,address:贵州省黔西南布依族苗族自治州

4.使用场景

满足一下需求的时候推荐使用建造者模式:

  1. 创建复杂对象的算法应该独立于组成对象的各个部分以及它们的组装方式。
  2. 构造过程必须允许对构造的对象进行不同的表示

5.建造者模式应用例子

6.总结

建造者模式使得对象内部的属性可以独立变化,使用者不必知道对象内部的组成细节,每个建造器相对独立,与其他的建造器无关。建造者模式的应用让对象创建过程更加灵活和易于控制。建造者模式也有相应的弊端,它使得对象的创建过程暴露给外界,让整个对象的 “加工工艺” 变得不透明。

参考