1 WEB基础回顾
1.1 什么是协议?Java WEB的通信遵循什么协议?它基于什么协议?
1.2 HTTP中的请求和响应
1.3 浏览器的作用是什么?
1.4 WEB服务器、Servlet容器
1.5 Servlet的核心API
1.5.1 interface javax.servlet.Servlet
1.5.2 interface javax.servlet.ServletConfig
1.5.3 interface javax.servlet.ServletContext(Servlet上下文环境)
1.5.4 abstract javax.servlet.http.HttpServlet extends GenericServlet
1.6 常用API的处理者
1.7 一次请求的过程
1.7.1 服务器启动阶段
1.7.2 请求发出
2 餐厅案例
2.1 数据库表及字段
2.2 什么是数据源、数据库连接池、DBCP
2.2.1 数据源
2.2.2 数据库连接池
2.2.3 DBCP
2.2.4 Properties配置文件
2.3 代码测试
2.3.1 使用DBCP的DataSource获取数据连接
2.3.2 使用properties文件作为配置文件配合DBCP重构工厂类
软件开发中大概分成两种架构,C/S(Client/Server)客户端/服务器架构和B/S(Browser/Server)浏览器/服务器架构。而Java WEB大多基于后者,用来开发网站后台、管理系统等。
网络中的计算机进行数据交换遵循的规范,称为协议。在Java WEB中,客户端和服务器之间进行通信遵循HTTP协议。HTTP协议是建立在TCP协议基础之上(传输双发需要建立连接),基于请求响应模式的无状态的应用层协议。
什么是TCP/IP
互联网协议(英语:Internet Protocol Suite,缩写IPS)是一个网络通信模型,以及一整个网络传输协议家族,为互联网的基础通信架构。它常被通称为TCP/IP协议族(英语:TCP/IP Protocol Suite,或TCP/IP Protocols),简称TCP/IP。因为该协议家族的两个核心协议:TCP(传输控制协议)和IP(网际协议),为该家族中最早通过的标准。由于在网络通讯协议普遍采用分层的结构,当多个层次的协议共同工作时,类似计算机科学中的堆栈,因此又被称为TCP/IP协议栈(英语:TCP/IP Protocol Stack)。转自维基百科
上图中提到的4个层,应用层关注的是应用程序的细节,而不是数据在网络中的传输活动;其他三层主要处理所有的通信细节,对应用程序一无所知。
HTTP(超文本传输协议)
HTTP协议建立在TCP协议基础之上(传输双发需要建立链接),基于请求/响应模式的、无状态的应用层协议。所谓的无状态指的是前一次和后一次请求没有关系,若要建立关系,在Java中靠的是cookie和session、持久层建立关系。
TCP和UDP协议的区别:
在实现信息的可靠传递方面,TCP协议中包含了专门的传递保证机制,当数据接收方收到发送方传来的信息时,会自动向发送方发出确认消息;发送方只有在接收到该确认消息之后才继续传送其它信息,否则将一直等待直到收到确认信息为止(三次握手)。而UDP协议并不提供数据传送的保证机制。如果在从发送方到接收方的传递过程中出现数据报的丢失,协议本身并不能做出任何检测或提示。因此,通常人们把UDP协议称为不可靠的传输协议。
在处理接收突发性的多个数据包方面,在网络非常拥挤的情况下,UDP不能确保数据的发送和接收顺序,但这种乱序情况非常偶发;而TCP一定可以保证数据的发送接收顺序事实上。
虽然UDP是一种不可靠的传输协议,但在有些情况下UDP协议可能会变得非常有用。因为UDP具有TCP所望尘莫及的速度优势。虽然TCP协议中植入了各种安全保障功能,但是在实际执行的过程中会占用大量的系统开销,无疑使速度受到严重的影响。反观UDP由于排除了信息可靠传递机制,将安全和排序等功能移交给上层应用来完成,极大降低了执行时间,使速度得到了保证。
网络(网际)层
使用无连接的网际协议IP和许多种路由选择协议。负责为分组交换网上的不同主机提供通信服务,把传输层产生的报文段或用户数据报封装成分组(也叫IP数据报或数据报)或包进行传送。另一个任务就是选择合适的路由,尽可能快地将分组从源结点送到目的结点。
物理(链路)层
通常包括操作系统的设备驱动程序和计算机对应的网络接口卡,主要处理有关通信媒介的细节(如以太网,令牌环网等)。
能够发出标准的HTTP请求,返回标准的HTTP响应并解析响应正文显示的软件。
能够接受标准的HTTP请求并解析,发出标准的HTTP响应的软件。Tomcat就是一款免费、开源、纯Java实现的WEB服务器。
管理Servlet生命周期,提供Servlet的运行环境的类库(组件)。一般Servlet容器存在于WEB服务器中,Tomcat中有一个Servlet容器—Catalina。Calalina作为一种技术规范,Servlet容器是它的实现。
联动自己几天前发的文章:什么是Tomcat
服务器端Server.java
import java.io.*;
import java.net.*;
public class Server{
public static void main(String arg[])throws Exception{
//创建一个服务器端的ServerSocket对象,监听本机的8080端口
ServerSocket ss = new ServerSocket(8080);
//这句打印类似于Tomcat的启动时间在控制台的打印,只是这句是假的
System.out.println("server startup in 0.0001ms");
//不断监听8080端口,打印客户端发来的请求信息
while(true){
//创建客户端Socket对象,接收由客户端发来的请求信息
Socket so = ss.accept();
//创建字节输入输出流对象
InputStream is = so.getInputStream();
OutputStream os = System.out;
//创建一个长度,装载取出的字节数组的长度
int length;
//创建一个足够大的字节数组,
byte[] data = new byte[1024<<3];
//将发来的请求使用字节数组读到流中
//并遍历使用字节流按数组输出,直到输出完毕
while((length = is.read(data))!=-1){
os.write(data,0,length);
}
//真正的Tomcat在初始化时会将request、response的重要参数先设定好
//HttpServletRequest request = new HttpServletRequest();
//request.setMethod(GET);
//request.setRequestURI("/index.html");
//request.setProtocol();
//request.setHeader();
//request.setHeaderNames();
//request.setDateHeader();
//..
//reqeust.setParameter()//参数
//Response
//关闭输入输出流
is.close();
os.close();
}
}
}
客户端Client.java
import java.io.*;
import java.net.*;
public class Client
{
public static void main(String artgs[])throws Exception{
//创建一个客户端Socket对象,向127.0.0.1:8080发请求
Socket so = new Socket("127.0.0.1",8080);
//创建字节输出流,输出要发送的请求
OutputStream os = so.getOutputStream();
//按字节输出请求
os.write("大家好".getBytes());
//关闭输出流
os.close();
}
}
当运行Client.java时,服务端控制台输出的和浏览器访问时服务端控制台输出的内容如图:
因为这是一个单实例单线程的服务器,所以在服务端没有返回当前响应前,服务器端无法发送接收到下一个客户端的响应。
一个Socket是一个端点,普通Socket是客户端,ServerSocket是服务器端。
在真正的Tomcat这个WEB服务器中,由它自己启动main方法,监听服务器接到请求。在初始化时new一个Request对象,并初始化关于请求的信息,这些信息被存储在new出来的Request对象中。
据Catalina实现的servlet容器调用servlet方法来管理生命周期。Jasper包实现了JSP引擎。
该接口与Servlet的关系是一一对应的,ServletConfig主要用来存放Servlet一些私有的启动信息。
Servlet容器创建ServletConfig的对象,在Servlet类初始化时传递启动信息给Servlet。
方法列表:
1):getInitParameter()
2):getInitParameterNames()
3):getServletContext()
4):getServletName()
继承了Servlet类和ServletConfig类的抽象类GenericServlet
abstract Javax.servlet.GenericServlet implements Servlet,ServletConfig
(1)init(ServletConfig)用户一般继承重写空参的init()
init(ServletConfig config){
this.config = config;
setServletConfig();
init();//调用本类的init()(一般由用户继承编写)加载资源配置文件等。。
}
(2)abstract service(ServletRequest,ServletResponse) //由用户继承编写
(3)destroy()
(4)getServletConfig()
(5)getServletInfo()
(6)getInitParameter(String)
(7)getInitParameterNames()
(8)getServletContext()
(9)getServletName()
(10)init()
(11)log(String)
(12)log(String, Throwable)
API接口名 | 创建者new | 实现者implements | 调用者 |
---|---|---|---|
Servlet | 容器(反射) | 程序员 | 容器 |
ServletConfig | 容器 | 容器 | 程序员 |
ServletContext | 容器 | 容器 | 程序员 |
Request | 容器 | 容器 | 程序员 |
RequestDispatcher | 容器 | 容器 | 程序员 |
HttpSession | 容器 | 容器 | 程序员 |
Cookie | 程序员 | 容器 | 程序员 |
Filter | 容器 | 程序员 | 容器 |
Listener | 容器 | 程序员 | 容器 |
S1. 解析自身的配置文件(conf),如果有错,则服务器无法启动;如果没错,则继续解析webapps下所有的工程中的web.xml
S2. 如果web.xml解析有错,则该工程无法使用,但是服务器可以启动;没错,正常启动。到此时,web.xml中的信息已经解析完毕
S1. 浏览器地址栏输入网址,向网站的域名发出GET请求(地址栏只能发送GET请求 )
S2. 浏览器把请求地址打包成标准的请求数据报(字符串),通过网络发出域名,DNS域名解析器将域名解析为IP地址后发往服务器
S3. 服务器接收到标准的请求,解析请求,将数据报信息打包成HttpServletRequest对象(字符串转为对象);根据web.xml中的部署描述信息,找到对应的 Servlet类(url-pattern–>servletname–>找到指定的类),容器通过反射构造并调用Servlet类
S4. 查看内存中是否有指定类型的Servlet的对象,如果有则使用,如果没有则创建新的Servlet对象(容器使用反射创建对象)(Servlet是单实例多线程的)
S5. 容器调用init(ServletConfig)方法初始化,我们只需要重写空参init(){初始化代码}加载需要初始化的内容,初始化过程完毕
S6. 容器调用service()完成请求的处理。(servlet类中的protected service方法,容器根据发来的请求调用执行我们重写的doXX方法)
S7. 我们在doXX方法中,通过response对象写出响应信息。
S8. 服务器根据response对象中的内容生成标准的HTTP响应信息(response对象转为字符串)发送到客户端
S9. 客户端浏览器接收到标准的HTTP响应信息,解析并显示。
明天起进入密集的项目实训期,更新速度会慢一些,代码上传Github,这里会更新一些需求和技术介绍。
DataSource(数据源),javax.sql.DataSource接口。数据源就是用来获得链接的工厂,是前面工厂类用到的DriverManager类的替代者。
数据库连接池是一种实现数据源的解决方案,一种抽象的方式方法,而不是一个具体的实例。
DBCP是apache-commons提供的一款采用了数据库连接池方案的数据源的具体实现产品(一个类库),类似的连接池产品还有C3P0/Druid,作用均为提供多个可循环使用的数据库连接。具体用法见后面案例的代码。
Properties配置文件主要以Key-Value形式存放数据库或者系统的配置信息,在Java中使用util包中的Properties类(继承自HashTable类)可以加载写在.properties文件中的配置信息。具体用法见后面案例的代码。
省略实体类,前面的数据库建表中有描述。
package com.test.restaurant.test;
import org.apache.commons.dbcp.BasicDataSource;
import java.sql.Connection;
public class Test {
public static void main(String[] args) throws Exception{
//1.构造对象,实现数据源
BasicDataSource bds = new BasicDataSource();
//2.给属性赋值
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUrl("jdbc:mysql://127.0.0.1:3306/数据库名");
bds.setUsername("数据库账号(一般为root)");
bds.setPassword("数据库密码");
//3.获得连接
Connection conn = bds.getConnection();
System.out.println(conn);
}
}
获取到了连接池中的一个连接
尝试使用for循环获取十个连接但打印出来结果只得到八个,说明连接池默认最多可以得到八个连接,显然程序没有执行完。
可以通过在上面的代码中加bds.setMaxActive(10)修改为连接池允许用户获取最多十个连接。
设置最大等待时间,通过bds.setMaxWait(3000),这里等待3000ms,如果获取不到连接就抛出异常。
package com.test.restaurant.test;
import org.apache.commons.dbcp.BasicDataSource;
import java.sql.Connection;
public class Test {
public static void main(String[] args) throws Exception{
//1.构造对象,实现数据源
BasicDataSource bds = new BasicDataSource();
//2.给属性赋值
//必选项
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUrl("jdbc:mysql://127.0.0.1:3306/数据库名");
bds.setUsername("数据库账号(一般为root)");
bds.setPassword("数据库密码");
//可选项
//连接池最多允许用户获取10个连接
bds.setMaxActive(10);
//当连接不够时最多等待3000ms,超过就抛异常
bds.setMaxWait(3000);
//3.尝试获取11个连接
for(int i = 0; i < 11; i++) {
Connection conn = bds.getConnection();
System.out.println(conn);
}
}
}
在src目录下建立db.properties,key值并不是规定死的写法,在Java文件中可以调取到即可
##必选项
m.driver=com.mysql.jdbc.Driver
m.url=jdbc:mysql://127.0.0.1:3306/test
##或者m.url=jdbc:mysql://localhost:3306/test
##或者当url为localhost(127.0.0.1)且端口为3306时,可写为m.url=jdbc:mysql:///:3306/test
m.user=数据库账号(一般为root)
m.pwd=数据库密码
##可选项
m.maxActive=100
m.maxWait=3000
m.maxIdle=20
重构工厂类
package com.test.restaurant.common.factory;
import org.apache.commons.dbcp.BasicDataSource;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;
public class ConnectionFactory {
//具体实现基础的数据源
private static BasicDataSource bds = new BasicDataSource();
private ConnectionFactory(){}
//在静态代码块中加载db.properties的配置资源文件
static{
try {
Properties properties = new Properties();
//获取输入流,将配置资源文件以流的形式加载进来
//第一种找资源文件的方式
//以ConnectionFactory.class(类文件与.java文件目录结构相同)所在目录(包的内部)为起点,"../"为转到上一级目录,
//要找到db.properties文件,需要向上转五次
//InputStream is = ConnectionFactory.class.getResourceAsStream("../../../../../db.properties");
//第二种找资源文件的方式
//通过获取classpath的路径(类加载器的路径)直接从包外目录获取文件
InputStream is = ConnectionFactory.class.getClassLoader().getResourceAsStream("db.properties");
//加载文件内容
properties.load(is);
is.close();
//设置连接池属性
//properties对象中根据key获取加载进来的value值
//(用法同map的get()通过key取value,但是map返回Object,properties返回String)
bds.setDriverClassName(properties.getProperty("m.driver"));
bds.setUrl(properties.getProperty("m.url"));
bds.setUsername(properties.getProperty("m.user"));
bds.setPassword(properties.getProperty("m.pwd"));
bds.setMaxWait(Long.parseLong(properties.getProperty("m.maxWait")));
bds.setMaxActive(Integer.parseInt(properties.getProperty("m.maxActive")));
bds.setMaxIdle(Integer.parseInt(properties.getProperty("m.maxIdle")));
} catch (Exception e) {
e.printStackTrace();
}
}
//对外提供连接
public static Connection getConnection(){
try {
return bds.getConnection();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//对外提供数据源
public static DataSource getDataSource(){
return bds;
}
}
DBUtils底层如何将数据库的一条条数据转换为一个个对象?
DBUtils.java
package com.test.restaurant.test.utils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DBUtils {
public DBUtils(){}
//不想每次都在使用连接时向方法中传Connection的对象,就先在构造方法中定义它
private DataSource ds;
public DBUtils(DataSource ds){
this.ds = ds;
}
/*
* 可变参数:
* 1.放在参数列表最后
* 2.用法同数组
* */
//发现方法中代码复用频繁,提取出用于填充参数的方法
private void fillStatement(PreparedStatement pst, Object...params) throws Exception{
//给参数赋值
if(params != null){
for(int i = 0; i < params.length; i++){
//数据库对应的列参数索引从1开始,数组下标从0开始
pst.setObject(i + 1, params[i]);
}
}
}
//模拟DBUtils-->QueryRunner中的update方法
public Integer update(Connection conn, String sql, Object...params) throws Exception{
//被提取到另一个方法中
/* //S1.创建statement对象
PreparedStatement pst = conn.prepareStatement(sql);
//S2.给参数赋值
if(params != null){
for(int i = 0; i < params.length; i++){
//数据库对应的列参数索引从1开始,数组下标从0开始
pst.setObject(i + 1, params[i]);
}
}*/
//S1.创建Statement对象
PreparedStatement pst = conn.prepareStatement(sql);
//S2.给参数赋值
fillStatement(pst);
//S3.执行操作
//insert/delete/update均可以使用这个方法处理
return pst.executeUpdate();
}
//模拟DBUtils-->QueryRunner中的query方法
public List<Map<String, Object>> query(Connection conn, String sql, Object...params) throws Exception{
//S1.创建Statement对象
PreparedStatement pst = conn.prepareStatement(sql);
//S2.给参数赋值
fillStatement(pst);
//S3.接收查询到的结果
//创建对象接收Map,有几条记录就有几个Map
List<Map<String, Object>> resultList = new ArrayList<>();
//通过ResultSet获得行数(查到几条记录)
ResultSet rs = pst.executeQuery();
//获取结果集的元数据(字段名/字段类型/字段长度)
ResultSetMetaData rsmd = rs.getMetaData();
//获取列数
int count = rsmd.getColumnCount();
while(rs.next()){
Map<String, Object> map = new HashMap<>();
//遍历每一列(列的索引值从1开始)
for(int i = 1; i <= count; i++){
//获取列名
String cName = rsmd.getColumnName(i);
//获取列内容
Object obj = rs.getObject(cName);
map.put(cName, obj);
}
resultList.add(map);
}
return resultList;
}
//不需要传连接的update()
//模拟DBUtils-->QueryRunner中的update方法
public Integer update(String sql, Object...params) throws Exception{
//被提取到另一个方法中
/* //S1.创建statement对象
PreparedStatement pst = conn.prepareStatement(sql);
//S2.给参数赋值
if(params != null){
for(int i = 0; i < params.length; i++){
//数据库对应的列参数索引从1开始,数组下标从0开始
pst.setObject(i + 1, params[i]);
}
}*/
//S1.创建Statement对象
PreparedStatement pst = this.ds.getConnection().prepareStatement(sql);
//S2.给参数赋值
fillStatement(pst);
//S3.执行操作
//insert/delete/update均可以使用这个方法处理
return pst.executeUpdate();
}
public <T> T query(String sql, ResultSetHandler<T> rsh, Object...params) throws Exception{
//S1.创建Statement对象
PreparedStatement pst = this.ds.getConnection().prepareStatement(sql);
//S2.给参数赋值
fillStatement(pst, params);
//S3.接收查询到的结果
//通过ResultSet获得行数(查到几条记录)
ResultSet rs = pst.executeQuery();
//直接使用ResultSetHandler的实现类处理结果集
//比如 传入BeanListHandler,这里就返回BeanListHandler实现的handle方法
return rsh.handle(rs);
}
}
ResultSetHandler.java
package com.test.restaurant.test.utils;
import java.sql.ResultSet;
public interface ResultSetHandler<T> {
public <T> T handle(ResultSet rs);
}
接口的两个实现类,在查询时根据自己需求灵活传进这个接口的实现类
BeanListHandler.java
package com.test.restaurant.test.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.ResultSet;
public class BeanHandler<T> implements ResultSetHandler<T>{
private Class<T> cls;
public BeanHandler(Class<T> cls){
this.cls = cls;
}
//把任意的ResultSet转换为任意类型
@Override
public <T> T handle(ResultSet rs){
try {
while(rs.next()){
//通过反射的方式构造对象
//newInstance()构造对象会new这个类的空参构造方法
//很多框架底层也使用了反射
//所以实体类的空参构造方法务必要保证可用
T obj = (T)cls.newInstance();
//数据库中字段->属性
//获取这个类中所有属性对象
Field[] fs = cls.getDeclaredFields();
//f为实体类中的属性对象
for(Field f : fs) {
//属性名字
String attrName = f.getName();
//获得属性所属类的名字
Class filedType = cls.getDeclaredField(attrName).getType();
//拼出这个属性的setter方法名
String setter0 = "set" + attrName.substring(0, 1).toUpperCase() + attrName.substring(1);
//反射获取setter方法,setter代表setter方法的名字
//最后一个参数指的是setter方法所属类的类型,因为每个setter方法的返回值类型就是其类
Method setter = cls.getDeclaredMethod(setter0, filedType);
//给属性赋值
//obj的setter这个方法被执行
setter.invoke(obj, rs.getObject(attrName));
}
return obj;
}
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
BeanHandler.java
package com.test.restaurant.test.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.ResultSet;
public class BeanHandler<T> implements ResultSetHandler<T>{
private Class<T> cls;
public BeanHandler(Class<T> cls){
this.cls = cls;
}
//把任意的ResultSet转换为任意类型
@Override
public <T> T handle(ResultSet rs){
try {
while(rs.next()){
//通过反射的方式构造对象
//newInstance()构造对象会new这个类的空参构造方法
//很多框架底层也使用了反射
//所以实体类的空参构造方法务必要保证可用
T obj = (T)cls.newInstance();
//数据库中字段->属性
//获取这个类中所有属性对象
Field[] fs = cls.getDeclaredFields();
//f为实体类中的属性对象,f的值精确到了属性的名字,getName相当于获取属性名字
for(Field f : fs) {
//属性名字
String attrName = f.getName();
//获得属性所属类的名字
Class filedType = cls.getDeclaredField(attrName).getType();
//拼出这个属性的setter方法名
String setter0 = "set" + attrName.substring(0, 1).toUpperCase() + attrName.substring(1);
//反射获取setter方法,setter代表setter方法的名字
//最后一个参数指的是setter方法所属类的类型,因为每个setter方法的返回值类型就是其类
Method setter = cls.getDeclaredMethod(setter0, filedType);
//给属性赋值
//obj的setter这个方法被执行
setter.invoke(obj, rs.getObject(attrName));
}
return obj;
}
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}