虽然Servlet和JSP学习也使用了挺长时间了,但是最近读了下《Servlet和JSP核心编程》这本书,虽然是熟悉的知识,但是仍然有些因为过长时间没使用而忘记了,记下这篇读书笔记,就当是对Servlet和JSP涉及的核心知识点进行一个整理与回顾吧。书中对Servlet与JSP进行了完整的介绍,总体上还是非常基础的,如果是初学者,或者有了一定开发经验的开发人员,都能从书中得到收获。
下面的条目是我在阅读过程中记下的笔记,调理也许不是那么清晰,但是对于自己在需要的时候复习还是很有帮助的。
1、应该尽可能覆盖doGet()和doPost()方法,不要为了省事直接覆盖service()方法。
原因由两个:一是丧失想要覆盖其他doXXX()方法的可能性,不排除以后需要覆盖这些方法的可能性;二是无法实现getLastModified方法,该字段可以得到请求最后修改的时间,如果请求的资源没有发生修改,那么服务端将返回304状态码,表示请求的资源已经是最新的,上次请求的资源可以继续使用。
getLastModified扩展:如果方法返回的是整数,并且客户端请求头没有包含If-Modified-Since字段或者已经包含If-Modified-Since字段,但是返回值比If-Modified-Since指定的时间更新的话,会调用doGet方法请求最新的资源,并返回包含Last-Modified头字段的响应信息。如果返回值比If-Modified-Since字段指定的时间小的话(发生在之前),那么服务端可以做出判断上次响应给客户端的资源仍然可用,于是返回304状态码,告诉客户端你上次请求的这个资源可以继续使用。
2、不要为了阻止并发访问Servlet实例而去实现SingleThreadModel接口,原因有二:
1) 由于实现SingleThreadModel接口就相当于对Servlet实例进行了同步,表面上不用担心因为多线程并发访问的问题造成数据的安全性和一致性,但是如果servlet被频繁地访问,那么同步对性能造成的影响是巨大的。比如在servlet等待IO任务的时候,servlet不能处理其他的请求
2) servlet规范允许服务器使用多个实例处理请求,来替代对单个实例的请求进行排队的方案。当然我们并不希望使用多个实例,因为多个servlet实例都拥有变量的单独副本,从而造成数据不能正确共享,在servlet之间传递消息也有困难。
注:SingleThreadModel在servlet规范2.4中明确反对使用这种方式。为了实现同步,使用synchronized往往是更好的选择。
3、如果需要读取请求参数,为了防止跨站脚本攻击(XSS),必须过滤出特殊的HTML字符。这种过滤方式的问题可能导致输出部分缺失。比如将’<’替换成’<’,’>’替换成’>’,’”’替换成’"’,’&’替换成’&’。
4、使用Servlet构建Excel电子表格
设置响应头:
response.setContentType("application/vnd.ms-excel");
PrintWriter out = response.getWriter();
例子:
public class App1 extends HttpServlet{
public void doGet(HttpServletRequest req,HttpServletResponse res){
response.setContentType("application/vnd.ms-excel");
PrintWriter out = response.getWriter();
out.println(\tQ1\tQ2\tQ3\tQ4\tTotal);
out.println("Apples\t78\t87\t92\t29\t=SUM(B2:E2)");
out.println("Oranges\t78\t87\t92\t29\t=SUM(B3:E3)");
}
5、使用Servlet生成JPEG图像
第一步:创建一个BufferedImage
int width = 100px;
int height = 100px;
BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
第二步:在BufferedImage绘制图像
Graphics2D g = (Graphics2D)image.getGraphics();
g.setXXX();
g.fill();
g.draw();
第三步:设置Content-Type响应头
response.setContentType("image/jpeg");
第四步:获取输出流
OutputStream out = response.getOutputStream();
第五步:以JPEG格式将图像发送到输出流
ImageIO.write(image,"jpg",out);
6、使用Cookie记录偏好
form.jsp
<form action="/register" method="post">
firstName:<input name="firstName"/><br>
lastName:<input name="lastName"/><br>
<input type="submit"/>
</form>
RegisterServlet.java
@WebServlet("/register");
public class RegisterServlet extends HttpServlet{
public void doGet(...){
response.setContentType("text/html");
String firstName = request.getParameter("firstName");
String lastName = request.getParameter("lastName");
Cookie c1 = new Cookie("firstName",firstName);
response.addCookie(c1);
Cookie c2 = new Cookie("lastName".lastName);
response.addCookie(c2);
}
}
7、对发往客户端的URL进行编码
如果使用URL重写进行会话跟踪,大部分页面或者全部页面都必须动态生成,站点的任何静态HTML文档都不能指向站点的动态页面的链接
情况一:在servlet生成的Web页面中含有嵌入的URL,应该调用response的encodeURL,确定当前是否在使用URL重写,仅在必须时附加回话信息;否则不做任何修改直接返回传入的URL。
情况二:在sendRedirect调用中,应该调用encodeRedirectURL
如果最终可能使用URL重写替代Cookie,那么使用URL编码是最好的选择
8、JSP的好处
1) 易于编写HTML并维护
2) 可以使用标准的开发工具开发
3) 开发人员可以集中精力在表示上
4) JSP擅长生成结构化的HTML页面,Servlet擅长业务的处理,可以生成二进制的数据
9、JSP脚本、表达式编译成的Servlet
Foo.jsp
<h2>foo</h2>
<!-- JSP表达式,调用后会显示bar方法执行返回的结果,结尾不需要分号 -->
<%= bar() %>
<!-- JSP scriplet,直接调用bar(),结尾需要分号 -->
<% bar(); %>
Foo_jsp.java,之后被编译成Foo_jsp.class
public void _jspInit(){ ..} – 当JSP网页一开始执行时,最先执行此方法,执行初始化工作
public void _jspDestory(){...} – JSP网页最后执行的方法
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {
HttpSession session = request.getSession();
JspWriter out = response.getWriter();
out.println(<h2>Foo</h2>);
out.println(bar());
bar();
}
10、对于JSP的初始化和清理工作,可以使用JSP声明覆盖jspInit()和jspDestroy()方法
JSP声明格式
<%! 变量或者方法 %>
现考虑如下JSP声明:
<%! private int accessCount = 0;%>
<%= ++accessCount%>
不同的客户端得到的技术不是唯一的,也就是由于多个请求共享accessCount变量,该代码不是线程安全的。但是在服务器重新启动之前,所有用户看到的都是同样的结果
11、session属性
使用形式:<%@ page session = “true” %><%– default –%>
session属性属于jsp的page指令。可以控制是否参与会话,
true表示如果存在会话,则绑定到现有的会话中,如果没有则创建一个新的session会话(session的类型是HttpSession);
值为false,表示不会自动创建会话,在将jsp转为servlet时,对变量session的访问会导致出错。但是false也不是无之地,对于高流量的网站,使用false可以节省大量的服务器内存。但是使用false并不表示会禁用会话跟踪,而且会话只是针对用户,并不针对页面,所以关闭页面的会话跟踪没有任何益处
12、isELIgnored属性
默认为false,表示对JSP表达式进行正常的求值,为true则忽略表达式语言。也就是在JSP页面${expression}得到的就是表达式。
在JSP1.2之前(包括JSP1.2)该属性的值都是true,只有在Servlet2.4之后(JSP2.0)该值的默认属性为false。所以如果在访问JSP的时候出现直接输出JSP表达式的情况,需要检查一下你的Servlet版本是否在2.4之前,如果不想更换版本,只需要在JSP页面只能中直接添加该属性为false即可
13、jsp:include动作与include指令
前者会对被包含的JSP的页面在请求时进行处理,后者在页面转换期间就被处理。而且前者会生产两个Servlet文件,后者只会产生一个。
在维护和能力两方面造成jsp:include动作优于include指令。
由于jsp:include动作会会产生两个Servlet而后者则有两个,那么如果通过include指令被包含的页面发生了修改,那么所有包含该页面的jsp文件都需要重新转换成新的Servlet,不然下次请求访问的仍然是修改之前的Servlet。
对于文件包含应该尽可能用jsp:include动作,仅在所包含的文件中定义了主页面需要用到的字段或者方法,或者在所包含的文件中设置了主页面的响应头的时候,才应该使用include指令。
然而,不能因为在所包含的页面定义了主页面的需要的字段或者方法就认为必须应该使用include指令。对此合理的解释是,如果使用jsp:include动作,那么针对类似访问计数功能的页面来说,所有使用该页面的主jsp页面都将显示相同的计数。核心就是产生了两个Servlet,而这两个Servlet之间是无法共享计数值的。
14、基于请求的共享
public class NumberBean{
private int num;
...
}
public class NumberServlet extends HttpServlet{
... doGet(...){
NumberBean number = new NumberBean(new Random().nextInt(Integer.MAX_VALUE));
request.setAttribute("number",number);
String addr = "/WEB-INF/view/number.jsp";
request.getRequestDispatcher(addr).forward(request,response);
}
}
number.jsp
...
<jsp:useBean id = "number" type="servlet.NumberServlet" scope="request"/>
Your number is:
<jsp:getProperty name="number" property="num">
以上这种组合方式只能访问简单类型的属性,对于包装的POJO的属性则需要复杂的语法才能实现,在现在主流的MVC框架中(包括Struts2和Spring Web MVC)都对这点进行了封装,让用户可以像访问简单POJO的属性一样访问包装类型的POJO的属性。
15、EL
${bash}
输出
${bash}
\${1+1} is ${1+1}
输出
${1+1} is 2
${name}
作用域访问次序:PageContext–>HttpServletRequest–>HttpSession–>ServletContext
等同于调用JSP表达式:<%= pageContext.findAttribute(“name”)%>
等同于:
<jsp:useBean id="name" type="somePackage.someClass" scope="...">
<%= name%>
返回某个作用域变量的某个属性
customer.fistName等同于 {customer[“fistName”]}(这种方式很少使用,不推荐使用)
等同于
<%@ page import="bean.Customer"%>
<% Customer customer = (Customer)pageContext.findAttribute("customer"); %>
<%= customer.getFistName() %>
等同于
<jsp:useBean id="customer" type="bean.Customer" scope="request,session, or application" />
<jsp:getProperty name="customer" property="fistName" />
但是对于类似${customer.address.zipCode},使用和则不能完成相同的获取属性的工作
访问集合
${attributeName[entryName]}
对于JSP2.0,Servlet2.4以上(包含)的版本,还规定了以下的隐式对象:
pageContext:一次拥有request、response、session、out和servletContext属性。获取会话ID可以用${pageContext.session.id}
param和paramValues:允许访问基本的参数值和请求参数值数组
header和headerValues:访问HTTP请求报头的主要值和全部值
cookie:
initParam:
pageScope,requestScope,sessionScope和applicationScope:
算数运算符:+,-,*,/(div)
关系运算符:==和eq(比较两个参数是否相等)、!=和ne(比较两个参数是否不等)、<和lt、>和gt、<=和le、>=和ge
逻辑运算符:&&、and、or、||、!、not
空运算符:empty
16、JDBC
使用JDBC连接数据库:
1) 载入JDBC连接数据库的驱动文件
Class.forName(“com.mysql.jdbc.Driver”);
2) 定义要打开的连接
String url = “jdbc:mysql://localhost:3306/test”;
3) 建立连接
String username = “root”;
String password = 1234;
Connection connection = DriverManager.getConnection(url,username,password);
4) 创建Statement对象
Statement statement = connection.createStatement();
5) 执行查询
String sql = “select * from user where user_id = 3”;
ResultSet rs = statement.executeQuery(sql);
6) 结果处理
ResultSet的第一行的索引是1
while(rs.next()){
rs.getString(COLUMN_NAME);
}
7) 关闭连接
connection.close();
调用数据库的存储过程:
1) 定义对数据库存储过程的调用
? = call procedure_name(?,?..?);
2) 准备CallableStatement
String procedure = {? = call procedure_name(?..?)}
CallableStatement statement = connection.prepareCall(procedure);
3) 注册输出参数的类型
statement.registerOutParameters(n,type);
n是输出参数的索引(第一个的索引是1),type是java.sql.Types定义的常量
4) 提供输入值
statement.setInt(2,12);
statement.setString(3,”name”);
将第一个输入参数设为12,将第二个输入参数设为name,输入参数的索引从第一个输出参数开始计起
5) 执行存储过程
statement.execute();
6) 访问返回的输出参数
int res = statement.getInt(1);
这里返回的是第一个输出参数,如果是第二个参数则将参数1变为2,其他以此类推
事务管理模板:
Connection connection = DriverManager.getConnection(url.username,password);
boolean autoCommit = connection.getAutoCommit();
try{
connection.setAutoCommit(fasle);
Statement st = connection.createStatement();
//执行事务单元
...
st.close();
}catch(SQLException e){
connection.rollBack();
throw new Throwable(e.getCause());
}finally{
connection.commit();
connection.setAutoCommit(autoCommit);
}
mysql用户授权
grant all privileges on database_name.* to USER_NAME@"%" identified by 'PASSWORD'