移动互联笔记一

重新认识面向对象

软件测试

功能性测试

测试名称测试内容
Unit Test单元测试——在最低的功能/参数上验证程序的正确性
Functional Test功能测试——验证模块的功能
Integration Test集成测试——验证几个互相有依赖关系的模块的功能
Scenario Test场景测试——验证几个模块是否能够完成一个用户场景
System Test系统测试——对于整个系统功能的测试
Alpha/Beta Test外部软件测试人员(Alpha/Beta测试员)在实际用户环境中对软件进行全面的测试

非功能测试

测试名称测试内容
Stress/load test测试软件在负载情况下能否正常工作
Performance test测试软件的效能
Accessibility test软件辅助功能测试——测试软件是否向残疾用户提供足够的辅助功能
Localization/Globalization Test本地化/全球化测试
Compatibility Test兼容性测试
Configuration Test配置测试——测试软件在各种配置下能否正常工作
Security Test软件安全性测试

其他类型的测试

测试名称测试内容
Smoke Test“冒烟”——如果测试不通过,则不能进行下一步工作
Build Verification Test验证代码能否顺利地通过构建
Acceptance Test验收测试,为了全面考核某方面功能/特性而做的测试
Regression Test“回归”测试——对一个新的版本,重新运行以往的测试用例,看看新版本和已知的版本相比是否有“退化”(regression)
Ad hoc Test随机进行的、探索性的测试
Bug BashBug大扫荡——全体成员参加的找“小强”活动
Buddy Test伙伴测试——测试人员为开发人员(伙伴)的特定模块作的测试

单元测试

Spring概述

Spring概述

Spring是一个 Java EE 应用开发框架,最初是以 Rod Johnson 编写的《Expert one-on-one:J2EE Design and Development一书的代码为基础发展而来的。
因为J2EE 实在弄得太复杂了,难学难用。所以,Spring最初的设计目标是:简化企业级Java 应用程序的开发。

失败的Java EE(原先的J2EE)
Java EE是一组规范的集合,其技术的核心就是“ EJBEnterprise Java Bean)”及其容器。
1.开发一个EJB 需要实现特定的接口,还有大量的配置文件,弄得配置的工作量比写代码的工作量还要大,一旦配置有错,噩梦开始
2.EJB跑在 EJB 容器 中,而 Java Web (主要是 JSP 和 Servlet )跑在Web容器中,可以想象,如果 Servlet 要调用 EJB ,就意味着 Web 容器调用EJB 容器,这里头的配置实在太烦人了!而且性能不佳,调试复杂。

Rod Johnson设计出了 Spring 的最初版本。比之 J2EE ,这是一个巨大的简化,但相比于其他技术,比如 .NET 和 Python 等, Spring 的早期版本还是太麻烦,于是 Spring 技术持续演化,使用越来越简单。
“简单易用且够用”的技术,才有生命力!

Spring特点:
Spring使用“依赖注入( Dependency Injection )”这种手段来管理各类 Java 资源,降低了各种资源间的耦合。
Spring通过动态代理技术实现了面向切面的编程( AOP Aspect Oriented Programming ),避免了编写大量重复的代码。

Spring技术演化:
在这里插入图片描述
Spring“全家桶”
历经多年发展,Spring 己经发展成为一个“成员众多”的技术家族,在 Java 开发领域占据统治地位,是 Java 生态圈的核心成员。
在这里插入图片描述

Spring Framework概述

Spring Framework是 Spring 技术大厦的根基

Spring Framework(Spring 框架)是整个 Spring 技术家族的基础,主要提供了对 IoC (依赖反转 Inverse Of Control )容器、 AOP (面向切面编程)、数据访问、 Web 开发、消息、测试等相关技术特性的支持。

Spring Framework特点:
Spring使用简单的POJO(Plain Old Java Object,即无特殊要求的普通Java对象)来进行企业级开发。每一个被Spring管理的Java对象都称之为Bean;而Spring提供了一个IoC容器用来初始化Bean对象,解决Bean对象间的依赖管理和对象的使用问题。
经过多年的发展,Spring己经发展成为一个庞大的技术家族,包容诸多的成员,并且成为Java EE开发领域事实上的标准。

基于Spring Framework ,出现了 N 多特定的 Spring 项目模块,使用这些现成的项目模块,能够高效地开发特定的应用。
在这里插入图片描述
左图展示了Spring Framework (5.0 以前版本)的各个组成部分,可以看到它拥有一个分层的组件化架构,并且这些组件可以随着技术的发展而被单独替换掉,遵循开放规范且可方便替换,是 Spring生命力的主要来源。

Spring Core中最重要的两个核心特性
在这里插入图片描述
利用这两个特性,Spring 技术家族的其他成员可以方便地组合各种组件,并且能方便地重用代码,从而轻松地构建出了多种多样的技术框架,在实际开发中得到了广泛的应用。
在这里插入图片描述
在这里插入图片描述
Spring Framework特性
①使用 POJO 进行轻量级和最小侵入式开发。
②通过依赖注入和基于接口编程实现松耦合。
③通过 AOP 和默认约定进行声明式编程,减少模式化的固定而重复的代码。
④Spring 是开放的,能很方便地整合其他开发框架。

Spring Boot概述

从本质上来说,Spring Boot 就是 Spring Framework ,它做了那些没有它你自己也会去做的 Spring Bean 配置。简单地说,就是在早期Spring 技术的“人工劳动”的基础之上,实现了“自动化”配置。
在这里插入图片描述
Spring Boot是当前 Spring 应用开发的主流。新项目不要再使用早期的 Spring 技术(比如使用 XML 定义 Bean )。
在这里插入图片描述
Spring Boot设计原则:约定优先于配置
在这里插入图片描述
Spring Boot的特点-1
Spring Boot提供了一些预先组织好的“起步依赖(spring-bootstarter)”,从而使开发者不再需要自行维护复杂的组件(jar包)依赖,而是只需要声明“我需要什么功能”就行了。
Spring Boot让Spring应用开发,从早期的以“组件为中心”,转换为“以功能为中心”。
(Spring Boot 2.X与1.X相比有许多变化,并且只支持JDK 8及以上版本,新项目不要再使用1.X版本。)

Spring Boot Starter
在这里插入图片描述
Spring Boot的特点 2
Spring Boot在应用程序里嵌入了一个 Servlet 容器(Tomcat、Jetty或Undertow),可独立运行,无需部署到外部的Servlet容器中。
Spring Boot 2.0还提供了支持响应式编程特性的容器(默认为Netty ),因此,它应用就是一个 jar 包,可以直接使用 java jar 命令来运行。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Spring Boot的特点-3
Spring Boot提供了一整套工具,称为Spring Boot Actuator,可以用于监控Spring Boot应用程序运行的状态。
Spring技术家族的其他成员(比如Spring MVC),基本上都针对Spring Boot进行了调整或重写,并且增加了新的成员,比如Web Flux。
Spring Boot还是Spring Cloud技术的基础,单个的微服务,可以使用Spring Boot来开发。
!要学习Spring,应该从Spring Boot起步!
在这里插入图片描述
学习指南:
学习Spring技术,要先学Spring Framework,再学Spring Boot,Spring Boot学好之后,才能学Spring Cloud及其它Spring技术。这个顺序不能弄反了。

Spring Boot

创建Spring Boot

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.jinuxliang.first_springboot_app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.jinuxliang.first_springboot_app.controllers;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 标准的Spring Boot REST控制器
 */
@RestController
@RequestMapping("/spring")
public class MySpringBootController {
    @RequestMapping("/hello")
    public String hello(){
        return "Hello,Spring Boot!";
    }
}
package com.jinuxliang.first_springboot_app.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 标准的Spring MVC控制器
 */
@Controller
@RequestMapping("/springWeb")
@ResponseBody
public class MySpringWebController {
    @RequestMapping("/hello")
    public String hello(){
        return "<h2>Hello,Spring Boot!";
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Spring Bean的依赖注入

依赖注入与控制反转

对象间的依赖关系与组合:
一个对象通常需要“依赖 ”其他对象所提供的各种服务。在实际开发中,这通常是使用对象组合 来实现的。
要实现对象组合,就必须创建出所有用到的对象,然后依据它们之间的依赖关系将它们关联起来。比如 A 用到 B ,就要 new 出 A 和 B 两个对象,再让 A 对象关联上 B 对象(通常是让 A 对象的某个字段引用B对象)。

理解“依赖注入”的概念:
“依赖注入”指的是不让对象本身自己解决自己的依赖(即自己new出自己所需要的对象),而是让一个专门的外部对象(称为IoC容器)创建好相关对象,再将它们提供(即“注入”)给需要用到它的对象,比如,IoC容器发现A“依赖”于B,于是它在new A时,会自动再new一个B对象,并且设置A的相应字段引用这个B对象,这样一来,A对象自己就不需写代码去创建B对象了。
完成这些所有工作之后,IoC容器再把它们Return出去交给外界使用。
IoC容器可以看成是一个“对象工厂”,它接收外部“订单”,按照订单要求生产“产品(即对象)”,以满足用户需求。

理解“IoC”与“DI”间的关系:
“控制反转(IoC:Inversion of Control)”和依赖注入DI:Dependency Injection)”,前者是特性 即:我只管用,便不负责创建和管理对象,这事由外部负责),后者是技术实现手段即:IOC这个特性,具体应该怎样用Java 实现呢?)。
简单地说:控制反转是通过依赖注入实现的。

Spring Bean与 Spring IoC 容器

Spring中把每一个需要管理的对象称为 Spring Bean (简称 Bean)而 Spring 管理这些 Bean 的容器,被称为“ Spring IoC 容器”。
IoC容器的功能:扫描和识别 Bean 之间的依赖关系,创建和装配 Bean。
图解:
在这里插入图片描述
重要概念:

  • POJO - Plain old Java object
  • JavaBeans - Simple objects with only getters and setters
  • Spring beans - POJOs configured in the application context
  • DTO - Bean used to move state between layers

外界通过IoC容器获取Bean容器的两种方式:
按类型:<T> T egtBean(Class<T> requiredType)
按名称:Object getBean(String name)

示例

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

import org.springframework.stereotype.Component;

//使用@Component注解,可以定义一个Bean
@Component
public class MyAnnotationBean {
}
//对于没有注解的普通Java类
//则需要配合使用Configuration类来定义
//Spring的IoC容器才能识别它
public class POJOBean {
}
import com.jinxuliang.dependency_inject.bean.POJOBean;
import com.jinxuliang.dependency_inject.bean.POJOBeanContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
//使用@ComponentScan指定额外的包扫描组件
@ComponentScan({"com.jinxuliang.dependency_inject.other"})
public class MyBeanConfig
{
    //使用@Bean这个注解,
    //定义那些没有@Component注解的普通Java类
    @Bean
    POJOBean pojoBean()
    {
        return new POJOBean();
    }
    //将POJOBean通过构造方法注入到POJOBeanContainer中
    @Bean(name = "beanContainer")
    POJOBeanContainer pojoBeanContainer()
    {
        return new POJOBeanContainer(pojoBean());
    }
}

在这里插入图片描述
在这里插入图片描述

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

//展示@Value的用法
@Component
public class AtValueBean {

    //可以使用@Value指定默认值
    @Value("Hello")
    private String info;

    //从application.properties中提取值
    @Value(value="${message}")
    private String message;

    @Override
    public String toString() {
        return "AtValueBean{" +
                "info='" + info + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}

在这里插入图片描述

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

//可以使用@Autowired构建组合对象
@Component
public class MyContainerBean {

    //直接附加在字段上
    @Autowired
    MyAnnotationBean annotationBean;

    private POJOBean pojoBean=null;

    //附加在字段的setter方法上
    @Autowired
    public void setPojoBean(POJOBean pojoBean) {
        this.pojoBean = pojoBean;
    }

    @Override
    public String toString() {
        return "MyContainerBean{" +
                "annotationBean=" + annotationBean +
                ", pojoBean=" + pojoBean +
                '}';
    }
}

在这里插入图片描述

public interface CustomerRepository {
}
import org.springframework.stereotype.Repository;

//定义一个Bean,实现特定的接口
@Repository
public class CustomerRepositoryImpl implements CustomerRepository {
}
import com.jinxuliang.dependency_inject.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {

    private CustomerRepository repository;

    //使用构造方法进行注入
    @Autowired
    public CustomerService(CustomerRepository repository) {
        this.repository = repository;
    }

    @Override
    public String toString() {
        return "CustomerService{" +
                "repository=" + repository +
                '}';
    }
}

在这里插入图片描述

public interface IOptionalBean {
}
//如果注释掉@Component,则UserOptionalBean中的相应字段
//将得到一个null
//@Component
public class OptionalBean implements IOptionalBean {
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class UseOptionalBean {
    //如果没有实现IOptional接口的Bean,则@Autowired将会失败
    @Autowired(required = false)
    private IOptionalBean optionalBean;

    @Override
    public String toString() {
        return "UseOptionalBean{" +
                "optionalBean=" + optionalBean +
                '}';
    }
}

在这里插入图片描述

//对于没有注解的普通Java类
//则需要配合使用Configuration类来定义
//Spring的IoC容器才能识别它
public class POJOBean {
}
//这是一个普通的Java类,不包容任何与依赖注入
//相关的代码
public class POJOBeanContainer {

    private POJOBean pojoBean;

    public POJOBeanContainer(POJOBean pojoBean) {
        this.pojoBean = pojoBean;
    }

    @Override
    public String toString() {
        return "POJOBeanContainer{" +
                "pojoBean=" + pojoBean +
                '}';
    }
}
@Configuration
//使用@ComponentScan指定额外的包扫描组件
@ComponentScan({"com.jinxuliang.dependency_inject.other"})
public class MyBeanConfig
{
    //使用@Bean这个注解,
    //定义那些没有@Component注解的普通Java类
    @Bean
    POJOBean pojoBean()
    {
        return new POJOBean();
    }
    //将POJOBean通过构造方法注入到POJOBeanContainer中
    @Bean(name = "beanContainer")
    POJOBeanContainer pojoBeanContainer()
    {
        return new POJOBeanContainer(pojoBean());
    }
}
//通过名字实现Bean的实例化
POJOBeanContainer beanContainer= (POJOBeanContainer) context
        .getBean("beanContainer");
System.out.println(beanContainer);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

//当一个Bean需要访问容器中的其它Bean,或者需要访问外部资源时,
//可以让其实现ApplicationContextAware,从而获取对外部ApplicationContext的引用
@Component
public class ContextAwareBean implements ApplicationContextAware
{

    private ApplicationContext context;

    public ApplicationContext getContext()
    {
        return context;
    }

    //此方法将会在实例化Bean时由Spring Framework自动调用,从而将一个ApplicationContext
    //对象注入到Bean中
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
    {
        context = applicationContext;
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.jinxuliang.dependency_inject;

import com.jinxuliang.dependency_inject.bean.*;
import com.jinxuliang.dependency_inject.controller.BeanServiceController;
import com.jinxuliang.dependency_inject.other.MyOtherClass;
import com.jinxuliang.dependency_inject.service.CustomerService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class DependencyInjectApplication {

	public static void main(String[] args) {

	    //获取Spring的IoC容器
		ApplicationContext context= SpringApplication.run(
		        DependencyInjectApplication.class, args);

		//获取Bean的实例
		MyAnnotationBean myAnnotationBean = context.getBean(MyAnnotationBean.class);
		System.out.println(myAnnotationBean);

		POJOBean pojoBean=context.getBean(POJOBean.class);
		System.out.println(pojoBean);

        MyContainerBean containerBean=context.getBean(MyContainerBean.class);
        System.out.println(containerBean);

        CustomerService customerService=context.getBean(CustomerService.class);
        System.out.println(customerService);

        //通过名字实现Bean的实例化
        POJOBeanContainer beanContainer= (POJOBeanContainer) context
                .getBean("beanContainer");
        System.out.println(beanContainer);


		System.out.println("扫描获取其他包中的组件");
		MyOtherClass myOtherClass=context.getBean(MyOtherClass.class);
		System.out.println(myOtherClass);

		//由于ContextAwareBean实现了ApplicationContextAware接口,所以,
		//IoC容器在实例化ContextAwareBean时,会自动地将ApplicationContext注入进去
		ContextAwareBean contextAwareBean=context.getBean(ContextAwareBean.class);
		//检测Bean内部的ApplicationContext与外部的ApplicationContext本质上是一回事
		System.out.println(contextAwareBean.getContext()==context);

		//按名字实例化Bean
		BeanServiceController controller=context.getBean(BeanServiceController.class);
		System.out.println(controller);

		System.out.println("测试@Value的用法");
		AtValueBean atValueBean=context.getBean(AtValueBean.class);
		System.out.println(atValueBean);

		UseOptionalBean useOptionalBean=context.getBean(UseOptionalBean.class);
		System.out.println(useOptionalBean);
	}
}
import org.springframework.stereotype.Component;

@Component
public class MyOtherClass {
    @Override
    public String toString() {
        return "MyOtherClass{} in com.jinxuliang.dependency_inject.other package";
    }
}
import com.jinxuliang.dependency_inject.service.IBeanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;

@Controller
public class BeanServiceController {
    @Autowired()
    @Qualifier("beanServiceA")
    IBeanService serviceA;

    @Autowired()
    @Qualifier("beanServiceB")
    IBeanService serviceB;

    //如果取消以下注释,则无法注入,因为实现IBeanService的有两个Bean
//    @Autowired
//    IBeanService service;


    @Override
    public String toString() {
        return "BeanServiceController{" +
                "serviceA=" + serviceA +
                ", serviceB=" + serviceB +
                '}';
    }
}
import org.springframework.stereotype.Service;

@Service
public class BeanServiceA implements IBeanService {
}
import org.springframework.stereotype.Service;

@Service
public class BeanServiceB implements IBeanService {
}
//它有两个实现类
public interface IBeanService {
}

Spring Bean生命周期

Spring所管理的对象,称为“ Spring Bean ”,它与普通 Java 对象的区别,在于它的创建是由 Spring 框架所负责的。
Spring提供了几种典型的对象创建策略,称为“ Scope ”,可以很方便地配置特定类对象的创建策略。

Bean的Scope
Scope决定了 Spring 容器如何新建和管理 Bean 的实例,分为 5 种。
一、适合于所有项目的Scope
1.Singleton :一个 Spring 容器中只有一个 Bean 的实例,此为 Spring的默认配置,全容器共享一个实例。
2.Prototype :每次调用新建一个 Bean 的实例。
二、仅适合于Web 项目:
1.Request Web 项目中,为每一个 Http Request 新建一个 Bean 实例。
2.Session Web 项目中,为每一个 Http Session 新建一个 Bean 实例。
3.GlobalSession :仅在 portal 应用(现在很少用的技术)中有用。

Singleton示例

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
//设定Bean的Scope是Singleton
@Component
@Scope("singleton")
public class Singleton {
}
//测试代码
private static void testSingletonBean(ApplicationContext context) {
    //Singleton模式,始终只有一个实例
    System.out.println("Singleton模式的两个Bean");
    Singleton singleton1 = context.getBean(Singleton.class);
    Singleton singleton2 = context.getBean(Singleton.class);
    System.out.println(singleton1 == singleton2); //输出:true
}

Prototype示例

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class Prototype {
}
private static void testPrototypeBean(ApplicationContext context) {
    //Prototype Scope模式,每次请求都实例化一个对象。
    System.out.println("Prototype模式的两个Bean");
    Prototype prototype1 = context.getBean(Prototype.class);
    Prototype prototype2 = context.getBean(Prototype.class);
    System.out.println(prototype1 == prototype2); //输出:false
}

Bean的初始化和销毁

在我们实际开发的时候,经常会需要在Bean 在使用之前或者之后做些必要的
操作,有两种方式可以做到这一点:使用注解方式、使用配置类方式。

使用注解:

import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

//使用注解定义初始化与销毁时自动调用的方法
@Component
public class InitAndDestoryAnnotationBean {

	//对象构造完毕时调用
    @PostConstruct
    public void init(){
        System.out.println("InitAndDestoryAnnotationBean's PostConstruct method.");
    }

	//对象销毁前调用
    @PreDestroy
    public void destory(){
        System.out.println("InitAndDestoryAnnotationBean's PreDestroy method.");
    }
}

使用配置类:

//这是一个普通的POJO类
//希望在其初始化时,调用init()方法
//在其销毁时,调用destory()方法
public class InitAndDestoryBean {
    public void init(){
        System.out.println("InitAndDestoryBean's init method.");
    }

    public void destory(){
        System.out.println("InitAndDestoryBean's destory method.");
    }
}
import com.jinxuliang.beanscope.beans.InitAndDestoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig {
    //指定POJO类作为Bean的初始化和销毁方法
    @Bean(initMethod = "init", destroyMethod = "destory")
    InitAndDestoryBean initAndDestoryBean() {
        return new InitAndDestoryBean();
    }
}

在这里插入图片描述
在这里插入图片描述

import com.jinxuliang.beanscope.beans.InitAndDestoryAnnotationBean;
import com.jinxuliang.beanscope.beans.InitAndDestoryBean;
import com.jinxuliang.beanscope.beans.Prototype;
import com.jinxuliang.beanscope.beans.Singleton;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
public class BeanscopeApplication {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(BeanscopeApplication.class, args);
        //演示BeanScope的作用
        //testSingletonBean(context);
        //testPrototypeBean(context);

		//System.out.println("指定Bean的初始化和销毁方法");
		//InitAndDestoryBean initAndDestoryBean = context.getBean(InitAndDestoryBean.class);
		//System.out.println(initAndDestoryBean);
		//
		//InitAndDestoryAnnotationBean initAndDestoryAnnotationBean = context.getBean(InitAndDestoryAnnotationBean.class);
		//System.out.println(initAndDestoryAnnotationBean);
    }

    private static void testPrototypeBean(ApplicationContext context) {
        //Prototype Scope模式,每次请求都实例化一个对象。
        System.out.println("Prototype模式的两个Bean");
        Prototype prototype1 = context.getBean(Prototype.class);
        Prototype prototype2 = context.getBean(Prototype.class);
        System.out.println(prototype1 == prototype2); //输出:false
    }

    private static void testSingletonBean(ApplicationContext context) {
        //Singleton模式,始终只有一个实例
        System.out.println("Singleton模式的两个Bean");
        Singleton singleton1 = context.getBean(Singleton.class);
        Singleton singleton2 = context.getBean(Singleton.class);
        System.out.println(singleton1 == singleton2); //输出:true
    }
}

Spring Boot项目配置

发展足迹:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Spring Boot项目初始化过程:
在这里插入图片描述

读入外部文件中的配置参数

Spring Boot允许使用properties文件或者命令行参数作为外部配置。
在SpringBoot里,我们只需在application.properties定义属性,直接使用@Value注入即可。
Spring Boot还提供了基于类型安全的配置方式,通过@ConfigurationProperties将properties属性和一个Bean及其属性关联,从而实现类型安全的配置。
Spring Boot通常使用application.properties文件存放配置参数,但也可以使用另外一种yaml格式的配置文件,其文件名改为:application.yaml

application.properties

jxl.info='hello,spring boot'
jxl.counter=1

spring.profiles.active=DEV

test.properties

book.author=jxl
book.name=spring boot

test.txt

this is a string in file named of test.txt.

在这里插入图片描述

import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;

import java.io.IOException;
import java.nio.charset.Charset;

@Configuration
//指明配置文件所在位置,其内容会被抽取出来,放到环境变量中
@PropertySource("classpath:test.properties")
public class ELConfig {

    //注入普通字符串
    @Value("this is a string constant")
    String normal;

    //注入操作系统属性
    @Value("#{systemProperties['os.name']}")
    String osName;

    //注入表达式结果
    @Value("#{T(java.lang.Math).random() *100}")
    double randomNumber;

    //注入文件资源
    @Value("classpath:test.txt")
    private Resource testFile;

    //注入自定义properties文件中的值
    @Value("${book.name}")
    private String bookName;

    //注入环境变量(会自动地从test.properties中提取数据)
    @Autowired
    Environment environment;

    public void printFields() throws IOException {
        System.out.println(normal);
        System.out.println(osName);
        System.out.println(randomNumber);
        System.out.println(IOUtils.toString(testFile.getInputStream(),
                Charset.forName("UTF-8")));
        System.out.println(bookName);
        System.out.println(environment.getProperty("book.author"));
    }
}
//演示Spring表达式
private static void testSpringEL(ApplicationContext context) throws IOException {
	ELConfig config=context.getBean(ELConfig.class);
	config.printFields();
}

在这里插入图片描述

强类型的配置方式(自动创建配置对象)

通过ConfigurationProperties 加载 properties 文件内的配置,通过 prefix 属性指定properties 的配置的前缀,必要时,也可以通过 locations 指定 properties 文件的位置,例如:

@ConfigurationProperties(prefix="jxl",location={"classpath:config/author.properties"}
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

//从application.properties中加载数据,
//初始化Bean的相应属性
@Component
@ConfigurationProperties(prefix = "jxl")
public class MyProperties {
    private String info;
    private int counter;

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

    public int getCounter() {
        return counter;
    }

    public void setCounter(int counter) {
        this.counter = counter;
    }

    @Override
    public String toString() {
        return "MyProperties{" +
                "info='" + info + '\'' +
                ", counter=" + counter +
                '}';
    }
}
<!--允许使用@ConfigurationProperties-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-configuration-processor -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<version>2.4.0</version>
</dependency>

在这里插入图片描述

//测试从application.properties中提取信息构建相应的配置对象
private static void testExtractInfoFromPropertyFileByPrefix(ApplicationContext context) {
	MyProperties myProperties=context.getBean(MyProperties.class);
	System.out.println(myProperties);
}

在这里插入图片描述

使用Profile

Profile是Spring 用来针对不同的环境对不同的配置提供支持的,全局 Profile 配置使用 application - { profile 名字 }.properties(如 application-prod.properties
通过在application.properties中设置spring.profiles.active=profile名字来指定活动的Profile。

//一个用于展示基于Profile选择不同Bean实例化而设计的接口
public interface IProfileBean {
}
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

//开发阶段使用的Bean
@Component
@Profile("DEV")
public class DevBean implements IProfileBean {
}
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

//发布阶段使用的Bean
@Component
@Profile("PROD")
public class ProdBean implements IProfileBean {
}

在这里插入图片描述

引入XML配置Bean

尽管Spring Boot建议使用注解和扫描配置Bean,但是同样地,它并不拒绝使用XML配置Bean,因为有些框架(如Dubbo)是基于Spring 的XML方式进行开发的。
这时,我们可以在Spring配置类中使用@ImportResource注解将放在外部XML配置文件中的Bean设置也一并导入,如下所示:

@Configuration
@ComponentScan(basePackages = "com.jinxuliang.*")
@ImportResource(value = {"classpath:spring other.xml"})
public class AppConfig{
	......
}

自动配置与条件注解

默认情况下,SpringBoot会启用自动配置,依据程序员在pom.xml中声明的项目依赖,或者是在application.properties中的设置参数,或者是命令行参数等地方提取出相应的信息,进行自动配置。
在多数情况下,这种自动配置能满足需求,举个例子:如果SpringBoot在应用程序的Classpath里发现有H2数据库的库,那么它就自动配置一个嵌入式H2数据库。如果在Classpath里发现了JdbcTemplate,那么它还会为你配置一个JdbcTemplate的Bean。配置好之后,你就可以在需要用到它们的地方,使用Autowired等方式注入。
appliction.properties中设置:debug=true可以看到Springboot到底进行了哪些配置。
在特定的情况下,如果自动配置不能满足需求,我们还可以自定义相应的组件,利用SpringBoot的自动配置机制完成特定的配置工作。

@Conditional注解的用途
@Conditional根据满足某一个特定条件创建一个特定的Bean。比方说,当某一个jar包在一个类路径下的时候,自动配置一个或多个Bean;或者只有某个Bean被创建才会创建另外一个Bean。总的来说,就是根据特定条件来控制Bean的创建行为,这样我们可以利用这个特性进行一些自动的配置。下面举一个例子,如何从application.properties中提取特定的配置参数,然后使用条件注解装载不同的组件
在这里插入图片描述
定义数据存取组件:

import java.util.List;

//数据存取组件要实现的接口
public interface UserDAO {
    List<String> getAllUserNames();
}
import java.util.Arrays;
import java.util.List;

//模拟从MongoDB数据库中提取数据
public class MongoUserDAO implements UserDAO {
    @Override
    public List<String> getAllUserNames() {
        System.out.println("**** Getting usernames from MongoDB *****");
        return Arrays.asList("Bond","James","Bond");
    }
}
import java.util.Arrays;
import java.util.List;

//模拟使用JDBC从关系型数据库中提取数据
public class JdbcUserDAO implements UserDAO {
    @Override
    public List<String> getAllUserNames() {
        System.out.println("**** Getting usernames from RDBMS *****");
        return Arrays.asList("Jim","John","Rob");
    }
}

定义Condition对象

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

//确认在运行环境中是否有dbType属性,并且其值为“MONGODB”
public class MongoDBDatabaseTypeCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext,
                           AnnotatedTypeMetadata metadata) {
        String enabledDBType = conditionContext.getEnvironment()
                .getProperty("dbType");
        return (enabledDBType != null &&
                enabledDBType.equalsIgnoreCase("MONGODB"));
    }
}

类似地,定义MySQLDatabaseTypeCondition类,用于检测dbType==MYSQL。

添加一个配置类

//指定不同的条件,实例化不同的数据存取组件
@Configuration
public class AppConfig {
    @Bean
    @Conditional(MySQLDatabaseTypeCondition.class)
    public UserDAO jdbcUserDAO(){
        return new JdbcUserDAO();
    }
    @Bean
    @Conditional(MongoDBDatabaseTypeCondition.class)
    public UserDAO mongoUserDAO(){
        return new MongoUserDAO();
    }
}

在这里插入图片描述

配置的优先级

可以将对SpringBoot 的行为可以进行干预的配置方式划分为几类:
1.命令行参数(CommandLineArgs)
2.系统环境变量(EnvironmentVariables)
3.位于文件系统中的配置文件
4.位于 classpath 中的配置文件
5.固化到代码中的配置项
以上几种方式按照优先级从高到低排列,高优先级方式提供的配置项可以覆盖或者优先生效,比如通过命令行参数传入的配置项会覆盖通过环境变量传入的同一配置项,当然也会覆盖其他后面几种方式给出的同一配置。
在这里插入图片描述

多profile配置

不同的运行环境
在这里插入图片描述
不同的环境,对应不同的profile
在这里插入图片描述

SpringBoot MVC开发经典Web网站

支持的引擎:
在这里插入图片描述
Thymeleaf的项目依赖:

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

在这里插入图片描述

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/home")
public class HomeController {

    //index方法的参数,Spring IoC容器会自动注入进来
    @RequestMapping("/index")
    public  String index(Model model){
        //向模板文件传送信息
        model.addAttribute("message","Hello Spring MVC");
        //模板文件位置/resources/templates/home/index.html
        return "/home/index";
    }
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Spring MVC视图引擎示例</title>
</head>
<body style="text-align: center">
    <h2 th:text="${message}"></h2>
</body>
</html>

Spring Boot静态文件保存位置
Thymleleaf的模板文件被视为静态资源,只不过它比较特殊,与普通的html图片,js代表等常规静态资源不一样,它需要经过模板引擎的处理之后再传给客户端。
SpringBoot将放置在以下文件夹中的文件视为静态文件:

•/static
•/public
•/resources
•/METAINF/resources

默认情况下,Thymeleaf从classpath:/templates/处加载视图模板。

控制器与模板之间的信息交换
在这里插入图片描述
调用Model对象的addAttribute()方法,可以将数据(需要指定一个标识)传给视图,视图中通过“${数据标识}”取出这个数据,填充,最终得到HTML网页。

前一个例子展示了最简单的MVC架构,仅有一个页面。在实际开发中,一个经典的Web网站通常包容有多个页面,并且当用户回发时,后一个页面往往需显示前一个页中的相关信息。下面我们通过一个典型的“创建用户”例子,介绍如何使用SpringBoot MVC来开发这样的程序。
在这里插入图片描述

public class User
{
    private String name;

    public String getName()
    {
        return name;
    }

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

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

    @Override
    public String toString()
    {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

在这里插入图片描述

import com.example.thymleafdemo.model.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MyController
{

    @GetMapping("/")
    public String index(Model model)
    {
        model.addAttribute("message",
                "从控制器传给thymeleaf视图的信息。");
        return "index";
    }

    @GetMapping(value = "/create")
    public String userInput(Model model)
    {
        var user = new User("testUser");
        model.addAttribute("user", user);
        return "userinput";
    }

    @PostMapping(value = "/create")
    public String userPost(User user, Model model)
    {
        model.addAttribute("user", user);
        return "result";
    }
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
    <link rel='stylesheet' href='/webjars/bootstrap/4.5.3/css/bootstrap.min.css'>
    <link rel="stylesheet" type="text/css" href="mysite.css">
</head>
<body class="container text-center">
    <h2>Spring Boot MVC 视图示例</h2>
    <div class="text-info" th:text="${message}"></div>
    <div class="m-2">
        <a href="/create" class="btn btn-primary">创建用户</a>
    </div>
</body>
</html>

在这里插入图片描述

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel='stylesheet' href='/webjars/bootstrap/4.5.3/css/bootstrap.min.css'>
    <link rel="stylesheet" type="text/css" href="mysite.css">
</head>
<body class="container text-center">
    <h2>创建用户</h2>
    <form action="create" method="post" th:object="${user}">
        <label for="username">用户姓名</label>
        <input id="username" type="text" placeholder="请输入用户姓名"
               th:field="${user.name}">
        <button type="submit" class="btn btn-outline-primary">确定</button>
    </form>
</body>
</html>

在这里插入图片描述

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Result</title>
    <link rel='stylesheet' href='/webjars/bootstrap/4.5.3/css/bootstrap.min.css'>
    <link rel="stylesheet" type="text/css" href="mysite.css">
</head>
<body class="container text-center">
    <h2>提交上来的用户数据</h2>
    <div>
        <span class="font-weight-bold mr-3">用户姓名:</span>
        <span class="text-primary" th:text="${user.name}"></span>
    </div>
</body>
</html>
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ThymleafDemoApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(ThymleafDemoApplication.class, args);
    }
}
body{
    background-color:azure;
}

Thymeleaf官网
在传统的Web网站中,用户需要频繁地Post页面,才能看到新的内容,所有页面的内容,都是在Server端生成的。本示例所展示的编程技巧,可用于开发这种经典的Web网站。现代网站开发中,Web技术有了新的进展,现在我们多采用单页面应用(SPA:SinglePageApplication)架构,使用JavaScript代码发出AJAX请求实现页面的局部刷新。进入移动互联时代之后,Web开发技术更进一步发展到了“前后端分离”模式,WebServer不再负责生成HTML网页,这一模式下的开发技术,后面介绍。

Spring Data

Spring JDBC

Spring提供了一个 JdbcTemplate 对 JDBC 进行了封装,配合 Spring Boot 的自动配置功能,能比较好地消除直接使用原生 JDBC 所带来的冗余代码。
在这里插入图片描述
在这里插入图片描述
项目配置文件:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

#启用H2数据库的Web控制台程序
spring.h2.console.enabled=true
spring.h2.console.path=/h2

#设定h2的文件名为./mydb,保存于项目根文件夹下
#如果设定为:~/mydb,则保存于操作系统的用户文件夹,比如,
#C:\Users\JinXu
#h2数据库文件扩展名默认为.mv.db
spring.datasource.url=jdbc:h2:file:./mydb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
使用H2的Web控制台用SQL命令创建完User表之后,可以看到在项目根目录下有一个mydb.mv.db 文件生成,现在就可以编写测试代码访问数据库了。
在这里插入图片描述
H2数据库官网
在这里插入图片描述
在这里插入图片描述

public class User
{
    private Integer id;
    private String name;
    private Integer age;
    private String gender;

    @Override
    public String toString()
    {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                '}';
    }

    //region "getter和setter方法"
    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    public String getName()
    {
        return name;
    }

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

    public Integer getAge()
    {
        return age;
    }

    public void setAge(Integer age)
    {
        this.age = age;
    }

    public String getGender()
    {
        return gender;
    }

    public void setGender(String gender)
    {
        this.gender = gender;
    }
    //endregion
}

在这里插入图片描述

import com.jinxuliang.springboot2jdbcdemo.model.User;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Random;

public class DaoHelper {

    //从ResultSet中提取数据,创建一个User对象
    public static User fillUser(ResultSet rs)
            throws SQLException {
        if (rs == null) {
            return null;
        }
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setAge(rs.getInt("age"));
        user.setGender(rs.getString("gender"));
        user.setName(rs.getString("name"));
        return user;
    }

    //用于创建一个示例的用户数据,以便进行测试
    public static User createUserObj() {
        Random random = new Random();
        int ranValue = random.nextInt(100);
        User user = new User();
        user.setName("user" + ranValue);
        user.setGender(ranValue % 2 == 0 ? "男" : "女");
        user.setAge(ranValue);
        return user;
    }
}

在这里插入图片描述

import com.jinxuliang.springboot2jdbcdemo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

//利用Spring IoC容器,实现UserDao对象的自动创建
@Component
public class UserDao {
    //自动注入Spring所提供的JdbcTemplate对象
    @Autowired
    private JdbcTemplate jdbcTemplate;

    //提取所有的用户数据
    //使用JdbcTemplate查询数据
    public List<User> getAllUsers() {
        String sql = "select * from user";
        final List<User> users = new ArrayList<>();
        //从数据库中提取记录集,针对每条记录,调用RowCallbackHandler回调函数
        jdbcTemplate.query(sql, new RowCallbackHandler() {
            public void processRow(ResultSet resultSet) throws SQLException {
                //调用辅助函数创建User对象,并将其加入到对象集合中
                users.add(DaoHelper.fillUser(resultSet));
            }
        });
        //上面那句可以使用Lambda表达式进行简化,因为RowCallbackHandler是个函数型接口
        //jdbcTemplate.query(sql,rs->{
        //    users.add(DaoHelper.fillUser(rs));
        //});
        return users;
    }

    //保存一个用户对象
    //使用JdbcTemplate保存数据UserDao.
    public int save(User user) {
        //SQL命令字符串中的”?“代表参数
        String sql = "insert into user(name,age,gender) values(?,?,?)";
        //返回被影响的记录条数
        int result = jdbcTemplate.update(sql, user.getName(),
                user.getAge(), user.getGender());
        return result;
    }
}

测试CRUD

import com.jinxuliang.springboot2jdbcdemo.dao.DaoHelper;
import com.jinxuliang.springboot2jdbcdemo.dao.UserDao;
import com.jinxuliang.springboot2jdbcdemo.model.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

@SpringBootApplication
public class Springboot2JdbcDemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Springboot2JdbcDemoApplication.class, args);
        testConnection(context);
        testSaveUsers(context);
        testGetAllUsers(context);
    }

    //测试保存新用户
    private static void testSaveUsers(ConfigurableApplicationContext context) {
        //获取UserDao对象
        UserDao dao = context.getBean(UserDao.class);
        System.out.println("\n保存新用户");
        User user = DaoHelper.createUserObj();
        dao.save(user);
        System.out.println(user + "已保存");
    }


    //测试提取全部用户清单
    private static void testGetAllUsers(ConfigurableApplicationContext context) {
        //获取UserDao对象
        UserDao dao = context.getBean(UserDao.class);
        System.out.println("\n提取所有用户清单");
        List<User> users = dao.getAllUsers();
        users.forEach(u -> {
            System.out.println(u);
        });
    }

    //测试数据库的连接
    private static void testConnection(ConfigurableApplicationContext context) {
        DataSource dataSource = (DataSource) context.getBean("dataSource");
        System.out.println("\n数据源对象类型:");
        System.out.println(dataSource);
        try (Connection connection = dataSource.getConnection()) {
            System.out.println("己连接");
        } catch (SQLException e) {
            System.out.println("未能连接:" + e.getMessage());
        }
    }
}

JDBC Template常用方法

提取单条记录,调用queryForObject方法。提取多条记录,调用query或queryForList方法。
新增、修改或删除一条记录,调用update方法。如果涉及多条记录,使用batchUpdate方法效率更高。
直接执行SQL命令,可以使用execute方法。

数据库初始化的两种方式

在SpringBoot项目中,可以让主类实现CommandLineRunner接口,然后在里面写Jdbc代码初始化数据库。
在SpringBoot项目中,可以将名为schema.sql和data.sql的两个文件放在项目的(src/main/resources)文件夹下,当SpringBoot项目启动时发现有这两个文件时,会自动执行它们。其中schema.sql用于存放创建数据库表的SQL命令,而data.sql则用于向表中插入初始化数据。

对于比较简单的项目,使用JDBC去访问数据库是可行的,但对于真实的项目,现在很少直接使用它了,所以,对这块内容了解即可。

Spring Data JDBC

JavaEE中,有关数据库的一块是由JPA(JavaPersistence API)规范的。一个著名的数据存取框架-Hibernate实现了这个规范。然后,Spring Data JPA对Hibernate进行了进一步的封装,以用于Spring项目开发。
Spring Data JPA的功能强大,但也相当复杂,并非所有的Spring程序都需要它的所有功能,为此,Spring开发团队于2018年推出了一个比较轻量级的数据存取框架,称为SpringDataJDBC,它采用了以接口为主的JPA编程模式,然后去掉了一些诸如延迟加载、数据缓存和状态跟踪等特性,可以看成是一个“精简版”的Spring Data。
Spring Data JDBC官网

特点:
它采用了与Spring Data JAP 类似的以接口为核心的 Repository 编程模型。
一张表对应一个数据实体,不支持一对多和多对多的关联。
调用Repository接口的方法时,直接生成 SQL 命令发送给数据库,不支持缓存,也不支持关联对象的延迟加载。

能访问的数据库种类
Spring Data JDBC:用于访问关系型数据库,当前版本支持以下数据库“方言( dialet)”:
DB2,H2,HSQLDB,MariaDB,Microsoft SQL Server,MySQL,Oracle,Postgres

项目依赖:

<!--导入Spring Data JDBC依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!--使用H2数据库-->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

application.properties

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb

在这里插入图片描述

DROP TABLE IF EXISTS person;
CREATE TABLE person
(
    id integer not null auto_increment,
    name varchar(255) not null,
    location varchar(255),
    primary key(id)
);
insert into PERSON(name,location)
values('jxl','BeiJin');

在这里插入图片描述

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.domain.Persistable;
import org.springframework.data.relational.core.mapping.Table;

//指定表名
@Table("PERSON")
public class Person {
    @Id
    private int id;//指定主键字段
    private String name;
    private String location;

    public Person() {
    }
    //当有多个构造方法时,使用这一注解指定要Spring Data JDBC
    //要使用的那个构造方法
    @PersistenceConstructor
    public Person(int id, String name, String location) {
        this.id = id;
        this.name = name;
        this.location = location;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", location='" + location + '\'' +
                '}';
    }

//region getter和setter方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }
    //endregion
}

在这里插入图片描述

import com.jinxuliang.springdatajdbcdemo.model.Person;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

import java.util.List;

//CRUD功能,使用接口表达
public interface PersonRepository
        extends CrudRepository<Person,Integer> {
    //在这里,可以添加自定义的数据存取方法
    @Query("select * from person where upper(name)" +
            " like '%'||upper(:name)||'%'")
    List<Person> findByName(@Param("name") String name);
}

在这里插入图片描述

实现CRUD

在这里插入图片描述
在这里插入图片描述

import com.jinxuliang.springdatajdbcdemo.model.Person;
import com.jinxuliang.springdatajdbcdemo.repository.PersonRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.time.LocalTime;
import java.util.Random;

@SpringBootApplication
public class SpringDataJdbcDemoApplication {
    public static void main(String[] args) {

        //获取IoC容器的引用
        ConfigurableApplicationContext context =
                SpringApplication.run(SpringDataJdbcDemoApplication.class, args);
        //获取Repository实例
        PersonRepository repository = context.getBean(PersonRepository.class);
        System.out.print("Repository的真实类型为:");
        //org.springframework.data.jdbc.repository.support.SimpleJdbcRepository
        System.out.println(repository);


        Random random = new Random();
        int ranValue = random.nextInt(100);
        //数据库表中的id字段设置为自增,当id字段为0时,Spring Data JDBC认为这个
        //对象是新增的,不为0时,会尝试进行更新记录而不是新建记录
        var person = new Person(0, "person " + ranValue,
                "location " + ranValue);
        System.out.println("\n将要保存对象:" + person);
        person = repository.save(person);
        System.out.println("保存到数据库中的对象:" + person);

        System.out.println("\n显示表中所有的记录");
        //findAll()是CurdRepository接口所定义的方法
        repository.findAll().forEach(System.out::println);

        System.out.println("\n查找名字中包容有'j'的记录");
        //findByName()是自定义接口所定义的方法,同时指定了要生成的SQL命令
        repository.findByName("j").forEach(System.out::println);
    }
}

新增还是更新?
CrudRepository接口所定义的Save()方法,会依据实体对象的id值来确定这个对象是“新增”的还是“原来就有的”,如果是新增的,会生成insert命令发送到数据库。如果是“老”的,则会生成update命令发送到数据库。默认情况下,id字段的值为0或者是nullSpringDataJDBC就认为这个对象是“新”的。如果希望自定义什么情况下才算新增,可以让数据实体类实现Persistable接口,这个接口定义了一个isNew方法,当其返回true时,SpringDataJDBC认为这个对象是新的。

查询记录

System.out.println("\n显示表中所有的记录");
//findAll()是CurdRepository接口所定义的方法
repository.findAll().forEach(System.out::println);
System.out.println("\n查找名字中包容有'j'的记录");
//findByName()是自定义接口所定义的方法,同时指定了要生成的SQL命令
repository.findByName("j").forEach(System.out::println);
显示表中所有的记录
Person{id=1, name='jxl', location='BeiJin'}
Person{id=2, name='person 37', location='location 37'}

SpringDataJDBC提供了比SpringJDBC更“面向对象”的编程模型,又不象SpringDataJPA那样“复杂”和“笨重”,适合于比较简单的数据存取任务(比如单表查询)。SpringDataJDBC的编程方式与SpringDataJPA高度一致,学会了前者,再学后者就比较轻松了,另外,在项目初起时使用SpringDataJDBC,当程序规模扩大时,可以方便地迁移到SpringDataJPA,代码改动量并不大。

Spring Boot & MyBatis

MyBatis是一个“半自动化”的数据存取框架,它通过定义Mapper,能将特定 Java方法与 SQL 命令“关联”起来,从而在Java对象与关系数据库中表之间建立了一座桥梁。之所以说MyBatis是“半自动化”的,主要是它居于直接使用JDBC(全手工操控SQL完成CRUD)与SpringDataJPA(全自动生成SQL命令)这两种极端中间。MyBatis在国内非常流行,它通常与Spring Boot,Spring Boot MVC相互配合,开发传统的Web应用,这三个框架,取其首字母,合称为“SSM”。

pom.xml相关依赖声明:
在这里插入图片描述

<!--MyBatis项目依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
<!--h2数据库依赖-->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

配置application.properties

#Spring的数据库设置,使用MySQL数据库(名为myDB)
#启用H2数据库的Web控制台程序
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

#spring数据源相关的配置参数
spring.datasource.url=jdbc:h2:file:./mydb
spring.datasource.username=sa
spring.datasource.password=
#设置Spring在启动时,为所有数据库都执行schema.sql和data.sql
#默认情况下,只有内存数据库时才会执行上述两个SQL文件
spring.datasource.initialization-mode=always

创建示例数据库表
在这里插入图片描述

DROP TABLE IF EXISTS USER;
CREATE TABLE USER
(
    id integer not null auto_increment,
    name varchar(255) not null,
    gender varchar(6),
    age integer ,
    primary key(id)
);

定义数据实体

package com.jinxuliang.mybatisspring.model;

public class User {

    private int id;
    private String name;
    private int age;
    private String gender;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                '}';
    }

    //region getter and setter
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }
    //endregion
}

定义MyBatis映射接口
所谓“映射接口”,其实就是将Java中的方法与特定的SQL命令关联上,其含义就是执行这个Java方法,MyBatis就会向数据库发出相应的SQL查询。接口中的方法名可以随意定义,关键在于注解中的参数要与方法参数相对应。

package com.jinxuliang.mybatisspring.mapper;

import com.jinxuliang.mybatisspring.model.User;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;

import org.springframework.stereotype.Component;
import java.util.List;

@Component(value="userMapper")
public interface UserMapper {
    @Select("select * from User where id=#{id}")
    User selectUserById(int id);

    @Select("Select * from user")
    List<User> getAllUser();

    @Delete("delete from user where id=#{id}")
    void deleteById(Integer id);

    @Delete("delete from user")
    void deleteAll();

    @Insert("insert into user(name,age,gender) " +
            "values(#{name},#{age},#{gender})")
    void addUser( User user);
}

程序入口点:

package com.jinxuliang.mybatisspring;

import com.jinxuliang.mybatisspring.mapper.UserMapper;
import com.jinxuliang.mybatisspring.model.User;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.Random;

@SpringBootApplication
//启用Mapper扫描,如果不加这个注解,
//则需要为每个MyBatis Mapper接口添加@Mapper注解
@MapperScan("com.jinxuliang.mybatisspring.mapper")
public class MybatisspringApplication {

    public static void main(String[] args) {
        var context =
                SpringApplication.run(MybatisspringApplication.class, args);
        //获取Mapper对象的实例
        var userMapper = context.getBean(UserMapper.class);

        System.out.println("\n--插入数据---");
        Random ran = new Random();
        for (int i = 0; i < 5; i++) {
            int ranValue = ran.nextInt(100);
            var user = createUser(ranValue);
            userMapper.addUser(user);
        }

        System.out.println("\n---查询所有对象---");
        var users = userMapper.getAllUser();
        users.forEach(System.out::println);

    }

    private static User createUser(int ranValue) {
        var user = new User();
        user.setName("user" + ranValue);
        user.setAge(ranValue);
        user.setGender(ranValue % 2 == 0 ? "男" : "女");
        return user;
    }
}

运行结果

---查询所有对象---
User{id=1, name='user30', age=30, gender='男'}
User{id=2, name='user23', age=23, gender='女'}
User{id=3, name='user50', age=50, gender='男'}
User{id=4, name='user20', age=20, gender='男'}
User{id=5, name='user58', age=58, gender='男'}

介绍了MyBatis的基础编程模式,其实是很简单的,也就几步:
(1)定义Java方法与SQL命令的映射(Mapper)接口
(2)启用Mapper扫描功能
(3)将Mapper注入到需要访问数据库的地方。

Spring Data JPA

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
JPA与Spring Data JPA
JPA(JavaPersistenceAPI,Java持久化API),是定义了对象关系映射(ORM)以及实体对象持久化的标准接口,属于JavaEE规范的组成部分。一些第三方的数据存取框架,比如Hibernate,TopLink等,实现了这一规范。
JPA主要定义了Java实体类与数据表之间的映射关系, Java 实体类的属性,对应于数据库表中的字段。
Spring Data JPA是对Hibernate的再一层封装,提供了一组Repository 接口,通过动态代理实现数据 存取功能,它是Spring Data家族的核心成员,在实际开发中应用很广。

Spring Data JPA文档
在这里插入图片描述
基于项目依赖让Spring Boot自动配置数据源

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

在添加了项目依赖SpringBoot的spring-boot-starter-data-jpa后SpringBoot就会默认为你配置数据源,这些默认的数据源主要是内存数据库,如h2、hqldb和Derby等内存数据,具体是哪个,依赖于你在项目的Maven配置文件pom.xml中加入了对哪个数据库的依赖。

配置H2数据库的依赖:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

如果要访问其他类型的数据设置(比如MySQL需要加入对MySQL的项目依赖,然后,在 application.properties 中配置相关的参数,Spring boot 就会自动地配置好数据源( DataSource )对象。虽然Spring Boot 支持在同一个项目中使用多种类型的数据库,但尽量坚持一个项目只使用一种类型的数据库,是理智的选择。

配置数据源特性
在Spring Boot 项目的 application.properties 文件中定义数据源的相关配置参数(以 spring.datasource 打头,有一堆可设置的参数,各参数的详情请自行查看官网文档):
spring.datasource.url = 特定数据库的连接字符串
spring.datasource.username= sa
spring.datasource.password= 登录密码
Spring Boot会尽可能地依据数据库URL去判断数据源是什么类型的,然后根据其默认的情况去匹配驱动类,上述配置信息,再佐以 pom.xml 中添加的项目依赖,通常足以让 Spring Boot 知道需要加载哪些特定数据库的JDBC数据库驱动程序。
Spring Boot2.0默认使用 com.zaxxer.hikari.HikariDataSource这个连接池。

配置参数:

#启用H2数据库的Web控制台程序
spring.h2.console.enabled=true
spring.h2.console.path=/h2

#设定h2的文件名为mydb,保存于项目根文件夹下
#如果设定为:~/mydb,则保存于操作系统的用户文件夹,比如,
#C:\Users\JinXu
spring.datasource.url=jdbc:h2:file:./mydb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver

#设置项目允许更新数据库结构
spring.jpa.hibernate.ddl-auto=update
#设置在控制台窗口中输出Spring Data JPA执行的SQL命令
spring.jpa.show-sql=true

Spring Data JPA 相关的配置,都以spring.jpa打头。
在这里插入图片描述
定义数据实体类:

package com.jinxuliang.springbootjpademo.model;

import javax.persistence.*;

@Entity
@Table()
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    private String gender;
    private int age;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                '}';
    }

    //region getter and setter
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    //endregion
}

创建数据实体类User ,对应于测试数据库中的 user 表。
类和字段上的注释是由JPA规范所定义的 ,可用于定义Java类名和字段名如何 与数据库表名和字段名进行配对。

定义数据存取接口:

package com.jinxuliang.springbootjpademo.jpa;

import com.jinxuliang.springbootjpademo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import java.util.List;

public interface JpaUserRepository
        extends CrudRepository<User, Integer> {

    //遵循Jpa的惯例,定义查询方法
    List<User> findAllByGender(String gender);
    //查询指定性别的大于指定岁数的用户
    List<User> findAllByGenderAndAgeGreaterThan(String gender,int age);
    //按照主键进行查询
    User getUserById(Integer id);
    //使用JPA查询语言自定义查询
    @Query("from User where name like concat('%', ?1, '%') ")
    public List<User> findUsers(String userName);
    //直接使用原生的SQL命令
    @Query(value = "select * from User where User.gender=?1",
            nativeQuery = true)
    List<User> findAllByGenderUseNativeSQL(String gender);
}

使用Spring Data JPA 存取数据库,需要通过数据接口实现,其方法有三种类型:
1.直接使用系统提供的接口方法
2.依据JPA约定编写自定义查询方法
3.直接指定查询命令的查询方法

Repository实例的获取:
我们只要定义好接口,Spring Data JPA会自动地帮助我们生成一个实现了这个接口的数据存取对象:获取到了Repository 的实例,下面就可用它来实现 CRUD 了。

package com.jinxuliang.springbootjpademo;


import com.jinxuliang.springbootjpademo.jpa.JpaUserRepository;
import com.jinxuliang.springbootjpademo.model.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.List;
import java.util.Optional;
import java.util.Random;


@SpringBootApplication
public class SpringbootJpaDemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication
                .run(SpringbootJpaDemoApplication.class, args);
        //获取JPA Repository实例,以便用它来实现CRUD
        JpaUserRepository repo = context.getBean(JpaUserRepository.class);
        //默认情况下,是repo引用一个以下类型的对象:
        //org.springframework.data.jpa.repository.support.SimpleJpaRepository
        System.out.println(repo);

        //存
        User user = createUser();
        System.out.println("\n---存储新用户对象---\n");
        User savedUser = repo.save(user);
        System.out.println("对象" + savedUser + "已存储。");

        //查
        System.out.println("\n---使用系统提供的JPA接口查询对象---\n");
        repo.findAll().forEach(System.out::println);
        System.out.println("\n查询Id=1的用户\n");
        Optional<User> result = repo.findById(1);
        if (result.isPresent()) {
            System.out.println(result.get());
        } else {
            System.out.println("没有找到");
        }

        System.out.println("\n----------使用约定的JPA接口方法查询------\n");
        System.out.println("\n查询男性用户\n");
        List<User> users = repo.findAllByGender("男");
        for (User u : users) {
            System.out.println(u);
        }

        System.out.println("\n查询指定性别的大于指定岁数的用户\n");
        users = repo.findAllByGenderAndAgeGreaterThan("男", 40);
        for (User u : users) {
            System.out.println(u);
        }

        System.out.println("\n----------使用显式指定的查询命令进行查询------\n");
        System.out.println("测试使用原生的SQL查询指定性别的用户");
        users = repo.findAllByGenderUseNativeSQL("女");
        users.forEach(System.out::println);

        System.out.println("\n使用JPA查询语言自定义查询\n");
        users = repo.findUsers("U");
        users.forEach(System.out::println);
        // context.close();
    }

    private static User createUser() {
        Random random = new Random();
        int ranValue = random.nextInt(120);
        User user = new User();
        user.setAge(ranValue);
        user.setGender(ranValue % 2 == 0 ? "男" : "女");
        user.setName("User" + ranValue);
        return user;
    }
}
---存储新用户对象---

Hibernate: insert into user (id, age, gender, name) values (null, ?, ?, ?)
对象User{id=296, name='User117', gender='女', age=117}已存储。
---使用系统提供的JPA接口查询对象---

Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.gender as gender3_0_, user0_.name as name4_0_ from user user0_
User{id=1, name='User105', gender='女', age=105}
User{id=33, name='User31', gender='女', age=31}
User{id=34, name='User5', gender='女', age=5}
User{id=35, name='User68', gender='男', age=68}
User{id=36, name='User33', gender='女', age=33}
User{id=37, name='User5', gender='女', age=5}
User{id=38, name='User19', gender='女', age=19}
User{id=39, name='User50', gender='男', age=50}
User{id=71, name='User94', gender='男', age=94}
User{id=103, name='User101', gender='女', age=101}
User{id=135, name='User43', gender='女', age=43}
User{id=167, name='User82', gender='男', age=82}
User{id=199, name='User37', gender='女', age=37}
User{id=231, name='User109', gender='女', age=109}
User{id=263, name='User114', gender='男', age=114}
User{id=295, name='User6', gender='男', age=6}
User{id=296, name='User117', gender='女', age=117}

JpaRepository定义的所有方法(比如 findAll 和 findById 方法),均可以直接使用,而根本不需要你写任何一行代码去实现这些功能!这就是Spring Data JPA 所提供给你的“魔术”。

查询Id=1的用户

Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.gender as gender3_0_0_, user0_.name as name4_0_0_ from user user0_ where user0_.id=?
User{id=1, name='User105', gender='女', age=105}

更神奇的是,Spring Data JPA 定义了一种特殊的“语言”,只要你遵循它的“语法”设定你的查询方法名,你也不需要手写一行 SQL 命令,Spring Data JPA 也能自动识别这些方法名字,并自动地帮你实现它,从而你就可以直接使用这些方法来查询数据。

----------使用约定的JPA接口方法查询------
查询男性用户

Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.gender as gender3_0_, user0_.name as name4_0_ from user user0_ where user0_.gender=?
User{id=35, name='User68', gender='男', age=68}
User{id=39, name='User50', gender='男', age=50}
User{id=71, name='User94', gender='男', age=94}
User{id=167, name='User82', gender='男', age=82}
User{id=263, name='User114', gender='男', age=114}
User{id=295, name='User6', gender='男', age=6}
查询指定性别的大于指定岁数的用户

Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.gender as gender3_0_, user0_.name as name4_0_ from user user0_ where user0_.gender=? and user0_.age>?
User{id=35, name='User68', gender='男', age=68}
User{id=39, name='User50', gender='男', age=50}
User{id=71, name='User94', gender='男', age=94}
User{id=167, name='User82', gender='男', age=82}
User{id=263, name='User114', gender='男', age=114}

如果你觉得前面所介绍的“魔术”不太可控,你也可以为查询方法直接执行要执行的 SQL 命令,或者使用 JPA 所专门定义的一种查询语言编写查询话句,满足你的“控制欲”。

----------使用显式指定的查询命令进行查询------

测试使用原生的SQL查询指定性别的用户
Hibernate: select * from User where User.gender=?
User{id=1, name='User105', gender='女', age=105}
User{id=33, name='User31', gender='女', age=31}
User{id=34, name='User5', gender='女', age=5}
User{id=36, name='User33', gender='女', age=33}
User{id=37, name='User5', gender='女', age=5}
User{id=38, name='User19', gender='女', age=19}
User{id=103, name='User101', gender='女', age=101}
User{id=135, name='User43', gender='女', age=43}
User{id=199, name='User37', gender='女', age=37}
User{id=231, name='User109', gender='女', age=109}
User{id=296, name='User117', gender='女', age=117}
使用JPA查询语言自定义查询

Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.gender as gender3_0_, user0_.name as name4_0_ from user user0_ where user0_.name like ('%'||?||'%')
User{id=1, name='User105', gender='女', age=105}
User{id=33, name='User31', gender='女', age=31}
User{id=34, name='User5', gender='女', age=5}
User{id=35, name='User68', gender='男', age=68}
User{id=36, name='User33', gender='女', age=33}
User{id=37, name='User5', gender='女', age=5}
User{id=38, name='User19', gender='女', age=19}
User{id=39, name='User50', gender='男', age=50}
User{id=71, name='User94', gender='男', age=94}
User{id=103, name='User101', gender='女', age=101}
User{id=135, name='User43', gender='女', age=43}
User{id=167, name='User82', gender='男', age=82}
User{id=199, name='User37', gender='女', age=37}
User{id=231, name='User109', gender='女', age=109}
User{id=263, name='User114', gender='男', age=114}
User{id=295, name='User6', gender='男', age=6}
User{id=296, name='User117', gender='女', age=117}

本讲介绍了Spring Data JPA的基础知识与基本编程套路。应该来说,Spring Data JPA还是相当强大与灵活的,设计者考虑得相当周到。
Spring Data JPA的问题是封装得过多,可控性没那么强,就像是一个“黑盒子”,学习曲线比较陡峭,并且主要用于关系型数据库,因此,在实际开发中的应用受到限制。

Java访问MongoDB

MongoDB是一种文档型的NoSQL数据库,文档采用流行的Json格式表达,以二进制的Json格式(Bson)保存,查询直接支持使用Json。
MongoDB的文档保存于文档集合,同一集合的文档,其结构可以不一样,另外,MongoDB有很完善的服务器集群管理功能,可伸缩性好,很好地满足了现代Web应用的需求,成为诸多NoSQL数据库中最有名的一个,在互联网应用开发技术NoSQL这块,市场占有率第一。
由于MongoDB采用与JavaScript友好的Json存储和查询数据,所以与JavaScript生态圈(比如Node.js和各种JS开发框架)关系密切,MEAN/MERN/MEVN大行于世。

Java访问MongoDB:
MongoDB为Java提供了特定的驱动,并且支持两种编程模式:传统的同步模式编程模型,反应式的异步流模式编程模型。同样地,Spring也为上述两种编程模式,提供了两种不同的起步依赖。
MongoDB的Java驱动官方文档
默认情况下,MongoDB监听本机27017端口。

Java编程访问MongoDB,Java访问MongoDB最方便的方式有两个:
1.创建一个Java Maven或Gradle项目,然后给项目添加MongoDB驱动的项目依赖。
2.在创建Spring项目时,选中MongoDB相关的起步依赖。

项目依赖:

<dependencies>
    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>mongodb-driver-sync</artifactId>
        <version>4.1.1</version>
    </dependency>
</dependencies>

MongoDB Java基础编程模型

在这里插入图片描述
编程访问MongDB步骤:
使用MongoClient连接并访问MongoDB服务器,注意,在整个App中应该只有一个MongoClient实例。
调用MongoClient.getDatabase方法获取MongoDatabase对象,再调用 MongoDatabase.getCollection方法得到MongoCollection之后,就可以使用 MongoCollection实现CRUD了。

连接数据库
使用MongoClients.create方法创建 一个采用默认参数配置的MongoClient对象,通过它来连接本机数据库。如果需要访问网络数据库,则需要指定连接字符串。
MongoClient可以跨线程安全地访问,它内部封装有一个数据库连接池,因此,不需要创建多个MongoClient对象实例,重用一个就好。

文档与文档集合:
MongoDB中的数据以“文档(document )”方式保存,多个文档的集合称为“集合( collection )”,放在“数据库(database )”中 。
MongoCollection对象是实现CRUD的核心对象。
调用MongoDatabase对象的getCollection方法获取文档集合对象的引用,如果指定名字的集合不存在,在向集合中第一次存取数据时,MongoDB会自动地创建它。

初始化相关:

static MongoClient mongoClient;
static MongoDatabase database;
static String databaseName = "mydb";
static MongoCollection<Document> collection = null;
static {
    //直接访问本机MongoDB,默认端口:27017
    MongoClient mongoClient = MongoClients.create();
    //也可以使用连接字符串连接MongoDB
    //MongoClient mongoClient =
    //            MongoClients.create("mongodb://localhost:27017");
    //获取数据库对象的引用
    database = mongoClient.getDatabase(databaseName);
    //获取用于测试的文档集合的引用
    collection = database.getCollection("testCollection");
}

基于文档的CRUD

package connect;

import com.mongodb.Block;
import com.mongodb.MongoClientSettings;
import com.mongodb.ServerAddress;
import com.mongodb.client.*;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.bson.Document;
import org.bson.json.JsonMode;
import org.bson.json.JsonWriterSettings;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import static com.mongodb.client.model.Filters.*;
import static com.mongodb.client.model.Updates.inc;
import static com.mongodb.client.model.Updates.set;

public class VisitMongoDBUseDocument {

    static MongoClient mongoClient;
    static MongoDatabase database;
    static String databaseName = "mydb";
    static MongoCollection<Document> collection = null;

    static {
        //直接访问本机MongoDB,默认端口:27017
        MongoClient mongoClient = MongoClients.create();
        //也可以使用连接字符串连接MongoDB
        //MongoClient mongoClient =
        //            MongoClients.create("mongodb://localhost:27017");

        //获取数据库对象的引用
        database = mongoClient.getDatabase(databaseName);
        //获取用于测试的文档集合的引用
        collection = database.getCollection("testCollection");
    }

    public static void main(String[] args) {
        //列出所有的集合
        //要删除一个集合,可以调用集合对象的drop方法。
        for (String name : database.listCollectionNames()) {
            System.out.println(name);
        }

		//deleteMany()方法要求指定一个筛选条件
		//如果要删除全部,传入一个空的Document对象就行了。
        System.out.println("\n---清空集合中的所有文档---\n");
        collection.deleteMany(new Document());

        System.out.println("\n---插入单个文档---\n");
        //创建新文档
        Document doc = new Document("name", "MongoDB")
                .append("type", "database")
                .append("count", 1)
                .append("versions", Arrays.asList("v3.2", "v3.0", "v2.6"))
                .append("info", new Document("x", 203).append("y", 102));
		//插入新文档时,无需指定其Id,MongoDB 会自动为新文档生成Id值,
		//MongoDB文档的Id类型为ObjectId,
		//是MongoDB使用特定算法生成的,本集合内唯一。
        var result = collection.insertOne(doc);
        //新记录id:BsonObjectId{value=5fd46efe198bc304b260ef45}
        System.out.println("新文档id:" + result.getInsertedId());

		//将多个文档加入到集合中,然后,调用insertMany()方法插入即可。
        System.out.println("\n---插入多个文档---\n");
        List<Document> documents = new ArrayList<Document>();
        for (int i = 0; i < 10; i++) {
            documents.add(new Document("i", i));
        }
        var insertManyResult = collection.insertMany(documents);
        System.out.println("插入记录数:" + insertManyResult.getInsertedIds().size());

		//使用countDocuments()方法统计集合中的文档数,使用forEach方法
		//传给它一个Lambda表达式,可以遍历其中的文档:
        System.out.println("\n---计数----\n");
        System.out.println(collection.countDocuments());
        System.out.println("\n--遍历---\n");
        collection.find().forEach(document -> System.out.println(document.toJson()));

        System.out.println("\n---查询单个文档----\n");
        Document myDoc = collection.find().first();
		//使用find()查询表达式方法,此方法返回一个FindIterable<T>接口对象
		//通过它的first()方法可以获取第一个文档。
		//拿到文档对象之后,可以调用它的toJson()方法获取它的Json 表示形式:
		//{"_id": {"$oid": "5fdc1d875f6a03013bd9a9cd"}, "name": "MongoDB", "type":"database", "count": 1, "versions": ["v3.2", "v3.0", "v2.6"], "info":{"x": 203, "y": 102}}
        //com.mongodb.client.model.Filters提供了多种过滤器,eq是其中之一
        //以下代码,查询属性i=2的文档。
        myDoc = collection.find(eq("i", 2)).first();
        System.out.println(myDoc.toJson());

        System.out.println("\n---查询多个文档---\n");
        //定义一个Consumer,用于输入单个文档的Json格式数据
        Consumer<Document> printBlock = d -> System.out.println(d.toJson());
        //可以组合各种过滤器,构建出复杂的查询表达式
        //以下代码,查询i>5且i<=10的文档,然后遍历输出
        //在集合中查询文档时,通常需要指定筛选条件,往往需要构建“数据过滤器”。
        collection.find(and(gt("i", 5), lte("i", 10)))
                .forEach(printBlock);

        System.out.println("\n---更新单个文档---\n");
        collection.updateOne(eq("i", 5), set("i", 50));
        System.out.println("\n---更新多个文档---\n");
        UpdateResult updateResult = collection.updateMany(lt("i", 100),
                inc("i", 100));
        System.out.println("更新文档数:" + updateResult.getModifiedCount());

        System.out.println("\n---删除单个文档---\n");
        collection.deleteOne(eq("i", 110));
        System.out.println("\n---删除多个文档---\n");
        DeleteResult deleteResult = collection.deleteMany(gte("i", 100));
        System.out.println("删除条数:" + deleteResult.getDeletedCount());
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
MongoDB采用BSON(二进制格式的 Json )格式表达数据,文档是保存数据的基本单元。
文档放在集合中,因此,CRUD 都是针对集合实现的。
MongoDB的Java驱动为基于文档的CRUD提供了方便且直观的编程模型,只需要很少的代码就能实现 CRUD 操作,但不足之处在于这些都是针对文档的,在现实开发中往往需要你手工完成从文档到JavaBean(集合)的转换工作。

基于POJO访问MongoDB

前面介绍了如何基于“文档( Document )”来直接实现 MongoDB数据库的CRUD 操作,虽然很方便,但我们在实际开发中,更多使用的是JavaBean和由它所构成的集合,因此,直接使用文档的话,你必须手工完成从文档到 JavaBean 之间的转换工作。
针对这个需求,MongoDB Java驱动提供了相应的组件,支持MongoDB的文档(Document)与 Java 编程中用到的JavaBean之间的“双向转换”。
在这里插入图片描述
在这里插入图片描述
Codec(文档编解码器)
Codec负责实现MongoDB底层数据文档与特定数据类型之间的转换任务。
默认情况下, MongoCollection内置以下Codec(文档编解码器):
1.Document json 文档
2.BasicDBObject(早期版本使用)
3.BsonDocument:二进制的Json文档
可以根据自己的需求,添加自定义的Codec 。

CodecRegistry
Codec的集合称为"CodecRegistry(文档编解码器 注册表 )"。
MongoDB使用CodecRegistry完成底层数据文档与App所用数据类型JavaBean )之间的转换工作。
依据CodecRegistry 的适用范围,可以使用 withCodecRegistry 方法,将 它附加到以下三种对象之上:
(1)MongoClient :适用于 此连接实例上的所有数据库
(2) MongoDatabase :适用于特定的数据库中的所有集合
(3) MongoCollection :仅适用于特定的文档集合。
CodecRegistry是一种自包容类型,它可以包容多个 Codec 对象,也能包容多个 CodecRegistry 对象,类似于文件夹与文件之间的关系。

CodeRegistry的构建:
CodeRegistry对象,主要通过 CodecRegistries.fromRegistries方法创建。

import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;

DefaultCodeRegistry包容有一堆的“ CodecProvider"对象

package connect;

import com.mongodb.Block;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;

import entities.MyAddress;
import entities.Person;
import org.bson.Document;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;

import java.util.List;

import static com.mongodb.client.model.Filters.*;
import static com.mongodb.client.model.Updates.*;
import static java.util.Arrays.asList;

import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;

public class VisitMongoDBUsePOJO {
    public static void main(String[] args) {

        //构建CodeRegistry对象(其实就是Codec的集合)
        CodecRegistry pojoCodecRegistry = fromRegistries(
                //获取MongoDB Java驱动内置的标准Codec集合
                MongoClientSettings.getDefaultCodecRegistry(),
                //获取一个可以自动依据POJO对象字段字生成Codec集合的对象
                fromProviders(PojoCodecProvider.builder()
                        .automatic(true).build()));
        //将上述CodeRegistry对象与MongoClient对象相关联,表明这些Codec对象
        //将适用于当前连接的MongoDB数据库服务器上的所有数据库和文档集合
        MongoClientSettings settings = MongoClientSettings.builder()
                .codecRegistry(pojoCodecRegistry)
                .build();
        MongoClient mongoClient = MongoClients.create(settings);

        //获取MongoDB数据库引用
        MongoDatabase database = mongoClient.getDatabase("mydb");
        //获取文档集合的引用,注意其泛型参数为Person类
        MongoCollection<Person> collection = database.getCollection("people", Person.class);

        System.out.println("\n---清空所有文档---\n");
        collection.deleteMany(new Document());
        System.out.println("集合中的文档数目:" + collection.countDocuments());

        Person ada = new Person("Ada Byron", 20,
                new MyAddress("St James Square", "London", "W1"));
        //插入一个文档
        var insertResult = collection.insertOne(ada);
        System.out.println("\n新插入文档的Id:" + insertResult.getInsertedId());

        System.out.println("\n---插入多个文档---\n");
        List<Person> people = asList(
                new Person("Charles Babbage", 45, new MyAddress("5 Devonshire Street", "London", "W11")),
                new Person("Alan Turing", 28, new MyAddress("Bletchley Hall", "Bletchley Park", "MK12")),
                new Person("Timothy Berners-Lee", 61, new MyAddress("Colehill", "Wimborne", null))
        );

        var insertManyResult = collection.insertMany(people);
        System.out.println(insertManyResult.getInsertedIds());

        System.out.println("\n---显示所有文档---\n");
        collection.find(new Document()).forEach(System.out::println);

        System.out.println("\n---查找符合条件的第一个文档---\n");
        var somebody = collection.find(eq("address.city", "Wimborne")).first();
        System.out.println(somebody);

        System.out.println("\n---查找年纪大于30的文档---\n");
        collection.find(gt("age", 30)).forEach(System.out::println);


        System.out.println("\n---修改---\n");
        collection.updateOne(eq("name", "Ada Byron"),
                combine(set("age", 23), set("name", "Ada Lovelace")));
        UpdateResult updateResult = collection.updateMany(not(eq("zip", null)),
                set("zip", null));
        System.out.println(updateResult.getModifiedCount());
        System.out.println("\n---替换---\n");
        updateResult = collection.replaceOne(eq("name", "Ada Lovelace"), ada);
        System.out.println(updateResult.getModifiedCount());

        System.out.println("\n---删除---\n");
        collection.deleteOne(eq("address.city", "Wimborne"));
        DeleteResult deleteResult = collection.deleteMany(eq("address.city", "London"));
        System.out.println(deleteResult.getDeletedCount());
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package entities;

public class MyAddress {
    private String street;
    private String city;
    private String zip;

    public MyAddress() {
    }

    //注意一下参数前面要有一个final
    public MyAddress(final String street,final String city,final String zip) {
        this.street = street;
        this.city = city;
        this.zip = zip;
    }

    @Override
    public String toString() {
        return "MyAddress{" +
                "street='" + street + '\'' +
                ", city='" + city + '\'' +
                ", zip='" + zip + '\'' +
                '}';
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getZip() {
        return zip;
    }

    public void setZip(String zip) {
        this.zip = zip;
    }
}
package entities;

import org.bson.types.ObjectId;

public class Person {
    //id是ObjectId类型。
    private ObjectId id;
    private String name;
    private int age;
    private MyAddress myAddress;

    public Person() {
    }

    public Person(final String name, final int age, final MyAddress myAddress) {
        this.name = name;
        this.age = age;
        this.myAddress = myAddress;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", myAddress=" + myAddress +
                '}';
    }

    //region getter and setter
    public ObjectId getId() {
        return id;
    }

    public void setId(ObjectId id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public MyAddress getMyAddress() {
        return myAddress;
    }

    public void setMyAddress(MyAddress myAddress) {
        this.myAddress = myAddress;
    }
    //endregion
}

Spring访问MongoDB

在这里插入图片描述

在这里插入图片描述

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
#访问本机MongoDB,无密码
spring.data.mongodb.uri=mongodb://localhost:27017/mymongodb
#有密码
#spring.data.mongodb.uri=mongodb://用户名:密码@服务器IP地址:端口号/数据库名

实体类型:
可以使用@Document 注解标识MongoDB数据实体类,使用@Id指明其标识字段。

package com.example.springmongodbdemo.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document
public class Customer {
    @Id
    public String id;
    public String firstName;
    public String lastName;
    public Customer() {}

    public Customer(String firstName,
                    String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format(
                "Customer[id=%s, firstName='%s', lastName='%s']",
                id, firstName, lastName);
    }
}

定义Repository:
Spring Data MongoDB定义了专门的接口,从它派生出自己的Repository接口:

package com.example.springmongodbdemo.repository;

import com.example.springmongodbdemo.model.Customer;
import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.List;

public interface CustomerRepository
        extends MongoRepository<Customer, String> {

    Customer findByFirstName(String firstName);
    List<Customer> findByLastName(String lastName);

}

获取Repository实例

package com.example.springmongodbdemo;

import com.example.springmongodbdemo.model.Customer;
import com.example.springmongodbdemo.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringMongodbDemoApplication
        implements CommandLineRunner {

    //注入自定义MongoDB Repository
    @Autowired
    private CustomerRepository repository;

    public static void main(String[] args) {
        SpringApplication.run(SpringMongodbDemoApplication.class, args);
    }
    //获取了Repository实例之后,基于它来访问MongoDB就变得很简单了

    @Override
    public void run(String... args) throws Exception {

        //删除
        repository.deleteAll();

        // 保存
        repository.save(new Customer("Alice", "Smith"));
        repository.save(new Customer("Bob", "Smith"));

        // 遍历
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // 查询
        System.out.println("Customer found with findByFirstName('Alice'):");
        System.out.println("--------------------------------");
        System.out.println(repository.findByFirstName("Alice"));

        System.out.println("Customers found with findByLastName('Smith'):");
        System.out.println("--------------------------------");
        for (Customer customer : repository.findByLastName("Smith")) {
            System.out.println(customer);
        }
    }
}

在这里插入图片描述
与直接使用MongoDB的Java驱动相比,使用Spring Data Mongo访问 MongoDB出奇地简单,你根本不需要去理会Codec和CodecRegistry等东西,只要按照Spring Data的通用编程模式,定义好Repository接口,然后将其注入到相应的类中,就可以使用它来完成MongoDB的CRUD了。

Java平台数据存取技术选型指南

Java平台提供了多种数据存取技术,本课程介绍过的主要成员如下:
在这里插入图片描述
JDBC的使用场景:
使用原生的JDBC 访问数据库,代码冗长,开发效率较低,但由于封装少,更接近于“原生”的数据库存取方式,所以具有很高的可控性和自由度,数据存取效率比较高,并且易于持续优化性能。开发桌面应用或控制台应用,需要存取关系型数据库,项目的规模不大。

Spring JDBC API:
Spring为简化传统的JDBC编程而打造,编程模型简单,中心组件就是一个 JdbcTemplate,开发效率有较大的提升。比较适合于开发控制台应用。缺点在于必须添加对庞大的Spring平台核心组件群的引用,显得比较笨重,通常必须使用Maven或Gradle进行构建,想单独地引用jar包,基本上就行不通了。

Spring Data JDBC:
是一个精简版的Spring Data JPA ,编程方式基本上与 JPA 一致,也使用 Repository 编程模式,不需要手写 SQL 命令。
由于它不支持多表关联,因此,只能用于比较小的Spring 项目,并且数据库中的表之间没有建立关联。
可以在Spring项目的初期阶段使用,等项目数据层变复杂,可以比较容易地升级到使用 Spring Data JPA 。

Spring Data JPA:
Java平台功能强大的 ORM 框架,高度封装,全自动化,几乎什么功能都有,功能集合庞大与完善。
过于庞大与复杂,学习曲线陡峭,如果不熟悉,会觉得一切都好像在“变魔术”,可控性较差,扩展不易。
适合于规模比较大,需要比较长期维护的Spring Web后端项目。

MyBatis:
MyBatis国内非常流行的“半自动化”框架,学起来比Spring Data JPA 简单。
使用Mapper将Java方法与SQL命令直接关联,编程模型比较直观。
需要手写SQL ,开发效率不如自动化程度高的Spring Data JPA,但好处是可控性更强,优化数据存取也比较方便。
使用场景与Spring Data JPA高度一致 。

Spring Data MongoDB:
MongoDB是市场占有率第一的 NoSQL 数据库,以文档方式保存数据,使用起来灵活方便。
Java访问MongoDB有三种方式:
直接使用MongoDB的Java驱动。这种方式比较底层,数据存取效率高,编程模型简单直接,可以直接操控Document,也可以使用POJO,缺点是代码量较大。
Spring Data MongoDB:采用与Spring Data JPA类似的Repository编程模式访问MongoDB,开发效率较高,比较适合于开发传统的Spring Web项目。
Spring Data Reactive MongoDB:采用反应式编程方式,数据存取是异步的、非阻塞的,与 Spring WebFlux 无缝配合,适合于开发微服务。

SQL还是NoSQL?
对于那些比较规范的,数据项很少变化的数据,推荐使用关系数据库存储。
数据项变化较大,并且不适合于采用二维表格形式表达的数据(比如呈现为多叉树或图),推荐使用相应的NoSQL数据库:
多叉树:使用MongoDB的文档保存。Key-value:Value可以有多种类型,可以使用Redis进行保存。图:使用图数据库Neo4j进行保存。

在这里插入图片描述

互联网编程概述

Web工作原理

计算机网络(Computer Network):指的是通过各种通讯手段(有线的或无线的)连接在一起,从而可以相互交换信息的计算机(或其他种类的信息处理设备)所构成的一个整体。
互联网(Internet):多个不同的计算机网络相互连接起来,就构成了。
Internet:并不是一个网络,而是多个计算机网络的集合 。因此,人们又把它形象地表述为“ 网中网 ”。
万维网( World Wide Web ):运行在互联网上的一个分布式信息系统 。
在这里插入图片描述
在这里插入图片描述
Web Server含义:我们这里所说的“Web Server(Web 服务器)并不指真实的计算机,而是指跑在这些计算机之上的一种特殊的程序,它能完成 Web 各项基础功能,并且可以承载多个“ Web 应用程序( WebApplication )”,即我们所说的”网站( Web Site )”。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
嵌入式的Web Server vs. CGI
传统的Web Server ,比如 Apache Web Server 、 Nginx 、 IIS 、 Tomcat 等,使用 CGI/ FastCGI 方式提供 Web 服务。这些 Web Server 都运行于独立的进程中,使用 Java/C# 等编写的 Web 应用程序 ,承载于 Web Server 进程中。
使用Spring Boot 或 Node.js 开发的 Web 应用,它内部可以内嵌一个 Web Server (比如 Spring Boot Web 应用在内部嵌入了 Tomcat 或 Netty ),本身是一个独立运行的应用,不依赖于外部的 Web Server.
传统的Web 应用,多依赖于独立的 Web Server ,现在,微服务大行于世,更趋向于极用嵌入式的 Web Server ,以便于单独部署和管理。

Web资源与URL

在这里插入图片描述
一个严重的“大”问题:Web资源太多了,我们怎么样才找到需要的资源?
为了能正确地定位特定的资源,每个资源需要有一个唯一的标识,“ URL(Uniform Resoure Locator :统一资源定位器 )”起到的就是这个作用,俗称它为“ 网址 ”。
在这里插入图片描述
URL实际上把Web Server与Web Client两种不同类型的应用程序给隔离开了,从而让两者可以相互独立地采用不同的技术、平台和软件架构进行开发和构建。
有两种主要的Web Resource提供方式:静态资源(预先准备好的“物理”文件),动态资源(当客户端请求时,由Web Server动态地创建和提供)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
MIME与内容协商
互联网上的资源有很多种: 文字、图片、视频、 Flash 动画、 JavaScript 代码
问题一:我们采用哪种方式来标识出资源 的类型?
问题二:浏览器怎么知道它所接收的是哪种类型的数据?
问题三:浏览器能否通知 Web Server 它所期望接收的数据类型?
针对Web 资源,人们制订了一个 MIME 标准 ,为每种类型的资源提供了一个统一的标准字符串 ,比较好地解决了资源类型的标识问题。
MIME:Multipurpose Internet Mail Extensions
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
同一个 Web 资源可能有多种表现形式,比如中文英文, Xml/Json 等等。不同的客户端,拥有不同的特性,会期望服务器能提供它所需要的指定类型的Web资源 。为此,Web Server与Web Client之间,需要进行内容协商。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

HTTP协议

HTTP消息
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
HTTP方法:

方法名描述有无消息主体
GET从服务器上获取一个文档No
HEAD仅从服务器上提取文档的相关信息No
POST将数据发送给服务器处理Yes
PUT将消息主体所包容的内容保存到服务器上Yes
TRACE获取HTTP消息所通过代理服务器到达最终服务器的路径信息No
OPTIONS询问服务器它是否支持某一HTTP 方法No
DELETE从服务器中删除一个文档No

HTTP状态码:
HTTP的状态码,主要供Web服务器通知客户端: XXX 事情发生了,XXX请求的处理结果是什么。

限定的范围己使用的范围种类
100-199100-101Informational
200-299200-206Successful
300-399300-305Redirection
400-499400-415Client error
500-599500-505Server error
常用标准HTTP状态码:
状态码名称说明
200OK操作成功完成
301Moved Permanently要访问的资源已被永久移动,不要再访问这个URL了
302Moved Temporaily要访问的资源己被暂时移动到另一个地方,过些时间这个URL可能又可以访问了
304Not Modified资源自从上次访问之后,没有被更改
400Bad Request发来的请求是无效的
401Unauthorized客户端需要进行身份验证
403Forbidden禁止访问此资源
404Not found要访问的资源不存在
500Internal Server服务端在处理请求时出现了错误
503Service Unavailable服务暂时不可用

HTTP请求与响应过程:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
几个探索HTTP奥秘的工具:
Chrome Developer Tools:浏览器自带的开发工具,分析网页与HTTP请求和响应的利器
Fiddler/Postman:功能强大的通用 HTTP 开发与调试工具
HttpClient:.NET/Java所提供的简单易用的组件,可以方便地编程访问Web资源
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Fiddler:
fiddler网址
在这里插入图片描述
在这里插入图片描述

编程访问Web(C#)

.NET HttpClient简介

使用HttpClient:
HttpClient可用于取代原先.NET基类库中System.Net命名空间中的WebClient、WebRequest、WebResponse等组件,更简单易用。
HttpClient使用HttpRequestMessage/HttpResponseMessage封装请求与响应消息,支持async/await异步编程模式,可以同时向多个URI (甚至是跨域的)发出HTTP请求。

HttpClient基本编程技巧:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

OkHttp(Java)

OkHttp是一个开源的Java网络库,可以用于Java应用程序(主要是Java SE和Android ),完成基于HTTP协议的各种通讯任务。
OkHttp官网
OkHttp在底层使用 Okio ,所以,如果不使用 Maven 等方式自动导入到项目中,就必须单独下载 Okio 组件。
在这里插入图片描述
基本思路:
在OkHttp中,每一次网络请求就是一个Request,我们只要在Request里填写需要的url,header等其他参数,基于Request构造出一个Call对象,再调用它的execute()方法,就能取得Web Server回复的数据。
如果同步调用,需要在独立的线程中执行,使用异步调用,则采用回调的方式执行,在内部封装了一个请求队列。
OkHttp依赖另一个组件okio完成高性能的I/O操作。

发送请求:

package cn.edu.bit.cs;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import com.google.gson.Gson;

import cn.edu.bit.cs.model.CalculatorResult;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class OkHttpTest {

	public static void main(String[] args) throws IOException {
		// doGet();
		//calculatorViaGet();
		// calculatorViaPost();
		 downloadImageByGet();
	}

	/**
	 * 使用HTTP Get方法获取百度首页
	 */
	private static void doGet() {
		OkHttpClient okHttpClient = new OkHttpClient();
		// 使用Builder设计模式构建请求对象
		Request.Builder builder = new Request.Builder();
		Request request = builder.get().url("http://www.baidu.com").build();
		// 创建调用对象,将请求对象注入
		Call call = okHttpClient.newCall(request);
		// 发出异步调用请求,当请求完成时,回调Callback对象的相应方法
		call.enqueue(new Callback() {
			@Override
			public void onResponse(Call arg0, Response response) throws IOException {
				// 成功时,从Http Body中取出数据
				System.out.println(response.body().string());
			}

			@Override
			public void onFailure(Call arg0, IOException e) {
				// 访问失败,输出信息
				System.out.println(e.getMessage());
			}
		});
	}

	/**
	 * 通过Get方法调用教学网站上的四则运算服务,信息通过查询参数传送
	 */
	private static void calculatorViaGet() {
		String calculatorUrl = "https://jinxuliang.com/openservice/api/OnlineCalculator/Calculate?expr=";
		final String expression = "(9-3.5)*100";
		OkHttpClient okHttpClient = new OkHttpClient();
		Request.Builder builder = new Request.Builder();
		Request request = builder.get().url(calculatorUrl + expression).build();
		Call call = okHttpClient.newCall(request);
		call.enqueue(new Callback() {
			@Override
			public void onResponse(Call arg0, Response response) throws IOException {
				String resultString = response.body().string();
				System.out.println(resultString);
				// 使用Gson组件将Server端返回的json字符串转换为CalculatorResult对象
				Gson gson = new Gson();
				CalculatorResult resultObj = gson.fromJson(resultString, CalculatorResult.class);
				System.out.println(expression + "=" + resultObj.getResult());
			}

			@Override
			public void onFailure(Call arg0, IOException e) {
				System.out.println(e.getMessage());
			}
		});
	}

	/**
	 * 通过Post方法调用教学网站上的四则运算服务,信息通过放在Body中的json字符串传送
	 */
	private static void calculatorViaPost() {

		String calculatorUrl = "https://jinxuliang.com/openservice/api/OnlineCalculator/Calculate";
		final String expression = "6+7*11";
		OkHttpClient okHttpClient = new OkHttpClient();
		Request.Builder builder = new Request.Builder();
		String json = "{expr:'" + expression + "'}";
		RequestBody body = RequestBody.create(MediaType.parse("application/json"), json);
		//创建post请求对象
		Request request = builder.url(calculatorUrl).post(body).build();
		Call call = okHttpClient.newCall(request);
		call.enqueue(new Callback() {
			@Override
			public void onResponse(Call arg0, Response response) throws IOException {
				String resultString = response.body().string();
				System.out.println(resultString);
				Gson gson = new Gson();
				CalculatorResult resultObj = gson.fromJson(resultString, CalculatorResult.class);
				System.out.println(expression + "=" + resultObj.getResult());
			}
			@Override
			public void onFailure(Call arg0, IOException e) {
				System.out.println(e.getMessage());
			}
		});
	}

	/**
	 * 使用HttpGet方法下载互联网上的图片
	 */
	private static void downloadImageByGet() {
		String imgUrl = "https://jinxuliang.com/courseimages/java.jpg";
		OkHttpClient okHttpClient = new OkHttpClient();
		Request.Builder builder = new Request.Builder();
		Request request = builder.get().url(imgUrl).build();
		Call call = okHttpClient.newCall(request);
		call.enqueue(new Callback() {
			@Override
			public void onResponse(Call arg0, Response response) throws IOException {
				InputStream inputStream = response.body().byteStream();
				int len = 0;
				File file = new File("test.jpg");
				FileOutputStream fos = new FileOutputStream(file);
				byte[] buf = new byte[2048];
				while ((len = inputStream.read(buf)) > 0) {
					fos.write(buf, 0, len);
				}
				fos.flush();
				fos.close();
				inputStream.close();
				System.out.println("Download finished!");
			}
			@Override
			public void onFailure(Call arg0, IOException e) {
				System.out.println(e.getMessage());
			}
		});
	}
}
package cn.edu.bit.cs.model;

/**
 * 此类封装从Server端传回的信息
 * @author JinXuLiang
 *
 */
public class CalculatorResult {
	private String result="";

	public String getResult() {
		return result;
	}

	public void setResult(String result) {
		this.result = result;
	}
}

HttpClient(Java)

在这里插入图片描述
HttpClient,是Java用于访问HTTP Server的一个网络组件,它既可以用于 Java 桌面应用,也可以用于Java Web应用 ,通过它来访问远程的Web服务。
支持HTTP/2和WebSocket协议,同时提供同步与异步两套编程模型。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述

package net.branchandbound.linkvalidator;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

public class LinkValidatorAsync {

    private static HttpClient client;

    public static void main(String[] args) throws Exception {
        client = HttpClient.newHttpClient();
        //构建由多个异步HTTP请求构成的List集合
        var requests =
                Files.lines(Path.of("urls.txt"))
                     .map(LinkValidatorAsync::validateLink)
                     .collect(Collectors.toList());
        //等待所有异步任务的完成
        requests.stream()
                .map(CompletableFuture::join)
                .forEach(System.out::println);
    }

    //完成异步检测工作
    private static CompletableFuture<String> validateLink(String link) {
        HttpRequest request = HttpRequest.newBuilder(URI.create(link))
                                .GET()
                                .build();
        //针对每个URL发出异步HTTP请求
        return client.sendAsync(request, HttpResponse.BodyHandlers.discarding())
                     .thenApply(LinkValidatorAsync::responseToString)
                     .exceptionally(e -> String.format("%s -> %s", link, false));

    }

    //将HTTP响应转换为字符串
    private static String responseToString(HttpResponse<Void> response) {
        int status = response.statusCode();
        boolean success = status >= 200 && status <= 299;
        return String.format("%s -> %s (status: %s)", response.uri(), success, status);
    }
}
package net.branchandbound.linkvalidator;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;

public class LinkValidatorSync {

    private static HttpClient client;

    public static void main(String[] args) throws IOException {

        client = HttpClient.newHttpClient();

        Files.lines(Path.of("urls.txt"))
             .map(LinkValidatorSync::validateLink)
             .forEach(System.out::println);
    }

    private static String validateLink(String link) {
        HttpRequest request = HttpRequest.newBuilder(URI.create(link))
                                .GET()
                                .build();

        try {
            HttpResponse<Void> response = client.send(request,
                    HttpResponse.BodyHandlers.discarding());
            return responseToString(response);
        } catch( IOException | InterruptedException e) {
            e.printStackTrace();
            return String.format("%s -> %s", link, false);
        }
    }

    private static String responseToString(HttpResponse<Void> response) {
        int status = response.statusCode();
        boolean success = status >= 200 && status <= 299;
        return String.format("%s -> %s (status: %s)", response.uri(), success, status);
    }
}
https://www.jinxuliang.com
https://openjdk.java.net
https://httpstat.us/500
https://httpstat.us/200?sleep=5000
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
  <component name="NewModuleRootManager" inherit-compiler-output="true">
    <exclude-output />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
  </component>
</module>
https://www.jinxuliang.com -> true (status: 200)
https://openjdk.java.net -> true (status: 200)
https://httpstat.us/500 -> false (status: 500)
https://httpstat.us/200?sleep=5000 -> true (status: 200)

Spring Boot Web

在这里插入图片描述
Spring Boot MVC是实现上述移动互联网应用的主流开发框架之一。
Spring两种相应Web请求方式:Spring Boot MVC,Spring Boot WebFlux
Web开发技术栈:
在这里插入图片描述
在这里插入图片描述
Spring MVC概述:
Spring MVC是Spring技术家族中用于Web应用开发的一个框架,其中的MVC是Model-View-Controller
在这里插入图片描述
Spring Boot与Spring MVC:
早在Spring Boot之前,Spring MVC就已经存在。Spring MVC是构建于Spring Framework上的应用了MVC设计模式的Web开发框架。Spring Boot本质上是Spring Framework的自动化,从而简化了原有Spring MVC应用的启动和配置过程。现在开发Spring MVC项目,都是基于Spring Boot的,所以通常将其合并成为SpringBoot MVC。

Spring Boot MVC工作原理:
Spring MVC是对传统Java Web技术(Servlet)的再度封装
在这里插入图片描述
Servlet API存在的问题:过于底层与琐碎,开发效率低
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当前技术进展:
可以使用传统的Servlet实现Spring Boot MVC所有功能(适用于Spring Boot 2.0以前版本)
从Spring Boot 2.0开始,Spring Web开发技术进行了扩展和演化,现在同时支持传统的Servlet技术栈和新增的Reactive技术栈,后者更适合于开发微服务,是一种更为现代的Web应用开发技术。

Servlet

Servlet和 JSP 只是 Java 企业版中众多技术中的两个,其他 Java EE 技术还有 Java 消息服务,企业 Java 对象、 Java Server Faces 以及 Java持久化等。
在这里插入图片描述
示例:
添加依赖:
使用IntelliJ创建Spring Boot项目,添加“spring-boot-starter-web”的项目依赖,之后,给程序入口类添加@ServletComponentScan注解,之后,就可以在项目中通过给类添加@WebServlet注解定义Servlet了。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
package com.jinxuliang.springbootservletdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
//启动Sevlet扫描
@ServletComponentScan
public class SpringBootServletDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootServletDemoApplication.class, args);
    }
}

在这里插入图片描述
在这里插入图片描述
原理:
Servlet容器将Servlet类载入内存,并在Servlet实例上调用具体的方法。在一个应用程序中,每种Servlet类型只能有一个实例(Singleton Object)。
用户请求致使Servlet容器调用Servlet的Service方法,并传入一个ServletRequest实例(封装HTTP请求)和一个ServletResponse实例(封装HTTP响应)。
对于每一个应用程序,Servlet容器还会创建一个ServletContext实例。这个对象中封装了上下文(应用程序)的环境详情。

Servelt接口:

package com.jinxuliang.springbootservletdemo.servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 直接实现Servlet接口,定义的Servlet
 */
@WebServlet(name = "MyFirstServlet",
        urlPatterns = {"/myfirst"})
public class MyFirstServlet implements Servlet {
    private transient ServletConfig servletConfig;
    @Override
    public void init(ServletConfig servletConfig)
            throws ServletException {
        this.servletConfig = servletConfig;
        System.out.println("MyFirst Servlet initialized.");
    }
    @Override
    public ServletConfig getServletConfig() {
        return servletConfig;
    }
    @Override
    //最重要的方法用于生成HTTP响应
    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
            throws ServletException, IOException {
        String servletName = servletConfig.getServletName();
        servletResponse.setContentType("text/html");
        PrintWriter writer = servletResponse.getWriter();
        writer.print("<html><head></head>"
                + "<body>Hello from " + servletName
                + "</body></html>");
    }
    @Override
    public String getServletInfo() {
        return "My First Servlet";
    }
    @Override
    public void destroy() {
        System.out.println("My First Servlet Destoryed.");
    }
}

Web应用是多线程的,而Servlet是Singleton的,因此,访问它的字段要考虑到线程安全问题。
在这里插入图片描述
使用web.xml定义Servlet

package com.jinxuliang.springbootservletdemo.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/welcome")
public class WelcomeServlet extends HttpServlet {
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.print("<html><head></head>"
                + "<body>Welcome</body></html>");
    }
}

ServletRequest:
对于每一个HTTP请求,Servlet容器都会创建一个ServletRequest实例,并将它传给 Servlet的Service方法。ServletRequest封装了关于这个请求的信息,其中包容一些重要的方法:

方法说明
int getContentLength()响应数据的大小
String getContentType()MIME类型
String getProtocol()返回这个HTTP请求的协议名称和版本
String getParameter()返回HTML表单域(或查询字符串)的值

ServletResponse:
javax.servlet.ServletResponse接口表示一个Servlet响应 。
在调用 Servlet 的 Service 方法前, Servlet 容器首先创建一个ServletResponse ,并将它作为第二个参数传给 Service 方法。ServletResponse 隐藏了向浏览器发送响应的复杂过程。

方法说明
getWriter()返回了一个可以向客户端发送文本的java.io.PrintWriter

ServletConfig:
当Servlet容器初始化Servlet时,Servlet容器会给Servlet的init方法传入一个ServletConfig对象。
ServletConfig封装可以通过@WebServlet或者部署描述符(即web.xml)传给Servlet的配置信息。这样传入的每一条信息就叫一个初始参数。一个初始参数有key和value两个组成部分。可以调用getInitParameter方法读取这些配置参数值。

通常有两种方式配置Servlet:
1.在web.xml中以XML元素方式进行配置(现在基本不用这种方式了)
2.直接在Servlet中使用注解进行配置

package com.jinxuliang.springbootservletdemo.servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "ServletConfigDemoServlet",
        urlPatterns = { "/servletConfigDemo" },
        initParams = {
        		//使用注解定义初始化参数
                @WebInitParam(name="admin", value="JinXuLiang"),
                @WebInitParam(name="email", value="admin@example.com")
        }
)
public class ServletConfigDemoServlet implements Servlet {
    private transient ServletConfig servletConfig;
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        this.servletConfig = servletConfig;
    }

    @Override
    public ServletConfig getServletConfig() {
        return servletConfig;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
            throws ServletException, IOException {
        ServletConfig servletConfig = getServletConfig();
        //使用ServletConfig,可以获取初始化参数值
        String admin = servletConfig.getInitParameter("admin");
        String email = servletConfig.getInitParameter("email");
        servletResponse.setContentType("text/html");
        PrintWriter writer = servletResponse.getWriter();
        writer.print("<html><head></head><body>" +
                "Admin:" + admin +
                "<br/>Email:" + email +
                "</body></html>");
    }

    @Override
    public String getServletInfo() {
        return "ServletConfig demo";
    }

    @Override
    public void destroy() {
    }
}

ServletContext:
ServletContext表示Servlet应用程序的运行环境。 每个Web应用程序只有一个上下文。在将一个应用程序同时部署到多个容器的分布式环境中,每台Java虚拟机上的Web应用都会有一个ServletContext对象。
通过在ServletConfig中调用getServletContext方法,可以获得ServletContext,这一对象拥有以下重要的方法,可以用来保存各种数据:
在这里插入图片描述
在这里插入图片描述
GenericServlet:
GenericServlet实现了Servlet和ServletConfig接口,并完成以下任务:
•将 init 方法中的ServletConfig赋给一个类级变量,以便可以通过调用 getServletConfig 获取。
•为Servlet 接口中的所有方法提供默认的实现。
•提供方法,包围ServletConfig中的方法。
自定义的Servlet,可以直接派生自GenericServlet,并可选重写其init方法(注意要选没有参数的那个,以保证ServletConfig字段肯定有值)。

package com.jinxuliang.springbootservletdemo.servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "GenericServletDemoServlet",
        urlPatterns = { "/generic" },
        initParams = {
                @WebInitParam(name="admin", value="JinXuLiang"),
                @WebInitParam(name="email", value="admin@example.com")
        }
)
public class GenericServletDemoServlet extends GenericServlet {
    private static final long serialVersionUID = 62500890L;
    @Override
    public void service(ServletRequest request,
                        ServletResponse response)
            throws ServletException, IOException {

        ServletConfig servletConfig = getServletConfig();
        String admin = servletConfig.getInitParameter("admin");
        String email = servletConfig.getInitParameter("email");
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.print("<html><head></head><body>" +
                "Admin:" + admin +
                "<br/>Email:" + email +
                "</body></html>");
    }
}

HttpServlet:
HttpServlet类继承自javax.servlet.GenericServlet类。
使用HttpServlet 时 ,可以使用分别代表Servlet请求和Servlet响应的HttpServletRequest和HttpServletResponse 对象。
HttpServletRequest派生自javax.servlet.ServletRequest,
HttpServletResponse派生自javax.servlet.ServletResponse。

HttpServlet覆盖GenericServlet中的Service方法,并通过下列签名再添加了一 个 Service 方法:
protected void service(HttpServletRequest request,HttpServletResponse response)throws ServletExceptionjava.io.IOException

HttpServlet有两个特性是 GenericServlet 所不具备的:
1.在实际开发中不是重写 Service 方法, 而是重写 doGet 或者 doPost 或者重写 doGet 和 doPost 。在少数情况下,还 会重写: doHead 、 doPut 、 doTrace 、doOptions 和 doDelete 方法
2.使用 HttpServletRequest 和 HttpServletResponse ,而不是ServletRequest 和 ServletResponse ,前两者比后两者提供了更多的方法可供调用。

如果在实际开发中需要编写Servlet ,建议从HttpServlet类进行派生。

HttpServletRequest对象:
封装了HTTP 请求相关的信息

重要方法说明
String getContextPath()返回表示请求上下文的请求URI部分。
Cookie[] getCookies()返回一个Cookie对象数组
String getHeader(String name)返回指定HTTP首部的值
Stirng getMethod()返回生成这个请求的HTTP方法名称
String getQueryString()返回请求URL中的查询字符串。
HttpSession getSeddion()返回与这个请求相关的会话对象。如果没有,将会创建一个新的会话对象。

HttpServletResponse对象
HttpServletResponse表示 HTTP 环境中的 Servlet 响应,可以通过它向客户端返回相应的数据。

重要方法说明
void addCookie(Cookie cookie)给这个响应对象添加一个cookie
void addHeader(String name, String value)给这个响应对象添加一个header
void sendRedirect(String location)发送一个响应码。将浏览器跳转到指定位置

Servlet应用之“表单处理”:
使用Servlet直接生成表单:
当用户提交表单时,在表单元素中输入的值就会被当作请求参数发送到服务器。
Input元素的值,会被当作字符串发送到服务器 。
包含多个值的select 元素(允许选择多个选项并且用 <select multiple>表示的 select 元素)发出一个字符串数组,并且必须通过SelectRequest.getParameterValues 进行处理。
复选框比较奇特。选中的复选框会发送字符串“on”到服务器。未选中的复选框则不向服务器发送任何内容, ServletRequest.getParameter(fieldName)返回null 。
单选框将被选中按钮的值发送到服务器。如果没有选择任何按钮,将没有任何内容被发送到服务器,并且ServletRequest.getParameter(fieldName)返回null。
如果一个表单中包含多个输入同名的元素,那么所有值都会被提交,并且必须利用ServletRequest.getParameterValues来获取它们。
ServletRequest.getParameter将只返回最后一个值。
在真实的Java Web项目中,是不会使用Servlet来生成表单的,最差也是使用JSP来设计,如果是Spring Boot,则可以使用Thymeleaf等来设计,而在“前后端分离”的Web应用中,表单是由前端框架(比如Angular/Vue/React)生成。所以,对于Servlet生成表单,只需要了解即可。

package com.jinxuliang.springbootservletdemo.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

/**
 * 展示如何处理表单域
 */
@WebServlet(name = "FormServlet", urlPatterns = { "/form" })
public class FormServlet extends HttpServlet {
    private static final long serialVersionUID = 54L;
    private static final String TITLE = "Order Form";
    @Override
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.println("<html>");
        writer.println("<head>");
        writer.println("<title>" + TITLE + "</title></head>");
        writer.println("<body><h1>" + TITLE + "</h1>");
        writer.println("<form method='post'>");
        writer.println("<table>");
        writer.println("<tr>");
        writer.println("<td>Name:</td>");
        writer.println("<td><input name='name'/></td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Address:</td>");
        writer.println("<td><textarea name='address' "
                + "cols='40' rows='5'></textarea></td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Country:</td>");
        writer.println("<td><select name='country'>");
        writer.println("<option>United States</option>");
        writer.println("<option>Canada</option>");
        writer.println("</select></td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Delivery Method:</td>");
        writer.println("<td><input type='radio' " +
                "name='deliveryMethod'"
                + " value='First Class'/>First Class");
        writer.println("<input type='radio' " +
                "name='deliveryMethod' "
                + "value='Second Class'/>Second Class</td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Shipping Instructions:</td>");
        writer.println("<td><textarea name='instruction' "
                + "cols='40' rows='5'></textarea></td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>&nbsp;</td>");
        writer.println("<td><textarea name='instruction' "
                + "cols='40' rows='5'></textarea></td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Please send me the latest " +
                "product catalog:</td>");
        writer.println("<td><input type='checkbox' " +
                "name='catalogRequest'/></td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>&nbsp;</td>");
        writer.println("<td><input type='reset'/>" +
                "<input type='submit'/></td>");
        writer.println("</tr>");
        writer.println("</table>");
        writer.println("</form>");
        writer.println("</body>");
        writer.println("</html>");
    }
    @Override
    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.println("<html>");
        writer.println("<head>");
        writer.println("<title>" + TITLE + "</title></head>");
        writer.println("</head>");
        writer.println("<body><h1>" + TITLE + "</h1>");
        writer.println("<table>");
        writer.println("<tr>");
        writer.println("<td>Name:</td>");
        writer.println("<td>" + request.getParameter("name")
                + "</td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Address:</td>");
        writer.println("<td>" + request.getParameter("address")
                + "</td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Country:</td>");
        writer.println("<td>" + request.getParameter("country")
                + "</td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Shipping Instructions:</td>");
        writer.println("<td>");
        String[] instructions = request
                .getParameterValues("instruction");
        if (instructions != null) {
            for (String instruction : instructions) {
                writer.println(instruction + "<br/>");
            }
        }
        writer.println("</td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Delivery Method:</td>");
        writer.println("<td>"
                + request.getParameter("deliveryMethod")
                + "</td>");
        writer.println("</tr>");
        writer.println("<tr>");
        writer.println("<td>Catalog Request:</td>");
        writer.println("<td>");
        if (request.getParameter("catalogRequest") == null) {
            writer.println("No");
        } else {
            writer.println("Yes");
        }
        writer.println("</td>");
        writer.println("</tr>");
        writer.println("</table>");
        writer.println("<div style='border:1px solid #ddd;" +
                "margin-top:40px;font-size:90%'>");
        writer.println("Debug Info<br/>");
        Enumeration<String> parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String paramName = parameterNames.nextElement();
            writer.println(paramName + ": ");
            String[] paramValues = request
                    .getParameterValues(paramName);
            for (String paramValue : paramValues) {
                writer.println(paramValue + "<br/>");
            }
        }
        writer.println("</div>");
        writer.println("</body>");
        writer.println("</html>");
    }
}

Servlet新变化:
新版的Servlet ,支持异步编程模式左侧为一个典型的示例,相应地,运行这一 Servlet 的 Tomcat 服务器也需要支持异步 Servlet 的新版本。
技术关键点:
(1 )打开 WebServlet 的异步支持开关。
(2 )调用请求对象的 startAsync方法获取异步响应上下文对象
(3 )调用上下文对象的 start()方法启动异步响应
(4 )调用上下文对象的 complete方法结束异步响应。

package com.jinxuliang.springbootservletdemo.servlet;

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//异步操作,需要打开asyncSupported开关
@WebServlet(urlPatterns = "/my/async",
        asyncSupported = true)
public class MyAsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp)
            throws ServletException, IOException {
        //启动异步响应
        AsyncContext asyncContext = req.startAsync();
        asyncContext.start(() -> {
            try {
                resp.getWriter().println("Hello,Aync World!");

            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //必须结束异步上下文
                asyncContext.complete();
            }
        });
    }
}

虽然现在Servlet的功能进行了增强,支持异步非阻塞的运行模式,但毕竟是以一种打补丁的模式实现的,因此,并不推荐在实际开发中大规模地应用异步Servlet 。
需要开发高性能的异步非阻塞的Java Web 后端应用,应该使用更新的开发框架及服务器,比如Spring WebFlux,Netty和Vert.x 。

Servlet是Java传统Web应用的核心,拥有超过二十年的历史,即使在技术不断升级换代的今天(比如2018年发布的Spring Boot 2),Servlet技术栈仍然没有被废弃,仍然是Spring Boot Web项目的基础技术,因此,掌握它的基本编程技巧,了解它的运行原理,仍然是十分必要的,但具体的编程技巧,其实可以不求甚解。
学习Servlet的前提是你必须对HTTP协议、HTML网页、Web Server和浏览器这些Web开发基础知识有所了解。
学习Servlet的关键是了解它的几个核心类型的功能和它所封装的信息,其实还是挺简单的,只要你对HTTP协议比较了解得。

Spring Boot MVC

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

<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>

在这里插入图片描述
在这里插入图片描述
给项目添加一个controller包,再添加一个HelloController类,在类上添加@Controller注解,在hello方法上添加RequestMapping注解。

package com.jinxuliang.spring_boot_demo.controller;

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

@Controller
public class HelloController {
    @RequestMapping("/hello")
    public String hello(){
        //使用放在resources/templates/hello.html作为模板
        //Thymeleaf生成HTML返回给客户端
        return "hello";
    }
}

给类添加@Controller (或 RestController )注解 表明这个类是一个控制器, Spring IoC 容器能自动识别 @Controller 注解并在合适的时候实例化它。
给控制器类中的方法添加RequestMapping 注解,指定此方法所关联的 URL Spring 负责 将此 URL 请求映射 到附加了 RequestMapping 注解的方法上,换句话说,就是使用控制器类中的某个 特定方法 来响应这个 Web 请求,生成HTML响应发回给客户端。
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
    <h2>Hello!</h2>
</body>
</html>

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.jinxuliang.spring_boot_demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

控制器编程基础技巧

传入与传出

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.jinxuliang.spring_mvc_controller_demo.controller;

import com.jinxuliang.spring_mvc_controller_demo.domain.DemoObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Random;

//展示Spring Boot项目中MVC控制器的基本功能
@Controller
@RequestMapping("/mvc")
public class MVCDemoController {

    //访问标准Servlet对象
    @RequestMapping(value = "/index", produces = "text/plain;charset=UTF-8")
    @ResponseBody
    public String index(HttpServletRequest request) {
        return "url:" + request.getRequestURL() + " can access";
    }

    //使用查询字符串queryString?id=100,可让方法的id得到一个值
    //如果不传,id参数得到一个null值
    @RequestMapping(value = "/queryString")
    @ResponseBody
    public String queryString(Long id, HttpServletRequest request) {
        return "url:" + request.getRequestURL() + " can access,id:" + id;
    }

    //使用@RequestParam显式指定参数匹配方式
    //可以指定其是否是必需的参数,如果不是必需的参数,则它可以为null
    @RequestMapping("/reqParam")
    @ResponseBody
    public String requestParam(
            @RequestParam("int_val") Integer intVal,
            @RequestParam(value = "str_val",required = false) String strVal) {
        if(strVal==null)
            strVal="default value";
        return "intValue:"+intVal+" strVal:"+strVal;
    }

    //展示PathVariable注解的使用
    @RequestMapping(value = "/pathvar/{id:\\d+}")
    @ResponseBody
    public String pathVar(@PathVariable String id, HttpServletRequest request) {
        return "url:" + request.getRequestURL() + " can access,id:" + id;
    }


    //使用查询字符串传送对象:/obj?name=jxl&id=100
    @RequestMapping(value = "/obj", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public String passObj(DemoObject obj, HttpServletRequest request) {
        return "url:" + request.getRequestURL() + " can access,object id:"
                + obj.getId() + " object name:" + obj.getName();
    }

    //客户端使用POST方式,将一个Json字符串提交上来
    //服务端从HTTP body中取出数据,将其转换为DemoObject对象作为方法参数
    @RequestMapping(value="/fromJson",method = RequestMethod.POST)
    @ResponseBody
    public String fromJson(@RequestBody DemoObject demo){
        if(demo!=null){
            return demo.toString();
        }
        return "未能从HTTP请求中提取信息创建DemoObject对象";
    }

    Random ran = new Random();

    //将向外界返回一个Json字符串:
    // {"id":2628847830238592887,"name":"DemoObject 2628847830238592887"}
    @RequestMapping("/getjson")
    @ResponseBody
    public DemoObject getObj() {
        Long ranValue = ran.nextLong();
        return new DemoObject(ranValue, "DemoObject " + ranValue);
    }

    //一个方法,可以响应多个URL
    @RequestMapping(value = {"/url1", "/url2"})
    @ResponseBody
    public String multiURL(HttpServletRequest request) {
        return "url:" + request.getRequestURL() + " can access";
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
http://localhost:8080/mvc/pathvar/100
在这里插入图片描述

package com.jinxuliang.spring_mvc_controller_demo.domain;

public class DemoObject {
    private Long id;

    @Override
    public String toString() {
        return "DemoObject{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    private String name;

    public DemoObject() {
    }

    public DemoObject(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {

        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

转发与重定向

转发(forward):就是指某个控制器方法在响应 HTTP 请求时,它只把“活”干一半,另一半转给另一个方法去干,但所有的工作仍然是在同一个 HTTP 请求处理过程中完成 的。
重定向(redirect):就是指某个 控制器方法在响应 HTTP请求时,“因为自己现在不干这事了”,就向浏览器发出一个301/302 ”响应,通知浏览器向另一个 URL 发出请求,“让其他人干这事”。在这里插入图片描述

package com.jinxuliang.spring_mvc_controller_demo.controller;

import com.jinxuliang.spring_mvc_controller_demo.domain.DemoObject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/redir")
public class RedirectController {

    //显示转发传过来的数据
    @RequestMapping("/index")
    public String index(Model model, HttpServletRequest request){
        //将数据传给视图
        model.addAttribute("message",request.getAttribute("message"));
        return "redir/index";
    }
    //实现转发
    @RequestMapping("/forward")
    public String forward(HttpServletRequest request){
        //转发时需要保存的数据
        request.setAttribute("message","从/forward转发过来");
        return "forward:/redir/index";
    }

    //从Session中提取数据,然后显示在视图中
    @RequestMapping("/showInfo")
    public String showInfo(@ModelAttribute("demo_obj") DemoObject obj,
                           Model model){
        //将数据传给视图
        model.addAttribute("demo_obj",obj);
        return "redir/show";
    }

    //重定向过程中传送数据
    @RequestMapping("/redirToShow")
    public String redirect(RedirectAttributes ra){
        DemoObject obj=new DemoObject(100l,"hello");
        //保存重定向数据到Session中
        ra.addFlashAttribute("demo_obj",obj);
        return "redirect:/redir/showInfo";
    }
}

在这里插入图片描述

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>转发功能演示</title>
</head>
<body style="text-align: center">
    <h2>转发功能演示</h2>
    <p th:if="${message}!=null" th:text="${message}">
    </p>
</body>
</html>

保存于Attribute 中的数据,在视图中可以使用 ${...}的形式取出来。
在这里插入图片描述
在这里插入图片描述

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>重定向功能演示</title>
</head>
<body style="text-align: center">
    <h2>重定向功能演示</h2>
    <div th:if="${demo_obj.getId()!=null}">
        <p th:text="${demo_obj.getId()}"></p>
        <p th:text="${demo_obj.getName()}"></p>
    </div>
</body>
</html>

在这里插入图片描述

package com.jinxuliang.spring_mvc_controller_demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringMvcControllerDemoApplication {

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

在这里插入图片描述

package com.jinxuliang.async_demo.controllers;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.Callable;

@RestController
@RequestMapping("/async")
public class AsyncController {

    private Logger logger= LoggerFactory.getLogger(getClass());

    //同步代码
    @RequestMapping("/order1")
    public String order1() throws InterruptedException {
        logger.info("主线程开始");
        Thread.sleep(1000);
        logger.info("主线程返回");
        return "success";
    }

    //使用Callable<T>来编写异步代码,会自动创建一个线程
    @RequestMapping("/order2")
    public Callable<String> order() {

        //主线程:nio-8080-exec-1
        logger.info("主线程开始:"+Thread.currentThread().getName());

        //Callable的线程,是task-1这样的线程
        Callable<String> result = new Callable<String>() {
			@Override
			public String call() throws Exception {
				logger.info("工作线程开始:"+Thread.currentThread().getName());
				Thread.sleep(1000);
				logger.info("工作线程返回"+Thread.currentThread().getName());
				return "success";
			}
		};

        logger.info("主线程返回:"+Thread.currentThread().getName());
        return result;
    }
}
package com.jinxuliang.async_demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AsyncDemoApplication {

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Spring Boot Web应用数据校验

在实际开发中,为了保证数据的安全,必须对用户提交上来的数据进行有效性检测。
数据有效性检测分为两类,一类是在客户端进行的,常用的Web前端框架(比如Vue和React)都提供了相应的功能,在提交数据之前对数据的有效性进行检测;另一类则是服务端数据检测,就是在调用Spring MVC控制器方法之间对客户端数据的有效性进行校验。
本讲PPT介绍的是如何使用Spring MVC框架在服务端完成数据有效性检测的工作。
在这里插入图片描述
项目依赖:

  <!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>
package com.jinxuliang.datavalidator_demo.domain;

import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.*;
import java.util.Date;

public class ValidatorPojo {
    // 非空判断
    @NotNull(message ="id不能为空")
    private Long id;

    @Future(message = "需要一个将来日期")
    // @Past // 只能是过去的日期
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @NotNull // 不能为空
    private Date date;

    @NotNull // 不能为空
    @DecimalMin(value = "0.1") // 最小值0.1元
    @DecimalMax(value = "10000.00") // 最大值10000元
    private Double doubleValue = null;

    @Min(value = 1, message = "最小值为1") // 最小值为1
    @Max(value = 88, message = "最大值为88") // 最大值为88
    @NotNull // 不能为空
    private Integer integer;

    @Range(min = 1, max = 888, message = "范围为1至888") // 限定范围
    private Long range;

    // 邮箱验证
    @Email(message = "邮箱格式错误")
    private String email;
    @Size(min = 20, max = 30, message = "字符串长度要求20到30之间。")
    private String size;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Double getDoubleValue() {
        return doubleValue;
    }

    public void setDoubleValue(Double doubleValue) {
        this.doubleValue = doubleValue;
    }

    public Integer getInteger() {
        return integer;
    }

    public void setInteger(Integer integer) {
        this.integer = integer;
    }

    public Long getRange() {
        return range;
    }

    public void setRange(Long range) {
        this.range = range;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getSize() {
        return size;
    }

    public void setSize(String size) {
        this.size = size;
    }

    public ValidatorPojo(){

    }

    public ValidatorPojo(Long id,
                         Date date,
                         Double doubleValue,
                         Integer integer, Long range,
                         String email,String size) {
        this.id = id;
        this.date = date;
        this.doubleValue = doubleValue;
        this.integer = integer;
        this.range = range;
        this.email = email;
        this.size = size;
    }

    @Override
    public String toString() {
        return "ValidatorPojo{" +
                "id=" + id +
                ", date=" + date +
                ", doubleValue=" + doubleValue +
                ", integer=" + integer +
                ", range=" + range +
                ", email='" + email + '\'' +
                ", size='" + size + '\'' +
                '}';
    }
}

Java为数据校验定义了规范(JSR-303),主要体现为一堆的注解。
左图展示了一些常用的注解,把这些注解附加在类的字段上,表明这些字段需要满足相应的约束条件。
在这里插入图片描述

package com.jinxuliang.datavalidator_demo.controller;

import com.jinxuliang.datavalidator_demo.domain.ValidatorPojo;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
@RequestMapping("/datavalidate")
public class DataValidatorController {

    //向外界返回一个有效的数据实使用对象,以方便测试
    @GetMapping("/get")
    @ResponseBody
    public ValidatorPojo getValidatorPojo(){
        long now=new Date().getTime();
        return new ValidatorPojo(100l, new Date((int)now+1000),
                500d,99, 888l,
                "jinxuliang@bit.edu.cn",
                "01234567890 01234567890 012345"
        );
    }

    /***
     * 解析验证参数错误
     * @param vp —— 需要验证的POJO,使用注解@Valid 表示验证
     * @param errors  错误信息,它由Spring MVC通过验证POJO后自动填充
     * @return 错误信息Map
     */
    @RequestMapping(value = "/post",method = RequestMethod.POST)
    @ResponseBody
    public Map<String, Object> validate(
            @Valid @RequestBody ValidatorPojo vp, Errors errors) {
        Map<String, Object> infoMap = new HashMap<>();
        // 获取错误列表
        List<ObjectError> oes = errors.getAllErrors();
        if(oes.size()==0){
            infoMap.put("info",vp.toString());
        }
        else{
            for (ObjectError oe : oes) {
                String key = null;
                String msg = null;
                // 字段错误
                if (oe instanceof FieldError) {
                    FieldError fe = (FieldError) oe;
                    key = fe.getField();// 获取错误验证字段名
                } else {
                    // 非字段错误
                    key = oe.getObjectName();// 获取验证对象名称
                }
                // 错误信息
                msg = oe.getDefaultMessage();
                infoMap.put(key, msg);
            }
        }
        return infoMap;
    }
}

在这里插入图片描述
在控制器中编写集成了校验功能的方法
给需要校验的参数加上@Valid注解,Spring MVC就会对其进行数据校验,如果通不过,相关信息会放入到Errors集合中。
在这里插入图片描述
在这里插入图片描述
本讲介绍了Spring Boot Web项目中如何在服务端实现数据校验的基本方法,适用于开发“前后端分离”的现代Web应用。
本讲示例未涉及如何在Spring Boot MVC项目的视图上显示数据校验信息,其实在掌握了Spring Boot MVC视图编程技术(比如Thymeleaf)之后,这块是很容易补上的,只需将数据校验信息直接转给视图引擎,让它将这些信息填充到生成的HTML中即可。

使用Webjars

Spring Boot支持一种被称为webjar的技术,将一些前端框架(比如Bootstrap)打包为单个文件作为一个整体引用。
所有指向/webjars/的HTTP请求,都会被导向这个包中所包容的静态资源文件。
访问以下网址了解详情:网址

在这里插入图片描述
在这里插入图片描述

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>4.5.3</version>
</dependency>

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebJar示例</title>
    <link rel='stylesheet'
          href='/webjars/bootstrap/4.5.3/css/bootstrap.min.css'>

</head>
<body class="container">
    <h1 class="text-center text-primary">基于WebJar使用Bootstrap</h1>
   <div class="text-center">
       <img src="images/lotus.jpg" class="img-thumbnail img-fluid"
            alt="一幅莲花图">
   </div>
</body>
</html>

在这里插入图片描述
在这里插入图片描述

<dependency>
    <groupId>org.webjars.npm</groupId>
    <artifactId>vue</artifactId>
    <version>3.0.4</version>
</dependency>

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel='stylesheet'
          href='/webjars/bootstrap/4.5.3/css/bootstrap.min.css'>
    <script src="webjars/vue/3.0.4/dist/vue.global.js"></script>
</head>
<body>
<div id="root" class="text-center display-1">0</div>
<script>
    Vue.createApp({
        data() {
            return {
                content: 1
            }
        },
        mounted() {
            setInterval(() => {
                this.content++;
            }, 1000);
        },
        template: '<div>{{content}}</div>'
    }).mount('#root')
</script>
</body>
</html>

Spring Boot Web示例:实现文件上传

在Web应用中, 文件上传几乎是必须具备的功能。
通常我们会在网页上设置一个form元素,里面再放置一个文件上传控件,用户选择要上传的文件之后,点击“提交”按钮,采用“multipart/form-data”方式上传文件到服务端。服务端只需要取出文件数据,再将其保存到硬盘上即可。
Spring MVC对文件上传提供了良好的支持,而在SpringBoot中可以更为简单地配置文件上传所需的内容。
在这里插入图片描述
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>上传文件</title>
</head>
<body>
<form method="post"
      action="/file/upload" enctype="multipart/form-data">
    <input type="file" name="file" value="请选择上传的文件" />
    <input type="submit" value="提交" />
</form>
</body>
</html>

配置文件上传选项:在application.properties中定义文件上传的相关参数,注意事先创建好用于保存文件的文件夹:

# 指定默认上传的文件夹
spring.servlet.multipart.location=c:/upload
# 限制单个文件最大大小,这里设置为5MB
spring.servlet.multipart.max-file-size=5MB
# 限制所有文件最大大小,这里设置为20MB
spring.servlet.multipart.max-request-size=20MB
package com.jinxuliang.uploader_demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/file")
public class FileController {

    /**
     * 打开文件上传请求页面
     * @return 指向JSP的字符串
     */
    @GetMapping("/upload")
    public String uploadPage() {
        return "upload";
    }
    // 使用Spring MVC的MultipartFile类作为参数
    @PostMapping("/upload")
    @ResponseBody
    public Map<String, Object> uploadMultipartFile(MultipartFile file) {
        String fileName = file.getOriginalFilename();
        File dest = new File(fileName);
        try {
            file.transferTo(dest);
        } catch (Exception e) {
            e.printStackTrace();
            return dealResultMap(false, "上传失败");
        }
        return dealResultMap(true, "上传成功");
    }

    // 处理上传文件结果
    private Map<String, Object> dealResultMap(boolean success, String msg) {
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("success", success);
        result.put("msg", msg);
        return result;
    }
}
package com.jinxuliang.uploader_demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UploaderDemoApplication {

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

异常处理

Spring Boot默认异常处理机制
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.jinxuliang.exceptiondemo.controllers;

import com.jinxuliang.exceptiondemo.domain.User;
import com.jinxuliang.exceptiondemo.domain.UserNotFoundException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.Random;

@RestController
@RequestMapping(path = "/user")
public class UserController {
    private Random random=new Random();

    @GetMapping()
    public User getDefaultUserObject(){
        int ranValue=random.nextInt(100);
        User user=new User(ranValue,"user"+ranValue,"password");
        return user;
    }

    @GetMapping("/{id}")
    public User findById(@PathVariable("id") int id){
        throw new UserNotFoundException(id,"user not existed.");
    }


    @PostMapping(path="/withBindingResult",consumes = "application/json")
    public ResponseEntity createUser(@Valid @RequestBody User user, BindingResult result){
        if(result.hasErrors()){
            StringBuffer sb=new StringBuffer();
            result.getAllErrors().stream().forEach(err->{
                FieldError fieldError=(FieldError)err;
                sb.append(fieldError.getField());
                sb.append(" ");
                sb.append(fieldError.getDefaultMessage());
                sb.append("\n");
            });
            return new ResponseEntity<>(sb.toString(),HttpStatus.BAD_REQUEST);
        }
        else{
            return new ResponseEntity<>(user, HttpStatus.CREATED);
        }
    }

    @PostMapping(path="/noBindingResult",consumes = "application/json")
    public ResponseEntity<User> createUser(@Valid @RequestBody User user){
        System.out.println("从客户端Post上来的数据中创建对象:"+user);
        return new ResponseEntity<>(user, HttpStatus.CREATED);
    }
}
package com.jinxuliang.exceptiondemo.domain;

import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotBlank;

//加上了数据校验功能的数据实体类
public class User {
    private int id;
    @NotBlank(message = "姓名不能为空")
    private String name;
    @Length(min=1,max = 8,message = "密码为1到8个字符")
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public User(int id, String name, String password) {
        this.id = id;
        this.name = name;
        this.password = password;
    }

    public User() {
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
package com.jinxuliang.exceptiondemo.domain;

//自定义异常
public class UserNotFoundException
        extends RuntimeException {

    private int id;

    public UserNotFoundException(int id,String message){
        super(message);
        this.id=id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}
package com.jinxuliang.exceptiondemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ExceptionDemoApplication {

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

Spring Boot项目的默认异常处理机制,是由一个内置的BasicErrorController 实现的。
通过前面的介绍,可以看到这个默认处理机制能应付多数的场景,无需做太多的工作,就可以直接用在项目中。如果希望能针对特定的场景定义自己的异常处理机制,那也是有办法的,下面介绍相应的技术。
在这里插入图片描述

package com.jinxuliang.exceptiondemo.controllers;

import com.jinxuliang.exceptiondemo.domain.UserNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

//限定被标注为@Controller或者@RestController的类才被拦截
@ControllerAdvice(annotations = {Controller.class, RestController.class})
public class MyAdviceController {
    public MyAdviceController() {
    }

    // 异常处理,可以定义异常类型进行拦截处理
    @ExceptionHandler(value = UserNotFoundException.class)
    // 以JSON表达方式响应
    @ResponseBody
    // 定义为服务器错误状态码
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, Object> exception(HttpServletRequest request,
                                         UserNotFoundException ex) {
        Map<String, Object> msgMap = new HashMap<>();
        // 获取异常信息
        msgMap.put("code", ex.getId());
        msgMap.put("message", "发生时间:"+new Date());
        return msgMap;
    }
}

在这里插入图片描述

Spring Boot开发 RESTful Service

在互联网发展的早期阶段,浏览器是最主要的上网工具,相应地, Web 应用主要考虑到浏览器的需求。只要它能够在浏览器中正确显示,一切就 OK 。
由于在很长的一段时间内,浏览器的功能受限,它仅能承担网页渲染和一些简单脚本的执行工作,因此, Web Server需要负责生成所有 HTML 文档和提供数据资源的任务。
在这个时代,占主流的是各种MVC 框架, Web Server 负责生成好 HTML 代码之后,再发回给浏览器显示。
2010年以后,以手机为代表的智能设备异军突起,很快地,手机引发的互联网流量就超过了PC端引发的。
手机App的界面与用户使用体验,与浏览器中的网页,有着巨大的差异,并不必须使用HTML。
为了能同时支持手机和PC,传统互联网进化为了“移动互联网”,相应地,Web Server端应用的职责也发生了大的变化,Web Server慢慢地不再负责“组装”或“生成”用户界面,而将这个工作“下发”给客户端App负责。
在这里插入图片描述
在这里插入图片描述
REST的由来:RESTREST这个词,是 Roy Thomas Fielding 在他 2000 年的博士论文中提出的。Fielding 博士是 HTTP 协议( 1.0 版和 1.1 版)的主要设计者、 Apache 服务器软件的作者之一、 Apache 基金会的第一任主席。所以,他的这篇论文一经发表,就引起了关注, 并且对互联网开发产生了深远的影响。
Fielding将他对互联网软件的架构原则,命名为 REST(Representational State Transfer )。
如果一个架构符合REST 原则,就称它为 REST 风格架构。
在这里插入图片描述
HTTP method定义了若干种资源访问方式:

HTTP method说明
GET(VISIT)访问服务器资源(一个或者多个资源)。
POST(CREATE)提交服务器资源信息,用来创建新的资源。
PUT(UPDATE)修改服务器已经存在的资源,使用PUT时需要把资源的所有属性一并提交。
PATCH(UPDATE)修改服务器已经存在的资源,使用PATCH 时只需要将部分资源属性提交。
DELETE(DELETE)从服务器将资源删除。
HEAD获取资源的元数据
OPTIONS提供资源可供客户端修改的属性信息。

RESTful API设计
区分传统的Web API和RESTful API。

功能传统的Web APIverbRESTful APIverb
查询/user/query?name=tomGET/user?name=tomGET
详情/user/getInfo?id=1GET/user/1GET
创建/user/create?name=tomPOST/userPOST
修改/user/update?id=1&name=jerryPOST/user/1PUT
删除/user/delete?id=1GET/user/1DELETE

RESTful Service的特点:
使用URL 描述资源
使用HTTP 方法描述行为,使用 HTTP 状态码来表示不同的结果
使用json 交互数据
RESTful只是一种风格,并不是强制的标准

Richardson Maturity Model
链接
在这里插入图片描述
开发RESTful服务:
在这里插入图片描述
在这里插入图片描述
编写一个普通的JavaBean它将被用来展示RESTful Service的编写方法。

package com.jinxuliang.rest_demo.domain;

import javax.validation.constraints.NotBlank;
import java.util.Date;

//一个用于测试的类
public class MyClass {

    private int id;

    private String info;
    private Date date=new Date();
    @NotBlank
    private String secretInfo;

    public String getSecretInfo() {
        return secretInfo;
    }

    public void setSecretInfo(String secretInfo) {
        this.secretInfo = secretInfo;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

    public MyClass(int id, String info) {
        this.id = id;
        this.info = info;
    }

    public MyClass(){

    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    @Override
    public String toString() {
        return "MyClass{" +
                "id=" + id +
                ", info='" + info + '\'' +
                '}';
    }
}

在这里插入图片描述

package com.jinxuliang.rest_demo.controllers;

import com.jinxuliang.rest_demo.domain.MyClass;
import com.jinxuliang.rest_demo.domain.UserQueryCondition;
import com.jinxuliang.rest_demo.repositories.MyClassRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;



//指定本控制器中的方法是RESTful Service
@RestController
@RequestMapping(path="/myservice")
//允许跨域调用
@CrossOrigin(origins = "*")
public class MyServiceController {

    //返回一个JavaBean,将会被序列化为Json字符串
    @GetMapping(path="/hello")
    public MyClass hello(){
        return new MyClass(1,"Hello");
    }

    //传入路径参数
    @GetMapping("/find/{id}/{info}")
    public MyClass find(@PathVariable("id") int id,
                            @PathVariable("info") String info){
        return new MyClass(id,info);
    }

    //使用查询参数
    @GetMapping("/query")
    public String testQueryParam(@RequestParam(name="info",defaultValue = "hello") String message){
        return message;
    }

    @GetMapping("/querycondition")
    //合并多个查询参数为查询参数对象
    public UserQueryCondition testQueryObject(UserQueryCondition condition){
        //Do something...
        return  condition;
    }

    @GetMapping("/page")
    public Pageable testPageable(Pageable pageable){
        return pageable;
    }

    //将Repository注入到Controller中
    @Autowired
    MyClassRepository repository;

    @GetMapping("/find/{id}")
    public ResponseEntity<MyClass> findById(@PathVariable("id") int id){
        //查找指定Id对象
        MyClass obj=repository.findById(id);
        if(obj==null){
            //生成404响应
            return new ResponseEntity<>(obj,HttpStatus.NOT_FOUND);
        }
        //生成200响应
        return new ResponseEntity<>(obj,HttpStatus.OK);
    }

    //指定Post上来的数据必须是json,返回值也序列化为json
    @PostMapping(consumes = "application/json",
            produces = "application/json",
            path = "/post")
    public ResponseEntity<MyClass> postMyClass(@RequestBody MyClass obj){
        if(obj!=null){
            repository.save(obj);
            //设置location Header
            return new ResponseEntity<>(obj,HttpStatus.CREATED);
        }
        else{
            //非法数据,返回一个400响应
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }
    }

    //添加自定义的Header
    @GetMapping("/header/{value}")
    public ResponseEntity creatHeader(@PathVariable("value") String value){
        HttpHeaders headers=new HttpHeaders();
        headers.add("info",value);
        return new ResponseEntity(headers,HttpStatus.OK);
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
信息的发送方式
在REST风格的设计中,如果是简单的参数,往往会通过URL直接传递,在Spring MVC可以使用注解PathVariable进行获取,这样就能够满足REST风格传递参数的要求。
对于那些复杂的参数,例如,你需要传递一个复杂的资源需要十几个甚至几十个字段,可以考虑使用请求体JSON的方式提交给服务器,这样就可以使用注解RequestBody将 JSON数据集转换为Java对象。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
本讲介绍了如何使用Spring Boot 开发 Web 后端 RESTfulService 的基本编程技巧,掌握好这些内容,配合前面己经学会的内容,你就己经可以开发出能完成各种 Web 服务了。
RESTful Service
主要供 Web 前端和手机 App 所调用,比如,Web 前端应用可以使用 axios 这样的库访问 Spring Boot 所开发出来的 RESTful Service 。类似地, Android App 可以使用 Retrofit 来访问服务。
组合Web 前端、手机 App 和后端 RESTful Service ,开发一个移动互联应用的三大关键要素就齐备了。

声明

文章中所有截图,图片,代码等均来自于北京理工大学金旭亮老师,本人仅作笔记用途。
如需使用本文中任何资料请与教师联系。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhj12399

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值