大家好,我是文sir !
话说咱们Java程序员呀,在互联网公司中一般要做的事情就是进项目组,然后分配需求,完成任务,
但是呀! 其实除了完成项目的需求以外还有一些其他的功能,像:
- 权限管理 呀
- 统一返回结果集
- 统一异常处理
- 对于前端传过来的参数建立相应do
- 对于返回给前端的结果建立相应的vo
- 中间件的使用,【redis做缓存,做锁】【消息中间件】
- 跨域问题呀
- 建立线程池来异步处理请求呀
- 自定义拦截器,然后注册拦截器呀
- mybatis的分页呀
- token令牌呀
- 图片上传呀
- 文件上传下载导入导出呀
- 等等等等
这些都是要我们了解,并且掌握的,不要什么都去问别人,毕竟不是谁都是你的妈┗( ▔, ▔ )┛。
首先呢,掌握上面这些扩展的东西,你得明白明白再明白那一套经典的流程
咱今儿个来梳理梳理
经典的流程:
controller - - service - - dao - -[xml-SQL] - - entity
这是一套经典的流程
控制层调服务层
服务层调【数据库映射xml】层
数据库映射xml】层调xml层
即可得到数据库返回的结果
然后封装成entity返回给调用方,最终返回前端。
紧接着,听好了!!
你得了解各层里的详细知识【例如:注解啊,继承基类啊等等】
- 从controller层来开始分析
来一个例子【代码中有详细解释】
package com.javadao.blogapi.controller;//此类所在的包名【全限定包名】
import com.javadao.blogapi.common.aop.LogAnnotation;//引入的类
@RestController //自动返回字符串给前端的注解,【@Controller @ResponseBody】这个两个注解的组合体
@RequestMapping("articles") //http://localhost:8080/archives/ (请求地址)
public class ArticleController {
@Autowired //自动注入,等于是new了下面的这个类
private ArticleService articleService;
------------------------------------------------------------------
@PostMapping
@LogAnnotation(module="文章",operator="获取文章列表") //自定义切面注解
@Cache(expire = 5 * 60 * 1000,name = "listArticle") //自定义缓存注解
public Result listArticle(@RequestBody PageParams pageParams){ //@RequestBody:接收传来的参数,自动封装到形参实体内; Result :统一结果返回集
return articleService.listArticle(pageParams); //调用服务层的方法
}
---------------------------------------------------------------------
@PostMapping("view/{id}")//请求地址 http://localhost:8080/view/1442043508905000962
public Result findArticleById(@PathVariable("id") Long articleId){ //@PathVariable("id"):接收一个请求url中的参数
return articleService.findArticleById(articleId);
}
----------------------------------------------------------------------
@GetMapping("currentUser")
public Result currentUser(@RequestHeader("Authorization") String token){
//@RequestHeader("Authorization") :表示从请求头里获取Authorization的信息,并赋给token
return sysUserService.findUserByToken(token);
}
}
还有常用的注解,详情见:
Controller常用注解功能全解析
- service层的使用
一般服务层就是写一些数据加工,服务调用,异常处理等等之类的;
一般都有一个接口服务层,和一个接口实现服务层;
常见的service层代码模板如下:
- 接口服务层 xxxService(无实现,控制层自动注入这个层)
//这里才是精华;xxxService extends IService<此实体类>(需要引入mybatisplus才有)
public interface ArticleService extends IService<ArticleEntity> {
Result listArticle(PageParams pageParams);
Result hotArticle(int limit);
Result newArticles(int limit);
Result listArchives();
Result findArticleById(Long articleId);
Result publish(ArticleParam articleParam);
}
- 接口实现服务层 xxxServiceImpl
package com.javadao.blogapi.service.impl;
//这个服务接口实现层是最重要的,需要引用很多的包、类,来完成需求
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.javadao.blogapi.dao.ArticleBodyDao;
import com.javadao.blogapi.dao.dos.Archives;
import com.javadao.blogapi.entity.ArticleBodyEntity;
import com.javadao.blogapi.service.*;
import com.javadao.blogapi.utils.UserThreadLocal;
import com.javadao.blogapi.vo.ArticleBodyVo;
import com.javadao.blogapi.vo.params.ArticleParam;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
.......
@Service("articleService") //注册到容器中,代表它是服务层
//这里又是精华了,xxxServiceImpl extends ServiceImpl<xxxDao, xxxEntity> implements xxxService [基本是这一套了,跑不了]
public class ArticleServiceImpl extends ServiceImpl<ArticleDao, ArticleEntity> implements ArticleService {
@Autowired
private TagService tagService;
@Override
public Result listArticle(PageParams pageParams) {
//具体业务需求....
//baseMapper : 它是从dao层继承能过来的,它特别有用,可以自动完成一些简单的crud,相当于是
/*
@Autowired
private ArticleDao baseMapper;
*/
IPage<ArticleEntity> articleIPage = baseMapper.listArticle();
List<ArticleEntity> records = articleIPage.getRecords();
return Result.success(copyList(records,true,true));//得到最后前端想要的结果,统一返回结果集给前端,code码,message
}
-------------------------------------------------------------
@Override
public Result hotArticle(int limit) {
//这是查询条件构造器:LambdaQueryWrapper
LambdaQueryWrapper<ArticleEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(ArticleEntity::getViewCounts);
queryWrapper.select(ArticleEntity::getId,ArticleEntity::getTitle);
queryWrapper.last("limit "+limit);
//select id,title from article order by view_counts desc limit 5
List<ArticleEntity> articles = baseMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false));
}
-------------------------------------------------------------
//很精华的一个业务操作,能看懂就是最好了**
@Override
public Result findArticleById(Long articleId) {
/**
* 1. 根据id查询 文章信息
* 2. 根据bodyId和categoryid 去做关联查询
*/
ArticleEntity article = this.baseMapper.selectById(articleId);
ArticleVo articleVo = copy(article, true, true,true,true);
//查看完文章了,新增阅读数,有没有问题呢?
//查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低
// 更新 增加了此次接口的 耗时 如果一旦更新出问题,不能影响 查看文章的操作
//线程池 可以把更新操作 扔到线程池中去执行,和主线程就不相关了
threadService.updateArticleViewCount(articleMapper,article);
return Result.success(articleVo);
}
-------------------------------------------------------------
@Override
public Result publish(ArticleParam articleParam) {
//此接口 要加入到登录拦截当中
SysUserEntity sysUser = UserThreadLocal.get();
/**
* 1. 发布文章 目的 构建Article对象
* 2. 作者id 当前的登录用户
* 3. 标签 要将标签加入到 关联列表当中
* 4. body 内容存储 article bodyId
*/
ArticleEntity article = new ArticleEntity();
article.setAuthorId(sysUser.getId());
article.setWeight(ArticleEntity.Article_Common);
article.setViewCounts(0);
article.setTitle(articleParam.getTitle());
article.setSummary(articleParam.getSummary());
article.setCommentCounts(0);
article.setCreateDate(System.currentTimeMillis());
article.setCategoryId(Integer.parseInt(articleParam.getCategory().getId()));
//插入之后 会生成一个文章id
this.articleMapper.insert(article);
//tag
List<TagVo> tags = articleParam.getTags();
if (tags != null){
for (TagVo tag : tags) {
Long articleId = article.getId();
ArticleTagEntity articleTag = new ArticleTagEntity();
articleTag.setTagId(Long.parseLong(tag.getId()));
articleTag.setArticleId(articleId);
articleTagMapper.insert(articleTag);
}
}
//body
ArticleBodyEntity articleBody = new ArticleBodyEntity();
articleBody.setArticleId(article.getId());
articleBody.setContent(articleParam.getBody().getContent());
articleBody.setContentHtml(articleParam.getBody().getContentHtml());
articleBodyMapper.insert(articleBody);
article.setBodyId(articleBody.getId());
articleMapper.updateById(article);
Map<String,String> map = new HashMap<>();
map.put("id",article.getId().toString());
return Result.success(map);
}
private List<ArticleVo> copyList(List<ArticleEntity> records, boolean isTag, boolean isAuthor) {
List<ArticleVo> articleVoList = new ArrayList<>();
for (ArticleEntity record : records) {
articleVoList.add(copy(record,isTag,isAuthor,false,false));
}
return articleVoList;
}
private List<ArticleVo> copyList(List<ArticleEntity> records, boolean isTag, boolean isAuthor, boolean isBody,boolean isCategory) {
List<ArticleVo> articleVoList = new ArrayList<>();
for (ArticleEntity record : records) {
articleVoList.add(copy(record,isTag,isAuthor,isBody,isCategory));
}
return articleVoList;
}
private ArticleVo copy(ArticleEntity article, boolean isTag, boolean isAuthor, boolean isBody,boolean isCategory){
ArticleVo articleVo = new ArticleVo();
articleVo.setId(String.valueOf(article.getId()));
BeanUtils.copyProperties(article,articleVo);
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
//并不是所有的接口 都需要标签 ,作者信息
if (isTag){
Long articleId = article.getId();
articleVo.setTags(tagService.findTagsByArticleId(articleId));
}
if (isAuthor){
Long authorId = article.getAuthorId();
articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
}
if (isBody){
Long bodyId = article.getBodyId();
articleVo.setBody(findArticleBodyById(bodyId));
}
if (isCategory){
Integer categoryId = article.getCategoryId();
articleVo.setCategory(categoryService.findCategoryById(categoryId));
}
return articleVo;
}
private ArticleBodyVo findArticleBodyById(Long bodyId) {
ArticleBodyEntity articleBody = articleBodyMapper.selectById(bodyId);
ArticleBodyVo articleBodyVo = new ArticleBodyVo();
articleBodyVo.setContent(articleBody.getContent());
return articleBodyVo;
}
}
是在实现需求中最重要的一环!!
- dao层的使用
@Mapper //这个注解是必须的
//精华,经典写法:xxxDao extends BaseMapper<xxxEntity>
public interface ArticleDao extends BaseMapper<ArticleEntity> {
//需求业务... 映射到xml 执行sql操作数据库
List<Archives> listArchives();
//...
}
- 【数据库映射xml】层
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javadao.blogapi.dao.ArticleDao"> <!--找到与dao层对应的类名进行映射-->
<!-- 可根据自己的需求,是否要使用 映射实体 ,起名为articleMap,在下面就可以引用 -->
<resultMap type="com.javadao.blogapi.entity.ArticleEntity" id="articleMap">
<result property="id" column="id"/>
<result property="commentCounts" column="comment_counts"/>
<result property="createDate" column="create_date"/>
<result property="summary" column="summary"/>
<result property="title" column="title"/>
<result property="viewCounts" column="view_counts"/>
<result property="weight" column="weight"/>
<result property="authorId" column="author_id"/>
<result property="bodyId" column="body_id"/>
<result property="categoryId" column="category_id"/>
</resultMap>
<select id="listArticle" resultMap="articleMap">
select * from ms_article
<where>
1 = 1
<if test="categoryId != null">
and category_id=#{categoryId}
</if>
<if test="tagId != null">
and id in (select article_id from ms_article_tag where tag_id=#{tagId})
</if>
<if test="year != null and year.length>0 and month != null and month.length>0">
and (FROM_UNIXTIME(create_date/1000,'%Y') =#{year} and FROM_UNIXTIME(create_date/1000,'%m')=#{month})
</if>
</where>
order by weight,create_date desc
</select>
<select id="listArchives" resultType="com.javadao.blogapi.dao.dos.Archives">
select FROM_UNIXTIME(create_date/1000,'%Y') as year,FROM_UNIXTIME(create_date/1000,'%m') as month,count(*) as count from ms_article group by year,month
</select>
</mapper>
这个基本的一套流程算是整完了
接下来就是一些更加需要掌握的了
1.统一返回结果集
其实就是一个工具类了啦
package com.javadao.blogapi.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 统一返回给前端信息集
* 一般都有一个成功与否,code,msg信息反馈,还有数据返回这些基本的信息封装
*/
@Data
@AllArgsConstructor
public class Result {
private boolean success;
private int code;
private String msg;
private Object data;
public static Result success(Object data){
return new Result(true,200,"success",data);
}
public static Result fail(int code, String msg){
return new Result(false,code,msg,null);
}
}
2.统一异常处理
//对加了@Controller注解的方法进行拦截处理 AOP的实现
@ControllerAdvice
public class AllExceptionHandler {
//进行异常处理,处理Exception.class的异常
@ExceptionHandler(Exception.class)
@ResponseBody //返回json数据
public Result doException(Exception ex){
ex.printStackTrace();
return Result.fail(-999,"系统异常"); //错误码
}
}
同时涉及到了一个错误码的概念
public enum ErrorCode {
PARAMS_ERROR(10001,"参数有误"),
ACCOUNT_PWD_NOT_EXIST(10002,"用户名或密码不存在"),
TOKEN_ERROR(10003,"token不合法"),
ACCOUNT_EXIST(10004,"账号已存在"),
NO_PERMISSION(70001,"无访问权限"),
SESSION_TIME_OUT(90001,"会话超时"),
NO_LOGIN(90002,"未登录"),;
private int code;
private String msg;
ErrorCode(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
3.do,vo等等数据操作实体
做这些其实都是为了更加清晰,更加方便
前端可能不一定需要全部的字段显示,或者,前端传不过来所有的参数,这时候就与数据库实体有差距了
所以就应该这样做:
前端传过来什么 我们就建相应的do接收住
前端需要我们传什么过去,我们就建相应的vo传过去
4.跨域问题cors
其实跨域问题就是因为前端与后端交互,地址不同导致不能通行,放行就好了
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
}
5.建立线程池来异步处理请求
为什么要这么做呢,因为异步好啊,不用等啊,能给客户良好体验啊!
上代码:
@Configuration
@EnableAsync //******开启多线程******
public class ThreadPoolConfig {
@Bean("taskExecutor") //多线程名字,设置一些参数
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(20);
//配置队列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("赵志文的博客");
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor;
}
}
----------------------------------------------------------
调用 threadService.updateArticleViewCount(articleMapper,article);
------------------------------------------------------------
@Component
public class ThreadService {
//期望此操作在线程池 执行 不会影响原有的主线程
@Async("taskExecutor")
public void updateArticleViewCount(ArticleDao articleMapper, ArticleEntity article) {
业务...
这个方法的调用会异步执行
}
}
6.自定义拦截器,然后注册拦截器
实现HandlerIntercepter即可
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
业务...(方法执行前)
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不删除 ThreadLocal中用完的信息 会有内存泄漏的风险
UserThreadLocal.remove();
}
}
注册进mvcconfig中
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//都被登录拦截器 拦截住, 发布文章和对文章评论功能
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/comments/create/change")
.addPathPatterns("/articles/publish");
}
}
7.token令牌
流程是这样子滴朋友们
首先,token令牌常用于在登录成功之后得到一个由用户ID创建的JWT字符串令牌,
成功后返回给前端这个令牌,前端来访问后端时,在头header信息里就会携带这个,
然后后还需要在redis里面存一份,设置一个过期时间
看JWT工具类:
public class JWTUtils {
private static final String jwtToken = "123456Mszlu!@#$$";
public static String createToken(Long userId){
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
String token = jwtBuilder.compact();
return token;
}
public static Map<String, Object> checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
8.序列化与反序列化
把对象序列化成字符串 存入redis: JSON.toJSONString(sysUser)
把字符串反序列化成对象返回前端: JSON.parseObject()
9.redis做缓存
思路:加一个切面类,环绕通知,定义一个注解,加了这个注解的就触发切面,将访问到的数据存入redis中,并设置过期时间,第一次访问数据库;
@Cache(expire = 5 * 60 * 1000,name = "listArticle")
public Result listArticle(@RequestBody PageParams pageParams){
return articleService.listArticle(pageParams);
}
-------------------------------
//自定义缓存注解
package com.javadao.blogapi.common.cache;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
long expire() default 1 * 60 * 1000;
//缓存标识 key
String name() default "";
}
-------------------------------
//自定义缓存切面类
//aop 定义一个切面,切面定义了切点和通知的关系
@Aspect
@Component
@Slf4j
public class CacheAspect {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Pointcut("@annotation(com.javadao.blogapi.common.cache.Cache)")//这个注解使用的地点就是切点
public void pt(){}
@Around("pt()")//对使用了这个注解的方法进行环绕通知
public Object around(ProceedingJoinPoint pjp){
try {
Signature signature = pjp.getSignature();
//类名
String className = pjp.getTarget().getClass().getSimpleName();
//调用的方法名
String methodName = signature.getName();
Class[] parameterTypes = new Class[pjp.getArgs().length];
Object[] args = pjp.getArgs();
//参数
String params = "";
for(int i=0; i<args.length; i++) {
if(args[i] != null) {
params += JSON.toJSONString(args[i]);
parameterTypes[i] = args[i].getClass();
}else {
parameterTypes[i] = null;
}
}
if (StringUtils.isNotEmpty(params)) {
//加密 以防出现key过长以及字符转义获取不到的情况
params = DigestUtils.md5Hex(params);
}
Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
//获取Cache注解
Cache annotation = method.getAnnotation(Cache.class);
//缓存过期时间
long expire = annotation.expire();
//缓存名称
String name = annotation.name();
//先从redis获取
String redisKey = name + "::" + className+"::"+methodName+"::"+params;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(redisValue)){
log.info("走了缓存~~~,{},{}",className,methodName);
Result result = JSON.parseObject(redisValue, Result.class);
return result;
}
Object proceed = pjp.proceed();
redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
log.info("存入缓存~~~ {},{}",className,methodName);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return Result.fail(-999,"系统错误");
}
}
10.redis做锁
redisson 加锁
11.权限管理
实现UserDetailsService 完成从数据库查到账号密码
然后认证,授权
12.图片上传
就是写一个工具类,秘钥什么的填上就好了
13.文件上传下载导入导出
进口:导入一个文件涉及到MultipartFile,应用工具类变成流,转换成实体对象,存入数据库,更新即可;
出口:点一个按钮而后得到你要导出的数据,发给后端,应用工具类变成流,应用工具类创建excel工作蒲工作表(如果要导出为excel的话(狗头)),把数据写进去,然后下载即可得到导出内容。