键盘敲烂,月薪过万作业不做,等于没学
SpringMVC:性能:缓存 / 异步 / Ajax应用

当前系列: Java Web开发 修改

缓存

Spring本身不直接提供缓存功能的实现,但提供了对缓存功能的抽象:

  • CacheManager:Cache的容器对象,获取Cache对象的入口:
    public interface CacheManager {
  • Cache:它是Ehcache的核心类,它有多个Element,并被CacheManager管理。它实现了对缓存的逻辑行为。
    public interface Cache { 

演示:Spring自带了上述接口的实现EhCacheCacheManager和EhCacheCache

public class EhCacheCacheManager extends AbstractTransactionSupportingCacheManager {
public class EhCacheCache implements Cache 
注意以上接口/实现类的package:
package org.springframework.cache;
package org.springframework.cache.ehcache;

他们都不是真正的ehcache实现。

Ehcache实现

首先需要maven引入(复习:Hibernate二级缓存

然后用在springmvc-servlet.xml中配置CacheManager的SpringBean为:

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
  <property name="cacheManager" ref="ehcache"/>
</bean>	
注意其中的ref,实际上指向的就是:
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
  <property name="configLocation" value="/WEB-INF/ehcache.xml"/>
</bean>
EhCacheManagerFactoryBean才利用ehcache.xml文件真正生成EhCacheManager……

PS:Ehcache的CacheManager构造函数或工厂方法被调用时,会默认加载classpath下名为ehcache.xml的配置文件。如果加载失败,会加载Ehcache jar包中的ehcache-failsafe.xml(演示)文件,这个文件中含有简单的默认配置。

ehcache.xml配置

<cache name="user"
       maxElementsInMemory="1000"
       eternal="false"
       timeToIdleSeconds="50"
       timeToLiveSeconds="50"
       overflowToDisk="false"
       memoryStoreEvictionPolicy="LRU"/>

参数说明:

  • name:缓存名称,(唯一)必填,后面会用到
  • maxElementsInMemory:内存中缓存元素最大个数。
  • eternal:缓存中对象是否永久保存。如果是,超时设置将被忽略,对象从不过期。
  • timeToIdleSeconds:缓存对象失效前允许闲置时间(TTI,单位:秒)。默认值是0,也就是可闲置时间无穷大。
  • timeToLiveSeconds:缓存数据的最长生存时间(TTL单位:秒),默认值是0就意味着元素可以停顿无穷长的时间。
  • overflowToDisk:内存空间不足后是否可以使用磁盘,不推荐。
  • memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。

API方式

通过依赖注入获取CacheManager对象:

@Autowired	
private CacheManager cacheManager;

将CacheManager对象转换成EhCacheCacheManager对象(必须的,要忘记):

EhCacheCacheManager ecm = (EhCacheCacheManager)cacheManager;
然后,通过name获取Cache对象:
//Collection<String> cacheNames = ecm.getCacheNames();
Cache cache = ecm.getCache("user");

cache中的元素是以键值对的形式存储的,可以通过其get()方法获取,put()方法存入:

String key = "time";
ValueWrapper valueWrapper = cache.get(key);
if (valueWrapper == null) {
	cache.put(key, LocalDateTime.now());
}else {
	System.out.println((LocalDateTime)valueWrapper.get()); 
}

注意:Spring中Cache对象get()返回的还是一个ValueWrapper对象(为了提供一套对外一致的API),还需要再get() 一次才能获取到真正的缓存值,且类型为Object。也可以使用泛型方法,指定缓存对象类型:

LocalDateTime time = cache.get(key, LocalDateTime.class);

以上方式,通常用于缓存从数据库中获取的对象。但我们的项目已使用Hibernate的二级缓存技术,所以再API缓存的意义不大……

注解缓存

首先需要在springmvc-servlet.xml中配置使用cache annotation:
<cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>

说明:

  • cache-manager:前文所述springmvc-servlet.xml中指定CacheManager的SpringBean的id
  • proxy-target-class:基于类(而不是接口)的代理是否起作用(复习:Spring:AOP)。
    #理解:注解能够起作用,归功于AOP代理#

PS:附带的xmlns

xmlns:cache="http://www.springframework.org/schema/cache"
和xsi:schemaLocation
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd 

@Cacheable

在一个(public)方法上添加注解@Cacheable,就可以把该方法参数和返回结果作为一个键值对存放在缓存中。下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。

@Cacheable(cacheNames = "user", key = "#root.methodName")
protected LoginStatusModel setLoginStatus() {
  • cacheNames是必须的,注意它是一个复数,即:可以使用多个cache……
    public class Cache {	
    	static class Names{
    		final static String Article = "article";
    
  • key指的是缓存的键。这里使用了

SpEL表达式

Spring Expression Language,Spring表达式语言。

可以简单的理解成Spring可理解可解析的一种语言表达形式,常用于Cache注解的包括:

  • #root.method, #root.target, 和 #root.caches 分别指代 当前方法, 调用对象, 和受影响的缓存
  • #root.methodName和#root.targetClass两个简写也是OK的
  • 方法参数可以用下标索引,不然第二个方法参数就可以通过#root.args[1]、#p1或者#a1指明,也可以#后直接跟方法名
public class Cache {	
	static class Keys{
		final static String Prefix = "#root.targetClass.Name+'-'+#root.methodName+'-'+";
为了调试或追踪缓存运行情况,有时候需要

使用log

在log4j.xml中添加:
<logger name="net.sf.ehcache" level="all">
	<AppenderRef ref="FileSQL" />
</logger>
<logger name="org.springframework.cache" level="all">
	<AppenderRef ref="FileSQL" />
</logger>
#体会:默认使用包名做logger……#

演示:@Cacheable生效,但

Adding cacheable method 'setLoginStatus' with attribute:被添加了n次,因为setLoginStatus()是基类方法,被n个子类继承……

handler method

要作用于(返回ModelAndView或String的)handler method!

@想一想@:为什么?

演示:给model添加attribute的代码不会执行

@Cacheable(cacheNames = "user" , key = "#root.methodName" )
public ModelAndView on(Model model) {

Key生成器

理论上可以省略@Cacheable中的属性key声明,但是这样的话就会直接使用Spring默认的SimpleKeyGenerator:

public class SimpleKeyGenerator implements KeyGenerator {
	@Override  public Object generate(Object target, Method method, Object... params) {
		return generateKey(params);
比较坑爹: 不管调用方法的对象(target)和方法(method)的……所以任何方法只要有相同的参数(param)就会生成相同的key?!

所以,我们一般都使用自定义的KeyGenerator :

@Component(Cache.KeyGenerator.Name)
public class FullCacheKeyGenerator extends SimpleKeyGenerator {
	@Override
	public Object generate(Object target, Method method, Object... params) {	
		String paramsKeyPart = null;
		return String.join("-", 
				target.getClass().getName(),
				method.getName() ,
				paramsKeyPart);
@Component(Cache.KeyGenerator.Name)中指定的字符串可用于:
public class Cache {	
	static class KeyGenerator{
		final static String Name = "fullCacheKeyGenerator";
如何利用利用params生成相应的key,实际开发中有很多种方式。这里出于简单展示的目的,可以利用其基类的方法:
if (params.length > 0) {
	if (params.length == 1) {
		paramsKeyPart = String.valueOf(super.generateKey(params).hashCode());
	}else {
		paramsKeyPart = ((SimpleKey)(super.generateKey(params))).toString();	
	}			
}//else nothing

@CacheEvict

演示:修改文章后仍然显示缓存的过期数据

解决方案:利用@CacheEvict,每当文章被修改,就删除缓存数据。

@CacheEvict(cacheNames = Cache.Names.Article, keyGenerator = Cache.KeyGenerator.Name, 
	condition = "#root.target.canEvict(#p0)", beforeInvocation = true)
public SingleModel GetById(int id) {
需要设置一个条件(condition),否则每次调用都会删除该方法对应的缓存。condition中使用的仍然是SpEL,调用了当前对象的方法:
public boolean canEvict(int id) {
	return editedArticleIds.contains(id);
editedArticleIds是一个静态字段,记录所有应该被evict的文章id:
static Set<Integer> editedArticleIds = new HashSet<>();
哪些文章id应该被加入/移出editedArticleIds呢?
  • 编辑文章时:
    public SingleModel GetById(int id) {
    	editedArticleIds.remove(id);
  • 生成缓存时:
    public void Edit(NewOrEditModel model, int id) {
    	editedArticleIds.add(id);

@想一想@:能不能使用API直接删除某条缓存数据?

PS:其他cache相关注解@CachePut、@Cacheable、@CacheConfig..等略


异步方法

在Spring中,被添加了@Async注解的方法,就是异步方法:
@Async
public void sendEmail() {
	System.out.println("send email ……");	

它的本质是依赖于多线程,即:对注解了@Async的方法另开一个线程予以执行。

要让异步生效,还需要在中开启:

<task:executor id="asynExecutor" pool-size="5" />
<task:annotation-driven executor="asynExecutor" />
xmlns:task="http://www.springframework.org/schema/task"

http://www.springframework.org/schema/task 
http://www.springframework.org/schema/task/spring-task-4.0.xsd

对比演示:

  • sendEmail()的执行顺序:
    public ModelAndView input(
    	userService.sendEmail();
    	System.out.println("after userService.sendEmail()");
  • 使用了不同的线程
    System.out.println("currentThread() in xxx:" + Thread.currentThread().getId());

诸如发送email、log记录等和HTTP响应无关,不需要返回值的方法是最适合使用@Async注解的。

一些极其特殊的情况,我们可能需要异步获得方法的返回值,这时候:

  • 方法返回值需要用CompletableFuture<>或Future<>包装
    @Async
    public CompletableFuture<Boolean> sendEmail() {
        	return CompletableFuture.supplyAsync(()->true);
    
  • 获取返回值时:
    userService.sendEmail().thenAccept(r->System.out.println(r));

但是,在我们SessionPerRequest的架构中,不要在异步方法中通过绑定request声明周期的session/entityManager获取数据库的值。因为当异步方法执行时,很可能request已经被释放!演示:

Error creating bean with name 'scopedTarget.emWrapper': Scope 'request' is not active for the current thread;


Ajax

Ajax请求仍然是一个HTTP请求,如果要求的响应是HTML片段的话,后台的处理和“响应一个完整的HTML页面”没有区别。比如通过Ajax加载消息栏内容:

<p b-notification></p>
<script>
	$(document).ready(function () { 
		$('[b-notification]').load("/17bang/Notification/GetRandom");
	})	
</script>

后台一样用MVC模式,将/Notification/GetRandom请求dispatch到NotificationController的GetRandom()方法而已……

@Controller
@RequestMapping(URLMapping.Notification.Base)
public class NotificationController extends BaseController {
	@RequestMapping(URLMapping.Notification.GetRandom)
	public String GetRandom() {		
		//这里你就可以为所欲为啦!
		return URLMapping.Notification.Base + URLMapping.Notification.GetRandom;

对SpringMVC而言,响应Ajax请求只有一个新知识点:

返回JSON

首先需要在handler method上添加@ResponseBody注解(复习:动态文件输出

@ResponseBody
public boolean GetRandom() {    //用boolean表示是否有新消息……
演示:500错误,没有converter

PS:使用String作为返回值解决不了问题,因为String格式的false仍然会被认为是true:

$.get("/17bang/Notification/GetRandom", function(result){
	if(result){    //"false" => true
		$('[b-notification]').text("有新消息啦!");
	}
});

所以需要使用maven引入jackson(注意版本和tomcat匹配):

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.9.7</version>
</dependency>

无论是基本类型数据(比如int、boolean……)还是自定义类型,都可以通过jackson进行转化:

public MineModel GetRandom() {
	return new MessageService().Get();

PS:请求为JSON格式的场景在SpringBoot中演示错误处理

错误处理

Ajax请求中出现错误,就不宜再进行页面跳转,而是应该返回一些错误信息。否则无法在前台击中Ajax的error handler:

$.ajax({
	//...以上略
	error : function(jqXHR, textStatus, errorThrown) {
		console.log(errorThrown);
	}
})
F12演示:302状态码……

可以使用两种方式:

  • 直接使用response的方法:
    response.sendError(500, "系统错误……");
  • 如果返回值是ModelAndView的话,还可以:
    mv.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);

演示:

  • response的状态码为500,
  • error handler被击中

但上述方法不能很方便的给予详细的错误消息(比如status和message)。所以有时候项目/架构会要求用JSON数据返回错误信息,这时候根据请求返回的类型,如果:

  • 本来就是要直接返回(JSON格式的)Model的,
    public MineModel GetRandom() {
    
    可以让Model都继承自BaseModel,
    public class MineModel extends BaseModel {
    在BaseModel中添加status和message,
    public class BaseModel {
    	private String status = "success";
    	private String message = "OK";
    最后在catch中设置
    model.setStatus("fail");
    model.setMessage("系统错误,巴拉巴拉……");
  • 本来是要返回HTML内容的,
    public ModelAndView GetRandom() {
    需要“凭空生成”一个view,
    MappingJackson2JsonView view = new MappingJackson2JsonView();
    mv.setView(view);
    
    这样model中的值就可以作为JSON内容返回给前端了
    mv.addObject("status", "fail");
    mv.addObject("message", "系统错误……");
但这种方式response的status code是200,所以Ajax的error handler一样不会被触发,前台代码大致是这样的:
$.ajax({
	success: function(result){
		if(result.status == "fail"){    
			console.log(result.message);			
		}else{
如果是在@ExceptionHandler方法中统一处理,就需要首先判断一个请求

是不是Ajax请求

根据请求头中x-requested-with的值是否为XMLHttpRequest确定:

public static boolean isAjaxRequest(HttpServletRequest request) {
	String requestedWith = request.getHeader("x-requested-with");
	return requestedWith != null && 
		requestedWith.equalsIgnoreCase("XMLHttpRequest");
}

然后就可以在@ExceptionHandler方法中:

if (isAjaxRequest(request)) {


作业

  1. 利用缓存优化项目性能。但注意:
    1. 文章/评论发布之后要能及时更新相应的列表页
    2. (仅ASP.NET)同一个页面,不同的当前用户会有些许差别,比如文章作者访问他自己的文章单页,就会有“修改”链接。这种差异在应用了缓存之后如何体现?
  2. 使用@Async优化用户体验和项目性能。
  3. 使用Ajax完成以下功能:
    1. 异步加载呈现:消息栏/侧边栏关键字
    2. 点赞和踩(一篇文章一个用户只能点一次赞或踩),注意:
      • 点赞成功后显示的应是后台获取的即时数据,不是简单的在原赞数上加1
      • 防止CSRF攻击
    3. 发布评论,注意:
      • 一次性的收集所有form表单内容
      • 使用hidden input向后台传递评论的文章id
      • 文本内容过滤,防注入
      • 重用【难】:发布成功过后的新评论 和 评论列表里的每一条
    4. 通过获得JSON数据实现:
      1. 文章发布时添加一个新的分类
      2. 注册时即时检查用户名是否已使用
    5. 回复评论【难】,注意:
      • 在页面呈现时确定楼层数,并将楼层数传递到前端
      • 回复评论时显示的是楼层数,但传递到后台的、表明其引用的,还是Id
      • @想一想@:能否和发布评论重用?
    6. 当前用户有未读消息时铃铛闪烁,没有时不闪烁。注意性能优化【难】:如果铃铛已经闪烁,除非打开过我的消息页面,否则就不用再轮询查询……
    7. 调整文章顺序:
      • 在拖拽结束后发起Ajax请求
      • 后台【难】:为文章建立双向链表,每个文章都有Prev和Next属性,调整文章顺序的实质是链表节点的插入……
    8. 首页即时聊天【难】:
      每隔若干秒查询有无新的聊天记录。怎么才算“新”?所以要记录
  4. 功能索引页sample.17bang.ren其他未实现之功能

缓存 异步 Ajax
觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 Java Web开发 中继续学习:

我们的 特色

  • 面向就业
  • 线上线下结合
  • 同学互助
  • 师生共赢

报班 QQ群:273534701

  • 获取视频资料
  • 要求作业点评
  • 参加阶段性测试
  • 模拟面试