框架阶段面试题

文章目录

框架阶段面试题

1.SpringMVC的五大组件和请求响应流程

1.1 MVC设计模式

MVC设计模式是一种通用的软件编程思想,在MVC设计模式中认为, 任何软件都可以分为三部分组成:

  1. 控制程序流转的控制器(Controller)
  2. 封装数据处理数据的模型(Model)
  3. 负责展示数据的视图(view)
    并且在MVC设计思想中要求一个符合MVC设计思想的软件应该保证上面这三部分相互独立,互不干扰,每一个部分只负责自己擅长的部分。如果某一个模块发生变化,应该尽量做到不影响其他两个模块。这样做的好处是,软件的结构会变得更加的清晰,可读性强。有利于后期的扩展和维护,并且代码可以实现复用。
    在这里插入图片描述

1.2 初识SpringMVC

1.2.1 Servlet的缺点

  1. 通常情况下,一个Servlet类只负责处理一个请求,若项目中有成百上千个请求需要处理,就需要有成百上千个Servlet类,这样会使得项目中Servlet类的个数暴增
  2. 在Servlet3.0版本之前,每一个Servlet都需要在web.xml文件中至少做八行配置信息,配置内容多且繁琐。当Servlet特别多时,web.xml 配置量太多,不利于团队开发;
  3. 当通过客户端提交参数到服务器,通过Servlet进行接收时,无论数据本身是什么格式,在Servlet中一律按照字符串进行接收后期需要进行类型转换,复杂类型还需要特殊处理,特别麻烦!
    String value = request.getParameter(String name);
  4. servlet具有容器依赖性,必须放在服务器中运行,不利于单元测试

1.2.2 SpringMVC简介

Springmvc是spring框架的一个模块,spring和springmvc无需中间整合层整合,Springmvc是一个基于mvc的web框架。

1.2.3 Spring执行原理

1.2.3.1 SpringMVC五大核心组件

Spring MVC 是Spring 框架中基于MVC设计思想实现的一个用于处理Web请求的模块。其简易架构分析,如图所示:
在这里插入图片描述

  1. DispatcherServlet  前端控制器,请求入口;作用:接收请求,调用其它组件处理请求,响应结果,相当于转发器、中央处理器,是整个流程控制的中心。
  2. HandlerMapping   处理器映射器,请求派发,用于管理url与对应controller的映射关系。负责请求和控制器建立一一对应的关系
  3. Controller      后端控制器-handler,,负责处理由DispatcherServlet 分发的请求
  4. Interceptors 拦截器,实现请求响应的共性处理。
  5. ViewResolver    视图解析器,ViewResolver 的主要作用是把一个逻辑上的视图名称解析为一个真正的视图。
1.2.3.2 请求响应流程

在这里插入图片描述

  1. 用户发送请求 至 前端控制器(DispatcherServlet);
    提示:DispatcherServlet的作用:接收请求,调用其它组件处理请求,响应结果,相当于转发器、中央处理器,是整个流程控制的中心;
  2. 前端控制器(DispatcherServlet)收到请求后调用处理器映射器(HandlerMapping),处理器映射器(HandlerMapping)找到具体的Controller(可以根据xml配置、注解进行查找),并将Controller返回给DispatcherServlet;
  3. 前端控制器(DispatcherServlet)调用处理器适配器(HandlerAdapter)。处理器适配器经过适配调用具体的Controller;(Controller–> service --> Dao --> 数据库),Controller执行完成后返回ModelAndView,
    提示:Model(模型数据,即Controller处理的结果,Map) View(逻辑视图名,即负责展示结果的JSP页面的名字)处理器适配器(HandlerAdapter)将controller执行的结果(ModelAndView)返回给前端控制器(DispatcherServlet);
  4. 前端控制器(DispatcherServlet)将执行的结果(ModelAndView)传给视图解析器(ViewReslover),视图解析器(ViewReslover)根据View(逻辑视图名)解析后返回具体JSP页面
  5. 前端控制器(DispatcherServlet)根据Model对View进行渲染(即将模型数据填充至视图中),前端控制(DispatcherServlet)将填充了数据的网页响应给用户。

其中整个过程中需要开发人员编写的部分有Controller、Service、Dao、View;

2.SpringIOC的原理(概念,如何创建和管理对象的,依赖注入的方式)

2.1 什么是IOC

IoC(Inverse of Control:控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理,也就是指将对象的创建、对象的存储、对象的管理交给了spring容器。 IoC 在其他语言中也有应用,并非 Spring 特有。IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。

2.2 如何创建和管理对象

2.3 依赖注入的方式

2.3.1 Set方式注入

  • 普通属性注入
<!-- applicationContext.xml中声明User类的bean实例 -->
<bean id="user" class="com.tedu.spring.User">
	<!-- 通过set方式为普通属性赋值 -->
	<property name="name" value="韩少云"></property>
	<property name="age" value="20"></property>
</bean>
  • 对象属性注入
<!-- 声明User类的bean实例 -->
<bean id="user" class="com.tedu.spring.User">
	<!-- 通过set方式为普通属性赋值 -->
	<property name="name" value="韩少云"></property>
	<property name="age" value="20"></property>
	<!-- 通过set方式为对象属性赋值 -->
	<property name="userInfo" ref="userInfo"></property>
</bean>

由于此处是将UserInfo对象作为值赋值给另一个对象的属性,因此ref属性的值,为UserInfo对象bean标签的id值。

2.3.2 构造方法注入

<bean id="user" class="com.tedu.spring.User">
	<!-- 通过set方式为普通属性赋值
	<property name="name" value="韩少云"></property>
	<property name="age" value="20"></property>
	<property name="userInfo" ref="userInfo"></property> 
	 -->
	<!-- 通过构造器中参数为属性赋值 -->
	<constructor-arg name="name" value="马云"></constructor-arg>
	<constructor-arg name="age" value="35"></constructor-arg>
	<constructor-arg name="userInfo" ref="userInfo"></constructor-arg>
</bean>
<!-- 声明UserInfo类的bean实例 -->
<bean id="userInfo" class="com.tedu.spring.UserInfo"></bean>

其中,constructor-arg标签name属性的值必须和构造函数中参数的名字相同! 同样的,普通属性直接通过value注入即可;
对象属性通过ref属性注入。

3.AOP的原理(概念,代理机制,应用场景,几大通知的执行顺序)

3.1 概念

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。如图所示:
在这里插入图片描述
AOP与OOP字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。

3.2 代理机制

参考文章

3.3 AOP 应用场景

事务处理、日志管理、权限控制

思考:现有一业务,在没有AOP编程时,如何基于OCP原则实现功能扩展?
方法1:继承重写父类方法
方法2:静态代理

3.4 Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring A 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ,它比Spring AOP 快很多。

4.mybatis的核心对象,$和#的区别,mybatis的事务处理

4.1 mybatis的核心对象

SqlSessionFactory是mybatis的核心对象,是一个数据库连接的抽象,通过他可以执行一次又一次的数据库操作。SqlSessionFactoryBuilder是专门用于构造SqlSessionFactory的工厂,其通过build函数的重载,支持以不同的方式创建SqlSessionFactory;

4.2 MyBatis基本要素

4.2.1 MyBatis的核心接口和类

在这里插入图片描述
注:

  1. SqlSessionFactory是我们MyBatis整个应用程序的中心;整个程序都是以SqlSessionFactory的实例为核心的。
  2. SqlSessionFactory对象是由SqlSessionFactoryBuilder对象创建而来的。
  3. SqlSessionFactoryBuilder是通过xml配置文件或者configuration类的实例去构造这个对象。然后调用build()方法去构造SqlSessionFactory对象。
  4. 使用SqlSessionFactory对象的openSession()方法来获取SqlSession对象,有了SqlSession对象,我们就可以去进行数据库操作了,因为SqlSession里面包含了以数据库为背景所有执行sql操作的方法
4.2.1.1 SqlSessionFactoryBuilder
  • 用过即丢,其生命周期只存在于方法体内
  • 可重用其来创建多个SqlSessionFactory实例
  • 负责构建SqlSessionFactory,并提供多个build方法的重载
    在这里插入图片描述

在我们之前的测试中,使用的是读取xml文件,将其转换为输入流参数的形式提供给SqlSessionFactoryBuilder然后来构造SqlSessionFactory对象。对于SqlSessionFactoryBuilder来说,创建完SqlSessionFactory对象之后,其使命也就完成了,可以直接丢弃了。

4.2.1.2 SqlSessionFactory

SqlSessionFactory 简单的理解就是创建 SqlSession 实例的工厂。所有的 MyBatis 应用都是以 SqlSessionFactory 实例为中心,SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 对象来获得。有了它之后,顾名思义,就可以通过 SqlSessionFactory 提供的 openSession() 方法来获取 SqlSession 实例。

说明: openSession() 方法的参数为 boolean 值时,若传入 true 表示关闭事务控制,自动提交; false 表示开启事务控制。若不传入参数,默认为 true。

openSession (boolean autoCommit)
openSession()//若不传入参数,默认为 true ,自动提交
  • SqlSessionFactory 的生命周期和作用域
      SqlSessionFactory 对象一旦创建,就会在整个应用运行过程中始终存在。没有理由去销毁或再创建它,并且在应用运行中也不建议多次创建 SqlSessionFactory 。因此 SqlSessionFactory 的最佳作用域是 Application,即随着应用的生命周期一同存在。那么这种 “存在于整个应用运行期间,并且同时只存在一个对象实例” 的模式就是所谓的单例模式(指在应用运行期间有且仅有一个实例)。即 SqlSessionFactory 的最佳范围是应用范围。
4.2.1.3 SqlSession
  • 包含了执行SQL所需的所有方法
  • 对应一次数据库会话,会话结束必须关闭
  • 线程级别,不能共享
    在这里插入图片描述
  • 在SqlSession里可以执行多次SQL语句,但一旦关闭了SqlSession就需要重新创建。
  • SqlSession示例不能被共享,也不是线程安全的。所以其最佳作用域是reqeust作用域内或者方法体内的作用域内。

SqlSession 的两种使用方式

//方式 1:通过 SQLSession 实例调用 selectList、selectOne 等方法来直接执行已映射的 SQL 语句。(不需要编写 DAO 接口)
    
        // MyBatis 通过 mapper 文件的 namespace 和子元素的 id 来找到相应的 SQL,从而执行查询操作
        userList=sqlSession.selectList("com.smbms.dao.UserMapper.getUserList"); 

    说明:
        1、com.smbms.dao.UserMapper.getUserList=namespace+id
        2、使用方式一可以不用写接口中的方法,因为方式一直接执行映射文件的 SQL 语句。
     3、执行 CRUD 操作,有参数传递时此方式不适用,应使用方式 2//方式 2:基于 mapper 接口方式操作数据。(官方推荐使用)

        //创建绑定映射语句的接口 UserMapper.java,并提供接口方法 getUserList(),该接口称为映射器。
         userList=sqlSession.getMapper(UserMapper.class).getUserList();

     说明:
        1、接口的方法必须与 SQL 映射文件中 SQL 语句的 id 一一对应。
        2、第二种方式是通过 SQLSession 实例调用 getMapper(Mapper.class) 执行 Mapper 接口方法来实现对数据库的查询操作。
        3、第一种方式是旧版本的 MyBatis 提供的操作方式,虽然现在也可以正常工作,但是第二种方式是 MyBatis 官方所推荐使用的,其表达方式也更加直白。代码更加清晰,类型安全,也不用担心易错的字符串字面值以及强制类型转换。

4.2.2 mybatis-config.xml 系统核心配置文件

参考文章

4.2.3 mapper.xml SQL映射文件

4.3 $和#的区别

  1. #传入的参数在sql中会进行转义处理(在字符串日期类型的值的两边加上单引号),$传入的参数在sql中直接显示为传入的值。
  2. #方式能够很大程度防止sql注入,$方式无法防止Sql注入
  3. 能用 #{} 的地方就用 #{},不用或少用 ${}
  4. 表名作参数时,必须用 ${}。如:select * from ${tableName}
  5. order by 时,必须用 ${}。如:select * from t_user order by ${columnName}
  6. 使用 ${} 时,要注意何时加或不加单引号,即 和′和 ′{}’

4.4 mybatis的事务处理

5.spring的常用注解及其作用

5.1用于注册bean对象注解

5.1.1 @Component

作用:调用无参构造创建一个bean对象,并把对象存入spring的IOC容器,交由spring容器进行管理。相当于在xml中配置一个bean
属性:

value:指定bean的id。如果不指定value属性,默认bean的id是当前类的类名。首字母小写。

5.1.2 @Controller

作用:作用上与@Component。一般用于表现层的注解。
属性:

value:指定bean的id。如果不指定value属性,默认bean的id是当前类的类名。首字母小写。

5.1.3 @Service

作用:作用上与@Component。一般用于业务层的注解。
属性:

value:指定bean的id。如果不指定value属性,默认bean的id是当前类的类名。首字母小写。

5.1.4 @Repository

作用:作用上与@Component。一般用于持久层的注解。
属性:

value:指定bean的id。如果不指定value属性,默认bean的id是当前类的类名。首字母小写。

5.1.5 @Bean

作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
属性:

name:用于指定bean的id。当不写时,默认值是当前方法的名称。注意:当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象,查找的方式和Autowired注解的作用是一样的。

案例:

/**
 * 获取DataSource对象
 * @return
 */
@Bean(value = "dataSource")
public DataSource getDataSource() {
    
    
 try {
    
    
	 ComboPooledDataSource dataSource = new ComboPooledDataSource();
	 dataSource.setDriverClass(this.driver);
	 dataSource.setJdbcUrl(this.url);
	 dataSource.setUser(this.username);
	 dataSource.setPassword(this.password);
	 return dataSource;
 }catch (Exception exception) {
    
    
 	throw new RuntimeException(exception);
 }
} 

5.2 用于依赖注入的注解

5.2.1 @Autowired

作用:@Autowire和@Resource都是Spring支持的注解形式动态装配bean的方式。Autowire默认按照类型(byType)装配,如果想要按照名称(byName)装配,需结合@Qualifier注解使用。
属性:

required:@Autowire注解默认情况下要求依赖对象必须存在。如果不存在,则在注入的时候会抛出异常。如果允许依赖对象为null,需设置required属性为false。

案例:

@Autowire
@Qualifier("userService") 
private UserService userService;

5.2.2 @Qualifier

作用:在自动按照类型注入的基础之上,再按照 Bean 的 id 注入。它在给字段注入时不能独立使用,必须和 @Autowire一起使用;但是给方法参数注入时,可以独立使用
属性:

value:用于指定要注入的bean的id,其中,该属性可以省略不写。

案例:

@Autowire
@Qualifier(value="userService") 
//@Qualifier("userService") //value属性可以省略不写
private UserService userService;

5.2.3 @Resource

作用:@Autowire和@Resource都是Spring支持的注解形式动态装配bean的方式。@Resource默认按照名称(byName)装配,名称可以通过name属性指定。如果没有指定name,则注解在字段上时,默认取(name=字段名称)装配。如果注解在setter方法上时,默认取(name=属性名称)装配。
属性:

name:用于指定要注入的bean的id
type:用于指定要注入的bean的type 装配顺序
1.如果同时指定name和type属性,则找到唯一匹配的bean装配,未找到则抛异常;
2.如果指定name属性,则按照名称(byName)装配,未找到则抛异常;
3.如果指定type属性,则按照类型(byType)装配,未找到或者找到多个则抛异常;
4.既未指定name属性,又未指定type属性,则按照名称(byName)装配;如果未找到,则按照类型(byType)装配。

案例:

@Resource(name="userService")
//@Resource(type="userService")
//@Resource(name="userService", type="UserService")
private UserService userService;

5.2.4 @Value

作用:通过@Value可以将外部的值动态注入到Bean中,可以为基本类型数据和String类型数据的变量注入数据
案例:

// 1.基本类型数据和String类型数据的变量注入数据
@Value("tom") 
private String name;
@Value("18") 
private Integer age;
// 2.从properties配置文件中获取数据并设置到成员变量中
// 2.1jdbcConfig.properties配置文件定义如下
jdbc.driver \= com.mysql.jdbc.Driver 
jdbc.url \= jdbc:mysql://localhost:3306/eesy 
jdbc.username \= root 
jdbc.password \= root
// 2.2获取数据如下
@Value("${jdbc.driver}") 
private String driver;
@Value("${jdbc.url}") 
private String url; 
 
@Value("${jdbc.username}") 
private String username; 
 
@Value("${jdbc.password}") 
private String password;

5.3用于改变bean作用范围的注解

5.3.1 @Scope

作用:指定bean的作用范围。
属性:

value:
1)singleton:单例
2)prototype:多例
3)request:
4)session:
5)globalsession:

案例:

@Autowire
@Scope(value="prototype")
private UserService userService;

5.4 其他

  • @Lazy注解用于描述类,其目的是告诉spring框架此类支持延迟加载,通常会配合单例作用域使用。
  • @PostConstruct 注解用于描述bean对象生命周期方法中的初始化方法,此方法会在对象的构造方法之后执行。
  • @PreDestroy 注解用于描述Bean对象生命周期方法中的销毁方法,此方法会在对象销毁之前执行(当作用域为prototype时,此方法不会执行)。

注意:@PostConstruct和@PreDestroy 是Java自己的注解**

6.spring的事务管理(声明式,编程式)

6.1 编程式事务

在代码中硬编码。(不推荐使用)

6.2 声明式事务

在配置文件中配置(推荐使用)
声明式事务又分为两种:

  1. 基于XML的声明式事务
  2. 基于注解的声明式事务

7.springboot有啥优点

7.1 Spring Boot 核心特性

Spring boot是一个脚手架(而非框架),构建于Spring框架(Framework)基础之上,基于快速构建理念,提供了自动配置功能,可实现其开箱即用特性(创建完一个基本的项目以后,可零配置或者少量配置即可运行我们的项目),其核心主要有如下几个方面:

  • 起步依赖(Starter Dependency)。

spring-boot-starter-xxx就是SpringBoot的起步依赖。SpringBoot通过提供众多起步依赖降低项目依赖的复杂度。起步依赖本质上是一个Maven项目对象模型,定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。很多起步依赖的命名都暗示了他们提供的某种>或某类功能。
起步依赖,还有一个不得不提的好处——版本号管理
SpringBoot官方提供的起步依赖都和SpringBoot版本紧密相连,为我们传递的第三方依赖是经过足够测试后敲定下来最合适的版本。

  • 自动配置(Auto Configuration)。(自动配置负责减少人工配置的工作量)
  • 健康检查(Actator)-监控。
    Spring Boot Framework旨在简化Spring开发。
    在这里插入图片描述

7.2 Spring Boot的主要优点

  1. 开发基于 Spring 的应用程序很容易
  2. Spring Boot项目所需的开发或工程时间明显减少,通常会提高整体生产力。
  3. Spring Boot不需要编写大量样板代码、XML配置和注释
  4. Spring引导应用程序可以很容易地与Spring生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security等。
  5. Spring Boot遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。
  6. Spring Boot应用程序提供嵌入式HTTP服务器,如Tomcat和Jetty,可以轻松地开发和测试web应用程序。(这点很赞!普通运行Java程序的方式就能运行基于Spring Boot web 项目,省事很多)
  7. Spring Boot提供命令行接口(CLI)工具,用于开发和测试Spring Boot应用程序,如Java或Groovy。
  8. Spring Boot提供了多种插件,可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。

7.3 Spring Boot项目启动过程分析

在这里插入图片描述
SpringBoot 项目在启动时,首先基于启动入口类上的注解描述,进行自动配置并扫描指定包以及子包中的类进行加载,然后检测类上是否有Spring框架中指定的注解描述(例如@Component,@Controller,@Service等)。假如有,则将类交给Spring框架中的BeanFactory工厂接口的实现类对象,此工厂对象会基于反射创建Bean的实例,假如此Bean指定了生命周期方法,还会调用生命周期方法。当实例创建以后,Spring框架还会基于类的作用域描述,将实例存储到不同作用域的容器中。以实现Bean对象的科学应用。

8.对RESTFUL有何理解

8.1 概念

REST,即 Representational State Transfer 的缩写。这个词组的翻译过来就是"表现层状态转化"。这样理解起来甚是晦涩,实际上 REST 的全称是 Resource Representational State Transfe ,直白地翻译过来就是 “资源”在网络传输中以某种“表现形式”进行“状态转移” 。如果还是不能继续理解,请继续往下看,相信下面的讲解一定能让你理解到底啥是 REST 。

我们分别对上面涉及到的概念进行解读,以便加深理解,不过实际上你不需要搞懂下面这些概念,也能看懂我下一部分要介绍到的内容。不过,为了更好地能跟别人扯扯 “RESTful API”我建议你还是要好好理解一下!

  • 资源(Resource) :我们可以把真实的对象数据称为资源。一个资源既可以是一个集合,也可以是单个个体。比如我们的班级 classes 是代表一个集合形式的资源,而特定的 class 代表单个个体资源。每一种资源都有特定的 URI(统一资源定位符)与之对应,如果我们需要获取这个资源,访问这个 URI 就可以了,比如获取特定的班级:/class/12。另外,资源也可以包含子资源,比如 /classes/classId/teachers:列出某个指定班级的所有老师的信息
  • 表现形式(Representational):“资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式比如 json,xml,image,txt 等等叫做它的"表现层/表现形式”。
  • 状态转移(State Transfer) :大家第一眼看到这个词语一定会很懵逼?内心 BB:这尼玛是啥啊? 大白话来说 REST 中的状态转移更多地描述的服务器端资源的状态,比如你通过增删改查(通过 HTTP 动词实现)引起资源状态的改变。ps:互联网通信协议 HTTP 协议,是一个无状态协议,所有的资源状态都保存在服务器端。

综合上面的解释,我们总结一下什么是 RESTful 架构:

  1. 每一个 URI 代表一种资源;
  2. 客户端和服务器之间,传递这种资源的某种表现形式比如 json,xml,image,txt 等等;
  3. 客户端通过特定的 HTTP 动词,对服务器端资源进行操作,实现"表现层状态转化"。

8.2 REST 接口规范

1、动作

  • GET :请求从服务器获取特定资源。举个例子:GET /classes(获取所有班级)
  • POST :在服务器上创建一个新的资源。举个例子:POST /classes(创建班级)
  • PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /classes/12(更新编号为 12 的班级)
  • DELETE :从服务器删除特定的资源。举个例子:DELETE /classes/12(删除编号为 12 的班级)
  • PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。

2、路径(接口命名)

路径又称"终点"(endpoint),表示 API 的具体网址。实际开发中常见的规范如下:

  1. 网址中不能有动词,只能有名词,API 中的名词也应该使用复数。 因为 REST 中的资源往往和数据库中的表对应,而数据库中的表都是同种记录的"集合"(collection)。如果 API 调用并不涉及资源(如计算,翻译等操作)的话,可以用动词。 比如:GET /calculate?param1=11&param2=33
  2. 不用大写字母,建议用中杠 - 不用下杠 _ 比如邀请码写成 invitation-code而不是 invitation_code

Talk is cheap!来举个实际的例子来说明一下吧!现在有这样一个 API 提供班级(class)的信息,还包括班级中的学生和教师的信息,则它的路径应该设计成下面这样。

接口尽量使用名词,禁止使用动词。 下面是一些例子:

GET    /classes:列出所有班级
POST   /classes:新建一个班级
GET    /classes/classId:获取某个指定班级的信息
PUT    /classes/classId:更新某个指定班级的信息(一般倾向整体更新)
PATCH  /classes/classId:更新某个指定班级的信息(一般倾向部分更新)
DELETE /classes/classId:删除某个班级
GET    /classes/classId/teachers:列出某个指定班级的所有老师的信息
GET    /classes/classId/students:列出某个指定班级的所有学生的信息
DELETE classes/classId/teachers/ID:删除某个指定班级下的指定的老师的信息

反例:

/getAllclasses
/createNewclass
/deleteAllActiveclasses

理清资源的层次结构,比如业务针对的范围是学校,那么学校会是一级资源:/schools,老师: /schools/teachers,学生: /schools/students 就是二级资源。

3、过滤信息(Filtering)

如果我们在查询的时候需要添加特定条件的话,建议使用 url 参数的形式。比如我们要查询 state 状态为 active 并且 name 为 guidegege 的班级:

GET    /classes?state=active&name=guidegege

比如我们要实现分页查询:

GET    /classes?page=1&size=10 //指定第1页,每页10个数据

4、状态码(Status Codes)

状态码范围:

2xx:成功 3xx:重定向 4xx:客户端错误 5xx:服务器错误
200 成功 301 永久重定向 400 错误请求 500 服务器错误
201 创建 304 资源未修改 401 未授权 502 网关错误
403 禁止访问 504 网关超时
404 未找到
405 请求方法不对

9 补充

9.1 MyBatis

9.1.1 什么是MyBatis

MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注SQL本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码。

Mybatis通过xml或注解的方式将要执行的各种statement(statement、preparedStatemnt)配置起来,并通过java对象和statement中的sql进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射成java对象并返回。

总之,Mybatis对JDBC访问数据库的过程进行了封装,简化了JDBC代码,解决JDBC将结果集封装为Java对象的麻烦。
下图是MyBatis架构图:
在这里插入图片描述

  1. mybatis-config.xml是Mybatis的核心配置文件,通过其中的配置可以生成SqlSessionFactory,也就是SqlSession工厂
  2. 基于SqlSessionFactory可以生成SqlSession对象
  3. SqlSession是一个既可以发送SQL去执行,并返回结果,类似于JDBC中的Connection对象,也是Mybatis中至关重要的一个对象。
  4. Executor是SqlSession底层的对象,用于执行SQL语句
  5. MapperStatement对象也是SqlSession底层的对象,用于接收输入映射(SQL语句中的参数),以及做输出映射(即将SQL查询的结果映射成相应的结果)

9.1.2 为什么要使用MyBatis

思考:在开始之前,思考下如何通过JDBC查询Emp表中的所有记录,并封装到一个List集合中返回。(演示:准备数据、导包、导入JDBC程序)

  • 使用传统方式JDBC访问数据库:
  1. 使用JDBC访问数据库有大量重复代码(比如注册驱动、获取连接、获取传输器、释放资源等);
  2. JDBC自身没有连接池,会频繁的创建连接和关闭连接,效率低;
  3. SQL是写死在程序中,一旦修改SQL,需要对类重新编译;
  4. 对查询SQL执行后返回的ResultSet对象,需要手动处理,有时会特别麻烦;
  • 使用mybatis框架访问数据库:
  1. Mybatis对JDBC对了封装,可以简化JDBC代码;
  2. Mybatis自身支持连接池(也可以配置其他的连接池),因此可以提高程序的效率;
  3. Mybatis是将SQL配置在mapper文件中,修改SQL只是修改配置文件,类不需要重新编译。
  4. 对查询SQL执行后返回的ResultSet对象,Mybatis会帮我们处理,转换成Java对象。

总之,JDBC中所有的问题(代码繁琐、有太多重复代码、需要操作太多对象、释放资源、对结果的处理太麻烦等),在Mybatis框架中几乎都得到了解决!!

9.1.3 mybatis中的占位符

在mybatis中占位符有两个,分别是 #{} 占位符 和 ${} 占位符

9.1.3.1 #{}占位符

#{}相当于JDBC中的问号(?)占位符,是为SQL语句中的参数值进行占位,大部分情况下都是使用#{}占位符; 并且当#{}占位符是为字符串或者日期类型的值进行占位时,在参数值传过来替换占位符的同时,会进行转义处理(在字符串或日期类型的值的两边加上单引号);

#{}占位符是为了获取值,获取的值作用在where语句后insert语句后,update语句后,

#{}获取值,是根据值得名称取值

  1. 参数是基本数据类型,那么在映射的语句中可以不写paramterType,#{}中的参数名也可以随意写
  2. 参数是自定义类型,那么必须填paramterType,#{}中的名称是自定义类型的属性名,该属性有对应的get方法。如果没有get方法,那么会根据反射去获取该类型的值,如果找不到,那么ReflectionException异常
  3. 参数可以是map类型,那么#{}中的名称为map的key值即可
9.1.3.2 ${} 占位符

${}是为SQL片段进行占位,将传过来的SQL片段直接拼接在${}占位符所在的位置,不会进行任何的转义处理。(由于是直接将参数拼接在SQL语句中,因此可能会引发SQL注入攻击问题);
${} 占位符可以从properties文件中获取值,也可以作为表名,列名等;

需要注意的是,在传递 ${} 对应的值时,即使只有一个参数,也需要将值存入map集合中!

9.1.3.3 在mybatis中#和$的主要区别
  1. #传入的参数在sql中会进行转义处理(在字符串日期类型的值的两边加上单引号),$传入的参数在sql中直接显示为传入的值。
  2. #方式能够很大程度防止sql注入,$方式无法防止Sql注入
  3. 能用 #{} 的地方就用 #{},不用或少用 ${}
  4. 表名作参数时,必须用 ${}。如:select * from ${tableName}
  5. order by 时,必须用 ${}。如:select * from t_user order by ${columnName}
  6. 使用 ${} 时,要注意何时加或不加单引号,即 和′和 ′{}’

总结:在大多数情况下还是使用#{}占位符,而${}多用于为SQL片段进行占位!

9.2 Spring

9.2.1 什么是Spring?

Spring是分层的JavaSE及JavaEE应用于全栈的轻量级开源框架,以IoC(Inverse Of Control:控制反转/反转控制)和AOP(Aspact Oriented Programming:面向切面编程)为核心,提供了表现层SpringMVC和持久层Spring JDBC以及业务层事务管理等众多模块的企业级应用技术,还能整合开源世界中众多著名的第三方框架和类库,逐渐成为使用最多的JavaEE企业应用开源框架。
SSH(struts2 spring hibernate)
SSM(springmvc spring mybatis)
Spring的本质是管理软件中的对象,即创建对象和维护对象之间的关系

9.2.2 Spring的优势

  1. 方便解耦,简化开发
    通过 Spring提供的 IoC容器,可以将对象间的依赖关系交由Spring进行控制,避免硬编码所造成的过度程序耦合。用户也不必再为较为底层的需求编写代码,可以更专注于上层的应用。
  2. AOP 编程的支持
    通过 Spring的 AOP 功能,方便进行面向切面的编程,许多不容易用传统OOP(Object Oriented Programming:面向对象编程) 实现的功能可以通过 AOP 轻松应付。
  3. 声明式事务的支持
    可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,提高开发效率和质量。
  4. 方便程序的测试
    可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情。
  5. 方便集成各种优秀框架
    Spring可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、Quartz等)的直接支持。
  6. 降低 JavaEE API 的使用难度。
    Spring对 JavaEE API(如 JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些API 的使用难度大为降低。
  7. Spring框架源码是经典学习范例
    Spring的源代码设计精妙、结构清晰、匠心独用,处处体现着大师对Java设计模式灵活运用以及对 Java技术的高深造诣。它的源代码无疑是Java技术的最佳实践的范例。

9.2.3 Spring的架构

Spring 最初的目标就是要整合一切优秀资源,然后对外提供一个统一的服务。Spring 模块构建在核心容器之上,核心容器定义了创建、配置和管理 bean 的方式,如下图所示:
在这里插入图片描述
组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下:
在这里插入图片描述

9.2.4 Spring IOC控制反转

9.2.4.1 什么是控制反转

IOC(Inverse Of Control)控制反转,即,把创建对象的权利交给框架。也就是指将对象的创建、对象的存储、对象的管理交给了spring容器。(spring容器是spring中的一个核心模块,用于管理对象,底层可以理解为是一个map集合)
在这里插入图片描述

9.2.5 Bean对象的单例和多例

9.2.5.1Bean对象的单例和多例概述

在Spring容器中管理的Bean对象的作用域,可以通过scope属性或用相关注解指定其作用域。最常用是singleton(单例)或prototype(多例)。其含义如下:

  1. singleton:单实例,是默认值。这个作用域标识的对象具备全局唯一性。当把一个 bean 定义设置scope为singleton作用域时,那么Spring IOC容器只会创建该bean定义的唯一实例。也就是说,整个Spring IOC容器中只会创建当前类的唯一一个对象。
    这个单一实例会被存储到单例缓存(singleton cache)中,并且所有针对该bean的后续请求和引用都 将返回被缓存的、唯一的这个对象实例。singleton负责对象的创建、初始化、销毁。
  2. prototype:多实例。这个作用域标识的对象每次获取都会创建新的对象。当把一个 bean 定义设置scope为prototype作用域时,Spring IOC容器会在每一次获取当前Bean时,都会产生一个新的Bean实例(相当于new的操作)prototype只负责对象的创建和初始化,不负责销毁。
9.2.5.2 为什么用单实例或多实例?
  • 之所以用单实例,在没有线程安全问题的前提下,没必要每个请求都创建一个对象,这样子既浪费CPU又浪费内存;

  • 之所以用多例,是为了防止并发问题;即一个请求改变了对象的状态(例如,可改变的成员变量),此时对象又处理另一个请求,而之前请求对对象状态的改变导致了对象对另一个请求做了错误的处理;
    用单例和多例的标准只有一个:当对象含有可改变的状态时(更精确的说就是在实际应用中该状态会改变),使用多实例,否则单实例;

在Spring中配置Bean实例是单例还是多例方法是:

  • 单例:
<bean id="user" scope="singleton" class="com.tedu.spring.User"></bean>
  • 多例:
<bean id="user" scope="prototype" class="com.tedu.spring.User"></bean>

9.2.6 Spring DI依赖注入

DI(Dependency Injection)依赖注入 。依赖注入,即组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。
简单来说,所谓的依赖注入其实就是,在创建对象的同时或之后,如何给对象的属性赋值。

9.2.6.1两种注入方式介绍
9.2.6.1.1 Set方式注入
  • 普通属性注入
<!-- 声明User类的bean实例 -->
<bean id="user" class="com.tedu.spring.User">
	<!-- 通过set方式为普通属性赋值 -->
	<property name="name" value="韩少云"></property>
	<property name="age" value="20"></property>
</bean>
  • 对象属性注入
<!-- 声明User类的bean实例 -->
<bean id="user" class="com.tedu.spring.User">
	<!-- 通过set方式为普通属性赋值 -->
	<property name="name" value="韩少云"></property>
	<property name="age" value="20"></property>
	<!-- 通过set方式为对象属性赋值 -->
	<property name="userInfo" ref="userInfo"></property>
</bean>

由于此处是将UserInfo对象作为值赋值给另一个对象的属性,因此ref属性的值,为UserInfo对象bean标签的id值。

9.2.6.1.2 构造方法注入
<bean id="user" class="com.tedu.spring.User">
	<!-- 通过set方式为普通属性赋值
	<property name="name" value="韩少云"></property>
	<property name="age" value="20"></property>
	<property name="userInfo" ref="userInfo"></property>
	 -->
	<!-- 通过构造器中参数为属性赋值 -->
	<constructor-arg name="name" value="马云"></constructor-arg>
	<constructor-arg name="age" value="35"></constructor-arg>
	<constructor-arg name="userInfo" ref="userInfo"></constructor-arg>
</bean>
<!-- 声明UserInfo类的bean实例 -->
<bean id="userInfo" class="com.tedu.spring.UserInfo"></bean>

其中,constructor-arg标签name属性的值必须和构造函数中参数的名字相同! 同样的,普通属性直接通过value注入即可;
对象属性通过ref属性注入。

9.2.7Spring框架管理Bean对象有什么优势

Spring 是一个资源整合框架(Framework),通过spring可将很多资源(自己写的对象或第三方提供的对象,例如连接池等)整合在一起,然后进行科学应用,以便更好的对外提供服务。如图所示:

在这里插入图片描述
在图中,Spring框架可以为由它管理的对象(Bean)提供懒加载策略(对象暂时用不到,则无需加载和实例化),作用域(例如singleton-频繁用时可以考虑内存中只有一份,prototype-使用次数少时可以用时创建,不用时销毁),生命周期方法(更好实现对象的初始化和资源销毁),以实现对象对系统资源的有效使用。同时Spring框架还可以基于用户设计管理对象与对象的依赖关系,以降低对象与对象之间的直接耦合,提高程序的可维护性和可扩展性。,

9.3 设计模式(Design Patterns)

参考文章:Java之美[从菜鸟到高手演变]之设计模式

9.3.1 设计模式的分类

总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

9.3.2 设计模式的六大原则

  1. 开闭原则(Open Close Principle)
    开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

  2. 里氏代换原则(Liskov Substitution Principle)
    里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科

  3. 依赖倒转原则(Dependence Inversion Principle)
    这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。

  4. 接口隔离原则(Interface Segregation Principle)
    这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。

  5. 迪米特法则(最少知道原则)(Demeter Principle)
    为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。

  6. 合成复用原则(Composite Reuse Principle)
    原则是尽量使用合成/聚合的方式,而不是使用继承。

9.3.3 Java的23中设计模式

从这一块开始,我们详细介绍Java中23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析。

9.3.3.1 工厂模式

工厂模式大体分为简单工厂、工厂方法、抽象工厂三种模式

9.3.3.1.1 简单工厂模式( Simple Factory Pattern )

属于创建型模式,又叫做静态工厂方法模式。是由一个工厂对象决定创建出哪一种产品类的实例。实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例。

作用:将“类实例化的操作”与“使用对象的操作”分开,让使用者不用知道具体参数就可以实例化出所需要的“产品”类,从而避免了在客户端代码中显式指定,实现了解耦。

主要角色
工厂:负责实现创建所有实例的内部逻辑,并提供一个外界调用的方法,创建所需的产品对象。
抽象产品:负责描述产品的公共接口
具体产品:描述生产的具体产品。

举个简单易懂的例子:
“假设”有一台饮料机(工厂),可以调出各种口味的饮料(抽象产品),有三个按钮(参数)对应这三种饮料(具体产品)。这时候你可以根据点击按钮来选择你喜欢的饮料。

/**
 *  @ Product.java
 *  抽象产品
 *  描述产品的公共接口
 */
abstract  class Product {
    
    
    //产品介绍
    abstract void intro();
}

/**
 * @ AProduct.java
 * 具体产品A
 * (可以看成是一种饮料:可乐)
 */
public class AProduct extends Product{
    
    
    @Override
    void intro() {
    
    
        System.out.println("可乐");
    }
}

/**
 * @ BProduct.java
 * @具体产品B
 * @(可以看成是一种饮料:奶茶)
 */
public class BProduct extends Product{
    
    
    @Override
    void intro() {
    
    
        System.out.println("奶茶");
    }
}

/**
 * @ CProduct.java
 * 具体产品C
 * (可以看成是一种饮料:咖啡)
 */
public class CProduct extends Product{
    
    
    @Override
    void intro() {
    
    
        System.out.println("咖啡");
    }
}
/**
 * 工厂
 * 负责实现创建所有实例的内部逻辑,并提供一个外界调用的方法,创建所需的产品对象。
 */
public class Factory {
    
    
    /**
     * 供外界调用的方法
     * (可以看成是对外提供的三种按钮)
     * @param type 
     * @return 产品实例
     */
    public static Product getProduct(String type) {
    
    
        switch (type) {
    
    
            case "A":
                return new AProduct();
            case "B":
                return new BProduct();
            case "C":
                return new CProduct();
            default:
                return null;
        }
    }
}

测试:

public class Test {
    
    
    public static void main(String[] args) {
    
    
        //创建具体的工厂
        Factory factory = new Factory();
        //根据传入的参数生产不同的产品实例
        //(按下不同的按钮,获取饮料)
        Product A = Factory.getProduct("A");
        A.intro();
        Product B = Factory.getProduct("B");
        B.intro();
        Product C = Factory.getProduct("C");
        C.intro();
    }
}

在这里插入图片描述
根据例子可以描述为:一个抽象产品类,可以派生出多个具体产品类。一个具体工厂类,通过往此工厂的static方法中传入不同参数,产出不同的具体产品类实例。

优点:将创建使用工作分开,不必关心类对象如何创建,实现了解耦;
缺点:违背“开放 - 关闭原则”,一旦添加新产品就不得不修改工厂类的逻辑,这样就会造成工厂逻辑过于复杂。

9.3.3.1.2 工厂方法模式(Factory Method Pattern)

又称多态工厂模式、虚拟构造器模式,通过定义工厂父类负责定义创建对象的公共接口,而子类则负责生成具体的对象。一种常用的对象创建型设计模式,此模式的核心精神是封装 类中不变的部分。

作用:将类的实例化(具体产品的创建)延迟到工厂类的子类(具体工厂)中完成,即由子类来决定应该实例化(创建)哪一个类。

主要角色
抽象工厂:描述具体工厂的公共接口
具体工厂:描述具体工厂,创建产品的实例,供外界调用
抽象产品:负责描述产品的公共接口
具体产品:描述生产的具体产品

举个简单易懂的例子:
“假设”有各类的饮料机(抽象工厂),可以调出各种的饮料(抽象产品)。但是一类饮料机(具体工厂),只能生产一种饮料(具体产品)。如果你需要喝可乐,就需要买可乐饮料机。

产品:Product.java 、ProductA.java 、ProductB.java

/**
 * @ Product.java 
 *   抽象产品
 */
abstract class Product {
    
    
    //产品介绍
    abstract void intro();
}

/**
 * @ ProductA.java 
 * 具体产品A
 */
public class ProductA extends Product{
    
    
    @Override
    void intro() {
    
    
        System.out.println("饮料A");
    }
}

/**
 * @ ProductB.java 
 * 具体产品B
 */
public class ProductB extends Product{
    
    
    @Override
    void intro() {
    
    
        System.out.println("饮料B");
    }
}

工厂:Factory.java、FactoryA.java 、FactoryB.java

/**
 *  @ Factory.java
 *    抽象工厂
 */
abstract class Factory {
    
    
    //生产产品
    abstract Product getProduct();
}

/**
 * @ FactoryA.java
 * 具体工厂A
 * 负责具体的产品A生产
 */
public class FactoryA extends Factory{
    
    
    @Override
    Product getProduct() {
    
    
        return new ProductA();
    }
}

/**
 * @ FactoryB.java
 * @具体工厂B
 * 负责具体的产品B生产
 */
public class FactoryB extends Factory{
    
    
    @Override
    Product getProduct() {
    
    
        return new ProductB();
    }
}

测试:Test.java

public class Test {
    
    
    public static void main(String[] args) {
    
    
        //创建具体的工厂
        FactoryA factoryA = new FactoryA();
        //生产相对应的产品
        factoryA.getProduct().intro();
        FactoryB factoryB = new FactoryB();
        factoryB.getProduct().intro();
    }
}

根据例子可以描述为:一个抽象产品类,可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类只能创建一个具体产品类的实例。

  • 优点
    符合开-闭原则:新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可
    符合单一职责原则:每个具体工厂类只负责创建对应的产品
  • 缺点
    增加了系统的复杂度:类的个数将成对增加
    增加了系统的抽象性和理解难度
    一个具体工厂只能创建一种具体产品
9.3.3.1.3 抽象工厂模式(Abstract Factory Pattern )

目录
定义:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类;具体的工厂负责实现具体的产品实例。
解决的问题:每个工厂只能创建一类产品(工厂方法模式)

抽象工厂模式与工厂方法模式最大的区别:抽象工厂中每个工厂可以创建多种类的产品;而工厂方法每个工厂只能创建一类

主要对象
抽象工厂:描述具体工厂的公共接口
具体工厂:描述具体工厂,创建产品的实例,供外界调用
抽象产品族:描述抽象产品的公共接口
抽象产品:描述具体产品的公共接口
具体产品:具体产品

举个简单易懂的例子:(找了个不怎么好的比喻,看不懂得可以看相关推荐链接)
“假设”有各类的自动售卖机(抽象工厂),可以出售各类食品(抽象产品族)。
有饮料、零食(抽象产品),比如常见的零食售卖机(具体工厂),出售矿泉水与面包(具体产品)。

产品:Product、ProductA、ProductB、ProductAa、ProductBb

/**
 * @ Product.java
 * 抽象产品族 (食品)
 */
abstract class Product {
    
    
    //产品介绍
    abstract void intro();
}

/**
 * @ ProductA.java
 * 抽象产品  (饮料)
 */
abstract class ProductA extends Product{
    
    
    @Override
    abstract void intro();
}

/**
 * @ ProductB.java
 * 抽象产品  (零食)
 */
abstract class ProductB extends Product{
    
    
    @Override
    abstract void intro();
}

/**
 * @ ProductAa.java
 * 具体产品  (矿泉水)
 */
public  class ProductAa extends ProductA{
    
    
    @Override
    void intro() {
    
    
        System.out.println("矿泉水");
    }
}

/**
 * @ ProductBb.java
 * 抽象产品  (面包)
 */
public class ProductBb extends ProductB{
    
    
    @Override
    void intro() {
    
    
        System.out.println("面包");
    }
}

工厂:Factory.java、FactoryA.java

/**
 * @ Factory.java
 * 抽象工厂
 */
abstract class Factory {
    
    
    //生产饮料
    abstract Product getProductA();
    //生产零食
    abstract Product getProductB();
}

/**
 * @ FactoryA.java
 * 具体工厂A
 * 负责具体的A类产品生产
 */
public class FactoryA extends Factory{
    
    
    @Override
    Product getProductA() {
    
    
        //生产矿泉水
        return new ProductAa();
    }
    @Override
    Product getProductB() {
    
    
        //生产面包
        return new ProductBb();
    }
}

测试:Test.java

public class Test {
    
    
    public static void main(String[] args) {
    
    
        //创建零食售卖机(具体工厂),
        FactoryA factoryA = new FactoryA();
        //获取矿泉水与面包(具体产品)
        factoryA.getProductA().intro();
        factoryA.getProductB().intro();
    }
}

在这里插入图片描述
根据实例可以描述为: 多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。 每个具体工厂类可以创建多个具体产品类的实例。.

  • 优点
    降低耦合
    符合开-闭原则
    符合单一职责原则
    不使用静态工厂方法,可以形成基于继承的等级结构。
  • 缺点:难以扩展新种类产品

9.3.3.2 单例模式

Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。

单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择

  • 优点
  1. 确保对单例类的所有实例化得到的都是相同的一个实例,防止其它对象对自己的实例化。
  2. 避免对象的重复创建和销毁,节约系统资源。
  3. 避免由于操作多个实例导致的逻辑错误。
  4. 避免对共享资源的多重占用
  • 缺点:
  1. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  2. 由于单例模式中无抽象层,单例类扩展困难
  3. 单例类的职责过重,在一定程度上违背了“单一职责原则”
  • 适用场景:
    单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
  1. 需要频繁实例化然后销毁的对象。
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  3. 有状态的工具类对象。
  4. 频繁访问数据库或文件的对象。
  • 应用场景举例:
  1. 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
  2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
  3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
  7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
  8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
  9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.
  • 单例模式要素:
    • 私有构造方法
    • 私有静态引用指向自己实例
    • 以自己实例为返回值的公有静态方法

单例模式有很多种写法,大部分写法都或多或少有一些不足。下面将分别对这几种写法进行介绍。

9.3.3.2.1 饿汉式

类加载时创建实例对象
特点:线程安全,调用效率高,但是不能延时加载
应用场景:小对象(占用内存比较小)

class Singleton01{
    
    
	//private byte[]array=new byte[1024*1024];
	//1.构建方法私有化
	private Singleton01() {
    
    }
	//2.类的内部构建对象
	private static  Singleton01 instance=new Singleton01();
	//3.对外提供对象访问
	public static Singleton01 getInstance() {
    
    
		return instance;
	}
	public static void show() {
    
    }
	public void display() {
    
    }
}

思考:假如show方法调用次数很多,而display方法最后调用或者调用很少,会导致一开始Singleton01对象创建,如果对象很大,会造成资源浪费。

9.3.3.2.2 懒汉式

类的实例何时需要何时创建,
特点:线程安全,调用效率不高,但是能延时加载
应用场景:大对象(占用内存比较多),稀少用;

class Singleton02{
    
    
	//private byte[]array=new byte[1024*1024];
	//1.构建方法私有化
	private Singleton02() {
    
    }
	//2.类的内部构建对象
	private static  Singleton02 instance;
	//3.对外提供对象访问(会有阻塞)
	public synchronized static Singleton02 getInstance() {
    
    
		if(instance==null) {
    
    
			System.out.println("create()");
			instance=new Singleton02();
		}
		return instance;
	}
	public static void show() {
    
    }
	public void display() {
    
    }
}

思考:多线程情况下,需要加锁,如下代码如不加锁,对象会构建两次.加锁多线程会阻塞

	static void doMethod02() {
    
    
		Thread t1=new Thread() {
    
    
		   @Override
		   public void run() {
    
    
			  Singleton02.getInstance();
			  Singleton02.getInstance();
		   }	
		};
		Thread t2=new Thread() {
    
    
			@Override
			public void run() {
    
    
				Singleton02.getInstance();
				Singleton02.getInstance();
			}	
		};
		t1.start();
		t2.start();
	}
 
	public static void main(String[] args) {
    
    
		doMethod02();
	}

输出结果:

create()
create()
9.3.3.2.3 双重校验锁(Double Check Lock)

在懒汉式的基础上减少阻塞,双重验证减少大量阻塞
加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此就有了双重校验锁,先看下它的实现代码。

class Singleton03{
    
    //应用场景:大对象(占用内存比较多),稀少用
	//private byte[]array=new byte[1024*1024];
	//1.构建方法私有化
	private Singleton03() {
    
    }
	//2.类的内部构建对象
	private static Singleton03 instance;
	//3.对外提供对象访问(会有阻塞)
	//3.1多个线程并发访问此方法是否会有线程不会被阻塞?(有的)
	//3.2为什么synchronized内部还要有一次判断?(确保对象只创建1次)
	public static Singleton03 getInstance() {
    
    
		if(instance==null) {
    
    
			synchronized(Singleton03.class) {
    
    
				System.out.println("synchronized");
				if(instance==null) {
    
    //2
					//System.out.println("create()");
					instance=new Singleton03();
					//对象创建过程(开辟内存,初始化属性,调用构造方法,为instance赋值)
				}
			}
		}
		return instance;
	}
}

可以看到上面在同步代码块外多了一层instance为空的判断。由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。因此,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。不过还需要考虑一种情况,假如两个线程A、B,A执行了if (instance = = null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (instance = = null)语句,也就是上面看到的代码2。

我们看到双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?

这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。

这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。

以上就是双重校验锁会失效的原因,不过还好在JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。代码如下:

class Singleton03{
    
    //应用场景:大对象(占用内存比较多),稀少用
	//private byte[]array=new byte[1024*1024];
	//1.构建方法私有化
	private Singleton03() {
    
    }
	//2.类的内部构建对象
	private static volatile Singleton03 instance;
	//3.对外提供对象访问(会有阻塞)
	//3.1多个线程并发访问此方法是否会有线程不会被阻塞?(有的)
	//3.2为什么synchronized内部还要有一次判断?(确保对象只创建1次)
	public static Singleton03 getInstance() {
    
    
		if(instance==null) {
    
    
			synchronized(Singleton03.class) {
    
    
				System.out.println("synchronized");
				if(instance==null) {
    
    
					//System.out.println("create()");
					instance=new Singleton03();
					//对象创建过程(开辟内存,初始化属性,调用构造方法,为instance赋值)
				}
			}
		}
		return instance;
	}
}

volatile关键字的作用?
参考文章:Volatile是如何来保证可见性?
1)保证多线程之间变量的可见性(一个线程修改了此变量的值,其它的立刻可见)
2)禁止指令重排序(JVM内部对指令执行有优化)

9.3.3.2.4 静态内部类实现单例模式

基于内部类实现对象的延迟加载
特点:线程安全,调用效率高,可以延时加载
应用场景:大对象(延迟加载),频繁用
注意:静态内部类中才允许写静态变量,非静态内部类,不允许写静态变量(语法如此)

class Singleton04{
    
    
	private byte[] array=new byte[1024*1024];
	private Singleton04() {
    
    }
	//Singleton04加载时不会加载Inner类
	private static class Inner{
    
    
		private static final Singleton04 instance=new Singleton04();
	}
	//可以频繁访问(没有阻塞)
	public static Singleton04 getInstance() {
    
    
		//基于内部类实现对象的延迟加载
		return Inner.instance;
	}
	public static void show() {
    
    }
	public void display() {
    
    }
}

思考:当调用show方法时,不会导致array变量初始化,也不会导致Inner类加载;通过调用getInstance方法获取实例时,调用Inner.instance,会导致静态内部类Inner被加载,初始化intance,进而执行new Singleton04(),导致array被初始化,实现大对象的延迟加载.

9.3.3.2.5 基于枚举实现

特点:线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用
应用场景:小对象,频繁用

enum Singleton05{
    
    //Singleton05.class
	INSTANCE;//此对象可以延迟加载吗?不可以
	//private byte[] array=new byte[1024*1024];
	public static void show() {
    
    }
}

调用:

	static void doMethod05() {
    
    
		Singleton05.INSTANCE.show();
	}

思考:枚举对象类加载时构建对象,不能做延迟加载,如果是大对象array,在类加载时就占用资源,不合适;所以只适合小对象,频繁使用场景;

如何选用:

单例对象 占用资源少,不需要延时加载,枚举 好于 饿汉

单例对象 占用资源多,需要延时加载,静态内部类 好于 懒汉式

9.4 程序中的耦合和解耦

9.4.1 什么是程序的耦合

耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差(降低耦合性,可以提高其独立性)。耦合性存在于各个领域,而非软件设计中独有的,但是我们只讨论软件工程中的耦合。
总结:在软件工程中,耦合指的就是指对象之间的依赖关系。对象之间的依赖程度越高,耦合度就越高。对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。
降低程序之间的依赖程度,即降低程序之间的耦合度的过程就叫做解耦。
例如:早期的Jdbc操作中,在注册数据库驱动时,为什么采用的是Class.forName的方式,而不是采用DriverManager.registerDriver的方式?

public class TestJdbc {
    
    
  public static void main(String[] args) throws Exception {
    
    
    //1.注册数据库驱动
    // DriverManager.registerDriver( new Driver() );
    Class.forName("com.mysql.jdbc.Driver");
    //2.获取数据库连接
    //3.获取传输器
    //4.发送sql到服务器执行并返回执行结果
    //5.处理结果
    //6.释放资源
  }
}

除了DriverManager.registerDriver会导致驱动注册两次外,更重要的是,如果使用这种方式,JDBC程序就会依赖于数据库的驱动类(MySQL的Driver类),如果后期程序因数据量和性能原因升级到Oracle数据库,就需要修改程序源代码——重新导入新的驱动类,这会增加很多不必要的麻烦!
而是用Class.forName方式注册驱动,这样的好处是Jdbc程序不再依赖具体的驱动类,即使删除(或不导入)mysql驱动包,程序依然可以编译(当然不可能运行,因为运行时肯定需要依赖驱动)。
此时类中仅仅是将mysql驱动类的全限定类名写死在程序中(只是一个字符串),可以将这个字符串提取到配置文件中,后期可以通过修改配置文件(而不用修改程序代码)轻松的替换数据库产品。

9.4.2 工厂模式解耦介绍

在实际开发中可以将三层(表现层、业务层、持久层)的对象都使用配置文件配置起来,当启动服务器加载应用时,可以通过工厂读取配置文件,根据配置文件中的配置将这些对象创建出来,在接下来使用的时候,直接拿过来使用即可。那么,这个负责读取配置文件,根据配置文件创建并返回这些对象的类就是工厂。可以通过【工厂+接口+配置文件】的方式解除程序中的耦合。

9.5 Springmvc参数绑定

当项目中引入springmvc框架后,所有的请求流转将由springmvc进行控制,当客户端发送的请求中包含数据(也就是请求参数)时,那么该如何在controller层获取这些参数呢?

springmvc会自动的将请求中包含的参数和方法的参数进行匹配,也就是说只要保证,请求中的参数名称和方法中的参数名称相对应(另,参数的格式也要正确),在方法中就可以使用这些参数—即请求中的参数。

  • 基本类型参数绑定
    当需要获取客户端发送过来的少量数据时,可以在Controller中声明的方法上,通过声明方法参数对这些参数一个一个进行接收;
  • 包装类型参数绑定
    当需要获取客户端发送过来的多个数据时,可以在Controller中声明的方法上,通过声明方法参数对这些数据一个一个进行接收较麻烦,可以在方法上声明对象类型的参数,通过对这些数据统一进行接收,springmvc会自动将接收过来的参数封装在对象中,具体示例如下:
	/* 2、测试springmvc的包装类型参数绑定
	 * ../testParam2?name=关羽&age=30&addr=北京
	 * 如何获取请求中name、age、addr的参数值?
	 * 提供一个User类,在类中添加和请求参数同名的属性
	 * 底层通过调用setName、setAge、setAddr方法将参数值封装到User对象中 */
	@RequestMapping("/testParam2")
	public String testParam2(User user) {
    
    
		System.out.println( "user.name="+user.getName() );
		System.out.println( "user.age="+user.getAge() );
		System.out.println( "user.addr="+user.getAddr() );
		return "home";
	}
  • 日期类型参数绑定
    在这里插入图片描述
	 /* 3、测试springmvc的日期类型参数绑定
	 * ../testParam3?date=2020-4-10 16:40:39	报400错误,表示参数类型不匹配
	 * ../testParam3?date=2020/4/10 16:40:39
	 * 	如何获取请求中date参数的值?
	 * 	springmvc默认是以斜杠接收日期类型的参数, 如果提交的日期就是以斜杠
	 * 	分隔的, springmvc就可以接收这个日期类型的参数, 否则将无法接收
	 * 	如果提交的日期参数就是以横杠分隔, 也可以修改springmvc默认的接收格式
	 * 	改为以横杠分隔!!
	 */
	@RequestMapping("/testParam3")
	public String testParam3(Date date) {
    
    
		System.out.println( "date="+date );
		return "home";
	}

如果传递给服务器的日期数据是如下格式:
在这里插入图片描述
从图中可以看出,如果日期参数是 yyyy-MM-dd格式(以横杠分隔)就会出现400错误,其实是因为参数格式匹配错误,由于springmvc默认的日期格式是yyyy/MM/dd(以斜杠分隔),因此如果日期参数不是yyyy/MM/dd 格式,就会出现400错误!!
2、解决方案:
在springmvc中,提供了@InitBinder注解,用于指定自定义的日期转换格式,因此,我们只需要在Controller类中添加下面的代码即可,在接受日期类型的参数时,会自动按照自定义的日期格式进行转换。

/* 自定义日期格式转换器
	 * 将springmvc默认以斜杠(/)分隔日期改为以横杠分隔(-)
	 */
	@InitBinder
	public void InitBinder (ServletRequestDataBinder binder){
    
    
		binder.registerCustomEditor(java.util.Date.class, 
			new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), true)
		);
	}

9.6 Springmvc响应数据

  • Model的使用
    当请求发起访问Controller中的方法时,可以通过参数声明,在方法内使用Model。
@RequestMapping("/doorList")
public String doorList(Model model){
    
    }

Model对象实际上是一个Map集合,例如:往model中添加一个属性

model.addAttribute(String name, Object value);

其中,addAttribute方法会将属性保存到request域中,再通过转发将属性数据带到相应的JSP中,通过${}取出并显示。
示例,往Model中添加属性:

@RequestMapping("/testModel")
public String testModel(Model model){
    
    
	/* 往Model添加属性 */
	model.addAttribute("name", "马云");
	model.addAttribute("age", 20);
	return "home";
}

在home.jsp中取出属性并显示:

<body>
	<h1>hello springmvc~~~</h1>
	${
    
     name } <br/>
	${
    
     age }
</body>

猜你喜欢

转载自blog.csdn.net/qianzhitu/article/details/108415347