目录
一、DispatcherServlet处理请求时,完成用户注册业务
HttpServletRequest类:解析工作,添加对浏览器提交表单数据的解析
HttpServletResponse类:定义一个重定向方法:sendRedirect()
如果请求为一个业务,则通过UserController类来处理业务;如果请求为一个静态目录static下的文件,文件若存在就响应这个文件,没有则响应404页面。
HTTPServletRequest类:表单提交中文时避免乱码URLDecoder.decode(str,"utf-8")
元注解:JAVA有几个内置的注解是用来为我们自定义的注解添加某种特性的
ReflectDemo7类:判断当前类是否被注解标注:isAnnotationPresent(注解类.class)
AutoRunMethod注解类:定义注解类,在注解类中添加参数
Person类: 当前类的方法上定义注解类AutoRunMethod
ReflectDemo8类:获取注解参数 ,getAnnotation()
Test1:实例化与当前类Test1在同一包中 的所有类中 的无参方法
Test2:实例化与当前类Test2在同一包中被@AutoRunClass标注的类中所有无参方法
Test3:自动调用与当前类Test3在同一包中被注解@AutoRunClass标注的类的被@AutoRunMethod标注的方法指定次数
RequestMapping注解类: 用来标注某个Controller中处理某个请求的方法
DispatcherServlet类:用注解处理请求业务,使得将来添加新的业务时DispatcherServlet不必再添加分支判断
八、重构代码:DispatcherServlet处理请求仅扫描一次controller包
HandlerMapping类:获取@RequestMapping上的参数,该参数记录这该方法处理的请求路径
DispatcherServlet类:利用注解,反射机制处理业务
BirdBootApplication主启动类:用线程池创建线程
一、DispatcherServlet处理请求时,完成用户注册业务
本版本完成表单的提交以及提交后的请求的解析工作。这个工作是通用操作,无论将来处理何种业务,解析表单数据的方式都是相同的。
这里涉及的知识点:
1:页面表单GET请求提交,uri有两种情况:不含有参数的(/index.html),有参数的
2:DispatcherServlet如何区分请求是处理注册还是请求一个静态资源(页面,图片等)
实现:重构HttpServletRequest的解析工作,添加对表单数据的解析。
1:定义三个新的属性:String requestURI,String queryString,Map parameters
分别保存抽象路径中的请求部分,参数部分和每一组参数
2:定义parseUri方法,进一步解析抽象路径中包含的参数内容
3:在解析请求的方法中解析出三部分后调用parseUri进一步解析uri
HttpServletRequest类:解析工作,添加对浏览器提交表单数据的解析
package com.webserver.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**HTTPServletRequest
* 请求对象,该类的每一个实例用于标识HTTP协议规定的请求内容
* 一个请求由三部分构成:
* 1:请求行
* 2:消息头
* 3:消息正文
*/
public class HttpServletRequest {
private Socket socket;
//请求行的相关信息
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议版本
private String requestURI;//uri中的请求部分,即:"?"左侧内容
private String queryString;//uri中的参数部分,即:"?"右侧内容
private Map<String,String> parameters = new HashMap<>();//每一组参数
//消息头的相关信息
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//1解析请求行
parseRequestLine();
//2解析消息头
parseHeaders();
//3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
if(line.isEmpty()){//若请求行为空字符串
//抛出空请求异常
throw new EmptyRequestException();
}
System.out.println("请求行内容:"+line);
String[] data = line.split("\\s");
method = data[0]; //请求方式
uri = data[1]; //抽象路径
protocol = data[2]; //协议版本
//进一步解析uri
parseURI();
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//进一步解析uri
private void parseURI(){
/*
uri有两种情况:
1:不含有参数的
例如: /index.html
直接将uri的值赋值给requestURI即可.
2:含有参数的
例如:/regUser?username=fancq&password=&nickname=chuanqi&age=22
将uri中"?"左侧的请求部分赋值给requestURI
将uri中"?"右侧的参数部分赋值给queryString
将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值
并将参数名作为key,参数值作为value存入到parameters中。
如果表单某个输入框没有输入信息,那么存入parameters时对应的值应当保存为空字符串
*/
//将抽象路径以?拆分
String[] data = uri.split("\\?");
//第一个值一定是抽象路径
requestURI = data[0];
if(data.length>1){//数组长度>1说明"?"后面有参数
queryString = data[1];
//拆分每一组参数
data = queryString.split("&"); //[username=sa,password=123...]
for(String para : data){
//para username=fancq 或 username=
String[] arr = para.split("=",2); //[username,sa]
parameters.put(arr[0],arr[1]);
}
}
System.out.println("requestURI:"+requestURI);
System.out.println("queryString:"+queryString);
System.out.println("parameters:"+parameters);
}
//解析消息头
private void parseHeaders() throws IOException {
while(true) {
String line = readLine();
if(line.isEmpty()){//如果读取的消息头是空字符串,说明单独读取到了CRLF
break;
}
System.out.println("消息头:"+line);
//将消息头按照": "拆分为名字和值并作为key,value存入到headers中
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
System.out.println("headers:"+headers);
}
//解析消息正文
private void parseContent(){}
private String readLine() throws IOException {
//调用同一个socket对象若干次getInputStream()方法返回的始终是同一条输入流
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();
char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
int d;
while((d = in.read())!=-1){
cur = (char)d;//本次读取到的字符
if(pre==13&&cur==10){//若上次读取的是回车符并且本次读取的是换行符
break;
}
builder.append(cur);//拼接本次读取到的字符
pre = cur;//进入下次循环前将本次读取的字符记作上次读取的字符
}
return builder.toString().trim();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
/**
* 根据消息头的名字获取对应的值
* @param name
* @return
*/
public String getHeader(String name) {
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
split方法:根据匹配给定的正则表达式来拆分字符串
语法:public String[] split(String regex, int limit)
注意: . 、 $、 | 和 * 等转义字符,必须得加 \\。
注意:多个分隔符,可以用 | 作为连字符。
package com.webserver.test;
import java.util.Arrays;
public class SplitDemo {
public static void main(String[] args) {
String line = "1=2=3=4=5=6=======";
String[] array = line.split("=");
//array:{1,2,3,4,5,6}
System.out.println(Arrays.toString(array));
//limit=2 表达仅拆分为两项
array = line.split("=",2);
//array:{1,2=3=4=5=6=======}
System.out.println(Arrays.toString(array));
//limit=3 表达仅拆分为三项
array = line.split("=",3);
//array:{1,2=3=4=5=6=======}
System.out.println(Arrays.toString(array));
//当可拆分项不足limit时,仅保留所有可拆分项
array = line.split("=",100);
//array:{1, 2, 3, 4, 5, 6, , , , , , ,}
System.out.println(Arrays.toString(array));
//当limit为0时,作用与split(String regex)一致
array = line.split("=",0);
//array:{1, 2, 3, 4, 5, 6}
System.out.println(Arrays.toString(array));
//limit为负数时为应拆尽拆并且全保留。
array = line.split("=",-1);
//array:{1, 2, 3, 4, 5, 6, , , , , , , }
System.out.println(Arrays.toString(array));
}
}
二、实现重定向
内部跳转问题:重复提交表单请求、处理注册、直接响应地址,这样会浪费CPU性能。
即:浏览器上的地址栏中地址显示的是提交表单的请求,而实际看到的是注册结果的提示页面。
这有一个问题,如果此时浏览器刷新,会重复上一次的请求,即:再次提交表单请求注册业务。
问题解决:使用重定向。
重定向是当我们处理完请求后,不直接响应一个页面,而是给浏览器回复一个路径,让其再次根据
该路径发起请求。这样一来,无论用户如何刷新,请求的都是该路径。避免表达的重复提交。
重定向流程图:
实现:
1:在HttpServletResponse中定义一个重定向方法:sendRedirect()该方法中设置状态代码为 302,并在响应头中包含Location指定需要浏览器重新发起请求的路径。
HttpServletResponse类:定义一个重定向方法:sendRedirect()
package com.webserver.http;
import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**HTTPServletResponse
* 响应对象
* 该类的每一个实例用于表示HTTP协议规定的响应。
* 一个响应由三部分构成:状态行,响应头,响应正文。
*/
public class HttpServletResponse {
private static MimetypesFileTypeMap mime = new MimetypesFileTypeMap();
private Socket socket;
//状态行相关信息
private int statusCode = 200;//状态代码
private String statusReason = "OK";//状态描述
//响应头相关信息
private Map<String,String> headers = new HashMap<>();
//响应正文相关信息
private File contentFile;//响应正文对应的实体文件
public HttpServletResponse(Socket socket){
this.socket = socket;
}
/**
* 该方法会将当前响应对象内容以标准的HTTP响应格式通过socket获取的输出流发送给
* 对应的客户端。
*/
public void response() throws IOException {
//发送状态行
sendStatusLine();
//发送响应头
sendHeaders();
//发送响应正文
sendContent();
}
//发送状态行
private void sendStatusLine() throws IOException {
println("HTTP/1.1"+" "+statusCode+" "+statusReason);
}
//发送响应头
private void sendHeaders() throws IOException {
/*
headers
key value
Content-Type text/html
Content-Length 245
Server BirdWebServer
... ...
*/
//遍历headers发送每一个响应头
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e : entrySet){
String key = e.getKey();
String value = e.getValue();
println(key+": "+value);
}
//单独发送回车+换行表达响应头发送完毕
println("");
}
//发送响应正文
private void sendContent() throws IOException {
OutputStream out = socket.getOutputStream();
if(contentFile!=null) {//如果正文文件不为空才发送内容
try (
FileInputStream fis = new FileInputStream(contentFile);
) {
byte[] data = new byte[1024 * 10];
int len;
while ((len = fis.read(data)) != -1) {
out.write(data, 0, len);
}
}
}
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
out.write(line.getBytes(StandardCharsets.ISO_8859_1));
out.write(13);
out.write(10);
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
public File getContentFile() {
return contentFile;
}
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
addHeader("Content-Type",mime.getContentType(contentFile));
addHeader("Content-Length",contentFile.length()+"");
}
/**
* 添加一个响应头
* @param name 响应头的名字
* @param value 响应头的值
*/
public void addHeader(String name,String value){
headers.put(name,value);
}
/**
* 重定向到指定路径
* @param location
*/
public void sendRedirect(String location){
/*
重定向的响应要求:
1:状态代码为302
2:包含一个响应头Location,用于指定要求浏览器重定向的路径
*/
statusCode = 302;
statusReason = "Moved Temporarily";
addHeader("Location",location);
}
}
三、实现注册登录业务
用户注册业务大致流程:
1:用户访问注册页面,并在页面上输入注册信息后点击注册按钮
2:数据提交发到服务端,服务端解析页面提交上来的数据
3:根据解析出来的数据进行响应的注册处理
4:给用户回复一个注册处理结果的页面(注册成功或失败)
用户登录业务大致流程:
1:用户在首页上点击超链接来到登录页面:login.html
2:在登录页面上输入用户名和密码并点击登录按钮提交
3:服务端处理登录逻辑,并响应登录结果页面(登录成功或失败)
使用当前User类实例表示一个注册用户
package com.webserver.entity;
import java.io.Serializable;
/**
* 使用当前类实例表示一个注册用户
*/
public class User implements Serializable {
private String username;
private String password;
private String nickname;
private int age;
public User(){}
public User(String username, String password, String nickname, int age) {
this.username = username;
this.password = password;
this.nickname = nickname;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", nickname='" + nickname + '\'' +
", age=" + age +
'}';
}
}
实现:
1:在userController中添加一个处理注册业务的方法:reg
- 获取表单信息,进行必要验证,如果信息输入不符合规范则重定向到错误页面。
- 将表单信息放入到user对象中,并创建以username名.obj形式命名的文件名。
- 如果文件存在,则用户已经被注册过了,重定向到用户名存在页面;如果不存在则用IO流进行对象序列化,将表单信息写入文件,重定向到注册成功页面。
2:在userController中添加一个处理登录业务的方法:login
- 通过resquest获取登录页面上用户输入的用户名和密码,获取后用户名和密码为null的话,响应登录信息输入有误页面。
- 根据users目录下查看该obj文件是否存在,不存在则没有此用户,响应登录失败页面。
- 如果文件存在,则反序列化User对象,然后比较该User对象(注册时输入的密码)和当前表单提交的密码是否一致,若一致响应登录成功页面,否则响应登录失败页面。
UserController类:实现注册登录业务功能
package com.webserver.controller;
import com.webserver.entity.User;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.*;
/*
实现具体注册登录业务
*/
public class UserController {
private static File userDir;//用来表示存放所有用户信息的目录
static {
userDir = new File("./users");
if(!userDir.exists()){
userDir.mkdirs();
}
}
public void reg(HttpServletRequest request, HttpServletResponse response){
System.out.println("开始处理注册!!!!");
//获取表单信息
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.将获取到的表单信息放到user中
User user = new User(username,password,nickname,age);
//参数1:userDir表示父目录 参数2:userDir目录下的子项
File file = new File(userDir,username+".obj");
if(file.exists()){//文件存在则说明该用户已经注册过了
response.sendRedirect("/have_user.html");
return;
}
try (
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
){
//将表单信息写入文件中
oos.writeObject(user);
//利用响应对象要求浏览器访问注册成功页面
response.sendRedirect("/reg_success.html");
} catch (IOException e) {
e.printStackTrace();
}
}
public void login(HttpServletRequest request, HttpServletResponse response){
System.out.println("开始处理登录!!!");
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println(username+","+password);
//必要的验证工作
if(username==null||username.trim().isEmpty()||
password==null||password.trim().isEmpty()){
response.sendRedirect("login_info_error.html");
return;
}
File file = new File(userDir,username+".obj");
if(file.exists()){//用户名是否存在(是否为一个注册用户)
try (
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
){
User user = (User)ois.readObject();//读取回来的是注册用户信息
//比较登录的密码和该注册用户的密码是否一致
if(user.getPassword().equals(password)){
//登录成功
response.sendRedirect("/login_success.html");
return;
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
//登录失败
response.sendRedirect("/login_fail.html");
}
}
DispatcherServlet类:
如果请求为一个业务,则通过UserController类来处理业务;如果请求为一个静态目录static下的文件,文件若存在就响应这个文件,没有则响应404页面。
package com.webserver.core;
import com.webserver.controller.UserController;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.net.URISyntaxException;
/**DispatcherServlet
* 这个类是SpringMVC框架与tomcat容器整合的一个关键类,接管了处理请求的工作。
* 这样当tomcat将请求对象和响应对象创建完毕后在处理请求的环节通过调用这个类来完成,从而
* 将处理请求交给了SpringMVC框架
* 并在处理后发送响应给浏览器
*/
public class DispatcherServlet {
private static DispatcherServlet instance = new DispatcherServlet();
private static File rootDir;
private static File staticDir;
static{
try {
//rootDir表示类加载路径:target/classes目录
rootDir = new File(
DispatcherServlet.class.getClassLoader()
.getResource(".").toURI()
);
//定位static目录(static目录下存放的是所有静态资源)
staticDir = new File(rootDir,"static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet(){}
public static DispatcherServlet getInstance(){
return instance;
}
public void service(HttpServletRequest request, HttpServletResponse response){
String path = request.getRequestURI();
//首先判断是否为请求某个特定的业务
if("/regUser".equals(path)){
UserController controller = new UserController();
controller.reg(request, response);
}else {
File file = new File(staticDir, path);
if (file.isFile()) {//根据用户提供的抽象路径去static目录下定位到一个文件
response.setContentFile(file);
response.addHeader("Server", "BirdWebServer");
} else {
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "/root/404.html");
response.setContentFile(file);
}
}
}
}
四、浏览器传递中文问题
表单使用GET形式提交时,如果参数含有中文,则不符合HTTP协议要求。
原因:
表单数据提交时会被包含在请求的抽象路径中,而HTTP协议要求请求的请求行和消息头符合的字符集必须为ISO8859-1。这是一个欧洲字符集,里面不支持中文。
解决:
思路:用ISO8859-1支持的字符去表达其不支持的字符。
例如:传递汉字:"范"。支持中文最常见的字符集为:UTF-8。
浏览器可以这样做:
先将 " 范 " 用UTF-8编码转换为2进制:11101000 10001100 10000011(3个字节)
2进制1和0可以用字符 ' 1 ' 和 ' 0 ' 描述。而ISO8859-1支持英文,数字,符号。既然支持数字类型的字符,那么我们就可以用数字类型的字符 ' 1 ',' 0 ' 传递UTF-8编码的汉字。
/regUser?username=范&password=.....
/regUser?username=111010001000110010000011&password=.....
'范'->'1''1''1''0''1''0''0''0'..... 问题虽然解决,但是新的问题出现了:太长
字符越多意味着网络间传输速度越慢,且开销大。
解决办法:将2进制用16进制形式表达:
2进制 | 10进制 | 16进制 |
0000 | 0 | 0 |
0001 | 1 | 1 |
0010 | 2 | 2 |
0011 | 3 | 3 |
0100 | 4 | 4 |
0101 | 5 | 5 |
0110 | 6 | 6 |
0111 | 7 | 7 |
1000 | 8 | 8 |
1001 | 9 | 9 |
1010 | 10 | A |
1011 | 11 | B |
1100 | 12 | C |
1101 | 13 | D |
1110 | 14 | E |
1111 | 15 | F |
"范" 的2进制:1110 1000 1000 1100 1000 0011
16进制: E 8 8 C 8 3/regUser?usenrame=111010001000110010000011&password=...
/regUser?usenrame=E88C83&password=...长度问题解决了,新的问题:如何与实际的英文数字组合区分呢?
例如:有一个人注册时,名字就想叫"E88C83"
解决办法:URL格式要求,如果字母与数字的组合表达的是16进制内容,则 要求每2位16进制前必须添加"%"
因此,若传递的是:
/regUser?usenrame=E88C83&password=...
说明此人就叫“E88C83”
若传递的是:
/regUser?usenrame=%E8%8C%83&password=...
则说明这是16进制的内容,因此要将E8 8C 83翻译成2进制11101000 10001100 10000011
然后再按照UTF-8编码还原为字符 '范'
表单使用GET形式提交时,如果参数含有中文时的解决办法:
str = URLDecoder.decode(str,"utf-8");
测试:
package com.webserver.test; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; /* 表单使用GET形式提交时,如果参数含有中文时的解决办法 */ public class URLDecoderDemo { public static void main(String[] args) { String str = "范"; byte[] data = str.getBytes(StandardCharsets.UTF_8); //11101000 10001100 10000011 //E8 8C 83 System.out.println(Arrays.toString(data)); str = "/regUser?username=%E8%8C%83%E4%BC%A0%E5%A5%87&password=123456"; try { str = URLDecoder.decode(str,"utf-8"); System.out.println(str); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }
HTTPServletRequest类:表单提交中文时避免乱码URLDecoder.decode(str,"utf-8")
package com.webserver.http;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
/**HTTPServletRequest
* 请求对象,该类的每一个实例用于标识HTTP协议规定的请求内容
* 一个请求由三部分构成:
* 1:请求行
* 2:消息头
* 3:消息正文
*/
public class HttpServletRequest {
private Socket socket;
//请求行的相关信息
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议版本
private String requestURI;//uri中的请求部分,即:"?"左侧内容
private String queryString;//uri中的参数部分,即:"?"右侧内容
private Map<String,String> parameters = new HashMap<>();//每一组参数
//消息头的相关信息
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//1解析请求行
parseRequestLine();
//2解析消息头
parseHeaders();
//3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
if(line.isEmpty()){//若请求行为空字符串
//抛出空请求异常
throw new EmptyRequestException();
}
System.out.println("请求行内容:"+line);
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//进一步解析uri
parseURI();
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//进一步解析uri
private void parseURI(){
/*
uri有两种情况:
1:不含有参数的
例如: /index.html
直接将uri的值赋值给requestURI即可.
2:含有参数的
例如:/regUser?username=fancq&password=&nickname=chuanqi&age=22
将uri中"?"左侧的请求部分赋值给requestURI
将uri中"?"右侧的参数部分赋值给queryString
将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值
并将参数名作为key,参数值作为value存入到parameters中。
如果表单某个输入框没有输入信息,那么存入parameters时对应的值应当保存为空字符串
*/
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){//数组长度>1说明"?"后面有参数
queryString = data[1];
//对参数部分进行转码
try {
queryString = URLDecoder.decode(queryString,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//拆分每一组参数
data = queryString.split("&");
for(String para : data){
//para username=fancq 或 username=
String[] arr = para.split("=",2);
parameters.put(arr[0],arr[1]);
}
}
System.out.println("requestURI:"+requestURI);
System.out.println("queryString:"+queryString);
System.out.println("parameters:"+parameters);
}
//解析消息头
private void parseHeaders() throws IOException {
while(true) {
String line = readLine();
if(line.isEmpty()){//如果读取的消息头是空字符串,说明单独读取到了CRLF
break;
}
System.out.println("消息头:"+line);
//将消息头按照": "拆分为名字和值并作为key,value存入到headers中
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
System.out.println("headers:"+headers);
}
//解析消息正文
private void parseContent(){}
private String readLine() throws IOException {
//调用同一个socket对象若干次getInputStream()方法返回的始终是同一条输入流
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();
char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
int d;
while((d = in.read())!=-1){
cur = (char)d;//本次读取到的字符
if(pre==13&&cur==10){//若上次读取的是回车符并且本次读取的是换行符
break;
}
builder.append(cur);//拼接本次读取到的字符
pre = cur;//进入下次循环前将本次读取的字符记作上次读取的字符
}
return builder.toString().trim();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
/**
* 根据消息头的名字获取对应的值
* @param name
* @return
*/
public String getHeader(String name) {
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
五、当浏览器请求方式为POST时
当页面form表单中包含用户隐私信息或有附件上传时,应当使用POST形式提交。
POST会将表单数据包含在请求的消息正文中。 如果表单中没有附件,则正文中包含的表单数据就是一个字符串,而格式就是原GET形式 提交时抽象路径中"?"右侧的内容。
实现:完成HttpServletRequest中的解析消息正文的方法 当页面(reg.html或login.html)中form的提 交方式改为POST时,表单数据被包含在正文里,并且 请求的消息头中会出现 Content-Type和Content-Length用于告知服务端正文内容。 因此我们可以根据它们来解 析正文。
package com.webserver.http;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**HTTPServletRequest
* 请求对象,该类的每一个实例用于标识HTTP协议规定的请求内容
* 一个请求由三部分构成:
* 1:请求行
* 2:消息头
* 3:消息正文
*/
public class HttpServletRequest {
private Socket socket;
//请求行的相关信息
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议版本
private String requestURI;//uri中的请求部分,即:"?"左侧内容
private String queryString;//uri中的参数部分,即:"?"右侧内容
private Map<String,String> parameters = new HashMap<>();//每一组参数
//消息头的相关信息
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//1解析请求行
parseRequestLine();
//2解析消息头
parseHeaders();
//3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
if(line.isEmpty()){//若请求行为空字符串
//抛出空请求异常
throw new EmptyRequestException();
}
System.out.println("请求行内容:"+line);
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//进一步解析uri
parseURI();
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//进一步解析uri
private void parseURI(){
/*
uri有两种情况:
1:不含有参数的
例如: /index.html
直接将uri的值赋值给requestURI即可.
2:含有参数的
例如:/regUser?username=fancq&password=&nickname=chuanqi&age=22
将uri中"?"左侧的请求部分赋值给requestURI
将uri中"?"右侧的参数部分赋值给queryString
将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值
并将参数名作为key,参数值作为value存入到parameters中。
如果表单某个输入框没有输入信息,那么存入parameters时对应的值应当保存为空字符串
*/
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){//数组长度>1说明"?"后面有参数
queryString = data[1];
parseParameters(queryString);
}
System.out.println("requestURI:"+requestURI);
System.out.println("queryString:"+queryString);
System.out.println("parameters:"+parameters);
}
/**
* 解析参数。参数可能来自于抽象路径uri中或正文中
* @param line 字符串格式应当是name=value&name=value&...
*/
private void parseParameters(String line){
//对参数部分进行转码
try {
line = URLDecoder.decode(line,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//拆分每一组参数
String[] data = line.split("&");
for(String para : data){
String[] arr = para.split("=",2);
parameters.put(arr[0],arr[1]);
}
}
//解析消息头
private void parseHeaders() throws IOException {
while(true) {
String line = readLine();
if(line.isEmpty()){//如果读取的消息头是空字符串,说明单独读取到了CRLF
break;
}
System.out.println("消息头:"+line);
//将消息头按照": "拆分为名字和值并作为key,value存入到headers中
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
System.out.println("headers:"+headers);
}
//解析消息正文
private void parseContent() throws IOException {
//判断请求方式是否为post请求
if("post".equalsIgnoreCase(method)){
//通过消息头Content-Length来确定正文的长度
if(headers.containsKey("Content-Length")) {
int contentLength = Integer.parseInt(headers.get("Content-Length"));
//基于正文长度创建一个等长数组,用于块读正文数据。
byte[] data = new byte[contentLength];
InputStream in = socket.getInputStream();
in.read(data);
//获取消息头Content-Type的value值
String contentType = headers.get("Content-Type");
//判断正文类型进行不同处理
//字符串形式
if("application/x-www-form-urlencoded".equals(contentType)){
//表单数据,不含附件的。格式是一个字符串,就是原GET请求中在抽象路径"?"右侧内容
String line = new String(data, StandardCharsets.ISO_8859_1);
System.out.println("正文内容:"+line);
//调用parseParameters方法,拆分表单信息
parseParameters(line);
}
// else if(){}//后期可以再自行扩展判断其他类型并解析正文
}
}
}
private String readLine() throws IOException {
//调用同一个socket对象若干次getInputStream()方法返回的始终是同一条输入流
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();
char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
int d;
while((d = in.read())!=-1){
cur = (char)d;//本次读取到的字符
if(pre==13&&cur==10){//若上次读取的是回车符并且本次读取的是换行符
break;
}
builder.append(cur);//拼接本次读取到的字符
pre = cur;//进入下次循环前将本次读取的字符记作上次读取的字符
}
return builder.toString().trim();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
/**
* 根据消息头的名字获取对应的值
* @param name
* @return
*/
public String getHeader(String name) {
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
六、Class类型
Class的每一个实例用于表示JVM加载的一个类,所以也称Class的实例为类的对象。当JVM加载一个类时会同时实例化一个Class的实例与之对应,这个Class实例中会保存该类的一切信息(类名、方法、构造器、属性、注解等),程序运作期间某个类的 类对象 来操作这个类。因此使用反射操作某个 类的第一件事就是获取该类的 类对象。
Class是java反射的入口。
获取一个类的类对象的三种方式
1:类名.class ,(基本类型只能通过这种方式获取类对象)
2:Class.forName(String className),通过Class的静态方法forName,传入对应类的完全限定名(包类.类名)加载一个类。可能会抛出异常,原因是类名错误:ClassNotFoundException。
3:ClassLoader类加载器形式:是java底层用于加载对象API
先获得ClassLoader,当前类名.class.getClassLoader()
根据类名获取Class对象
package reflect;
import java.lang.reflect.Method;
import java.util.Scanner;
/**
* JAVA 反射机制
* 反射是java的动态机制,它允许我们在程序[运行期间]再确定要实例化的对象,调用的方法或操作
* 的属性。该机制可以大大的提高代码的灵活度和可扩展性,但是也随之带来了较低的运行效率和更多
* 的系统开销。因此程序不应当过度利用反射机制。
*/
public class ReflectDemo1 {
public static void main(String[] args) throws ClassNotFoundException {
//获取一个类对象的方式:
//1:类名.class 注:基本类型获取类对象只有上述方式。
Class cls1 = String.class;
Class cls2 = int.class;
//2:Class.forName(String className)基于类的完全限定名(包名.类名)加载一个类
Class cls3 = Class.forName("java.lang.String");
//3:ClassLoader类加载器形式
ClassLoader classLoader=ReflectDemo1.class.getClassLoader();
Class cls4=classLoader.loadClass("java.lang.String");
/*
测试类:
java.util.ArrayList
java.util.HashMap
java.io.FileInputStream
java.lang.String
reflect.Person
*/
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个类名:");
String className = scanner.nextLine();
Class cls = Class.forName(className);
//获取类对象表示的类的完全限定名
String name = cls.getName(); //java.util.ArrayList
System.out.println(name);
//仅获取类名
name = cls.getSimpleName(); //ArrayList
System.out.println(name);
/*
Package类的实例用于表示一个包的信息
*/
// Package pack = cls.getPackage();
// String pname = pack.getName();//获取包名
String pname = cls.getPackage().getName(); //仅获取包名:java.util
System.out.println("包名:"+pname);
/*
Method类的实例用于表示某个类上的一个方法
通过方法对象可以得知该方法的一切信息(访问修饰符,返回值类型,方法名,参数列表等等)
还可以通过方法对象来调用该方法(反射重点知识)
*/
//Class上的getMethods方法可以获取类对象所表示的类所有公开方法(包含从超类继承的)
Method[] methods = cls.getMethods();
for(Method method : methods){
System.out.println(method.getName());
}
}
}
使用反射动态创建对象
1:使用无参构造器实例化类对象,目标类型一定包含无参构造器,否则出现异常
Class.newInstance()方法:可以利用其表示的类的公开的无参构造器实例化
使用反射机制实例化对象person(在这之前创建一个Person类,一定要有无参构造器)
package reflect;
import java.util.Scanner;
/**
* 使用反射机制实例化对象
*/
public class ReflectDemo2 {
public static void main(String[] args) throws Exception {
Person p = new Person();
System.out.println(p);
//1加载需要实例化对象的类的类对象
// Class cls = Class.forName("reflect.Person");
/*
日期类,该类的每一个实例用于表示一个具体的时间点,默认创建表示当前系统时间
java.util.Date
*/
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个类名");
String className = scanner.nextLine();
Class cls = Class.forName(className);
//2类对象提供了方法:newInstance()可以利用其表示的类的公开的无参构造器实例化
Object obj = cls.newInstance();
System.out.println(obj);
}
}
2:使用指定构造器实例化类对象
1:Constructor getDeclaredConstructor():获取有参数的构造器
2:Constructor getConstructor()::获取有参数的构造器
根据参数类型找到特定的构造器,找不到出现异常NoSuchMethodException,找到返回构造器
package reflect;
import java.lang.reflect.Constructor;
/**
* 使用指定构造器实例化对象
*/
public class ReflectDemo3 {
public static void main(String[] args) throws Exception {
//Person(String,int),有参构造测试
Person p = new Person("张三",55);
System.out.println(p);
//使用指定构造器实例化对象
Class cls = Class.forName("reflect.Person");
//通过类对象获取其表示的类的某个指定的构造器
// cls.getConstructor();//Person()
/*
Constructor类的每一实例用于表示某个类上的某一个构造器
*/
//获取有参数的构造器
Constructor c1=cls.getDeclaredConstructor(String.class,int.class);
System.out.println(c1);
c1 = cls.getConstructor(String.class,int.class);//Person(String,int)
Object o = c1.newInstance("李四",55);//相当于new Person("李四",55);
System.out.println(o);
}
}
使用反射机制调用无参方法
invoke():通过方法对象调用该对象
getMethod()、getMethods():可以获取本类所有的公开方法
getDeclaredMethod()、getDeclaredMethods():可以获取被类定义的方法(包含私有方法,不含有超类继承的)
package reflect;
import java.lang.reflect.Method;
import java.util.Scanner;
/**
* 使用反射机制调用方法
*/
public class ReflectDemo4 {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.dance();
/*
//1实例化
Class cls = Class.forName("reflect.Person");
Object obj = cls.newInstance(); //相当于Person obj = new Person();
//2调用方法
//2.1通过类对象获取要调用方法的方法对象(Method对象)
Method method = cls.getMethod("dance");//dance方法
//2.2通过方法对象来调用该方法
method.invoke(obj);//p.dance();
*/
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要实例化的类的类名:");
String className = scanner.nextLine();
System.out.println("请输入方法名:");
String methodName = scanner.nextLine();
Class cls = Class.forName(className);
Object obj = cls.newInstance();//Person obj = new Person();
//2调用方法
//2.1通过类对象获取要调用方法的方法对象(Method对象)
Method method = cls.getMethod(methodName);//dance方法
//2.2通过方法对象来调用该方法
method.invoke(obj);//p.dance();
}
}
使用反射机制调用有参方法
getMethod("方法名",参数类型.class)、getMethods():可以获取本类所有的公开方法
getDeclaredMethod()、getDeclaredMethods():可以获取被类定义的方法(包含私有方法,不含有超类继承的)
package reflect;
import java.lang.reflect.Method;
/**
* 调用有参数方法
*/
public class ReflectDemo5 {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.say("hello");
p.say("world",5);
//获取Person的Class对象
Class cls = Class.forName("reflect.Person");
//创建对象
Object obj = cls.newInstance();
//找到具体参数调用的方法 say(String)
Method method = cls.getMethod("say",String.class);
//调用方法,invock的返回值就是,方法的返回值
method.invoke(obj,"大家好!");
//调用具体参数的方法 say(String,int)
Method method2 = cls.getMethod("say",String.class,int.class);
method2.invoke(obj,"hello world",10);
}
}
使用反射机制访问私有方法
利用反射API,在访问之前,打开访问权限,就可以访问私有的构造方法、构造器、属性
反射是底层API,具有很高权限
打开访问权限:setAccessible(true) 方法
package reflect;
import java.lang.reflect.Method;
public class ReflectDemo6 {
public static void main(String[] args) throws Exception {
// Person p = new Person();
// p.hehe();
Class cls = Class.forName("reflect.Person");
Object obj = cls.newInstance();
/*
getMethod()和getMethods()可以获取本类所有的公开方法
getDeclaredMethod()和getDeclaredMethods()可以获取被类定义的方法(包含私有方法,不含有超类继承的)
*/
// Method method = cls.getMethod("hehe");
Method method = cls.getDeclaredMethod("hehe");
method.setAccessible(true);//强行打开访问权限
method.invoke(obj);
method.setAccessible(false);//用后应当及时关闭访问权限
}
}
使用反射机制操作注解
JDK8后引入的一个新特性,可以利用注解辅助反射机制
注解:可以为类扩展功能
定义注解:
使用@interface定义注解
注解用于标注类、方法、属性、方法参数等
注解用于扩展类的功能
注解功能使用反射实现的
元注解:JAVA有几个内置的注解是用来为我们自定义的注解添加某种特性的
1:@Target()注解:用于说明我们定义的注解可以被用在那些位置
注解允许的值都被定义在ElementType上
常见:ElementType.TYPE类上 ElementType.Method方法上 ElementType.Field属性上
2:@Retention()注解:用于说明当前注解的保留级别,有三个可选值
RetentionPolicy.SOURCE:注解仅保留到源码文件中
RetentionPolicy.CLASS:注解会保留在字节码文件中,不可以被反射机制访问(默认级别)
RetentionPolicy.RUNTIME:注解被保留在字节码文件中,可以被反射机制访问(常用)
注解和反射的用途
AutoRunClass类:定义注解类
package reflect.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoRunClass {
}
Penson类:当前类上定义注解类AutoRunClass
@AutoRunClass public class Person { private String name = "张三"; private int age = 22;}
ReflectDemo7类:判断当前类是否被注解标注:isAnnotationPresent(注解类.class)
package reflect;
import reflect.annotations.AutoRunClass;
/**
* 反射机制中操作注解
*/
public class ReflectDemo7 {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("reflect.Person");
//判断当前cls(表示的Person类)所表示的类是否被注解@TestAnnotation标注
if(cls.isAnnotationPresent(AutoRunClass.class)){
System.out.println("被标注了!");
}else{
System.out.println("没有被标注!");
}
}
}
获取注解参数
注解中可以添加参数,格式为:参数类型 参数名() [default 默认值]
default可以为参数设定默认值,当使用注解不传递参数时则采取默认值。
当注解只有一个参数时,参数名建议选取"value",这样在使用注解时便于传递参数。
例如:@AutoRunMethod(22)
正常使用注解时,参数传递的格式为:@AutoRunMethod(value=22).即:参数名=参数值。
参数的顺序可以与定义时不一致。
获取该方法对象的方法上的特定注解:getAnnotation()
AutoRunMethod注解类:定义注解类,在注解类中添加参数
package reflect.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoRunMethod {
/*
注解中可以添加参数,格式为:
参数类型 参数名() [default 默认值]
default可以为参数设定默认值,当使用注解不传递参数时则采取默认值。
当注解只有一个参数时,参数名建议选取"value",这样在使用注解时便于传递参数。
例如:@AutoRunMethod(22)
正常使用注解时,参数传递的格式为:@AutoRunMethod(value=22).即:参数名=参数值。
参数的顺序可以与定义时不一致。
*/
int value() default 1;
/*
多个参数时,传参顺序不讲究:
@AutoRunMethod(name="world",num=6)
@AutoRunMethod(num=6,name="world")
*/
/* int value() default 1;
String name() default "abc";*/
}
Person类: 当前类的方法上定义注解类AutoRunMethod
@AutoRunMethod(15) public void sayHello(){ System.out.println(name+":hello!"); } @AutoRunMethod()//@AutoRunMethod()那么value采取默认值 public void dance(){ System.out.println(name+"正在跳舞"); } @AutoRunMethod//@AutoRunMethod那么value依然采取默认值 public void sing(){ System.out.println(name+"正在唱歌"); }
ReflectDemo8类:获取注解参数 ,getAnnotation()
package reflect;
import reflect.annotations.AutoRunMethod;
import java.lang.reflect.Method;
/**
* 获取注解参数
*/
public class ReflectDemo8 {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("reflect.Person");
Method method = cls.getDeclaredMethod("sayHello");
if(method.isAnnotationPresent(AutoRunMethod.class)){
//获取该方法对象所表示的方法上的特定注解
//@reflect.annotations.AutoRunMethod(value=15)
AutoRunMethod arm = method.getAnnotation(AutoRunMethod.class);
System.out.println(arm);
//通过注解对象获取参数value的值
int value = arm.value(); //15
System.out.println("参数:"+value);
}
}
}
测试
Test1:实例化与当前类Test1在同一包中 的所有类中 的无参方法
getModifiers():返回int类型,说明方法的修饰符类型
getParameterCount():方法的参数个数
package reflect;
import java.io.File;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
/**
* 实例化与当前类Test1在同一个包中的所有类
* 思路:
* 1:首先定位Test1的字节码文件所在的目录(main方法里第一行代码)
* 2:通过该目录获取里面所有的.class文件
* 3:由于字节码文件名与类名一致(JAVA语法要求),因此可以通过文件名确定类名
* 4:使用Class.forName()加载对应的类并实例化
*
* 上述完成后,自动调用这些类中所有的无参且公开的方法(本类定义的方法)。
*
*/
public class Test1 {
public static void main(String[] args) throws Exception {
/*
定位到当target/classes目录下
File dir=new File(
Test1.class.getClassLoader().getResource(".").toURI()
);
*/
//定位到当前类Test1 所在的包reflect
File dir = new File(
Test1.class.getResource(".").toURI()
);
System.out.println(dir);
//添加过滤器获取该目录中的所有字节码文件
File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){
String fileName = sub.getName();
//substring(int beginIndex, int endIndex):截取子字符串,含头不含尾
String className = fileName.substring(0,fileName.lastIndexOf("."));
// String className = fileName.replace(".class","");
String packName = Test1.class.getPackage().getName();
className = packName+"."+className;
Class cls = Class.forName(className);
Object obj = cls.newInstance();
System.out.println(obj);
Method[] methods = cls.getDeclaredMethods();
for(Method method : methods){
//判断是否为无参数方法
if(method.getParameterCount()==0 &&
method.getModifiers()== Modifier.PUBLIC //是否为公开方法
){
System.out.println("调用方法:"+method.getName()+"()");
method.invoke(obj);
}
}
}
}
}
Test2:实例化与当前类Test2在同一包中被@AutoRunClass标注的类中所有无参方法
package reflect;
import reflect.annotations.AutoRunClass;
import reflect.annotations.AutoRunMethod;
import java.io.File;
import java.lang.reflect.Method;
/**
* 自动调用与当前类Test2在同一个包中被注解@AutoRunClass标注的类 中那些被@AutoRunMethod
* 标注的方法。
* 注:
* 反射中的几个反射对象:Class,Method,Constructor,Field等都有isAnnotationPresent
* 用来判断是否被某个注解标注了
*/
public class Test2 {
public static void main(String[] args) throws Exception {
File dir = new File(Test2.class.getResource(".").toURI());
File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){
String className = sub.getName().replace(".class","");
Class cls = Class.forName(Test2.class.getPackage().getName()+"."+className);
//判断当前类是否被AutoRunClass注解标注
if(cls.isAnnotationPresent(AutoRunClass.class)){
System.out.println("实例化:"+className);
Object o = cls.newInstance();
System.out.println(o);
Method[] methods = cls.getDeclaredMethods();
for(Method method : methods){
if(method.isAnnotationPresent(AutoRunMethod.class)){
System.out.println("开始调用方法:"+method.getName());
method.invoke(o);
}
}
}
}
}
}
Test3:自动调用与当前类Test3在同一包中被注解@AutoRunClass标注的类的被@AutoRunMethod标注的方法指定次数
package reflect;
import reflect.annotations.AutoRunClass;
import reflect.annotations.AutoRunMethod;
import java.io.File;
import java.lang.reflect.Method;
public class Test3 {
public static void main(String[] args) throws Exception {
File dir = new File(Test3.class.getResource(".").toURI());
File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){
String className = sub.getName().replace(".class","");
Class cls = Class.forName(Test3.class.getPackage().getName()+"."+className);
if(cls.isAnnotationPresent(AutoRunClass.class)){
System.out.println("实例化:"+className);
Object o = cls.newInstance();
System.out.println(o);
Method[] methods = cls.getDeclaredMethods();
for(Method method : methods){
if(method.isAnnotationPresent(AutoRunMethod.class)){
AutoRunMethod arm = method.getAnnotation(AutoRunMethod.class);
int value = arm.value();
System.out.println("开始调用方法:"+method.getName()+"()"+value+"次");
for(int i=0;i<value;i++) {
method.invoke(o);
}
}
}
}
}
}
}
反射API 可能出现的异常
1:ClassNotFoundException:类没有找到异常
Class.forName(类名) ClassLoader.loadClass(类名):在类名错误没有对应类会出现
2:NoSuchMethodException:没有这样的方法异常
在类查找构造器的时候,没有找到对应构造器
在类上查找方法的时候,没有找到对应的方法
3:InvocationTargetException:目标方法调用异常
反射执行方法期间,方法出现异常
4:InstantiationException:实例化异常
在反射创建对象期间,对象无法实例化
5:IllegaAccessibleException:非法访问权限
七、利用反射机制重构DispatcherServlet
实现:
1:新建包com.webserver.annotation
2:在annotation包下添加两个注解
@Controller:用于标注哪些类是处理业务的Controller类
@RequestMapping:用于标注处理某个业务请求的业务方法
3:将com.webserver.controller包下的所有Controller类添加注解@Controller
并将里面用于处理某个业务的方法标注@RequestMapping并指定该方法处理的请求
4:DispatcherServlet在处理请求时,先扫描controller包下的所有Controller类,并找到处理该请求的业务方法,使用反射调用.
包结构:
Controller注解类:处理业务
package com.webserver.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用来标注处于处理业务的类
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
RequestMapping注解类: 用来标注某个Controller中处理某个请求的方法
package com.webserver.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用来标注某个Controller中处理某个请求的方法
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value();
}
DispatcherServlet类:用注解处理请求业务,使得将来添加新的业务时DispatcherServlet不必再添加分支判断
package com.webserver.core;
import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
/**
* 这个类是SpringMVC框架与tomcat容器整合的一个关键类,接管了处理请求的工作。
* 这样当tomcat将请求对象和响应对象创建完毕后在处理请求的环节通过调用这个类来完成,从而
* 将处理请求交给了SpringMVC框架
* 并在处理后发送响应给浏览器
*/
public class DispatcherServlet {
private static DispatcherServlet instance = new DispatcherServlet();
private static File rootDir;
private static File staticDir;
static {
try {
//rootDir表示类加载路径:target/classes目录
rootDir = new File(
DispatcherServlet.class.getClassLoader()
.getResource(".").toURI()
);
//定位static目录(static目录下存放的是所有静态资源)
staticDir = new File(rootDir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet() {
}
public static DispatcherServlet getInstance() {
return instance;
}
public void service(HttpServletRequest request, HttpServletResponse response) {
String path = request.getRequestURI();
//path:/regUser
//首先判断是否为请求某个特定的业务
/*
当我们得到本次请求路径path的值后,我们首先要查看是否为请求业务:
1:扫描controller包下的所有类
2:查看哪些被注解@Controller标注的过的类(只有被该注解标注的类才认可为业务处理类)
3:遍历这些类,并获取他们的所有方法,并查看哪些时业务方法
只有被注解@RequestMapping标注的方法才是业务方法
4:遍历业务方法时比对该方法上@RequestMapping中传递的参数值是否与本次请求
路径path值一致?如果一致则说明本次请求就应当由该方法进行处理
因此利用反射机制调用该方法进行处理。
5:如果扫描了所有的Controller中所有的业务方法,均未找到与本次请求匹配的路径
则说明本次请求并非处理业务,那么执行下面请求静态资源的操作
*/
//定位controller目录
try {
File dir = new File(rootDir,"com/webserver/controller");
File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){
String className = sub.getName().replace(".class","");
Class cls = Class.forName("com.webserver.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);
//获取@RequestMapping注解的参数值
String value = rm.value();
//若本次请求路径和@RequestMapping注解参数值一样则说明本次请求应当有该方法处理
if(path.equals(value)){
Object controller = cls.newInstance(); //实例化Controller
method.invoke(controller,request,response); //调用该方法处理业务
return;
}
}
}
}
}
} catch (ClassNotFoundException e) {
//处理业务过程中出错,可以设置响应500错误。
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
File file = new File(staticDir, path);
if (file.isFile()) {//根据用户提供的抽象路径去static目录下定位到一个文件
response.setContentFile(file);
response.addHeader("Server", "BirdWebServer");
} else {
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "/root/404.html");
response.setContentFile(file);
}
}
}
八、重构代码:DispatcherServlet处理请求仅扫描一次controller包
上一个版本那种DispatcherServlet每次处理请求时都要扫描controller包,这样性能
底下.因此改为仅扫描一次.
实现:
1:在com.webserver.core包下新建类HandlerMapping
2:定义静态属性Map requestMapping
key:请求路径
value:Method记录处理该请求的方法对象
3:在静态块中初始化,完成原DispatcherServlet扫描controller包的工作并初始化requestMapping
4:提供静态方法:getMethod()可根据请求路径获取处理该请求的方法
5:在DispatcherServlet中处理业务时只需要根据请求获取对应的处理方法利用反射机制调用即可
包结构:
HandlerMapping类:获取@RequestMapping上的参数,该参数记录这该方法处理的请求路径
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;
/**
* 该类用于维护请求路径与业务处理方法的对应关系
*/
public class HandlerMapping {
/**
* key:请求路径
* value:处理该请求的方法
* 例如:
* key->/regUser
* value->Method对象,表示UserController中的reg方法
*/
private static Map<String, Method> mapping = new HashMap<>();
static{
initMapping();
}
private static void initMapping(){
//定位类加载路径
try {
File rootDir = new File(
HandlerMapping.class.getClassLoader().getResource(".").toURI()
);
//定位controller包
File dir = new File(rootDir,"com/webserver/controller");
if(dir.exists()){//确保controller目录存在
File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){
String className = sub.getName().replace(".class","");
Class cls = Class.forName("com.webserver.controller."+className);
if(cls.isAnnotationPresent(Controller.class)){//判断该类是否被注解@Controller标注
Method[] methods = cls.getDeclaredMethods();
for(Method method : methods){
if(method.isAnnotationPresent(RequestMapping.class)){//判断方法是否被注解@RequestMapping标注
RequestMapping rm = method.getAnnotation(RequestMapping.class);
String value = rm.value();//获取@RequestMapping上的参数,该参数记录这该方法处理的请求路径
mapping.put(value,method);
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据请求路径过去对应的业务处理方法
* @param uri
* @return
*/
public static Method getMethod(String uri){
return mapping.get(uri);
}
public static void main(String[] args) {
System.out.println(mapping.size());
System.out.println(mapping);
}
}
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;
/**
* 这个类是SpringMVC框架与tomcat容器整合的一个关键类,接管了处理请求的工作。
* 这样当tomcat将请求对象和响应对象创建完毕后在处理请求的环节通过调用这个类来完成,从而
* 将处理请求交给了SpringMVC框架
* 并在处理后发送响应给浏览器
*/
public class DispatcherServlet {
private static DispatcherServlet instance = new DispatcherServlet();
private static File rootDir;
private static File staticDir;
static {
try {
//rootDir表示类加载路径:target/classes目录
rootDir = new File(
DispatcherServlet.class.getClassLoader()
.getResource(".").toURI()
);
//定位static目录(static目录下存放的是所有静态资源)
staticDir = new File(rootDir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet() {
}
public static DispatcherServlet getInstance() {
return instance;
}
public void service(HttpServletRequest request, HttpServletResponse response) {
String path = request.getRequestURI();
//是否为处理业务
try {
Method method = HandlerMapping.getMethod(path);
if(method!=null){
//通过方法对象获取其所属的类的类对象
Class cls = method.getDeclaringClass();
Object controller = cls.newInstance();
method.invoke(controller,request,response);
return;
}
} catch (Exception e) {
e.printStackTrace();
}
//处理静态资源
File file = new File(staticDir, path);
if (file.isFile()) {//根据用户提供的抽象路径去static目录下定位到一个文件
response.setContentFile(file);
response.addHeader("Server", "BirdWebServer");
} else {
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "/root/404.html");
response.setContentFile(file);
}
}
}
九:线程池:线程池是线程管理机制
作用是:复用线程,控制线程数量
线程池API:
Executors:创建线程池的工厂类,用于创建线程池对象
ExecutorService:线程池接口,规定了线程池的相关方法
创建线程池:
ExecutorService pool=Executors.newFixedThreadPool(容量);
线程池的关闭:
java进程是否结束取决于,用户线程是否销毁
shutdown():1.不会接受新的线程(此时调用execute后会抛出异常)
2.将现有的任务执行完毕在退出
3.结束线程池中所有线程
shutdownNow():1.调用所有内部线程的interrupt()方法,中断线程池所有线程
2.不在接收新的任务,当所有线程都停止后在退出
测试:线程池
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
* 线程池是线程的管理类,主要解决两个问题
* 1:控制线程数量
* 2:重用线程
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
//1创建一个线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
//2指派5个任务
for(int i=0;i<5;i++){
Runnable runnable = new Runnable() {
public void run() {
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在执行一个任务...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName()+":执行一个任务完毕!");
}
};
threadPool.execute(runnable);
System.out.println("将一个任务交给了线程池");
}
// threadPool.shutdown();
threadPool.shutdownNow();
System.out.println("线程池停止了");
}
}
BirdBootApplication主启动类:用线程池创建线程
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;
/**
* 项目的主类,用于启动WebServer
* 该项目主要功能是实现Tomcat这个WebServer的核心功能。了解HTTP协议以及浏览器与服务端
* 之间的处理细节。
* 了解SpringMVC核心类的设计与实现
*/
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();
}
}