0%

什么是MVC

将软件按照模型、视图、控制器来划分

  • M:Model,模型层,指工程中的JavaBean,作用是处理数据
    • 实体类Bean:专门用于存储业务的,比如Student、User等
    • 业务处理Bean:Service或者DAO等等
  • View:视图层,指工程中的htnml或者jsp等页面,作用是与用户进行交互
  • Controller,控制层,指工程中的Servlet,作用是接受请求与响应浏览器

什么是SpringMVC

SpringMVC是为Spring为表述层开发提供的一套完备的解决方案,三层架构分别为表述层(),业务逻辑层、数据访问层、表层表示前台页面和后台Servlet

表现层:SpringMVC
业务层:Spring
持久层:Mybatis

表现层一般与浏览器也就是我们的客户端进行数据的交互。浏览器会给我们发出一个http请求,请求会经过我们的表现层,浏览器会给我们传递一些请求的参数,表现层可以获取到我们请求的参数。获取到之后可以调用我们的业务层,业务层会调用我们的持久层,然后结果再返回,结果返回到表现层,最后把结果响应给我们的用户。

特点

基于原生Servlet,通过强大的前端控制器DispatcherServlet,对请求和响应进行统一处理

HelloWorld

创建Maven工程

导入相关的依赖包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<dependencies>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.18</version>
</dependency>

<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>

<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>

<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.15.RELEASE</version>
</dependency>
</dependencies>

配置web.xml

对前端的发送的请求进行响应的配置

默认配置方式

位置默认,早web-inf下,名称默认

注意在这个过程中url-patteren中/不会对jsp产生响应,应为jsp本质是一个servlet,但是/*会对jsp的请求产生响应

1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

扩展配置方式

但是在maven工程下,我们需要将配置文件否存放到resourse文件下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--接受所有的路径,转发给dispatcherservlet-->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--设置初始化值参数-->
<init-param>
<!--//上下文配置路径-->
<param-name>contextConfigLocation</param-name>
<!--路径-->
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!--我们文件配置的初始化如果在第一次访问页面的时候进行初始化,会影响我们访问页面的速度,所以我们将dispatcherservlet
的初始化时间提前到到服务器启动时-->
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

创建请求控制器

1
2
3
4
@Controller
public class HelloController {

}

注解

1
2
3
4
@Component---普通组件
@Controller---控制层组件
@Service---业务层组件
@Repository---持久层组件

创建配置文件

1
2
<!--扫描组件-->
<context:component-scan base-package="com.zss.mvc.controller"></context:component-scan>

测试HelloWorld

访问此页面

1
2
3
4
5
6
7
8
@Controller
public class HelloController {
//@RequestMapping( "/")表示此方法会对其进行响应
@RequestMapping( "/")
public String index(){
return "index";
}
}

转入到另外一个页面

1
2
3
4
5
6
//接受target的请求
@RequestMapping("/target")
public String toTarget(){
//返回target页面
return "target";
}

引入

image-20220413090242922

以下的注册界面,在注册过程中不应该出现function1成功,而function2不成功的现象,而事务的完成应该以业务为单位进行操作。

  1. 获取链接,取消自动提交conn.setAutoCommit(false)
  2. 执行相应的操作
  3. 提交事务 conn.commit()
  4. 回滚事务 conn.rollback

所以说我们应该在service上开启事务,进行管理,而为了更好的进行判断,我们将此代码前置到filter中

1
2
3
4
5
6
7
try{
autoCommit(false);
放行();
commit();
}catch(){
rollback;
}

但是需要将function1于function2处于同一个链接中

首先我们需要设置过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@WebFilter("*.do")
public class OpenSessionInViewFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try{
System.out.println("开始事务");
TransactionManager.beginTrans();
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("提交事务");
TransactionManager.commit();
}catch (Exception e){
e.printStackTrace();
System.out.println("回滚事务");
try {
TransactionManager.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
try {
ConnUtil.closeConn();
} catch (SQLException e) {
e.printStackTrace();
}

}

@Override
public void destroy() {
Filter.super.destroy();
}
}

而在这里负责事务的发送等操作,使过滤器进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TransactionManager {

private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

//开启事务
public static void beginTrans() throws SQLException {
ConnUtil.getConnection().setAutoCommit(false);
}

//提交事务
public static void commit() throws SQLException {
Connection connection= ConnUtil.getConnection();
connection.commit();

}

//回滚事务
public static void rollback() throws SQLException {
Connection connection= ConnUtil.getConnection();
connection.rollback();
}
}

image-20220413152925212

ThreadLocal

本地线程,可以通过set方法在当前线程上存储数据,get可以获取数据

1
2
3
4
5
6
7
8
9
10
11
12
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取维护自己的容器,一个容器中可能含有多个ThreadLocal
ThreadLocalMap map = getMap(t);
if (map != null) {
//this代表当前threadlocal
map.set(this, value);
} else {
createMap(t, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前容器维护的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取ThreadLocal对象,通过这个才能了解是哪一个工作纽带
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获得工具箱
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

而一个简易的ThreadLocal可以理解为下:

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyThreadLocal
{
private final ConcurrentHashMap<Thread, Object> valueMap = new ConcurrentHashMap<>();
public void set(Object newValue)
{
valueMap.put(Thread.currentThread(), newValue);
}
public Object get()
{
Thread currentThread = Thread.currentThread();
Object o = valueMap.get(currentThread);
if (o == null && !valueMap.containsKey(currentThread))
{
o = initialValue();
valueMap.put(currentThread, o);
}
return o;
}
public void remove()
{
valueMap.remove(Thread.currentThread());
}
public Object initialValue()
{
return null;
}
}

保存作用域

Request保存作用域

数据仅仅保存一次请求响应

https://moonshuo.cn/posts/43816.html

所以说,对于我们页面的重定向,经过了两次请求与响应,所以我们用request保存的不会在下一个页面对应的Servlet中展现出来,而在springmvc中model也是属于Request作用域的。

1
request.setAttribute("fruitlist",list);

image-20220402160545843

但是对于内部转发来说,可以获取到request保存的值

image-20220402160305352

Session保存作用域

保存一次会话的范围,只要我们的session没有过期,并且session相同,同一个浏览器不同请求之间传递数据。

https://moonshuo.cn/posts/16634.html

Application作用域

范围为整个应用范围(tomcat从启动到结束为一个应用),相对比于session,不同的浏览器进行访问都可以得到application的保存的数据,只要是同一个应用(即tomcat从)发出的,而session只要id不相同,就不会访问得到,所以application存放所有用户都共享的数据,而且大家都经常使用的。

同时保存的方法比较特殊,为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.zss.servlets;

import javax.servlet.ServletContext;
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;

/**
* @author zss
* 下面的注解表示于web.xml相匹配的作用相同
*/

@WebServlet("/demo06")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext servletContext =req.getServletContext();
servletContext.setAttribute("name","haha");
resp.sendRedirect("hi");
}
}

路径问题

image-20220404154436963

相对路径

我们要在FruitShow.html中引用其响应的css与js

相对引用,其一个点代表其fruit路径发,而两个点代表web路径下文件

1
2
<link href="../css/FruitShow.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="../js/FruitShow.js"></script>

绝对路径

1
2
<link href="http://localhost:8080/css/FruitShow.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="http://localhost:8080/js/FruitShow.js"></script>

前提是同名tomcat服务器的8080端口必须开启,而对于绝对路径来说我们可以使用base标签,表示当前页面的所有路径发根路径。

1
<base href="http://localhost:8080">

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.zss.Listener;


import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

/**
* @author zss
*/
@WebListener
public class Listener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("我监听到了上下文的初始化动作");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("我监听到了上下文的销毁动作");
}
}

image-20220413161624081

实例

我们在dispatcher中,初始化的调用了ClassPathXmlApplicationContext,明确了依赖关系

1
2
3
4
5
@Override
public void init() {
super.init();
beanFactory=new ClassPathXmlApplicationContext();
}

但是我们的在初始化的时候进行依赖关系的确认,这样我们后端对前端的响应速度会降低,那么如果我们将依赖关系调用到listener中,在监听上下文的时候就进行依赖关系的确认,虽然启动速度会降低。

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebListener
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
BeanFactory beanFactory=new ClassPathXmlApplicationContext();
ServletContext application=sce.getServletContext();
application.setAttribute("beanFactory",beanFactory);
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
1
2
3
4
5
6
7
8
9
10
11
@Override
public void init() {
super.init();
ServletContext application=getServletContext();
Object object=application.getAttribute("beanFactory");
if (object!=null){
beanFactory=(BeanFactory) object;
}else {
throw new RuntimeException("ioc容器获取失败,依赖关系加载错误");
}
}

解析

copy来源于代码重工 (gitee.io)

①ServletContextListener

作用:监听ServletContext对象的创建与销毁

方法名 作用
contextInitialized(ServletContextEvent sce) ServletContext创建时调用
contextDestroyed(ServletContextEvent sce) ServletContext销毁时调用

ServletContextEvent对象代表从ServletContext对象身上捕获到的事件,通过这个事件对象我们可以获取到ServletContext对象。

#②HttpSessionListener

作用:监听HttpSession对象的创建与销毁

方法名 作用
sessionCreated(HttpSessionEvent hse) HttpSession对象创建时调用
sessionDestroyed(HttpSessionEvent hse) HttpSession对象销毁时调用

HttpSessionEvent对象代表从HttpSession对象身上捕获到的事件,通过这个事件对象我们可以获取到触发事件的HttpSession对象。

#③ServletRequestListener

作用:监听ServletRequest对象的创建与销毁

方法名 作用
requestInitialized(ServletRequestEvent sre) ServletRequest对象创建时调用
requestDestroyed(ServletRequestEvent sre) ServletRequest对象销毁时调用

ServletRequestEvent对象代表从HttpServletRequest对象身上捕获到的事件,通过这个事件对象我们可以获取到触发事件的HttpServletRequest对象。另外还有一个方法可以获取到当前Web应用的ServletContext对象。

#④ServletContextAttributeListener

作用:监听ServletContext中属性的创建、修改和销毁

方法名 作用
attributeAdded(ServletContextAttributeEvent scab) 向ServletContext中添加属性时调用
attributeRemoved(ServletContextAttributeEvent scab) 从ServletContext中移除属性时调用
attributeReplaced(ServletContextAttributeEvent scab) 当ServletContext中的属性被修改时调用

ServletContextAttributeEvent对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getServletContext() 获取ServletContext对象

#⑤HttpSessionAttributeListener

作用:监听HttpSession中属性的创建、修改和销毁

方法名 作用
attributeAdded(HttpSessionBindingEvent se) 向HttpSession中添加属性时调用
attributeRemoved(HttpSessionBindingEvent se) 从HttpSession中移除属性时调用
attributeReplaced(HttpSessionBindingEvent se) 当HttpSession中的属性被修改时调用

HttpSessionBindingEvent对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getSession() 获取触发事件的HttpSession对象

#⑥ServletRequestAttributeListener

作用:监听ServletRequest中属性的创建、修改和销毁

方法名 作用
attributeAdded(ServletRequestAttributeEvent srae) 向ServletRequest中添加属性时调用
attributeRemoved(ServletRequestAttributeEvent srae) 从ServletRequest中移除属性时调用
attributeReplaced(ServletRequestAttributeEvent srae) 当ServletRequest中的属性被修改时调用

ServletRequestAttributeEvent对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getServletRequest () 获取触发事件的ServletRequest对象

#⑦HttpSessionBindingListener

作用:监听某个对象在Session域中的创建与移除

方法名 作用
valueBound(HttpSessionBindingEvent event) 该类的实例被放到Session域中时调用
valueUnbound(HttpSessionBindingEvent event) 该类的实例从Session中移除时调用

HttpSessionBindingEvent对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取当前事件涉及的属性名
getValue() 获取当前事件涉及的属性值
getSession() 获取触发事件的HttpSession对象

#⑧HttpSessionActivationListener

作用:监听某个对象在Session中的序列化与反序列化。

方法名 作用
sessionWillPassivate(HttpSessionEvent se) 该类实例和Session一起钝化到硬盘时调用
sessionDidActivate(HttpSessionEvent se) 该类实例和Session一起活化到内存时调用

HttpSessionEvent对象代表事件对象,通过getSession()方法获取事件涉及的HttpSession对象。

使用步骤

加入jar包

kaptcha-2.3.2.jarr

在web.xml文件中配置

注册KaptchaServlet,并设置验证码图片相关属性

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>KaptchaServlet</servlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>KaptchaServlet</servlet-name>
<url-pattern>/kaptch.jpg</url-pattern>
</servlet-mapping>

设置文件

查看constant文件

1
2
3
4
5
6
7
8
9
10
11
//设置边框
public static final String KAPTCHA_BORDER = "kaptcha.border";
//边框颜色
public static final String KAPTCHA_BORDER_COLOR = "kaptcha.border.color";
//噪声干扰
public static final String KAPTCHA_NOISE_COLOR = "kaptcha.noise.color";
//设置是否字母等
public static final String KAPTCHA_TEXTPRODUCER_CHAR_STRING = "kaptcha.textproducer.char.string";
//设置长度
public static final String KAPTCHA_TEXTPRODUCER_CHAR_LENGTH = "kaptcha.textproducer.char.length";
...
1
2
3
4
5
6
7
8
9
10
11
12
<servlet>
<servlet-name>KaptchaServlet</servlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
<init-param>
<param-name>kaptcha.border.color</param-name>
<param-value>red</param-value>
</init-param>
<init-param>
<param-name>kaptcha.textproducer.char.string</param-name>
<param-value>abcdefghigk</param-value>
</init-param>
</servlet>

在网页加入img验证码图片

1
<img th:src="@{/kaptch.jpg}" alt="" />

验证

在KaptchaServlet生成验证码图片的时候,会将图片的验证码保存到session中,并将用户的输入与其进行比较,session的键为KAPTCHA_SESSION_KEY

1
2
3
4
if (session.getAttribute(KAPTCHA_SESSION_KEY).equals(code)){
userService.register(user);
return "user/regist_success";
}

异步请求

了解

很多情况下,我们点击浏览器之后,浏览器会进行相应,比如点击加入购物车,如果此时比进行刷新,购物车的数量不会增加,但是如果刷新则需要刷新整个页面,此时占用的网络带宽比较大,而且浏览器进行渲染的工作量比较多

Ajax(asynchronous javascript and xml)

应用

现在在注册的时候,当文本输入框失去焦点的时候,我们自动在数据库查询是否用户名已经被注册,如果被注册,则返回给用户提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//首先创建xmlhttprequest
var xmlHttpRequest;
function createXmlHttpRequest(){
if (window.XMLHttpRequest){
//符合dom2标准的浏览器
xmlHttpRequest=new XMLHttpRequest();
}else if(window.ActiveXObject){
//ie浏览器
try {
xmlHttpRequest = new ActiveXObject("Microsoft.XMLHTTP");
}catch (e){
xmlHttpRequest=new ActiveXObject("Msxm12.XMLHTTP");
}
}
}

function ckUname(uname){
createXmlHttpRequest();
var url="user.do?operate=ckUname&uname="+uname;
xmlHttpRequest.open("GET",url,true);
//设置回调函数
xmlHttpRequest.onreadystatechange=ckUnameCB;
//发送请求
xmlHttpRequest.send();
}
function ckUnameCB(){
if (xmlHttpRequest.ready()==4&&xmlHttpRequest.status==200){
alert("用户名存在");
}
}

我们需要在后台进行相应的信号值,交给前端接受,比如返回1表示可以注册,返回0表示不可以注册

过滤器基本知识

image-20220412155509142

这是源代码,并没有对操作进行任何的限制

1
2
3
4
5
6
7
8
@WebServlet("/demo01.hi")
public class Test extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("demo01 is ing");
req.getRequestDispatcher("fruit/demo01.html").forward(req,resp);
}
}

下面进行限制,进行过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@WebFilter("/demo01.hi")
public class Demo01Filter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("我要放行给servlet");
//以上使servlet之前
filterChain.doFilter(servletRequest,servletResponse);

System.out.println("我要放行给浏览器");
}

@Override
public void destroy() {
Filter.super.destroy();
}
}

同时servlet也可以进行通配符设置等等,和servlet相同

过滤器链

在servlet之前,可以含有多个过滤器对其进行检查过滤,最终交给servlet执行。而过滤器链的运行是按照他的全类名顺序进行运行,但是如果在配置文件中进行了过滤器的定义,那么就是按照定义的顺序进行

image-20220412165340064

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.zss.filter;

import javax.servlet.*;
import java.io.IOException;

public class filter01 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("我是01");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("01走了");
}

@Override
public void destroy() {
Filter.super.destroy();
}
}

image-20220412165416284

在前面的小项目中在过滤器中设置编码

1
2
3
4
5
6
7
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

servletRequest.setCharacterEncoding("UTF-8");

filterChain.doFilter(servletRequest,servletResponse);
}

内部转发

image-20220402160305352

这是一次响应的时间,服务器内部经过了多少组件浏览器并不知道

下面时两个组件之间的关系

1
2
3
4
5
6
7
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("hi").forward(req,resp);
System.out.println("我是组件一");
}
}
1
2
3
4
5
6
public class GetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("我是组件2");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.zss.servlets.TestServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/add</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>GetServlet</servlet-name>
<servlet-class>com.zss.servlets.GetServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>GetServlet</servlet-name>
<url-pattern>/hi</url-pattern>
</servlet-mapping>

image-20220402161455991

成功将一个页面的处理,经过了两个组件‘

重定向

image-20220402160545843
1
2
3
4
5
6
7
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("我是组件一");
resp.sendRedirect("hi");
}
}

当我们重定向的时候,标志状态为302,将页面重新定向到hi

image-20220402161939291

Java中类与对象以及存在形式

Java中类与对象

在Java程序中可以认为成一种自定义发数据类型,而例如int类型是由Java提供的数据类型。比如人类,可以认为是一种包含一个鼻子两个眼睛等等属性和会直立行走等等行为的一个类,而猫类可以认为成由毛茸茸的毛四条腿等等属性和吃老鼠等等行为构成的一个生物集合,也可以认为一个数据类型。

而对象可以是类中一个具体的示例,比如张奶奶家里的小花猫就是猫类的一个对象,你我也是人类中的其中一个对象。

image-20220413094813438

观察以下代码:可以发现一个cat类可以定义好多对象,而且将来可以灵活添加很多属性。类是对象的抽象,对象是类的具体实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**首先定义猫类,猫拥有名字,年龄,性别,主人等属性*/
public class Cat {
String name;
int age;
char gender;
String host;
}
class test{
public static void main(String []args){
//定义对象cat1,分别给予名字,年龄等等属性
Cat cat1=new Cat();
cat1.name="花花";
cat1.age=2;
cat1.gender='公';
cat1.host="贵妃娘娘";
//定义对象cat2,并且定义等等一系列行为
Cat cat2=new Cat();
cat2.name="阿福";
cat2.age=3;
cat2.gender='母';
cat2.host="华妃娘娘";
//访问猫的属性
System.out.println(cat1.name+cat1.host+cat1.age+cat1.gender);
System.out.println(cat2.name+cat2.host+cat2.age+cat2.gender);
}
}

image-20220413094840226

在内存中的存在形式

  1. 栈:一般存放基本数据类型。
  2. 堆:用于存放对象。
  3. 方法区:常量池(常量,比如字符串等),类加载信息。

根据图中可知,Cat类存在于栈中,存储着cat1的地址信息,而cat1在堆中,存储着其属性的地址信息,而真正的属性值在方法区的常量池中。所有类的属性值都存储在方法区中,采用套娃的方式进行存储。

image-20220413095040736

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**首先定义猫类,猫拥有名字,年龄,性别,主人等属性*/
public class Cat {
String name;
int age;
char gender;
String host;
}
class test extends Cat{
public static void main(String []args){
//定义对象cat1,分别给予名字,年龄等等属性
Cat cat1=new Cat();
cat1.name="花花";
cat1.age=2;
cat1.gender='公';
cat1.host="贵妃娘娘";
//定义对象cat2,并且定义等等一系列行为
Cat cat2=new Cat();
cat2.name="阿福";
cat2.age=3;
cat2.gender='母';
cat2.host="华妃娘娘";
//访问猫的属性
System.out.println(cat1.name+cat1.host+cat1.age+cat1.gender);
System.out.println(cat2.name+cat2.host+cat2.age+cat2.gender);

}
}

image-20220413095113870

接下来我们执行一下下一段代码,注意其中对象的机制。直接将对象cat1赋值给cat2,其实相当于将cat1的地址给予的cat2,

image-20220413095134754

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**首先定义猫类,猫拥有名字,年龄,性别,主人等属性*/
public class Cat {
String name;
int age;
char gender;
String host;
}
class test {
public static void main(String []args){
//定义对象cat1,分别给予名字,年龄等等属性
Cat cat1=new Cat();
cat1.name="花花";
cat1.age=2;
cat1.gender='公';
cat1.host="贵妃娘娘";
//定义对象cat2,并且定义等等一系列行为
Cat cat2=cat1;
System.out.println(cat1.name+cat1.host+cat1.age+cat1.gender);
System.out.println(cat2.name+cat2.host+cat2.age+cat2.gender);

}
}

image-20220413095217379

最终我们更改cat2的值,看看是否cat1的值也是否会发生改变???我们更改了cat2的name,可以发现最终cat1也发生了改变,是因为这里改变直接改变了方法区中的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**首先定义猫类,猫拥有名字,年龄,性别,主人等属性*/
public class Cat {
String name;
int age;
char gender;
String host;
}
class test {
public static void main(String []args){
//定义对象cat1,分别给予名字,年龄等等属性
Cat cat1=new Cat();
cat1.name="花花";
cat1.age=2;
cat1.gender='公';
cat1.host="贵妃娘娘";
//定义对象cat2,并且定义等等一系列行为
Cat cat2=cat1;
cat2.name="旺旺";
System.out.println(cat1.name+cat1.host+cat1.age+cat1.gender);
System.out.println(cat2.name+cat2.host+cat2.age+cat2.gender);

}
}

image-20220413095240876

Java对象中的方法调用机制以及传参陷阱

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String []args){
Test test=new Test();
int sumNum=test.doubleSum(10,20);
System.out.println("两数之和为"+sumNum);
}
public int doubleSum(int num1,int num2){
int sum=num1+num2;
return sum;
}
}

image-20220413095415163

执行上述代码得到结果为30.

1
2
3
Test test=new Test();
int sumNum=test.doubleSum(10,20);
System.out.println("两数之和为"+sumNum);

这一段代码在main栈里面执行,Test test=new Test();执行之后其内存结构如图,会在堆中创建一个对象。

image-20220413095448347

而当执行int sumNum=test.doubleSum(10,20);时候,栈中会产生一个独立的栈用于运行函数doubleSum。结构如下:

image-20220413095514244

而当在栈中运行到return时,独立的栈运算结束,独立的空间栈被释放,将最终得到的值根据原来保留的地址传回main栈中,并且将值读给sumNum。

方法传参机制常见陷阱

首先说明,Java中分为值传递机制与引用传递机制。

首先我们回顾以下Java中jvm的虚拟内存存储机制,其中包括栈,堆和方法区

  1. 栈:一般存放基本数据类型。
  2. 堆:用于存放对象。
  3. 方法区:常量池(常量,比如字符串等),类加载信息。

image-20220413095710506

而当我们更改时,如果调用的方法仅仅涉及独立方法栈(即运行方法时独立开辟的栈)时,除了return值可以将其结果保存下来之外,其他都不会将更改的结果保存,是因为方法栈在中的方法在运行结束后,就立即释放掉。其保存子啊方法栈中所有的信息都会释放掉,但是如果方法栈云运行的数据信息更改到堆或者方法区时,就可以永久保存。

查看以下的例子:可以发现在运行过程中num1[0]=200; num2[0]=100;这两个语句的运行结果100,200是保存在堆中,即使方法执行完毕释放,也没有影响到堆中的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Arrays;

/**
* @author 张硕硕
*/
public class Test {

public static void main(String []args){
int []a={10,20};
int []b={30,40};
Test test=new Test();
test.exchangeSum(a,b);
System.out.println(Arrays.toString(a)+Arrays.toString(b));
}
public void exchangeSum(int[] num1, int []num2){
System.out.println(Arrays.toString(num1)+Arrays.toString(num2));
num1[0]=200;
num2[0]=100;
System.out.println(Arrays.toString(num1)+Arrays.toString(num2));
}
}

img

最后我们来观察以下这个例子:可以发现a数组与b数组的交换只是在栈中的独立方法栈中运行的,exchansum函数结束之后,该栈被立即释放掉。虽然a数组与b数组的地址发生了交换,但是这些改变仅仅在方法栈中运行,并没有传回到堆或者main栈,所以改变并不会被记录。

image-20220413095807232

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Arrays;

public class Test {

public static void main(String []args){
int []a={10,20};
int []b={30,40};
Test test=new Test();
test.exchangeSum(a,b);
System.out.println(Arrays.toString(a)+Arrays.toString(b));
}
public void exchangeSum(int[] num1, int []num2){
System.out.println(Arrays.toString(num1)+Arrays.toString(num2));
int []temp=num1;
num1=num2;
num2=temp;
System.out.println(Arrays.toString(num1)+Arrays.toString(num2));
}
}

image-20220413095834868

我们再来运行一段代码,发现函数的作用是将对象置空,这样输出t.name会发生报错,但是结果却是正常的输出,其原因是因为这个过程仅仅在独立方法栈中运行,并没有影响到main中的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Arrays;

public class Test {
String name="你好";
public static void main(String []args){
Test t=new Test();
t.exchangeSum(t);
System.out.println(t.name);
}
public void exchangeSum(Test test){
test=null;
}
}

image-20220413095856606

递归方法调用中内存细节以及代码运行顺序

首先运行以下代码块,观察其运行结果,输出1234,4321而不是。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
String name="你好";
public static void main(String []args){
Test test=new Test();
test.function(4);
}
public void function(int n){
if (n>1){
function(n-1);
}
System.out.println("n="+n);
}
}

img

我们来仔细看一下代码的运行情况以及运行顺序,可以发现在递归调用的过程中,会占用大量的空间。

image-20220417114354935

最后我们加深一下理解,运行一下以下代码,仅仅在方法区加上了一个else,发现结果仅仅输出了一个1,是因为在其他满足的条件下,就不会区执行输出语句了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
String name="你好";
public static void main(String []args){
Test test=new Test();
test.function(4);
}
public void function(int n){
if (n>1){
function(n-1);
}
else{
System.out.println("n="+n);}
}
}

image-20220417114447564