#手写一个Spring MVC框架
##一次浏览器请求的大致过程
Sping MVC是一个Web框架,所以我们要了解Spring MVC就要先了解一次浏览器请求到收到服务器的响应大概发生了哪些事情。
1.域名解析;
2.请求连接(三次握手);
3.浏览器发起http请求;
4.服务器解析http交给Spring MVC处理;
5.将响应的结果封装返回;
##Socket编程
了解了这个过程之后开始做一个简单的Socket编程
首先服务端代码:
public class Server {
public static void main(String[] args) throws IOException {
//1.建立服务端
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("等待链接...");
//2.监听客户端连接
Socket socket = serverSocket.accept();
System.out.println("客户端已连接:"+socket.getInetAddress().getHostAddress());
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(socket.getOutputStream(),true)){
//4.从客户端读取消息,会阻塞
System.out.println("client:"+in.readLine());
//6.从命令行读取消息传给客户端
String line = sin.readLine();
out.println(line);
}
}
}
客户端代码
public static void main(String[] args) throws IOException {
//3.请求连接服务器
Socket socket = new Socket("localhost",8080);
System.out.println("成功连接服务器");
try (BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(socket.getOutputStream(),true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
//5.从命令行读取消息传给服务端
String line = sin.readLine();
out.println(line);
//7.收到服务器消息
System.out.println("server:"+in.readLine());
}
}
这知识一个简单的Socket编程一次读写后就会关闭(try-with-resource,资源会自动释放),但是服务端未接收到消息就不能给客户端发送消息;
##多线程版
Server
public class HTTPServer {
private static Map<String,String> contentMap = new HashMap<>();
static {
contentMap.put("/","welcome");
contentMap.put("/hello","world");
}
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true){
Socket socket = serverSocket.accept();
System.out.println("客户端已连接"+socket.getInetAddress().getHostAddress());
new Thread(()->{
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(),true)){
while (true){
String line = in.readLine();
System.out.println(line);
if (line == null || line.equals("bye")){
break;
}
String s = contentMap.get(line);
out.println(s==null?404:s);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
Client
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 8080);
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(),true);
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in))){
while (true){
String line = sin.readLine();
if (line==null || line.equals("bye")){
break;
}
out.println(line);
String readLine = in.readLine();
System.out.println(readLine);
}
}
}
这样我们就可以实现客户端和服务端之间的多次请求响应了,而且可以实现多个客户端连接;那我们再写一个服务器浏览器的例子
##BS版
public class HTTPServer {
public static Map<String,String> contentMap = new HashMap<>();
static {
contentMap.put("/","welcome");
contentMap.put("/hello","world");
contentMap.put("/yuxuyang","sb");
}
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(80);
while (true){
Socket socket = serverSocket.accept();
new Thread(()->{
//System.out.println("客户端已连接"+socket.getInetAddress().getHostAddress());
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(),true)){
String line = null;
StringBuffer buffer = new StringBuffer();
String request = null;
while ((line = in.readLine()) !=null && !line.equals("")){
buffer.append(line).append("\n");
if (request==null){
request = line;
}
}
//System.out.println(buffer.toString());
//System.out.println(buffer);
out.println("HTTP/1.1 200 OK");
out.println("Content-Type:text/html;charset=utf-8");
//空行表示请求头结束
out.println();
out.println("<html><head><title>HttpTest</title></head><body>");
if (request != null){
String url = request.split(" ")[1].split("\\?")[0];
System.out.println(url);
if (request.split(" ")[1].split("\\?").length>1){
String parameter = request.split(" ")[1].split("\\?")[1];
String[] parameters = parameter.split("&");
for (String s:parameters) {
String[] split = s.split("=");
String s1 = split[0];
String s2 = split[1];
System.out.println(s1 +":"+s2);
}
}
String s = contentMap.get(url);
out.println(s==null?"404":s);
}
out.println("</body></html>");
} catch (IOException e) {
//e.printStackTrace();
}
}).start();
}
}
}
这样我们在浏览器访问localhost的80端口就能从map里取到值显示在前端页面;
但是上面的两个例子都是多线程实现的,所以说如果访问量大的时候就会有很多线程,线程不断的创建销毁就会消耗资源;所以肯定是不行的;
##IO多路复用
public static void main(String[] args) throws IOException {
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.socket().bind(new InetSocketAddress(80));
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
if (selector.select(1000)<=0){
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
httpHandle(key);
iterator.remove();
}
}
}
private static void httpHandle(SelectionKey key) throws IOException {
if (key.isAcceptable()){
acceptHandle(key);
}else if (key.isReadable()){
requestHandle(key);
}
}
private static void acceptHandle(SelectionKey key) throws IOException {
SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();
socketChannel.configureBlocking(false);
socketChannel.register(key.selector(),SelectionKey.OP_READ,ByteBuffer.allocate(1024));
}
private static void requestHandle(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = (ByteBuffer)key.attachment();
byteBuffer.clear();
if (socketChannel.read(byteBuffer) == -1){
socketChannel.close();
return;
}
byteBuffer.flip();
String request = new String(byteBuffer.array());
String url = request.split("\r\n")[0].split(" ")[1];
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("HTTP/1.1 200 OK\r\n");
stringBuffer.append("Content-Type:text/html;charset=utf-8\r\n\r\n");
stringBuffer.append("<html><head><title>HttpTest</title></head><body>");
String s = HTTPServer.contentMap.get(url);
stringBuffer.append(s==null?"404":s);
stringBuffer.append("</body></html>");
socketChannel.write(ByteBuffer.wrap(stringBuffer.toString().getBytes()));
socketChannel.close();
}
}
为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:
1、connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT 2、accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT 3、read:读事件,对应值为SelectionKey.OP_READ 4、write:写事件,对应值为SelectionKey.OP_WRITE
当socket有请求时遍历这些请求然后一个个处理或者发送到每一个线程。
##手写Spring MVC
在了解了IO多路复用后我们就可以自己写一个Spring MVC了
@MyRestController
public class UserController {
private static List<User> userList = new ArrayList<>();
static {
userList.add(new User(1, "Jim"));
userList.add(new User(2, "Lily"));
}
@MyRequestMapping("/get")
public String get(int id) {
for (User user:userList) {
if (user.getId()==id){
return user.getName();
}
}
return "";
}
@MyRequestMapping("/getAll")
public String getAll() {
StringBuffer stringBuffer = new StringBuffer();
for (User user:userList) {
stringBuffer.append("id:").append(user.getId()).append(" name:").append(user.getName()).append("\r\n");
}
return stringBuffer.toString();
}
@MyRequestMapping("/get2")
public String get2(int id,String name){
return id+":"+name;
}
@MyRequestMapping("/addUser")
public String addUser(int id,String name){
User user = new User(id, name);
userList.add(user);
return getAll();
}
}
@MyRestController 用来将类实例化后放到容器里
@MyRequestMapping用来映射路径
解析路径
public class ParseUrl {
private static Map<String,Object> beanMap = new HashMap<>();
private static Map<String, MethodInfo> methodMap = new HashMap<>();
// 1. 初始化 beanMap
// 2. 初始化 methodMap
public static void refreshBeanFactory(String pkg) {
URL url = ParseUrl.class.getClassLoader().getResource(pkg.replace(".","/"));
File file = new File(url.getPath());
parseFile(file);
//System.out.println(file);
/* for (Map.Entry entry:beanMap.entrySet()) {
System.out.println(entry.getKey()+":"+entry.getValue());
}*/
}
//解析file
private static void parseFile(File file) {
if (!file.isDirectory()){
return;
}
File[] files = file.listFiles(new FileFilter() {
@Override
public boolean accept(File pathName) {
if (pathName.isDirectory()){
parseFile(pathName);
return false;
}
return pathName.getName().endsWith(".class");
}
});
for (File f:files) {
String classPath = f.getAbsolutePath().split("classes\\\\")[1].replace("\\", ".").replace(".class", "");
try {
Class<?> aClass = Class.forName(classPath);
if (aClass.getDeclaredAnnotation(MyRestController.class)!=null){
parseClass(aClass);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
private static void parseClass(Class<?> aClass) {
try {
beanMap.put(aClass.getSimpleName(),aClass.getDeclaredConstructor().newInstance());
} catch (Exception e) {
e.printStackTrace();
}
Method[] methods = aClass.getDeclaredMethods();
for (Method method:methods) {
MyRequestMapping myRequestMapping = method.getDeclaredAnnotation(MyRequestMapping.class);
if (myRequestMapping!=null){
String url = myRequestMapping.value();
methodMap.put(url,new MethodInfo(method,aClass.getSimpleName()));
}
}
}
//解析url
/**
*
* @param url 全路径
*/
public static String parseUrl(String url) throws InvocationTargetException, IllegalAccessException {
if (url.contains(".ico")){
return "";
}
HashMap<String,String> urlParameter = new HashMap<String,String>();
if (!url.contains("?")){
return methodInvoke(url,urlParameter);
}
String[] split = url.split("\\?");
String[] parameters = url.replaceFirst(".*?\\?","").split("&");
url = split[0];
for (String parameter:parameters) {
if (!parameter.contains("=")){
continue;
}
String[] split1 = parameter.split("=");
urlParameter.put(split1[0],split1[1]);
}
return methodInvoke(url,urlParameter);
}
//调用方法
/**
*
* @param url 不带参数的路径
* @param urlParameter 路径带的参数(参数名,value)
* @return
*/
public static String methodInvoke(String url,HashMap<String,String> urlParameter) throws InvocationTargetException, IllegalAccessException {
//根据URl从容器取出method和对象
MethodInfo methodInfo = methodMap.get(url);
if (methodInfo==null){
return "404";
}
String className = methodInfo.getClassName();
Object o = beanMap.get(className);
Method method = methodInfo.getMethod();
//判断有无参数
/* if (urlParameter.size()==0){
return (String) method.invoke(o);
}*/
//接收参数的数组
Object[] parameters = new Object[urlParameter.size()];
Parameter[] methodParameters = method.getParameters();
if(parameters.length!=methodParameters.length){
return "参数个数不匹配";
}
//判断参数类型
for (int i = 0; i<methodParameters.length;i++){
String name = methodParameters[i].getName();
String type = methodParameters[i].getType().getSimpleName();
boolean flag = false;
if(type.equals("int")){
parameters[i] = Integer.parseInt(urlParameter.get(name));
flag = true;
}else if (type.equals("String")){
parameters[i] = urlParameter.get(name);
flag = true;
}
if (!flag){
return "404";
}
}
return (String) method.invoke(o, parameters);
}
}
首先我们要将类和方法解析出来放到我们的map里,也就是初始化容器;beanMap就相当于Spring的IOC容器,methodMap用来映射访问的路径和对应的方法;
Server
public class NIOTest {
static {
ParseUrl.refreshBeanFactory("com.bailiban.day1.myMVC");
}
public static void main(String[] args) throws IOException {
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.socket().bind(new InetSocketAddress(80));
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
socketChannel.register(selector,SelectionKey.OP_ACCEPT);
while (true){
if (selector.select(1000)<=0){
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
httpHandle(key);
iterator.remove();
}
}
}
private static void httpHandle(SelectionKey key) throws IOException {
if (key.isAcceptable()){
acceptHandle(key);
}else if (key.isReadable()){
requestHandle(key);
}
}
private static void acceptHandle(SelectionKey key) throws IOException {
SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();
socketChannel.configureBlocking(false);
socketChannel.register(key.selector(),SelectionKey.OP_READ,ByteBuffer.allocate(1024));
}
private static void requestHandle(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = (ByteBuffer)key.attachment();
byteBuffer.clear();
if (socketChannel.read(byteBuffer) == -1){
socketChannel.close();
return;
}
byteBuffer.flip();
String request = new String(byteBuffer.array());
String url = request.split("\r\n")[0].split(" ")[1];
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("HTTP/1.1 200 OK\r\n");
stringBuffer.append("Content-Type:text/html;charset=utf-8\r\n\r\n");
stringBuffer.append("<html><head><title>HttpTest</title></head><body>");
/* String s = HTTPServer.contentMap.get(url);*/
ParseUrl parseUrl = new ParseUrl();
String response = null;
try {
response = parseUrl.parseUrl(url);
} catch (Exception e) {
e.printStackTrace();
}
stringBuffer.append(response);
/* stringBuffer.append(s==null?"404":s);*/
stringBuffer.append("</body></html>");
socketChannel.write(ByteBuffer.wrap(stringBuffer.toString().getBytes()));
socketChannel.close();
}
}
在我们一步步的完善下,这样一个简单的Spring MVC就完成了;
完整代码:添加链接描述