继续贯彻“项目功能驱动”的原则,完成文章发布功能……
注册页面演示:
怎么解决呢?使用编码过滤器,在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一样,
就可以监听对象的生命周期和属性变化事件:
System.out.println(request.getServletContext().getContextPath());
一般可用于:
演示:在/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 protected void setLoginStatus(Model model) {
这样,setLoginStatus()方法就会在controller中所有handler methods调用前被调用。
还可以注解有带返回值的方法:
@ModelAttribute("loginStatus") protected LoginStatusModel setLoginStatus() { return userService.GetLoginStatus(); }也可以在单个子controller中使用,实现各式各样的目的……(比如权限验证)
拦截器,可以对handler method调用进行更细节的控制,在:
插入自定义的逻辑。
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返回值:
if (userService.GetLoginStatus() != null) { return true; }
//重定向到登录页面,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过滤器。
mvc:mapping中path可以使用通配符,注意这里的通配符比较奇怪(ant-style):
<mvc:mapping path="/article/*" />表示访问所有/article/开头的路径,都要使用NeedLogOn过滤器。
<mvc:mapping path="/**" />表示所有路径。
还可以使用mvc:exclude-mapping排除部分路径,比如:
<mvc:exclude-mapping path="/article/page-*" />
注意:
再加一个需求,除了判断是否登录,还要检查用户的角色(role);而且不同的页面,检查不同的角色,比如:
怎么办?声明一个属性:
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(),是不是还有改进空间?
发布文章的本质,其实就是持久化一个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) {
protected EntityManager getEntityManager() { EntityManagerFactory entityManagerFactory = emFactoryContainer.getNativeEntityManagerFactory(); return entityManagerFactory.createEntityManager(); }
@想一想@:之前的文章发布,是不是漏了什么?文章的作者!
#体会:层层防御# 假如:
@NotNull private User Author;
User author = GetCurrentUser(); if (author == null) { throw new IllegalArgumentException("当前用户为空"); }
演示:使用当前用户作为作者,发布文章
article.setAuthor(author);
多次调用getEntityManager()带来的问题,同一个请求中:
if (request.getAttribute("em")== null) { request.setAttribute("em", entityManager); } return (EntityManager)request.getAttribute("em");但既然是在Spring框架中,干嘛不利于一下SpringBean的request scope设置(复习)呢?
@想一想@:现在我们的EntityManagerFactory是不是singleton的?为什么?如何验证?
首先解决
的问题
@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始终都是同一个对象
同样的道理,我们再
@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:不用担心只读(只有查询)操作声明事务会影响性能:无论事务声明事务,所有的数据库操作都默认是在事务中的
在何时关闭呢?
1、可以在SpringBean(即:emWrapper)销毁(destroy)时提交:
@PreDestroy public void preDestroy() {
public class EmWrapper implements DisposableBean {
@Override public void destroy() throws Exception {顾名思义,destroy应在preDestroy之后执行(syso演示)
try { transaction.commit(); } catch (Exception e) { transaction.rollback(); throw e; } finally { em.close(); }
但(至少看起来)更高效的方法是在handler method结束之后,因为
这就需要使用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" };
还需要正则表达式(涉及命名分组、零宽断言等高级语法):
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:
if (id == null) { title ="文章发布"; article = new NewOrEditModel(); }else { title = "修改文章"; article = service.GetEdit(id); }
因为:
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对象
多快好省!前端后端,线上线下,名师精讲
更多了解 加: