Java Web 开发--- tomcat/servlet/maven

  Java web开发,就是用Java程序来处理浏览器的请求,比如显示哪个页面,返回哪些数据等,而不是用Java程序来写web服务器,已经有现成的服务器可用了,比如Tomcat,只需要把写完程序打包部署到服务器上,服务器接收到浏览器的请求,就调用部署的程序。但这就有一个问题,服务器是怎么知道调用哪一个java程序?

  当把项目部署到Tomcat并启动后,它里面的Servlet容器就会为项目创建一个Context,然后寻找WEB-INF下的web.xml进行解析,web.xml文件中的各个配置项都会被解析成相应的属性保存在WebXml对象,解析完xml后,还会扫描jar包(Meta-INF/web-fragment.xml)和类上的注解了,也放到Xml对象上(这里不一定对,只是好理解)。这项工作完成后,就把WebXml对象中的属性设置到Context容器,比如定义的Servlet,filter,和listener,然后实例化Servlet对象,调用它的init()并初始化,项目中的所有servlet都在context容器中,所以servlet中都能获取到Context和ContextConfig 对象,filter和listener 也是一样。

  当Tomcat接收到客户端请求后,传递给Servlet容器,Servlet容器根据请求的host,找到Context(部署的项目),再根据路径到Context容器找对应的Servlet实例,向其传递表示请求和响应的对象,调用它的service方法,获取响应的对象的内容,将其变成响应文本的格式发送给返回给客户端。Tomcat接收http请求文本并解析,然后封装成HttpServletRequest类型的request请求对象。为什么要传递响应对象,底层的socket.io, 当浏览器和服务器相联时,它底层是一个socket的连接,因为可以同时有多个浏览器和服务器进行相联,每一个都是一个socket连接,所以对每一个浏览器的响应,要从socket 获到到响应对象,响应对象代表着对应的浏览器,Tomcat同时会要响应的信息封装为HttpServletResponse类型的response对象。

  所以写Java web程序,就两件事,一个配置web.xml或写注解,做一个URL到实现类的映射,让服务器根据接收到的的http路径,比如/, /test, 找到实现类。二是写一个类实现Servlet 接口(init(), service()),处理请求,做出响应。实现类通常不会直接实现servlet接口,而是继承HttpServlet。因为HttpServlet实现了Servlet接口,并提供了init 方法,service方法的默认实现,service默认实现是读取http request中的方法,如果是get请求,就调用类中的doGet()方法,如果是post请求,就调用类中的doPost()方法,所以实现类继承HttpServlet,只要覆写doGet()或doPost()方法就可以了。两个方法都传入了HttpServletRequest和HttpServletResponse两个对象。

  Tomcat的官网 http://tomcat.apache.org/ 下载服务器,它是zip压缩包,解压就可以了。

  打开tomcat文件夹,有bin目录和webapps目录等,bin目录下有startup.bat,双击,出现一个黑色的tomcat命令窗口,底部光标闪烁,服务器启动成功。浏览器中输入localhost:8080, 出现了页面。页面怎么来的?这就是webapps目录,它包含了部署到tomcat中的所有项目。当Tomcat启动时,它默认启动ROOT项目,如果想访问其它项目,可以在url后面加项目名,比如localhost:8080/examples/,看到example项目中的内容。这就是Tomcat为每一个项目创建一个context,也意味着,我们的项目放到webapps下,也可以被访问到。打开examples目录,有WEB-INF,jsp等。jsp用于展示页面,WEB-INF下面有类(classes目录),引用包(lib目录)和web.xml,这就是部署到Tomcat服务器上的每个项目的目录结构。开发一个web项目,如果手动创建这些目录结构有点麻烦,Eclipse或Spring Tool Suite有Dynamic web项目。 在Spring Tool Suite 中, File--->New--->Other--->输入Web--->选择Dynamic Web Project,就会弹窗

   project name就是项目名,比如输入first-java-web,点击next,再点击next,

   勾选生成web.xml,点Finish,这时Spring Tool Suite 左侧面板中多了新建的项目。

    webapp文件夹,它下面有一个WEB-INF,它下面有lib,想到Tomcat中examples中的目录,可以发现,webapp就是整个web项目的目录结构,但它少了classes目录(java 代码),多了 java目录,且和webapp 是同一级,我们写的java代码,并没有webapp中。这是因为java是编译型语言,不管java代码写到什么地方,最后只要编译到webapp 下面classes就可以了,程序运行,也是运行java编译后的代码。Java是编译型语言,且目录结构的不同,和前端开发有很大的不同,这点一定要注意,java目录结构,注意的是Java代码最终编译到的目录结构,而不是写源代码的目录结构。在项目名称first-java-web上右击,选properties 或点击选中项目名称,按Alt+Enter,弹窗中,选Java Build Path

   将Default output folder设置为first-java-web/src/main/webapp/WEB-INF/classes,点击Apply and Close 完成配置。在写项目之前,要把项目编码设置成UTF-8. 项目名右击,property -> resource 

   如果在Intellij IDEA中创建的项目,则File -> Settings,搜索 File Encodings ,

  在STS写一个简单的请求,访问页面时,显示Hello World。新建一个Servlet实现HttpServlet,Java Resources目录下,src/main/java 右击 ,new -> servlet,如果没有servlet,就选other,搜索一下,

   输入package名和类名,比如包名是com.sam, 类名是Home, 点Finish生成一个类,但类中有报错,

  因为Servlet 依赖javax.servlet包,而我们并没有把包导进来。 还是在项目名上,右击 ->Build Path -> configure build path

  点击Add External Jars,  在弹出的对话框中找到tomcat的解压路径,在lib文件夹下选中"servlet-api.jar" ,

  点击打开,可以看到servelet-api.jar 已经导入到项目中,再点击Apply and Close, 代码没有报错了。看一下Servlet,它已经添加了@WebServlet("/Home"),也就是说,可以不用配置web.xml了。现在返回hello world。

public class Home extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // 设置响应类型:
        resp.setContentType("text/html");
        // 获取输出流:
        PrintWriter pw = resp.getWriter();
        // 写入响应:
        pw.write("<h1>Hello, world!</h1>");
        // 最后不要忘记flush强制输出:
        pw.flush();
    }
}

  发送响应,就是设置正确的响应类型,然后获取PrintWriter,调用write方法,向响应对象写入要返回的内容。代码写完,要编译。STS可以自动编译,project->Build Automatically ,点击进行勾选

  现在把webapp的目录拷贝到tomcat目录下的webapps目录下,然后startup.bat启动tomcat,http://localhost:8080/webapp/Home,页面上显示Hello World,第一个java web 程序成功了。如果每次修改,都要部署到tomcat文件夹中有点麻烦,可以直接把Tomcat 配置到STS中,右击项目名,选Run as-> Run on server

   在Select the Server type 下面,先点击Apache,再选Tomcat版本,本地安装的版本,就选什么版本,点Next

    Tomcat installation directory 就是本地安装的路径,点击后面的Browse... 添加就可以。再点next ,直到finish,可以看到服务启动了,多了一个server

  右上角的绿色三角,启动服务器,红色方框是停止服务器,这样的话,修改代码,启动和停止服务器,就可以在STS中完成了,非常方便。重新看回Servlet。

  Servlet

  @WebServlet 标注在继承了 HttpServlet 的类之上,标注请求路径。如果Servlet只处理一个url请求,就把这个url添加到注解后面,用括号括起来。

@WebServlet ("/test”)
public class TestServlet extends HttpServlet {}

  如果要处理多个url请求,就要使用@WebServlet的urlPatterns 属性 

@WebServlet(urlPatterns= { "/emailList ", "/email/*"})
public class TestServlet extends HttpServlet {}

  其实,url和servlet类之间的映射,是通servlet名字实现的。Tomcat会为每一个url 映射到一个servlet名字上,然后再把这个名字对应到servlet类上。写过xml配置的都记得。

<servlet>
    <servlet-name>testServlet</servlet-name>
    <servlet-class>com.sam.TestServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>testServlet</servlet-name>
    <url-pattern>/test</url-pattern>
</servlet-mapping>

  所以在注解的情况下,也会@WebServlet 注解的servlet类生成一个名字,默认的名字就是servlet name。但由于类是分布在不同的包中,可能每个包中都有一个testSevlet类,这样servlet name就有了冲突。解决冲突要用到@WebServlet的name属性,为每一个Servlet定义一个唯一的名字。

@WebServlet (name= "MurachTestServlet", urlPatterns= {"/test"})
public class TestServlet extends HttpServlet {}

  Servlet 获取请求参数,无论是get还是post请求,request参数都有两个方法,getParameter(string param) return string or null. getParameterValues(string param) return string array or null ( 用于获取多个复选框的值)。 当时Servlet主要用于form表单操作,它获取的是表单的name的属性名, 复选框的值都是一个name属性,所以用getParameterValues。在src/main/webapp 下,新建index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Insert title here</title>
</head>
<body>
<form method="post" action="/first-java-web/info">
    <p>姓名:<input type="text" name="name"></p>
    <p>
        爱好:<label><input type="checkox" name="like" value="football">足球</label>
        <label><input type="checkox" name="like" value="basketball">篮球</label>
        <label><input type="checkox" name="like" value="music·">排球</label>
    </p>
    <p><input type="submit" value="提交"></p></form>
</body>
</html>

  然后在src/main/java下面,com.sam 包下新建Info类,实现httpServlet

@WebServlet("/info")
public class Info extends HttpServlet {
    private static final long serialVersionUID = 1L;
    
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        String [] likes = request.getParameterValues("like");
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        var pw = response.getWriter();
       
        var name = request.getParameter("name");
        pw.write("<h1>姓名:" + name +  "</h1>");
        for (String like : likes)
        {
            System.out.println("like   " +like.toString());
             pw.write("<h1>爱好:" + like +  "</h1>");
        }
       
        pw.flush();
    }
}

  http://localhost:8080/first-java-web/, 添加用户名,点击提交,页面返回用户输入的内容。注意request.setCharactreEncoding("UTF-8"),如果没有设置,post提交的数据会按系统默认的编码进行解析,可能出现乱码。Get请求中,如果URL的query参数中有中文,可能出现乱码。前端最好对参数encodeURIcomonet 一下,后端用Java.net.URLDecoder指定utf-8 解码。其实最方便的是设置tomcat,在程序中就不用管编解码问题了。在tomcat 安装目录下,config/serve.xml 中配置UTF-8编码,支持中文。

<Connector port="8080" protocol="HTTP/1.1"
        connectionTimeout="20000"
        redirectPort="8443" URIEncoding="UTF-8" />

  如果想在项目启动时,做一些初始化(容器初始化参数),可以在web.xml中 配置context-param,所有的servlet都能获取到这些值。Tomcat启动时,会读取web.xml创建一个Context对象, context-param可以看做是给context对象添加属性值。ServletContext代表整个应用,一个应用程序中只有一个ServletContext对象。

<context-param>
    <param-name> custServEmail</param-name>
    <param-value>custserv@murach.com</param-value>
</context-param>

  如果想对哪个servlet做一些初始化,可以使用@WebServlet的initParams

@WebServlet(urlPatterns= {"/info"}, initParams= {@WebInitParam(name="relative Path To File", value= "/EmailList.Txt")})

  在Servlet中,getServletContext().getInitParameter()可以获取到容器初始化的参数,getServletConfig().getInitParameter() 可以获取到Servlet本身的初始化参数,一个是getServletContext, 一个是getServletConfig。

// 在doPost()中
var initCp = getServletContext().getInitParameter("custServEmail");
var initSp = getServletConfig().getInitParameter("relative Path To File");
System.out.println(initCp + "   " + initSp);

  serveletContext 对象有setAttribute, getAttribute, 和removeAttribute 三个方法,由于每一个servlet 下都可以调用这个对象,设置属性, 在其他的servlet中 可以获取属性,实现多个servlet 共享数据

  Servlet工作原理:Servlet 容器只会为每一个servlet类创建一个实例,此时会调用init()方法, init()方法只会调用一次。当请求进来时,服务器会创建一个线程,获取到Servlet实例,调其Service方法。如果一个实例长期不用,就会销毁,调用destroy()方法,destroy方法,只会执行一次。

   JDBC连接数据库

  JDBC是Java程序访问数据库的标准接口,是Java 连接数据库的规范,定义在了Java的sql 包中。各大数据库厂商都提供各自数据库的实现,称为JDBC驱动,就是Jar包,具体的数据库操作则是由数据库驱动完成。所以Java程序只要引入对应数据库驱动的jar包,引用Java标准库提供的java.sql包下面的相关接口,就可以正常访问数据库。所以JDBC访问数据库分为以下几个步骤

  1, 引入Java标准库提供的java.sql包

  2,下载数据库驱动,并注册到程序中,比如,到MySql官网下载mysql 8 的驱动connect J,然后在程序中Class.forName("com.mysql.cj.jdbc.Driver"); 注册数据库

  3,获取数据库连接,DriverManager.getConnection(数据库地址,数据库用户名,登录密码)

  4,创建各种Statement, 有三种statement,Statement(可以执行带不参数的SQL语句), PreparedStatement(执行带参数的SQL语句),CallableStatement(执行数据库存储过程)

  5,执行query,executeUpdate 执行增删改,executeQuery 执行查询,它返回查询结果ResultSet,可以看作一个表, ResultSet是一个游标,指向表中某一行,一行一行移动

  6,清理工作,关闭打开的各种资源。

  假设Mysql中有一个test数据库,里面有一张user表(name和likes属性),likes 简单用字符串表示了

CREATE TABLE `test`.`users` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(45) NOT NULL,
  `likes` VARCHAR(100) NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `name_UNIQUE` (`name` ASC) VISIBLE);

  在src/main/java下,com.sam包中新建一个JDBCUtils类

package com.sam;
import java.sql.*; // 引入Java标准库提供的java.sql包

public class JDBCUtils {
    // 数据库url、用户名和密码
    static final String DB_URL = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false";
    static final String USER = "root";
    static final String PASS = "123";

    public static void insertInfo(String name, String likes) {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver"); // 注册JDBC驱动
            Connection connection = DriverManager.getConnection(DB_URL, USER, PASS); // 获取数据库连接

            // 操作数据库
            String sql = "insert into users(name, likes) values(?,?)";
            PreparedStatement statement = connection.prepareStatement(sql);// 获取操作数据库的对象
            statement.setString(1, name);
            statement.setString(2, likes);
            statement.executeUpdate(); // 执行sql

            // 4、关闭结果集、数据库操作对象、数据库连接
            statement.close();
            connection.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void getInfo() {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver"); // 注册JDBC驱动
            Connection connection = DriverManager.getConnection(DB_URL, USER, PASS); // 获取数据库连接

            // 操作数据库
            String sql = "select name, likes from users";
            Statement statement = connection.createStatement();// 获取操作数据库的对象
            ResultSet resultSet = statement.executeQuery(sql);// 执行sql,获取结果集

            while (resultSet.next()) { // 遍历结果集,取出数据
                String name = resultSet.getString("name");
                String likes = resultSet.getString("likes");

                System.out.print("info :" + name + "    " + likes);
                System.out.println();

            }

            // 4、关闭结果集、数据库操作对象、数据库连接
            statement.close();
            connection.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  除了createStatement外,还可以开启事务  connection.setAutoCommit(false); 随之而来就是connect.comiit() 和rollback。这样配置数据库有一个问题,就是如果配置信息发生变化,就要重新编译代码。如把配置信息提取出来,放到一个文件中,不需要编译代码,直接重启服务器就可以。Java有专门的.properties 文件来做这事的,为此还有一个java.utils.Properties类来解析 .properties文件。在src/main/java下,新建db.properties。

url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
user=root
password=123
driver=com.mysql.cj.jdbc.Driver

  把获取数据库连接的Connection connection = DriverManager.getConnection(DB_URL, USER, PASS); 换成以下代码

Properties props = new Properties();
InputStream fis = JDBCUtils.class.getClassLoader().getResourceAsStream("db.properties");

props.load(fis); // 加载的是intputstream,

String db_url = props.getProperty("url"); // 加载完成后,读取属性
String user = props.getProperty("user");
String password = props.getProperty("password");
Connection connection = DriverManager.getConnection(db_url, user, password);

  数据库的连接也存在问题,无论是插入数据,还是查询数据,都是先连接数据库,再断开连接,数据库的连接是很耗时的,所以有数据库连接池的概念。程序启动的时候,创建一些数据库的连接,放到一个对象中(database connect pool) DBCP中,程序直接从对象中获取连接,操作数据库,操作完数据库,再还回去,释放连接,而不是关闭连接,数据库一直在链接着,不会断开。

 

  有现成的连接池产品可用,比如druid,只需要在项目中配置一下。下载druid的jar包,放到lib下,然后在src/main.java下面,建druid.properties

url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=FALSE
username=root
password=123
driverClassName=com.mysql.cj.jdbc.Driver
initialSize=5
maxActive=10
maxWait=3000

  创建一个连接池对象,或连接池的Utils类,在com.sam包下,

package com.sam;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import com.alibaba.druid.pool.DruidDataSourceFactory;

public class ConnectionPool {
    private static ConnectionPool pool = null; // 单例模式
    private static DataSource dataSource = null; // 真正的datasource

    private ConnectionPool() {
        try {
             Properties pro = new Properties();
             //使用ClassLoader加载配置文件,获取字节输入流
             InputStream fis = ConnectionPool.class.getClassLoader().getResourceAsStream("druid.properties");
             pro.load(fis);
             //初始化连接池对象,创建数据库连接池
             dataSource = DruidDataSourceFactory.createDataSource(pro);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static synchronized ConnectionPool getInstance() {
        if (pool == null) {
            pool = new ConnectionPool();
        }
        return pool;
    }

    public Connection getConnection() {
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
            System.out.println(e);
            return null;
        }
    }

    public void freeConnection(Connection c) {
        try {
            c.close();
        } catch (SQLException e) {
            System.out.println(e);
        }
    }
}

  把数据库的连接改成从连接池中拿连接

public static void insertInfo(String name, String likes) {
    try {
        // 获取连接池对象,获取数据库连接
        ConnectionPool pool = ConnectionPool.getInstance();
        Connection connection = pool.getConnection();
        // 操作数据库
        String sql = "insert into users(name, likes) values(?,?)";
        PreparedStatement statement = connection.prepareStatement(sql);// 获取操作数据库的对象
        statement.setString(1, name);
        statement.setString(2, likes);
        statement.executeUpdate(); // 执行sql

        // 4、关闭结果集、数据库操作对象、数据库连接
        pool.freeConnection(connection);
        statement.close();
        connection.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

   JPA 

  用JDBC来操作数据库,有点繁琐了,所以Java 在JDBC的基础上又提供了JPA规范(接口)。在数据库建模的时候,有实体,属性,实例和关系等,对应到Java中,也正好有类,属性,对象等,所以可以提供类和表之间的映射。一个类对应一张表,类中的属性对应表的属性,类的实例对象对应表中的一行,对象和行自动映射,省去了JDBC的转换操作。类中的属性,可以是复合属性(另外一个类),这样就可以表示关系。怎样让一个类表示一张表,以及关系,添加注解。

  对一个类注解@Entity,就表示它和一张表关系(类名就是表名),自动的,类的属性就对应表中的属性(列)(字段名就是列名),当然,也可以使用 @Table(name = ''Customer'') 和@Column (name = "LAST_NAME") 指定表名和列名。需要注意的是,要明确指定表中的主键,用@Id注解到某个属性上。主键通常是自动生成的,再加注解,@GeneratedValue(strategy=GenerationType.AUTO)。@ManyToOne ,@OneToMany,@ OneToOne, @ManyToMany 来表示关系,比如User类有一个List<Email>属性,一个user可以有多个email, 所以在List<Email>属性上标注@OneToMany。关系是相互的,Email类中也应该有一个User 属性,要标注@ManyToOne,多个email 都属于一个user,关系都是类相对属性来言。

  在使用JPA之前,要配置一个持久化单元,告诉JPA 怎么连接数据库。持久化单元在persistence.xml文件中定义,这个文件位于 src/main/java/META-INF 目录中,还要下载一下实现类,比如eclipselink-3.0.4.jar,放到lib中, 以前就需要一个依赖javaee-api的jar,提供javax.* 但Java EE 已经被 Eclipse 基金会接管并更名为 Jakarta EE。javax.* 的包名会被更改为 jakarta.*。jakarta.persistence-3.0.0.jar,  javaee 还依赖一个commonj.sdo-2.1.1.jar

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
    xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="emailListPU"
        transaction-type="RESOURCE_LOCAL">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <property name="javax.persistence.jdbc.url"
                value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false" />
            <property name="javax.persistence.jdbc.user" value="root" />
            <property name="javax.persistence.jdbc.password" value="123" />
            <property name="javax.persistence.jdbc.driver"
                value="com.mysql.cj.jdbc.Driver" />
            <property
                name="javax.persistence.schema-generation.database.action"
                value="create" />
        </properties>
    </persistence-unit>
</persistence>

  注意

  persistence-unit有个name属性,在代码中使用name获取到entity manager. Transaction-type: RESOURCE_LOCAL 则是告诉JPA, 你想自己创建entity manager和管理事务。provider 就是提供JPA的实现者,JPA 是接口。exclude-unlisted-classes: 你想让persistence-unit管理所有标注@entity 的实体类,还是在这里列出来的类,false 就是表示管理所有的类。property 就是数据库的连接参数,有一个action, 表示,如查@entity,没有对应的数据库表,要怎么处理,create 表示创建,none 表示不创建。

  创建User实体类,

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;

@Entity
@Table(name = "users") 
public class User {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private String name;
    private String likes;

    public String getLikes() {
        return likes;
    }

    public void setLikes(String likes) {
        this.likes = likes;
    }

    public User() {}
    
    public User(String name, String likes) {
        this.name = name;
        this.likes = likes;
    }
  // 省略 get和 set方法 }

  使用Jpa 获取操作数据库,先获取 entity manager factory,以此获取Entity manager 。 entity manager factory是线程安全的,entity managers 并不是线程安全的,所以在每一个请求中都要获取新的entity managers 来操作数据库。在com.sam包下,创建DBUtils来创建entity manager factory

import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class DBUtils {
    private static final EntityManagerFactory emf = Persistence.createEntityManagerFactory("emailListPU");
    
    public static EntityManagerFactory getEmFactory() {
        return emf;
    }
}

  再写一个API来getInfo,那就首先要调用entity manager factory 的createEntityManager 来获到entity managers。每次操作数据库之前,都要获取entity manager。有了entity manager,自动获取一个find方法,用主键来查取对象,第一个参数,是要操作的实体类,这样entity manager就知道操作数据库的哪张表,哪个是主键,以及返回什么类型。第二个参数是主键,它是Long类型。如要找到记录(实例)就返回,没有找到,返回null。操作完数据库,不要忘记关闭entity manager。

protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        var em = DBUtils.getEmFactory().createEntityManager();
        
        try {
            var user = em.find(User.class, 1L);
            System.out.print(user.getName());
        } finally {
            em.close();
        }
    }

   但要用其它实体属性来获取实体时,就要写JPQL (Java PersistenceQuery Language)语句,

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    var result = selectUser("sam");
    if(result != null) {
        System.out.println(result.getName());
    }
}

private static User selectUser(String name) {
    var em = DBUtils.getEmFactory().createEntityManager();
    String qStr = "SELECT user From User user WHERE user.name= :name"; // :声明变量
    TypedQuery<User> q = em.createQuery(qStr, User.class);
    q.setParameter("name", name); // 为query语句中的变量赋值
    
    User user = null;

    try {
        user = q.getSingleResult();
    } catch(NoResultException | NonUniqueResultException e) {
        System.out.println(e);
    } finally {
        em.close();
    }
    
    return user;
}

  在qStr语句中,user就是一个User实体的对象,Select user就是获取整个实体(表的所有列),user.name 实体的某个属性,用来操作表中的哪一列。如果想获取表中的某一列,就是操作实体的某个属性,比如 String qStr = "SELECT user.name From User user WHERE user.name= :name";  就是获取user.name列,那就返回String了。

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    var result = selectUser("sam");
    if(result != null) {
        System.out.println(result);
    }
}

private static String selectUser(String name) {
    var em = DBUtils.getEmFactory().createEntityManager();
    String qStr = "SELECT user.name From User user WHERE user.name= :name"; // :声明变量
    
    // 由于返回user.name是String类型,所以TypedQuery要用String来接收
    TypedQuery<String> q = em.createQuery(qStr, String.class);
    q.setParameter("name", name); // 为query语句中的变量赋值
    
    String userName = null;
    try {
        userName = q.getSingleResult();
    } catch(NoResultException | NonUniqueResultException e) {
        System.out.println(e);
    } finally {
        em.close();
    }
    
    return userName;
}

  用其它列(实体属性)查询很可能查出多个实例,这时只需要调用getResultList, 查询的query没有变化。

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    var result = selectUser("sam");
    // 没有查到数据时,jpa并没有规定返回null,还是空列表
    if(result != null || !result.isEmpty()) {
        System.out.println(result);
    }
}

private static List<String> selectUser(String name) {
    var em = DBUtils.getEmFactory().createEntityManager();
    String qStr = "SELECT user.name From User user WHERE user.name= :name"; // :声明变量
    
    // 虽然是返回list<String>,但是query竟然没有变化
    TypedQuery<String> q = em.createQuery(qStr, String.class);
    q.setParameter("name", name); // 为query语句中的变量赋值
    
    List<String> userName = null;
    try {
        userName = q.getResultList(); // 返回list要调用getResultList
    } catch(NoResultException | NonUniqueResultException e) {
        System.out.println(e);
    } finally {
        em.close();
    }
    
    return userName;
}

  如果要选择多个实体属性,比如user.name和user.likes,这时createQeury返回值是Object[].

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    var result = selectUser("sam");
    // 由于返回的是Object类,所以需要强制类型转换
    if(result != null || result.length != 0) {
        var name = (String)result[0];
        var likes = (String) result[1];
        System.out.println(name + "   " + likes);
    }
}

private static Object[] selectUser(String name) {
    var em = DBUtils.getEmFactory().createEntityManager();
    String qStr = "SELECT user.name, user.likes From User user WHERE user.name= :name"; // :声明变量
    
    // 当要返回表的多个列时,就是select 多个实体属性,createQuery默认返回的是Object[]
    // 每一个列的值就是数组的每一项
    TypedQuery<Object[]> q = em.createQuery(qStr, Object[].class);
    q.setParameter("name", name); // 为query语句中的变量赋值
    
    Object[] nameLikes = null;
    try {
        nameLikes = q.getSingleResult(); // 返回list要调用getResultList
    } catch(NoResultException | NonUniqueResultException e) {
        System.out.println(e);
    } finally {
        em.close();
    }
    
    return nameLikes;
}

  增加,删除,更新单个实体,比较简单,分别调用entity manager的persit, remove和 merge方法,这些方法都要在事务中完成。

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    var em = DBUtils.getEmFactory().createEntityManager();
    var trans = em.getTransaction();
    var user = new User("Jason", "football");
    try {
        trans.begin();
        em.persist(user); // create
        
        var userReturned = em.find(User.class, 51L);
        userReturned.setName("Jason11");
        em.merge(userReturned);
        
        em.remove(userReturned);
        trans.commit();
    } catch(Exception e) {
        trans.rollback();
    } finally {
        em.close();
    }
}

  如果批量更新,要写query语句

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    var em = DBUtils.getEmFactory().createEntityManager();
    var trans = em.getTransaction();
    
    String qString = "UPDATE User user SET user.likes = 'basketball' WHERE user.id > 50";
    var query = em.createQuery(qString);
    try {
        trans.begin();
        int count = query.executeUpdate();
        System.out.println(count);
        trans.commit();
    } catch(Exception e) {
        trans.rollback();
    } finally {
        em.close();
    }
}

  批量删除和批量更新步骤一致,只要把String qstring 换成删除语句,就可以了,比如String qString = "DELETE FROM User user WHERE user.id > 50";

  再创建Email表,多表关联。由于jpa中数据之间的关系,是靠数据库的外键关联的,Email在一对多的关系下,是多的一方,外键应该建在它的上面,

CREATE TABLE `test`.`emails` (
`id` INT NOT NULL AUTO_INCREMENT,
`email` VARCHAR(45) NOT NULL,
`user_id` INT NULL,
PRIMARY KEY (`id`),
INDEX `user_email_idx` (`user_id` ASC) VISIBLE,
CONSTRAINT `user_email`
FOREIGN KEY (`user_id`)
REFERENCES `test`.`users` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE);

insert into test.emails(email, user_id) 
    Values("123@qq.com", 1), ("456@qq.com", 1);

  在实体类中建立一对多的关系有两种方法,一种是在 一的一方(User实体类)使用oneToMany和JoinColumn,添加一个List属性,并设置好get和Set方法

@OneToMany
@JoinColumn(name = "user_id")
private List<Email> emailList = new ArrayList<>();

  Email 没有什么变化

@Entity
@Table(name = "emails") 
public class Email {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    private String email;
    
    public Email() {}
    public Email(String email) {this.email = email;}
   // 省略 get set方法  
}

  那更新的时候,要同时设置user和email, 要注意的 多表关联,还要设置@OneToMany(cascade=CascadeType.ALL)。

protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    var em = DBUtils.getEmFactory().createEntityManager();
    var trans = em.getTransaction();
    var user = new User("Jason", "football");
    user.getEmailList().add(new Email("789@qq.com"));
    
    try {
        trans.begin();
        em.persist(user);
        trans.commit();
    } catch (Exception e) {
        System.out.println(e);
        trans.rollback();
    } finally {
        em.close();
    }
}

  另一种是在 一的一方,使用OneToMany, 在多的一方使用ManyToOne.  OneToMany要定义mappedby, manyToOne要定义一个字段,指定mappedBy的实体类,也就是在Email 实体加 user 实体类字段,User实体中还要添加addEmailt和removeEmail来维护两者之间的关系

  @OneToMany(cascade=CascadeType.ALL, mappedBy = "user")
    private List<Email> emailList = new ArrayList<>();
    
    public void addEmail(Email email) {
        emailList.add(email);
        email.setUser(this);
    }
    public void removeEmail(Email email) {
        emailList.remove(email);
        email.setUser(null);
    }

  在Email 类中,增加user类字段,由于在removeEmail删除email的时候需要判断相等性,最好重写一下hashCode和equal。

    @ManyToOne
    private User user;
    
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Email )) return false;
        return id != null && id.equals(((Email) o).getId());
    }
 
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
// 省略getUser setUser

  在更新的时候,

var user = new User("Jason", "football");
user.addEmail(new Email("789@qq.com"));

  当去查询一个user的时候,email并不会返回,因为懒加载。如果想一次性的返回 email,可以给@OneToMany 再加一个注解 fetch=FetchType.EAGER。如果实体类中有时间类型,由于 java.util中有太多的时间类型,所以要添加类型

@Temporal(javax.persistence.TemporalType.DATE)
private Date updateTime;

   Session 和Cookie

  当用户登录到页面,需要记住它,就需要Session和Cookie。当浏览器请求Tomcat服务器时,Tomcat服务器会先检查请求中有没有一个ID,就是请求的请求头中的cookies中有没带JSESSIONID(HttpServletRequest#getCookies() ), 如果有,什么都不做,如果没有,它会创建一个ID和一个与这之对应的HttpSession 对象,然后存储到一个类似map的数据结构中,key是ID,值是HttpSession 对象。再通过HTTPServletResponse#AddCookie()把ID保存到浏览器。自此以后,浏览器的请求中都会JSESSIONID的cookie,也就是说,每一个浏览器请求都有一个与之相对应的HttpSession 对象

  session的管理是tomcat服务器自动完成的,我们在程序中只需要 HttpServletRequest#getSession() 来获取到session对象进行使用(setAttribute 和getAttribute)。

当用户登录成功的时候,
request.getSession().setAttribute(“user”, user) // 把user 对象放到session中。

// 在其它页面获取到session中的user对象
HttpSession session = request.getSession(false)// false 表示如果session对象不存在,不用创建新对象。
if(session !=null && session.getAttribute(“user”) !== null) 

  当在程序中调用HttpServletRequest#getSession(), 服务器就会通过HttpServletRequest#getCookies() 获取jessionid ,然后通过jessionid 获取到对应的HttpSession,并返回回来。下图是浏览器中的jessionid 

  监听器(listener)

  监听器是一个类来监听web应用生命周期中的各种事件,以提供当这些事件发生时,要执行的方法。web应用生命周期中的各种事件是通过接口提供的。不同的接口,提供不同的事件,监听什么事件,就是实现什么接口。比如ServletContextListener接口,提供了Tomcat容器启动和销毁事件。Tomcat 容器启动要做什么,销毁时要做什么,就实现ServletContextListener 接口,复写它的方法,方法内容就是要做的事情。写完类之后,要记得注册(@WebListener),让服务器知道这是个监听器,以在程序运行过程中发生事件后调用方法。这相当于在前端开发中添加click事件,@WebListener相当于addEventListener,  监听器就相当于'click', 复写的方法,相当于事件处理函数。当真发生click时,调用事修的处理函数就可以了。

  过滤器filter

  拦截http请求,在请求到servlet之前或response之后执行代码,

  类实现Filter, 重写doFliter, doFilter方法,就相当于node.js 中Express的中间件,它有一个chain.doFilter(request, response); 方法,就相当于中间件中的next()方法,放行,到下一个Filter或到Servlet, 所以在doFilter方法中,chain.doFilter()之前的代码,会在请求到来的时候执行,chain.doFilter()之后的代码会在respone返回响应之后执行。

   请求到来,执行第一行,然后doFilter,就是请求到来,chain.doFilter()方法之前的代码执行,进入到severlt,然后servlet 返回数据,再执行最后一行,chain.doFilter()之后的代码会在respone返回响应之后执行。如果没有最后一句,这个filter只在请求进来执行,response 返回的时候,并不执行。如果没有第一句,那么这个filter 不会处理请求进来的时候,只在在reponse返回的时候执行。

  当然不要忘记了注册fitler,添加个Fliter注解。在STS中,可以直接  new -> other -> 搜索filter 来创建filter,listener也可以。

@WebFilter("/info") // 注解内容表示,对什么路径进行拦截,比如 /* 拦截所有
public class PathFilter extends HttpFilter implements Filter {
 
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletContext sc = getServletContext();
        sc.log(getFilterName() + " | " + " before request");
        chain.doFilter(request, response);
        sc.log(getFilterName() + " | " + " after request");
    }
}

  当项目越来越大,类越来越多,都写到一个包里,不好管理,按功能区分,有的是和数据库打交道,有是控制请求,整个程序的架构是mvc架构。

  Model: 和数据打交道,它还可以再细分为service(处理业务逻辑)和dao层(读写数据库)。Controller接受用户请求,调用service(service再调用dao),拿到数据,把数据注入到jsp中,进行返回。随着前端Ajax的发展,后端就变成了只提供数据了,变成restful API。可以使用Servlet 实现restful API,但要手动处理url map等,比较麻烦,所以Java提供了一套Web Service的标准,用来开发restful api,或者web服务,web service 是建立在Servlet API之上的。The services that can be accessed over network are called web services。

 

  JAX-RS定义了服务暴露restful API的标准,就在类的方法标注处理什么方法,返回值什么类型,处理请求的路径等,Jersey和RESTeasy是实现了标准,用 JAX-RS开发的程序可以部署到任何J2EE容器。Spring MVC 也能实现Restful, 但和JAX-RS没有关系, 可以把它看做是JAX-RS的 代替品,两者都能实现restful,但实现方式完全不同。MVC框架的基本原理是所有请求都映射到一个Servlet,然后去实现它的service方法。

  Maven

  以上开发方式,有一个重大问题,就是依赖的安装,需要搜索,下载,然后复制到项目中,依赖管理不方便。这时需要Maven,当然,Maven不仅可以管理依赖,还可以创建项目,打包,编译部署等,是自动化构建工具。使用maven,就要先下载maven。Maven官网(http://maven.apache.org/) 左侧边栏有一个Download,点击,进入到下载页面。页面上Files列出了下载项

  Windows下选第二行(Binary zip archive),点击它右侧的Link列进行下载。下载完成后,比如放到D:盘,然后进行解压。打开解压后的文件夹,可以看到bin目录包含maven命令,conf包含maven的配置文件等。为了使用maven命令,也要配置环境变量。和Java一样,定义一个MAVEN_HOME 变量,然后在path中,新增%MAVEN_HOME%\bin。

  使用maven创建项目。maven提供了archetypes,就是提前定义好的项目模版,用来生成新项目。在STS中,File -> new -> Maven Project,点next,

  maven-archetype-webapp 就是用来创建web项目的,选中,点next,然后填写由groupId, artifactId, 点finish创建,现在好像要在console 控制台中输入y,才能创建完成。Maven项目由groupId, artifactId, version 合在一起作唯一标识。groupId: 哪个组织负责,artifactId: 组织中的哪个项目,version: 版本号。

  打开创建的项目,看到src/main/java, src/main/test等目录结构。maven建议Java源代码放到src\main\java目录下, 测试文件放到test目录下,配置文件放到src/main/resoures下面,maven标准化项目的组织或目录结构。但有时在STS中创建的目录不全,没有src/mian/java和src/main/test,但你又不能自己创建这些目录。右键单击项目名,bulid path -> Configurable Build Path...,显示3 build path entries are missing, 点击Source,如下图所示,正好就是maven 没有生成的目录

   这种错误通常是由于项目配置的JRE System Library不对,点击上图中的Libraries,

   选中JRE System Library,Edit亮起, 点击,弹出右侧的框,选Alernate JRE,就是Installed JREs..., 点finish,Apply and close, 配置完成,src/main/java等就显示出来。有时可能右击项目名,Maven –> Update Project 一下。

  Maven使用pom.xml 管理依赖,项目根目录下有一个pom.xml文件,打开看一下,除了groupId, artifactId, version, 还有packaging, dependencies,build等。packaging表示把项目打包成什么格式,war,jar,还是pom,如果不写, 默认打包成jar包,web项目是war包,所以还是要写一下。dependencies就是项目中使用到的依赖,用到哪个依赖,就在dependencies下面建一个dependency,dependency里面就是依赖的标识,默认安装了junit依赖。build是配置项目构建的插件等。在pom.xml中声明依赖,Maven就会自动下载它们,在项目中就可以使用这些依赖了,项目根目录下Maven Dependencies 中已经有了junit jar包。maven到哪里下载jar包,中央仓库,由于中央仓库在国外,下载会比较慢,最好配置成阿里的国内仓库。

  Maven 设置,都是通过settings.xml文件进行配置,在两个地方有setting.xml。maven安装目录和用户目录下的 .m2。前者称为全局配置,后者称为用户配置,用户配置的优先级高。通常来说,执行maven命令,maven会自动创建 .m2目录。如果没有,可以手动建一个 .m2目录,从maven的安装目录复制一份settings.xml,然后修改配置。

<mirrors>
    <mirror>
    <id>aliyunmaven</id>
    <mirrorOf>*</mirrorOf>
    <name>阿里云公共仓库</name>
    <url>https://maven.aliyun.com/repository/public</url>
  </mirror>
</mirrors>

  项目中依赖了mysql 驱动和eclipselink, 在maven 仓库中,有两个eclipselink, 一个是EclipseLink, 一个是EclipseLink JPA, 没有太搞懂有什么区别,用了EclipseLink JPA

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.1.0</version>
</dependency>
<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>org.eclipse.persistence.jpa</artifactId>
    <version>3.0.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.18</version>
</dependency>

  当安装依赖后,查看Maven Dependencies , 发现多了好多依赖,这是传递依赖。在pom中声明的依赖,称为直接依赖,直接依赖它们也会有自己的依赖,称为传递依赖,maven 安装依赖的时候,会把所有的依赖都装上。这就会存在一个问题,多个依赖都依赖同一个依赖,但版本不一致,怎么办 ?比如下图

  直接依赖是A,B,C,D,E,它们按照在pom.xml中的书写顺序依次排开,pom.xml中是从上到下,在图中用从左到右表示这种关系,整个依赖形成一棵树。有2个B依赖,3个F依赖,项目中要安装哪个依赖?安装离项目(树根)最近的依赖,那就是安装0.0.8版本的B依赖。如果离项目(树根) 一样近,那就安装先发现的依赖(在pom文件中先声明的直接依赖的依赖), 就是0.1.3版本的F依赖,在一个maven项目,依赖只能存在一个版本。如果用过npm,一个项目中一个依赖可以有多个版本共存。maven的自动冲突机制虽好,但有时也会引发无法预知的问题,比如安装不想要的依赖或过时的依赖。maven提供了一个插件,可以视觉化项目的依赖树,在项目根目录下,打开命令行,mvn dependency:tree, 第一次执行的时候,可能要安装插件,

   如要发现过时的依赖,比如上图的F(0.1.3),需要安装F(2.2.0),就要在pom.xml中明显指定F(2.2.0),让它成为直接依赖,或者在安装A,C直接依赖时,不让它安装传递依赖F,dependency中可以使用exclusions,把不想安装的传递依赖排除掉。

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>3.8.1</version>
    <scope>test</scope>
    
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest</artifactId>
        </exclusion>
    </exclusions>
</dependency>

  项目还处于报错的状态,因为maven生成的项目中,默认有一个index.jsp,需要servtlet依赖,但把项目部署到tomcat时,servlet又是不需要的,因为tomcat会提供,这就需要设置依赖的scope。scope 有几种:

  compile:在src/main/和src/test/中,会被import的jar包,它们会被打包到最终的项目jar包或war包中,在项目运行时使用,这样的jar包scope是compile的。compile是默认的scope,如果依赖没有写scope,它的scope就是complie

  test:只在src/test/中使用的jar包,用来测试代码,不会被打包到jar包或war中,比如junit。

  runtime:写代码时不用,运行时使用的jar包,比如mysql 的驱动jar包。你不会在代码中import 驱动jar包中的类或接口,但要把它打包到最终项目的jar包或war中,因为项目运行的时候用,需要用它来操作数据库。

  provide: 在开发的时候使用,要从jar中import类或接口,但却不用把它们打包到项目最终要部署的jar包或war中。比如servlet,在开发代码时,要import  javax.servlet.*, 但部署到tomcat时,却不需要,因为tomcat自己提供。

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

  Maven –> Update Project,项目没有报错了。pom.xml中还有一个小的实用属性property, 它定义版本号变量,声明版本号,比如spring开发的时候,安装spring-mvc, spring jbdc等,它们要统一使用一个版本号,这时就可以声明一个版本号变量,version中使用变量,保证统一。

<properties>
    <junit.version>4.12</junit.version>
</properties>
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency>

  项目开发完了,要进行部署,这需要编译,打包,有可能还要跑单元测试,在maven中,这些任务统称为goal. 怎么执行goal呢?goal 被封装在maven的插件中,插件是goal的集合,一个插件可能包含一个或多个goal,所以执行一个goal,就要先找到对应的插件,再调用其中的goal,maven自带了一些插件,使用Intellj IEDA 打开项目,右侧有一个maven,点击一下

   Maven自带了clean, compiler, deploy, war 等插件,compiler插件封装了compile, help 和testCompile等goal,clean插件封装了clean和help等 goal。所以编译就要执行mvn compiler:compile, 要清理就要执行mvn clean:clean,命令格式:mvn 插件名:goal名。maven的构建功能都是通过插件实现的。如果要配置插件,可以pom.xml的build中配置,build中plugins列出要配置的插件,每一个插件都有configuration

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

  有时maven自带的插件不太好用,比如war插件才2.2版本,还是需要在pom.xml中配置插件,来代替Maven自带的插件, 直接升级版本号就可以,比如使用3.2版本

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>3.2.2</version>
        </plugin>
    </plugins>
</build>

  maven goal这种方式,一次只能执行一个任务,如果要实现打包部署的任务,就要执行编译,测试,打包,部署4个命令,非常麻烦,为此,maven 提出了生命周期和阶段的概念,生命周期是由一系列的阶段组成,每一阶段封装不同的goal, 直接maven 阶段名就能执行多个goal,生命周期是抽象的概念,不能执行。

  maven内置了3个生命周期,default(负责编译,打包,部署),clean(负责清理工作)和site(不常用)。default生命周期有 validation(依赖是否下载并可用), complie(编译),test(运行单元测试), package(打包成jar或war), insall(把jar包部署到本地仓库,供本机其它项目使用), deploy(把jar包部署到远程仓库)。clean 生命周期只有一个阶段就是clean。mvn package 就执行package阶段,并把package之前的阶段(validation, compile, test)全执行了,触发周期后面的命令会自动触发同一周期前面的命令。maven的阶段命令还可以并用,mvn clean package 先清理,再打包, mvn clean complie 先清理,再编译,mvn clean install,先清理,再打包部署。maven 中的部署不是经常说的部署到服务器,而是部署到本地仓库或远程仓库。IEDA中也显示生命周期,

   想运行哪个阶段,只要双击哪个阶段就可以了。

  怎么在IDE中运行程序(web应用程序)?基于Maven 工程,运行web应用的方式有两种:一种是在IDE工具中,配置web应用服务器,一种是在pom.xml中配置web应用服务器。推荐使用第二种:配置jetty服务器插件。

<build>
    <plugins>
        <plugin>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>11.0.19</version>
            <configuration>
                <httpConnector>
                    <port>8000</port>
                    <idleTimeout>60000</idleTimeout>
                </httpConnector>
                <webApp>
                    <contextPath>/web</contextPath>
                </webApp>
                <scan>10</scan>
            </configuration>
        </plugin>
    </plugins>
</build>

  httpConnector 配置Jetty的监听端口,如果不配置, 默认的端口是8080。webApp/contextPath: 配置web应用的上下文,就是在url中输入什么,才能访问到项目。配置了/web,就可以localhost:8000/web/访问项目,如果不配置,默认是/,url直接输入localhost:8000。scan: 扫描项目变更的时间间隔,在设置间隔内检查Web项目是否有变化,如果发现有变化就自动热部署。默认是0,表示禁用热部署。任何一个大于0的数字都将表示启用热部署。

  pom文件配置好后,IDEA右边Maven,单击刷新图标,plugins下面有jetty插件,

  双击jetty:run或jetty:run-explode,可以运行jetty服务器。右键单击它们,则有debug模式,启动服务器。IDEA左下角的run 表示程序在运行。

   单击红色方块则是停止服务。在STS中,运行maven,全是靠命令操作,没有可视化操作,项目名右击,Run as, 里面有默认的命令(install等)。如果没有要执行的命令,就需要配需,Run as -> Run Configurations...,  在输入框中输入名字,比如run,在Goals中输入要执行的命令jetty:run,

   所有自定义的命令放了Maven Build下面,配置好后,项目名右击,Run As -> Maven Build,就可以执行自定义的命令,如果配置了多个命令,就会弹窗,让你选择执行哪个命令。在上图中,maven Build下面的所有命令,选中,右击,可以删除它们。

  多模块项目: 在一个maven项目下嵌套多个maven项目,这一个maven项目称为父项目或父工程,它只有一个pom.xml文件,嵌套的maven项目就是子项目或子工程,为什么要有一个父工程呢?因为多个项目有相同的依赖,可以统一依赖的版本,这个pom文件的package是pom,表示不对这个项目进行打包,它只是被继承的父工程。这程目录结构,在Intelj IDEA中,实现起来更为直观,因为,它直接支持module。new -> project -> maven archetype, 建一个maven项目,比如multi-parent 项目,把项目的内容都删除,只剩pom.xml文件。然后,在项目名multi-parent上右击,new -> module -> maven archetype 新建一个maven web项目,这是真正要开发和部署的项目,比如child-web, 再项目名multi-parent上右击,new -> module -> maven archetype 新建一个maven java项目,比如child-service

   分别打开各自的pom.xml文件,可以发现,在父工程multi-parent中,除了package还有module,它用来聚合,表示在父工程上,执行maven的命令,所有子项目都会按照顺序执行这个maven命令。比如,在父工程上执行mvn package,  父工程下的所有项目都打包了。

<packaging>pom</packaging>
<modules>
    <module>child-web</module>
    <module>child-service</module>
</modules>

  在子工程child-web或child-service中,pom.xml文件多了parent,用来继承版本号

<parent>
    <groupId>org.example</groupId>
    <artifactId>multi-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>

  父工程中的pom.xml 统一管理子工程中的依赖,比如在父工程pom.xml 中声明junit, 各个子工程都用这个版本的junit, 注意管理依赖使用的是depedencyManagement,在父工程中pom.xml 声明一个

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

  在子工程中的junit 就不用声明version, 直接写

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

   在STS中创建多模块目录,就比较麻烦了,因为它没有模块的概念,新建就是一个项目,这就需要手动设置项目之间的父子关系。新建三个maven项目, multi-parent, child-web, child-service, 它们三个是平级目录

   multi-parent 项目下,还是只有pom.xml,它要配置package 是pom 和module,由于是平级的关系,module只能通过相对路径的方式来找到子模块或子项目

<groupId>com.sam</groupId>
<artifactId>multi-parent</artifactId>
<version>0.0.1</version>
<packaging>pom</packaging>

<modules>
    <module>../child-service</module>
    <module>../child-web</module>
</modules>

  child-web, child-service 子项目要配置parent,指定哪个项目是其parent, 那就要在子项目中Pom中使用relativePath 来引用父项目中的pom。还要注意子项目,就不要再配置groupId和verison了,只要配置artifactId 就可以,

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--子项目不用配置groupId和verison,只配artifactId-->
    <artifactId>child-service</artifactId>
    <packaging>jar</packaging>

     <!--parent中 groupId,artifactId,version 必须和父项目一致,并且用relativePath来指定  -->
    <parent>
        <groupId>com.sam</groupId>
        <artifactId>multi-parent</artifactId>
        <version>0.0.1</version>
        <relativePath>../multi-parent</relativePath>
    </parent>

    <name>child-service</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
   <-- ...... -->
</project>

  Maven部署打包多环境(开发、测试、生产)配置(环境隔离),

  1, 在pom.xml的<build>中,写resources: 定义环境公用的文件,或不同环境用的文件,不用环境用的文件用参数代替。

<build>
    <resources>
        <!--   不同环境下的配置文件所在的目录  -->
        <resource>
            <directory>src/main/resources.${deploy.type}</directory>
        </resource>
        <!--   各个环境下的公用的配置文件所在的目录   -->
        <resource>
            <directory>src/main/resources</directory>
        </resource>
    </resources>
</build>

  2,  <build> 同一级 建<profiles>中,定义不同的环境,和默认开启哪一个环境

<profiles>
    <profile>
        <!-- 本地开发环境 -->
        <id>dev</id>
        <properties>
            <!-- 赋值给 resources中定义的变量deploy.type -->
            <deploy.type>dev</deploy.type>
        </properties>
        <!-- 默认是本地开发环境 -->
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <!-- 测试环境 -->
        <id>test</id>
        <properties>
            <deploy.type>test</deploy.type>
        </properties>
    </profile>
    <profile>
        <!-- 生产环境 -->
        <id>prod</id>
        <properties>
            <deploy.type>prod</deploy.type>
        </properties>
    </profile>
</profiles>

  3,在src/main下面建3个目录分别对应3个环境:resources.test, resources.dev, resoureces.prod 分别存放三个环境下的不同配置。src/main/resourecs存的是相同的配置。

  4,在cmd下 mvn clean package -Dmaven.test.skip=true -Pdev , -Pdev就是dev环境,-Pprod就是生产环境。在powershell下, mvn clean package '-Dmaven.test.skip=true' -Pdev , 加引号。看一下target目录,生成classes中有application.properties文件,是dev环境的。虽然在开发的时候,分src/main/java,和src/main/resources,但打包后,它们统一放到classes中。在IDE是,点击jetty,运行时,怎么选择环境,在Itellij IDEA中,比较简单,右上角的maven中,直接显示profiles,如果没有,点击刷新一下

  选哪一个就是在IDEA中自动编译打包的使用的环境。在编辑器中点Jetty运行时,编译打包使用的环境。在STS中,则要项目名右击,maven ->Select Maven profiles,进行选择

   有时下载依赖时,下载不下来或者无法更新依赖,这有可能是本地Maven仓库缓存被污染或破坏,导致Maven无法正确使用现有的依赖项,并且也无法重新下载!解决办法是清除本地Maven仓库缓存(lastUpdated 文件),因为只要存在lastupdate缓存文件,就不会重新下载。默认情况下,本地仓库在C:\Users\用户名\.m2\repository下,哪个依赖无法更新,就根据groupId, artifactId进行查找,每一个点号就代表一级目录,然后删除缓存文件。 

 

posted @ 2017-10-04 19:59  SamWeb  阅读(5989)  评论(0编辑  收藏  举报