❤❤创造出一个Java企业级项目的“旅程“(经验之谈)收藏!!!❤❤

大家好,我是文sir !

话说咱们Java程序员呀,在互联网公司中一般要做的事情就是进项目组,然后分配需求,完成任务

但是呀! 其实除了完成项目的需求以外还有一些其他的功能,像:

  • 权限管理 呀
  • 统一返回结果集
  • 统一异常处理
  • 对于前端传过来的参数建立相应do
  • 对于返回给前端的结果建立相应的vo
  • 中间件的使用,【redis做缓存,做锁】【消息中间件】
  • 跨域问题呀
  • 建立线程池来异步处理请求呀
  • 自定义拦截器,然后注册拦截器呀
  • mybatis的分页呀
  • token令牌呀
  • 图片上传呀
  • 文件上传下载导入导出呀
  • 等等等等

这些都是要我们了解,并且掌握的,不要什么都去问别人,毕竟不是谁都是你的妈┗( ▔, ▔ )┛。
在这里插入图片描述

首先呢,掌握上面这些扩展的东西,你得明白明白再明白那一套经典的流程

咱今儿个来梳理梳理

经典的流程:

controller - - service - - dao - -[xml-SQL] - - entity
这是一套经典的流程
控制层调服务层
服务层调【数据库映射xml】层
数据库映射xml】层调xml层
即可得到数据库返回的结果
然后封装成entity返回给调用方,最终返回前端。

紧接着,听好了!!
你得了解各层里的详细知识【例如:注解啊,继承基类啊等等】

  1. 从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常用注解功能全解析

  1. 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;
    }

}

是在实现需求中最重要的一环!!

  1. dao层的使用
@Mapper   //这个注解是必须的
//精华,经典写法:xxxDao extends BaseMapper<xxxEntity> 
public interface ArticleDao extends BaseMapper<ArticleEntity> {
    //需求业务...   映射到xml 执行sql操作数据库

    List<Archives> listArchives();
    //...

}

  1. 【数据库映射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的话(狗头)),把数据写进去,然后下载即可得到导出内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值