研发过程管理系统培训文档

快速了解

项目简介

基于 Spring Boot 2.1.0 、 Spring Boot Jpa、 JWT、Spring Security、Redis、Mysql、Vue、Element-UI 的前后端分离的研发过程管理系统, 项目采用按功能分模块的开发方式,支持数据字典与数据权限管理,支持一键生成前后端代码,支持前端菜单动态路由等。

此项目用的开源项目Eladmin(2.3版本)

前端地址

https://gitee.com/elunez/eladmin-web

后端地址

https://gitee.com/elunez/eladmin

后端手册

项目结构

  • common 为系统的公共模块,各种工具类,公共配置存在该模块
  • system 为系统核心业务模块也是项目入口模块,也是最终需要打包部署的模块
  • logging 为系统的日志模块,其他模块如果需要记录日志需要引入该模块
  • tools 为第三方工具模块,包含:图床、邮件、云存储、本地存储、支付宝
  • generator 为系统的代码生成模块,代码生成的模板在 system 模块中

详细结构

- common 公共模块
    - annotation 为系统自定义注解
    - aspect 自定义注解的切面
    - base 提供了Entity、DTO基类和mapstruct的通用mapper
    - config 自定义权限实现、redis配置、swagger配置、Rsa配置等
    - exception 项目统一异常的处理
    - utils 系统通用工具类
- system 系统核心模块(系统启动入口)
	- config 配置跨域与静态资源,与数据权限
	    - thread 线程池相关
	- modules 系统相关模块(登录授权、系统监控、定时任务、业务处理逻辑等)
- logging 系统日志模块
- tools 系统第三方工具模块
- generator 系统代码生成模块

权限控制

本系统安全框架使用的是 Spring Security + Jwt Token, 访问后端接口需在请求头中携带token进行访问,请求头格式如下:

# Authorization: Bearer 登录时返回的token
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTU1ODk2NzY0OSwiaWF0IjoxNTU4OTQ2MDQ5fQ.jsJvqHa1tKbJazG0p9kq5J2tT7zAk5B6N_CspdOAQLWgEICStkMmvLE-qapFTtWnnDUPAjqmsmtPFSWYaH5LtA
数据交互

用户登录 -> 后端验证登录返回 token -> 前端带上token请求后端数据 -> 后端返回数据, 数据交互流程如下:

image-20200817164751991
权限注解

Spring Security 提供了Spring EL表达式,允许我们在定义接口访问的方法上面添加注解,来控制访问权限,常用的 EL如下

表达式 描述
hasRole([role]) 当前用户是否拥有指定角色。
hasAnyRole([role1,role2]) 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。

下面的接口表示用户拥有 adminmenu:edit 权限中的任意一个就能能访问update方法, 如果方法不加@preAuthorize注解,意味着所有用户都需要带上有效的 token 后能访问 update 方法

@Log(description = "修改菜单")
@PutMapping(value = "/menus")
@PreAuthorize("hasAnyRole('admin','menu:edit')")
public ResponseEntity update(@Validated @RequestBody Menu resources){
    // 略
}

由于每个接口都需要给超级管理员放行,而使用 hasAnyRole('admin','user:list') 每次都需要重复的添加 admin 权限,因此加入了自定义权限验证方式,在验证的时候默认给拥有admin权限的用户放行。

使用方式:

@PreAuthorize("@el.check('user:list','user:add')") 

权限放行

在我们使用的时候,有写接口是不需要验证权限,这个时候就需要我们给接口放行,使用方式如下

1、使用注解方式

只需要在Controller的方法上加入该注解即可

@AnonymousAccess

2、修改配置文件方式

system -> modules -> security -> config -> SecurityConfig

TIP

使用 permitAll() 方法所有人都能访问,包括带上 token 访问

使用 anonymous() 所有人都能访问,但是带上 token 访问后会报错

// 关键代码,部分略
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            // 支付宝回调
            .antMatchers("/api/aliPay/return").anonymous()
            // 所有请求都需要认证
            .anyRequest().authenticated();
    httpSecurity
            .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}

项目模块

这里演示使用 Idea 创建一个新的子模块

新建模块

选择 File -> New -> Module

image-20200817134448596

选择 Maven -> Next

image-20200817134537031

选择父模块 -> 定义name -> Finish

image-20200817134732536

回到项目,在新模块的java目录下中创建包,包名hundsun.pdpm

TIP

如果包名和 System 模块 hundsun.pdpm 不一样,那么需要在 AppRun.class 中配置扫描路径

需要在 system 模块中的 AppRun 中配置注解

@ComponentScan(basePackages = {"包名"})

因为 springboot 默认扫描规则是扫描启动器类的同包或者其子包的下的注解

而你新加的模块的包名与 hundsun.pdpm 不一致,没有被扫描到会是 404

给新模块起一个名字

image-20200817140256391

最后在 System 模块的 pom.xml 的 dependencies 节点加入创建的子模块

<!--测试模块-->
<dependency>
    <groupId>com.hundsun.pdpm</groupId>
    <artifactId>test</artifactId>
    <version>2.3</version>
</dependency>
image-20200817140414054
代码目录
- domain 数据层,放置与数据库中表一一对应的JavaBean对象 ,直接进行数据库的读写操作,里面放置DO(Data Object)
- repository 数据库服务接口
- rest 控制层,处理外部请求,调用service层
- service 业务层,实现业务逻辑,返回的对象需要把DO转化为DTO(Data Transfer Object 数据传输对象)
  - dto 放置DTO,用于数据传递。(接口入参和接口返回值都可以)
  - impl service接口或者mapper接口的实现
  - mapper 进行DO和DTO一个转换操作
image-20200817145804614

区分DO、VO、DTO的好处:

  • 防止不需要的字段也会传递到前端页面。
  • 可以支持需要转换的字段。
  • 避免某些字段要展示,但是并不希望出现在数据库中

通用查询

本项目对 Jpa 的查询进行了封装,现可以通过 @Query 注解实现简单的查询与复杂查询

简单查询:等于(默认)、大于等于、小于等于、左模糊、右模糊、中模糊、多字段模糊、NOT_EQUAL 、BETWEEN 、NOT_NULL

复杂查询:包含(IN)查询、左连接、右连接等

参数说明
字段名称 字段描述 默认值
propName 对象的属性名,如果字段名称与实体字段一致,则可以省略 ""
type 查询方式,默认为 EQUAL
blurry 多字段模糊查询,值为实体字段名称,仅支持String类型字段 ""
joinName 关联实体的名称 ""
join 连接查询方式,左连接或者右连接 LEFT
nvl 取出来如果为空,返回默认值,仅支持String类型字段 ""
使用方式

1、创建一个查询类 QueryCriteria

@Data
public class QueryCriteria {

    // 等于
    @Query
    private String a;

    // 左模糊
    @Query(type = Query.Type.LEFT_LIKE)
    private String b;

    // 右模糊
    @Query(type = Query.Type.RIGHT_LIKE)
    private String c;

    // 大于等于
    @Query(type = Query.Type.GREATER_THAN, propName = "createTime")
    private Timestamp startTime;

    // 小于等于
    @Query(type = Query.Type.LESS_THAN, propName = "createTime")
    private Timestamp endTime;

    // BETWEEN
    @Query(type = Query.Type.BETWEEN)
    private List<Timestamp> startTime;

    // 多字段模糊查询,blurry 为字段名称
    @Query(blurry = "a,b,c")
    private String blurry;

    // IN 查询
    @Query(type = Query.Type.IN)
    private List<String> d;

    // 左关联查询,left Join , joinName为关联实体名称
    @Query(joinName = "")
    private String e;

    // 右关联查询,right Join , joinName为关联实体名称
    @Query(joinName = "", join = Query.Join.RIGHT)
    private String f;

    // NOT_EQUAL 不等于
    @Query(type = Query.Type.NOT_EQUAL)
    private String g;

    // NOT_NULL 不为空
    @Query(type = Query.Type.NOT_NULL)
    private String g;
}

2、在控制器中使用

// Pageable 分页查询
public ResponseEntity query(QueryCriteria criteria, Pageable pageable){
    return new ResponseEntity(service.queryAll(criteria,pageable), HttpStatus.OK);
}

3、Service 中查询

@Override
public Object queryAll(QueryCriteria criteria, Pageable pageable){
    Page<实体> page = repository.findAll(((root, criteriaQuery, cb) -> QueryHelp.getPredicate(root, criteria, cb)),pageable);
    return page;
}

TIP

如果需要添加一个字段查询,只需要在查询类 QueryCriteria 中添加就可以了,可节省大量时间。

源码可以查看 common 模块中的 hundsun.pdpm.annotation.Queryhundsun.pdpm.utils.QueryHelp

引入模块

如果在自定义模块中需要用到common模块,需要在此模块的pom.xml中引入common依赖,如图。

<dependencies>
    <dependency>
        <groupId>com.hundsun.pdpm</groupId>
        <artifactId>common</artifactId>
        <version>2.3</version>
    </dependency>
</dependencies>
image-20200817153054220

引入后需要点击右边Maven中的刷新一下Maven库

image-20200817153332983

自定义复杂查询

使用@Query注解

这个注解的包为:org.springframework.data.jpa.repository.Query 非自定义注解

image-20200817153407128
查询
    @Query(value = "select count(1) from pdpmtest t group by t.teststring",nativeQuery = true)
    Integer getCountGroupByTestString();

TIP

加了nativeQuery = true 可以直接使用sql,如果没加则需要用hsql,hsql这里不过多赘述,想详细了解的自行百度一下。

更新/删除

更新/删除操作需要增加注解@Modifying

  • 更新
@Modifying
@Query(value = "update pdpmtest set teststring = '1'", nativeQuery = true)
void updateAllPdPmTest();
  • 删除
@Modifying
@Query(value = "delete from pdpmtest", nativeQuery = true)
void deleteAllPdPmTest();
使用形参

使用形参有两种方式

第一种方式:

@Query(value = " select t.* from pdpmtest t where t.id = ?1 or teststring = ?2", nativeQuery = true)
List<PdPmTest> getPdPmTestById(int id,String testString);

?1?2分别指形参idtestString

第二种方式:

@Query(value = "select t.* from pdpmtest t where t.id in :ids", nativeQuery = true)
List<PdPmTest> getPdPmTestInIdList(@Param("ids") List<Integer> ids);

形参加注解@Param,sql中用:ids指代

返回类型

第一种:

返回全部对象,则返回类型为具体对象,如:

@Query(value = " select t.* from pdpmtest t where t.id = ?1 or teststring = ?2", nativeQuery = true)
List<PdPmTest> getPdPmTestById(int id,String testString);

第二种:

返回特定列,则返回List<Map<String,Object>>

@Query(value = " select t.testString,t.testNumber from pdpmtest t where t.id = ?1 ", nativeQuery = true)
List<Map<String,Object>> getTestStringAndTestNumber(int id);

前端手册

目录结构

- config 配置文件
- src
  - api 请求接口
  - assets 图片、logo
  - components 封装的组件
  - config 系统全局配置
  - icons 图标
  - mixins 混合加载代码
  - router 路由
  - store 本地缓存代码
  - styles 样式
  - utils 工具类
  - views 业务视图
  - APP.vue 根组件
  - main.js 项目的入口文件,实例化Vue;

菜单路由

添加固定菜单

公共的菜单只需要在 src/router/routers.js 中添加就可以了,

如:个人中心页面

{
  path: '/user',//路径
  component: Layout,//组件
  hidden: false,//是否隐藏
  redirect: 'noredirect',//是否重定向
  children: [ //子元素
    {
      path: 'center',
      component: () => import('@/views/system/user/center'),
      name: '个人中心',
      meta: { title: '个人中心', icon: 'user' }
    }
  ]
}
添加动态菜单
image-20200817174514033

本项目的动态菜单支持到 4级 菜单,支持 外链,支持自定义图标,添加教程如下:

(1)添加外链

外链菜单路由地址必须带上 https:// 或者 http://,并且外链菜单选择

image-20200817174354074
(2)内部菜单
  • 外链菜单:这个选择否就好
  • 菜单缓存:选择为 那么切换到其他菜单当前菜单会缓存
  • 菜单课件:如果不想在左侧显示,可以选择为
  • 路由地址:这个就是浏览器访问菜单的地址
  • 组件名称:这个非必填,如果设置了菜单缓存,那么必填,不然缓存会无效
  • 组件路径:项目的组件文件的路径 src/views
添加内部菜单 组件路径对应
image-20200817174839614 image-20200817174948427

分配菜单

创建完菜单还需要在角色管理中给角色分配菜单

![image-20200817185030349](/Users/baiyuliuguang/Library/Application Support/typora-user-images/image-20200817185030349.png)

权限控制

可以引入权限判断函数或者使用全局指令函数实现前端的权限控制
1、使用全局指令函数v-permission=""

<!-- 新增 -->
<div v-permission="['admin','user:add']" style="display: inline-block;margin: 0px 2px;">
      <el-button
        class="filter-item"
        size="mini"
        type="primary"
        icon="el-icon-plus"
        @click="add">新增</el-button>
      <eForm ref="form" :sup_this="sup_this" :is-add="true" :dicts="dicts"/>
    </div>

2、使用判断函数 checkPermission()

<template>
  	<el-tab-pane v-if="checkPermission(['admin'])" label="Admin">
		admin 权限的用户才能看到
	 </el-tab-pane>
</template>

<script>
import checkPermission from '@/utils/permission' // 权限判断函数

export default{
   methods: {
    checkPermission
   }
}
</script>

数据字典

首先我们需要在字典管理中创建一个字典

image-20200817185358963

数据字典使用一共有两种方式。

使用全局组件

TIP

建议使用该方式

使用方式:

<template>
  <div class="app-container">
  </div>
</template>

<script>
export default {
  // 设置数据字典
  dicts: ['job_status'],
  created() {
    // 得到完整数据
    console.log(this.dict.job_status)
    // 打印简化后的label数据
    console.log(this.dict.job_status.label)
  }
}
</script>

打印如下:

1、完整数据

image-20200817185628518

2、简化后的label数据

image-20200817185742842
使用混入方式

源码位于: src/mixins/initDict.js,代码如下

(1)引入组件

import initDict from '@/mixins/initDict'
export default {
  mixins: [initDict]
}

(2)使用钩子函数获取字典

import initDict from '@/mixins/initDict'
export default {
 mixins: [initDict],
 created() {
    this.$nextTick(() => {
      // 加载数据字典
      this.getDict('job_status')
    })
  }
}

(3)使用字典

<el-form-item v-if="form.pid !== 0" label="状态" prop="enabled">
        <el-radio v-for="item in dicts" :key="item.id" v-model="form.enabled" :label="item.value">{{ item.label }}</el-radio>
 </el-form-item>

简单例子

引入logging日志包

因为logging模块中依赖已经有common模块,所以如果同时需要common和logging模块则只需引入logging模块即可,修改test的pom.xml如下:

image-20200817193721678

测试表

CREATE TABLE `pdpmtest` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `testString` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '测试String',
  `testNumber` decimal(16, 2) NULL DEFAULT 0.00 COMMENT '测试Number',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

后端代码

目录总览

image-20200817192313004

1、定义DO对象--PdPmTest

定义DO对象

@Entity
@Data
@Table(name="pdpmtest")
public class PdPmTest implements Serializable {

    @Id
    @Column(name = "id")
    private Integer id;

    @Column(name = "teststring")
    private String testString;

    @Column(name = "testnumber")
    private BigDecimal testNumber;

}
  • @Entity 代表此对象是与数据库映射的对象;
  • @Data 注解的主要作用是提高代码的简洁,使用这个注解可以省去代码中大量的get()set()toString()等方法;
  • @Table 表名注解,代表与数据库哪个表映射;
  • @Id 代表此字段为数据库里的主键列
  • @Column 实体类中属性与数据表中字段的对应关系
  • implements Serializable 实现Serializable代表序列化

TIP

  • 什么是Serializable接口

一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才能被序列化。

  • 什么是序列化?

序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据。

  • 为什么要序列化对象

1、把对象转换为字节序列的过程称为对象的序列化

2、把字节序列恢复为对象的过程称为对象的反序列化

  • 什么情况下需要序列化?

当我们需要把对象的状态信息通过网络进行传输,或者需要将对象的状态信息持久化,以便将来使用时都需要把对象进行序列化。方便下次使用的时候,可以很快捷的重建一个副本。

2、定义DTO对象--PdPmTestDTO

@Data
public class PdPmTestDTO  implements Serializable  {


    private Integer id;

    private String testString;


    private BigDecimal testNumber;


    private BigDecimal average;

}

3、定义查询条件对象--PdPmTestQueryCriteria

@Data
public class PdPmTestQueryCriteria {

    // 精确
    @Query
    private String id;

    // 模糊
    @Query(type = Query.Type.INNER_LIKE)
    private String testString;

    //大于
    @Query(type = Query.Type.GREATER_THAN)
    private BigDecimal testNumber;
    
}

4、定义持久层--PdpmTestRepository

public interface PdPmTestRepository extends JpaRepository<PdPmTest,Long>, JpaSpecificationExecutor<PdPmTest> {
}
  • 继承JpaRepository<T,ID>提供了数据访问功能和一组 JPA 规范相关的方法;

  • 继承JpaSpecificationExecutor<T> 实现一组 JPA Criteria 查询相关的方法 ;

TIP

其中T代表接口返回值类型,ID代表主键类型,当复合主键时,可以建一个主键对象。

5、定义DO转DTO服务接口-- PdPmTestMapper

@Service
public interface PdPmTestMapper extends BaseMapper<PdPmTestDTO, PdPmTest> {

}

@Service用在类上,注册为一个bean,bean名称默认为类名称(首字母小写),也可以手动指定@Service("abc")@Service(value = "abc")

6、PdPmTestMapper的实现 -- PdPmTestMapperImpl

@Component
public class PdPmTestMapperImpl implements PdPmTestMapper {

    @Override
    public PdPmTest toEntity(PdPmTestDTO dto) {
        PdPmTest pdPmTest = new PdPmTest();
        pdPmTest.setId(dto.getId());
        pdPmTest.setTestString(dto.getTestString());
        pdPmTest.setTestNumber(dto.getTestNumber());
        return pdPmTest;
    }

    @Override
    public PdPmTestDTO toDto(PdPmTest entity) {
        PdPmTestDTO pdPmTestDTO = new PdPmTestDTO();
        pdPmTestDTO.setId(entity.getId());
        pdPmTestDTO.setTestString(entity.getTestString());
        pdPmTestDTO.setTestNumber(entity.getTestNumber());
        //求testNumber与id的平均值
        BigDecimal testNumber = entity.getTestNumber() == null ? BigDecimal.ZERO : entity.getTestNumber();
        pdPmTestDTO.setAverage(testNumber.divide(new BigDecimal(entity.getId()), BigDecimal.ROUND_UP));
        return pdPmTestDTO;
    }

    @Override
    public List<PdPmTest> toEntity(List<PdPmTestDTO> dtoList) {
        List<PdPmTest> pdPmTests = new ArrayList<>();
        dtoList.forEach(pdPmTestDTO -> {
            pdPmTests.add(toEntity(pdPmTestDTO));
        });
        return pdPmTests;
    }

    @Override
    public List<PdPmTestDTO> toDto(List<PdPmTest> entityList) {
        List<PdPmTestDTO> pdPmTests = new ArrayList<>();
        entityList.forEach(pdPmTestDTO -> {
            pdPmTests.add(toDto(pdPmTestDTO));
        });
        return pdPmTests;
    }
}
  • @Component 实现bean的注入

7、定义业务层--PdPmTestService

@Service
public interface PdPmTestService {

    /**
     * 查询所有信息根据查询条件
     *
     * @param criteria 查询条件
     * @return 查询结果
     * @author yantt21019
     * @date 2020/8/17
     */
    List<PdPmTestDTO> queryAllByCriteria(PdPmTestQueryCriteria criteria);
    
}

8、业务层实现定义--PdPmTestServiceImpl

@Component
public class PdPmTestServiceImpl implements PdPmTestService {

    PdPmTestRepository  pdPmTestRepository;

    @Autowired
    PdPmTestMapper pdPmTestMapper;

    @Autowired
    EntityManager entityManager;

    PdPmTestServiceImpl(PdPmTestRepository pdPmTestRepository){
        this.pdPmTestRepository = pdPmTestRepository;
    }

    @Override
    public List<PdPmTestDTO> queryAllByCriteria(PdPmTestQueryCriteria criteria) {
       Specification<PdPmTest> specification = new Specification<PdPmTest>() {
                private static final long serialVersionUID = 1L;
                @Override
            public Predicate toPredicate(Root<PdPmTest> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                return QueryHelp.getPredicate(root,criteria,criteriaBuilder);
            }
        };
        List<PdPmTest> testRepositoryAll = pdPmTestRepository.findAll(specification);
        return pdPmTestMapper.toDto(testRepositoryAll);
    }
  • @Autowired注解,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作;
  • Specification为Spring Data Jpa的规范对象,如果用规范封装查询就必须创建Specification对象并对查询条件;

9、定义控制层--PdPmTestController

@Controller
@RequestMapping("/api/PdPmTest")
public class PdPmTestController {

    @Autowired
    PdPmTestService pdPmTestService;
    
    @GetMapping(value = "/getTests")
    @Log("查询测试信息")
    @PreAuthorize("@el.check('test:list')")
    public ResponseEntity getTests(PdPmTestQueryCriteria criteria){

        return new ResponseEntity(pdPmTestService.queryAllByCriteria(criteria), HttpStatus.OK);
    }

}
  • @Controller用于标记在一个类上,使用它标记的类就是一个SpringMvc Controller对象,分发处理器会扫描使用该注解的类的方法,并检测该方法是否使用了@RequestMapping注解;
  • @RequestMapping来映射请求,也就是通过它来指定控制器可以处理哪些URL请求;
  • @GetMapping获取get请求,value值为指定请求的实际地址;
  • ResponseEntity为处理Http响应的返回值,ResponseEntity(@Nullable T body, HttpStatus status),第一个参数为,请求实体内容,第二个为请求返回状态;

前端代码

新增测试文件目录

在src/views/下新建test目录

image-20200818095652774

新增VUE文件

image-20200818095525001

新增testindex.vue文件

image-20200818100214539
  • template 标签为模板标签,里面存放组件元素;
  • script 标签为用于定义客户端脚本,比如 JavaScript;
  • style标签用于定义样式信息;

添加testindex到固定路由展示

,
{
  path: '/test',
  component: Layout,
  hidden: false,
  redirect: 'noredirect',
  children: [
    {
      path: 'testindex',
      component: () => import('@/views/test/testindex'),
      name: '测试界面',
      meta: { title: '测试界面', icon: 'user' }
    }
  ]
}
image-20200818101545697

启动前后台看一下测试界面

image-20200818102225291

现在测试界面还是空空如也

增加表格展示从数据库查询到的数据

1、打开Element-Ui组件库网址

Element-Ui组件库网址

image-20200818102514937
2、找到表格组件
image-20200818102546164
3、选取需要的样式点击显示代码,并复制到对应的地方
image-20200818102641953 image-20200818102818900

看一下测试界面

image-20200818102850957

测试界面已经多了一个表格

但是我们发现这个表格的数据都是固定,字段也不符合要求。

4、修改表格组件显示字段
  <el-table
    :data="tableData"
    style=" 100%">
    <el-table-column
      prop="id"
      label="测试id"
      width="180">
    </el-table-column>
    <el-table-column
      prop="testString"
      label="测试String"
      width="180">
    </el-table-column>
    <el-table-column
      prop="testNumber"
      label="测试数字">
    </el-table-column>
    <el-table-column
      prop="average"
      label="平均值">
    </el-table-column>
  </el-table>

由于此时数据需要从数据库查询则需要新增api接口

5、新增api文件和查询接口

在src/api/目录下

image-20200818104727225

新建test.js文件,并新增查询请求

image-20200818104836860
import request from '@/utils/request'

export function getAllTests(params) {
  return request({
    url: '/api/PdPmTest/getTests',
    method: 'get',
    params
  })
}
6、在testindex文件中导入查询接口,并在创建界面时查询一次数据库
image-20200818105224501
import { getAllTests } from '@/api/test'

引入创建钩子函数,这个created会在界面创建时调用一次

image-20200818105500558

调用查询函数并且赋值给tableData

image-20200818110927369
created() {
  getAllTests().then(res=>{
     this.tableData = res.content;
  }).catch(e=>{
    console.log(e)
  })

}
7、插入数据库数据
INSERT INTO `pdpmtest` VALUES (1, '测试1', 123456.78);
INSERT INTO `pdpmtest` VALUES (2, '测试2', 66666.66);

例子进阶需求

实现以下需求:

  1. 测试界面增加查询输入框、查询和重置按钮;
  2. 增加新增、修改和删除按钮;
  3. 实现新增、修改和删除功能;

参考:

前端参考:
--testindex可以参考
  src/views/business/product/index.vue
-- 新增的form可以参考
  src/views/business/product/form.vue
-- api参考
  src/api/product.js
后端参考:
  -- service接口参考
  hundsun/pdpm/modules/system/service/ProductService.java
  --service实现参考
  hundsun/pdpm/modules/system/service/impl/ProductServiceImpl.java
  -- controller接口参考
  hundsun/pdpm/modules/system/rest/ProductController.java

如何看错误信息

看如下错误信息:

pdpm- 2020-08-18 15:26:36 [http-nio-8000-exec-8] ERROR h.p.e.handler.GlobalExceptionHandler - java.lang.NullPointerException
	at hundsun.pdpm.service.impl.PdPmTestMapperImpl.toDto(PdPmTestMapperImpl.java:48)
	at hundsun.pdpm.service.impl.PdPmTestServiceImpl.queryAllByCriteria(PdPmTestServiceImpl.java:65)
	at hundsun.pdpm.rest.PdPmTestController.getTests(PdPmTestController.java:32)
	at hundsun.pdpm.rest.PdPmTestController$$FastClassBySpringCGLIB$$577cefa1.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
	at hundsun.pdpm.aspect.LogAspect.logAround(LogAspect.java:54)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
	at hundsun.pdpm.rest.PdPmTestController$$EnhancerBySpringCGLIB$$997650fb.getTests(<generated>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:215)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:142)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:998)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:890)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:875)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:123)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.jasig.cas.client.util.AssertionThreadLocalFilter.doFilter(AssertionThreadLocalFilter.java:50)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.jasig.cas.client.util.HttpServletRequestWrapperFilter.doFilter(HttpServletRequestWrapperFilter.java:70)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:320)
	at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127)
	at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:119)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:170)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at hundsun.pdpm.modules.security.security.JwtAuthorizationTokenFilter.doFilterInternal(JwtAuthorizationTokenFilter.java:60)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:66)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:770)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)

1、看错误信息首先看第一行,一般第一行会告诉我们确切的错误原因

java.lang.NullPointerException

这个错误原因显而易见是空指针

2、接着去在这大长串的错误栈信息里找属于我们自己包的第一条!

at hundsun.pdpm.service.impl.PdPmTestMapperImpl.toDto(PdPmTestMapperImpl.java:48)

这个是说明在PdPmTestMapperImpl文件中toDto函数,48代表在文件PdPmTestMapperImpl.java48行;

一般第一个出现我们自己包的错误是改错误发生的位置,但不一定是错误真的原因位置我们需要打断点去自己排查

image-20200818153309171

因为此入参是传过来的,所以接着看错误信息第二行:

at hundsun.pdpm.service.impl.PdPmTestServiceImpl.queryAllByCriteria(PdPmTestServiceImpl.java:65)

说明在PdPmTestServiceImpl.java的第65行调用了刚刚的函数;

image-20200818153405813

可以看到代码因为我想故意使他报错,所以真正的原因是此变量被强行赋值为null了;

基础知识

常用框架

Spring

什么是Spring?

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。

Spring 官网列出的 Spring 的 6 个特征:

  • 核心技术 :依赖注入(DI),AOP,事件(events),资源,i18n,验证,数据绑定,类型转换,SpEL。
  • 测试 :模拟对象,TestContext框架,Spring MVC 测试,WebTestClient。
  • 数据访问 :事务,DAO支持,JDBC,ORM,编组XML。
  • Web支持 : Spring MVC和Spring WebFlux Web框架。
  • 集成 :远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
  • 语言 :Kotlin,Groovy,动态语言。
Spring有哪些模块?
image-20200818154411696
  • Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IoC 依赖注入功能。
  • Spring Aspects : 该模块为与AspectJ的集成提供支持。
  • Spring AOP :提供了面向切面的编程实现。
  • Spring JDBC : Java数据库连接。
  • Spring JMS :Java消息服务。
  • Spring ORM : 用于支持Hibernate等ORM工具。
  • Spring Web : 为创建Web应用程序提供支持。
  • Spring Test : 提供了对 JUnit 和 TestNG 测试的支持
IoC

IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语言中也有应用,并非 Spring 特有。 IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

Spring 时代之前一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。

推荐阅读:https://www.zhihu.com/question/23277575/answer/169698662

IoC源码阅读

AOP

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。

使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。

Mybatis

#{}和${}的区别是什么?
  • ${}是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于静态文本替换,比如${driver}会被静态替换为com.mysql.jdbc.Driver
  • #{}是 sql 的参数占位符,Mybatis 会将 sql 中的#{}替换为?号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的?号占位符设置参数值,比如 ps.setInt(0, parameterValue),#{item.name} 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 param.getItem().getName()
Xml 映射文件中,除了常见的 select|insert|updae|delete 标签之外,还有哪些标签?

<resultMap><parameterMap><sql><include><selectKey>,加上动态 sql 的 9 个标签,trim|where|set|foreach|if|choose|when|otherwise|bind等,其中为 sql 片段标签,通过<include>标签引入 sql 片段,<selectKey>为不支持自增的主键生成策略标签。

Java基础

面向对象和面向过程的区别

  • 面向过程面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。

  • 面向对象面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低

Java 和 C++的区别?

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理机制,不需要程序员手动释放无用内存
  • 在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘’来表示结束。但是,Java 语言中没有结束符这一概念。

== 与 equals(重要)

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

举个例子:

public class test1 {
    public static void main(String[] args) {
        String a = new String("ab"); // a 为一个引用
        String b = new String("ab"); // b为另一个引用,对象的内容一样
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 从常量池中查找
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false,非同一对象
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true
            System.out.println("true");
        }
    }
}

说明:

  • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

Java集合

List,Set,Map三者的区别?

  • List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
  • Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。
  • Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

常用的Map集合

graph LR A[Map集合]-->B(HashMap) A-->C(HashTable) A-->D(ConcurrentHashMap) B-->E(线程非安全) C-->F(线程安全) D-->F

如何选用集合?

主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。

学习路线

image-20200818200850037

原文地址:https://www.cnblogs.com/yantt/p/yan-fa-guo-cheng-guan-li-xi-tong-pei-xun-wen-dang.html