键盘敲烂,月薪过万作业不做,等于没学
当前系列: 从JSP到Spring 修改讲义

继续贯彻“项目功能驱动”的原则,完成文章发布功能……


filter和listener

注册页面演示:

  • 已经在“所有”地方(页面/springmvc-servlet.xml)设置了编码格式
  • 但仍然有汉字乱码问题

怎么解决呢?使用编码过滤器,在server.xml中配置:

<filter>
	<filter-name>characterEncodingFilter</filter-name>
	<!-- 该过滤器由Java类org.springframework.web.filter.CharacterEncodingFilter实现 -->
	<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
	<!-- 强制(force)使用UTF-8的编码(encoding) -->
	<init-param>
		<param-name>encoding</param-name>
		<param-value>UTF-8</param-value>
	</init-param>
	<init-param>
		<param-name>forceEncoding</param-name>
		<param-value>true</param-value>
	</init-param>
</filter>	
<filter-mapping>
	<!-- 对所有请求进行过滤 -->
	<filter-name>characterEncodingFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

背后原理:

  • filter是servelet的功能,所以要在web.xml中配置
  • filter在servelet接收到request之后,在response返回客户端之前被调用
F3演示:CharacterEncodingFilter实现了接口Filter

listener

和filter一样,

  • 是servlet的功能,
  • 定义一个实现某接口(详见:Servlet Listener的类
  • 在web.xml中注册,

就可以监听对象的生命周期和属性变化事件:

  • ServletContext:servelet启动后生成、全局共享、直到servelet停止
    System.out.println(request.getServletContext().getContextPath());
  • HttpSession:同session,单个用户独享
  • ServletRequest:从请求到达servelet开始生成,生成response时结束

一般可用于:

  • 统计:比如通过request的生成统计有多少访问量,用session的生成统计用户的在线时长……(但现在一般都由第三方插件完成)
  • 缓存:在上述对象生成时加载缓存并记录变化,在对象销毁时将变化同步到数据库(类似于hibernate的em和session,但需要考虑的因素太多,使用风险较大……)
  • ……


BaseController

演示:在/article/new中引入_layout,登录状态栏无法显示“欢迎……”字样

@想一想@:为什么?因为/article/new的handler method中没有:

model.addAttribute("loginStatus", loginStatus);
但是,难道我们要在所有的handler methods中这样复制粘贴吗?

引入基类:

public class BaseController {
public class ArticleController extends BaseController {
再抽象出方法在controller子类中调用:
@Autowired
protected IUserService userService;
protected void setLoginStatus(Model model) {
	LoginStatusModel loginStatus = userService.GetLoginStatus();
	model.addAttribute("loginStatus", loginStatus);
}


@ModelAttribute

但我们其实有更好的办法:在setLoginStatus()方法上添加@ModelAttribute
@ModelAttribute
protected void setLoginStatus(Model model) {

这样,setLoginStatus()方法就会在controller中所有handler methods调用前被调用。

还可以注解有带返回值的方法:

@ModelAttribute("loginStatus")
protected LoginStatusModel setLoginStatus() {
	return userService.GetLoginStatus();
}
也可以在单个子controller中使用,实现各式各样的目的……(比如权限验证


interceptor

拦截器,可以对handler method调用进行更细节的控制,在:

  1. preHandle:方法调用之前
  2. postHandle:方法调用之后
  3. afterCompletion:request请求处理完成(或页面生成)之后

插入自定义的逻辑。

实现方式

声明一个类,继承自HandlerInterceptorAdapter:
public class NeedLogOn extends HandlerInterceptorAdapter {

或者,直接实现HandlerInterceptor接口

F3演示:HandlerInterceptorAdapter也是实现了HandlerInterceptor接口,但里面都没啥内容……

@想一想@:这个抽象类的作用是什么?(如果接口方法不是default的话),就可以只override需要override的方法即可!比如:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

因为里面要通过依赖注入使用到:

@Autowired
protected IUserService userService;
所以不要忘了在类上添加@Component注解

注意这个boolean返回值:

  • true:继续处理请求
    if (userService.GetLoginStatus() != null) {
    	return true;
    }
  • false:不再继续后面的流程,由方法内部确定如何响应
    //重定向到登录页面,url参数带上“之前页面”
    response.sendRedirect(
    		request.getContextPath() + 
    		"/log/on?prepage=" + 
    		request.getRequestURI());
    return false;

然后,还是要配置,^_^,在springmvc-servlet.xml中:

<mvc:interceptors>
	<mvc:interceptor>
		<mvc:mapping path="/article/new" />
		<bean class="controllers.NeedLogOn"></bean>
	</mvc:interceptor>
</mvc:interceptors>

意思是当访问/article/new时,就使用NeedLogOn过滤器。

mapping-path

mvc:mapping中path可以使用通配符,注意这里的通配符比较奇怪(ant-style):

  • *代表0个或多个字符但不包含反斜杠;
    <mvc:mapping path="/article/*" />
    表示访问所有/article/开头的路径,都要使用NeedLogOn过滤器。
  • **才能代表包含所有字符
    <mvc:mapping path="/**" />
    表示所有路径。

还可以使用mvc:exclude-mapping排除部分路径,比如:

<mvc:exclude-mapping path="/article/page-*" />

注意:

  • 如果请求的url本身就匹配不到正确的handler method,不会进行拦截
  • 当项目规模增加、页面变多之后,配置会变得复杂,务必注意其整洁清晰
  • 对权限类interceptor,可以使用白名单策略,即首先声明全部都要拦截,然后再exclude不需要拦截的path
  • 可以考虑能够使用能够直接在controller或handler methods上直接注解的AOP或AspectJ技术

添加属性

再加一个需求,除了判断是否登录,还要检查用户的角色(role);而且不同的页面,检查不同的角色,比如:

  • /article/admin需要blogger的角色,
  • /approve需要admin的角色

怎么办?声明一个属性:

private String role;
在xml中赋值:
<bean class="controllers.NeedLogOn">
	<property name="role" value="admin"></property>
</bean>
演示:java代码中能拿到role值:
System.out.println(getRole());
@想一想@:@ModelAttribute和NeedLogOn中重复调用userService.GetLoginStatus(),是不是还有改进空间?


Repository的继承

发布文章的本质,其实就是持久化一个Article对象,我们马上能想到的就是ArticleRepository.Save()方法:

但UserRepository好像也有类似的方法?能不能重用一下呢?

抽象出一个BaseRespository,利用泛型,还可以加一个约束:

public class BaseRepository<T extends BaseEntity<Integer>> {

封装所有entity都会用到的基本方法:

  • 增:
    public void Save(T entity) {
  • 删:
    public void Remove(T entity) {
  • 取:
    public T Get(Class<T> entityClass, Integer id) {
    public T Load(Class<T> entityClass, Integer id) {
演示:将EntityManager对象的生成封装起来……
protected EntityManager getEntityManager() {
	EntityManagerFactory entityManagerFactory = emFactoryContainer.getNativeEntityManagerFactory();
	return entityManagerFactory.createEntityManager();		
}


SessionPerRequest

@想一想@:之前的文章发布,是不是漏了什么?文章的作者!

#体会:层层防御# 假如:

  • 数据库authorId字段有NOT NULL的约束
    @NotNull
    private User Author;
  • Service层有current user的检查
    User author = GetCurrentUser();
    if (author == null) {
    	throw new IllegalArgumentException("当前用户为空");
    }

演示:使用当前用户作为作者,发布文章

article.setAuthor(author);

多次调用getEntityManager()带来的问题,同一个请求中:

  • 多次调用生成EntityManager对象,影响性能
  • 生成多个事务,影响HTTP请求的完整性
怎么办呢?一个request请求总是使用一个entityManager/session(OneEm/SessionPerRequest)就可以搞定。

利用SpringBean

可以自己编码,利用request对象的scope(作用域/生命周期):
if (request.getAttribute("em")== null) {
	request.setAttribute("em", entityManager);	
}
return (EntityManager)request.getAttribute("em");
但既然是在Spring框架中,干嘛不利于一下SpringBean的request scope设置(复习)呢?

@想一想@:现在我们的EntityManagerFactory是不是singleton的?为什么?如何验证?

首先解决

EntityManagerFactory

的问题

@Component
public class EmFacWrapper {
	@Autowired	
	protected LocalContainerEntityManagerFactoryBean emFactoryContainer;
	
	public EntityManagerFactory expose() {
		return emFactoryContainer.getNativeEntityManagerFactory();
	}
}

现在EmFacWrapper是singleton(默认)的,所以其LocalContainerEntityManagerFactoryBean及其expose()暴露出来的EntityManagerFactory也只会有一个。

在BaseRepository中:

@Autowired
private EmFacWrapper emFacWrapper;
protected EntityManager getEntityManager() {
	EntityManagerFactory entityManagerFactory = emFacWrapper.expose();		
	System.out.println(entityManagerFactory.hashCode());		

演示:entityManagerFactory始终都是同一个对象

同样的道理,我们再

封装EntityManager

声明一个EmWrapper类,其中注入EmFacWrapper:
@Component
@Scope(scopeName = "request" , proxyMode = ScopedProxyMode.TARGET_CLASS )
public class EmWrapper {
	@Autowired
	private EmFacWrapper emFacWrapper;
指定EmWrapper的scope为request!

@想一想@:为什么要指定proxyMode?

然后,封装/暴露EntityManager对象:

private EntityManager em;
public EntityManager expose() {
	System.out.println("this:" + this.hashCode());
	if (em == null) {
		EntityManagerFactory entityManagerFactory = emFacWrapper.expose();
		System.out.println("emf:" + entityManagerFactory.hashCode());
		em = entityManagerFactory.createEntityManager();
		System.out.println("em:" + em.hashCode());
	} // else nothing
	return em;
}
最后,在BaseRepository中:
@Autowired
private EmWrapper emWrapper;
protected EntityManager getEntityManager() {
	return emWrapper.expose();
}

这样,因为emWrapper是request scope的,所以我们拿到的EntityManager也必然是request scope的。

现在我们只打开了数据库连接,没有关闭!更关键的是,没有事务的声明和提交。


开启事务

事务的开始很简单:
private EntityTransaction transaction;
transaction = em.getTransaction();
transaction.begin();
PS:不用担心只读(只有查询)操作声明事务会影响性能:无论事务声明事务,所有的数据库操作都默认是在事务中的

在何时关闭呢?

destroy

1、可以在SpringBean(即:emWrapper)销毁(destroy)时提交:

  • 注解@PreDestroy
    @PreDestroy
    public void preDestroy() {
  • 实现DisposableBean
    public class EmWrapper implements DisposableBean {
    @Override public void destroy() throws Exception {
    顾名思义,destroy应在preDestroy之后执行(syso演示)
  • xml配置:略
然后在destroy方法中:
try {
	transaction.commit();
} catch (Exception e) {
	transaction.rollback();
	throw e;
} finally {
	em.close();
}

postHandle()

但(至少看起来)更高效的方法是在handler method结束之后,因为

  • 这符合事务尽早提交的原则,且
  • 按我们的框架,此后模板使用的是和数据库无关的model,不再需要任何数据库连接

这就需要使用interceptor!

声明一个类:

public class EmPerRequest extends HandlerInterceptorAdapter {

直接注册在mvc:interceptors下,表示所有的HTTP请求都要使用该filter

<mvc:interceptors>
	<bean class="controllers.inteceptors.EmPerRequest"></bean>
定义其postHandle()方法:
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, 
应在其中调用service的方法完成事务提交。
service.Commit();
简洁起见,我们选用UserService,并直接依赖注入EmWrapper字段并调用它的Commit()方法
private EmWrapper emWrapper;
public void Commit() {
	emWrapper.Commit();

emWrapper的Commit()方法极度类似上文destroy方法。

实际开发中,为了避免各种乱七八糟、稀奇古怪的问题,还会加上更精准的if()判断,以及进入else非正常分支的日志……

if (transaction != null) {

		if (transaction.isActive()) {

		if (em.isOpen()) {

			transaction = null;
			em = null;
		} else { 
	}
}else {

事务的回滚见……


正则过

复习:

准备好测试用输入字符串:

<img src=/logo.png onclick="alert('hhaha')" /><p>
	<span
 style="font-size:16px;">@Autowired</span> 
</p><p><script>location="https://17bang.ren"</script>
	<strong>@Primary</strong><span style="font-size:16px;">注解使其优先被选择</span> 
</p>

为了便于重用,可以专门新建一个HtmlFilter类

public class HtmlFilter {

我们的策略是先找到标签,然后替换掉其中非白名单上的标签和属性,所以先准备标签(tags)和属性(properties)的白名单:

static String[] tags = new String[] { "span", "img", "p", "strong" };
static String[] properties = new String[] { "style", "src" };

还需要正则表达式(涉及命名分组零宽断言等高级语法):

  • 标签:开合一网打尽,分组中确定标签(tagName)
    static String regexOfTags = "</?(?<tagName>[^>/\\s]*)[\\s\\S]*?>";
    
  • 然后在标签里找属性 (作业:略)

最后进行匹配(find())和替换(appendReplacement()):

if (Arrays.binarySearch(tags, tag) < 0) {				
	matcher.appendReplacement(sb, matcher.group()  //(666,赞……) 
	        .replace(tag,"bad"));				
}//else nothing

为了能使用二分查找快速的比对,我们需要事先对tags和properties进行排序

static {
	Arrays.sort(tags);
	Arrays.sort(properties);
}


文章编辑

@想一想@:要不要发布和编辑共用一个模板?

如果共用的话

首先要何必URL映射(dispatch):

@GetMapping({URLMapping.Article.New, URLMapping.Article.Edit})
public String NewOrEdit(Model model, @PathVariable Integer id) {
如何区分是发布还是编辑?只能根据URL的path中有无id:
  • /article/new:没有id
  • /article/edit/2:id=2
if (id == null) {
	title ="文章发布";
	article = new NewOrEditModel();			
}else {
	title = "修改文章"; 
	article = service.GetEdit(id);			
}

@NotNull的影响

因为:

  • 之前我们在数据库表article中插入了一条没有author的数据,
  • 然后又在Article entity的author上标记了@NotNull
  • 现在使用Hibernate entityManager的find()方法
就会生成inner join的SQL语句:
    from
        Article article0_ 
    inner join
        User user1_ 
            on article0_.Author_id=user1_.id 

造成取不出该条article的结果!

编辑

不要忘了,确定当前用户的权限:要么是文章的作者,要么……

if (article.getAuthor() != current) {
	throw new IllegalArgumentException("当前用户(id=)不是文章(id=)的作者(id=)");			
}
因为我们已经实现了SessionPerRequest模式,所以可以确信两个User对象是可以直接比较的;否则的话,应该比较id:
if (article.getAuthor().getId() != current.getId()) {
最后,使用正确的convert方法,修改而不是发布文章
articleConvert.toArticle(model, article);

演示:使用Ctrl+Shift+D打开debug shell面板,调试Load()出来的proxy对象


作业

  1. 修改密码,注意修改之前检查用户输入的原密码是否正确
  2. 用一个(或多个)独立页面实现文章分类(有标题和说明)的添加/删除/修改功能,注意:
    1. 用户只能操作自己的分类
    2. @想一想@:要不要对用户输入进行HTML编码?
    3. 删除某分类之后,以前属于这个分类的文章怎么办?
  3. 过滤用户输入:
    1. 非法标签进行HTML编码
    2. 非法属性改为bad
    3. 文章内容中(被<pre></pre>框住)的代码部分内容不要过滤,而是HTML转义
    4. 将外链的href="https://17bang.ren/Code/831"替换为href="/outsite?url=https://17bang.ren/Code/831"
    5. 自动提取255字以内摘要(去除html和空格等)
  4. 文章发布/编辑时:(可在学习了下一章之后完成
    1. 被归入选中的分类
    2. 包含关键字(发布和编辑时不一样),学有余力的同学推荐这种分级样式

学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

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

在当前系列 从JSP到Spring 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码