红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Java编程笔记
  4. 正文

Java编程笔记28:Servlet II

2023年3月29日 1051点热度 0人点赞 0条评论

image-20221101145143893

图源:Fotor懒设计

在Java编程笔记27:Servlet - 红茶的个人站点 (icexmoon.cn)中介绍了如何编写和运行一个基于Servlet编写的Web应用,本篇文章将学习更多Servlet应用中如何使用常见的Web技术。

重定向和转发

页面重定向应该不会陌生,我们只需要在返回的HTTP响应报文中,将响应状态码设置为30X,并且通过报文头标签Location指定重定向后的目标URL即可。

类似之前的实现,先设置一个欢迎页面:

package cn.icexmoon.java.note.ch28;
// ...
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        if (name == null) {
            name = "none";
        }
        resp.setContentType("text/html");
        PrintWriter writer = resp.getWriter();
        writer.print("<h1>This is a first servlet app example.</h1>");
        String msg = String.format("<div>hello %s</div>", name);
        writer.print(msg);
        writer.flush();
    }
}

设置一个页面,访问该页面的请求将重定向到欢迎页面:

package cn.icexmoon.java.note.ch28;
// ...
@WebServlet(urlPatterns = "/redirect")
public class RedirectServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        //302 临时重定向
//        resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
        //301 永久重定向
        resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
        String url = "/hello/";
        if (name != null) {
            url += String.format("?name=%s", name);
        }
        resp.setHeader("Location", url);
    }
}

现在如果请求/hello/redirect,浏览器将会重定向到/hello/这个页面。

通常使用的重定向状态码有301和302,分别对应常量SC_MOVED_PERMANENTLY和SC_MOVED_TEMPORARILY,它们的区别是301是永久重定向,浏览器通常会缓存这个重定向目标页面,下次请求会利用本地缓存直接访问目标页面,302是临时重定向,浏览器不会缓存这种重定向关系。

需要注意的是,重定向实际上是服务端让客户端重新请求另一个URL,所以如果这种方式实现的页面跳转,还想保留某些请求信息,比如请求原始页面时附加的查询参数(这个例子里就是?name=xxx),就需要在重定向时服务端附加上相应的查询参数,比如示例中的url += String.format("?name=%s", name)。

除了利用HTTP的重定向实现HTTP请求“跳转”以外,还可以直接在Servlet应用内部将一个请求转交给另一个Servlet类来处理:

package cn.icexmoon.java.note.ch28;
// ...
@WebServlet(urlPatterns = "/transmit")
public class TransmitServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher("/").forward(req, resp);
    }
}

这种方式实现的结果是客户端不会再次请求其它URL,而是在第一次请求后就获取到转发后的目标结果(页面)。

image-20230329143552700

cookie和session

cookie和session是进行Web编程时常用的状态保持技术,下面看如何在Servlet应用中使用。

假设这里我们需要模拟用户登录和注销的操作,简单起见,这里用一个Map来保存用户名和密码,一个简单的登录页提供给用户进行登录操作,如果成功,则跳转到一个用户的HOME页,并显示一些简单的欢迎信息,并且提供一个超链接进行退出操作。

用于登录相关的Servlet:

package cn.icexmoon.java.note.ch28;
// ...
@WebServlet(urlPatterns = "/sign")
public class SignServlet extends HttpServlet {
    private static Map<String, String> users = Collections.synchronizedMap(new HashMap<>());
​
    static {
        users.put("icexmoon", "123");
    }
​
    /**
     * 返回登录页面
     *
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String user = (String) req.getSession().getAttribute("user");
        if (user != null){
            resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
            resp.setHeader("Location", "/hello/home");
            return;
        }
        resp.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = resp.getWriter();
        String pageContent = this.getSignPage();
        writer.print(pageContent);
        writer.flush();
    }
​
    /**
     * 执行登录动作
     *
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        String password = req.getParameter("password");
        if (!checkLogin(name, password)){
            resp.setContentType("text/html; charset=UTF-8");
            //登录失败
            PrintWriter writer = resp.getWriter();
            writer.print("<h1>用户名或密码错误</h1>");
            writer.flush();
        }
        else {
            //登录成功
            //写入session
            req.getSession().setAttribute("user", name);
            //跳转到home页
            resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
            resp.setHeader("Location", "/hello/home");
        }
    }
​
    /**
     * 检查用户密码是否正确
     * @param name
     * @param password
     * @return
     */
    private static boolean checkLogin(String name, String password){
        if (name == null || name.isEmpty()){
            return false;
        }
        if (password == null || password.isEmpty()){
            return false;
        }
        if (!users.containsKey(name)){
            return false;
        }
        String correctPass = users.get(name);
        if (correctPass == null){
            return false;
        }
        if (!correctPass.equals(password)){
            return false;
        }
        return true;
    }
​
    private static String getSignPage() {
        StringBuilder sb = new StringBuilder();
        sb.append("<form action=\"/hello/sign\" method=\"post\">");
        sb.append("用户名<input name=\"name\"/><br/>");
        sb.append("密码<input name=\"password\"/><br/>");
        sb.append("<button type=\"submit\">登录</button>");
        sb.append("</form>");
        return sb.toString();
    }
}

这里需要注意的是,保存用户信息的Map是一个线程安全的同步容器,因为Web应用一般都是多线程的,服务端同时可以为多个客户端提供服务,这里实际上是由Web Server(比如Tomcat)来进行多线程调度的,并且很可能会进行线程复用。

在客户端执行POST请求/hello/sign后,服务端会检查用户名和密码,如果正确,就可以认为用户已处于登录状态,这个状态我们通过向Session中写入用户名来实现。在Servlet中,可以用req.getSession().setAttribute("user", name)这样的方式向Session中写入信息。

在客户端执行GET请求/hello/sign后,服务端会从通过从Session中获取用户名的方式来判断当前用户是否已经登录,如果登录用户名是什么。具体通过req.getSession().getAttribute("user")来获取Session中的指定信息。

下面是一个用户主页:

package cn.icexmoon.java.note.ch28;
// ...
@WebServlet(urlPatterns = "/home")
public class HomeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = (String) req.getSession().getAttribute("user");
        if (name == null) {
            //没有登录
            resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
            resp.setHeader("Location", "/hello/sign");
            return;
        }
        //如果客户端没有记录天气,就写入cookie
        String clientWeather = getWeatherFromCookie(req);
        String nowWeather;
        if (clientWeather == null) {
            String weather = getWeather();
            writeWeather2Cookie(resp, weather);
            nowWeather = weather;
        } else {
            nowWeather = clientWeather;
        }
        //已经登录
        resp.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = resp.getWriter();
        //显示天气
        writer.print(String.format("<div>%s</div>", nowWeather));
        writer.print("<h1>This is your home page</h1>");
        writer.print(String.format("<div>hello, %s</div>", name));
        writer.print("<div><a href=\"/hello/exit\">注销</a></div>");
        writer.flush();
    }
​
    private static String getWeather() {
        String[] weathers = {"天气晴 15度~20度", "天气阴 10度~20度", "多云转阴 15度~22度"};
        Random random = new Random();
        int i = random.nextInt(weathers.length);
        return weathers[i];
    }
​
    private String getWeatherFromCookie(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if (cookies == null) {
            return null;
        }
        for (Cookie c : cookies) {
            if (c.getName().equals("weather")) {
                String weather = c.getValue();
                weather = new String(Base64.getDecoder().decode(weather), Charset.forName("UTF-8"));
                return weather;
            }
        }
        return null;
    }
​
    private void writeWeather2Cookie(HttpServletResponse resp, String weather) {
        String encodedWeather = Base64.getEncoder().encodeToString(weather.getBytes(StandardCharsets.UTF_8));
        Cookie cookie = new Cookie("weather", encodedWeather);
        //有效期为1天
        cookie.setMaxAge(24 * 60 * 60);
        resp.addCookie(cookie);
    }
}

在主页上,我添加了一个显示当前天气情况的功能(示例中天气获取用一个随机产生的方式替代)。因为天气情况实际上并不重要,我们甚至可以允许用户进行篡改,所以这里利用Cookie来保存用户当地的天气情况。

当服务端获取到用户当地天气后,就可以通过设置响应报文头Set-Cookie的方式将该信息保存到客户端cookie中。当然Servlet已经封装好了,这里只需要创建一个Cookie对象,并通过resp.addCookie方法添加即可。

这里需要注意两点:

  • 给客户端添加Cookie时通常需要指定有效期(时长,单位秒),这里指定的是1天。

  • 保存Cookie时只能是ASCII字符,如果是中文之类的,就会报错,这里需要通过Base64编码进行转码后保存,相应的,读取时也需要进行Base64解码。

其它部分代码相对简单,这里不详细介绍,具体可以从 java-notebook/ch28 (github.com) 获取完整示例代码。

好了,就到这里了,谢谢阅读。

参考资料

  • 使用Session和Cookie - 廖雪峰的官方网站 (liaoxuefeng.com)

  • Servlet 网页重定向 | 菜鸟教程 (runoob.com)

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: cookie servlet session 转发 重定向
最后更新:2023年3月29日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号