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&useUnicode=true&characterEncoding=utf-8&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进行查找,每一个点号就代表一级目录,然后删除缓存文件。