从头开始之JSP+Servlet

时间:2020-12-12 21:07:29

前言

本篇包括 Mac 下的 Eclipse 环境搭建,配置 Tomcat 服务器,一个简单的登录页面Demo,包括后台到数据库的一系列逻辑,以及使用到了这几天刚学的 AngularJS 做的页面。

因为 AngularJS4 版本太新了,中文官网没有足够的以 JavaScript 为开发语言的文档,所以先看了AngularJS 2.0之前的文档,先放下以后有时间再看。

从 AngularJS 2 开始官方就推荐使用 TypeScript 。TypeScript是微软开发的开源编程语言,它是JavaScript的一个严格超集,所以任何现有的JavaScript程序都是合法的TypeScript程序。

以下是我学习的资料:

  1. 官方文档
  2. 菜鸟 AngularJS
  3. 菜鸟 AngularJS2
  4. 慕课 AngularJS 实战

Eclipse 搭建 Tomcat 环境

一、下载Eclipse

选择 最新 的版本。注意,选择 Eclipse IDE for Java EE Developers 这个版本,选择好对应的操作系统和位数,然后点击 Download 即可。

二、下载Tomcat

这里,我选择的是 Tomcat 7.0,我们可以根据需要下载适合自己的版本

解压缩下载的tomcat文件之后,进入 tomcat 的 bin 目录,使用更改使用权限的命令

 $ sudo chomd 755 *.sh

sudo 命令可能会要求输入管理员密码。

小知识:bin 目录下存在用于启动和停止 tomcat 的许多脚本。Unix下所有需要直接调用的脚本均以 shell 脚本文件(.sh)形式提供,而 windows 系统则以批处理文件(.bat)形式提供

启动 tomcat

cd 到 tomcat 根目录下,使用如下命令即可启动和关闭

$ sudo sh startup.sh
$ sudo sh shutdown.sh

创建数据库

创建user表

从头开始之JSP+Servlet

创建web工程

打开 Eclipse,创建 Dynamic web project 项目。

创建 webContent/login.jsp 页面

创建 /src/me/nijun/action/login/LoginAction.java

第一步 创建login.jsp页面

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html ng-app="LoginModule">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
<link rel="stylesheet" href="css/bootstrap-3.0.0/css/bootstrap.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"
type="text/javascript">
</script>
<script type="text/javascript" src="js/angular-1.3.0.js"></script>
<script type="text/javascript" src="js/form.js"></script>
</head>
<body>
<div class="panel panel-primary">
<div class="panel-heading">
<div class="panel-title">登录</div>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<%--<form class="form-horizontal" role="form" ng-submit="submit()">--%>
<form class="form-horizontal" role="form" ng-controller="LoginForm">
<div class="form-group">
<label class="col-md-2 control-label">
邮箱:
</label>
<div class="col-md-10">
<input type="email" id="email" name="email" class="form-control"
placeholder="推荐使用126邮箱" ng-model="email">

</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">
密码:
</label>
<div class="col-md-10">
<input type="password" id="password" name="password"
class="form-control" placeholder="只能是数字、字母、下划线"
ng-model="password">

</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="autoLogin">自动登录
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button class="btn btn-default" ng-click="submit()">提交</button>
<button class="btn btn-default" ng-click="resetInfo()">重置</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>

引入了 bootstrap 作为样式来源,并且引入 AngularJS 1 版本。

  1. ng-app=" " 定义 angularJS 的使用范围;
  2. ng-init="变量=值;变量='值'" 初始化变量的值,有多个变量时,中间用分号隔开;
  3. ng-model="变量" 定义变量名;
  4. ng-bind="变量" 绑定变量名,获取该变量的数据。这里的变量就是第3条的变量名。但是一般都用双重花括号来获取变量的值,比如:{{变量}}。
  5. ng-model是用于表单元素的,支持双向绑定。对普通元素无效;
  6. ng-bind用于普通元素,不能用于表单元素,应用程序单向地渲染数据到元素;
  7. ng-bind{ { } }同时使用时,ng-bind 绑定的值覆盖该元素的内容。

具体请参见官方文档

第二步 提交表单信息到servlet

创建模块

var loginModule = angular.module('LoginModule', []);

添加控制器

loginModule.controller('LoginForm', ["$scope","$http",func]);

func函数

function func($scope,$http) {
//初始化表单信息
$scope.email = "nijun717@gmail.com",
$scope.password = "123456",
$scope.autoLogin = false
//重置表单函数
$scope.resetInfo = function(){
$scope.email = "",
$scope.password = "",
$scope.autoLogin = false
},
//提交表单函数
$scope.submit = function(){
var postData = {
email:$scope.email,
password:$scope.password,
autoLogin:$scope.autoLogin
};

// $http请求servlet...
}
}

其中 $scope.submit 函数在 jsp 中的提交按钮 ng-click="submit()" 中被调用。最为关键的使用 $http 请求 servlet 代码如下:

//方式一
$http({
method : 'POST',
url : 'loginAction.do',
data : $.param(postData), // pass in data as strings
headers : { 'Content-Type': 'application/x-www-form-urlencoded' } // set the headers so angular passing info as form data (not request payload)
}).then(function(response){
console.log(response.config);//请求信息
console.log(response.config.data);//请求字段
console.log(response.data.code);
if (response.data.code == 0){
window.location.href = "/success.jsp";
}
},function(response){

});

//方式二
$http({
method : 'POST',
url : 'loginAction.do',
data : $.param(postData), // pass in data as strings
headers : { 'Content-Type': 'application/x-www-form-urlencoded' }
}).success(function(data){
console.log(data);
if (data.code == 0){
window.location.href="/success.jsp";
}
});

注意,这里有一个坑,当我们在页面中使用 ajax 来异步调用 controller (这里就是servlet)时,这时候页面跳转的函数就不会生效了。包括 forward() 和 sendRedirect() 两个方法。而且我们想要跳转的页面会被当做返回值来返回。这个问题怎么解决呢?

其实 ajax 在调用 controller 之后会自动返回到上面代码方式二的 success 函数位置,因此,若我们直接在 controller 中进行页面跳转,则目标页面的源代码会被返回到这个 success 函数里,正确的页面跳转方式应该是在 success 函数中完成,如上面代码中的:

window.location.href="anotherAction.do";  
//这行代码中的href值是另一个controller的名字,通过另一个controller跳转到另一个页面。当然也可以直接使用 html 或者 jsp 页面

为此,我总结一下:$http 是 ajax 的封装,是用来与服务器交互获取 json 数据的,其实就是异步调用,因此在服务器不能直接进行页面的跳转与转发,服务器处理表单数据后将所需的 json 信息返回给客户端中的 success 函数,我们在这个函数里再处理是否要进行页面跳转。

第三步 创建 Controller

创建 LoginAction.java 继承 HttpServlet,然后在 web.xml 中配置servlet和servlet映射的URL路径。web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">


<display-name>SimpleWebDemo</display-name>
<welcome-file-list>
<welcome-file>login.jsp</welcome-file>
</welcome-file-list>

<servlet>
<servlet-name>LoginAction</servlet-name>
<servlet-class>me.nijun.action.login.LoginAction</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>LoginAction</servlet-name>
<url-pattern>/loginAction.do</url-pattern>
</servlet-mapping>
</web-app>

覆盖HttpServlet中的doPost()和doGet()方法,一般在doPost()中调用doGet()方法。

@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("servlet doPost.");
doGet(request,response);
String email = (String) request.getParameter("email");
String password = (String) request.getParameter("password");

PrintWriter writer = response.getWriter();
JSONObject jsonObject = new JSONObject();

if (EMAIL.equals(email) && PASSWORD.equals(password)) {
jsonObject.put("code", 0);
} else {
jsonObject.put("code", 1);
}

writer.print(jsonObject);
writer.flush();
writer.close();
}

第四步 访问数据库

使用传统的JDBC方式来访问数据库,首先导入 mysql-connector-java-5.0.8-bin.jar 连接mysql的核心jar包。

编写实体类

package me.nijun.domain;

/**
* Created by nimon on 2017/7/13.
*/

public class User {

private int id;
private String email;
private String password;

public User(){}

public User(String email, String password) {
this.email = email;
this.password = password;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

连接的获取和释放

采用单例模式,driver、url的格式可以参考 xxxx

/**
* Created by nimon on 2017/7/13.
* 实现了单例模式的JDBC连接数据库的工具类.
*/

public final class JDBCUtilSingleton {

static String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/smartni";
String username = "root";
String password = "123";
Connection connection = null;

private static JDBCUtilSingleton instance = null;

//私有构造
public JDBCUtilSingleton() {

}

//并发 加锁
public static JDBCUtilSingleton getInstance() {
if (instance == null) {
synchronized (JDBCUtilSingleton.class) {
if (instance == null) {
instance = new JDBCUtilSingleton();
}
}
}
return instance;
}

//加载JVM时只创建一次连接
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//获取连接
public Connection getConnection() throws SQLException {
return (Connection) DriverManager.getConnection(url, username, password);
}

//规范释放资源方法
public void Free(ResultSet rs, Statement stmt, Connection conn){
try{
if(rs != null){
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
try{
if(stmt != null){
stmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
try{
if(conn != null){
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}

定义dao接口

UserDao.java

public interface UserDao {

/**
* 添加一个用户
* @param user 需要添加到数据库的用户对象
* @return 影响的行数
* @throws DaoException
*/

public int addUser(User user) throws DaoException;

/**
* 得到一个用户
* @param username 得到用户所需要的用户名字段
* @return 得到的用户对象
* @throws DaoException
*/

public User findUser(String username) throws DaoException;

}

实现dao接口的实现类

UserDaoImpl.java

public class UserDaoImpl implements UserDao {

JDBCUtilSingleton instance = JDBCUtilSingleton.getInstance();

@Override
public int addUser(User user) throws DaoException {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet set = null;
int res = 0;
try {
conn = instance.getConnection();
//防止sql注入
String sql = "insert into user values(null,? ,? ,?)";
stmt = conn.prepareStatement(sql);
//设置不自动提交
conn.setAutoCommit(false);
stmt.setString(1, user.getUsername());
stmt.setString(2, user.getEmail());
stmt.setString(3, user.getPassword());
res = stmt.executeUpdate();

System.out.println("插入记录数: " + res + "条.");
conn.setAutoCommit(true);
return res;
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
instance.Free(set,stmt,conn);
return res;
}
}

@Override
public User findUser(String username) throws DaoException {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet set = null;
User user = new User();
try {
conn = instance.getConnection();

String sql = "select id,username,email,password from user where username = ?";
stmt = conn.prepareStatement(sql);
stmt.setString(1, username);
set = stmt.executeQuery();
//将指针移动到第一条记录之后
if (set.next()) {
user.setId(set.getInt(1));
user.setUsername(set.getString(2));
user.setEmail(set.getString(3));
user.setPassword(set.getString(4));
}
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
instance.Free(set,stmt,conn);
}
return user;
}

}

PreparedStatement.executeQuery();

执行完成之后获得的 ResultSet 对象, ResultSet 是一个接口,¢在获取内容时需要先调用 next() 方法,将读取的指针移动到需要读取的记录的后面,这样才可以获取到数据,这有点类似于LinkedList中的Iterator迭代器。最近也在重新复习这块,以后整理一篇Java集合框架的笔记。

测试 Dao

UserDaoImplTest.java

public class UserDaoImplTest {

UserDao ud = new UserDaoImpl();
@Test
public void addUser() throws Exception {
User user = new User();
user.setUsername("SmartNi");
user.setEmail("nijun717@gmail.com");
user.setPassword("123456");

ud.addUser(user);
}

@Test
public void findUser() throws Exception {
User user = ud.findUser("SmartNi");
System.out.println(user);
}

@Test
public void testSplit() {
String text = "f:/jsp:sad";
String[] split = text.split(":");
for (String s : split){
System.out.println(s);
}
}
}

自定义异常,继承 RuntimeException ,具体操作由父类实现。

public class DaoException extends Exception {

public DaoException() {
}

public DaoException(String message) {
super(message);
}

public DaoException(String message, Throwable cause) {
super(message, cause);
}

public DaoException(Throwable cause){
super(cause);
}
}

定义Service接口

UserService.java

public interface UserService {
/**
* 获取用户名对应的用户对象
* @param username 得到用户所需要的用户名字段
* @return 得到的用户对象
* @throws DaoException
*/

public User login(String username) throws ServiceException;


/**
* 添加用户的服务
* @param user 需要添加到数据库的用户对象
* @return 影响的行数
* @throws ServiceException
*/

public int add(User user) throws ServiceException;
}

实现 Service 接口

UserServiceImpl.java

public class UserServiceImpl implements UserService {

UserDao ud = new UserDaoImpl();

@Override
public User login(String username) throws ServiceException {
try {
return ud.findUser(username);
} catch (DaoException e) {
e.printStackTrace();
throw new ServiceException(e.getMessage(), e);
}
}

@Override
public int add(User user) throws ServiceException {
try {
return ud.addUser(user);
} catch (DaoException e) {
e.printStackTrace();
throw new ServiceException(e.getMessage(), e);
}
}
}

有个想法

因为,这个例子中我用了 AngularJS ,使用到了 $http 异步来发送表单提交请求,页面的跳转必须还得由客户端根据服务端传来的信息做判断再进场跳转。

还有一点,我这里将登录的逻辑放在了doPost()中,但是 UserAction 类将来必定不只是处理登录操作,还有登出、注册等等操作,不可能将所有操作都放在 doPost() 方法中,所以我的解决办法是:

  1. 每个请求操作对应一个方法。
  2. 根据客户端传递过来的 method 字段判断调用哪个方法。

我们来看看改进后的 doPost()

    @Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("servlet doPost.");
doGet(request,response);

String method = request.getParameter("method");
//判断请求的是什么方法
if("login".equals(method)){
login(request,response);
}
//...未来有更多的方法
}

但是,有没有发现,我们每新增一个方法,就需要手动在这里多写一个判断语句。运用反射机制就可以解决这个问题。

BaseAction.java

/**
* Created by nimon on 2017/7/14.
* 功能: 执行指定方法, 方便跳转页面
*/

public class BaseAction extends HttpServlet {

/**
*
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
/*
得到方法名
*/

String methodName = req.getParameter("method");

System.out.println("BaAction : 方法名 : " + methodName);
if (methodName == null || methodName.trim().isEmpty()) {
throw new RuntimeException("您没有传递需要调用的method方法参数,没有调用方法.");
}
/*
通过反射得到方法
*/

Class c = this.getClass();
Method method = null;
try {
method = c.getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
throw new RuntimeException("没有找到你需要调用的方法.");
}
/*
调用method表示的方法
*/

try {
String result = (String) method.invoke(this, req, resp);

/*
如果用户返回 null 或者返回空字符串,就什么也不做.
*/

if (result == null || result.trim().isEmpty()) {
return;
}

/*
1. 约定返回的字符串中没有包含 ":" ,就说明是转发操作.
2. 如果包含 ":" ,分为下面两种情况:
1). 前缀 f 表示转发.
2). 前缀 r 表示重定向.
*/

if (result.contains(":")) {
String[] split = result.split(":");
if (split[0].equalsIgnoreCase("r")) {
req.getRequestDispatcher(split[1]).forward(req, resp);
} else if (split[0].equalsIgnoreCase("f")) {
resp.sendRedirect(split[1]);
} else {
throw new RuntimeException("您请求的地址,当前还不支持.");
}
}else{
req.getRequestDispatcher(result).forward(req, resp);
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("您调用的方法--" + methodName + "--它内部抛出了异常.");
throw new RuntimeException();
}
}
}

只要让其他 Servlet 继承这个 BaseAction,然后在自定义方法中返回约定的字符串,即可实现页面跳转。其实这个想法是根据 Strust2 的设计思路来实现的。