V16部分
利用反射机制重构DispatcherServlet,使得将来添加新的业务时DispatcherServlet
不必再添加分支判断(不进行修改)
实现:
1:新建包com.webserver.annotation
2:在annotation包下添加两个注解
@Controller:用于标注哪些类是处理业务的Controller类
@RequestMapping:用于标注处理某个业务请求的业务方法
3:将com.webserver.controller包下的所有Controller类添加注解@Controller
并将里面用于处理某个业务的方法标注@RequestMapping并指定该方法处理的请求
4:DispatcherServlet在处理请求时,先扫描controller包下的所有Controller类
并找到处理该请求的业务方法,使用反射调用.
DispatcherServlet
package com.webserver.core;
import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import com.webserver.controller.Usercontroller;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
public class DispatcherServlet {
private static DispatcherServlet instance = new DispatcherServlet();
private static File root;
private static File staticDir;
static {
try {
root = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
staticDir = new File(root,"static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet(){}
public static DispatcherServlet getInstance(){
return instance;
}
public void service(HttpServletRequest request, HttpServletResponse response) {
//判断用户的请求路径不应当含有参数部分。所以uri不适用。
String path = request.getRequestURI();
//判断是否为请求业务
/*
当我们得到本次请求路径path的值后,我们首先要查看是否为请求业务:
1:扫描controller包下的所有类
2:查看哪些被注解@Controller标注的过的类(只有被该注解标注的类才认可为业务处理类)
3:遍历这些类,并获取他们的所有方法,并查看哪些时业务方法
只有被注解@RequestMapping标注的方法才是业务方法
4:遍历业务方法时比对该方法上@RequestMapping中传递的参数值是否与本次请求
路径path值一致?如果一致则说明本次请求就应当由该方法进行处理
因此利用反射机制调用该方法进行处理。
5:如果扫描了所有的Controller中所有的业务方法,均未找到与本次请求匹配的路径
则说明本次请求并非处理业务,那么执行下面请求静态资源的操作
*/
try {
File dir = new File(
DispatcherServlet.class.getClassLoader().getResource("./com/webserver/controller").toURI()
);
File[] subs = dir.listFiles(f -> f.getName().endsWith(".class"));
for (File sub : subs){
String fileName = sub.getName();
String className = fileName.substring(0,fileName.indexOf("."));
Class cls = Class.forName("com.webserver.controller"+className);
//是否为@Controller标注的类
if (cls.isAnnotationPresent(Controller.class)){
Method[] method = cls.getDeclaredMethods();
for (Method methods : method){
if (methods.isAnnotationPresent(RequestMapping.class)){
RequestMapping rm = methods.getAnnotation(RequestMapping.class);
String value = rm.value();
if (path.equals(value)){
Object obj = cls.newInstance();
methods.invoke(obj,request,response);
return;
}
}
}
}
}
}catch (Exception e){
e.printStackTrace();
}
File file = new File(staticDir, path);
if (file.isFile()) {//判断请求的文件真实存在且确定是一个文件(不是目录)
response.setContentFile(file);
} else {//404情况
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "404.html");
response.setContentFile(file);
response.addHeader("Content-Type", "text/html");
response.addHeader("Content-Length", "" + file.length());
}
response.addHeader("Server", "BirdServer");
}
}
v16结束
v17 DispatcherServlet
- 将DispatcherServlet中的判断是否为请求业务的逻辑代码,放在新创建的类HandlerMapping中,将被注解的全部方法对应的请求路径装在Map集合中。通过输入路径就可以调取相应的业务方法。
- 在DispatcherServlet中根据请求路径判断是否为请求业务,如果根据路径能够找到业务方法,则实列化业务逻辑类,并调用业务处理方法。
HandlerMapping
package com.webserver.core;
import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import java.io.File;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 用于维护所有请求路径与对应的业务处理类Controller的处理方法
*/
public class HandlerMapping {
/*
key:请求路径(方法上的注解@RequestMapping中的参数值)
value:处理该请求的方法(某Controller的一个方法)
*/
private static Map<String, Method> mapping = new HashMap<>();
static{
initMapping();
}
private static void initMapping(){
try {
/*
定位引入当前BirdBoot项目类的启动类所在的包
实际上SpringBoot的约定时:提供的所有Controller所在包至少要放到启动类所在的包里
*/
File root = new File(
BirdBootApplication.primarySource.getResource(".").toURI()
);
//定位启动类所在目录下的controller目录(所有Controller类都应当在这里)
File dir = new File(root,"controller");
if(!dir.exists()){//controller目录不存在则不进行初始化
return;
}
File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){
String fileName = sub.getName();
String className = fileName.substring(0,fileName.indexOf("."));
//由于约定所有Controller类都要在名为controller的包中,而这个包必须与启动类在同一个包里
String packageName = BirdBootApplication.primarySource.getPackage().getName();
Class cls = Class.forName(packageName+".controller."+className);
//是否为@Controller标注的类
if(cls.isAnnotationPresent(Controller.class)){
Method[] methods = cls.getDeclaredMethods();
for(Method method : methods){
//是否该方法被@RequestMapping标注
if(method.isAnnotationPresent(RequestMapping.class)){
RequestMapping rm = method.getAnnotation(RequestMapping.class);
String value = rm.value();
//将注解参数作为key,该方法对象作为value存储
mapping.put(value,method);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据给定的请求路径获取对应的处理方法
* @param path 应当与某个Controller中的业务方法上@RequestMapping注解值一致
* @return 与path匹配的处理方法或null
*/
public static Method getMethod(String path){
return mapping.get(path);
}
public static void main(String[] args) {
Method m = mapping.get("/regUser");
System.out.println(m);
}
}
DispatcherServlet
package com.webserver.core;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
/**
* DispatcherServlet实际是由SpringMVC框架提供的一个类,用于和Tomcat整合并负责
* 接手处理请求的工作。
* <p>
* Servlet是JAVA EE里的一个接口,译作:运行在服务端的小程序
* Servlet中有一个重要的抽象方法:
* public void service(HttpServletRequest request,HttpServletResponse response)
* 该方法用于处理某个服务
* <p>
* SpringMVC框架提供的DispatcherServlet就实现了该接口并重写了service方法,那么与Tomcat整合后,Tomcat在处理
* 请求的环节就可以调用DispatcherServlet的service方法将请求对象与响应对象传递进去由SpringMVC框架完成处理请求
* 的操作。
*/
public class DispatcherServlet {
private static DispatcherServlet instance = new DispatcherServlet();
private static File root;
private static File staticDir;
static {
try {
root = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
staticDir = new File(root, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet() {
}
public static DispatcherServlet getInstance() {
return instance;
}
public void service(HttpServletRequest request, HttpServletResponse response) {
//判断用户的请求路径不应当含有参数部分。所以uri不适用。
String path = request.getRequestURI();
//判断是否为请求业务
Method method = HandlerMapping.getMethod(path);
if(method!=null){
try {
//通过方法对象可以获取到该方法所属的类的类对象
Object obj = method.getDeclaringClass().newInstance();
method.invoke(obj,request,response);
} catch (Exception e) {
//若调用某个Controller的方法时出现了异常应当回复浏览器500错误
response.setStatusCode(500);
response.setStatusReason("Internal Server Error");
response.setContentFile(new File(staticDir,"500.html"));
}
}else {
File file = new File(staticDir, path);
if (file.isFile()) {//判断请求的文件真实存在且确定是一个文件(不是目录)
response.setContentFile(file);
} else {//404情况
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "404.html");
response.setContentFile(file);
}
}
response.addHeader("Server", "BirdServer");
}
}
V17结束
V18部分
- 创建线程池,优化线程管理。
- 线程池
线程池是线程的管理机制,主要解决两方面问题
1:重复使用线程
2:控制线程数量
package com.webserver.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BirdBootApplication {
private ServerSocket serverSocket;
private ExecutorService threadPool;
public BirdBootApplication(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
threadPool = Executors.newFixedThreadPool(50);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while (true) {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
//启动线程来与该客户端交互
ClientHandler handler = new ClientHandler(socket);
threadPool.execute(handler);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BirdBootApplication application = new BirdBootApplication();
application.start();
}
}
V18结束
V19部分
- 为了数据便于管理,将客户端输入的数据放入数据库中,与数据库建立连接-JDBC。
第一步:注册驱动
作用是告知java程序即将要连接的数据库厂商品牌(MySQL、Oracle、SQLserver)。
第二步:获取连接
打开JVM进程与数据库进程之间的通道,属于进程间通信,重量级。
第三步:获取数据库操作对象
专门执行SQL语句的对象。
第四步:执行SQL语句(DQL、DML)
第五步:处理查询结果
只有当第四步执行的是select语句时,才会处理查询后的结果集。
第六步:释放资源
使用完毕,一定要及时关闭连接。
JDBC java数据库连接 Java Database Connectivity
JAVA在JDBC中提供一套通用的接口,用于连接和操作数据库。不同的数据库厂商都提供了一套对应的实现类来操作自家提供的
DBMS。而这套实现类也称为连接该DBMS的驱动(Driver)
使用步骤:
1:加载对应数据库厂商提供的驱动(MAVEN添加依赖即可)
2:基于标准的JDBC操作流程操作该数据库
加入依赖代码如下:
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
</dependencies>
-
当调用Connection的方法:prepareStatement(String sql)
就会先将预编译SQL发送给数据库,使之生成一个执行计划。
意思:当数据库接收到这条SQL时就会理解该SQL的作用,并生成一个内置的函数用来执行SQL表达的操作
准备后并不会立即执行,因为还缺少数据(预编译SQL中?部分是未知的)
当执行计划生成后,数据库方对于该SQL的语义就定死了。
方法返回的PreparedStatement实例表示的就是数据库端已经生成的执行计划。
我们现在需要做得就是通过PreparedStatement传递"?"对应的值就可以让数据库执行一次该执行计划。 -
UserController中处理业务逻辑代码将输出到users文件中,改为与数据库进行交互。
- 注册逻辑:
首先使用预编译查询数据库中是否有重复用户名,如有重复,则响应用户已存在页面,否则使用预编译sql将请求中的数据放入数据库中,响应注册成功页面。 - 登录逻辑:
使用预编译文件判断接收到的用户名和密码,与数据库是否一致,若成功,则响应登陆成功页面,若失败,则响应登陆成功页面。
DBUtil
- 注册逻辑:
package com.webserver.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* 管理数据库连接
*/
public class DBUtil {
static {
try {
//DBUtil第一次被使用时先加载数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/birdboot?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true","root","root");
}
}
UserController
package com.webserver.controller;
import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import com.webserver.entity.User;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import com.webserver.util.DBUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.URISyntaxException;
import java.sql.*;
/**
* 用于处理用户相关业务
*/
@Controller
public class UserController {
@RequestMapping("/regUser")
public void reg(HttpServletRequest request, HttpServletResponse response){
System.out.println("开始处理用户注册!!!!!!!!!!");
//对应的是reg.html页面上<input name="username">
String username = request.getParameter("username");
String password = request.getParameter("password");
String nickname = request.getParameter("nickname");
String ageStr = request.getParameter("age");
//必要验证工作
if(username==null||username.isEmpty()||password==null||password.isEmpty()||
nickname==null||nickname.isEmpty()||ageStr==null||ageStr.isEmpty()||
!ageStr.matches("[0-9]+")){
//要求浏览器查看错误提示页面
response.sendRedirect("/reg_info_error.html");
return;
}
System.out.println(username+","+password+","+nickname+","+ageStr);
int age = Integer.parseInt(ageStr);
//2
/*
将该注册用户插入到数据库userinfo表中。
插入成功后,响应注册成功页面
*/
try (
Connection conn = DBUtil.getConnection();
){
//首先判断userinfo包中是否已经用该用户名的记录?如果有,则直接提示用户:have_user.html
/*
1:首先执行DQL语句取userinfo下寻找是否有该用户的记录
2:判断查询结果集是否存在一条记录,若有则说明为重复用户
*/
String sql1 = "SELECT username FROM userinfo WHERE username=?";
PreparedStatement ps = conn.prepareStatement(sql1);
ps.setString(1,username);
ResultSet rs = ps.executeQuery();
if(rs.next()){//结果集若存在记录,该用户已存在。
response.sendRedirect("/have_user.html");
return;
}
String sql2 = "INSERT INTO userinfo(username,password,nickname,age) " +
"VALUES (?,?,?,?)";
ps = conn.prepareStatement(sql2);
ps.setString(1,username);
ps.setString(2,password);
ps.setString(3,nickname);
ps.setInt(4,age);
int sum = ps.executeUpdate();
if(sum>0){
response.sendRedirect("/reg_success.html");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
@RequestMapping("/loginUser")
public void login(HttpServletRequest request,HttpServletResponse response){
System.out.println("开始处理登录!!!");
/*
1:通过request获取表单信息
2:验证表单信息是否输入有误,有误则跳转输入有误的提示页面
3:连接数据库去userinfo表中根据输入的用户名和密码检索记录
3.1:若查询出记录,则表明登录成功
3.2:若没有查询出记录,则登录失败
*/
//1
String username = request.getParameter("username");
String password = request.getParameter("password");
//2
if(username==null||username.isEmpty()||password==null||password.isEmpty()){
response.sendRedirect("/login_info_error.html");
return;
}
//3
try (
Connection conn = DBUtil.getConnection();
){
/*
username=123123
password=123' OR '1'='1
当将上述的密码进行拼接SQL后,发现语法含义被改变了。
这样的情况被称为【SQL注入攻击】
String sql = "SELECT id,username,password,nickname,age " +
"FROM userinfo " +
"WHERE username='"+username+"' AND password='"+password+"'";
SELECT id,username,password,nickname,age
FROM userinfo
WHERE username='123123' AND password='123' OR '1'='1'
将数据拼接到SQL中有两个弊端:
1:有SQL注入攻击的安全隐患
2:编码复杂,可读性差
*/
String sql = "SELECT id,username,password,nickname,age " +
"FROM userinfo " +
"WHERE username=? AND password=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1,username);
ps.setString(2,password);
ResultSet rs = ps.executeQuery();
if(rs.next()){
response.sendRedirect("/login_success.html");
}else{
response.sendRedirect("/login_fail.html");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
V19结束
V20部分
- DBUtil管理数据库类中,设置阿里提供的连接池,对jdbc进行相应的设置。
DBUtil
package com.webserver.util;
import com.alibaba.druid.pool.DruidDataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* 管理数据库连接
*/
public class DBUtil {
//阿里提供的连接池(重复使用连接,管理连接数量)
private static DruidDataSource dds;
static {
init();
}
private static void init(){
dds = new DruidDataSource();//实例化连接池
//设置数据库用户名,密码。最大连接数,初始连接数。连接数据的URL
dds.setUsername("root");
dds.setPassword("root");
dds.setUrl("jdbc:mysql://localhost:3306/birdboot?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true");
dds.setInitialSize(10);//初始连接数
dds.setMaxActive(20);//最大连接数
}
public static Connection getConnection() throws SQLException {
/*
连接池返回的Connection是对真实数据库连接的2次封装,它返回这个连接的close方法不是真将的连接关闭,
而是相当于将连接还给连接池。
*/
return dds.getConnection();
}
}
V20结束
Springboot简码项目结束 😁