前言
这一节主要讲的是Request和Response还有一些实例
1. 介绍
就是这两个参数
@WebServlet("/demo7")
public class ServletDemo7 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//使用request对象,获取请求数据 输入网址就是getString name = req.getParameter("name");//url?name=zhangsan//使用response对象,设置响应数据resp.setHeader("content-type", "text/html;charset=UTF-8");resp.getWriter().write("<h1>" + name+",欢迎你" + "</h1>");}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
}
2. requset
2.1 request继承体系
前面两个我们都用过了
RequestFacade实现的方法都是前面两个接口的
requset对象是由Tomcat创建的
2.2 request请求数据
请求行:请求方式+请求资源路径+请求协议+版本号
虚拟目录就是我们项目的目录
URL就是问号前面的
问号后面的是get方式的请求参数
URI:短的路径
@WebServlet("/demo7")
public class ServletDemo7 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String method = req.getMethod();//获取请求方式System.out.println(method);//GETString contextPath=req.getContextPath();//获取虚拟目录,就是项目的访问路径System.out.println(contextPath);//没有设置项目的访问路径,那么返回的默认就是这个模块的名称StringBuffer url=req.getRequestURL();System.out.println(url.toString());//获取URLString uri=req.getRequestURI();//获取uriSystem.out.println(uri);String queryString=req.getQueryString();//获取请求参数System.out.println(queryString);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
}
拿着请求参数,切割就可以得到很多个键值对了
在把等号切割就OK了
获取客户端使用的浏览器的版本信息
只有post请求才有请求体
比如提交表单参数,就会把表单的参数放在请求体里面
上传图片文件就用字节流
上传的文本就用字符流
String agent=req.getHeader("User-Agent");//User-Agent是获取浏览器版本的请求头System.out.println(agent);
获取请求体要post请求,我们写个表单
<!--form的action就是虚拟路径的名称,就是项目加资源路径,这个就会把你表单提交的数据,通过post提交到demo7就是dopost-->
<form action="/Tomcat-demo1/demo7" method="post" ><input type="text" name="username"><input type="password" name="password"><input type="submit"></input></input>
</form>
@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//获取post的请求体:请求参数,通过流的方式来获取//表单里面的数据都是字符串,所以我们用字符流BufferedReader br = req.getReader();//写完req.getReader(),alt加enter就会自动添加局部变量。// table补齐,或者enter也可以补齐String line=br.readLine();//参数只有一行System.out.println(line);//流不用关闭,因为req被销毁的时候,流就自动关闭了}
注意要关闭@WebServlet(urlPatterns = “/”)这个哦,上一节说过的
2.2 request通用方式获取请求参数
这里要使用两种方式获取请求参数,要写两份,很麻烦,能够统一就好了
因为获取请求参数,后面执行的代码都是一样的,如果不同意就要写两份,统一的话,互相调用就可以了,因为代码是一样的
但是不能删post的,因为有post请求咋办,但是post和get代码一样,所以互相调用
params就长这个样子了
这三个方法就是通用方式
<!--form的action就是虚拟路径的名称,就是项目加资源路径,这个就会把你表单提交的数据,通过get提交到demo7就是dopost-->
<form action="/Tomcat-demo1/demo7" method="get" ><input type="text" name="username"><br><input type="password" name="password"><br><input type="checkbox" name="hobby" value="1" >游泳<input type="checkbox" name="hobby" value="2" >爬山<br>
<!-- 因为hobby是一个复选框 选第一个值,提交过来的就是1,-->
</br><input type="submit"></input></input>
</form>
@WebServlet("/demo7")
public class ServletDemo7 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("get.....");//1.获取所有参数的map集合Map<String, String[]> map = req.getParameterMap();for (String key : map.keySet()) {//这个是遍历map集合//username:zhangsan lisiSystem.out.print(key+":");//获取值String[] values = map.get(key);for (String value : values) {System.out.print(value+" ");}System.out.println();}}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// this.doGet(req, resp);}
}
变成get了,因为参数在地址那里
System.out.println("---------------------------");//2.根据可以获取参数值,数组,,,意思就是根据一个key来获取对应的值,因为一个key可能会对应多个值,所以用的数组//我们明确知道hobby对应多个值String[] hobbies = req.getParameterValues("hobby");for (String hobby : hobbies) {System.out.println(hobby);}
重启服务器,刷新表单页面
这里的密码是空字符串
//3.根据key来获取单个参数值,意思是这个键对应的值只有一个String username = req.getParameter("username");String password = req.getParameter("password");System.out.println(password);System.out.println(username);
然后我们把代码原封不动的复制给dopost
然后表单提交方式改为post
这里建议要刷新一下,不然可能一直是get,如果刷新不行,就关了重新来
既然get和post写的一模一样了,那就不要写两遍了
this.doGet(req, resp);
这样就会去调用doget,然后对应的参数也是一样的,就可以正常使用了,因为doget和dopost参数一模一样所以可以相互调用
我们还是post的提交方式
这个的反应就是post
然后打印出来,其实走的是doget
2.4 idea模版创建servlet
我们点击设置,编辑器,文件和代码模版,其他,web,Java代码模版,点Servlet Annotated Class.java
改造一下,修改dopost,和webservlet就可以了
如果新建里面没有servlet的话,要到设置里面,编辑器,文件和代码模版,文件,新建一个servlet
这样就有了
第一个选项可以不用选
就这样我们就创建好了
下面分享一下里面的模版内容
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")#if ($JAVAEE_TYPE == "jakarta")
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
#else
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
#end
import java.io.IOException;@WebServlet("/${Entity_Name}")
public class ${Class_Name} extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doGet(request,response);}
}
2.5 请求参数中文乱码-post解决方案
我们先演示一下乱码
改成get和demo8
这样就乱码了,其实post也会乱码
改成post
服务器后退一下,就可以访问了,只有servlet的代码没有变,就没有必要重启代码
也是乱码
//2.解决乱码:post,getReader()--->读中文数据的时候不是utf-8,所以会出错request.setCharacterEncoding("UTF-8");//设置字符输入流的编码、、、但是要先设置在获取//1.获取一下usernameString username = request.getParameter("username");System.out.println(username);
下面来get
2.6 请求参数中文乱码-get解决方案
产生get乱码的原因就是解码与编码不一样,但是getQueryString解码方式写死了,所以我们要了解URL编码才行
这里就是URL编码了
Java里面也有URL编码
选第一个
String username="张三";//1.url编码String encode=URLEncoder.encode(username,"utf-8");System.out.println(encode);
//url解码String decode = URLDecoder.decode(encode, "utf-8");System.out.println(decode);
String decode = URLDecoder.decode(encode, "ISO-8859-1");
Tomcat就是这样处理的
所以会这样
注意乱码和张三的字节是一样的
所以可以把乱码转换成二进制数据,在用UTF-8变成张三
//3.转换成字节数据byte[] bytes = decode.getBytes("ISO-8859-1");for (byte b : bytes) {System.out.print(b+" ");}
6个字节
//3.转换成字节数据byte[] bytes = decode.getBytes("ISO-8859-1");//转换为字符串String s = new String(bytes, "utf-8");System.out.println(s);
所以我们只需要干第三步和第四步
//1.获取一下usernameString username = request.getParameter("username");System.out.println("乱码前"+username);//3.get getParameter针对post用的流,针对get用的字符串,所以不一样,,所以get//get获取参数的方式;getQueryString,没有走流,所以不一样//乱码原因//tomcat用的是ISO-8859-1解码//先对乱码数据编码byte[] bytes = username.getBytes("ISO-8859-1");
// byte[] bytes = username.getBytes(StandardCharsets.ISO_8859_1);//这个也是一样的
// String s = new String(bytes, "utf-8");username = new String(bytes, StandardCharsets.UTF_8);System.out.println("乱码后"+username);
//合二为一username = new String(username.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
这句代码还可以解决post的
因为我们当前的tomcat只到了7版本所以会出错
2.7 request请求转发
创建两个demo
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {System.out.println("demo9");//请求转发request.getRequestDispatcher("/demo10").forward(request, response);}
资源跳转,那么我们就要共享数据,数据就要存在一个地方,这个地方就是request
//存储数据request.setAttribute("msg", "hello");//这个数据就会放在request,随着转发而转发
//获取数据Object msg = request.getAttribute("msg");System.out.println(msg);
修改了servlet代码就要重启服务器
第一虽然资源转变了但是路径并没有变
只能在同一个服务器里边,不能跑到百度那里去了
3. response
3.1 设置响应数据功能介绍
http是协议和版本号,200是状态码,OK是状态码的描述
3.2 完成重定向
302是告诉我们要重定向,location是告诉重定向的位置在哪里
System.out.println("resp1");//重定向//1. 设置响应状态码response.setStatus(302);//2,。设置响应头//response.setHeader("Location", "http://localhost:8080/resp2");//或者response.setHeader("Location", "/Tomcat-demo1/resp2");//这个路径要加虚拟目录名称
重启服务器,反退输入resp1
我们发现重定向的302和location都固定,所以可以优化
response.sendRedirect( "/Tomcat-demo1/resp2");
因为请求资源是通过浏览器发出的,所以可以到任意的资源
response.sendRedirect( "https://www.itcast.cn");
这个就是外部资源
两次请求就有两个requset域,就不能在多个资源中使用request共享数据
3.3 资源路径问题
重定向要加上虚拟目录,资源转发不需要
要访问远的,浏览器使用,就加上虚拟目录,近的,服务端使用,就不需要加上虚拟目录
href是超链接,浏览器发出的,远的
form表单浏览器发出请求,提交到服务器的某一个资源
转发–》服务器内部
注意虚拟目录的这个值我们可以动态的配置,
path那里就可以动态的配置
但是这里改的话,其他地方的虚拟目录就写死了
所以可以让虚拟目录的值动态的获取
String contextPath = request.getContextPath();//获得虚拟目录response.sendRedirect(contextPath+"/resp2");
这样和前面也是一样的
3.4 response响应字符数据
//响应字符数据,设置字符数据的响应体PrintWriter writer = response.getWriter();//字符输出流writer.write("aaaa");
writer.write("<h1>aaaa</h1>");
但是这样却原模原样的展示了
因为没有告诉我们响应的是html的,这里默认的就是纯文本的形式
//响应字符数据,设置字符数据的响应体PrintWriter writer = response.getWriter();//字符输出流//设置头response.setHeader("content-type", "text/html");//告诉浏览器,将来我响应的数据格式是html格式,不是纯文本writer.write("aaaa");writer.write("<h1>aaaa</h1>");
但是真正响应的数据还是原来的样子,只不过用了html的解析
第一writer不用关闭,这个流不用关闭
第二中文数据也可以响应,但是会乱码,因为这个字符输出流还是上面那个,不是UTF-8
所以要设置流的编码
就是对 response.setHeader(“content-type”, “text/html”);简化书写格式
setContentType这个方法不仅可以设置tContentType这个头,还可以设置流的编码,但还是要先设置,在获取流的编码
response.setContentType("text/html;charset=utf-8");//要的是html的数据格式,;charset=utf-8就可以支持中文了PrintWriter writer = response.getWriter();//字符输出流writer.write("你好");writer.write("<h1>aaaa</h1>");
3.5 response响应字节数据
为了响应字节数据,我们用一张图片来
//读取文件FileInputStream fis = new FileInputStream("E:\\JavaWeb\\java-web\\Tomcat-demo1\\src\\main\\resources\\pin\\a.png");//获取字节输出流ServletOutputStream os = response.getOutputStream();//目的地就是页面的响应数据体//完成流的copybyte[] b = new byte[1024];int len = 0;while ((len = fis.read(b)) != -1) {os.write(b, 0, len);}fis.close();//关闭流。os不用关
我们一般不这样实现流的对拷,我们一般使用工具类,引个坐标
<dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.17.0</version></dependency>
选第三个,如果没有就刷新Maven
//读取文件FileInputStream fis = new FileInputStream("E:\\JavaWeb\\java-web\\Tomcat-demo1\\src\\main\\resources\\pin\\a.png");//获取字节输出流ServletOutputStream os = response.getOutputStream();//目的地就是页面的响应数据体IOUtils.copy(fis,os);//参数就是一个输入,一个输出,就可以拷贝了
4. 案例
4.1 环境准备与用户登录
先准备页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<!--注意将请求提交给loginServlet-->
<form action="/web_demo_war_exploded/loginServlet" method="post">用户名:<input type="text" name="username"><br>密码:<input type="password" name="password"><br><input type="submit" value="登录">
</form>
</body>
</html>
mabatis环境准备
数据库的
-- 创建数据库
CREATE DATABASE db1;USE db1;-- 创建用户表
CREATE TABLE tb_user(id INT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(20) UNIQUE,PASSWORD VARCHAR(32)
);-- 添加数据
INSERT INTO tb_user(username,PASSWORD) VALUES('zhangsan','123'),('lisi','234');SELECT * FROM tb_user;
OK这样我们就创建好了
现在在创建user的实体类
package pojo;public class User {private Integer id;private String username;private String password;public User() {}public User(Integer id, String username, String password) {this.id = id;this.username = username;this.password = password;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}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;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +", password='" + password + '\'' +'}';}
}
现在导入mybatis坐标,mysql驱动坐标
原来
现在导入mybatis坐标
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.5</version></dependency>
mysql驱动坐标
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.34</version></dependency>
现在创建mybatis核心配置文件,UserMapper.xml映射文件,和UserMapper接口
mybatis核心配置文件
在resources底下新建
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><!-- 起别名--><typeAliases><package name="pojo"/></typeAliases><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/>
<!-- db1数据库,useSSL=false关闭安全链接,性能更高,useServerPrepStmts=true,预编译功能&就是and--><property name="url" value="jdbc:mysql://localhost:13306/db1?useSSL=false&useServerPrepStmts=true"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></environment></environments><mappers>
<!-- 扫描mapper,是从java开始的--><package name="mapper"/></mappers>
</configuration>
在新建一个接口
UserMapper.xml映射文件,
在resources底下新建目录,这个目录,和java对应,和mapper的目录必须要一模一样
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.UserMapper">
<!-- 这里的接口名称就是全路径名,就是不写java目录的接口的路径名--></mapper>
public interface UserMapper {//查询用户的,根据用户名和密码,返回user对象@Select("select * from tb_user where username = #{username} and password = #{password}")User select(String username, String password);//接下来对这个接口的方法,声明一个sql来对应,因为sql语句简单,所以用注解
}
还不够,因为有多个参数,所以还要加上param注解
public interface UserMapper {//查询用户的,根据用户名和密码,返回user对象@Select("select * from tb_user where username = #{username} and password = #{password}")User select(@Param("username") String username,@Param("password") String password);////@Param("username")这个username要和#{username}里面的一一对应//接下来对这个接口的方法,声明一个sql来对应,因为sql语句简单,所以用注解
}
所以mabatis的工作就做完了
<form action="/web_demo_war_exploded/loginServlet" method="post">
在login.html中,action还要改一下
要加上虚拟目录
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<!--注意将请求提交给loginServlet-->
<!--虚拟地址中的Tomcat-demo1,就是项目名称,loginServlet就是资源-->
<form action="/Tomcat-demo1/loginServlet" method="post" id="form"><h1 id="loginMsg">LOGIN IN</h1><p>Username:<input type="text" name="username" id="username"></p><p>Password:<input type="password" name="password" id="password"></p><div id="subDiv"><input type="submit" class="button" value="login up"><input type="reset" class="button" value="reset"><a href="register.html">没有账号?点击注册</a></div>
</form>
</body>
</html>
下面写一个login的servlet
我们在response底下创建
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//1.接收用户名和密码String username = request.getParameter("username");String password = request.getParameter("password");//2.调用mabatis来完成查询//2.1 获取SqlSessionFactory对象------》直接官网粘贴String resource = "mybatis-config.xml";//mybatis-config.xml这个直接就在resources下,所以直接写名字就可以了InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//2.2 获取SqlSession对象SqlSession sqlSession = sqlSessionFactory.openSession();//2.3 获取MapperUserMapper userMapper = sqlSession.getMapper(UserMapper.class);//Mapper的类型就是UserMapper.class//2.4 调用方法User user = userMapper.select(username, password);//2.5 释放资源sqlSession.close();//3.判断user是否null,为null登录失败//获取字符输出流,并设置content typeresponse.setContentType("text/html;charset=utf-8");PrintWriter writer = response.getWriter();//用来响应数据,响应的是中文数据,所以还要设置if (user != null) {//登录成功writer.write("登录成功");}else {//登录失败writer.write("登录失败");}}
这样我们就写完了,我们先访问login.html
先故意输错密码
但是我这样运行不了,我修改一下
这里加个cj,因为我是mysql8版本的
然后就是我修改了一下目录
然后就是这里,我们应该写3306,或者不写locallhost
<property name="url" value="jdbc:mysql:///db1?useSSL=false&useServerPrepStmts=true"/>
ok,经过我的仔细检查,发现错误最终在
<property name="url" value="jdbc:mysql://localhost:3306/db1?allowPublicKeyRetrieval=true&useSSL=false"/>
就是这个url没写对,害得我网页一直是500错误,可以把网页的错误给ai看一下,就可能知道是什么错误了
在修改一下
<property name="url" value="jdbc:mysql://localhost:3306/db1?allowPublicKeyRetrieval=true&useSSL=false&useServerPrepStmts=true"/>
其实没什么大的错误,就是cj,还有mysql版本,还有就是这个url问题
具体细节和代码,可以看我的gitee
gitee
下面我们就直接运行了
4.2 用户注册
UserMapper
//根据用户名,查询用户对象@Select("select * from tb_user where username = #{username}")
// User selectByUsername(@Param("username") String username);User selectByUsername(String username);//只有一个参数,不需要写Param注解//添加用户@Insert("insert into tb_user values(null,#{username},#{password})")//因为主键是自增长的,所以不用写出来,写个null就可以了void add(User user);
下面开始写注册页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div class="reg-content"><h1>欢迎注册</h1><span>已有账号?</span><a href="login.html">登录</a>
</div><form action="/Tomcat-demo1/registerServlet" method="post" id="reg-form"><table><tr><td>用户名:</td><td class="inputs"><input type="text" name="username" id="username"><br>
<!-- <span id="username_err" class="err-msg" style=".err-msg">用户名不太受欢迎</span>--></td></tr><tr><td>密码:</td><td class="inputs"><input type="password" name="password" id="password"><br>
<!-- <span id="password_err" class="err-msg" style="color: red; font-size: 12px;">密码格式有误</span>--></tr></table><div class="button"><input value="注册" type="submit" id="reg-btn"></div><br class="clear">
</form>
</body>
</html>
下面开始创建servlet
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//1.接收用户数据String username = request.getParameter("username");String password = request.getParameter("password");//分装用户对象,因为void add(User user);,参数就是用户对象User user = new User();user.setUsername(username);user.setPassword(password);//2.调用mapper,根据用户名查询用户对象//直接复制过来String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);//调用方法User u = userMapper.selectByUsername(username);//3.判断用户对象是否为nullif (u == null) {//不存在就添加userMapper.add(user);//因为这个是增删改的操作,所以还要一定要提交事务sqlSession.commit();//释放资源sqlSession.close();}else {//存在就添加response.setContentType("text/html;charset=utf-8");response.getWriter().write("用户名已存在");//中文的话,就要设置一下编码}}
OK,我们就这样水淋淋的写完了
先访问登录页面,因为登录页面可以跳转到注册页面
点击注册
就这样我们就成功了
4.3 SqlSessionFactory工具类抽取
SqlSessionFactory这个对象我们在两个servlet中都创建了一次,相当于创建了两次,所以有点问题
第一就是重复了
第二就是SqlSessionFactory工厂对象创建一个就可以了,因为这是一个链接池,创建一个就好了
一般来说,重复代码,抽取一个工具类就可以了
连包带类的创建的话,直接就创建java类就可以了
public class SqlSessionFactoryUtils {private static SqlSessionFactory sqlSessionFactory;//将那三行代码写在静态代码块里面,就只会执行一次了,随着类的加载而自动执行,只会一次,但是静态代码块不能抛异常,所以我们要try catchstatic {try {String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);} catch (IOException e) {e.printStackTrace();}}public static SqlSessionFactory getSqlSessionFactory() {//这样我们只需要在这个方法里面放回静态代码块里面的工厂就可以了//如何用静态代码块里面的局部变量呢,提升一下作用域就可以了---》设置一个private变量return sqlSessionFactory;}
}
这样我们就写完了工具类
我们在doget里面添加这个代码就可以了
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();
然后我们发现sqlSession也是重复的
但是这个不需要共享同一个
因为这只是一个对象,不同用户之间最好不要共享,连接池共享就可以了
欧克,我们还是可以运行的
总结
最好再提供一下gitee
gitee
下一次讲JSP