一、Allatori Allatori 是一个 Java 混淆器，它属于第二代混淆器，因此它能够全方位地保护你的知识产权。 Allatori 具有以下几种保护方式：命名混淆，流混淆，调试信息混淆，字符串混淆，以及水印技术。对于教育和非商业项目来说这个混淆器是免费的。支持 war 和 jar 文件格式，并且允许对需要混淆代码的应用程序添加有效日期。 有项目需要对代码进行保护，比较初级的方案就是对代码进行混淆，打包之后的文件进行反编译后，就可以看到效果。此外，使用 Allatori 打的包体积也会小一点。 官网：http://www.allatori.com/ 二、简单混淆 解压找到目录: Allatori-8.6-Demo\tutorial\step01\files。 Clean.bat config.xml 配置文件 mousegestures-1.2.jar MouseGesturesObfuscated.bat MouseGesturesOriginal.bat RunAllatori.bat 双击生成混淆的jar test.jar 混淆前的jar 如果你只有一个项目，并且混淆次数也不多，你也可以通过这种方式，把项目拷贝到这个目录，修改配置文件，手动执行混淆的脚本。 三、多项目集成idea 2.1、lib 文件夹添加 allatori.jar allatori-annotations.jar 2.2、pom.xml <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>2.6</version> <executions> <execution> <id>copy-and-filter-allatori-config</id> <phase>package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${basedir}/target</outputDirectory> <resources> <resource> <directory>src/main/resources/config</directory> <includes> <include>allatori.xml</include> </includes> <filtering>true</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <executions> <execution> <id>run-allatori</id> <phase>package</phase> <goals> <goal>exec</goal> </goals> </execution> </executions> <configuration> <executable>java</executable> <arguments> <argument>-Xms128m</argument> <argument>-Xmx512m</argument> <argument>-jar</argument> <argument>../lib/allatori.jar</argument> <argument>${basedir}/target/allatori.xml</argument> </arguments> </configuration> </plugin> </plugins> </build> 2.3、allatori.xml <config> <jar in="web-api-1.0.jar" out="web-obfuscated.jar"/> <ignore-classes> <class template="class *Application*"/> <class template="class *springframework*"/> <class template="class *alibaba*"/> <class template="class *persistence*"/> <class template="class *apache*"/> <class template="class *model*"/> <class template="class *enums*"/> <class template="class *repository*"/> </ignore-classes> <keep-names> <method template="*(**)" parameters="keep"/> <!-- <class template="class com.a.b.c.*"/>--> </keep-names> <property name="log-file" value="log.xml"/> </config> 打包 mvn package -DskipTests 。
线程池 ThreadPoolExecutor public class ThreadPoolExample { public static void main(String[] args) { // 创建自定义线程工厂 ThreadFactory customeThreadFactory = new ThreadFactory() { private final AtomicInteger count = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("自定义线程-" + count.getAndIncrement()); return thread; } }; ThreadPoolExecutor executor = new ThreadPoolExecutor( 1, // 核心线程数 2, // 最大线程数 60L, // 临时线程空闲存活时间 TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),// 队列容量 customeThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy()// 触发拒绝策略（CallerRunsPolicy） ); // 提交任务 for (int i = 0; i < 10; i++) { final int taskId = i; executor.submit(() -> { System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId); return taskId; }); } // 关闭线程池 executor.shutdown(); } } 处理流程： 第1个任务： 创建核心线程处理（1个核心线程） 第2、3个任务： 进入队列等待（队列容量2个） 第4个任务： 创建临时线程处理（最大线程数2 - 核心线程数1 = 1个临时线程） 第5个任务： 触发拒绝策略（CallerRunsPolicy） 在调用者线程中执行 临时线程： keepAliveTime = 60L // 临时线程空闲存活时间 临时线程在空闲超过60秒后会被回收。 核心线程默认不回收。 可以通过 allowCoreThreadTimeOut(true) 允许回收核心线程。 如何设置合适的参数： 如何设置合适的参数 核心线程数设置考虑因素： CPU密集型：通常设置为CPU核心数+1 IO密集型：可以设置为CPU核心数*2 混合型：CPU核心数（1+平均等待时间/平均工作时间） 最大线程数设置考虑因素： 系统内存大小 任务的响应时间要求 任务的类型（CPU密集或IO密集） 示例配置 // CPU密集型配置 int cpuCores = Runtime.getRuntime().availableProcessors(); ThreadPoolExecutor cpuIntensiveExecutor = new ThreadPoolExecutor( cpuCores + 1, // 核心线程数 cpuCores + 1, // 最大线程数 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000) ); // IO密集型配置 ThreadPoolExecutor ioIntensiveExecutor = new ThreadPoolExecutor( cpuCores * 2, // 核心线程数 cpuCores * 4, // 最大线程数 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2000) ); 核心线程用于处理正常负载。 临时线程用于处理突发负载。 参数设置需要根据实际业务场景调整和测试。 CallerRunsPolicy 的作用： 流量控制 当线程池满载时，让调用者自己执行任务。 这会降低任务提交的速度。 起到自然的背压作用。 防止任务丢失 不像 DiscardPolicy 直接丢弃任务。 不像 AbortPolicy 抛出异常。 确保任务最终会被执行。 优缺点 优点：不会丢失任务。 缺点：可能会影响调用者线程的响应时间。
1、新建好 web 模块后，在 module 里面要添加进去 2、libraries 下点 +，选择 java，把这个模块的 lib 目录添加，lib 中包含所需要的的各种 jar 包 3、生成 war exploded 包，选择你要运用到的模块 4、facets 中添加 web 模块，确认好路径，path 要定位到 web.xml 目录下，web resource 要定位到 web 目录下 5、配置 tomcat 命名推荐改为你当前的模块/项目名。 deployment 选择上面步骤生成的 war 包。 url 中 8080/ 后的目录要和 deployment 里面 applicationContext 吻合。
一、PO : persistent object 持久对象 1、有时候也被为 Data 对象，对应数据库中的 entity，可以简单认为一个 PO 对应数据库中的一条记录。 2、在 hibernate 持久化框架中与 insert/delet 操作密切相关。 3、PO 中不应该包含任何对数据库的操作。 持久化对象，它跟持久层（通常是关系型数据库）的数据结构形成一一对应的映射关系，如果持久层是关系型数据库，那么，数据表中的每个字段（或若干个）就对应 PO 的一个（或若干个）属性。 二、POJO : plain ordinary java object 无规则简单 java 对象 使用 POJO 名称是为了避免和 EJB 混淆起来, 而且简称比较直接。其中有一些属性及其 getter setter 方法的类，没有业务逻辑，有时可以作为 VO(value-object) 或 DTO(Data Transform Object) 来使用.当然，如果你有一个简单的运算属性也是可以的，但不允许有业务方法，也不能携带有 connection 之类的方法。 POJO 是 Plain Old Java Objects 的缩写不错，但是它通指没有使用 Entity Beans 的普通 java 对象，可以把 POJO 作为支持业务逻辑的协助类。 POJO 有一些 private 的参数作为对象的属性。然后针对每个参数定义了 get 和 set 方法作为访问的接口。 POJO 是 MartinFowler 等发明的一个术语，用来表示普通的 Java 对象，不是 JavaBean, EntityBean 或者 SessionBean。 POJO 不担当任何特殊的角色，也不实现任何特殊的 Java 框架的接口如，EJB， JDBC 等等。 即 POJO 是一个简单的普通的 Java 对象，它不包含业务逻辑或持久逻辑等，但不是 JavaBean、EntityBean 等，不具有任何特殊角色和不继承或不实现任何其它 Java 框架的类或接口。 一个中间对象，可以转化为 PO、DTO、VO。 1、POJO 持久化之后 ==〉PO 在运行期，由 Hibernate 中的 cglib 动态把 POJO 转换为 PO，PO 相对于 POJO 会增加一些用来管理数据库 entity 状态的属性和方法。PO 对于 programmer 来说完全透明，由于是运行期生成 PO，所以可以支持增量编译，增量调试。 2、POJO 传输过程中 ==〉DTO 3、POJO 用作表示层 ==〉VO 三、BO : business object 业务对象 封装业务逻辑为一个对象（可以包括多个 PO，通常需要将 BO 转化成 PO，才能进行数据的持久化，反之，从 DB 中得到的 PO，需要转化成 BO 才能在业务层使用）。 关于 BO 主要有三种概念 1、只包含业务对象的属性； 2、只包含业务方法； 3、两者都包含。 在实际使用中，认为哪一种概念正确并不重要，关键是实际应用中适合自己项目的需要。 四、VO : value object 值对象 / view object表现层对象 1、主要对应页面显示（web 页面 / swt、swing 界面）的数据对象。 2、可以和表对应，也可以不，这根据业务的需要。 注 ：在 struts 中，用 ActionForm 做 VO，需要做一个转换，因为PO是面向对象的，而 ActionForm 是和 view 对应的，要将几个 PO 要显示的属性合成一个 ActionForm，可以使用 BeanUtils 的 copy 方法。 五、DTO（TO） : Data Transfer Object 数据传输对象 1、用在需要跨进程或远程传输时，它不应该包含业务逻辑。 2、比如一张表有100个字段，那么对应的PO就有100个属性（大多数情况下，DTO 内的数据来自多个表）。但 view 层只需显示10个字段，没有必要把整个 PO 对象传递到 client，这时我们就可以用只有这10个属性的 DTO 来传输数据到 client，这样也不会暴露 server 端表结构。到达客户端以后，如果用这个对象来对应界面显示，那此时它的身份就转为 VO。 六、DAO : data access object 数据访问对象 1、主要用来封装对 DB 的访问（CRUD 操作）。 2、通过接收 Business 层的数据，把 POJO 持久化为 PO。 七、DO（Domain Object） : 领域对象，就是从现实世界中抽象出来的有形或无形的业务实体。 八、区别 8.1、POJO 和 JaveBean 的区别 POJO 的内在含义是指那些没有从任何类继承、也没有实现任何接口，更没有被其它框架侵入的 java 对象。 JavaBean 则比 POJO 复杂很多，Java Bean 是可复用的组件，对 Java Bean 并没有严格的规范，理论上讲，任何一个 Java 类都可以是一个 Bean 。但通常情况下，由于 Java Bean 是被容器所创建（如 Tomcat) 的，所以 Java Bean 应具有一个无参的构造器，另外，通常 Java Bean 还要实现 Serializable 接口用于实现 Bean 的持久性。 Java Bean 是不能被跨进程访问的。JavaBean 是一种组件技术， 8.2、VO 和 DTO 的区别 既然DTO是展示层与服务层之间传递数据的对象，为什么还需要一个 VO 呢？对于绝大部分的应用场景来说，DTO 和 VO 的属性值基本是一致的，而且他们通常都是 POJO，因此没必要多此一举，但不要忘记这是实现层面的思维，对于设计层面来说，概念上还是应该存在 VO 和 DTO，因为两者有着本质的区别，DTO 代表服务层需要接收的数据和返回的数据，而 VO 代表展示层需要显示的数据。 public xxDto getxxList(xxDto dto){} //服务层 xxDto-->xxVo //控制层(展示层) 或者：public xxVo getxxList(xxDto dto) //服务层 dto --> vo 例子： 例如服务层有一个 getUser 的方法返回一个系统用户，其中有一个属性是gender(性别)，对于服务层来说，它只从语义上定义：1-男性，2-女性，0-未指定，而对于展示层来说，它可能需要用“帅哥”代表男性，用“美女”代表女性，用“秘密”代表未指定。说到这里，可能你还会反驳，在服务层直接就返回“帅哥美女”不就行了吗？对于大部分应用来说，这不是问题，但设想一下，如果需求允许客户可以定制风格，而不同风格对于“性别”的表现方式不一样，又或者这个服务同时供多个客户端使用（不同门户），而不同的客户端对于表现层的要求有所不同，那么，问题就来了。再者，回到设计层面上分析，从职责单一原则来看，服务层只负责业务，与具体的表现形式无关，因此，它返回的 DTO，不应该出现与表现形式的耦合。 8.3、DTO 与 DO 的区别 概念上，DTO 是展示层和服务层之间的数据传输对象（可以认为是两者之间的协议），而 DO 是对现实世界各种业务角色的抽象，这就引出了两者在数据上的区别，例如 UserInfo 和 User（对于 DTO 和 DO 的命名规则），对于一个 getUser 方法来说，本质上它永远不应该返回用户的密码，因此 UserInfo 至少比 User 少一个 password 的数据。而在领域驱动设计中，正如第一篇系列文章所说，DO 不是简单的 POJO，它具有领域业务逻辑。 8.4、DO 和 PO 的区别 DO 和 PO 在绝大部分情况下是一一对应的，PO 是只含有 get/set 方法的 POJO，但某些场景还是能反映出两者在概念上存在本质的区别： DO 在某些场景下不需要进行显式的持久化，例如利用策略模式设计的商品折扣策略，会衍生出折扣策略的接口和不同折扣策略实现类，这些折扣策略实现类可以算是 DO，但它们只驻留在静态内存，不需要持久化到持久层，因此，这类 DO 是不存在对应的 PO 的。 同样的道理，某些场景下，PO 也没有对应的 DO，例如老师 Teacher 和学生 Student 存在多对多的关系，在关系数据库中，这种关系需要表现为一个中间表，也就对应有一个 TeacherAndStudentPO 的 PO，但这个 PO 在业务领域没有任何现实的意义，它完全不能与任何 DO 对应上。这里要特别声明，并不是所有多对多关系都没有业务含义，这跟具体业务场景有关，例如：两个PO之间的关系会影响具体业务，并且这种关系存在多种类型，那么这种多对多关系也应该表现为一个 DO，又如：“角色”与“资源”之间存在多对多关系，而这种关系很明显会表现为一个 DO——“权限”。 某些情况下，为了某种持久化策略或者性能的考虑，一个 PO 可能对应多个 DO，反之亦然。例如客户Customer有其联系信息 Contacts，这里是两个一对一关系的 DO，但可能出于性能的考虑（极端情况，权作举例），为了减少数据库的连接查询操作，把 Customer 和 Contacts 两个 DO 数据合并到一张数据表中。反过来，如果一本图书 Book，有一个属性是封面 cover，但该属性是一副图片的二进制数据，而某些查询操作不希望把 cover 一并加载，从而减轻磁盘 IO 开销，同时假设 ORM 框架不支持属性级别的延迟加载，那么就需要考虑把 cover 独立到一张数据表中去，这样就形成一个 DO 对应对个 PO 的情况。 PO 的某些属性值对于 DO 没有任何意义，这些属性值可能是为了解决某些持久化策略而存在的数据，例如为了实现“乐观锁”， PO 存在一个 version 的属性，这个 version 对于 DO 来说是没有任何业务意义的，它不应该在 DO 中存在。同理，DO 中也可能存在不需要持久化的属性。 总体上来说都是概念基本一致，视角略有不同而已。
实现并发控制的主要手段分为乐观并发控制和悲观并发控制两种。 乐观锁比较适用于读多写少的情况(多读场景)，悲观锁比较适用于写多读少的情况(多写场景)。 一、悲观锁(Pessimistic Lock) 1.1、理解 当要对数据库中的一条数据进行修改的时候，为了避免同时被其他人修改，最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制，在修改数据之前先锁定，再修改的方式被称之为悲观并发控制 Pessimistic Concurrency Control，缩写 PCC，又名悲观锁。 悲观锁，具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务，以及来自外部系统的事务处理)修改持保守态度。因此，在整个数据处理过程中，将数据处于锁定状态。悲观锁的实现，往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性，否则，即使在本系统中实现了加锁机制，也无法保证外部系统不会修改数据)。 之所以叫做悲观锁，是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况，每次读取数据的时候都默认其他线程会更改数据，因此需要进行加锁操作，当其他线程想要访问数据时，都需要阻塞挂起。悲观锁的实现： 传统的关系型数据库使用这种锁机制，比如行锁、表锁、读锁、写锁等，都是在操作之前先上锁。 Java 里面的同步 synchronized 关键字的实现。 1.2、悲观锁主要分为共享锁和排他锁： 共享锁 shared locks 又称为读锁，简称 S 锁。顾名思义，共享锁就是多个事务对于同一数据可以共享一把锁，都能访问到数据，但是只能读不能修改。 排他锁 exclusive locks 又称为写锁，简称 X 锁。顾名思义，排他锁就是不能与其他锁并存，如果一个事务获取了一个数据行的排他锁，其他事务就不能再获取该行的其他锁，包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。 1.3、说明 悲观并发控制实际上是 先取锁再访问 的保守策略，为数据处理的安全提供了保证。但是在效率方面，处理加锁的机制会让数据库产生额外的开销，还有增加产生死锁的机会。另外还会降低并行性，一个事务如果锁定了某行数据，其他事务就必须等待该事务处理完才可以处理那行数据。 二、乐观锁(Optimistic Locking) 2.1、理解 乐观锁是相对悲观锁而言的，乐观锁假设数据一般情况不会造成冲突，所以在数据进行提交更新的时候，才会正式对数据的冲突与否进行检测，如果冲突，则返回给用户异常信息，让用户决定如何去做。乐观锁适用于读多写少的场景，这样可以提高程序的吞吐量。 乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制，但乐观锁不会刻意使用数据库本身的锁机制，而是依据数据本身来保证数据的正确性。乐观锁的实现： CAS 实现：Java 中 java.util.concurrent.atomic 包下面的原子变量使用了乐观锁的一种 CAS 实现方式。 版本号控制：一般是在数据表中加上一个数据版本号 version 字段，表示数据被修改的次数。当数据被修改时，version 值会 +1。当线程 A 要更新数据时，在读取数据的同时也会读取 version 值，在提交更新时，若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新，否则重试更新操作，直到更新成功。 2.2、说明 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的，因此尽可能直接做下去，直到提交的时候才去锁定，所以不会产生任何锁和死锁。 三、具体实现 3.1、悲观锁实现方式 悲观锁的实现，往往依靠数据库提供的锁机制。在数据库中，悲观锁的流程如下： 在对记录进行修改前，先尝试为该记录加上排他锁(exclusive locks)。 如果加锁失败，说明该记录正在被修改，那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。 如果成功加锁，那么就可以对记录做修改，事务完成后就会解锁了。 期间如果有其他对该记录做修改或加排他锁的操作，都会等待解锁或直接抛出异常。 以 MySql Innodb 引擎举例，说明 SQL 中悲观锁的应用 要使用悲观锁，必须关闭 MySQL 数据库的自动提交属性set autocommit=0。因为 MySQL 默认使用 autocommit 模式，也就是说，当执行一个更新操作后，MySQL 会立刻将结果进行提交。 以电商下单扣减库存的过程说明一下悲观锁的使用： 使用 select…for update 锁数据，需要注意锁的级别，MySQL InnoDB 默认行级锁。行级锁都是基于索引的，如果一条 SQL 语句用不到索引是不会使用行级锁的，会使用表级锁把整张表锁住，这点需要注意。 3.2、乐观锁实现方式乐观锁不需要借助数据库的锁机制 当要更新一条记录的时候，希望这条记录没有被别人更新 乐观锁实现方式： 取出记录时，获取当前 version 更新时，带上这个 version 执行更新时， set version = newVersion where version = oldVersion 如果 version 不对，就更新失败 主要就是两个步骤：冲突检测和数据更新。比较典型的就是 CAS (Compare and Swap)。 CAS 即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制，CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值(V)与预期原值(A)相匹配，那么处理器会自动将该位置值更新为新值(B)。否则，处理器不做任何操作。无论哪种情况，它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置(V)应该包含值(A)。如果包含该值，则将新值(B)放到这个位置；否则，不要更改该位置，只告诉我这个位置现在的值即可”。Java 中，sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个 CAS。java.util.concurrent 包下大量的类都使用了这个 Unsafe.java 类的 CAS 操作。 当多个线程尝试使用 CAS 同时更新同一个变量时，只有其中一个线程能更新变量的值，而其它线程都失败，失败的线程并不会被挂起，而是被告知这次竞争中失败，并可以再次尝试。比如前面的扣减库存问题，通过乐观锁可以实现如下： 在更新之前，先查询一下库存表中当前库存数(quantity)，然后在做 update 的时候，以库存数作为一个修改条件。当提交更新的时候，判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对，如果数据库表当前库存数与第一次取出来的库存数相等，则予以更新，否则认为是过期数据。
1、filer public void test01() { List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1); // 遍历输出符合条件的元素 list.stream().filter(x -> x > 6).forEach(System.out::println); // 匹配第一个 Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst(); // 匹配任意（适用于并行流） // 串行流：适合存在线程安全问题、阻塞任务、重量级任务，以及需要使用同一事务的逻辑。 // 并行流：适合没有线程安全问题、较单纯的数据处理任务。 Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny(); // 是否包含符合特定条件的元素 boolean anyMatch = list.stream().anyMatch(x -> x < 6); System.out.println("匹配第一个值：" + findFirst.get()); System.out.println("匹配任意一个值：" + findAny.get()); System.out.println("是否存在大于6的值：" + anyMatch); } 2、to list public class Person { private String name; // 姓名 private int salary; // 薪资 private int age; // 年龄 private String sex; // 性别 private String area; // 地区 } public void test03() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); List<String> fiterList = personList.stream().filter(x -> x.getSalary() > 8000).map(Person::getName) .collect(Collectors.toList()); System.out.print("高于8000的员工姓名：" + fiterList); } 3、max public void test04() { List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd"); Optional<String> max = list.stream().max(Comparator.comparing(String::length)); System.out.println("最长的字符串：" + max.get()); } 4、sort public void test05() { List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6); // 自然排序 Optional<Integer> max = list.stream().max(Integer::compareTo); // 自定义排序 Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }); System.out.println("自然排序的最大值：" + max.get()); System.out.println("自定义排序的最大值：" + max2.get()); } public void test06() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); Optional<Person> max = personList.stream().max(Comparator.comparingInt(Person::getSalary)); System.out.println("员工工资最大值：" + max.get().getSalary()); } 5、count public void test07() { List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9); long count = list.stream().filter(x -> x > 6).count(); System.out.println("list中大于6的元素个数：" + count); } 6、对象转换 public void test09() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); // 不改变原来员工集合的方式 List<Person> personListNew = personList.stream().map(person -> { Person personNew = new Person(person.getName(), 0, 0, null, null); personNew.setSalary(person.getSalary() + 10000); return personNew; }).collect(Collectors.toList()); System.out.println("一次改动前：" + personList.get(0).getName() + "-->" + personList.get(0).getSalary()); System.out.println("一次改动后：" + personListNew.get(0).getName() + "-->" + personListNew.get(0).getSalary()); // 改变原来员工集合的方式 List<Person> personListNew2 = personList.stream().map(person -> { person.setSalary(person.getSalary() + 10000); return person; }).collect(Collectors.toList()); System.out.println("二次改动前：" + personList.get(0).getName() + "-->" + personListNew.get(0).getSalary()); System.out.println("二次改动后：" + personListNew2.get(0).getName() + "-->" + personListNew.get(0).getSalary()); } 一次改动前：Tom-->8900 一次改动后：Tom-->18900 二次改动前：Tom-->18900 二次改动后：Tom-->18900 7、转stream public void test10() { List<String> list = Arrays.asList("m,k,l,a", "1,3,5,7"); List<String> listNew = list.stream().flatMap(s -> { // 将每个元素转换成一个stream String[] split = s.split(","); Stream<String> s2 = Arrays.stream(split); return s2; }).collect(Collectors.toList()); System.out.println("处理前的集合：" + list); System.out.println("处理后的集合：" + listNew); } 8、规约 归约，也称缩减，顾名思义，是把一个流缩减成一个值，能实现对集合求和、求乘积和求最值操作。 public void test11() { List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4); // 求和方式1 Optional<Integer> sum = list.stream().reduce((x, y) -> x + y); // 求和方式2 Optional<Integer> sum2 = list.stream().reduce(Integer::sum); // 求和方式3 Integer sum3 = list.stream().reduce(0, Integer::sum); // 求乘积 Optional<Integer> product = list.stream().reduce((x, y) -> x * y); // 求最大值方式1 Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y); // 求最大值写法2 Integer max2 = list.stream().reduce(12, Integer::max); System.out.println("list求和：" + sum.get() + "," + sum2.get() + "," + sum3); System.out.println("list求积：" + product.get()); System.out.println("list求和：" + max.get() + "," + max2); } public void test12() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); // 求工资之和方式1： Optional<Integer> sumSalary = personList.stream().map(Person::getSalary).reduce(Integer::sum); // 求工资之和方式2： Integer sumSalary2 = personList.stream().reduce(0, (sum, p) -> sum += p.getSalary(), (sum1, sum2) -> sum1 + sum2); // 求工资之和方式3： Integer sumSalary3 = personList.stream().reduce(0, (sum, p) -> sum += p.getSalary(), Integer::sum); // 求最高工资方式1： Integer maxSalary = personList.stream().reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(), Integer::max); // 求最高工资方式2： Integer maxSalary2 = personList.stream().reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(), (max1, max2) -> max1 > max2 ? max1 : max2); System.out.println("工资之和：" + sumSalary.get() + "," + sumSalary2 + "," + sumSalary3); System.out.println("最高工资：" + maxSalary + "," + maxSalary2); } 9、归集 即toList/toSet/toMap public void test13() { List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20); List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList()); Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet()); List<Person> personList = new ArrayList<Person>(); personList.add(new Person(null, 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", null)); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000) .collect(Collectors.toMap(Person::getName, p -> p)); System.out.println("toList:" + listNew); System.out.println("toSet:" + set); System.out.println("toMap:" + map); Map<String, String> collect2 = personList.stream().collect(Collectors.toMap(Person::getName, Person::getArea)); System.out.println(collect2); Map<String, String> collect = personList.stream().collect(Collectors.toMap(Person::getName, Person::getArea)); System.out.println(collect); } 10、统计 count/averaging public void test14() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); // 求总数 Long count = personList.stream().collect(Collectors.counting()); // 求平均工资 Double average = personList.stream().collect(Collectors.averagingDouble(Person::getSalary)); // 求最高工资 Optional<Integer> max = personList.stream().map(Person::getSalary).collect(Collectors.maxBy(Integer::compare)); // 求工资之和 Integer sum = personList.stream().collect(Collectors.summingInt(Person::getSalary)); // 一次性统计所有信息 DoubleSummaryStatistics collect = personList.stream().collect(Collectors.summarizingDouble(Person::getSalary)); System.out.println("员工总数：" + count); System.out.println("员工平均工资：" + average); System.out.println("员工工资总和：" + sum); System.out.println("员工工资所有统计：" + collect); } 11、分组 partitioningBy/groupingBy public void test15() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 18, "male", "New York")); personList.add(new Person("Jack", 7000, 18, "male", "Washington")); personList.add(new Person("Lily", 7800, 18, "female", "Washington")); personList.add(new Person("Anni", 8200, 18, "female", "New York")); personList.add(new Person("Owen", 9500, 18, "male", "New York")); personList.add(new Person("Alisa", 7900, 18, "female", "New York")); // 将员工按薪资是否高于8000分组 Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000)); // 将员工按性别分组 Map<String, List<Person>> group = personList.stream().collect(Collectors.groupingBy(Person::getSex)); // 将员工先按性别分组，再按地区分组 Map<String, Map<String, List<Person>>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea))); System.out.println("员工按薪资是否大于8000分组情况：" + part); System.out.println("员工按性别分组情况：" + group); System.out.println("员工按性别、地区：" + group2); } 12、joining joining可以将stream中的元素用特定的连接符（没有的话，则直接连接）连接成一个字符串。 public void test16() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); String names = personList.stream().map(p -> p.getName()).collect(Collectors.joining(",")); System.out.println("所有员工的姓名：" + names); List<String> list = Arrays.asList("A", "B", "C"); String string = list.stream().collect(Collectors.joining("-")); System.out.println("拼接后的字符串：" + string); } 13、reducing Collectors类提供的reducing方法，相比于stream本身的reduce方法，增加了对自定义归约的支持。 public void test17() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); // 每个员工减去起征点后的薪资之和（这个例子并不严谨，但一时没想到好的例子） Integer sum = personList.stream().collect(Collectors.reducing(0, Person::getSalary, (i, j) -> (i + j - 5000))); System.out.println("员工扣税薪资总和：" + sum); // stream的reduce Optional<Integer> sum2 = personList.stream().map(Person::getSalary).reduce(Integer::sum); System.out.println("员工薪资总和：" + sum2.get()); } 14、排序 sorted，中间操作。有两种排序： sorted()：自然排序，流中元素需实现Comparable接口。 sorted(Comparator com)：Comparator排序器自定义排序。 public void test18() { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Sherry", 9000, 24, "female", "New York")); personList.add(new Person("Tom", 8900, 22, "male", "Washington")); personList.add(new Person("Jack", 9000, 25, "male", "Washington")); personList.add(new Person("Lily", 8800, 26, "male", "New York")); personList.add(new Person("Alisa", 9000, 26, "female", "New York")); long count = personList.stream().map(Person::getSalary).distinct().count(); System.out.println(count); personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName); // 按工资升序排序（自然排序） List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName) .collect(Collectors.toList()); // 按工资倒序排序 List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed()) .map(Person::getName).collect(Collectors.toList()); // 先按工资再按年龄升序排序 List<String> newList3 = personList.stream() .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName) .collect(Collectors.toList()); // 先按工资再按年龄自定义排序（降序） List<String> newList4 = personList.stream().sorted((p1, p2) -> { if (p1.getSalary() == p2.getSalary()) { return p2.getAge() - p1.getAge(); } else { return p2.getSalary() - p1.getSalary(); } }).map(Person::getName).collect(Collectors.toList()); System.out.println("按工资升序排序：" + newList); System.out.println("按工资降序排序：" + newList2); System.out.println("先按工资再按年龄升序排序：" + newList3); System.out.println("先按工资再按年龄自定义降序排序：" + newList4); } 15、提取/组合 流也可以进行合并、去重、限制、跳过等操作。 public void test19() { String[] arr1 = {"a", "b", "c", "d"}; String[] arr2 = {"d", "e", "f", "g"}; Stream<String> stream1 = Stream.of(arr1); Stream<String> stream2 = Stream.of(arr2); // concat:合并两个流 distinct：去重 List<String> newList = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList()); // limit：限制从流中获得前n个数据 List<Integer> collect = Stream.iterate(1, x -> x + 2).limit(10).collect(Collectors.toList()); // skip：跳过前n个数据 List<Integer> collect2 = Stream.iterate(1, x -> x + 2).skip(1).limit(5).collect(Collectors.toList()); System.out.println("流合并：" + newList); System.out.println("limit：" + collect); System.out.println("skip：" + collect2); } 16、collect详解 public class CollectTest { /** * stream.collect() 的本质由三个参数构成, * 1. Supplier 生产者, 返回最终结果 * 2. BiConsumer<R, ? super T> accumulator 累加器 * 第一个参数是要返回的集合, 第二个参数是遍历过程中的每个元素, * 将流中每个被遍历的元素添加到集合中 * 3. BiConsumer<R, R> combiner 合并器, 在有并行流的时候才会有用, 一个流时代码不会走到这里 * 将第二步遍历得到的所有流形成的list都添加到最终的list中, * 最后返回list1 */ @Test public void Test() { Stream<String> stream = Stream.of("hello", "world", "helloworld"); // 最原始和基础的方式 /* List<String> list = stream.collect( ()->new ArrayList(), (theList, item) -> theList.add(item), (list1, list2) -> list1.addAll(list2) ); */ // 打印出更详尽的过程 List<String> listDetail = stream.collect( () -> { ArrayList<String> arrayList = new ArrayList<>(); System.out.println("第一个list诞生, size: " + arrayList.size()); return arrayList; }, (theList, item) -> { System.out.println("第二个list的size: " + theList.size()); theList.add(item); }, (list1, list2) -> { System.out.println("第三个list1的size: " + list1.size()); System.out.println("第四个list2的size: " + list2.size()); list1.addAll(list2); } ); /* 输出 第一个list诞生, size: 0 第二个list的size: 0 第二个list的size: 1 第二个list的size: 2 * */ // 使用方法引用来传递行为, 更加清晰易懂, new(新建) -> add(累加) -> addAll(合并) List<String> list2 = stream.collect(LinkedList::new, LinkedList::add, LinkedList::addAll); String concat = stream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString(); System.out.println(concat); } @Test public void Test2() { Stream<String> stream = Stream.of("hello", "world", "helloworld"); // 这样的写法兼具灵活和简单 ArrayList<String> list = stream.collect(Collectors.toCollection(ArrayList::new)); TreeSet<String> treeSet = stream.collect(Collectors.toCollection(TreeSet::new)); String s = stream.collect(Collectors.joining()); // 拼接成字符串 HashMap<String, String> map = stream.collect(HashMap::new, (x, y) -> { x.put(y, y); // 自己做自己的key }, HashMap::putAll); } } 17、flatMap public void test() { List<String> teamIndia = Arrays.asList("Virat", "Dhoni", "Jadeja"); List<String> teamAustralia = Arrays.asList("Warner", "Watson", "Smith"); List<String> teamEngland = Arrays.asList("Alex", "Bell", "Broad"); List<List<String>> playersInWorldCup2016 = new ArrayList<>(); playersInWorldCup2016.add(teamIndia); playersInWorldCup2016.add(teamAustralia); playersInWorldCup2016.add(teamEngland); // Let's print all players before Java 8 List<String> listOfAllPlayers = new ArrayList<>(); for (List<String> team : playersInWorldCup2016) { for (String name : team) { listOfAllPlayers.add(name); } } System.out.println("Players playing in world cup 2016"); System.out.println(listOfAllPlayers); // Now let's do this in Java 8 using FlatMap List<String> flatMapList = playersInWorldCup2016.stream() // .flatMap(pList -> pList.stream()) .flatMap(List::stream) .collect(Collectors.toList()); System.out.println("List of all Players using Java 8"); System.out.println(flatMapList); }
JDK1.8 版本中引入了 DateTime API，加强了对时间的管理。 一、Date 存在的缺陷 Date 如果不格式化，打印出的日期可读性差,如下所示。 Tue Sep 10 09:34:04 CST 2019 所以通常会使用SimpleDateFormate来实现格式化，但是SDF最大的问题是线程不安全的。 private StringBuffer format(Date var1, StringBuffer var2, FieldDelegate var3) { this.calendar.setTime(var1); boolean var4 = this.useDateFormatSymbols(); int var5 = 0; calendar 是共享变量，并且这个共享变量没有做线程安全控制。当多个线程同时使用相同的 SimpleDateFormat 对象【如用 static 修饰的 SimpleDateFormat】调用 format 方法时，多个线程会同时调用 calendar.setTime 方法，可能一个线程刚设置好 time 值另外的一个线程马上把设置的 time 值给修改了导致返回的格式化时间可能是错误的。 多线程并发如何保证线程安全： 1、避免线程之间共享一个 SimpleDateFormat 对象，每个线程使用时都创建一次 SimpleDateFormat 对象 => 创建和销毁对象的开销大。对使用 format 和 parse 方法的地方进行加锁 => 线程阻塞性能差。 2、使用 ThreadLocal 保证每个线程最多只创建一次 SimpleDateFormat 对象 => 较好的方法。 3、Date 对时间处理比较麻烦，比如想获取某年、某月、某星期，以及n天以后的时间，如果用 Date 来处理的话真是太难了，你可能会说 Date 类不是有 getYear、getMonth 这些方法吗，获取年月日很Easy，但是这些方法都被弃用了。 二、LocalDateTime 使用 JDK8 带来了新的时间日期 API 也就是 java.time 包下面带来 Local 和 Zone，两者的区别一个是基于你本地时间的，一个是你自己指定时区的。 而 Local 都有这几个类：LocalDateTime（既包含时间也包含日期），LocalDate（只关注日期，一般来说你只关注日期不需要时间的话用这个），LocalTime（只关注时间，不关注日期）。 当然了，实际上 LocalDate 和 LocalTime 也支持日期+时间的形式，因为 LocalDate 里面包含一个 atTime 来存储时间，同理 LocalTime 也有一个 atDate 属性 例如： LocalDate localDate = LocalDate.of(2021,3,15); localDate.atTime(LocalTime.now()); 但是如果不想多设置的话可以直接使用 LocalDateTime，将日期时间一起处理了。 LocalDateTime localDateTime = LocalDateTime.now(); 所以说 LocalDateTime 取代 Date 是一个趋势，当然有人比较过二者的效率，结果是 Date 要比 LocalDateTIme 快一些，这也没办法毕竟线程安全可能就要多点取舍，未来的版本也许 LocalDateTime 也可以像 Current 集合那样线程又安全又快。
一、Cookie Cookie 数据是由客户端来保存和携带的，所以称之为客户端技术。 创建一个 cookie，cookie 是 servlet 发送到 Web 浏览器的少量信息。这些信息由浏览器保存，然后发送回服务器 cookie 的值可以唯一地标识客户端，因此 cookie 常用于会话管理。 一个 cookie 拥有一个名称、一个值和一些可选属性，比如注释、路径和域限定符、最大生存时间和版本号。一些 Web 浏览器在处理可选属性方面存在 bug，因此有节制地使用这些属性可提高 serviet 的互操作性。 servlet 通过使用 HttpServletResponse#addCookie 方法将 cookie 发送到浏览器：该方法将字段添加到 HTTP 响应头以便一次一个地将 cookie 发送到浏览器。浏览器应该支持每台 Web 服务器有 20 个 cookie，总共有 300 个 cookie，并且可能将每个 cookie 的大小限定为 4 KB。 浏览器通过向 HTTP 请求头添加字段将 cookie 返回给 serlet 可使用 HttpServletRequest#getCookies 方法从请求中获取 cookie。一些 cookie 可能有相同的名称，但却有不同的路径属性。 cookie 影响使用它们的 Web 页面的缓存。HTTP 1.0 不会缓存那些使用通过此类创建的 cookie 的页面。此类不支持 HTTP 1.1 中定义的缓存控件。 此类支持版本0(遵守Netscape协议)和版本1(遵守RFC 2109协议) cookie 规范。默认情况下, cookie 是使用版本0创建的,以确保最佳互操作性。 cookie 容量：一个浏览器最多保存 300 个 cookie。每个站点做多保存 20 个 cookie，每个 cookie 的大小限制是 4kb。 cookie 默认存活时间：浏览器关闭的时候结束。 cookie 持久化：若希望浏览器将 cookie 存储到磁盘上就需要用到 maxAge()，以秒为单位的时间。 cookie 删除：删除 cookie 时，path 必须一致，否则不会被删除。 属性： 1、name：名称不能唯一确定一个 Cookie。路径可能不同。 2、value：不能存中文。 3、path：默认值是写 Cookie 的那个程序的访问路径。 比如：http://localhost:8080/day10_00_cookie/servlet/login写的 Cookie path 就是：/day10_00_cookie 看当前创建 cookie 的资源（servlet）文件路径 也可以是 reuest.getContextPath(); 获取当前应用的路径。 客户端在访问服务器另外资源时，根据访问的路径来决定是否带着 Cookie 到服务器。 当前访问的路径如果是以 cookie 的 path 开头的路径，浏览器就带。否则不带。 4、maxAge(有效存活时间)：cookie 的缓存时间。默认是 -1（默认存在浏览器的内存中）。单位是秒。 负数：cookie的数据存在浏览器缓存中。 0：删除。路径要保持一致，否则可能删错人。 正数：缓存（持久化到磁盘上）的时间。 cookie 在磁盘上持久化的文本形式： cookie 在磁盘上持久化的文本形式： public class CookieDemo1 extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter pw = resp.getWriter(); Cookie[] cookies = req.getCookies(); for (int i = 0;cookies!=null&& i < cookies.length; i++) { if("lastAccessTime".equals(cookies[i].getName())) { long l = Long.parseLong(cookies[i].getValue()); pw.write("您上次的登陆时间为:"+new Date(l).toLocaleString()); } } //删除cookie跳转的servlet pw.print("clear cookie"); //创建cookie Cookie ck = new Cookie("lastAccessTime", System.currentTimeMillis()+""); //设置cookie的有效时间，单位是秒 // ck.setMaxAge(5*60); //设置cookie的path // ck.setPath("/day10_00_cookie/servlet"); // ck.setPath(req.getContextPath()); //建议这两种写法 表示当前的应用/day10_00_cookie ck.setPath("/"); //表示 /day10_00_cookie ck.setPath("http://www.taobao.com");==>也可以将cookie带到该网站 resp.addCookie(ck); //需要将cookie添加给响应报文==>设置好了cookie的相关属性再进行添加 } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doGet(req, resp); } } cookie 的取出： public class CookieDemo2 extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter pw = resp.getWriter(); Cookie[] cookies = req.getCookies(); for (int i = 0;cookies!=null&& i < cookies.length; i++) { if("lastAccessTime".equals(cookies[i].getName())) { long l = Long.parseLong(cookies[i].getValue()); pw.write("您上次的登陆时间为:"+new Date(l).toLocaleString()); } } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doGet(req, resp); } } cookie 的清除 public class ClearServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //创建一个cookie对象 Cookie ck = new Cookie("lastAccessTime", ""); //和创建cookie时候的路径path对应起来，不然可能会删除对象 ck.setPath("/"); ck.setMaxAge(0); resp.addCookie(ck); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } } 二、Session Session：在计算机中，尤其是在网络应用中，称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样，当用户在应用程序的 Web 页之间跳转时，存储在 Session 对象中的变量将不会丢失，而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话，则 Web 服务器将自动创建一个Session 对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如，如果用户指明不喜欢查看图形,就可以将该信息存储在 Session 对象中。有关使用 Session 对象的详细信息，请参阅"ASP应用程序"部分的"管理会话”。注意 会话状态仅在支持 cookie 的浏览器中保留。 也是一个域对象: session(多servlet可以共享一个session数据) servletContext（整个应用的范围） request(servlet执行之前创建，结束的时候销毁。) session 默认存活时间：30min。 同一个会话下，可以使一个应用的多个资源共享数据。 cookie 客户端技术，只能存字符串。HttpSession 服务器端的技术，可存储对象。 在 WEB 开发中，服务器为每一个客户端(请求你创建且没有 session 才创建)创建一个会话对象(session 对象)。注意一个浏览器独占一个 session 对象. 原理详解： session 机制是一种服务器端的机制。 用来在无状态的 HTTP 协议下越过多个请求页面来维持和识别用户。 当程序需要为某个客户端的请求创建一个 session 的时候， 服务器首先检查这个客户端的请求里是是否包含一个 session id 标识。 如果包含一个 session id 则说明以前为此客户端创建过 session。 服务器就按照 session id 把这个 session 检索出来使用，若没有创建过，则创建一个新的 Session。 创建时机： HttpSession session = request.getSession(); 如果当前没有 session，则会立刻建立一个 session； 如果有 session 则返回当前 session。 这句话和 HttpSession session = request.getSession(true);的效果是一样的。 HttpSession session = request.getSession(false); 则不会自动建立 session。 若当前没有 session，你所得到的 seesion 只会是一个 null。 总结： session 不是一打开网站就会立刻建立。 它的建立需要基于下面两个条件中的任意一个： 1、在 servlet 中手动调用 HttpSession session = request.getSession(); 或者 HttpSession session = request.getSession(true); 2、jsp 中没有写 <%@ page session="false"%> (默认情况下它是<%@ page session="true"%>的) 如果两个条件同时都不满足，那么你建立的只是一个无 seesion 的连接。 session 格式： 30min 后服务端的 session 和客户端的 JSESSIONID=xxx，才会被删除掉。重新调用HttpSession session = request.getSession();重新创建此两个对象。 常用方法： 把数据保存在 HttpSession 对象中，该对象也是一个域对象。 void setAttribute(String name,Object value); Object getAttribute(String name); void removeAttribute(String name); HttpSession.getId(): setMaxInactiveInterval(int interval) 设置session的存活时间 单位s 或者使用配置文件的方式 <session-config> <session-timeout>10</session-timeout> </session-config> invalidate() 使此会话无效。 getSession()：内部执行原理 1、获取名称为 JSESSIONID 的 cookie 的值。 2、没有这样的 cookie，服务端创建一个新的 HttpSession 对象，分配一个唯一的 SessionID。并且在浏览器中写一个名字为 JSESSIONID=sessionID(dbe22defb...) 的 cookie。 3、有这样的 Cookie，获取 cookie 的值(dbe22defb...)，从服务器的内存中找到对应的 HttpSession 对象。 HttpSession request.getSession(boolean create): true：和 getSession() 功能一样。 false：根据客户端 JSESSIONID 的 cookie 的值，找对应的 HttpSession 对象，找不到返回 null（不会创建新的，只是查询）。 客户端禁用 cookie 后的会话数据保存问题： 客户端禁用 cookie：浏览器永远不会向服务器发送 cookie 的请求消息头。 或者存在浏览器不支持 cookie 技术。==>看浏览器有没有发送 cookie 请求消息头，没有就重写 URL，有就不重写。 解决方案： 方案一：在主页上给出提示：请不要禁用您的 cookie。 方案二：URL 重写。必须对网站的所有地址都重写。 public String encodeURL(String url) 通过将会话ID 包含在指定 URL 中对该 URL进行编码，如果不需要编码，则返回未更改的 URL。此方法的实现包含可以确定会话 ID 是否需要在 URL 中编码的逻辑。例如，如果浏览器支持 cookie，或者关闭了会话跟踪，则 URL 编码就不是必需的。 对于健壮的会话跟踪，servlet 发出的所有 URL 都应该通过此方法运行。否则，URL 重写不能用于不支持 cookie 的浏览器。 url 要编码的 URL。 return 如果需要编码，则返回编码的 URL;否则返回未更改的 URL。 示例： public class AddCart extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out =resp.getWriter(); String id = req.getParameter("id"); Book book = DBUtil.getBookById(id); HttpSession session = req.getSession(); //这时候是带JSESSIONID来找session ArrayList<Book> list = (ArrayList<Book>) session.getAttribute("cart"); if(null==list) { list =new ArrayList<Book>(); } list.add(book); session.setAttribute("cart", list); out.write("购买成功:"+book.getName()); String url = req.getContextPath()+"/servlet/showAll"; resp.setHeader("refresh", "2;url="+resp.encodeURL(url)); // resp.setHeader("refresh", "2;url="+req.getContextPath()+"/servlet/showAll"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }
一、示例 ?xml version="1.0" encoding="UTF-8"?> <Server port="8005" shutdown="SHUTDOWN"> <Listener className="org.apache.catalina.core.AprLifecycleListener" /> <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" /> <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> <Listener className="org.apache.catalina.storeconfig.StoreConfigLifecycleListener"/> <GlobalNamingResources> <Environment name="simpleValue" type="java.lang.Integer" value="30"/> <Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml" /> </GlobalNamingResources> <!-- 每个Service元素只能有一个Engine元素.元素处理在同一个<Service>中所有<Connector>元素接收到的客户请求 name:Service服务器的名称 --> <Service name="Catalina"> <!-- Connector元素: 由Connector接口定义.<Connector>元素代表与客户程序实际交互的组件,它负责接收客户请求,以及向客户返回响应结果. --> <Connector port="80" maxHttpHeaderSize="8192" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" connectionTimeout="20000" disableUploadTimeout="true" /> <!-- 属性说明 port:服务器连接器的端口号,该连接器将在指定端口侦听来自客户端的请求。 enableLookups:如果为true，则可以通过调用request.getRemoteHost()进行DNS查询来得到远程客户端的实际主机名； 若为false则不进行DNS查询，而是返回其ip地址。 redirectPort:服务器正在处理http请求时收到了一个SSL传输请求后重定向的端口号。 acceptCount:当所有可以使用的处理请求的线程都被用光时,可以放到处理队列中的请求数,超过这个数的请求将不予处理，而返回Connection refused错误。 connectionTimeout:等待超时的时间数（以毫秒为单位）。 maxThreads:设定在监听端口的线程的最大数目,这个值也决定了服务器可以同时响应客户请求的最大数目.默认值为200。 protocol:必须设定为AJP/1.3协议。 address:如果服务器有两个以上IP地址,该属性可以设定端口监听的IP地址,默认情况下,端口会监听服务器上所有IP地址。 minProcessors:服务器启动时创建的处理请求的线程数，每个请求由一个线程负责。 maxProcessors:最多可以创建的处理请求的线程数。 minSpareThreads:最小备用线程 。 maxSpareThreads:最大备用线程。 debug:日志等级。 disableUploadTimeout:禁用上传超时,主要用于大数据上传时。 --> <Connector port="8009" enableLookups="false" redirectPort="8443" protocol="AJP/1.3" /> <Engine name="Catalina" defaultHost="localhost"> <!-- 属性说明 name:对应$CATALINA_HOME/config/Catalina中的Catalina defaultHost:对应<Host>元素中的name属性,也就是和$CATALINA_HOME/config/Catalina/localhost中的localhost 缺省的处理请求的虚拟主机名，它至少与其中的一个Host元素的name属性值是一样的 debug:日志等级 --> <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> <!-- 由Host接口定义.一个Engine元素可以包含多个<Host>元素. 每个<Host>的元素定义了一个虚拟主机.它包含了一个或多个Web应用. --> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> <Context path="/demm" docBase="E:\\projects\\demm\\WebRoot" debug="0" reloadable="true" > </Context> <!-- 属性说明 path:访问的URI,如:http://localhost/是我的应用的根目录,访问此应用将用:http://localhost/demm进行操作,此元素必须， 表示此web application的URL的前缀，用来匹配一个Context。请求的URL形式为http://localhost:8080/path/* docBase:WEB应用的目录,此目录必须符合Java WEB应用的规范，web application的文件存放路径或者是WAR文件存放路径。 debug:日志等级 reloadable:是否在程序有改动时重新载入,设置成true会影响性能,但可自动载入修改后的文件， 如果为true，则Tomcat将支持热部署，会自动检测web application的/WEB-INF/lib和/WEB-INF/classes目录的变化， 自动装载新的JSP和Servlet，我们可以在不重启Tomcat的情况下改变web application 。 --> <Context docBase="F:\TomCat\apache-tomcat-9.0.12\wtpwebapps\day11_03_PageContext" path="/day11_03_PageContext" reloadable="true" source="org.eclipse.jst.jee.server:day11_03_PageContext"/> <Context docBase="F:\TomCat\apache-tomcat-9.0.12\wtpwebapps\day11_04_el" path="/day11_04_el" reloadable="true" source="org.eclipse.jst.jee.server:day11_04_el"/> <Context docBase="F:\TomCat\apache-tomcat-9.0.12\wtpwebapps\day07_03_myApp1" path="/day07_03_myApp1" reloadable="true" source="org.eclipse.jst.jee.server:day07_03_myApp1"/> </Host> </Engine> </Service> </Server> 二、web.xml 解析 2.1、web.xml 中 servlet 配置及其含义 一个 web 中可以没有 web.xml 文件，也就是说，web.xml 文件并不是 web 工程必须的。 web.xml 文件是用来初始化配置信息：比如 Welcome 页面、servlet、servlet-mapping、filter、listener、启动加载级别等。 每个 xml 文件都有定义它书写规则的 Schema 文件，也就是说 javaEE 的定义 web.xml 所对应的 xml Schema 文件中定义了多少种标签元素，web.xml 中就可以出现它所定义的标签元素，也就具备哪些特定的功能。web.xml 的模式文件是由 Sun 公司定义的，每个 web.xml 文件的根元素为 <web-app> 中，必须标明这个 web.xml 使用的是哪个模式文件。如： <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> </web-app> 下面列出 web.xml 我们常用的一些标签元素及其功能： 1、指定欢迎页面： <welcome-file-list> <welcome-file-list> <welcome-file>index.jsp</welcome-file> <welcome-file>index1.jsp</welcome-file> </welcome-file-list> PS：指定了2个欢迎页面，显示时按顺序从第一个找起，如果第一个存在，就显示第一个，后面的不起作用。如果第一个不存在，就找第二个，以此类推。 2、命名与定制 URL。我们可以为 Servlet 和 JSP 文件命名并定制URL，其中定制 URL 是依赖命名的，命名必须在定制 URL 前。下面拿 serlet 来举例： (1)、为Servlet命名： <servlet> <servlet-name>servlet1</servlet-name> <servlet-class>org.whatisjava.TestServlet</servlet-class> </servlet> (2)、为Servlet定制URL、 <servlet-mapping> <servlet-name>servlet1</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> 3、定制初始化参数：可以定制 servlet、JSP、Context 的初始化参数，然后可以再 servlet、JSP、Context 中获取这些参数值。 <servlet> <servlet-name>servlet1</servlet-name> <servlet-class>org.whatisjava.TestServlet</servlet-class> <init-param> <param-name>userName</param-name> <param-value>Daniel</param-value> </init-param> <init-param> <param-name>E-mail</param-name> <param-value>125485762@qq.com</param-value> </init-param> </servlet> 经过上面的配置，在 servlet 中能够调用 getServletConfig().getInitParameter("param1") 获得参数名对应的值。 4、指定错误处理页面，可以通过“异常类型”或“错误码”来指定错误处理页面。 <error-page> <error-code>404</error-code> <location>/error404.jsp</location> </error-page> ----------------------------- <error-page> <exception-type>java.lang.Exception<exception-type> <location>/exception.jsp<location> </error-page> 5、设置过滤器：比如设置一个编码过滤器，过滤所有资源 <filter> <filter-name>XXXCharaSetFilter</filter-name> <filter-class>net.test.CharSetFilter</filter-class> </filter> <filter-mapping> <filter-name>XXXCharaSetFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 6、设置监听器： <listener> <listener-class>net.test.XXXLisenet</listener-class> </listener> 7、设置会话(Session)过期时间，其中时间以分钟为单位，假如设置 60 分钟超时： <session-config> <session-timeout>60</session-timeout> </session-config> Servlet 配置及含义： <servlet> <servlet-name>ServletDemo</servlet-name> <servlet-class>lm.practice.ServletDemo</servlet-class> </servlet> <servlet-mapping> <servlet-name>ServletDemo</servlet-name> <url-pattern>/servlet</url-pattern> </servlet-mapping> 这两个一个是配置 Servlet，一个是配置其映射信息，其中 中的 <servlet-name> 可以随意指定，但要有一定的意义，一般取为类的名称，例如我的类名为 ServletDemo，这里取名为 ServletDemo，下面的 <servlet-class> 是类的全路径，package+calssname，一定要是全路径！ <servlet-Mapping> 是映射信息，它也有一个 <servlet-name>，里面的名字是对应的 Servlet 名，也就是我们上面配置的 Servlet 名字，这里是 ServletDemo，下面的是映射路径，也就是访问 Servlet 的名称，这里也是以方便和有意义为前提的，是我们在访问 Servlet 在浏览器地址栏后面输入的那个信息，例如我的映射路径命名为 /servlet，在地址栏中输入http://localhost/20170323/servlet。 注意：这里的映射路径一定不是丢掉/，否则就会出错了，一定要写成 /servlet，不能是 servlet。 这里说一下在配置映射路径的时候，有以下两种： url-pattern 浏览器输入 精确匹配 /servlet http://localhost:8080/day10/servlet 模糊匹配 /* http://localhost:8080/20170323/任意路径 /lm/* http://localhost:8080/20170323/lm/任意路径 *.后缀名 *.do *.action *.html(伪静态) http://localhost:8080/20170323/任意路径.do 下面是项目中 web.xml 文件中 springmvc 的 Servlet 配置信息： <servlet> <servlet-name>SpringMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <servlet> <servlet-name>jobDispatcherServlet</servlet-name> <servlet-class>com.suning.framework.uts.client.core.JobDispatcherServlet</servlet-class> <load-on-startup>3</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jobDispatcherServlet</servlet-name> <url-pattern>/jobDispatcher</url-pattern> </servlet-mapping> <session-config> <session-timeout>30</session-timeout> </session-config> 注意： url-pattern 要么以 / 开头，要么以*开头。绝对不能漏掉斜杠。 不能同时使用两种模糊匹配，例如 /lm/*.do是非法路径。 当有输入的 URL 有多个 servlet 同时被匹配的情况下： 精确匹配优先。（长的最像优先被匹配）。 以后缀名结尾的模糊匹配先级最低。 2.2、一个 web 项目 web.xml 的配置中 <context-param> 配置作用 1、启动一个 WEB 项目的时候，容器(如:Tomcat)会去读它的配置文件web.xml.读两个节点: <listener></listener> 和 <context-param></context-param>。 2、紧接着，容器创建一个ServletContext(上下文),这个WEB项目所有部分都将共享这个上下文。 3、容器将<context-param></context-param>转化为键值对，并交给 ServletContext。 4、容器创建<listener></listener>中的类实例，即创建监听。 5、在监听中会有contextInitialized(ServletContextEvent args)初始化方法，在这个方法中获得ServletContext = ServletContextEvent.getServletContext(); context-param 的值 = ServletContext.getInitParameter("context-param的键"); 6、得到这个 context-param 的值之后，你就可以做一些操作了。注意，这个时候你的WEB项目还没有完全启动完成。这个动作会比所有的 Servlet 都要早。换句话说，这个时候，你对 <context-param> 中的键值做的操作，将在你的 WEB 项目完全启动之前被执行. 7、举例，你可能想在项目启动之前就打开数据库。那么这里就可以在 <context-param> 中设置数据库的连接方式，在监听类中初始化数据库的连接。 8、这个监听是自己写的一个类，除了初始化方法，它还有销毁方法。用于关闭应用前释放资源。比如说数据库连接的关闭。 <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml,/WEB-INF/action-servlet.xml,/WEB- INF/jason-servlet.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> 又如: --->自定义context-param,且自定义listener来获取这些信息 <context-param> <param-name>urlrewrite</param-name> <param-value>false</param-value> </context-param> <context-param> <param-name>cluster</param-name> <param-value>false</param-value> </context-param> <context-param> <param-name>servletmapping</param-name> <param-value>*.bbscs</param-value> </context-param> <context-param> <param-name>poststoragemode</param-name> <param-value>1</param-value> </context-param> <listener> <listener-class>com.laoer.bbscs.web.servlet.SysListener</listener-class> </listener> 监听器： public class SysListener extends HttpServlet implements ServletContextListener { private static final Log logger = LogFactory.getLog(SysListener.class); public void contextDestroyed(ServletContextEvent sce) { //用于在容器关闭时,操作 } //用于在容器开启时,操作 public void contextInitialized(ServletContextEvent sce) { String rootpath = sce.getServletContext().getRealPath("/"); System.out.println("-------------rootPath:"+rootpath); if (rootpath != null) { rootpath = rootpath.replaceAll("\\\\", "/"); } else { rootpath = "/"; } if (!rootpath.endsWith("/")) { rootpath = rootpath + "/"; } Constant.ROOTPATH = rootpath; logger.info("Application Run Path:" + rootpath); String urlrewrtie = sce.getServletContext().getInitParameter("urlrewrite"); boolean burlrewrtie = false; if (urlrewrtie != null) { burlrewrtie = Boolean.parseBoolean(urlrewrtie); } Constant.USE_URL_REWRITE = burlrewrtie; logger.info("Use Urlrewrite:" + burlrewrtie); 其它略之.... } } /*最终输出 -------------rootPath:D:\tomcat_bbs\webapps\BBSCS_8_0_3\ 2009-06-09 21:51:46,526 [com.laoer.bbscs.web.servlet.SysListener]-[INFO] Application Run Path:D:/tomcat_bbs/webapps/BBSCS_8_0_3/ 2009-06-09 21:51:46,526 [com.laoer.bbscs.web.servlet.SysListener]-[INFO] Use Urlrewrite:true 2009-06-09 21:51:46,526 [com.laoer.bbscs.web.servlet.SysListener]-[INFO] Use Cluster:false 2009-06-09 21:51:46,526 [com.laoer.bbscs.web.servlet.SysListener]-[INFO] SERVLET MAPPING:*.bbscs 2009-06-09 21:51:46,573 [com.laoer.bbscs.web.servlet.SysListener]-[INFO] Post Storage Mode:1 */ 2.3、context-param 和 init-param 区别 web.xml 里面可以定义两种参数： 1、application 范围内的参数，存放在 servletcontext中，在 web.xml 中配置如下： <context-param> <param-name>context/param</param-name> <param-value>avalible during application</param-value> </context-param> 2、servlet 范围内的参数，只能在 servlet 的 init() 方法中取得，在 web.xml 中配置如下： <servlet> <servlet-name>MainServlet</servlet-name> <servlet-class>com.wes.controller.MainServlet</servlet-class> <init-param> <param-name>param1</param-name> <param-value>avalible in servlet init()</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> 在servlet中可以通过代码分别取用： package com.wes.controller; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; public class MainServlet extends HttpServlet ...{ public MainServlet() ...{ super(); } public void init() throws ServletException ...{ System.out.println("下面的两个参数param1是在servlet中存放的"); System.out.println(this.getInitParameter("param1")); System.out.println("下面的参数是存放在servletcontext中的"); System.out.println(getServletContext().getInitParameter("context/param")); } } 第一种参数在 servlet 里面可以通过以下方式得到。 getServletContext().getInitParameter("context/param") 第二种参数只能在 servlet 的 init() 方法中通过this.getInitParameter("param1")取得。