小例子说明

Spring提供了很好的架构和很方便的工具,在作为工具使用的同时,也应注意正确使用spring的架构,虽然不是强制的,但是这是spring的精髓。用spring,也要用spring的框架。例如在某次的code review中,看到了在一个Controller中注入另一个controller实例,这种组织方式是凌乱,无法理解一个controller为何是另一个controller的属性,为何有这种从属关系。显然可以通redirect view的方式避免这种情况,又或者将其纳入也作为本UI的一部分。我们需要注意每个UI呈现都应该是独立的,不应该出现交叉。我们要习惯对象编程,不要仅仅为了让代码调通,就随便加入一个参数,关键的衡量在于这个参数是否是这个类或者这个对象的一个属性。如果不是,又需要加入,那么就要考虑你的代码组织或者设计是否存在问题。

小例子模拟论坛,用户可以在上面发布贴(discussion),用户也可以跟帖(reply)。UI界面如下:

数据设计

数据设计很重要,和业务需求密切相关,先看看对贴的需求:

  1. 用户名采用email格式
  2. 帖列表按最后更新(跟帖)时间排序 ➤ 需要维护一个创建时间和一个更新时间
  3. 创建表和跟帖的用户视为订购了这个贴,一旦有更新,需要进一步的操作(例如邮件提示,websocket推送等等)➤ 需要维护要给subscribe的列表
  4. 为了便于搜索引擎搜索,帖子除了id号外,根据subject生成一个uri safe subject,/discussion/{id}/{uriSafeSubject}。➤ 需要uriSafeSubject

更过需求分析,很容易得到Discussion的数据结构

public class Discussion{private long id;private String user;private String subject;private String uriSafeSubject;private String message;private Instant created;private Instant lastUpdated;private Set<String> subscribedUsers = new HashSet<>();// mutators and accessors
}

对跟帖的需求比较简单

  1. 是哪楼的跟帖 ➤ 有一个顺序的id号
  2. 需要知道是谁,什么时候的跟帖 ➤ 需要user和创建时间
  3. 和discussion间的关系,需满足双向检索
    • Discussion和Reply有上下级的归属关系 ➤ 通过discussionId,查找到贴下属的reply
    • 新增Reply会影响Discussion的更新,包括最后更新时间,订购用户列表。➤ 通过discussionId,查找跟帖归属的discussion

Reply的数据结构:

public class Reply{    private long id;private long discussionId;private String user;private String message;private Instant created;// mutators and accessors
}

这些数据结构我们可以统一存放在一个package中,例如cn.wei.chapter14.site.entity,entity是因为他们是我们数据操作的实体。

需要什么样的UI

小例子很简单,就连个事情:

  1. 贴的管理 --》BoardController,具体而言,显示帖子列表,创建帖子
  2. 跟帖的管理 --》ReplyController,具体而言,查看帖子(含提交跟帖的form),发布跟帖

Controller要进行什么UI的互动

我们很明确知道UI互动的要求,可以给出下面的Controller代码,暂无具体实现(暂时返回值为null)。

@Controller
@RequestMapping("discussion")
public class BoardController{//显示帖列表@RequestMapping(value = {"", "list"}, method = RequestMethod.GET)public String listDiscussions(Map<String, Object> model) { return null; }//进入创建帖页面@RequestMapping(value = "create", method = RequestMethod.GET)public String createDiscussion(Map<String, Object> model) { return null; }//创建帖@RequestMapping(value = "create", method = RequestMethod.POST)public View createDiscussion(DiscussionForm form) { return null; }}
//每个帖子都有一个id号,在路径在携带该id号,进入该贴
//id号位整数,使用正则表达式规范:\\d+ ,由于java String的写法,相当于正则表达式 \d+,\d表示数字,+表示1~N个
@Controller
@RequestMapping("discussion/{discussionId:\\d+}")
public class DiscussionController{//查看帖子(含提交跟帖的form)@RequestMapping(value = {"", "*"}, method = RequestMethod.GET)public String viewDiscussion(Map<String, Object> model,@PathVariable("discussionId") long id) { return null; }//发布跟帖 @RequestMapping(value = "reply", method = RequestMethod.POST)public ModelAndView reply(ReplyForm form, @PathVariable("discussionId") long id) { return null; }
}

设计Form的数据结构

一再强调,Form是和用户的UI交互,不等同业务逻辑数据。在业务逻辑中的贴(Discussion)的id,创建时间,更新时间,订购用户列表都是自动设置或更新的,跟帖(Reply)的时间、id、uriSafeSubject也是自动设置和更新的。

public class DiscussionForm {private String user;private String subject;private String message;... ...
}
public class ReplyForm {private String user;private String message;... ...
}

设计service的接口

抽象接口

我们可以直接提供service的实例,但仍建议进行抽象,如同我们第一个spring的例子。将service的接口抽象处理,有下面的好处:

  1. 明确层次边界,当项目越大,越能感受到好处
  2. controller的代码编写可以无需关注service的具体实现
  3. 可以有不同的service实现,方便controller调测和测试

我们已经设计了数据结构,这是我们处理的实体,在这个例子中,显然不同实体各有一个接口。接口的设计要考虑业务逻辑,我们在之前已经分析了需求,对于帖子:

public interface DiscussionService {// 显示帖列表public List<Discussion> getAllDiscussions();// 在数据分析中,要求根据discussionId实现双向检索,这是discussionId检索Discussionpublic Discussion getDisscussion(long discussionId); // 保持帖子(create和update)。从UI互动来看,似乎用createDiscussion()更为合适,但是业务逻辑的设计不局限于UI,对帖管理,最常规的是增删改查,其中增和改(虽然目前不含此需求)都可以使用此方法。public void saveDiscussion(Discussion discussion);
}

对于跟帖:

public interface ReplyService {// 获取帖的全部跟帖public List<Reply> getRepliesForDiscussion(long discussionId);// 保存取帖,和discussion一样,不仅仅提供create,还提供update(edit)的功能。public void saveReply(Reply reply);
}

完成controller

有了接口,我们就可以完成controller。在数据处理中,有自动生成或更新的部分,例如创建时间,用户列表,更新时间,这些应该留在service的具体实现,而不是controller中,controller只负责数据交互,实现很简单:

  1. 注入service的实例
  2. 根据service接口实现UI
@Controller
@RequestMapping("discussion")
public class BoardController {@Inject private DiscussionService discussionService;@RequestMapping(value = {"","list"}, method = RequestMethod.GET)public String listDiscussions(Map<String, Object> model){model.put("discussions", this.discussionService.getAllDiscussions());return "discussion/list";}@RequestMapping(value = "create", method = RequestMethod.GET)public String createDiscussion(Map<String, Object> model){model.put("discussionForm", new DiscussionForm());return "discussion/create";}@RequestMapping(value = "create", method = RequestMethod.POST)public View createDiscussion(DiscussionForm form){Discussion discussion = new Discussion();discussion.setUser(form.getUser());discussion.setSubject(form.getSubject());discussion.setMessage(form.getMessage());this.discussionService.saveDiscussion(discussion);        return new RedirectView("/discussion/" + discussion.getId() + "/" + discussion.getUriSafeSubject(),true,false);}
}

对于跟帖的controller,也类似,controller的代码都不复杂。

@Controller
@RequestMapping("discussion/{discussionId:\\d+}")
public class DiscussionController {@Inject private DiscussionService discussionService;@Inject    private ReplyService replyService;    @RequestMapping(value = {"", "*"}, method = RequestMethod.GET)public String viewDiscussion(Map<String, Object> model,@PathVariable("discussionId") long id){Discussion discussion = this.discussionService.getDisscussion(id);if(discussion == null)return "discussion/errorNodiscussion";model.put("discussion", discussion);model.put("replies", this.replyService.getRepliesForDiscussion(id));model.put("replyForm", new ReplyForm());return "discussion/view";}@RequestMapping(value = "reply", method = RequestMethod.POST)public ModelAndView reply(ReplyForm form,@PathVariable("discussionId") long id){Discussion discussion = this.discussionService.getDisscussion(id);if(discussion == null)return new ModelAndView("discussion/errorNoDiscussion");Reply reply = new Reply();reply.setDiscussionId(id);reply.setUser(form.getUser());reply.setMessage(form.getMessage());this.replyService.saveReply(reply);return new ModelAndView(new RedirectView("/discussion/" + id + "/" + discussion.getUriSafeSubject(),true,false));        }
}

数据处理

从上之下,我们已经完成UI层的代码,业务逻辑层的业务接口,接下来就是继续往下进行数据的具体处理。

Repository接口的设计

虽然Spring没有强制要求,但和service接口一样,我们强烈建议提供要给数据读写的接口,即仓库的接口,以便:

  1. 清晰代码的层次接口
  2. 当底层存储介质出现变化,例如内存方式,关系型数据库方式,文件方式,不影响上层的实现;此外如果数据库表格的接口出现变化,例如列的名字变更,接口仍可保持不变
  3. service层可以基于repository接口编程,无需理会具体的实现,便于调测和测试。

在本例,很明显有两个仓库接口。和service接口设计一样,仓库接口也不限定在当前service的需求,重点的数据的读写,一般而言就是增删改查。虽然小例子目前不需要修改和删除(在写底层的时候,可以比上层当前的需求要放宽),但是仓库接口中,我们也可以一并列出来,因此这是数据读写的接口。

public interface DiscussionRepository {public List<Discussion> getAll();public Discussion get(long id);public void add(Discussion discussion);public void update(Discussion discussion);public void delete(long id);
}
public interface ReplyRepository {public List<Reply> get(long discussionId);public void add(Reply reply);public void update(Reply reply);public void delete(long discussionId);
}

Service的具体实现

有了仓库接口就可以实现Service,也就是在数据读写的基础上,实现我们的业务逻辑。我们先读一下DiscussionService的实现DefaultDiscussionService,有些人习惯使用DiscussionServiceImpl表明是Service的实现,这也是不错的命名方式。同样的,和Controller实现一样,需要仓库实例的注入。

@Service
public class DefaultDiscussionService implements DiscussionService{    //【1】注入所需的仓库实例和Service实例(Service可以调用其它Service)@Inject private DiscussionRepository discussionRepository;//【2】具体实现DiscussionService的接口// 2.1】显示帖子,并根据更新时间排序。这是小例子,在实际中,更倾向于在仓库中实现排序,特别是有数据库存储的时候,且数据量非常的的时候。如果数据量有限,可以在service实现,甚至在controller实现,特别是有几个维度的排序选择的情况。虽然我们进行的逻辑分层,有些内容其实也是具有一定的弹性。@Overridepublic List<Discussion> getAllDiscussions() {List<Discussion> list = this.discussionRepository.getAll();list.sort((d1,d2) -> d1.getLastUpdated().compareTo(d2.getLastUpdated()));return list;}// 2.2】根据discussionId获取帖子@Overridepublic Discussion getDisscussion(long discussionId) {return this.discussionRepository.get(discussionId);}// 2.3】保持帖子(涵盖create和update两种情况),自动设置应在此实现,例如时间设置,注意对于Id的设置我们放置在仓库的具体实现中,id的分配有些是数据库表格自动分配的,又获取我们需要获得当前最大的id号,然后加1,因此这部分属于仓库实现。@Overridepublic void saveDiscussion(Discussion discussion) {// 获得uriSafeSubject,以提供 SEO 友好的 URI 字符串。Normalizer类提供 normalize 方法,它把 Unicode 文本转换为等效的组合或分解形式,允许对文本进行更方便地分类和搜索。Normalizer.Form.NFD是正常化,所有字符摆脱音符 (以便如 é、 ö,à 成为 e,o,a)String uriSafeSubject = Normalizer.normalize(discussion.getSubject().toLowerCase(), Normalizer.Form.NFD);.replaceAll("\\p{InCombiningDiacriticalMarks}+", "")                .replaceAll("[^\\p{Alnum}]+", "-") // 替换所有非字母数字字符由 .replace("--", "-").replace("--", "-") .replaceAll("[^a-z0-9]+$", "") // 如果全都不是a~z,0~9,设置为空,+表示一或多次,$表示匹配输入字符串的结束位置.replaceAll("^[^a-z0-9]+", ""); //开始(^)如果不是(a~z,0~9),删除discussion.setUriSafeSubject(subject);    Instant now = Instant.now();discussion.setLastUpdated(now);if(discussion.getId() < 1){ //new:creatediscussion.setCreated(now);discussion.getSubscribedUsers().add(discussion.getUser());this.discussionRepository.add(discussion);}else{ //old:update            this.discussionRepository.update(discussion);}}
}

仓库的Service试下如下。对于代码编写,如果出现if,else if,else等条件,应该习惯要给出注解

@Service
public class DefaultReplyService implements ReplyService{//【1】注入所需的仓库实例和Service实例(Service可以调用其它Service)@Injectprivate ReplyRepository replyRepostory;@Injectprivate DiscussionService discussionService;@Overridepublic List<Reply> getRepliesForDiscussion(long discussionId) {List<Reply> list = this.replyRepostory.get(discussionId);list.sort((r1,r2)->r1.getId() < r2.getId() ? -1 : 1);return list;}@Overridepublic void saveReply(Reply reply) {Discussion discussion = this.discussionService.getDisscussion(reply.getDiscussionId());if(reply.getId() < 1){ //new:user first time reply            discussion.getSubscribedUsers().add(reply.getUser());reply.setCreated(Instant.now());this.replyRepostory.add(reply);            }else{ //old: just update his reply            this.replyRepostory.update(reply);}        this.discussionService.saveDiscussion(discussion);}}

仓库的实现

这个小例子的实现,是从高处到底层,从接口到具体,最后,我们需要实现仓库。仓库一般要求数据库,我们还没有学习Hibernate,就先使用内存方式。这里也展示了如何通过接口,采用临时的方式用于调测或测试。

@Repository
public class InMemoryDiscussionRepository implements DiscussionRepository{private final Map<Long, Discussion> database = new Hashtable<>();private volatile long discussionIdSequence = 1L;    @Inject private ReplyRepository replyRepository;@Overridepublic List<Discussion> getAll() {return new ArrayList<>(this.database.values());}@Overridepublic Discussion get(long id) {return this.database.get(id);}@Overridepublic void add(Discussion discussion) {discussion.setId(getNextDiscussionId());this.database.put(discussion.getId(), discussion);        }@Overridepublic void update(Discussion discussion) {this.database.put(discussion.getId(), discussion);    }@Overridepublic void delete(long id) {this.database.remove(id);this.replyRepository.delete(id);        }private synchronized long getNextDiscussionId(){return discussionIdSequence++;}
}
@Repository
public class InMemoryReplyRepository implements ReplyRepository{private final Map<Long, Reply> database = new Hashtable<>();private volatile long replyIdSequence = 1L;@Overridepublic List<Reply> get(long discussionId) {ArrayList<Reply> list = new ArrayList<>(database.values());list.removeIf(r -> r.getDiscussionId() != discussionId);return list;}//因为hashtable是threadsafe的,当然我们也可以使用ConcurrentHashMap,所以这里没有必要同步@Overridepublic /*synchronized*/ void add(Reply reply) {reply.setId(this.getNextReplyId());this.database.put(reply.getId(), reply);}@Overridepublic void update(Reply reply) {this.database.put(reply.getId(), reply);}@Overridepublic void delete(long discussionId) {this.database.entrySet().removeIf(e -> e.getValue().getDiscussionId() == discussionId);}private synchronized long getNextReplyId(){return replyIdSequence ++;}
}

小结

小例子中,我们将spring的抽象分层概念落实到实际的代码中,如何分析需求,将需求映射到数据结构,如何从上之下(从高层到底层)逐层编写代码,如何通过接口明确各层之间的边界。

相关链接: 我的Professional Java for Web Applications相关文章

Java for Web学习笔记(六七):Service和Repository(2)抽象分层例子相关推荐

  1. Java for Web学习笔记(三五):自定义tag(3)TLDS和Tag Handler

    JSTL的TLD 这是JSTL采用的方式.TLD(Tag Library Descriptor)描述tag和function,以及具体执行的java代码tag handler.Tag Handler是 ...

  2. Java for Web学习笔记(一一八):【篇外】Soap client

    使用Eclipse自动生成Soap Client的代码 在项目按右键,选择New -> Other ->Web Services -> Web Service Client,进入向导 ...

  3. Java for Web学习笔记(一零八):再谈Entity映射(1)数据转换

    timestamp或datetime的匹配 存放毫秒 在数据库中缺省的精度为秒,如果需要存放毫秒甚至更好,可以如下: CREATE TABLE Ticket (TicketId BIGINT UNSI ...

  4. Java小白的学习笔记(七)——浅谈JSON

    JSON:JSON是一种与开发语言无关的轻量级的数据格式全称是:JavaScript Object Notation.可以说他是一种数据格式的标准,或者规范. JSON的优点:易于人的阅读和编写,易于 ...

  5. 《疯狂Java讲义》学习笔记(七)Java集合

    1.Java集合概述 为了保存数量不确定的数据,以及保存具有映射关系的数据(关联数据),Java提供了集合类.Java集合大致可分为Set.List.Queue和Map四种体系,其中Set代表无序.不 ...

  6. java web学习笔记(持续更新)

    java web学习笔记 一.Java Web简介 二.认识Servlet 1.什么是Servlet? 2.请求路径 3.tomcat 4.Servlet的使用 三.Servlet简单应用 1.创建S ...

  7. 2019年Java Web学习笔记目录

    Java Web学习笔记目录 1.Java Web学习笔记01:动态网站初体验 2.Java Web学习笔记02:在Intellij里创建Web项目 3.Java Web学习笔记03:JSP元素 4. ...

  8. 《疯狂Java讲义》学习笔记 第六章 面向对象(下)

    <疯狂Java讲义>学习笔记 第六章 面向对象(下) 6.1包装类 基本数据类型 包装类 byte Byte short Short int Integer long Long char ...

  9. Java学习笔记 六、面向对象编程中级部分

    Java学习笔记 六.面向对象编程中级部分 包 包的注意事项和使用细节 访问修饰符 访问修饰符的注意事项和使用细节 面向对象编程三大特征 封装 封装的实现步骤(三步) 继承 继承的细节问题 继承的本质 ...

最新文章

  1. sql 只取一条记录_后端程序员必备:书写高质量SQL的30条建议
  2. mysql怎样删除上一行_mysql如何删除第一行数据
  3. 话里话外:信息整合之障
  4. SwiftUI3优秀文章 NavigationLink图片和文字显示蓝色或者图片无显示
  5. c语言二叉树的生成,C语言实现二叉树的创建以及遍历(递归)
  6. Mycat和Mysql搭建高可用企业数据库集群
  7. java+mysql校园学校宿舍管理系统源码
  8. VB根据窗体自动调整窗体内控件大小 注:实用,可以直接引用
  9. AS数据库自动备份的DOS语句
  10. 缺少对公共可见类型或成员的XML注释
  11. cydia无法加载未能连接服务器,cydia无法加载_Cydia无法加载如何办?Cydia加载失败故障的解决方...
  12. 模式识别学习笔记——1(线性分类器)
  13. 学做网站论坛怎么样?分享新手学习建网站5天感受
  14. 西安市建筑物矢量数据(Shp格式+带高度)
  15. c++【吃鸡坑人版8.0】免费复制
  16. 几行代码轻松实现百度定位和在地图显示指定坐标
  17. 工程提示Unfortunately you can‘t have non-Gradle Java modules and > Android-Gradle modules in one project
  18. 今天分享两行Python代码实现视频负片特效
  19. MacBook pro 用什么vpn 好_扫地机器人真得好用么?云米互联网扫地机Pro测评!
  20. 高效工具-局域网服务器访问公网

热门文章

  1. 【Kaggle】 Russia房产价格预测top1%(22/3270)方案总结
  2. 油管视频目录正则整理
  3. 计算机上的aece代表什么意思,Myristicaceae是什么意思
  4. 加拿大IC VOC和无线产品IC ID认证
  5. python对excel筛选提取文本中数字_详解利用python提取pdf文本数字
  6. 再忆年少,再见年少——青春路上的我们
  7. IDEA 项目启动报错 Shorten the command line via JAR manifest or via a classpath file and rerun.
  8. Windows查看Java内存使用情况
  9. 《中国程序化广告技术生态图》2015年三月号更新发布
  10. React Native微信分享