java 基础(二)java 基础(二)
2016-2-1 by Damon
61. 编写多线程程序有几种实现方式
Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。
补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法可以在线程执行结束时产生一个返回值,代码如下所示:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; class MyTask implements Callable<Integer> { private int upperBounds; public MyTask(int upperBounds) { this.upperBounds = upperBounds; } @Override public Integer call() throws Exception { int sum = 0; for(int i = 1; i <= upperBounds; i++) { sum += i; } return sum; } } class Test { public static void main(String[] args) throws Exception { List<Future<Integer>> list = new ArrayList<>(); ExecutorService service = Executors.newFixedThreadPool(10); for(int i = 0; i < 10; i++) { list.add(service.submit(new MyTask((int) (Math.random() * 100)))); } int sum = 0; for(Future<Integer> future : list) { // while(!future.isDone()) ; sum += future.get(); } System.out.println(sum); } }
62. synchronized关键字的用法
synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第60题的例子中已经展示了synchronized关键字的用法。
63. 举例说明同步和异步
如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。
64. 启动一个线程是调用run()还是start()方法
启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。
65. 什么是线程池(thread pool)
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
- newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
第60题的例子中演示了通过Executors工具类创建线程池并使用线程池执行线程的代码。如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
66. 线程的基本状态以及状态之间的关系
说明:其中Running表示运行状态,Runnable表示就绪状态(万事俱备,只欠CPU),Blocked表示阻塞状态,阻塞状态又有多种情况,可能是因为调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断。
67. 简述synchronized 和java.util.concurrent.locks.Lock的异同
Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。
68. Java中如何实现序列化,有什么意义
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆(可以参考第29题)。
69. Java中有几种类型的流
字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。关于Java的I/O需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外Java中的流不同于C#的是它只有一个维度一个方向。
面试题 - 编程实现文件拷贝。(这个题目在笔试的时候经常出现,下面的代码给出了两种实现方案)
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public final class MyUtil { private MyUtil() { throw new AssertionError(); } public static void fileCopy(String source, String target) throws IOException { try (InputStream in = new FileInputStream(source)) { try (OutputStream out = new FileOutputStream(target)) { byte[] buffer = new byte[4096]; int bytesToRead; while((bytesToRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesToRead); } } } } public static void fileCopyNIO(String source, String target) throws IOException { try (FileInputStream in = new FileInputStream(source)) { try (FileOutputStream out = new FileOutputStream(target)) { FileChannel inChannel = in.getChannel(); FileChannel outChannel = out.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(4096); while(inChannel.read(buffer) != -1) { buffer.flip(); outChannel.write(buffer); buffer.clear(); } } } } }
70. 写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数
import java.io.BufferedReader; import java.io.FileReader; public final class MyUtil { // 工具类中的方法都是静态方式访问的因此将构造器私有不允许创建对象(绝对好习惯) private MyUtil() { throw new AssertionError(); } /** * 统计给定文件中给定字符串的出现次数 * * @param filename 文件名 * @param word 字符串 * @return 字符串在文件中出现的次数 */ public static int countWordInFile(String filename, String word) { int counter = 0; try (FileReader fr = new FileReader(filename)) { try (BufferedReader br = new BufferedReader(fr)) { String line = null; while ((line = br.readLine()) != null) { int index = -1; while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) { counter++; line = line.substring(index + word.length()); } } } } catch (Exception ex) { ex.printStackTrace(); } return counter; } }
71. 如何用Java代码列出一个目录下所有的文件
如果只要求列出当前文件夹下的文件,代码如下所示:
import java.io.File; class Test12 { public static void main(String[] args) { File f = new File("/Users/Hao/Downloads"); for(File temp : f.listFiles()) { if(temp.isFile()) { System.out.println(temp.getName()); } } } }
如果需要对文件夹继续展开,代码如下所示:
import java.io.File; class Test12 { public static void main(String[] args) { showDirectory(new File("/Users/Hao/Downloads")); } public static void showDirectory(File f) { _walkDirectory(f, 0); } private static void _walkDirectory(File f, int level) { if(f.isDirectory()) { for(File temp : f.listFiles()) { _walkDirectory(temp, level + 1); } } else { for(int i = 0; i < level - 1; i++) { System.out.print("\t"); } System.out.println(f.getName()); } } }
在Java 7中可以使用NIO.2的API来做同样的事情,代码如下所示:
class ShowFileTest { public static void main(String[] args) throws IOException { Path initPath = Paths.get("/Users/Hao/Downloads"); Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println(file.getFileName().toString()); return FileVisitResult.CONTINUE; } }); } }
72. 用Java的套接字编程实现一个多线程的回显(echo)服务器
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
private static final int ECHO_SERVER_PORT = 6789; public static void main(String[] args) { try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) { System.out.println("服务器已经启动..."); while(true) { Socket client = server.accept(); new Thread(new ClientHandler(client)).start(); } } catch (IOException e) { e.printStackTrace(); } } private static class ClientHandler implements Runnable { private Socket client; public ClientHandler(Socket client) { this.client = client; } @Override public void run() { try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); PrintWriter pw = new PrintWriter(client.getOutputStream())) { String msg = br.readLine(); System.out.println("收到" + client.getInetAddress() + "发送的: " + msg); pw.println(msg); pw.flush(); } catch(Exception ex) { ex.printStackTrace(); } finally { try { client.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
注意:上面的代码使用了Java 7的TWR语法,由于很多外部资源类都间接的实现了AutoCloseable接口(单方法回调接口),因此可以利用TWR语法在try结束的时候通过回调的方式自动调用外部资源类的close()方法,避免书写冗长的finally代码块。此外,上面的代码用一个静态内部类实现线程的功能,使用多线程可以避免一个用户I/O操作所产生的中断影响其他用户对服务器的访问,简单的说就是一个用户的输入操作不会造成其他用户的阻塞。当然,上面的代码使用线程池可以获得更好的性能,因为频繁的创建和销毁线程所造成的开销也是不可忽视的。
下面是一段回显客户端测试代码:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; public class EchoClient { public static void main(String[] args) throws Exception { Socket client = new Socket("localhost", 6789); Scanner sc = new Scanner(System.in); System.out.print("请输入内容: "); String msg = sc.nextLine(); sc.close(); PrintWriter pw = new PrintWriter(client.getOutputStream()); pw.println(msg); pw.flush(); BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); System.out.println(br.readLine()); client.close(); } }
如果希望用NIO的多路复用套接字实现服务器,代码如下所示。NIO的操作虽然带来了更好的性能,但是有些操作是比较底层的,对于初学者来说还是有些难于理解。
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class EchoServerNIO { private static final int ECHO_SERVER_PORT = 6789; private static final int ECHO_SERVER_TIMEOUT = 5000; private static final int BUFFER_SIZE = 1024; private static ServerSocketChannel serverChannel = null; private static Selector selector = null; // 多路复用选择器 private static ByteBuffer buffer = null; // 缓冲区 public static void main(String[] args) { init(); listen(); } private static void init() { try { serverChannel = ServerSocketChannel.open(); buffer = ByteBuffer.allocate(BUFFER_SIZE); serverChannel.socket().bind(new InetSocketAddress(ECHO_SERVER_PORT)); serverChannel.configureBlocking(false); selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (Exception e) { throw new RuntimeException(e); } } private static void listen() { while (true) { try { if (selector.select(ECHO_SERVER_TIMEOUT) != 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); handleKey(key); } } } catch (Exception e) { e.printStackTrace(); } } } private static void handleKey(SelectionKey key) throws IOException { SocketChannel channel = null; try { if (key.isAcceptable()) { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); channel = serverChannel.accept(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { channel = (SocketChannel) key.channel(); buffer.clear(); if (channel.read(buffer) > 0) { buffer.flip(); CharBuffer charBuffer = CharsetHelper.decode(buffer); String msg = charBuffer.toString(); System.out.println("收到" + channel.getRemoteAddress() + "的消息:" + msg); channel.write(CharsetHelper.encode(CharBuffer.wrap(msg))); } else { channel.close(); } } } catch (Exception e) { e.printStackTrace(); if (channel != null) { channel.close(); } } } } import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; public final class CharsetHelper { private static final String UTF_8 = "UTF-8"; private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder(); private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder(); private CharsetHelper() { } public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{ return encoder.encode(in); } public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{ return decoder.decode(in); } }
73. XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式
XML文档定义分为DTD和Schema两种形式,二者都是对XML语法的约束,其本质区别在于Schema本身也是一个XML文件,可以被XML解析器解析,而且可以为XML承载的数据定义类型,约束能力较之DTD更强大。对XML的解析主要有DOM(文档对象模型,Document Object Model)、SAX(Simple API for XML)和StAX(Java 6中引入的新的解析XML的方式,Streaming API for XML),其中DOM处理大型文件时其性能下降的非常厉害,这个问题是由DOM树结构占用的内存较多造成的,而且DOM解析方式必须在解析文件之前把整个文档装入内存,适合对XML的随机访问(典型的用空间换取时间的策略);SAX是事件驱动型的XML解析方式,它顺序读取XML文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过事件回调代码来处理XML文件,适合对XML的顺序访问;顾名思义,StAX把重点放在流上,实际上StAX与其他解析方式的本质区别就在于应用程序能够把XML作为一个事件流来处理。将XML作为一组事件来处理的想法并不新颖(SAX就是这样做的),但不同之处在于StAX允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。
74. 你在项目中哪些地方用到了XML
XML的主要作用有两个方面:数据交换和信息配置。在做数据交换时,XML将数据用标签组装成起来,然后压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再从XML文件中还原相关信息进行处理,XML曾经是异构系统间交换数据的事实标准,但此项功能几乎已经被JSON(JavaScript Object Notation)取而代之。当然,目前很多软件仍然使用XML来存储配置信息,我们在很多项目中通常也会将作为配置信息的硬代码写在XML文件中,Java的很多框架也是这么做的,而且这些框架都选择了dom4j作为处理XML的工具,因为Sun公司的官方API实在不怎么好用。
补充:现在有很多时髦的软件(如Sublime)已经开始将配置文件书写成JSON格式,我们已经强烈的感受到XML的另一项功能也将逐渐被业界抛弃。
75. 阐述JDBC操作数据库的步骤
下面的代码以连接本机的Oracle数据库为例,演示JDBC操作数据库的步骤。
- 加载驱动
Class.forName("oracle.jdbc.driver.OracleDriver");
- 创建连接
Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "scott", "tiger");
- 创建语句
PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?"); ps.setInt(1, 1000); ps.setInt(2, 3000);
- 执行语句
ResultSet rs = ps.executeQuery();
- 处理结果
while(rs.next()) { System.out.println(rs.getInt("empno") + " - " + rs.getString("ename")); }
- 关闭资源
finally { if(con != null) { try { con.close(); } catch (SQLException e) { e.printStackTrace(); } } }
提示:关闭外部资源的顺序应该和打开的顺序相反,也就是说先关闭ResultSet、再关闭Statement、在关闭Connection。上面的代码只关闭了Connection(连接),虽然通常情况下在关闭连接时,连接上创建的语句和打开的游标也会关闭,但不能保证总是如此,因此应该按照刚才说的顺序分别关闭。此外,第一步加载驱动在JDBC 4.0中是可以省略的(自动从类路径中加载驱动),但是我们建议保留。
76. Statement和PreparedStatement有什么区别?哪个性能更好
与Statement相比,①PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL的编译错误并增加SQL的安全性(减少SQL注射攻击的可能性);②PreparedStatement中的SQL语句是可以带参数的,避免了用字符串连接拼接SQL语句的麻烦和不安全;③当批量处理SQL或频繁执行相同的查询时,PreparedStatement有明显的性能上的优势,由于数据库可以将编译优化后的SQL语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成执行计划)。
补充:为了提供对存储过程的调用,JDBC API中还提供了CallableStatement接口。存储过程(Stored Procedure)是数据库中一组为了完成特定功能的SQL语句的集合,经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。虽然调用存储过程会在网络开销、安全性、性能上获得很多好处,但是存在如果底层数据库发生迁移时就会有很多麻烦,因为每种数据库的存储过程在书写上存在不少的差别。
77. 使用JDBC操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能
要提升读取数据的性能,可以指定通过结果集(ResultSet)对象的setFetchSize()方法指定每次抓取的记录数(典型的空间换时间策略);要提升更新数据的性能可以使用PreparedStatement语句构建批处理,将若干SQL语句置于一个批处理中执行。
78. 在进行数据库编程时,连接池有什么作用
由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行TCP的三次握手,释放连接需要进行TCP四次握手,造成的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。池化技术在Java开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于Java的开源数据库连接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
补充:在计算机系统中时间和空间是不可调和的矛盾,理解这一点对设计满足性能要求的算法是至关重要的。大型网站性能优化的一个关键就是使用缓存,而缓存跟上面讲的连接池道理非常类似,也是使用空间换时间的策略。可以将热点数据置于缓存中,当用户查询这些数据时可以直接从缓存中得到,这无论如何也快过去数据库中查询。当然,缓存的置换策略等也会对系统性能产生重要影响,对于这个问题的讨论已经超出了这里要阐述的范围。
79. 什么是DAO模式
DAO(Data Access Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作。在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在一个公共API中。用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO模式实际上包含了两个模式,一是Data Accessor(数据访问器),二是Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。
80. 事务的ACID是指什么
- 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
- 一致性(Consistent):事务结束后系统状态是一致的;
- 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
- 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。
补充:关于事务,在面试中被问到的概率是很高的,可以问的问题也是很多的。首先需要知道的是,只有存在并发数据访问时才需要事务。当多个事务访问同一数据时,可能会存在5类问题,包括3类数据读取问题(脏读、不可重复读和幻读)和2类数据更新问题(第1类丢失更新和第2类丢失更新)。
脏读(Dirty Read):A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。
时间 | 转账事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 取出500元余额修改为500元 | |
T5 | 查询账户余额为500元(脏读) | |
T6 | 撤销事务余额恢复为1000元 | |
T7 | 汇入100元把余额修改为600元 | |
T8 | 提交事务 |
不可重复读(Unrepeatable Read):事务A重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务B修改过了。
时间 | 转账事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100元修改余额为900元 | |
T6 | 提交事务 | |
T7 | 查询账户余额为900元(不可重复读) |
幻读(Phantom Read):事务A重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务B提交的行。
时间 | 统计金额事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计总存款为10000元 | |
T4 | 新增一个存款账户存入100元 | |
T5 | 提交事务 | |
T6 | 再次统计总存款为10100元(幻读) |
第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。
时间 | 取款事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 汇入100元修改余额为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元将余额修改为900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为1000元(丢失更新) |
第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。
时间 | 转账事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100元将余额修改为900元 | |
T6 | 提交事务 | |
T7 | 汇入100元将余额修改为1100元 | |
T8 | 提交事务 | |
T9 | 查询账户余额为1100元(丢失更新) |
数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,按锁定对象不同可以分为表级锁和行级锁;按并发事务锁定关系可以分为共享锁和独占锁,具体的内容大家可以自行查阅资料进行了解。
直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别,数据库就会通过分析SQL语句然后为事务访问的资源加上合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的(就是说你不用理解,事实上我确实也不知道)。ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,如下表所示:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 第一类丢失更新 | 第二类丢失更新 |
READ UNCOMMITED | 允许 | 允许 | 允许 | 不允许 | 允许 |
READ COMMITTED | 不允许 | 允许 | 允许 | 不允许 | 允许 |
REPEATABLE READ | 不允许 | 不允许 | 允许 | 不允许 | 不允许 |
SERIALIZABLE | 不允许 | 不允许 | 不允许 | 不允许 | 不允许 |
需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。
81. JDBC中如何进行事务处理
Connection提供了事务处理的方法,通过调用setAutoCommit(false)可以设置手动提交事务;当事务完成后用commit()显式提交事务;如果在事务处理过程中发生异常则通过rollback()进行事务回滚。除此之外,从JDBC 3.0中还引入了Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。
82. JDBC能否处理Blob和Clob
Blob是指二进制大对象(Binary Large Object),而Clob是指大字符对象(Character Large Objec),因此其中Blob是为存储大的二进制数据而设计的,而Clob是为存储大的文本数据而设计的。JDBC的PreparedStatement和ResultSet都提供了相应的方法来支持Blob和Clob操作。下面的代码展示了如何使用JDBC操作LOB:
下面以MySQL数据库为例,创建一个张有三个字段的用户表,包括编号(id)、姓名(name)和照片(photo),建表语句如下:
create table tb_user ( id int primary key auto_increment, name varchar(20) unique not null, photo longblob );
下面的Java代码向数据库中插入一条记录:
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; class JdbcLobTest { public static void main(String[] args) { Connection con = null; try { // 1. 加载驱动(Java6以上版本可以省略) Class.forName("com.mysql.jdbc.Driver"); // 2. 建立连接 con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456"); // 3. 创建语句对象 PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)"); ps.setString(1, "骆昊"); // 将SQL语句中第一个占位符换成字符串 try (InputStream in = new FileInputStream("test.jpg")) { // Java 7的TWR ps.setBinaryStream(2, in); // 将SQL语句中第二个占位符换成二进制流 // 4. 发出SQL语句获得受影响行数 System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失败"); } catch(IOException e) { System.out.println("读取照片失败!"); } } catch (ClassNotFoundException | SQLException e) { // Java 7的多异常捕获 e.printStackTrace(); } finally { // 释放外部资源的代码都应当放在finally中保证其能够得到执行 try { if(con != null && !con.isClosed()) { con.close(); // 5. 释放数据库连接 con = null; // 指示垃圾回收器可以回收该对象 } } catch (SQLException e) { e.printStackTrace(); } } } }
83. 简述正则表达式及其用途
在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。
说明:计算机诞生初期处理的信息几乎都是数值,但是时过境迁,今天我们使用计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。
84. Java中是如何支持正则表达式操作的
Java中的String类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java中可以用Pattern类表示正则表达式对象,它提供了丰富的API进行各种正则表达式操作,请参考下面面试题的代码。
面试题: - 如果要从字符串中截取第一个英文左括号之前的字符串,例如:北京市(朝阳区)(西城区)(海淀区),截取结果为:北京市,那么正则表达式怎么写?
import java.util.regex.Matcher; import java.util.regex.Pattern; class RegExpTest { public static void main(String[] args) { String str = "北京市(朝阳区)(西城区)(海淀区)"; Pattern p = Pattern.compile(".*?(?=\\()"); Matcher m = p.matcher(str); if(m.find()) { System.out.println(m.group()); } } }
说明:上面的正则表达式中使用了懒惰匹配和前瞻,如果不清楚这些内容,推荐读一下网上很有名的《正则表达式30分钟入门教程》。
85. 获得一个类的类对象有哪些方式
- 方法1:类型.class,例如:String.class
- 方法2:对象.getClass(),例如:"hello".getClass()
- 方法3:Class.forName(),例如:Class.forName("java.lang.String")
86. 如何通过反射创建对象
- 方法1:通过类对象调用newInstance()方法,例如:String.class.newInstance()
- 方法2:通过类对象的getConstructor()或getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,例如:String.class.getConstructor(String.class).newInstance("Hello");
87. 如何通过反射获取和设置对象私有字段的值
可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。下面的代码实现了一个反射的工具类,其中的两个静态方法分别用于获取和设置私有字段的值,字段可以是基本类型也可以是对象类型且支持多级对象操作,例如ReflectionUtil.get(dog, "owner.car.engine.id");可以获得dog对象的主人的汽车的引擎的ID号。
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; /** * 反射工具类 * @author 骆昊 * */ public class ReflectionUtil { private ReflectionUtil() { throw new AssertionError(); } /** * 通过反射取对象指定字段(属性)的值 * @param target 目标对象 * @param fieldName 字段的名字 * @throws 如果取不到对象指定字段的值则抛出异常 * @return 字段的值 */ public static Object getValue(Object target, String fieldName) { Class<?> clazz = target.getClass(); String[] fs = fieldName.split("\\."); try { for(int i = 0; i < fs.length - 1; i++) { Field f = clazz.getDeclaredField(fs[i]); f.setAccessible(true); target = f.get(target); clazz = target.getClass(); } Field f = clazz.getDeclaredField(fs[fs.length - 1]); f.setAccessible(true); return f.get(target); } catch (Exception e) { throw new RuntimeException(e); } } /** * 通过反射给对象的指定字段赋值 * @param target 目标对象 * @param fieldName 字段的名称 * @param value 值 */ public static void setValue(Object target, String fieldName, Object value) { Class<?> clazz = target.getClass(); String[] fs = fieldName.split("\\."); try { for(int i = 0; i < fs.length - 1; i++) { Field f = clazz.getDeclaredField(fs[i]); f.setAccessible(true); Object val = f.get(target); if(val == null) { Constructor<?> c = f.getType().getDeclaredConstructor(); c.setAccessible(true); val = c.newInstance(); f.set(target, val); } target = val; clazz = target.getClass(); } Field f = clazz.getDeclaredField(fs[fs.length - 1]); f.setAccessible(true); f.set(target, value); } catch (Exception e) { throw new RuntimeException(e); } } }
88. 如何通过反射调用对象的方法
import java.lang.reflect.Method; class MethodInvokeTest { public static void main(String[] args) throws Exception { String str = "hello"; Method m = str.getClass().getMethod("toUpperCase"); System.out.println(m.invoke(str)); // HELLO } }
89. 简述一下面向对象的"六原则一法则"
- 单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是"高内聚",写代码最终极的原则只有六个字"高内聚、低耦合",就如同葵花宝典或辟邪剑谱的中心思想就八个字"欲练此功必先自宫",所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。我们都知道一句话叫"因为专注,所以专业",一个对象如果承担太多的职责,那么注定它什么都做不好。这个世界上任何好的东西都有两个特征,一个是功能单一,好的相机绝对不是***里面卖的那种一个机器有一百多种功能的,它基本上只能照相;另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
- 开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱,如果不清楚如何封装可变性,可以参考《设计模式精解》一书中对桥梁模式的讲解的章节。)
- 依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的里氏替换原则。)
里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的描述,Barbara Liskov女士的描述比这个要复杂得多,但简单的说就是能用父类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对代码进行重构。例如让猫继承狗,或者狗继承猫,又或者让正方形继承长方形都是错误的继承关系,因为你很容易找到违反里氏替换原则的场景。需要注意的是:子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。)- 接口隔离原则:接口要小而专,绝不能大而全。(臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。例如,琴棋书画就应该分别设计为四个接口,而不应设计成一个接口中的四个方法,因为如果设计成一个接口中的四个方法,那么这个接口很难用,毕竟琴棋书画四样都精通的人还是少数,而如果设计成四个接口,会几项就实现几个接口,这样的话每个接口被复用的可能性是很高的。Java中的接口代表能力、代表约定、代表角色,能否正确的使用接口一定是编程水平高低的重要标识。)
- 合成聚合复用原则:优先使用聚合或合成关系复用代码。(通过继承来复用代码是面向对象程序设计中被滥用得最多的东西,因为所有的教科书都无一例外的对继承进行了鼓吹从而误导了初学者,类与类之间简单的说有三种关系,Is-A关系、Has-A关系、Use-A关系,分别代表继承、关联和依赖。其中,关联关系根据其关联的强度又可以进一步划分为关联、聚合和合成,但说白了都是Has-A关系,合成聚合复用原则想表达的是优先考虑Has-A关系而不是Is-A关系复用代码,原因嘛可以自己从百度上找到一万个理由,需要说明的是,即使在Java的API中也有不少滥用继承的例子,例如Properties类继承了Hashtable类,Stack类继承了Vector类,这些继承明显就是错误的,更好的做法是在Properties类中放置一个Hashtable类型的成员并且将其键和值都设置为字符串来存储数据,而Stack类的设计也应该是在Stack类中放一个Vector对象来存储数据。记住:任何时候都不要继承工具类,工具是可以拥有并可以使用的,而不是拿来继承的。)
- 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。(迪米特法则简单的说就是如何做到"低耦合",门面模式和调停者模式就是对迪米特法则的践行。对于门面模式可以举一个简单的例子,你去一家公司洽谈业务,你不需要了解这个公司内部是如何运作的,你甚至可以对这个公司一无所知,去的时候只需要找到公司入口处的前台美女,告诉她们你要做什么,她们会找到合适的人跟你接洽,前台的美女就是公司这个系统的门面。再复杂的系统都可以为用户提供一个简单的门面,Java Web开发中作为前端控制器的Servlet或Filter不就是一个门面吗,浏览器对服务器的运作方式一无所知,但是通过前端控制器就能够根据你的请求得到相应的服务。调停者模式也可以举一个简单的例子来说明,例如一台计算机,CPU、内存、硬盘、显卡、声卡各种设备需要相互配合才能很好的工作,但是如果这些东西都直接连接到一起,计算机的布线将异常复杂,在这种情况下,主板作为一个调停者的身份出现,它将各个设备连接在一起而不需要每个设备之间直接交换数据,这样就减小了系统的耦合度和复杂度,如下图所示。迪米特法则用通俗的话来将就是不要和陌生人打交道,如果真的需要,找一个自己的朋友,让他替你和陌生人打交道。)
90. 简述一下你了解的设计模式
所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。
在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中给出了三类(创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化])共23种设计模式,包括:Abstract Factory(抽象工厂模式),Builder(建造者模式),Factory Method(工厂方法模式),Prototype(原始模型模式),Singleton(单例模式);Facade(门面模式),Adapter(适配器模式),Bridge(桥梁模式),Composite(合成模式),Decorator(装饰模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(解释器模式),Visitor(访问者模式),Iterator(迭代子模式),Mediator(调停者模式),Memento(备忘录模式),Observer(观察者模式),State(状态模式),Strategy(策略模式),Template Method(模板方法模式), Chain Of Responsibility(责任链模式)。
面试被问到关于设计模式的知识时,可以拣最常用的作答,例如:
- 工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
- 代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache代理、防火墙代理、同步化代理、智能引用代理。
- 适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。
- 模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。
除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式(Collections工具类和I/O系统中都使用装潢模式)等,反正基本原则就是拣自己最熟悉的、用得最多的作答,以免言多必失。
91. 用Java写一个单例类
- 饿汉式单例
public class Singleton { private Singleton(){} private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } }
- 懒汉式单例
public class Singleton { private static Singleton instance = null; private Singleton() {} public static synchronized Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; } }
注意:实现一个单例有两点注意事项,①将构造器私有,不允许外界通过构造器创建对象;②通过公开的静态方法向外界返回类的唯一实例。这里有一个问题可以思考:Spring的IoC容器可以为普通的类创建单例,它是怎么做到的呢?
92. 什么是UML
UML是统一建模语言(Unified Modeling Language)的缩写,它发表于1997年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持。使用UML可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系统的结构和行为。
93. UML中有哪些常用的图
UML定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构,包括:用例图(use case diagram)、类图(class diagram)、时序图(sequence diagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deployment diagram)等。在这些图形化符号中,有三种图最为重要,分别是:用例图(用来捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系)、类图(描述类以及类与类之间的关系,通过该图可以快速了解系统)、时序图(描述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能接收的消息也就是说对象能够向外界提供的服务)。
用例图:
类图:
时序图:
94. 用Java写一个冒泡排序
import java.util.Comparator; /** * 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可以相互替换) * @author骆昊 * */ public interface Sorter { /** * 排序 * @param list 待排序的数组 */ public <T extends Comparable<T>> void sort(T[] list); /** * 排序 * @param list 待排序的数组 * @param comp 比较两个对象的比较器 */ public <T> void sort(T[] list, Comparator<T> comp); } import java.util.Comparator; /** * 冒泡排序 * * @author骆昊 * */ public class BubbleSorter implements Sorter { @Override public <T extends Comparable<T>> void sort(T[] list) { boolean swapped = true; for (int i = 1, len = list.length; i < len && swapped; ++i) { swapped = false; for (int j = 0; j < len - i; ++j) { if (list[j].compareTo(list[j + 1]) > 0) { T temp = list[j]; list[j] = list[j + 1]; list[j + 1] = temp; swapped = true; } } } } @Override public <T> void sort(T[] list, Comparator<T> comp) { boolean swapped = true; for (int i = 1, len = list.length; i < len && swapped; ++i) { swapped = false; for (int j = 0; j < len - i; ++j) { if (comp.compare(list[j], list[j + 1]) > 0) { T temp = list[j]; list[j] = list[j + 1]; list[j + 1] = temp; swapped = true; } } } } }
95. 用Java写一个折半查找
折半查找,也称二分查找、二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜索范围缩小一半,其时间复杂度是O(logN)。
import java.util.Comparator; public class MyUtil { public static <T extends Comparable<T>> int binarySearch(T[] x, T key) { return binarySearch(x, 0, x.length- 1, key); } // 使用循环实现的二分查找 public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) { int low = 0; int high = x.length - 1; while (low <= high) { int mid = (low + high) >>> 1; int cmp = comp.compare(x[mid], key); if (cmp < 0) { low= mid + 1; } else if (cmp > 0) { high= mid - 1; } else { return mid; } } return -1; } // 使用递归实现的二分查找 private static<T extends Comparable<T>> int binarySearch(T[] x, int low, int high, T key) { if(low <= high) { int mid = low + ((high -low) >> 1); if(key.compareTo(x[mid])== 0) { return mid; } else if(key.compareTo(x[mid])< 0) { return binarySearch(x,low, mid - 1, key); } else { return binarySearch(x,mid + 1, high, key); } } return -1; } }
说明:上面的代码中给出了折半查找的两个版本,一个用递归实现,一个用循环实现。需要注意的是计算中间位置时不应该使用(high+ low) / 2的方式,因为加法运算可能导致整数越界,这里应该使用以下三种方式之一:low + (high - low) / 2或low + (high – low) >> 1或(low + high) >>> 1(>>>是逻辑右移,是不带符号位的右移)
96. 阐述Servlet和CGI的区别
Servlet与CGI的区别在于Servlet处于服务器进程中,它通过多线程方式运行其service()方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于Servlet。
补充:Sun Microsystems公司在1996年发布Servlet技术就是为了和CGI进行竞争,Servlet是一个特殊的Java程序,一个基于Java的Web应用通常包含一个或多个Servlet类。Servlet不能够自行创建并执行,它是在Servlet容器中运行的,容器将用户的请求传递给Servlet程序,并将Servlet的响应回传给用户。通常一个Servlet会关联一个或多个JSP页面。以前CGI经常因为性能开销上的问题被诟病,然而Fast CGI早就已经解决了CGI效率上的问题,所以面试的时候大可不必信口开河的诟病CGI,事实上有很多你熟悉的网站都使用了CGI技术。
97. Servlet接口中有哪些方法
Servlet接口定义了5个方法,其中前三个方法与Servlet生命周期相关:
- void init(ServletConfig config) throws ServletException
- void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException
- void destory()
- java.lang.String getServletInfo()
- ServletConfig getServletConfig()
Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其init()方法进行Servlet的初始化;请求到达时调用Servlet的service()方法,service()方法会根据需要调用与请求对应的doGet或doPost等方法;当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的destroy()方法。
98. 转发(forward)和重定向(redirect)的区别
forward是容器中控制权的转向,是服务器请求资源,服务器直接访问目标地址的URL,把那个URL 的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容是从哪儿来的,所以它的地址栏中还是原来的地址。redirect就是服务器端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,因此从浏览器的地址栏中可以看到跳转后的链接地址,很明显redirect无法访问到服务器保护起来资源,但是可以从一个网站redirect到其他网站。forward更加高效,所以在满足需要时尽量使用forward(通过调用RequestDispatcher对象的forward()方法,该对象可以通过ServletRequest对象的getRequestDispatcher()方法获得),并且这样也有助于隐藏实际的链接;在有些情况下,比如需要访问一个其它服务器上的资源,则必须使用重定向(通过HttpServletResponse对象调用其sendRedirect()方法实现)。
99. JSP有哪些内置对象?作用分别是什么
JSP有9个内置对象:
- request:封装客户端的请求,其中包含来自GET或POST请求的参数;
- response:封装服务器对客户端的响应;
- pageContext:通过该对象可以获取其他对象;
- session:封装用户会话的对象;
- application:封装服务器运行环境的对象;
- out:输出服务器响应的输出流对象;
- config:Web应用的配置对象;
- page:JSP页面本身(相当于Java程序中的this);
- exception:封装页面抛出异常的对象。
补充:如果用Servlet来生成网页中的动态内容无疑是非常繁琐的工作,另一方面,所有的文本和HTML标签都是硬编码,即使做出微小的修改,都需要进行重新编译。JSP解决了Servlet的这些问题,它是Servlet很好的补充,可以专门用作为用户呈现视图(View),而Servlet作为控制器(Controller)专门负责处理用户请求并转发或重定向到某个页面。基于Java的Web开发很多都同时使用了Servlet和JSP。JSP页面其实是一个Servlet,能够运行Servlet的服务器(Servlet容器)通常也是JSP容器,可以提供JSP页面的运行环境,Tomcat就是一个Servlet/JSP容器。第一次请求一个JSP页面时,Servlet/JSP容器首先将JSP页面转换成一个JSP页面的实现类,这是一个实现了JspPage接口或其子接口HttpJspPage的Java类。JspPage接口是Servlet的子接口,因此每个JSP页面都是一个Servlet。转换成功后,容器会编译Servlet类,之后容器加载和实例化Java字节码,并执行它通常对Servlet所做的生命周期操作。对同一个JSP页面的后续请求,容器会查看这个JSP页面是否被修改过,如果修改过就会重新转换并重新编译并执行。如果没有则执行内存中已经存在的Servlet实例。我们可以看一段JSP代码对应的Java程序就知道一切了,而且9个内置对象的神秘面纱也会被揭开。
JSP页面:
<%@ page pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE html> <html> <head> <base href="<%=basePath%>"> <title>首页</title> <style type="text/css"> * { font-family: "Arial"; } </style> </head> <body> <h1>Hello, World!</h1> <hr/> <h2>Current time is: <%= new java.util.Date().toString() %></h2> </body> </html>
对应的Java代码:
/* * Generated by the Jasper component of Apache Tomcat * Version: Apache Tomcat/7.0.52 * Generated at: 2014-10-13 13:28:38 UTC * Note: The last modified time of this file was set to * the last modified time of the source file after * generation to assist with modification tracking. */ package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent { private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory .getDefaultFactory(); private static java.util.Map<java.lang.String, java.lang.Long> _jspx_dependants; private javax.el.ExpressionFactory _el_expressionfactory; private org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map<java.lang.String, java.lang.Long> getDependants() { return _jspx_dependants; } public void _jspInit() { _el_expressionfactory = _jspxFactory.getJspApplicationContext( getServletConfig().getServletContext()).getExpressionFactory(); _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory .getInstanceManager(getServletConfig()); } public void _jspDestroy() { } public void _jspService( final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { // 内置对象就是在这里定义的 final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; out.write(‘\r‘); out.write(‘\n‘); String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; // 以下代码通过输出流将HTML标签输出到浏览器中 out.write("\r\n"); out.write("\r\n"); out.write("<!DOCTYPE html>\r\n"); out.write("<html>\r\n"); out.write(" <head>\r\n"); out.write(" <base href=\""); out.print(basePath); out.write("\">\r\n"); out.write(" <title>首页</title>\r\n"); out.write(" <style type=\"text/css\">\r\n"); out.write(" \t* { font-family: \"Arial\"; }\r\n"); out.write(" </style>\r\n"); out.write(" </head>\r\n"); out.write(" \r\n"); out.write(" <body>\r\n"); out.write(" <h1>Hello, World!</h1>\r\n"); out.write(" <hr/>\r\n"); out.write(" <h2>Current time is: "); out.print(new java.util.Date().toString()); out.write("</h2>\r\n"); out.write(" </body>\r\n"); out.write("</html>\r\n"); } catch (java.lang.Throwable t) { if (!(t instanceof javax.servlet.jsp.SkipPageException)) { out = _jspx_out; if (out != null && out.getBufferSize() != 0) try { out.clearBuffer(); } catch (java.io.IOException e) { } if (_jspx_page_context != null) _jspx_page_context.handlePageException(t); else throw new ServletException(t); } } finally { _jspxFactory.releasePageContext(_jspx_page_context); } } }
100. get和post请求的区别
①get请求用来从服务器上获得资源,而post是用来向服务器提交数据;
②get将表单中数据按照name=value的形式,添加到action 所指向的URL 后面,并且两者使用"?"连接,而各个变量之间使用"&"连接;post是将表单中的数据放在HTTP协议的请求头或消息体中,传递到action所指向URL;
③get传输的数据要受到URL长度限制(1024字节);而post可以传输大量的数据,上传文件通常要使用post方式;
④使用get时参数会显示在地址栏上,如果这些数据不是敏感数据,那么可以使用get;对于敏感数据还是应用使用post;
⑤get使用MIME类型application/x-www-form-urlencoded的URL编码(也叫百分号编码)文本的格式传递参数,保证被传送的参数由遵循规范的文本组成,例如一个空格的编码是"%20"。
101. 常用的Web服务器有哪些
Unix和Linux平台下使用最广泛的免费HTTP服务器是Apache服务器,而Windows平台的服务器通常使用IIS作为Web服务器。选择Web服务器应考虑的因素有:性能、安全性、日志和统计、虚拟主机、代理服务器、缓冲服务和集成应用程序等。下面是对常见服务器的简介:
- IIS:Microsoft的Web服务器产品,全称是Internet Information Services。IIS是允许在公共Intranet或Internet上发布信息的Web服务器。IIS是目前最流行的Web服务器产品之一,很多著名的网站都是建立在IIS的平台上。IIS提供了一个图形界面的管理工具,称为Internet服务管理器,可用于监视配置和控制Internet服务。IIS是一种Web服务组件,其中包括Web服务器、FTP服务器、NNTP服务器和SMTP服务器,分别用于网页浏览、文件传输、新闻服务和邮件发送等方面,它使得在网络(包括互联网和局域网)上发布信息成了一件很容易的事。它提供ISAPI(Intranet Server API)作为扩展Web服务器功能的编程接口;同时,它还提供一个Internet数据库连接器,可以实现对数据库的查询和更新。
- Kangle:Kangle Web服务器是一款跨平台、功能强大、安全稳定、易操作的高性能Web服务器和反向代理服务器软件。此外,Kangle也是一款专为做虚拟主机研发的Web服务器。实现虚拟主机独立进程、独立身份运行。用户之间安全隔离,一个用户出问题不影响其他用户。支持PHP、ASP、ASP.NET、Java、Ruby等多种动态开发语言。
- WebSphere:WebSphere Application Server是功能完善、开放的Web应用程序服务器,是IBM电子商务计划的核心部分,它是基于Java的应用环境,用于建立、部署和管理Internet和Intranet Web应用程序,适应各种Web应用程序服务器的需要。
- WebLogic:WebLogic Server是一款多功能、基于标准的Web应用服务器,为企业构建企业应用提供了坚实的基础。针对各种应用开发、关键性任务的部署,各种系统和数据库的集成、跨Internet协作等Weblogic都提供了相应的支持。由于它具有全面的功能、对开放标准的遵从性、多层架构、支持基于组件的开发等优势,很多公司的企业级应用都选择它来作为开发和部署的环境。WebLogic Server在使应用服务器成为企业应用架构的基础方面一直处于领先地位,为构建集成化的企业级应用提供了稳固的基础。
- Apache:目前Apache仍然是世界上用得最多的Web服务器,其市场占有率很长时间都保持在60%以上(目前的市场份额约40%左右)。世界上很多著名的网站都是Apache的产物,它的成功之处主要在于它的源代码开放、有一支强大的开发团队、支持跨平台的应用(可以运行在几乎所有的Unix、Windows、Linux系统平台上)以及它的可移植性等方面。
- Tomcat:Tomcat是一个开放源代码、运行Servlet和JSP的容器。Tomcat实现了Servlet和JSP规范。此外,Tomcat还实现了Apache-Jakarta规范而且比绝大多数商业应用软件服务器要好,因此目前也有不少的Web服务器都选择了Tomcat。
- Nginx:读作"engine x",是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP代理服务器。 Nginx是由Igor Sysoev为俄罗斯访问量第二的Rambler站点开发的,第一个公开版本0.1.0发布于2004年10月4日。其将源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。在2014年下半年,Nginx的市场份额达到了14%。
102. JSP和Servlet是什么关系
其实这个问题在上面已经阐述过了,Servlet是一个特殊的Java程序,它运行于服务器的JVM中,能够依靠服务器的支持向浏览器提供显示内容。JSP本质上是Servlet的一种简易形式,JSP会被服务器处理成一个类似于Servlet的Java程序,可以简化页面内容的生成。Servlet和JSP最主要的不同点在于,Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML分离开来。而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。有人说,Servlet就是在Java中写HTML,而JSP就是在HTML中写Java代码,当然这个说法是很片面且不够准确的。JSP侧重于视图,Servlet更侧重于控制逻辑,在MVC架构模式中,JSP适合充当视图(view)而Servlet适合充当控制器(controller)。
103. 讲解JSP中的四种作用域
JSP中的四种作用域包括page、request、session和application,具体来说:
- page代表与一个页面相关的对象和属性。
- request代表与Web客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域。
- session代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。
- application代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。
104. 如何实现JSP或Servlet的单线程模式
对于JSP页面,可以通过page指令进行设置。
<%@page isThreadSafe=”false”%>
对于Servlet,可以让自定义的Servlet实现SingleThreadModel标识接口。
说明:如果将JSP或Servlet设置成单线程工作模式,会导致每个请求创建一个Servlet实例,这种实践将导致严重的性能问题(服务器的内存压力很大,还会导致频繁的垃圾回收),所以通常情况下并不会这么做。
105. 实现会话跟踪的技术有哪些
由于HTTP协议本身是无状态的,服务器为了区分不同的用户,就需要对用户会话进行跟踪,简单的说就是为用户进行登记,为用户分配唯一的ID,下一次用户在请求中包含此ID,服务器据此判断到底是哪一个用户。
①URL 重写:在URL中添加用户会话的信息作为请求的参数,或者将唯一的会话ID添加到URL结尾以标识一个会话。
②设置表单隐藏域:将和会话跟踪相关的字段添加到隐式表单域中,这些信息不会在浏览器中显示但是提交表单时会提交给服务器。
这两种方式很难处理跨越多个页面的信息传递,因为如果每次都要修改URL或在页面中添加隐式表单域来存储用户会话相关信息,事情将变得非常麻烦。③cookie:cookie有两种,一种是基于窗口的,浏览器窗口关闭后,cookie就没有了;另一种是将信息存储在一个临时文件中,并设置存在的时间。当用户通过浏览器和服务器建立一次会话后,会话ID就会随响应信息返回存储在基于窗口的cookie中,那就意味着只要浏览器没有关闭,会话没有超时,下一次请求时这个会话ID又会提交给服务器让服务器识别用户身份。会话中可以为用户保存信息。会话对象是在服务器内存中的,而基于窗口的cookie是在客户端内存中的。如果浏览器禁用了cookie,那么就需要通过下面两种方式进行会话跟踪。当然,在使用cookie时要注意几点:首先不要在cookie中存放敏感信息;其次cookie存储的数据量有限(4k),不能将过多的内容存储cookie中;再者浏览器通常只允许一个站点最多存放20个cookie。当然,和用户会话相关的其他信息(除了会话ID)也可以存在cookie方便进行会话跟踪。
④HttpSession:在所有会话跟踪技术中,HttpSession对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建HttpSession,每个用户可以访问他自己的HttpSession。可以通过HttpServletRequest对象的getSession方法获得HttpSession,通过HttpSession的setAttribute方法可以将一个值放在HttpSession中,通过调用HttpSession对象的getAttribute方法,同时传入属性名就可以获取保存在HttpSession中的对象。与上面三种方式不同的是,HttpSession放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的Servlet容器可以在内存将满时将HttpSession中的对象移到其他存储设备中,但是这样势必影响性能。添加到HttpSession中的值可以是任意Java对象,这个对象最好实现了Serializable接口,这样Servlet容器在必要的时候可以将其序列化到文件中,否则在序列化时就会出现异常。
106. 过滤器有哪些作用和用法
Java Web开发中的过滤器(filter)是从Servlet 2.3规范开始增加的功能,并在Servlet 2.4规范中得到增强。对Web应用来说,过滤器是一个驻留在服务器端的Web组件,它可以截取客户端和服务器之间的请求与响应信息,并对这些信息进行过滤。当Web容器接受到一个对资源的请求时,它将判断是否有过滤器与这个资源相关联。如果有,那么容器将把请求交给过滤器进行处理。在过滤器中,你可以改变请求的内容,或者重新设置请求的报头信息,然后再将请求发送给目标资源。当目标资源对请求作出响应时候,容器同样会将响应先转发给过滤器,在过滤器中你可以对响应的内容进行转换,然后再将响应发送到客户端。
常见的过滤器用途主要包括:对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、转换图象格式、对响应内容进行压缩以减少传输量、对请求或响应进行加解密处理、触发资源访问事件、对XML的输出应用XSLT等。
和过滤器相关的接口主要有:Filter、FilterConfig和FilterChain。
编码过滤器的例子:
import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.annotation.WebInitParam; @WebFilter(urlPatterns = { "*" }, initParams = {@WebInitParam(name="encoding", value="utf-8")}) public class CodingFilter implements Filter { private String defaultEncoding = "utf-8"; @Override public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(defaultEncoding); resp.setCharacterEncoding(defaultEncoding); chain.doFilter(req, resp); } @Override public void init(FilterConfig config) throws ServletException { String encoding = config.getInitParameter("encoding"); if (encoding != null) { defaultEncoding = encoding; } } }
下载计数过滤器的例子:
import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; @WebFilter(urlPatterns = {"/*"}) public class DownloadCounterFilter implements Filter { private ExecutorService executorService = Executors.newSingleThreadExecutor(); private Properties downloadLog; private File logFile; @Override public void destroy() { executorService.shutdown(); } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; final String uri = request.getRequestURI(); executorService.execute(new Runnable() { @Override public void run() { String value = downloadLog.getProperty(uri); if(value == null) { downloadLog.setProperty(uri, "1"); } else { int count = Integer.parseInt(value); downloadLog.setProperty(uri, String.valueOf(++count)); } try { downloadLog.store(new FileWriter(logFile), ""); } catch (IOException e) { e.printStackTrace(); } } }); chain.doFilter(req, resp); } @Override public void init(FilterConfig config) throws ServletException { String appPath = config.getServletContext().getRealPath("/"); logFile = new File(appPath, "downloadLog.txt"); if(!logFile.exists()) { try { logFile.createNewFile(); } catch(IOException e) { e.printStackTrace(); } } downloadLog = new Properties(); try { downloadLog.load(new FileReader(logFile)); } catch (IOException e) { e.printStackTrace(); } } }
说明:这里使用了Servlet 3规范中的注解来部署过滤器,当然也可以在web.xml中使用和<filter-mapping>标签部署过滤器,如108题中所示。
107. 监听器有哪些作用和用法
Java Web开发中的监听器(listener)就是application、session、request三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件,如下所示:
①ServletContextListener:对Servlet上下文的创建和销毁进行监听。
②ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换。
③HttpSessionListener:对Session的创建和销毁进行监听。
补充:session的销毁有两种情况:1). session超时(可以在web.xml中通过<session-config>/<session-timeout>标签配置超时时间);2). 通过调用session对象的invalidate()方法使session失效。
④HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听。
⑤ServletRequestListener:对请求对象的初始化和销毁进行监听。
⑥ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。
下面是一个统计网站最多在线人数监听器的例子。
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; /** 上下文监听器,在服务器启动时初始化onLineCount和maxOnLineCount两个变量 并将其置于服务器上下文(ServletContext)中,其初始值都是0 */ @WebListener public class InitListener implements ServletContextListener { @Override public void contextDestroyed(ServletContextEvent evt) { } @Override public void contextInitialized(ServletContextEvent evt) { evt.getServletContext().setAttribute("onLineCount", 0); evt.getServletContext().setAttribute("maxOnLineCount", 0); } } import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import javax.servlet.ServletContext; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; /** 会话监听器,在用户会话创建和销毁的时候根据情况 修改onLineCount和maxOnLineCount的值 */ @WebListener public class MaxCountListener implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent event) { ServletContext ctx = event.getSession().getServletContext(); int count = Integer.parseInt(ctx.getAttribute("onLineCount").toString()); count++; ctx.setAttribute("onLineCount", count); int maxOnLineCount = Integer.parseInt(ctx.getAttribute("maxOnLineCount").toString()); if (count > maxOnLineCount) { ctx.setAttribute("maxOnLineCount", count); DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); ctx.setAttribute("date", df.format(new Date())); } } @Override public void sessionDestroyed(HttpSessionEvent event) { ServletContext app = event.getSession().getServletContext(); int count = Integer.parseInt(app.getAttribute("onLineCount").toString()); count--; app.setAttribute("onLineCount", count); } }
说明:这里使用了Servlet 3规范中的@WebListener注解配置监听器,当然你可以在web.xml文件中用标签配置监听器,如108题中所示。
108. web.xml文件中可以配置哪些内容
web.xml用于配置Web应用的相关信息,如:监听器(listener)、过滤器(filter)、 Servlet、相关参数、会话超时时间、安全验证方式、错误页面等,下面是一些开发中常见的配置:
①配置Spring上下文加载监听器加载Spring配置文件并创建IoC容器:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener>
②配置Spring的OpenSessionInView过滤器来解决延迟加载和Hibernate会话关闭的矛盾:
<filter> <filter-name>openSessionInView</filter-name> <filter-class> org.springframework.orm.hibernate3.support.OpenSessionInViewFilter </filter-class> </filter> <filter-mapping> <filter-name>openSessionInView</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
③配置会话超时时间为10分钟:
<session-config> <session-timeout>10</session-timeout> </session-config>
④配置404和Exception的错误页面:
<error-page> <error-code>404</error-code> <location>/error.jsp</location> </error-page> <error-page> <exception-type>java.lang.Exception</exception-type> <location>/error.jsp</location> </error-page>
⑤配置安全认证方式:
<security-constraint> <web-resource-collection> <web-resource-name>ProtectedArea</web-resource-name> <url-pattern>/admin/*</url-pattern> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> </login-config> <security-role> <role-name>admin</role-name> </security-role>
说明:对Servlet(小服务)、Listener(监听器)和Filter(过滤器)等Web组件的配置,Servlet 3规范提供了基于注解的配置方式,可以分别使用@WebServlet、@WebListener、@WebFilter注解进行配置。
补充:如果Web提供了有价值的商业信息或者是敏感数据,那么站点的安全性就是必须考虑的问题。安全认证是实现安全性的重要手段,认证就是要解决“Are you who you say you are?”的问题。认证的方式非常多,简单说来可以分为三类:
A. What you know? — 口令
B. What you have? — 数字证书(U盾、密保卡)
C. Who you are? — 指纹识别、虹膜识别
在Tomcat中可以通过建立安全套接字层(Secure Socket Layer, SSL)以及通过基本验证或表单验证来实现对安全性的支持。
109. 你的项目中使用过哪些JSTL标签
项目中主要使用了JSTL的核心标签库,包括<c:if>、<c:choose>、<c: when>、<c: otherwise>、<c:forEach>等,主要用于构造循环和分支结构以控制显示逻辑。
说明:虽然JSTL标签库提供了core、sql、fmt、xml等标签库,但是实际开发中建议只使用核心标签库(core),而且最好只使用分支和循环标签并辅以表达式语言(EL),这样才能真正做到数据显示和业务逻辑的分离,这才是最佳实践。
110. 使用标签库有什么好处?如何自定义JSP标签
使用标签库的好处包括以下几个方面:
- 分离JSP页面的内容和逻辑,简化了Web开发;
- 开发者可以创建自定义标签来封装业务逻辑和显示逻辑;
- 标签具有很好的可移植性、可维护性和可重用性;
- 避免了对Scriptlet(小脚本)的使用(很多公司的项目开发都不允许在JSP中书写小脚本)
自定义JSP标签包括以下几个步骤:
- 编写一个Java类实现实现Tag/BodyTag/IterationTag接口(开发中通常不直接实现这些接口而是继承TagSupport/BodyTagSupport/SimpleTagSupport类,这是对缺省适配模式的应用),重写doStartTag()、doEndTag()等方法,定义标签要完成的功能
- 编写扩展名为tld的标签描述文件对自定义标签进行部署,tld文件通常放在WEB-INF文件夹下或其子目录中
- 在JSP页面中使用taglib指令引用该标签库
下面是一个自定义标签库的例子。
步骤1 - 标签类源代码TimeTag.java:
package com.jackfrued.tags; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.TagSupport; public class TimeTag extends TagSupport { private static final long serialVersionUID = 1L; private String format = "yyyy-MM-dd hh:mm:ss"; private String foreColor = "black"; private String backColor = "white"; public int doStartTag() throws JspException { SimpleDateFormat sdf = new SimpleDateFormat(format); JspWriter writer = pageContext.getOut(); StringBuilder sb = new StringBuilder(); sb.append(String.format("<span style=‘color:%s;background-color:%s‘>%s</span>", foreColor, backColor, sdf.format(new Date()))); try { writer.print(sb.toString()); } catch(IOException e) { e.printStackTrace(); } return SKIP_BODY; } public void setFormat(String format) { this.format = format; } public void setForeColor(String foreColor) { this.foreColor = foreColor; } public void setBackColor(String backColor) { this.backColor = backColor; } }
步骤2 - 编写标签库描述文件my.tld:
<?xml version="1.0" encoding="UTF-8" ?> <taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0"> <description>定义标签库</description> <tlib-version>1.0</tlib-version> <short-name>MyTag</short-name> <tag> <name>time</name> <tag-class>com.jackfrued.tags.TimeTag</tag-class> <body-content>empty</body-content> <attribute> <name>format</name> <required>false</required> </attribute> <attribute> <name>foreColor</name> </attribute> <attribute> <name>backColor</name> </attribute> </tag> </taglib>
步骤3 - 在JSP页面中使用自定义标签:
<%@ page pageEncoding="UTF-8"%> <%@ taglib prefix="my" uri="/WEB-INF/tld/my.tld" %> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE html> <html> <head> <base href="<%=basePath%>"> <title>首页</title> <style type="text/css"> * { font-family: "Arial"; font-size:72px; } </style> </head> <body> <my:time format="yyyy-MM-dd" backColor="blue" foreColor="yellow"/> </body> </html>
提示:如果要将自定义的标签库发布成JAR文件,需要将标签库描述文件(tld文件)放在JAR文件的META-INF目录下,可以JDK中的jar工具完成JAR文件的生成,如果不清楚如何操作,可以请教谷老师和百老师。
111. 说一下表达式语言(EL)的隐式对象及其作用
EL的隐式对象包括:pageContext、initParam(访问上下文参数)、param(访问请求参数)、paramValues、header(访问请求头)、headerValues、cookie(访问cookie)、applicationScope(访问application作用域)、sessionScope(访问session作用域)、requestScope(访问request作用域)、pageScope(访问page作用域)。
用法如下所示:
${pageContext.request.method} ${pageContext["request"]["method"]} ${pageContext.request["method"]} ${pageContext["request"].method} ${initParam.defaultEncoding} ${header["accept-language"]} ${headerValues["accept-language"][0]} ${cookie.jsessionid.value} ${sessionScope.loginUser.username}
补充:表达式语言的.和[]运算作用是一致的,唯一的差别在于如果访问的属性名不符合Java标识符命名规则,例如上面的accept-language就不是一个有效的Java标识符,那么这时候就只能用[]运算符而不能使用.运算符获取它的值
112. 表达式语言(EL)支持哪些运算符
除了.和[]运算符,EL还提供了:
- 算术运算符:+、-、*、/或div、%或mod
- 关系运算符:==或eq、!=或ne、>或gt、>=或ge、<或lt、<=或le
- 逻辑运算符:&&或and、||或or、!或not
- 条件运算符:${statement? A : B}(跟Java的条件运算符类似)
- empty运算符:检查一个值是否为null或者空(数组长度为0或集合中没有元素也返回true)
113. Java Web开发的Model 1和Model 2分别指的是什么
Model 1是以页面为中心的Java Web开发,使用JSP+JavaBean技术将页面显示逻辑和业务逻辑处理分开,JSP实现页面显示,JavaBean对象用来保存数据和实现业务逻辑。Model 2是基于MVC(模型-视图-控制器,Model-View-Controller)架构模式的开发模型,实现了模型和视图的彻底分离,利于团队开发和代码复用,如下图所示。
114. Servlet 3中的异步处理指的是什么
在Servlet 3中引入了一项新的技术可以让Servlet异步处理请求。有人可能会质疑,既然都有多线程了,还需要异步处理请求吗?答案是肯定的,因为如果一个任务处理时间相当长,那么Servlet或Filter会一直占用着请求处理线程直到任务结束,随着并发用户的增加,容器将会遭遇线程超出的风险,这这种情况下很多的请求将会被堆积起来而后续的请求可能会遭遇拒绝服务,直到有资源可以处理请求为止。异步特性可以帮助应用节省容器中的线程,特别适合执行时间长而且用户需要得到结果的任务,如果用户不需要得到结果则直接将一个Runnable对象交给Executor并立即返回即可。
补充:多线程在Java诞生初期无疑是一个亮点,而Servlet单实例多线程的工作方式也曾为其赢得美名,然而技术的发展往往会颠覆我们很多的认知,就如同当年爱因斯坦的相对论颠覆了牛顿的经典力学一般。事实上,异步处理绝不是Serlvet 3首创,如果你了解Node.js的话,对Servlet 3的这个重要改进就不以为奇了。
下面是一个支持异步处理请求的Servlet的例子。
import java.io.IOException; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet(urlPatterns = {"/async"}, asyncSupported = true) public class AsyncServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 开启Tomcat异步Servlet支持 req.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true); final AsyncContext ctx = req.startAsync(); // 启动异步处理的上下文 // ctx.setTimeout(30000); ctx.start(new Runnable() { @Override public void run() { // 在此处添加异步处理的代码 ctx.complete(); } }); } }
115. 如何在基于Java的Web项目中实现文件上传和下载
在Sevlet 3 以前,Servlet API中没有支持上传功能的API,因此要实现上传功能需要引入第三方工具从POST请求中获得上传的附件或者通过自行处理输入流来获得上传的文件,我们推荐使用Apache的commons-fileupload。
从Servlet 3开始,文件上传变得无比简单,相信看看下面的例子一切都清楚了。上传页面index.jsp:
<%@ page pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Photo Upload</title> </head> <body> <h1>Select your photo and upload</h1> <hr/> <div style="color:red;font-size:14px;">${hint}</div> <form action="UploadServlet" method="post" enctype="multipart/form-data"> Photo file: <input type="file" name="photo" /> <input type="submit" value="Upload" /> </form> </body> </html>
支持上传的Servlet:
package com.jackfrued.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; @WebServlet("/UploadServlet") @MultipartConfig public class UploadServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 可以用request.getPart()方法获得名为photo的上传附件 // 也可以用request.getParts()获得所有上传附件(多文件上传) // 然后通过循环分别处理每一个上传的文件 Part part = request.getPart("photo"); if (part != null && part.getSubmittedFileName().length() > 0) { // 用ServletContext对象的getRealPath()方法获得上传文件夹的绝对路径 String savePath = request.getServletContext().getRealPath("/upload"); // Servlet 3.1规范中可以用Part对象的getSubmittedFileName()方法获得上传的文件名 // 更好的做法是为上传的文件进行重命名(避免同名文件的相互覆盖) part.write(savePath + "/" + part.getSubmittedFileName()); request.setAttribute("hint", "Upload Successfully!"); } else { request.setAttribute("hint", "Upload failed!"); } // 跳转回到上传页面 request.getRequestDispatcher("index.jsp").forward(request, response); } }
116. 服务器收到用户提交的表单数据,到底是调用Servlet的doGet()还是doPost()方法
HTML的
元素有一个method属性,用来指定提交表单的方式,其值可以是get或post。我们自定义的Servlet一般情况下会重写doGet()或doPost()两个方法之一或全部,如果是GET请求就调用doGet()方法,如果是POST请求就调用doPost()方法,那为什么为什么这样呢?我们自定义的Servlet通常继承自HttpServlet,HttpServlet继承自GenericServlet并重写了其中的service()方法,这个方法是Servlet接口中定义的。HttpServlet重写的service()方法会先获取用户请求的方法,然后根据请求方法调用doGet()、doPost()、doPut()、doDelete()等方法,如果在自定义Servlet中重写了这些方法,那么显然会调用重写过的(自定义的)方法,这显然是对模板方法模式的应用(如果不理解,请参考阎宏博士的《Java与模式》一书的第37章)。当然,自定义Servlet中也可以直接重写service()方法,那么不管是哪种方式的请求,都可以通过自己的代码进行处理,这对于不区分请求方法的场景比较合适。
117. JSP中的静态包含和动态包含有什么区别
静态包含是通过JSP的include指令包含页面,动态包含是通过JSP标准动作<jsp:forward>包含页面。静态包含是编译时包含,如果包含的页面不存在则会产生编译错误,而且两个页面的"contentType"属性应保持一致,因为两个页面会合二为一,只产生一个class文件,因此被包含页面发生的变动再包含它的页面更新前不会得到更新。动态包含是运行时包含,可以向被包含的页面传递参数,包含页面和被包含页面是独立的,会编译出两个class文件,如果被包含的页面不存在,不会产生编译错误,也不影响页面其他部分的执行。代码如下所示:
<%-- 静态包含 --%> <%@ include file="..." %> <%-- 动态包含 --%> <jsp:include page="..."> <jsp:param name="..." value="..." /> </jsp:include>
118. Servlet中如何获取用户提交的查询参数或表单数据
可以通过请求对象(HttpServletRequest)的getParameter()方法通过参数名获得参数值。如果有包含多个值的参数(例如复选框),可以通过请求对象的getParameterValues()方法获得。当然也可以通过请求对象的getParameterMap()获得一个参数名和参数值的映射(Map)。
119. Servlet中如何获取用户配置的初始化参数以及服务器上下文参数
可以通过重写Servlet接口的init(ServletConfig)方法并通过ServletConfig对象的getInitParameter()方法来获取Servlet的初始化参数。可以通过ServletConfig对象的getServletContext()方法获取ServletContext对象,并通过该对象的getInitParameter()方法来获取服务器上下文参数。当然,ServletContext对象也在处理用户请求的方法(如doGet()方法)中通过请求对象的getServletContext()方法来获得。
120. 如何设置请求的编码以及响应内容的类型
通过请求对象(ServletRequest)的setCharacterEncoding(String)方法可以设置请求的编码,其实要彻底解决乱码问题就应该让页面、服务器、请求和响应、Java程序都使用统一的编码,最好的选择当然是UTF-8;通过响应对象(ServletResponse)的setContentType(String)方法可以设置响应内容的类型,当然也可以通过HttpServletResponsed对象的setHeader(String, String)方法来设置。