SpringBoot技术栈搭建个人博客【后台开发】

前言:在之前,我们已经完成了项目的基本准备,那么就可以开始后台开发了,突然又想到一个问题,就是准备的时候只是设计了前台的RESTful APIs,但是后台管理我们同样也是需要API的,那么就在这一篇里面一起实现了吧...前序文章链接:SpringBoot技术栈搭建个人博客【项目准备】:https://www.jianshu.com/p/0293368fe750

一些设计上的调整

在查了一些资料和吸收了一些评论给出良好的建议之后,我觉得有必要对一些设计进行一些调整:

  • 1)数据库:命名应该更加规范,比如表示分类最好用category而不是sort,表示评论最好用comment而不是message;
  • 2)RESful APIs:在准备着手开始写后台的时候就已经发现,本来想的是凡是以/api开头的都是暴露出来给前端用的,凡是以/admin开头的都是给后台使用的地址,但是意外的没有设计后天的API也把一些删除命令暴露给了前端,这就不好了重新设计设计;
  • 3)命名规范的问题:因为使用MyBatis逆向工程自动生成的时候,配置了一个useActualColumnNames使用表真正名称的东西,所以整得来生成POJO类基础字段有下划线,看着着实有点不爽,把它给干掉干掉...;

数据库调整

把字段规范了一下,并且删除了分类下是否有效的字段(感觉这种不经常变换的字段留着也没啥用干脆干掉..),所以调整为了下面这个样子(调整字段已标红):

然后重新使用生成器自动生成对应的文件,注意记得修改generatorConfig.xml文件中对应的数据库名称;

创建和修改时间的字段设置

通过查资料发现其实我们可以通过直接设置数据库来自动更新我们的modified_by字段,并且可以像设置初始值那样给create_by和modified_by两个字段以当前时间戳设置默认值,这里具体以tbl_article_info这张表为例:

CREATE TABLE `tbl_article_info` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '文章标题',
  `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章简介,默认100个汉字以内',
  `is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '文章是否置顶,0为否,1为是',
  `traffic` int(10) NOT NULL DEFAULT '0' COMMENT '文章访问量',
  `create_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `modified_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改日期',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们通过设置DEFAULTCURRENT_TIMESTAMP,然后给modified_by字段多添加了一句ON UPDATE CURRENT_TIMESTAMP,这样它就会在更新的时候将该字段的值设置为更新时间,这样我们就不用在后台关心这两个值了,也少写了一些代码(其实是写代码的时候发现可以这样偷懒..hhh...);

RESTful APIs重新设计

我们需要把一些不能够暴露给前台的API收回,然后再设计一下后台的API,捣鼓了一下,最后大概是这个样子了:

后台Restful APIs:

前台开放RESful APIs:

这些API只是用来和前端交互的接口,另外一些关于日志啊之类的东西就直接在后台写就行了,OK,这样就爽多了,可以开始着手写代码了;

基本配置

随着配置内容的增多,我逐渐的想要放弃.yml的配置文件,主要的一点是这东西不好对内容进行分类(下图是简单配置了一些基本文件后的.yml和.properties文件的对比)..

最后还是用回.properties文件吧,不分类还是有点难受

编码设置

我们首先需要解决的是中文乱码的问题,对应GET请求,我们可以通过修改Tomcat的配置文件【server.xml】来把它默认的编码格式改为UTF-8,而对于POST请求,我们需要统一配置一个拦截器一样的东西把请求的编码统一改成UTF-8:

## ——————————编码设置——————————
spring.http.encoding.charset=UTF-8
spring.http.encoding.force=true
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8

但是这样设置之后,在后面的使用当中还是会发生提交表单时中文乱码的问题,在网上搜索了一下找到了解决方法,新建一个【config】包创建下面这样一个配置类:

@Configuration
public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {
    @Bean
    public HttpMessageConverter<String> responseBodyConverter() {
        StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
        return converter;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
    }
}

数据库及连接池配置

决定这一次试试Druid的监控功能,所以给一下数据库的配置:

## ——————————数据库访问配置——————————
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=UTF-8
spring.datasource.username = root
spring.datasource.password = 123456

# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
# 配置获取连接等待超时的时间
spring.datasource.druid.max-wait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.druid.filters=stat,wall,log4j

日志配置

在SpringBoot中其实已经使用了Logback来作为默认的日志框架,这是log4j作者推出的新一代日志框架,它效率更高、能够适应诸多的运行环境,同时天然支持SLF4J,在SpringBoot中我们无需再添加额外的依赖就能使用,这是因为在spring-boot-starter-web包中已经有了该依赖了,所以我们只需要进行配置使用就好了

第一步:创建logback-spring.xml

当项目跑起来的时候,我们不可能还去看控制台的输出信息吧,所以我们需要把日志写到文件里面,在网上找到一个例子(链接:http://tengj.top/2017/04/05/springboot7/

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <contextName>logback</contextName>
    <!--自己定义一个log.path用于说明日志的输出目录-->
    <property name="log.path" value="/log/wmyskxz/"/>
    <!--输出到控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
             <level>ERROR</level>
         </filter>-->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--输出到文件-->
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/logback.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>

    <!-- logback为java中的包 -->
    <logger name="cn.wmyskxz.blog.controller"/>
</configuration>

在Spring Boot中你只要按照规则组织文件名,就能够使得配置文件能够被正确加载,并且官方推荐优先使用带有-spring的文件名作为日志的配置(如上面使用的logback-spring.xml,而不是logback.xml),满足这样的命名规范并且保证文件在src/main/resources下就好了;

第二步:重启项目检查是否成功

我们定义的目录位置为/log/wmyskxz/,但是在项目的根目录下并没有发现这样的目录,反而是在当前盘符的根目录..不是很懂这个规则..总之是成功了的..

打开是密密麻麻一堆跟控制台一样的【info】级别的信息,因为这个系统本身就比较简单,所以就没有必要去搞什么文本切割之类的东西了,ok..日志算是配置完成;

实际测试了一下,上线之后肯定需要调整输出级别的,不然日志文件就会特别大...

拦截器配置

我们需要对地址进行拦截,对所有的/admin开头的地址请求进行拦截,因为这是后台管理的默认访问地址开头,这是必须进行验证之后才能访问的地址,正如上面的RESTful APIs,这里包含了一些增加/删除/更改/编辑一类的操作,而统统这些操作都是不能够开放给用户的操作,所以我们需要对这些地址进行拦截:

第一步:创建User实体类

做验证还是需要添加session,不然不好弄,所以我们还是得创建一个常规的实体:

public class User {
    private String username;
    private String password;

    /* getter and setter */
}

第二步:创建拦截器并继承HandlerInterceptor接口

在【interceptor】包下新建一个【BackInterceptor】类并继承HandlerInterceptor接口:

public class BackInterceptor implements HandlerInterceptor {

    private static String username = "wmyskxz";
    private static String password = "123456";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        boolean flag = true;
        User user = (User) request.getSession().getAttribute("user");
        if (null == user) {
            flag = false;
        } else {
            // 对用户账号进行验证,是否正确
            if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
                flag = true;
            } else {
                flag = false;
            }
        }
        return flag;
    }
}

在拦截器中,我们从session中取出了user,并判断是否符合要求,这里我们直接写死了(并没有更改密码的需求,但需要加密),而且我们并没有做任何的跳转操作,原因很简单,根本就不需要跳转,因为访问后台的用户只有我一个人,所以只需要我知道正确的登录地址就可以了...

第三步:在配置类中复写addInterceptors方法

刚才我们在设置编码的时候自己创建了一个继承自WebMvcConfigurerAdapter的设置类,我们需要复写其中的addInterceptors方法来为我们的拦截器添加配置:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    super.addInterceptors(registry);
}
  • 说明:这个方法也很简单,通过在addPathPatterns中添加拦截规则(这里设置拦截/admin开头的所有地址),并通过excludePathPatterns来排除拦截的地址(这里为/toLogin,即登录地址,到时候我可以弄得复杂隐蔽一点儿)

第四步:配置登录页面

以前我们在写Spring MVC的时候,如果需要访问一个页面,必须要在Controller中添加一个方法跳转到相应的页面才可以,但是在SpringBoot中增加了更加方便快捷的方法:

/**
 * 以前要访问一个页面需要先创建个Controller控制类,在写方法跳转到页面
 * 在这里配置后就不需要那么麻烦了,直接访问http://localhost:8080/toLogin就跳转到login.html页面了
 *
 * @param registry
 */
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/admin/login").setViewName("login.html");
    super.addViewControllers(registry);
}
  • 注意:login.html记得要放在【templates】下才会生效哦...(我试过使用login绑定视图名不成功,只能写全了...)

访问日志记录

上面我们设置了访问限制的拦截器,对后台访问进行了限制,这是拦截器的好处,我们同样也使用拦截器对于访问数量进行一个统计

第一步:编写前台访问拦截器

对照着数据库的设计,我们需要保存的信息都从request对象中去获取,然后保存到数据库中即可,代码也很简单:

public class ForeInterceptor implements HandlerInterceptor {

    @Autowired
    SysService sysService;

    private SysLog sysLog = new SysLog();
    private SysView sysView = new SysView();

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

        // 访问者的IP
        String ip = request.getRemoteAddr();
        // 访问地址
        String url = request.getRequestURL().toString();
        //得到用户的浏览器名
        String userbrowser = BrowserUtil.getOsAndBrowserInfo(request);

        // 给SysLog增加字段
        sysLog.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysLog.setOperateBy(StringUtils.isEmpty(userbrowser) ? "获取浏览器名失败" : userbrowser);
        sysLog.setOperateUrl(StringUtils.isEmpty(url) ? "获取URL失败" : url);

        // 增加访问量
        sysView.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysService.addView(sysView);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        // 保存日志信息
        sysLog.setRemark(method.getName());
        sysService.addLog(sysLog);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
  • 注意:但是需要注意的是测试的时候别把拦截器开了(主要是postHandle方法中中无法强转handler),不然不方便测试...

BrowserUtil是找的网上的一段代码,直接黏贴复制放【util】包下就可以了:

/**
 * 用于从Request请求中获取到客户端的获取操作系统,浏览器及浏览器版本信息
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 8:40
 */
public class BrowserUtil {
    /**
     * 获取操作系统,浏览器及浏览器版本信息
     *
     * @param request
     * @return
     */
    public static String getOsAndBrowserInfo(HttpServletRequest request) {
        String browserDetails = request.getHeader("User-Agent");
        String userAgent = browserDetails;
        String user = userAgent.toLowerCase();

        String os = "";
        String browser = "";

        //=================OS Info=======================
        if (userAgent.toLowerCase().indexOf("windows") >= 0) {
            os = "Windows";
        } else if (userAgent.toLowerCase().indexOf("mac") >= 0) {
            os = "Mac";
        } else if (userAgent.toLowerCase().indexOf("x11") >= 0) {
            os = "Unix";
        } else if (userAgent.toLowerCase().indexOf("android") >= 0) {
            os = "Android";
        } else if (userAgent.toLowerCase().indexOf("iphone") >= 0) {
            os = "IPhone";
        } else {
            os = "UnKnown, More-Info: " + userAgent;
        }
        //===============Browser===========================
        if (user.contains("edge")) {
            browser = (userAgent.substring(userAgent.indexOf("Edge")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("msie")) {
            String substring = userAgent.substring(userAgent.indexOf("MSIE")).split(";")[0];
            browser = substring.split(" ")[0].replace("MSIE", "IE") + "-" + substring.split(" ")[1];
        } else if (user.contains("safari") && user.contains("version")) {
            browser = (userAgent.substring(userAgent.indexOf("Safari")).split(" ")[0]).split("/")[0]
                    + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
        } else if (user.contains("opr") || user.contains("opera")) {
            if (user.contains("opera")) {
                browser = (userAgent.substring(userAgent.indexOf("Opera")).split(" ")[0]).split("/")[0]
                        + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
            } else if (user.contains("opr")) {
                browser = ((userAgent.substring(userAgent.indexOf("OPR")).split(" ")[0]).replace("/", "-"))
                        .replace("OPR", "Opera");
            }

        } else if (user.contains("chrome")) {
            browser = (userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0]).replace("/", "-");
        } else if ((user.indexOf("mozilla/7.0") > -1) || (user.indexOf("netscape6") != -1) ||
                (user.indexOf("mozilla/4.7") != -1) || (user.indexOf("mozilla/4.78") != -1) ||
                (user.indexOf("mozilla/4.08") != -1) || (user.indexOf("mozilla/3") != -1)) {
            browser = "Netscape-?";

        } else if (user.contains("firefox")) {
            browser = (userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("rv")) {
            String IEVersion = (userAgent.substring(userAgent.indexOf("rv")).split(" ")[0]).replace("rv:", "-");
            browser = "IE" + IEVersion.substring(0, IEVersion.length() - 1);
        } else {
            browser = "UnKnown, More-Info: " + userAgent;
        }

        return os + "-" + browser;
    }
}

第二步:设置拦截地址

还是在刚才的配置类中,新增这么一条:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin","/admin/**");
    super.addInterceptors(registry);
}

设置默认错误页面

在SpringBoot中,默认的错误页面比较丑(如下),所以我们可以自己改得稍微好看一点儿,具体的教程在这里:http://tengj.top/2018/05/16/springboot13/ ,我就搞前台的时候再去弄了...


Service 层开发

这是纠结最久应该怎么写的,一开始我还准备老老实实地利用MyBatis逆向工程生成的一堆东西去给每一个实体创建一个Service的,这样其实就只是对Dao层进行了一层不必要的封装而已,然后通过分析其实主要的业务也就分成几个:文章/评论/分类/日志浏览量这四个部分而已,所以创建这四个Service就好了;

比较神奇的事情是在网上找到一种通用Mapper的最佳实践方法,整个人都惊了,“wtf?还可以这样写哦?”,资料如下:http://tengj.top/2017/12/20/springboot11/

emmmm..我们通过MyBatis的逆向工程,已经很大程度上简化了我们的开发,因为在Dao层我们已经免去了自己写SQL语句,自己写实体,自己写XML映射文件的麻烦,但在Service层我们仍然无可避免的要写一些类似功能的代码,有没有什么方法能把这些比较通用的方法给提取出来呢? 答案就在上面的链接中,oh,简直太酷了...我决定在这里介绍一下...

通用接口开发

在Spring4中,由于支持了泛型注解,再结合通用Mapper,我们的想法得到了一个最佳的实践方法,下面我们来讲解一下:

第一步:创建通用接口

我们把一些常见的,通用的方法统一使用泛型封装在一个通用接口之中:

/**
 * 通用接口
 *
 * @author: wmyskxz
 * @create: 2018年6月15日10:27:04
 */
public interface IService<T> {

    T selectByKey(Object key);

    int save(T entity);

    int delete(Object key);

    int updateAll(T entity);

    int updateNotNull(T entity);

    List<T> selectByExample(Object example);

}

第二步:实现通用接口类

/**
 * 通用Service
 *
 * @param <T>
 */
public abstract class BaseService<T> implements IService<T> {

    @Autowired
    protected Mapper<T> mapper;

    public Mapper<T> getMapper() {
        return mapper;
    }

    /**
     * 说明:根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号
     *
     * @param key
     * @return
     */
    @Override
    public T selectByKey(Object key) {
        return mapper.selectByPrimaryKey(key);
    }

    /**
     * 说明:保存一个实体,null的属性也会保存,不会使用数据库默认值
     *
     * @param entity
     * @return
     */
    @Override
    public int save(T entity) {
        return mapper.insert(entity);
    }

    /**
     * 说明:根据主键字段进行删除,方法参数必须包含完整的主键属性
     *
     * @param key
     * @return
     */
    @Override
    public int delete(Object key) {
        return mapper.deleteByPrimaryKey(key);
    }

    /**
     * 说明:根据主键更新实体全部字段,null值会被更新
     *
     * @param entity
     * @return
     */
    @Override
    public int updateAll(T entity) {
        return mapper.updateByPrimaryKey(entity);
    }

    /**
     * 根据主键更新属性不为null的值
     *
     * @param entity
     * @return
     */
    @Override
    public int updateNotNull(T entity) {
        return mapper.updateByPrimaryKeySelective(entity);
    }

    /**
     * 说明:根据Example条件进行查询
     * 重点:这个查询支持通过Example类指定查询列,通过selectProperties方法指定查询列
     *
     * @param example
     * @return
     */
    @Override
    public List<T> selectByExample(Object example) {
        return mapper.selectByExample(example);
    }
}

至此呢,我们的通用接口就开发完成了

第三步:使用通用接口

编写好我们的通用接口之后,使用就变得很方便了,只需要继承相应的通用接口或者通用接口实现类,然后进行简单的封装就行了,下面以SortInfo为例:

public interface SortInfoService extends IService<SortInfo> {
}
========================分割线========================
/**
 * 分类信息Service
 *
 * @author:wmyskxz
 * @create:2018-06-15-上午 11:14
 */
@Service
public class SortInfoServiceImpl extends BaseService<SortInfo> implements SortInfoService {
}

对应到SortInfo的RESTful API设计,这样简单的继承就能够很好的支持,但是我们还是使用最原始的方式来创建吧...

Service接口申明

查了一些资料,问了一下实习公司的前辈老师,并且根据我们之前设计好的RESTful APIs,我们很有必要搞一个dto层用于前后端之间的数据交互,这一层主要是对数据库的数据进行一个封装整合,也方便前后端的数据交互,所以我们首先就需要分析在dto层中应该存在哪些数据:

DTO层开发

对应我们的业务逻辑和RESTful APIs,我大概弄了下面几个Dto:

① ArticleDto:

该Dto封装了文章的详细信息,对应RESTful API中的/api/article/{id}——通过文章ID获取文章信息

/**
 * 文章信息类
 * 说明:关联了tbl_article_info/tbl_article_content/tbl_article_category/tbl_category_info/
 * tbl_article_picture五张表的基础字段
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:13
 */
public class ArticleDto {

    // tbl_article_info基础字段
    private Long id;
    private String title;
    private String summary;
    private Boolean isTop;
    private Integer traffic;

    // tbl_article_content基础字段
    private Long articleContentId;
    private String content;

    // tbl_category_info基础字段
    private Long categoryId;
    private String categoryName;
    private Byte categoryNumber;

    // tbl_article_category基础字段
    private Long articleCategoryId;

    // tbl_article_picture基础字段
    private Long articlePictureId;
    private String pictureUrl;

    /* getter and setter */
}

②ArticleCommentDto:

该Dto封装的事文章的评论信息,对应/api/comment/article/{id}——通过文章ID获取某一篇文章的全部评论信息

/**
 * 文章评论信息
 * 说明:关联了tbl_comment和tbl_article_comment两张表的信息
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:09
 */
public class ArticleCommentDto {
    // tbl_comment基础字段
    private Long id;                // 评论id
    private String content;         // 评论内容
    private String name;            // 用户自定义的显示名称
    private String email;
    private String ip;

    // tbl_article_comment基础字段
    private Long articleCommentId;  // tbl_article_comment主键
    private Long articleId;         // 文章ID

    /* getter and setter */
}

③ArticleCategoryDto:

该Dto是封装了文章的一些分类信息,对应/admin/category/{id}——获取某一篇文章的分类信息

/**
 * 文章分类传输对象
 * 说明:关联了tbl_article_category和tbl_category_info两张表的数据
 *
 * @author:wmyskxz
 * @create:2018-06-20-上午 8:45
 */
public class ArticleCategoryDto {

    //  tbl_article_category表基础字段
    private Long id;            // tbl_article_category表主键
    private Long categoryId;    // 分类信息ID
    private Long articleId;     // 文章ID

    // tbl_category_info表基础字段
    private String name;        // 分类信息显示名称
    private Byte number;        // 该分类下对应的文章数量

    /* getter and setter */
}

④ArticleWithPictureDto:

该Dto封装了文章用于显示的基本信息,对应所有的获取文章集合的RESful APIs

/**
 * 带题图信息的文章基础信息分装类
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:53
 */
public class ArticleWithPictureDto {
    // tbl_article_info基础字段
    private Long id;
    private String title;
    private String summary;
    private Boolean isTop;
    private Integer traffic;

    // tbl_article_picture基础字段
    private Long articlePictureId;
    private String pictureUrl;

    /* getter and setter */
}

Service接口开发

Service层其实就是对我们业务的一个封装,所以有了RESTful APIs文档,我们可以很轻易的写出对应的业务模块:

文章Service

/**
 * 文章Service
 * 说明:ArticleInfo里面封装了picture/content/category等信息
 */
public interface ArticleService {

    void addArticle(ArticleDto articleDto);

    void deleteArticleById(Long id);

    void updateArticle(ArticleDto articleDto);

    void updateArticleCategory(Long articleId, Long categoryId);

    ArticleDto getOneById(Long id);

    ArticlePicture getPictureByArticleId(Long id);

    List<ArticleWithPictureDto> listAll();

    List<ArticleWithPictureDto> listByCategoryId(Long id);

    List<ArticleWithPictureDto> listLastest();
}

分类Service

/**
 * 分类Service
 */
public interface CategoryService {
    void addCategory(CategoryInfo categoryInfo);

    void deleteCategoryById(Long id);

    void updateCategory(CategoryInfo categoryInfo);

    void updateArticleCategory(ArticleCategory articleCategory);

    CategoryInfo getOneById(Long id);

    List<CategoryInfo> listAllCategory();

    ArticleCategoryDto getCategoryByArticleId(Long id);
}

留言Service

/**
 * 留言的Service
 */
public interface CommentService {
    void addComment(Comment comment);

    void addArticleComment(ArticleCommentDto articleCommentDto);

    void deleteCommentById(Long id);

    void deleteArticleCommentById(Long id);

    List<Comment> listAllComment();

    List<ArticleCommentDto> listAllArticleCommentById(Long id);
}

系统Service

/**
 * 日志/访问统计等系统相关Service
 */
public interface SysService {
    void addLog(SysLog sysLog);

    void addView(SysView sysView);

    int getLogCount();

    int getViewCount();

    List<SysLog> listAllLog();

    List<SysView> listAllView();
}

Controller 层开发

Controller层简单理解的话,就是用来获取数据的,所以只要Service层开发好了Controller层就很容易,就不多说了,只是我们可以把一些公用的东西放到一个BaseController中,比如引入Service:

/**
 * 基础控制器
 *
 * @author:wmyskxz
 * @create:2018-06-19-上午 11:25
 */
public class BaseController {
    @Autowired
    ArticleService articleService;
    @Autowired
    CommentService commentService;
    @Autowired
    CategoryService categoryService;
}

然后前后台的控制器只需要继承该类就行了,这样的方式非常值得借鉴的,只是因为这个系统比较简单,所以这个BaseController,我看过一些源码,可以在里面弄一个通用的用于返回数据的方法,比如分页数据/错误信息之类的;


记录坑

1)MyBatis中Text类型的坑

按照《阿里手册》(简称)上所规范的那样,我把文章的content单独弄成了一张表并且将这个“可能很长”的字段的类型设置成了text类型,但是MyBatis逆向工程自动生成的时候,却把这个text类型的字段单独给列了出去,即在生成的xml中多出了一个<resultMap>,标识id为ResultMapWithBLOBs,MyBatis这样做可能的原因还是怕这个字段太长影响前面的字段查询吧,但是操作这样的LONGVARCHAR类型的字段MyBatis好像并没有集成很好,所以想要很好的操作还是需要给它弄成VARCHAR类型才行;

在generatorConfig.xml中配置生成字段的时候加上这样一句话就好了:

<table domainObjectName="ArticleContent" tableName="tbl_article_content">  
    <columnOverride column="content" javaType="java.lang.String" jdbcType="VARCHAR" />  
</table>  

2)拦截器中Service注入为null的坑

在编写前台拦截器的时候,我使用@Autowired注解自动注入了SysService系统服务Service,但是却报nullpointer的错,发现是没有自动注入上,SysService为空..这是为什么呢?排除掉注解没有识别或者没有给Service添加上注解的可能性之后,我发现好像是拦截器拦截的时候Service并没有创建成功造成的,参考这篇文章:https://blog.csdn.net/slgxmh/article/details/51860278,成功解决问题:

@Bean
public HandlerInterceptor getForeInterceptor() {
    return new ForeInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
   registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin", "/admin/**");
    super.addInterceptors(registry);
}

其实就是添加上@Bean注解让ForeInterceptor提前加载;

3)数据库sys_log表中operate_by字段的坑

当时设计表的时候,就只是单纯的想要保存一下用户使用的浏览器是什么,其实当时并不知道应该怎么获取获取到的东西又是什么,只是觉得保存浏览器20个字段够了,但后来发现这是很蠢萌的...所以不得不调整数据库的字段长度,好在只需要单方面调整数据库的字段长度就好了:

4)保存文章的方式的坑

因为我想要在数据库中保存的是md源码,而返回前台前端希望的是直接拿到html代码,这样就能很方便的输出了,所以这要怎么做呢?找到一篇参考文章:https://my.oschina.net/u/566591/blog/1535380

我们不要搞那么复杂的封装,只要简单弄一个工具类就可以了,在【util】包下新建一个【Markdown2HtmlUtil】:

/**
 * Markdown转Html工具类
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 10:09
 */
public class Markdown2HtmlUtil {
    /**
     * 将markdown源码转换成html返回
     *
     * @param markdown md源码
     * @return html代码
     */
    public static String markdown2html(String markdown) {
        MutableDataSet options = new MutableDataSet();
        options.setFrom(ParserEmulationProfile.MARKDOWN);
        options.set(Parser.EXTENSIONS, Arrays.asList(new Extension[]{TablesExtension.create()}));
        Parser parser = Parser.builder(options).build();
        HtmlRenderer renderer = HtmlRenderer.builder(options).build();

        Node document = parser.parse(markdown);
        return renderer.render(document);
    }
}

使用也很简单,只需要在获取一篇文章的时候把ArticleDto里面的md源码转成html代码再返回给前台就好了:

/**
 * 通过文章的ID获取对应的文章信息
 *
 * @param id
 * @return 自己封装好的文章信息类
 */
@ApiOperation("通过文章ID获取文章信息")
@GetMapping("article/{id}")
public ArticleDto getArticleById(@PathVariable Long id) {
    ArticleDto articleDto = articleService.getOneById(id);
    articleDto.setContent(Markdown2HtmlUtil.markdown2html(articleDto.getContent()));
    return articleDto;
}

样式之类的交给前台就好了,搞定...


简单总结

关于统计啊日志类的Controller还没有开发,RESful API也没有设计,这里就先发布文章了,因为好像时间有点紧,后台的页面暂时可能开发不完,准备直接开始前台页面显示的开发(主要是自己对前端不熟悉还要学习..),这里对后台进行一个简单的总结:

其实发现当数据库设计好了,RESful APIs设计好了之后,后台的任务变得非常明确,开发起来也就思路很清晰了,只是自己还是缺少一些必要的经验,如对一些通用方法的抽象/层与层之间数据交互的典型设计之类的东西,特别是一些安全方面的东西,网上的资料也比较少一些,也是自己需要学习的地方;

欢迎转载,转载请注明出处!undefined简书ID:@我没有三颗心脏undefinedgithub:wmyskxzundefined欢迎关注公众微信号:wmyskxz_javawebundefined分享自己的Java Web学习之路以及各种Java学习资料

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

10 条评论
登录 后参与评论

相关文章

来自专栏菩提树下的杨过

Silverlight:用Enter键替换Tab键切换焦点

业务系统中,很多录入人员习惯于用Enter键来代替Tab键切换控件焦点(虽然我个人并不觉得这样录入速度会变得有多高效,呵呵),有需求了,自然就得想办法满足。 思...

18010
来自专栏张善友的专栏

ASP.NET MVC 4 - 测试驱动 ASP.NET MVC

测试驱动 ASP.NET MVC Keith Burnell 下载代码示例 模型-视图-控制器 (MVC) 模式的核心是将 UI 功能划分成三个组成部分。模型表...

1957
来自专栏疯狂的小程序

微信小程序使用WebSokcet实现相关请求

以前做小程序为了应急找了个插件去链接WebSokcet,文章传送门。

30810
来自专栏Urahara Blog

Joomla V3.7.0 核心组件SQL注入漏洞分析

1604
来自专栏技术小讲堂

使用Donut Caching和Donut Hole Caching在ASP.NET MVC应用中缓存页面何时使用Donut CachingDonut Caching 的Nuget 包Donut Ho

Donut Caching是缓存除了部分内容以外的整个页面的最好的方式,在它出现之前,我们使用“输出缓存”来缓存整个页面。 何时使用Donut Caching ...

2705
来自专栏刘望舒

APK是如何被解析的?

在本系列的前面文章中,我介绍了PackageInstaller的初始化和安装APK过程、PMS处理APK的安装和PMS的创建过程,这些文章中经常会涉及到一个类,...

873
来自专栏Create Sun

mvc+webapi 单元测试 3.使用Moq模拟AspnetMvc中的Request.Form

 1.前言     现在这个项目已经有阶段性的模块完成了,所以就想着对这些模块进行单元测试,以保证项目的代码的质量。首先虽然标题是mvc+webapi实质上我只...

4709
来自专栏一个会写诗的程序员的博客

第7章 Spring Boot集成模板引擎小结

因为Spring Boot其实是对Spring生态的封装整合打包,以简化开发中使用Spring框架。所以 Spring Boot在集成模板引擎过程中,其实就是对...

753
来自专栏纯洁的微笑

springboot(二):web综合开发

上篇文章介绍了Spring boot初级教程:spring boot(一):入门篇,方便大家快速入门、了解实践Spring boot特性;本篇文章接着上篇内容继...

3806
来自专栏菩提树下的杨过

Silverlight Telerik控件学习:数据录入、数据验证

相信很多人都听说过这句名言:garbage in ,garbage out ! 数据录入不规范(或错误)就象一颗定时炸弹,迟早会给系统带来麻烦,所以在数据录入时...

2466

扫码关注云+社区